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",
"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",
+1 -1
View File
@@ -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": [
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+34
View File
@@ -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;
+12 -1
View File
@@ -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);
+13 -1
View File
@@ -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 () => {