Add frame container atom header stripping & readding for ProRes in Matroska, bump patch

This commit is contained in:
Vanilagy
2026-07-01 16:22:38 +02:00
parent 0480ecbd3a
commit 8fb22633fe
11 changed files with 74 additions and 17 deletions
+8 -8
View File
@@ -1,12 +1,12 @@
{ {
"name": "mediabunny", "name": "mediabunny",
"version": "1.50.1", "version": "1.50.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mediabunny", "name": "mediabunny",
"version": "1.50.1", "version": "1.50.2",
"license": "MPL-2.0", "license": "MPL-2.0",
"workspaces": [ "workspaces": [
".", ".",
@@ -12894,7 +12894,7 @@
}, },
"packages/aac-encoder": { "packages/aac-encoder": {
"name": "@mediabunny/aac-encoder", "name": "@mediabunny/aac-encoder",
"version": "1.50.1", "version": "1.50.2",
"license": "MPL-2.0", "license": "MPL-2.0",
"devDependencies": { "devDependencies": {
"@types/emscripten": "^1.40.1" "@types/emscripten": "^1.40.1"
@@ -12909,7 +12909,7 @@
}, },
"packages/ac3": { "packages/ac3": {
"name": "@mediabunny/ac3", "name": "@mediabunny/ac3",
"version": "1.50.1", "version": "1.50.2",
"license": "MPL-2.0", "license": "MPL-2.0",
"devDependencies": { "devDependencies": {
"@types/emscripten": "^1.40.1" "@types/emscripten": "^1.40.1"
@@ -12924,7 +12924,7 @@
}, },
"packages/flac-encoder": { "packages/flac-encoder": {
"name": "@mediabunny/flac-encoder", "name": "@mediabunny/flac-encoder",
"version": "1.50.1", "version": "1.50.2",
"license": "MPL-2.0", "license": "MPL-2.0",
"devDependencies": { "devDependencies": {
"@types/emscripten": "^1.40.1" "@types/emscripten": "^1.40.1"
@@ -12939,7 +12939,7 @@
}, },
"packages/mp3-encoder": { "packages/mp3-encoder": {
"name": "@mediabunny/mp3-encoder", "name": "@mediabunny/mp3-encoder",
"version": "1.50.1", "version": "1.50.2",
"license": "MPL-2.0", "license": "MPL-2.0",
"devDependencies": { "devDependencies": {
"@types/emscripten": "^1.40.1" "@types/emscripten": "^1.40.1"
@@ -12954,7 +12954,7 @@
}, },
"packages/prores": { "packages/prores": {
"name": "@mediabunny/prores", "name": "@mediabunny/prores",
"version": "1.50.1", "version": "1.50.2",
"license": "MPL-2.0", "license": "MPL-2.0",
"dependencies": { "dependencies": {
"turbores": "^1.1.2" "turbores": "^1.1.2"
@@ -12969,7 +12969,7 @@
}, },
"packages/server": { "packages/server": {
"name": "@mediabunny/server", "name": "@mediabunny/server",
"version": "1.50.1", "version": "1.50.2",
"license": "MPL-2.0", "license": "MPL-2.0",
"dependencies": { "dependencies": {
"@mediabunny/prores": "^1.50.0", "@mediabunny/prores": "^1.50.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "mediabunny", "name": "mediabunny",
"author": "Vanilagy", "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.", "description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
"type": "module", "type": "module",
"workspaces": [ "workspaces": [
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/aac-encoder", "name": "@mediabunny/aac-encoder",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.50.1", "version": "1.50.2",
"description": "AAC encoder extension for Mediabunny, based on FFmpeg.", "description": "AAC encoder extension for Mediabunny, based on FFmpeg.",
"main": "./dist/bundles/mediabunny-aac-encoder.mjs", "main": "./dist/bundles/mediabunny-aac-encoder.mjs",
"module": "./dist/bundles/mediabunny-aac-encoder.mjs", "module": "./dist/bundles/mediabunny-aac-encoder.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/ac3", "name": "@mediabunny/ac3",
"author": "Vanilagy", "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.", "description": "AC-3 and E-AC-3 (Dolby Digital) decoder and encoder extension for Mediabunny, based on FFmpeg.",
"main": "./dist/bundles/mediabunny-ac3.mjs", "main": "./dist/bundles/mediabunny-ac3.mjs",
"module": "./dist/bundles/mediabunny-ac3.mjs", "module": "./dist/bundles/mediabunny-ac3.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/flac-encoder", "name": "@mediabunny/flac-encoder",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.50.1", "version": "1.50.2",
"description": "FLAC encoder extension for Mediabunny, based on libFLAC.", "description": "FLAC encoder extension for Mediabunny, based on libFLAC.",
"main": "./dist/bundles/mediabunny-flac-encoder.mjs", "main": "./dist/bundles/mediabunny-flac-encoder.mjs",
"module": "./dist/bundles/mediabunny-flac-encoder.mjs", "module": "./dist/bundles/mediabunny-flac-encoder.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/mp3-encoder", "name": "@mediabunny/mp3-encoder",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.50.1", "version": "1.50.2",
"description": "MP3 encoder extension for Mediabunny, based on LAME.", "description": "MP3 encoder extension for Mediabunny, based on LAME.",
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs", "main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs", "module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/prores", "name": "@mediabunny/prores",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.50.1", "version": "1.50.2",
"description": "Apple ProRes decoder extension for Mediabunny, based on TurboRes.", "description": "Apple ProRes decoder extension for Mediabunny, based on TurboRes.",
"main": "./dist/bundles/mediabunny-prores.mjs", "main": "./dist/bundles/mediabunny-prores.mjs",
"module": "./dist/bundles/mediabunny-prores.mjs", "module": "./dist/bundles/mediabunny-prores.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/server", "name": "@mediabunny/server",
"author": "Vanilagy", "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.", "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", "main": "./dist/bundles/mediabunny-server.cjs",
"module": "./dist/bundles/mediabunny-server.mjs", "module": "./dist/bundles/mediabunny-server.mjs",
+34
View File
@@ -46,6 +46,7 @@ import {
Rotation, Rotation,
roundIfAlmostInteger, roundIfAlmostInteger,
textDecoder, textDecoder,
toDataView,
TRANSFER_CHARACTERISTICS_MAP_INVERSE, TRANSFER_CHARACTERISTICS_MAP_INVERSE,
UNDETERMINED_LANGUAGE, UNDETERMINED_LANGUAGE,
} from '../misc'; } from '../misc';
@@ -141,6 +142,7 @@ type ClusterBlock = {
data: Uint8Array; data: Uint8Array;
lacing: BlockLacing; lacing: BlockLacing;
decoded: boolean; decoded: boolean;
postProcessed: boolean; // For codec-specific processing
mainAdditional: Uint8Array | null; mainAdditional: Uint8Array | null;
}; };
@@ -864,6 +866,7 @@ export class MatroskaDemuxer extends Demuxer {
data: frameData, data: frameData,
lacing: BlockLacing.None, lacing: BlockLacing.None,
decoded: true, decoded: true,
postProcessed: false,
mainAdditional: originalBlock.mainAdditional, mainAdditional: originalBlock.mainAdditional,
}); });
} }
@@ -1502,6 +1505,7 @@ export class MatroskaDemuxer extends Demuxer {
data: blockData, data: blockData,
lacing, lacing,
decoded: !hasDecodingInstructions, decoded: !hasDecodingInstructions,
postProcessed: false,
mainAdditional: null, mainAdditional: null,
}); });
}; break; }; break;
@@ -1538,6 +1542,7 @@ export class MatroskaDemuxer extends Demuxer {
data: blockData, data: blockData,
lacing, lacing,
decoded: !hasDecodingInstructions, decoded: !hasDecodingInstructions,
postProcessed: false,
mainAdditional: null, mainAdditional: null,
}; };
trackData.blocks.push(this.currentBlock); trackData.blocks.push(this.currentBlock);
@@ -2194,6 +2199,35 @@ abstract class MatroskaTrackBacking implements InputTrackBacking {
block.decoded = true; 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 data = options.metadataOnly ? PLACEHOLDER_DATA : block.data;
const timestamp = block.timestamp / this.internalTrack.segment.timestampFactor; const timestamp = block.timestamp / this.internalTrack.segment.timestampFactor;
const duration = block.duration / this.internalTrack.segment.timestampFactor; const duration = block.duration / this.internalTrack.segment.timestampFactor;
+12 -1
View File
@@ -897,6 +897,17 @@ export class MatroskaMuxer extends Muxer {
try { try {
const trackData = this.getVideoTrackData(track, packet, meta); 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'; const isKeyFrame = packet.type === 'key';
this.validateTimestamp(trackData.track, packet.timestamp, isKeyFrame); this.validateTimestamp(trackData.track, packet.timestamp, isKeyFrame);
@@ -913,7 +924,7 @@ export class MatroskaMuxer extends Muxer {
? packet.sideData.alpha ?? null ? packet.sideData.alpha ?? null
: 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); if (track.source._codec === 'vp9') this.fixVP9ColorSpace(trackData, videoChunk);
trackData.chunkQueue.push(videoChunk); trackData.chunkQueue.push(videoChunk);
+13 -1
View File
@@ -7,7 +7,7 @@ import { Output } from '../../src/output.js';
import { MkvOutputFormat, MovOutputFormat } from '../../src/output-format.js'; import { MkvOutputFormat, MovOutputFormat } from '../../src/output-format.js';
import { BufferTarget } from '../../src/target.js'; import { BufferTarget } from '../../src/target.js';
import { Conversion } from '../../src/conversion.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'; import { assert } from '../../src/misc.js';
const SAMPLE_URL = 'https://pub-1ee78aacb848486482b20a72b55b3121.r2.dev/turbores-sample.mov'; 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(); 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({ using newInput = new Input({
source: new BufferSource(output.target.buffer!), source: new BufferSource(output.target.buffer!),
formats: ALL_FORMATS, formats: ALL_FORMATS,
@@ -100,6 +104,14 @@ test.concurrent('ProRes transmuxing into MKV', { timeout: 10_000 }, async () =>
const decoderConfig = (await videoTrack.getDecoderConfig())!; const decoderConfig = (await videoTrack.getDecoderConfig())!;
expect(decoderConfig.codec).toBe('apch'); expect(decoderConfig.codec).toBe('apch');
expect(decoderConfig.description).toBeUndefined(); 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 () => { test('Custom coder registration', { timeout: 10_000 }, async () => {