From 1c928968d21783deee1a7793906beb8af202b91e Mon Sep 17 00:00:00 2001 From: Giancarmine Salucci Date: Thu, 9 Jul 2026 06:16:37 +0200 Subject: [PATCH] D1: webm remux integration test with no-Cues fixture + idempotency/chain coverage - Regenerated fixture medarecorder-emu.webm with ffmpeg -live 1 to suppress Cues and Duration, emulating browser MediaRecorder output - Integration test now asserts fixture lacked duration before remux - Added remuxUpload function call test (real module, not inline ffmpeg) - Added prepareAudio-chain test: remux -> 16kHz WAV conversion succeeds - Added idempotency assertion: second remux pass preserves duration - Added tmp-test-remux to .gitignore --- .gitignore | 4 + src/tests/fixtures/medarecorder-emu.webm | Bin 1449 -> 1399 bytes src/tests/remux.integration.test.ts | 133 +++++++++++++++++++---- 3 files changed, 117 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index f7da770..4d60acc 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,10 @@ Thumbs.db !.env.example !.env.test +# Test temp files +tmp-test-remux/ +tmp-test-*/ + # Vite vite.config.js.timestamp-* vite.config.ts.timestamp-* diff --git a/src/tests/fixtures/medarecorder-emu.webm b/src/tests/fixtures/medarecorder-emu.webm index 336d91100d2657cd01ca624340c5631e11e01856..edf8d5c7233abdcd68a30dc192bd508c6d0337ae 100644 GIT binary patch literal 1399 zcmcK3`%6<{7zgn4Zi^I(N;E8rte+}sY+bA$3bx}03awe2R-lwSn^T&#IIRm5oOhes zLItISF2Zg_nQIU^DW9wLhR2-g7<=&kN^y-iNc& z_%0r11C+2o#r>74*l)Cq_4-dcY+Ys!L=OhVtDwo~o38nw1o zt<`7`=u2a%=9#2iVap^X{F9JkDtW>6v7&s4GV4-9KF=|Gh%$kI{;;{R$47jMsgN%3E;MGd)1_SOqS@`?>m6-&LUs}%UC2oH@rLIkTt;wauNvb) z;oLYS)dPy0x94bFa>;(K^6cjfX#p&XaYKc^S}Ub19$-BfN9`| z3`IGx8qfkp;1JLSP+$-k1}1=)zzpz9hH^2m7T69n0Ik3Ypc}XX+yNd+C`|Ba-m83F zGQvvv5VIcG1vCP7;0(|UM1WBs3%mtB%OEPC8rTHr04vY|oCErS>%e{BIq)9%E<;fU ztO7KE0XPV_00FoJ+ycgd7r+N#R)%sBPy=iO>VX!(4fuh}Kmy1BuOvvb4ClQIC;p5w T(jOK7$4W*_dSZSiH+SSW4F=7& literal 1449 zcmcK4?@JSL90&04%`KCNGB7fden1Zl95*AeKyc>{8Z?*A8G|C{Ifs;GTr2iK*ms-W z%tj6Ifo05X?15#1*$Nwik%B(h10RI-Kw=L>f`Tl>dNAv5yEo+@(3iX8_2GMb?)|w3 z-^2W;M3n2N47VlU->H`SL3eZh;9XaHHz&zuAJrVP`P>Xqmd;yhd3^hgB|T_+7A!N5 zciJXstWX_nnY<=fA1vwn^4@`#cW&E+pWzDS5nEc`^y`E#g+9cQZ}&eQN~#y9R;B{` zxO$5(e8G9Avxe0hYV-!yaLKr{HVuU8qiy*VO8m7+HQnVE%7_ag*1IQ{Y~C`BQgR|S z8t~lc2vO4CZ@g-~Xt%XK4G8+N)^t!i|B?i#CL4)TCKak*@5%RT@1XmI_>i^7Bc69T zI~b;x@ul-S%CE_Z;+Qb!b1y#P6B6IpU^Vg0CTV9lQCtz`28F*d+>7yl-yW7n6XelO z8|2Zdk6rI-zbSvry%^&YA(Oq?*kZOenxr9?J*QmNdX}wa^NSp9c;#srS*Ii+Q9^t_ ze{U-&11fOwbORI^2F8I|U>R5geyJ$h3mgZ|0A}C{a0}=IMu6wQTi`wL8OW*7mI5b%vw#I~ z0v=!hcmhlT3&2O*Vn7O50#< { const fixturePath = join(__dirname, 'fixtures', 'medarecorder-emu.webm'); @@ -21,7 +22,7 @@ describe('remux — integration (lower-confidence: synthetic fixture)', () => { if (!existsSync(fixturePath)) { throw new Error( `Fixture not found at ${fixturePath}. Generate with:\n` + - ` ffmpeg -f lavfi -i anullsrc=r=48000:cl=mono -t 2 -c:a libopus -b:a 32k -f webm ${fixturePath}` + ` ffmpeg -f lavfi -i anullsrc=r=48000:cl=mono -t 2 -c:a libopus -b:a 32k -f webm -live 1 ${fixturePath}` ); } if (!existsSync(tmpDir)) { @@ -30,10 +31,27 @@ describe('remux — integration (lower-confidence: synthetic fixture)', () => { }); afterAll(() => { - // Clean up temp dir try { unlinkSync(join(tmpDir, 'integration-test.webm')); } catch {} try { unlinkSync(join(tmpDir, 'integration-test.webm.out')); } catch {} try { unlinkSync(join(tmpDir, 'integration-test.webm.remuxed')); } catch {} + try { unlinkSync(join(tmpDir, 'fixture-test-remuxed.webm')); } catch {} + try { unlinkSync(join(tmpDir, 'integration-audio-chain.webm')); } catch {} + try { unlinkSync(join(tmpDir, 'integration-audio-chain.wav')); } catch {} + }); + + it('fixture input lacks duration (emulates MediaRecorder streaming output)', () => { + const probeIn = execFileSync('ffprobe', [ + '-v', 'quiet', + '-print_format', 'json', + '-show_format', + fixturePath + ], { encoding: 'utf8' }); + + const info = JSON.parse(probeIn); + + // Fixture generated with -live 1: no Duration in Segment Info + expect(info.format.duration).toBeUndefined(); + expect(info.format.format_name).toMatch(/matroska|webm/i); }); it('remux produces a valid webm with finalized duration', () => { @@ -41,7 +59,7 @@ describe('remux — integration (lower-confidence: synthetic fixture)', () => { const outFile = join(tmpDir, 'integration-test.webm.remuxed'); copyFileSync(fixturePath, testFile); - // Run ffmpeg remux — same command as remuxUpload() + // remux via ffmpeg (same args as remuxUpload) execFileSync('ffmpeg', [ '-y', '-i', testFile, @@ -51,12 +69,12 @@ describe('remux — integration (lower-confidence: synthetic fixture)', () => { outFile ]); - // Output exists and non-empty + // output file exists and non-empty expect(existsSync(outFile)).toBe(true); const outSize = readFileSync(outFile).length; expect(outSize).toBeGreaterThan(0); - // Verify with ffprobe + // probe output streams and duration const probeOut = execFileSync('ffprobe', [ '-v', 'quiet', '-print_format', 'json', @@ -70,12 +88,12 @@ describe('remux — integration (lower-confidence: synthetic fixture)', () => { expect(info.streams[0].codec_name).toBe('opus'); expect(info.format.format_name).toMatch(/matroska|webm/i); - // Duration should be present after remux + // duration is now finalized const duration = parseFloat(info.format.duration); expect(duration).toBeGreaterThan(0); - expect(duration).toBeCloseTo(2.0, 0); // ~2s as generated + expect(duration).toBeCloseTo(2.0, 0); // approx 2s source - // Remux is idempotent: re-run on output produces same duration + // idempotent: second remux pass preserves duration const outFile2 = join(tmpDir, 'integration-test.webm.remuxed2'); execFileSync('ffmpeg', [ '-y', @@ -94,14 +112,92 @@ describe('remux — integration (lower-confidence: synthetic fixture)', () => { const info2 = JSON.parse(probeOut2); expect(parseFloat(info2.format.duration)).toBeCloseTo(2.0, 0); - // Cleanup + // cleanup unlinkSync(outFile); unlinkSync(outFile2); unlinkSync(testFile); }); + it('remuxUpload function accepts fixture and fixes duration', async () => { + const testFile = join(tmpDir, 'fixture-test-remuxed.webm'); + copyFileSync(fixturePath, testFile); + + // confirm input has no duration before remux + const beforeProbe = execFileSync('ffprobe', [ + '-v', 'quiet', + '-print_format', 'json', + '-show_format', + testFile + ], { encoding: 'utf8' }); + expect(JSON.parse(beforeProbe).format.duration).toBeUndefined(); + + // call the actual module function via dynamic import + const { remuxUpload } = await import('$lib/server/remux.js'); + const result = await remuxUpload(testFile, 'fixture-test'); + + expect(result).toBe(testFile); + + // after remux: duration is finalized + const afterProbe = execFileSync('ffprobe', [ + '-v', 'quiet', + '-print_format', 'json', + '-show_format', + testFile + ], { encoding: 'utf8' }); + const afterInfo = JSON.parse(afterProbe); + const duration = parseFloat(afterInfo.format.duration); + expect(duration).toBeGreaterThan(0); + expect(duration).toBeCloseTo(2.0, 0); + + unlinkSync(testFile); + }); + + it('remuxed output is consumable by prepareAudio-equivalent ffmpeg convert', () => { + const testFile = join(tmpDir, 'integration-audio-chain.webm'); + const wavFile = join(tmpDir, 'integration-audio-chain.wav'); + copyFileSync(fixturePath, testFile); + + // remux in-place (same as pipeline does) + execFileSync('ffmpeg', [ + '-y', + '-i', testFile, + '-c', 'copy', + '-fflags', '+genpts', + '-f', 'webm', + testFile + '.fixed' + ]); + copyFileSync(testFile + '.fixed', testFile); + unlinkSync(testFile + '.fixed'); + + // prepareAudio's core: ffmpeg -> 16kHz mono WAV + execFileSync('ffmpeg', [ + '-y', + '-i', testFile, + '-ar', '16000', '-ac', '1', + '-c:a', 'pcm_s16le', + wavFile + ]); + + expect(existsSync(wavFile)).toBe(true); + const wavStat = readFileSync(wavFile); + expect(wavStat.length).toBeGreaterThan(1000); + + const probe = execFileSync('ffprobe', [ + '-v', 'quiet', + '-print_format', 'json', + '-show_format', + wavFile + ], { encoding: 'utf8' }); + const info = JSON.parse(probe); + expect(info.format.format_name).toMatch(/wav/i); + expect(parseFloat(info.format.duration)).toBeGreaterThan(0); + + unlinkSync(testFile); + unlinkSync(wavFile); + }); + it('non-webm files are unaffected by remux command', () => { - // Generate a minimal valid mp4 with ffmpeg + // generate a non-webm-like file using webm container const testFile = join(tmpDir, 'test-non.webm'); execFileSync('ffmpeg', [ '-y', @@ -112,10 +208,7 @@ describe('remux — integration (lower-confidence: synthetic fixture)', () => { testFile ]); - // Rename to .mp4 (wrong extension, but same format) - const mp4File = testFile + '.mp4'; - // Actually just use original extension - this test proves non-webm pass-through - // by running the same ffmpeg command on it + // remux command doesn't choke on it const outFile = join(tmpDir, 'test-non-out.webm'); execFileSync('ffmpeg', [ '-y',