Let FFMPEG_PATH and FFPROBE_PATH be configurable

This commit is contained in:
Alex Gleason 2025-03-01 17:01:39 -06:00
parent 8a94be803d
commit 414a3b7651
No known key found for this signature in database
GPG key ID: 7211D1F99744FBB7
7 changed files with 42 additions and 12 deletions

View file

@ -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';
}
}

View file

@ -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) {

View file

@ -87,13 +87,16 @@ interface Disposition {
still_image: number;
}
export function analyzeFile(input: URL | ReadableStream<Uint8Array>): Promise<AnalyzeResult> {
export function analyzeFile(
input: URL | ReadableStream<Uint8Array>,
opts?: { ffprobePath?: string | URL },
): Promise<AnalyzeResult> {
const stream = ffprobe(input, {
'v': 'error',
'show_streams': '',
'show_format': '',
'of': 'json',
});
}, opts);
return new Response(stream).json();
}

View file

@ -12,7 +12,13 @@ export interface FFmpegFlags {
[key: string]: string | undefined;
}
export function ffmpeg(input: URL | ReadableStream<Uint8Array>, flags: FFmpegFlags): ReadableStream<Uint8Array> {
export function ffmpeg(
input: URL | ReadableStream<Uint8Array>,
flags: FFmpegFlags,
opts?: { ffmpegPath?: string | URL },
): ReadableStream<Uint8Array> {
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<Uint8Array>, 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',

View file

@ -6,7 +6,13 @@ export interface FFprobeFlags {
[key: string]: string | undefined;
}
export function ffprobe(input: URL | ReadableStream<Uint8Array>, flags: FFprobeFlags): ReadableStream<Uint8Array> {
export function ffprobe(
input: URL | ReadableStream<Uint8Array>,
flags: FFprobeFlags,
opts?: { ffprobePath?: string | URL },
): ReadableStream<Uint8Array> {
const { ffprobePath = 'ffprobe' } = opts ?? {};
const args = [];
for (const [key, value] of Object.entries(flags)) {
@ -26,7 +32,7 @@ export function ffprobe(input: URL | ReadableStream<Uint8Array>, 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',

View file

@ -3,6 +3,7 @@ import { ffmpeg } from './ffmpeg.ts';
export function extractVideoFrame(
input: URL | ReadableStream<Uint8Array>,
ss: string = '00:00:01',
opts?: { ffmpegPath?: string | URL },
): Promise<Uint8Array> {
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();
}

View file

@ -1,6 +1,9 @@
import { ffmpeg } from './ffmpeg.ts';
export function transcodeVideo(input: URL | ReadableStream<Uint8Array>): ReadableStream<Uint8Array> {
export function transcodeVideo(
input: URL | ReadableStream<Uint8Array>,
opts?: { ffmpegPath?: string | URL },
): ReadableStream<Uint8Array> {
return ffmpeg(input, {
'safe': '1', // Safe mode
'nostdin': '', // Disable stdin
@ -12,5 +15,5 @@ export function transcodeVideo(input: URL | ReadableStream<Uint8Array>): Readabl
'b:a': '128k', // Audio bitrate
'movflags': 'frag_keyframe+empty_moov', // Ensures MP4 streaming compatibility
'f': 'mp4', // Force MP4 format
});
}, opts);
}