From 7cba9c11d43c6f5929b9a2b046a18ddab738e651 Mon Sep 17 00:00:00 2001 From: PrivateGER Date: Thu, 29 May 2025 20:29:42 +0200 Subject: [PATCH 1/7] Add web optimization for video files during processing --- packages/backend/src/core/DriveService.ts | 4 ++ .../src/core/VideoProcessingService.ts | 64 ++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index 82c447baaa..abe9131089 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -159,6 +159,10 @@ export class DriveService { // thunbnail, webpublic を必要なら生成 const alts = await this.generateAlts(path, type, !file.uri); + if (type && type.startsWith('video/')) { + await this.videoProcessingService.webOptimizeVideo(path, type); + } + if (this.meta.useObjectStorage) { //#region ObjectStorage params let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) ?? ['']); diff --git a/packages/backend/src/core/VideoProcessingService.ts b/packages/backend/src/core/VideoProcessingService.ts index 747fe4fc7e..809934e1a9 100644 --- a/packages/backend/src/core/VideoProcessingService.ts +++ b/packages/backend/src/core/VideoProcessingService.ts @@ -9,18 +9,25 @@ import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { ImageProcessingService } from '@/core/ImageProcessingService.js'; import type { IImage } from '@/core/ImageProcessingService.js'; -import { createTempDir } from '@/misc/create-temp.js'; +import {createTemp, createTempDir } from '@/misc/create-temp.js'; import { bindThis } from '@/decorators.js'; import { appendQuery, query } from '@/misc/prelude/url.js'; +import {LoggerService} from "@/core/LoggerService.js"; +import type Logger from "@/logger.js"; @Injectable() export class VideoProcessingService { + private logger: Logger; + constructor( @Inject(DI.config) private config: Config, private imageProcessingService: ImageProcessingService, + + private loggerService: LoggerService, ) { + this.logger = this.loggerService.getLogger("video-processing"); } @bindThis @@ -60,5 +67,60 @@ export class VideoProcessingService { }), ); } + + /** + * Optimize video for web playback by adding faststart flag. + * This allows the video to start playing before it is fully downloaded. + * The original file is modified in-place. + * @param source Path to the video file + * @param mimeType The MIME type of the video + * @returns Promise that resolves when optimization is complete + */ + @bindThis + public async webOptimizeVideo(source: string, mimeType: string): Promise { + // faststart is only supported for MP4, M4A, M4W and MOV files (the MOV family). + // WebM (and Matroska) files always support faststart-like behavior. + const supportedMimeTypes = new Map([ + ['video/mp4', 'mp4'], + ['video/m4a', 'mp4'], + ['video/m4v', 'mp4'], + ['video/quicktime', 'mov'] + ]); + + const outputFormat = supportedMimeTypes.get(mimeType); + if (!outputFormat) { + this.logger.debug(`Skipping web optimization for unsupported MIME type: ${mimeType}`); + return; + } + + const [tempPath, cleanup] = await createTemp(); + + try { + await new Promise((resolve, reject) => { + FFmpeg(source) + .format(outputFormat) // Specify output format + .addOutputOptions('-c copy') // Copy streams without re-encoding + .addOutputOptions('-movflags +faststart') + .on('error', reject) + .on('end', async () => { + try { + // Replace original file with optimized version + const fs = await import('node:fs/promises'); + await fs.copyFile(tempPath, source); + this.logger.info(`Web-optimized video: ${source}`); + resolve(); + } catch (copyError) { + reject(copyError); + } + }) + .save(tempPath); + }); + } catch (error) { + this.logger.warn(`Failed to web-optimize video: ${source}`, { error }); + throw error; + } finally { + cleanup(); + } + } } From 0bf006b7d7e5e36d9edbf35920fad847d1b1d170 Mon Sep 17 00:00:00 2001 From: PrivateGER Date: Thu, 29 May 2025 20:40:15 +0200 Subject: [PATCH 2/7] Fix formatting and import spacing in VideoProcessingService --- packages/backend/src/core/VideoProcessingService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/VideoProcessingService.ts b/packages/backend/src/core/VideoProcessingService.ts index 809934e1a9..73edfbe96c 100644 --- a/packages/backend/src/core/VideoProcessingService.ts +++ b/packages/backend/src/core/VideoProcessingService.ts @@ -9,10 +9,10 @@ import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { ImageProcessingService } from '@/core/ImageProcessingService.js'; import type { IImage } from '@/core/ImageProcessingService.js'; -import {createTemp, createTempDir } from '@/misc/create-temp.js'; +import { createTemp, createTempDir } from '@/misc/create-temp.js'; import { bindThis } from '@/decorators.js'; import { appendQuery, query } from '@/misc/prelude/url.js'; -import {LoggerService} from "@/core/LoggerService.js"; +import { LoggerService } from "@/core/LoggerService.js"; import type Logger from "@/logger.js"; @Injectable() From 4ab3abaa14aca989f8048ad2c02ca94de1e351cb Mon Sep 17 00:00:00 2001 From: PrivateGER Date: Thu, 29 May 2025 21:18:02 +0200 Subject: [PATCH 3/7] Move supported MIME types into file-level const --- .../backend/src/core/VideoProcessingService.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/backend/src/core/VideoProcessingService.ts b/packages/backend/src/core/VideoProcessingService.ts index 73edfbe96c..daafe48601 100644 --- a/packages/backend/src/core/VideoProcessingService.ts +++ b/packages/backend/src/core/VideoProcessingService.ts @@ -15,6 +15,15 @@ import { appendQuery, query } from '@/misc/prelude/url.js'; import { LoggerService } from "@/core/LoggerService.js"; import type Logger from "@/logger.js"; +// faststart is only supported for MP4, M4A, M4W and MOV files (the MOV family). +// WebM (and Matroska) files always support faststart-like behavior. +const supportedMimeTypes = new Map([ + ['video/mp4', 'mp4'], + ['video/m4a', 'mp4'], + ['video/m4v', 'mp4'], + ['video/quicktime', 'mov'], +]); + @Injectable() export class VideoProcessingService { private logger: Logger; @@ -78,15 +87,6 @@ export class VideoProcessingService { */ @bindThis public async webOptimizeVideo(source: string, mimeType: string): Promise { - // faststart is only supported for MP4, M4A, M4W and MOV files (the MOV family). - // WebM (and Matroska) files always support faststart-like behavior. - const supportedMimeTypes = new Map([ - ['video/mp4', 'mp4'], - ['video/m4a', 'mp4'], - ['video/m4v', 'mp4'], - ['video/quicktime', 'mov'] - ]); - const outputFormat = supportedMimeTypes.get(mimeType); if (!outputFormat) { this.logger.debug(`Skipping web optimization for unsupported MIME type: ${mimeType}`); From 7ce8d0de446d3a69592a51d15e41cedd811a1fa5 Mon Sep 17 00:00:00 2001 From: PrivateGER Date: Thu, 29 May 2025 21:20:46 +0200 Subject: [PATCH 4/7] Add error handling for video optimization in DriveService --- packages/backend/src/core/DriveService.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index abe9131089..73125f36d7 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -160,7 +160,11 @@ export class DriveService { const alts = await this.generateAlts(path, type, !file.uri); if (type && type.startsWith('video/')) { - await this.videoProcessingService.webOptimizeVideo(path, type); + try { + await this.videoProcessingService.webOptimizeVideo(path, type); + } catch (err) { + this.registerLogger.warn(`Video optimization failed: ${err instanceof Error ? err.message : String(err)}`, { error: err }); + } } if (this.meta.useObjectStorage) { From 8948369a1bdb2d37f19f694badb5156dbe5d0352 Mon Sep 17 00:00:00 2001 From: PrivateGER Date: Fri, 30 May 2025 18:00:12 +0200 Subject: [PATCH 5/7] import fs dep at file level --- packages/backend/src/core/VideoProcessingService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/VideoProcessingService.ts b/packages/backend/src/core/VideoProcessingService.ts index daafe48601..4fd92486b1 100644 --- a/packages/backend/src/core/VideoProcessingService.ts +++ b/packages/backend/src/core/VideoProcessingService.ts @@ -23,6 +23,7 @@ const supportedMimeTypes = new Map([ ['video/m4v', 'mp4'], ['video/quicktime', 'mov'], ]); +const fs = await import('node:fs/promises'); @Injectable() export class VideoProcessingService { @@ -105,7 +106,6 @@ export class VideoProcessingService { .on('end', async () => { try { // Replace original file with optimized version - const fs = await import('node:fs/promises'); await fs.copyFile(tempPath, source); this.logger.info(`Web-optimized video: ${source}`); resolve(); From 31f4325e86b01fd3fda1c8d69026f3ccba700372 Mon Sep 17 00:00:00 2001 From: PrivateGER Date: Fri, 30 May 2025 18:01:36 +0200 Subject: [PATCH 6/7] make logger property readonly --- packages/backend/src/core/VideoProcessingService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/VideoProcessingService.ts b/packages/backend/src/core/VideoProcessingService.ts index 4fd92486b1..4b6fe14c8c 100644 --- a/packages/backend/src/core/VideoProcessingService.ts +++ b/packages/backend/src/core/VideoProcessingService.ts @@ -27,7 +27,7 @@ const fs = await import('node:fs/promises'); @Injectable() export class VideoProcessingService { - private logger: Logger; + private readonly logger: Logger; constructor( @Inject(DI.config) @@ -37,7 +37,7 @@ export class VideoProcessingService { private loggerService: LoggerService, ) { - this.logger = this.loggerService.getLogger("video-processing"); + this.logger = this.loggerService.getLogger('video-processing'); } @bindThis From f0207211d98d714a77b2240550817371a1ef6c00 Mon Sep 17 00:00:00 2001 From: PrivateGER Date: Fri, 30 May 2025 20:41:51 +0200 Subject: [PATCH 7/7] Import fs module with new syntax --- packages/backend/src/core/VideoProcessingService.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/VideoProcessingService.ts b/packages/backend/src/core/VideoProcessingService.ts index 4b6fe14c8c..3e4fd6a4b0 100644 --- a/packages/backend/src/core/VideoProcessingService.ts +++ b/packages/backend/src/core/VideoProcessingService.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import fs from 'node:fs/promises'; import { Inject, Injectable } from '@nestjs/common'; import FFmpeg from 'fluent-ffmpeg'; import { DI } from '@/di-symbols.js'; @@ -12,8 +13,8 @@ import type { IImage } from '@/core/ImageProcessingService.js'; import { createTemp, createTempDir } from '@/misc/create-temp.js'; import { bindThis } from '@/decorators.js'; import { appendQuery, query } from '@/misc/prelude/url.js'; -import { LoggerService } from "@/core/LoggerService.js"; -import type Logger from "@/logger.js"; +import { LoggerService } from '@/core/LoggerService.js'; +import type Logger from '@/logger.js'; // faststart is only supported for MP4, M4A, M4W and MOV files (the MOV family). // WebM (and Matroska) files always support faststart-like behavior. @@ -23,7 +24,6 @@ const supportedMimeTypes = new Map([ ['video/m4v', 'mp4'], ['video/quicktime', 'mov'], ]); -const fs = await import('node:fs/promises'); @Injectable() export class VideoProcessingService {