diff --git a/dev/demux.html b/dev/demux.html
index 7df202b..eff6137 100644
--- a/dev/demux.html
+++ b/dev/demux.html
@@ -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);
diff --git a/src/matroska/ebml.ts b/src/matroska/ebml.ts
index 1d89de4..2704ff7 100644
--- a/src/matroska/ebml.ts
+++ b/src/matroska/ebml.ts
@@ -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[] = [
diff --git a/src/matroska/matroska-demuxer.ts b/src/matroska/matroska-demuxer.ts
index bda341e..89617ce 100644
--- a/src/matroska/matroska-demuxer.ts
+++ b/src/matroska/matroska-demuxer.ts
@@ -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;