From e46b7bfa85f52a09baeb2c35c4039470c46cf19e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 27 Feb 2025 23:19:27 -0600 Subject: [PATCH] Rework ffmpeg to accept file URIs --- packages/transcode/analyze.test.ts | 11 +++++++++++ packages/transcode/analyze.ts | 17 +++++++++++++++++ packages/transcode/ffmpeg.test.ts | 13 +++++++++++++ packages/transcode/ffmpeg.ts | 15 +++++++++++---- packages/transcode/frame.test.ts | 9 +++++++++ packages/transcode/frame.ts | 13 +++++++++++++ 6 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 packages/transcode/analyze.test.ts create mode 100644 packages/transcode/frame.test.ts create mode 100644 packages/transcode/frame.ts diff --git a/packages/transcode/analyze.test.ts b/packages/transcode/analyze.test.ts new file mode 100644 index 00000000..67be60b1 --- /dev/null +++ b/packages/transcode/analyze.test.ts @@ -0,0 +1,11 @@ +import { assertEquals } from '@std/assert'; + +import { ffmpegDim } from './analyze.ts'; + +Deno.test('ffmpegDim', async () => { + await using file = await Deno.open(new URL('./buckbunny.mp4', import.meta.url)); + + const result = await ffmpegDim(file.readable); + + assertEquals(result, { width: 1280, height: 720 }); +}); diff --git a/packages/transcode/analyze.ts b/packages/transcode/analyze.ts index e69de29b..b3e0e5a9 100644 --- a/packages/transcode/analyze.ts +++ b/packages/transcode/analyze.ts @@ -0,0 +1,17 @@ +import { ffmpeg } from './ffmpeg.ts'; + +export async function ffmpegDim( + input: ReadableStream, +): Promise<{ width: number; height: number } | undefined> { + const result = ffmpeg(input, { + 'vf': 'showinfo', // Output as JSON + 'f': 'null', // Tell FFmpeg not to produce an output file + }); + + const text = await new Response(result).json(); + console.log(text); + const output = JSON.parse(text); + + const [stream] = output.streams ?? []; + return stream; +} diff --git a/packages/transcode/ffmpeg.test.ts b/packages/transcode/ffmpeg.test.ts index 57d69f80..06191d07 100644 --- a/packages/transcode/ffmpeg.test.ts +++ b/packages/transcode/ffmpeg.test.ts @@ -14,3 +14,16 @@ Deno.test('ffmpeg', async () => { await Deno.mkdir(new URL('./tmp', import.meta.url), { recursive: true }); await Deno.writeFile(new URL('./tmp/buckbunny-transcoded.mp4', import.meta.url), output); }); + +Deno.test('ffmpeg from file', async () => { + const output = ffmpeg(new URL('./buckbunny.mp4', import.meta.url), { + 'c:v': 'libx264', + 'preset': 'veryfast', + 'loglevel': 'fatal', + 'movflags': 'frag_keyframe+empty_moov', + 'f': 'mp4', + }); + + await Deno.mkdir(new URL('./tmp', import.meta.url), { recursive: true }); + await Deno.writeFile(new URL('./tmp/buckbunny-transcoded-fromfile.mp4', import.meta.url), output); +}); diff --git a/packages/transcode/ffmpeg.ts b/packages/transcode/ffmpeg.ts index cae2faa2..a76f7350 100644 --- a/packages/transcode/ffmpeg.ts +++ b/packages/transcode/ffmpeg.ts @@ -10,8 +10,8 @@ export interface FFmpegFlags { [key: string]: string | undefined; } -export function ffmpeg(input: ReadableStream, flags: FFmpegFlags): ReadableStream { - const args = ['-i', 'pipe:0']; // Input from stdin +export function ffmpeg(input: URL | ReadableStream, flags: FFmpegFlags): ReadableStream { + const args = ['-i', input instanceof URL ? input.href : 'pipe:0']; for (const [key, value] of Object.entries(flags)) { if (typeof value === 'string') { @@ -22,11 +22,18 @@ export function ffmpeg(input: ReadableStream, flags: FFmpegFlags): R args.push('pipe:1'); // Output to stdout // Spawn the FFmpeg process - const command = new Deno.Command('ffmpeg', { args, stdin: 'piped', stdout: 'piped' }); + const command = new Deno.Command('ffmpeg', { + args, + stdin: input instanceof ReadableStream ? 'piped' : undefined, + stdout: 'piped', + }); + const child = command.spawn(); // Pipe the input stream into FFmpeg stdin and ensure completion - input.pipeTo(child.stdin); + if (input instanceof ReadableStream) { + input.pipeTo(child.stdin); + } // Return the FFmpeg stdout stream return child.stdout; diff --git a/packages/transcode/frame.test.ts b/packages/transcode/frame.test.ts new file mode 100644 index 00000000..93894df2 --- /dev/null +++ b/packages/transcode/frame.test.ts @@ -0,0 +1,9 @@ +import { extractVideoFrame } from './frame.ts'; + +Deno.test('extractVideoFrame', async () => { + const uri = new URL('./buckbunny.mp4', import.meta.url); + const result = await extractVideoFrame(uri); + + await Deno.mkdir(new URL('./tmp', import.meta.url), { recursive: true }); + await Deno.writeFile(new URL('./tmp/buckbunny-poster.jpg', import.meta.url), result); +}); diff --git a/packages/transcode/frame.ts b/packages/transcode/frame.ts new file mode 100644 index 00000000..68e30e5e --- /dev/null +++ b/packages/transcode/frame.ts @@ -0,0 +1,13 @@ +import { ffmpeg } from './ffmpeg.ts'; + +export function extractVideoFrame(file: URL, ss: string = '00:00:01'): Promise { + const output = ffmpeg(file, { + 'ss': ss, // Seek to timestamp + 'frames:v': '1', // Extract only 1 frame + 'q:v': '2', // High-quality JPEG (lower = better quality) + 'f': 'image2', // Force image format + 'loglevel': 'fatal', + }); + + return new Response(output).bytes(); +}