From 8fb22633feefa456aa8080086cf5a9afd9e04b55 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:22:38 +0200 Subject: [PATCH] Add frame container atom header stripping & readding for ProRes in Matroska, bump patch --- package-lock.json | 16 +++++++------- package.json | 2 +- packages/aac-encoder/package.json | 2 +- packages/ac3/package.json | 2 +- packages/flac-encoder/package.json | 2 +- packages/mp3-encoder/package.json | 2 +- packages/prores/package.json | 2 +- packages/server/package.json | 2 +- src/matroska/matroska-demuxer.ts | 34 ++++++++++++++++++++++++++++++ src/matroska/matroska-muxer.ts | 13 +++++++++++- test/node/prores.test.ts | 14 +++++++++++- 11 files changed, 74 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6cdbf0c..00c3fdb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mediabunny", - "version": "1.50.1", + "version": "1.50.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mediabunny", - "version": "1.50.1", + "version": "1.50.2", "license": "MPL-2.0", "workspaces": [ ".", @@ -12894,7 +12894,7 @@ }, "packages/aac-encoder": { "name": "@mediabunny/aac-encoder", - "version": "1.50.1", + "version": "1.50.2", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" @@ -12909,7 +12909,7 @@ }, "packages/ac3": { "name": "@mediabunny/ac3", - "version": "1.50.1", + "version": "1.50.2", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" @@ -12924,7 +12924,7 @@ }, "packages/flac-encoder": { "name": "@mediabunny/flac-encoder", - "version": "1.50.1", + "version": "1.50.2", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" @@ -12939,7 +12939,7 @@ }, "packages/mp3-encoder": { "name": "@mediabunny/mp3-encoder", - "version": "1.50.1", + "version": "1.50.2", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" @@ -12954,7 +12954,7 @@ }, "packages/prores": { "name": "@mediabunny/prores", - "version": "1.50.1", + "version": "1.50.2", "license": "MPL-2.0", "dependencies": { "turbores": "^1.1.2" @@ -12969,7 +12969,7 @@ }, "packages/server": { "name": "@mediabunny/server", - "version": "1.50.1", + "version": "1.50.2", "license": "MPL-2.0", "dependencies": { "@mediabunny/prores": "^1.50.0", diff --git a/package.json b/package.json index 4fd085c..f0eeb7d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mediabunny", "author": "Vanilagy", - "version": "1.50.1", + "version": "1.50.2", "description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.", "type": "module", "workspaces": [ diff --git a/packages/aac-encoder/package.json b/packages/aac-encoder/package.json index 0829417..f121f0e 100644 --- a/packages/aac-encoder/package.json +++ b/packages/aac-encoder/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/aac-encoder", "author": "Vanilagy", - "version": "1.50.1", + "version": "1.50.2", "description": "AAC encoder extension for Mediabunny, based on FFmpeg.", "main": "./dist/bundles/mediabunny-aac-encoder.mjs", "module": "./dist/bundles/mediabunny-aac-encoder.mjs", diff --git a/packages/ac3/package.json b/packages/ac3/package.json index dd9cc9f..8f9b30d 100644 --- a/packages/ac3/package.json +++ b/packages/ac3/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/ac3", "author": "Vanilagy", - "version": "1.50.1", + "version": "1.50.2", "description": "AC-3 and E-AC-3 (Dolby Digital) decoder and encoder extension for Mediabunny, based on FFmpeg.", "main": "./dist/bundles/mediabunny-ac3.mjs", "module": "./dist/bundles/mediabunny-ac3.mjs", diff --git a/packages/flac-encoder/package.json b/packages/flac-encoder/package.json index 88cdd3d..ef7494e 100644 --- a/packages/flac-encoder/package.json +++ b/packages/flac-encoder/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/flac-encoder", "author": "Vanilagy", - "version": "1.50.1", + "version": "1.50.2", "description": "FLAC encoder extension for Mediabunny, based on libFLAC.", "main": "./dist/bundles/mediabunny-flac-encoder.mjs", "module": "./dist/bundles/mediabunny-flac-encoder.mjs", diff --git a/packages/mp3-encoder/package.json b/packages/mp3-encoder/package.json index 3c277bf..4514467 100644 --- a/packages/mp3-encoder/package.json +++ b/packages/mp3-encoder/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/mp3-encoder", "author": "Vanilagy", - "version": "1.50.1", + "version": "1.50.2", "description": "MP3 encoder extension for Mediabunny, based on LAME.", "main": "./dist/bundles/mediabunny-mp3-encoder.mjs", "module": "./dist/bundles/mediabunny-mp3-encoder.mjs", diff --git a/packages/prores/package.json b/packages/prores/package.json index af543d7..eb760b6 100644 --- a/packages/prores/package.json +++ b/packages/prores/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/prores", "author": "Vanilagy", - "version": "1.50.1", + "version": "1.50.2", "description": "Apple ProRes decoder extension for Mediabunny, based on TurboRes.", "main": "./dist/bundles/mediabunny-prores.mjs", "module": "./dist/bundles/mediabunny-prores.mjs", diff --git a/packages/server/package.json b/packages/server/package.json index 7bcc449..0f5b128 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/server", "author": "Vanilagy", - "version": "1.50.1", + "version": "1.50.2", "description": "Adds full video and audio decoder and encoder support to Mediabunny for use in server-side environments (Node, Bun, Deno). Based on NodeAV.", "main": "./dist/bundles/mediabunny-server.cjs", "module": "./dist/bundles/mediabunny-server.mjs", diff --git a/src/matroska/matroska-demuxer.ts b/src/matroska/matroska-demuxer.ts index 27ce186..26d538b 100644 --- a/src/matroska/matroska-demuxer.ts +++ b/src/matroska/matroska-demuxer.ts @@ -46,6 +46,7 @@ import { Rotation, roundIfAlmostInteger, textDecoder, + toDataView, TRANSFER_CHARACTERISTICS_MAP_INVERSE, UNDETERMINED_LANGUAGE, } from '../misc'; @@ -141,6 +142,7 @@ type ClusterBlock = { data: Uint8Array; lacing: BlockLacing; decoded: boolean; + postProcessed: boolean; // For codec-specific processing mainAdditional: Uint8Array | null; }; @@ -864,6 +866,7 @@ export class MatroskaDemuxer extends Demuxer { data: frameData, lacing: BlockLacing.None, decoded: true, + postProcessed: false, mainAdditional: originalBlock.mainAdditional, }); } @@ -1502,6 +1505,7 @@ export class MatroskaDemuxer extends Demuxer { data: blockData, lacing, decoded: !hasDecodingInstructions, + postProcessed: false, mainAdditional: null, }); }; break; @@ -1538,6 +1542,7 @@ export class MatroskaDemuxer extends Demuxer { data: blockData, lacing, decoded: !hasDecodingInstructions, + postProcessed: false, mainAdditional: null, }; trackData.blocks.push(this.currentBlock); @@ -2194,6 +2199,35 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { block.decoded = true; } + if (!block.postProcessed) { + if (this.internalTrack.info?.codec === 'prores') { + // For some reason, ProRes packets are stored in Matroska without the frame container atom. FFmpeg cites + // the "Matroska spec" but the actual spec says nothing about this. + + const hasFrameContainer = block.data.length >= 8 + && block.data[4] === 105 // 'i' + && block.data[5] === 99 // 'c' + && block.data[6] === 112 // 'p' + && block.data[7] === 102; // 'f' + + if (!hasFrameContainer) { + // Wrap the frame in a frame container + const newData = new Uint8Array(block.data.length + 8); + const newDataView = toDataView(newData); + + newDataView.setUint32(0, newData.length, false); + newData[4] = 105; // 'i' + newData[5] = 99; // 'c' + newData[6] = 112; // 'p' + newData[7] = 102; // 'f' + newData.set(block.data, 8); + block.data = newData; + } + } + + block.postProcessed = true; + } + const data = options.metadataOnly ? PLACEHOLDER_DATA : block.data; const timestamp = block.timestamp / this.internalTrack.segment.timestampFactor; const duration = block.duration / this.internalTrack.segment.timestampFactor; diff --git a/src/matroska/matroska-muxer.ts b/src/matroska/matroska-muxer.ts index 1ea6bae..79e2e93 100644 --- a/src/matroska/matroska-muxer.ts +++ b/src/matroska/matroska-muxer.ts @@ -897,6 +897,17 @@ export class MatroskaMuxer extends Muxer { try { const trackData = this.getVideoTrackData(track, packet, meta); + let packetData = packet.data; + if (track.source._codec === 'prores') { + if (packetData.byteLength < 8) { + throw new Error('ProRes packet too small, expected at least 8 bytes.'); + } + + // Trim off the frame container atom header. FFmpeg does this too and cites the "Matroska spec" as the + // reason, despite the spec not saying anything about this. + packetData = packetData.subarray(8); + } + const isKeyFrame = packet.type === 'key'; this.validateTimestamp(trackData.track, packet.timestamp, isKeyFrame); @@ -913,7 +924,7 @@ export class MatroskaMuxer extends Muxer { ? packet.sideData.alpha ?? null : null; - const videoChunk = this.createInternalChunk(packet.data, timestamp, duration, packet.type, additions); + const videoChunk = this.createInternalChunk(packetData, timestamp, duration, packet.type, additions); if (track.source._codec === 'vp9') this.fixVP9ColorSpace(trackData, videoChunk); trackData.chunkQueue.push(videoChunk); diff --git a/test/node/prores.test.ts b/test/node/prores.test.ts index ccab468..682cd93 100644 --- a/test/node/prores.test.ts +++ b/test/node/prores.test.ts @@ -7,7 +7,7 @@ import { Output } from '../../src/output.js'; import { MkvOutputFormat, MovOutputFormat } from '../../src/output-format.js'; import { BufferTarget } from '../../src/target.js'; import { Conversion } from '../../src/conversion.js'; -import { VideoSampleSink } from '../../src/media-sink.js'; +import { EncodedPacketSink, VideoSampleSink } from '../../src/media-sink.js'; import { assert } from '../../src/misc.js'; const SAMPLE_URL = 'https://pub-1ee78aacb848486482b20a72b55b3121.r2.dev/turbores-sample.mov'; @@ -88,6 +88,10 @@ test.concurrent('ProRes transmuxing into MKV', { timeout: 10_000 }, async () => }); await conversion.execute(); + // No 'icpf' means the frame container atom headers were successfully stripped from the ProRes packets + let str = new TextDecoder('ascii').decode(output.target.buffer!); + expect(str.includes('icpf')).toBe(false); + using newInput = new Input({ source: new BufferSource(output.target.buffer!), formats: ALL_FORMATS, @@ -100,6 +104,14 @@ test.concurrent('ProRes transmuxing into MKV', { timeout: 10_000 }, async () => const decoderConfig = (await videoTrack.getDecoderConfig())!; expect(decoderConfig.codec).toBe('apch'); expect(decoderConfig.description).toBeUndefined(); + + const sink = new EncodedPacketSink(videoTrack); + const firstPacket = await sink.getFirstPacket(); + assert(firstPacket); + + // The frame container atom headers are added back when reading out the packets + str = new TextDecoder('ascii').decode(firstPacket.data); + expect(str.includes('icpf')).toBe(true); }); test('Custom coder registration', { timeout: 10_000 }, async () => {