Add support for header removal content compression in Matroska

This commit is contained in:
Vanilagy
2025-09-17 23:09:04 +02:00
parent 6907e25c6d
commit 6d5bd58ae8
3 changed files with 195 additions and 5 deletions
+23 -1
View File
@@ -14,7 +14,29 @@
source: new Mediabunny.BlobSource(file),
});
console.log(await input.getMetadataTags())
const videoTrack = await input.getPrimaryVideoTrack();
const sink = new Mediabunny.VideoSampleSink(videoTrack);
const sink2 = new Mediabunny.EncodedPacketSink(videoTrack);
for await (const packet of sink2.packets()) {
console.log(packet)//, await videoTrack.determinePacketType(packet));
if (packet.timestamp >= 1) {
break;
}
}
console.log(await sink.getSample(0))
return;
for await (const sample of sink.samples()) {
console.log(sample);
if (sample.timestamp >= 1) {
break;
}
}
/*
const sink = new Mediabunny.EncodedPacketSink(videoTrack);
+8
View File
@@ -150,6 +150,14 @@ export enum EBMLId {
TagLanguage = 0x447a,
TagString = 0x4487,
TagBinary = 0x4485,
ContentEncodings = 0x6d80,
ContentEncoding = 0x6240,
ContentEncodingOrder = 0x5031,
ContentEncodingScope = 0x5032,
ContentCompression = 0x5034,
ContentCompAlgo = 0x4254,
ContentCompSettings = 0x4255,
ContentEncryption = 0x5035,
}
export const LEVEL_0_EBML_IDS: EBMLId[] = [
+164 -4
View File
@@ -105,6 +105,7 @@ type SeekEntry = {
};
type Cluster = {
segment: Segment;
elementStartPos: number;
elementEndPos: number;
dataStartPos: number;
@@ -115,6 +116,7 @@ type Cluster = {
};
type ClusterTrackData = {
track: InternalTrack | null;
startTimestamp: number;
endTimestamp: number;
firstKeyFrameTimestamp: number | null;
@@ -147,6 +149,32 @@ type CuePoint = {
clusterPosition: number;
};
enum ContentEncodingScope {
Block = 1,
Private = 2,
Next = 4,
}
enum ContentCompAlgo {
Zlib,
Bzlib,
lzo1x,
HeaderStripping,
}
type DecodingInstruction = {
order: number;
scope: ContentEncodingScope;
data: {
type: 'decompress';
algorithm: ContentCompAlgo | null;
settings: Uint8Array | null;
} | {
type: 'decrypt';
// Don't store more yet since this operation is unsupported
} | null;
};
type InternalTrack = {
id: number;
demuxer: MatroskaDemuxer;
@@ -162,6 +190,8 @@ type InternalTrack = {
defaultDuration: number | null;
name: string | null;
languageCode: string;
decodingInstructions: DecodingInstruction[];
info:
| null
| {
@@ -205,6 +235,7 @@ export class MatroskaDemuxer extends Demuxer {
currentCluster: Cluster | null = null;
currentBlock: ClusterBlock | null = null;
currentCueTime: number | null = null;
currentDecodingInstruction: DecodingInstruction | null = null;
currentTagTargetIsMovie: boolean = true;
currentSimpleTagName: string | null = null;
currentAttachedFile: {
@@ -583,6 +614,7 @@ export class MatroskaDemuxer extends Demuxer {
if (dataSlice instanceof Promise) dataSlice = await dataSlice;
const cluster: Cluster = {
segment,
elementStartPos,
elementEndPos: dataStartPos + size,
dataStartPos,
@@ -597,8 +629,8 @@ export class MatroskaDemuxer extends Demuxer {
this.readContiguousElements(dataSlice);
}
for (const [trackId, trackData] of cluster.trackData) {
const track = segment.tracks.find(x => x.id === trackId) ?? null;
for (const [, trackData] of cluster.trackData) {
const track = trackData.track;
// This must hold, as track datas only get created if a block for that track is encountered
assert(trackData.blocks.length > 0);
@@ -683,6 +715,7 @@ export class MatroskaDemuxer extends Demuxer {
let trackData = cluster.trackData.get(trackNumber);
if (!trackData) {
trackData = {
track: cluster.segment.tracks.find(x => x.id === trackNumber) ?? null,
startTimestamp: 0,
endTimestamp: 0,
firstKeyFrameTimestamp: null,
@@ -934,11 +967,21 @@ export class MatroskaDemuxer extends Demuxer {
defaultDuration: null,
name: null,
languageCode: UNDETERMINED_LANGUAGE,
decodingInstructions: [],
info: null,
};
this.readContiguousElements(slice.slice(dataStartPos, size));
if (this.currentTrack.decodingInstructions.some((instruction) => {
return instruction.data?.type !== 'decompress'
|| instruction.data.algorithm !== ContentCompAlgo.HeaderStripping;
})) {
console.warn(`Track #${this.currentTrack.id} has an unsupported content encoding; dropping.`);
this.currentTrack = null;
}
if (
this.currentTrack
&& this.currentTrack.id !== -1
@@ -1295,12 +1338,18 @@ export class MatroskaDemuxer extends Demuxer {
const lacing = (flags >> 1) & 0x3 as BlockLacing; // If the block is laced, we'll expand it later
const trackData = this.getTrackDataInCluster(this.currentCluster, trackNumber);
let blockData = readBytes(slice, size - (slice.filePos - dataStartPos));
if (trackData.track) {
blockData = this.decodeBlockData(trackData.track, blockData);
}
trackData.blocks.push({
timestamp: relativeTimestamp, // We'll add the cluster's timestamp to this later
duration: 0, // Will set later
isKeyFrame,
referencedTimestamps: [],
data: readBytes(slice, size - (slice.filePos - dataStartPos)),
data: blockData,
lacing,
});
}; break;
@@ -1331,12 +1380,18 @@ export class MatroskaDemuxer extends Demuxer {
const lacing = (flags >> 1) & 0x3 as BlockLacing; // If the block is laced, we'll expand it later
const trackData = this.getTrackDataInCluster(this.currentCluster, trackNumber);
let blockData = readBytes(slice, size - (slice.filePos - dataStartPos));
if (trackData.track) {
blockData = this.decodeBlockData(trackData.track, blockData);
}
this.currentBlock = {
timestamp: relativeTimestamp, // We'll add the cluster's timestamp to this later
duration: 0, // Will set later
isKeyFrame: true,
referencedTimestamps: [],
data: readBytes(slice, size - (slice.filePos - dataStartPos)),
data: blockData,
lacing,
};
trackData.blocks.push(this.currentBlock);
@@ -1469,12 +1524,117 @@ export class MatroskaDemuxer extends Demuxer {
this.currentAttachedFile.fileDescription = readUnicodeString(slice, size);
}; break;
case EBMLId.ContentEncodings: {
if (!this.currentTrack) break;
this.readContiguousElements(slice.slice(dataStartPos, size));
// "**MUST** start with the `ContentEncoding` with the highest `ContentEncodingOrder`"
this.currentTrack.decodingInstructions.sort((a, b) => b.order - a.order);
}; break;
case EBMLId.ContentEncoding: {
this.currentDecodingInstruction = {
order: 0,
scope: ContentEncodingScope.Block,
data: null,
};
this.readContiguousElements(slice.slice(dataStartPos, size));
if (this.currentDecodingInstruction.data) {
this.currentTrack!.decodingInstructions.push(this.currentDecodingInstruction);
}
this.currentDecodingInstruction = null;
}; break;
case EBMLId.ContentEncodingOrder: {
if (!this.currentDecodingInstruction) break;
this.currentDecodingInstruction.order = readUnsignedInt(slice, size);
}; break;
case EBMLId.ContentEncodingScope: {
if (!this.currentDecodingInstruction) break;
this.currentDecodingInstruction.scope = readUnsignedInt(slice, size);
}; break;
case EBMLId.ContentCompression: {
if (!this.currentDecodingInstruction) break;
this.currentDecodingInstruction.data = {
type: 'decompress',
algorithm: ContentCompAlgo.Zlib,
settings: null,
};
this.readContiguousElements(slice.slice(dataStartPos, size));
}; break;
case EBMLId.ContentCompAlgo: {
if (this.currentDecodingInstruction?.data?.type !== 'decompress') break;
this.currentDecodingInstruction.data.algorithm = readUnsignedInt(slice, size);
}; break;
case EBMLId.ContentCompSettings: {
if (this.currentDecodingInstruction?.data?.type !== 'decompress') break;
this.currentDecodingInstruction.data.settings = readBytes(slice, size);
}; break;
case EBMLId.ContentEncryption: {
if (!this.currentDecodingInstruction) break;
this.currentDecodingInstruction.data = {
type: 'decrypt',
};
}; break;
}
slice.filePos = dataStartPos + size;
return true;
}
decodeBlockData(track: InternalTrack, rawData: Uint8Array) {
let currentData = rawData;
// In the vast number of cases there are exactly zero decoding instructions
for (let i = 0; i < track.decodingInstructions.length; i++) {
const instruction = track.decodingInstructions[i]!;
assert(instruction.data);
switch (instruction.data.type) {
case 'decompress': {
switch (instruction.data.algorithm) {
case ContentCompAlgo.HeaderStripping: {
const prefix = instruction.data.settings ?? new Uint8Array(0);
const newData = new Uint8Array(prefix.length + currentData.length);
newData.set(prefix, 0);
newData.set(currentData, prefix.length);
currentData = newData;
}; break;
default: {
// Unhandled
};
}
}; break;
default: {
// Unhandled
};
}
}
return currentData;
}
processTagValue(name: string, value: string | Uint8Array) {
if (!this.currentSegment?.metadataTags) return;