diff --git a/packages/ditto/utils/buckbunny.mp4 b/packages/ditto/utils/buckbunny.mp4 new file mode 100644 index 00000000..91fdbb8a Binary files /dev/null and b/packages/ditto/utils/buckbunny.mp4 differ diff --git a/packages/ditto/utils/transcode.test.ts b/packages/ditto/utils/transcode.test.ts new file mode 100644 index 00000000..40c1fec4 --- /dev/null +++ b/packages/ditto/utils/transcode.test.ts @@ -0,0 +1,8 @@ +import { transcodeVideoStream } from './transcode.ts'; + +Deno.test('transcodeVideoStream', async () => { + await using file = await Deno.open(new URL('./buckbunny.mp4', import.meta.url)); + const output = await transcodeVideoStream(file.readable); + + await Deno.writeFile(new URL('./buckbunny-transcoded.mp4', import.meta.url), output); +}); diff --git a/packages/ditto/utils/transcode.ts b/packages/ditto/utils/transcode.ts new file mode 100644 index 00000000..8062f85a --- /dev/null +++ b/packages/ditto/utils/transcode.ts @@ -0,0 +1,62 @@ +export async function transcodeVideoStream( + inputStream: ReadableStream, +): Promise> { + const command = new Deno.Command('ffmpeg', { + args: [ + '-i', + 'pipe:0', // Read input from stdin + '-c:v', + 'libx264', // Convert to H.264 + '-preset', + 'veryfast', // Encoding speed + '-loglevel', + 'fatal', // Suppress logs + '-crf', + '23', // Compression level (lower = better quality) + '-c:a', + 'aac', // Convert to AAC audio + '-b:a', + '128k', // Audio bitrate + '-movflags', + 'frag_keyframe+empty_moov', // Ensures MP4 streaming compatibility + '-f', + 'mp4', // Force MP4 format + 'pipe:1', // Output to stdout + ], + stdin: 'piped', + stdout: 'piped', + stderr: 'piped', + }); + + // Spawn the FFmpeg process + const process = command.spawn(); + + // Capture stderr for debugging + const stderrPromise = new Response(process.stderr).text().then((text) => { + if (text.trim()) console.error('FFmpeg stderr:', text); + }); + + // Pipe the input stream into FFmpeg stdin and ensure completion + const writer = process.stdin.getWriter(); + const reader = inputStream.getReader(); + + async function pumpInput() { + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + await writer.write(value); + } + } finally { + writer.close(); // Close stdin to signal FFmpeg that input is done + } + } + + // Start pumping input asynchronously + pumpInput(); + + // Ensure stderr logs are captured + stderrPromise.catch(console.error); + + return process.stdout; +}