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;