mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 03:19:46 +00:00
Let FFMPEG_PATH and FFPROBE_PATH be configurable
This commit is contained in:
parent
8a94be803d
commit
414a3b7651
7 changed files with 42 additions and 12 deletions
|
|
@ -480,4 +480,14 @@ export class DittoConf {
|
||||||
get precheck(): boolean {
|
get precheck(): boolean {
|
||||||
return optionalBooleanSchema.parse(this.env.get('DITTO_PRECHECK')) ?? true;
|
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';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ export async function uploadFile(
|
||||||
perf.mark('start');
|
perf.mark('start');
|
||||||
|
|
||||||
const { conf, uploader } = c.var;
|
const { conf, uploader } = c.var;
|
||||||
|
const { ffmpegPath, ffprobePath } = conf;
|
||||||
|
|
||||||
if (!uploader) {
|
if (!uploader) {
|
||||||
throw new HTTPException(500, {
|
throw new HTTPException(500, {
|
||||||
|
|
@ -43,7 +44,7 @@ export async function uploadFile(
|
||||||
const [baseType] = file.type.split('/');
|
const [baseType] = file.type.split('/');
|
||||||
|
|
||||||
perf.mark('probe-start');
|
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('probe-end');
|
||||||
|
|
||||||
perf.mark('transcode-start');
|
perf.mark('transcode-start');
|
||||||
|
|
@ -62,7 +63,7 @@ export async function uploadFile(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needsTranscode) {
|
if (needsTranscode) {
|
||||||
const stream = transcodeVideo(file.stream());
|
const stream = transcodeVideo(file.stream(), { ffmpegPath });
|
||||||
const transcoded = await new Response(stream).bytes();
|
const transcoded = await new Response(stream).bytes();
|
||||||
file = new File([transcoded], file.name, { type: 'video/mp4' });
|
file = new File([transcoded], file.name, { type: 'video/mp4' });
|
||||||
}
|
}
|
||||||
|
|
@ -103,7 +104,7 @@ export async function uploadFile(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (baseType === 'video' && (!image || !thumb)) {
|
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 });
|
const [[, url]] = await uploader.upload(new File([bytes], 'thumb.jpg', { type: 'image/jpeg' }), { signal });
|
||||||
|
|
||||||
if (!image) {
|
if (!image) {
|
||||||
|
|
|
||||||
|
|
@ -87,13 +87,16 @@ interface Disposition {
|
||||||
still_image: number;
|
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, {
|
const stream = ffprobe(input, {
|
||||||
'v': 'error',
|
'v': 'error',
|
||||||
'show_streams': '',
|
'show_streams': '',
|
||||||
'show_format': '',
|
'show_format': '',
|
||||||
'of': 'json',
|
'of': 'json',
|
||||||
});
|
}, opts);
|
||||||
|
|
||||||
return new Response(stream).json();
|
return new Response(stream).json();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,13 @@ export interface FFmpegFlags {
|
||||||
[key: string]: string | undefined;
|
[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'];
|
const args = ['-i', input instanceof URL ? input.href : 'pipe:0'];
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(flags)) {
|
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
|
args.push('pipe:1'); // Output to stdout
|
||||||
|
|
||||||
// Spawn the FFmpeg process
|
// Spawn the FFmpeg process
|
||||||
const command = new Deno.Command('ffmpeg', {
|
const command = new Deno.Command(ffmpegPath, {
|
||||||
args,
|
args,
|
||||||
stdin: input instanceof ReadableStream ? 'piped' : 'null',
|
stdin: input instanceof ReadableStream ? 'piped' : 'null',
|
||||||
stdout: 'piped',
|
stdout: 'piped',
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,13 @@ export interface FFprobeFlags {
|
||||||
[key: string]: string | undefined;
|
[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 = [];
|
const args = [];
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(flags)) {
|
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
|
// Spawn the FFprobe process
|
||||||
const command = new Deno.Command('ffprobe', {
|
const command = new Deno.Command(ffprobePath, {
|
||||||
args,
|
args,
|
||||||
stdin: input instanceof ReadableStream ? 'piped' : 'null',
|
stdin: input instanceof ReadableStream ? 'piped' : 'null',
|
||||||
stdout: 'piped',
|
stdout: 'piped',
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { ffmpeg } from './ffmpeg.ts';
|
||||||
export function extractVideoFrame(
|
export function extractVideoFrame(
|
||||||
input: URL | ReadableStream<Uint8Array>,
|
input: URL | ReadableStream<Uint8Array>,
|
||||||
ss: string = '00:00:01',
|
ss: string = '00:00:01',
|
||||||
|
opts?: { ffmpegPath?: string | URL },
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
const output = ffmpeg(input, {
|
const output = ffmpeg(input, {
|
||||||
'ss': ss, // Seek to timestamp
|
'ss': ss, // Seek to timestamp
|
||||||
|
|
@ -10,7 +11,7 @@ export function extractVideoFrame(
|
||||||
'q:v': '2', // High-quality JPEG (lower = better quality)
|
'q:v': '2', // High-quality JPEG (lower = better quality)
|
||||||
'f': 'image2', // Force image format
|
'f': 'image2', // Force image format
|
||||||
'loglevel': 'fatal',
|
'loglevel': 'fatal',
|
||||||
});
|
}, opts);
|
||||||
|
|
||||||
return new Response(output).bytes();
|
return new Response(output).bytes();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { ffmpeg } from './ffmpeg.ts';
|
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, {
|
return ffmpeg(input, {
|
||||||
'safe': '1', // Safe mode
|
'safe': '1', // Safe mode
|
||||||
'nostdin': '', // Disable stdin
|
'nostdin': '', // Disable stdin
|
||||||
|
|
@ -12,5 +15,5 @@ export function transcodeVideo(input: URL | ReadableStream<Uint8Array>): Readabl
|
||||||
'b:a': '128k', // Audio bitrate
|
'b:a': '128k', // Audio bitrate
|
||||||
'movflags': 'frag_keyframe+empty_moov', // Ensures MP4 streaming compatibility
|
'movflags': 'frag_keyframe+empty_moov', // Ensures MP4 streaming compatibility
|
||||||
'f': 'mp4', // Force MP4 format
|
'f': 'mp4', // Force MP4 format
|
||||||
});
|
}, opts);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue