diff --git a/packages/conf/DittoConf.ts b/packages/conf/DittoConf.ts index f775a861..3eb0f5e5 100644 --- a/packages/conf/DittoConf.ts +++ b/packages/conf/DittoConf.ts @@ -480,4 +480,14 @@ export class DittoConf { get precheck(): boolean { return optionalBooleanSchema.parse(this.env.get('DITTO_PRECHECK')) ?? true; } + + /** Path to `ffmpeg` executable. */ + get ffmpegPath(): string { + return this.env.get('FFMPEG_PATH') || 'ffmpeg'; + } + + /** Path to `ffprobe` executable. */ + get ffprobePath(): string { + return this.env.get('FFPROBE_PATH') || 'ffprobe'; + } } diff --git a/packages/ditto/utils/upload.ts b/packages/ditto/utils/upload.ts index 02858e65..234556fa 100644 --- a/packages/ditto/utils/upload.ts +++ b/packages/ditto/utils/upload.ts @@ -27,6 +27,7 @@ export async function uploadFile( perf.mark('start'); const { conf, uploader } = c.var; + const { ffmpegPath, ffprobePath } = conf; if (!uploader) { throw new HTTPException(500, { @@ -43,7 +44,7 @@ export async function uploadFile( const [baseType] = file.type.split('/'); perf.mark('probe-start'); - const probe = await analyzeFile(file.stream()).catch(() => null); + const probe = await analyzeFile(file.stream(), { ffprobePath }).catch(() => null); perf.mark('probe-end'); perf.mark('transcode-start'); @@ -62,7 +63,7 @@ export async function uploadFile( } if (needsTranscode) { - const stream = transcodeVideo(file.stream()); + const stream = transcodeVideo(file.stream(), { ffmpegPath }); const transcoded = await new Response(stream).bytes(); file = new File([transcoded], file.name, { type: 'video/mp4' }); } @@ -103,7 +104,7 @@ export async function uploadFile( } if (baseType === 'video' && (!image || !thumb)) { - const bytes = await extractVideoFrame(file.stream()); + const bytes = await extractVideoFrame(file.stream(), '00:00:01', { ffmpegPath }); const [[, url]] = await uploader.upload(new File([bytes], 'thumb.jpg', { type: 'image/jpeg' }), { signal }); if (!image) { diff --git a/packages/transcode/analyze.ts b/packages/transcode/analyze.ts index 03f4aaff..1b137428 100644 --- a/packages/transcode/analyze.ts +++ b/packages/transcode/analyze.ts @@ -87,13 +87,16 @@ interface Disposition { still_image: number; } -export function analyzeFile(input: URL | ReadableStream): Promise { +export function analyzeFile( + input: URL | ReadableStream, + opts?: { ffprobePath?: string | URL }, +): Promise { const stream = ffprobe(input, { 'v': 'error', 'show_streams': '', 'show_format': '', 'of': 'json', - }); + }, opts); return new Response(stream).json(); } diff --git a/packages/transcode/ffmpeg.ts b/packages/transcode/ffmpeg.ts index 4a53f760..f2ebd5a5 100644 --- a/packages/transcode/ffmpeg.ts +++ b/packages/transcode/ffmpeg.ts @@ -12,7 +12,13 @@ export interface FFmpegFlags { [key: string]: string | undefined; } -export function ffmpeg(input: URL | ReadableStream, flags: FFmpegFlags): ReadableStream { +export function ffmpeg( + input: URL | ReadableStream, + flags: FFmpegFlags, + opts?: { ffmpegPath?: string | URL }, +): ReadableStream { + const { ffmpegPath = 'ffmpeg' } = opts ?? {}; + const args = ['-i', input instanceof URL ? input.href : 'pipe:0']; for (const [key, value] of Object.entries(flags)) { @@ -28,7 +34,7 @@ export function ffmpeg(input: URL | ReadableStream, flags: FFmpegFla args.push('pipe:1'); // Output to stdout // Spawn the FFmpeg process - const command = new Deno.Command('ffmpeg', { + const command = new Deno.Command(ffmpegPath, { args, stdin: input instanceof ReadableStream ? 'piped' : 'null', stdout: 'piped', diff --git a/packages/transcode/ffprobe.ts b/packages/transcode/ffprobe.ts index 3f5fe16f..7605cbe5 100644 --- a/packages/transcode/ffprobe.ts +++ b/packages/transcode/ffprobe.ts @@ -6,7 +6,13 @@ export interface FFprobeFlags { [key: string]: string | undefined; } -export function ffprobe(input: URL | ReadableStream, flags: FFprobeFlags): ReadableStream { +export function ffprobe( + input: URL | ReadableStream, + flags: FFprobeFlags, + opts?: { ffprobePath?: string | URL }, +): ReadableStream { + const { ffprobePath = 'ffprobe' } = opts ?? {}; + const args = []; for (const [key, value] of Object.entries(flags)) { @@ -26,7 +32,7 @@ export function ffprobe(input: URL | ReadableStream, flags: FFprobeF } // Spawn the FFprobe process - const command = new Deno.Command('ffprobe', { + const command = new Deno.Command(ffprobePath, { args, stdin: input instanceof ReadableStream ? 'piped' : 'null', stdout: 'piped', diff --git a/packages/transcode/frame.ts b/packages/transcode/frame.ts index 689677ff..d03ea63b 100644 --- a/packages/transcode/frame.ts +++ b/packages/transcode/frame.ts @@ -3,6 +3,7 @@ import { ffmpeg } from './ffmpeg.ts'; export function extractVideoFrame( input: URL | ReadableStream, ss: string = '00:00:01', + opts?: { ffmpegPath?: string | URL }, ): Promise { const output = ffmpeg(input, { 'ss': ss, // Seek to timestamp @@ -10,7 +11,7 @@ export function extractVideoFrame( 'q:v': '2', // High-quality JPEG (lower = better quality) 'f': 'image2', // Force image format 'loglevel': 'fatal', - }); + }, opts); return new Response(output).bytes(); } diff --git a/packages/transcode/transcode.ts b/packages/transcode/transcode.ts index 5e9cbda1..d31cacb1 100644 --- a/packages/transcode/transcode.ts +++ b/packages/transcode/transcode.ts @@ -1,6 +1,9 @@ import { ffmpeg } from './ffmpeg.ts'; -export function transcodeVideo(input: URL | ReadableStream): ReadableStream { +export function transcodeVideo( + input: URL | ReadableStream, + opts?: { ffmpegPath?: string | URL }, +): ReadableStream { return ffmpeg(input, { 'safe': '1', // Safe mode 'nostdin': '', // Disable stdin @@ -12,5 +15,5 @@ export function transcodeVideo(input: URL | ReadableStream): Readabl 'b:a': '128k', // Audio bitrate 'movflags': 'frag_keyframe+empty_moov', // Ensures MP4 streaming compatibility 'f': 'mp4', // Force MP4 format - }); + }, opts); }