mediabunny
Version:
Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.
1,513 lines • 73.6 kB
JavaScript
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { extractAv1CodecInfoFromPacket, extractAvcDecoderConfigurationRecord, extractHevcDecoderConfigurationRecord, extractVp9CodecInfoFromPacket, } from '../codec-data.js';
import { extractAudioCodecString, extractVideoCodecString, } from '../codec.js';
import { Demuxer } from '../demuxer.js';
import { InputAudioTrack, InputVideoTrack, } from '../input-track.js';
import { assert, AsyncMutex, binarySearchExact, binarySearchLessOrEqual, COLOR_PRIMARIES_MAP_INVERSE, findLastIndex, insertSorted, isIso639Dash2LanguageCode, last, MATRIX_COEFFICIENTS_MAP_INVERSE, normalizeRotation, roundToPrecision, TRANSFER_CHARACTERISTICS_MAP_INVERSE, UNDETERMINED_LANGUAGE, } from '../misc.js';
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet.js';
import { Reader } from '../reader.js';
import { assertDefinedSize, CODEC_STRING_MAP, EBMLId, EBMLReader, LEVEL_0_AND_1_EBML_IDS, MAX_HEADER_SIZE, MIN_HEADER_SIZE, readVarInt, } from './ebml.js';
import { buildMatroskaMimeType } from './matroska-misc.js';
var BlockLacing;
(function (BlockLacing) {
BlockLacing[BlockLacing["None"] = 0] = "None";
BlockLacing[BlockLacing["Xiph"] = 1] = "Xiph";
BlockLacing[BlockLacing["FixedSize"] = 2] = "FixedSize";
BlockLacing[BlockLacing["Ebml"] = 3] = "Ebml";
})(BlockLacing || (BlockLacing = {}));
const METADATA_ELEMENTS = [
{ id: EBMLId.SeekHead, flag: 'seekHeadSeen' },
{ id: EBMLId.Info, flag: 'infoSeen' },
{ id: EBMLId.Tracks, flag: 'tracksSeen' },
{ id: EBMLId.Cues, flag: 'cuesSeen' },
];
export class MatroskaDemuxer extends Demuxer {
constructor(input) {
super(input);
this.readMetadataPromise = null;
this.segments = [];
this.currentSegment = null;
this.currentTrack = null;
this.currentCluster = null;
this.currentBlock = null;
this.currentCueTime = null;
this.isWebM = false;
this.metadataReader = new EBMLReader(input._mainReader);
// Max 64 MiB of stored clusters
this.clusterReader = new EBMLReader(new Reader(input.source, 64 * 2 ** 20));
}
async computeDuration() {
const tracks = await this.getTracks();
const trackDurations = await Promise.all(tracks.map(x => x.computeDuration()));
return Math.max(0, ...trackDurations);
}
async getTracks() {
await this.readMetadata();
return this.segments.flatMap(segment => segment.tracks.map(track => track.inputTrack));
}
async getMimeType() {
await this.readMetadata();
const tracks = await this.getTracks();
const codecStrings = await Promise.all(tracks.map(x => x.getCodecParameterString()));
return buildMatroskaMimeType({
isWebM: this.isWebM,
hasVideo: this.segments.some(segment => segment.tracks.some(x => x.info?.type === 'video')),
hasAudio: this.segments.some(segment => segment.tracks.some(x => x.info?.type === 'audio')),
codecStrings: codecStrings.filter(Boolean),
});
}
readMetadata() {
return this.readMetadataPromise ??= (async () => {
this.metadataReader.pos = 0;
const fileSize = await this.input.source.getSize();
// Loop over all top-level elements in the file
while (this.metadataReader.pos <= fileSize - MIN_HEADER_SIZE) {
await this.metadataReader.reader.loadRange(this.metadataReader.pos, this.metadataReader.pos + MAX_HEADER_SIZE);
const header = this.metadataReader.readElementHeader();
if (!header) {
break; // Zero padding at the end of the file triggers this, for example
}
const id = header.id;
let size = header.size;
const startPos = this.metadataReader.pos;
if (id === EBMLId.EBML) {
assertDefinedSize(size);
await this.metadataReader.reader.loadRange(this.metadataReader.pos, this.metadataReader.pos + size);
this.readContiguousElements(this.metadataReader, size);
}
else if (id === EBMLId.Segment) { // Segment found!
await this.readSegment(size);
if (size === null) {
// Segment sizes can be undefined (common in livestreamed files), so assume this is the last
// and only segment
break;
}
}
else if (id === EBMLId.Cluster) {
// Clusters are not a top-level element in Matroska, but some files contain a Segment whose size
// doesn't contain any of the clusters that follow it. In the case, we apply the following logic: if
// we find a top-level cluster, attribute it to the previous segment.
if (size === null) {
// Just in case this is one of those weird sizeless clusters, let's do our best and still try to
// determine its size.
const nextElementPos = await this.clusterReader.searchForNextElementId(LEVEL_0_AND_1_EBML_IDS, fileSize);
size = (nextElementPos ?? fileSize) - startPos;
}
const lastSegment = last(this.segments);
if (lastSegment) {
// Extend the previous segment's size
lastSegment.elementEndPos = startPos + size;
}
}
assertDefinedSize(size);
this.metadataReader.pos = startPos + size;
}
})();
}
async readSegment(dataSize) {
const segmentDataStart = this.metadataReader.pos;
this.currentSegment = {
seekHeadSeen: false,
infoSeen: false,
tracksSeen: false,
cuesSeen: false,
timestampScale: -1,
timestampFactor: -1,
duration: -1,
seekEntries: [],
tracks: [],
cuePoints: [],
dataStartPos: segmentDataStart,
elementEndPos: dataSize === null
? await this.input.source.getSize() // Assume it goes until the end of the file
: segmentDataStart + dataSize,
clusterSeekStartPos: segmentDataStart,
clusters: [],
clusterLookupMutex: new AsyncMutex(),
};
this.segments.push(this.currentSegment);
// Let's load a good amount of data, enough for all segment metadata to likely fit into (minus cues)
await this.metadataReader.reader.loadRange(this.metadataReader.pos, this.metadataReader.pos + 2 ** 14);
let clusterEncountered = false;
while (this.metadataReader.pos <= this.currentSegment.elementEndPos - MIN_HEADER_SIZE) {
await this.metadataReader.reader.loadRange(this.metadataReader.pos, this.metadataReader.pos + MAX_HEADER_SIZE);
const elementStartPos = this.metadataReader.pos;
const header = this.metadataReader.readElementHeader();
if (!header) {
break;
}
const { id, size } = header;
const dataStartPos = this.metadataReader.pos;
const metadataElementIndex = METADATA_ELEMENTS.findIndex(x => x.id === id);
if (metadataElementIndex !== -1) {
const field = METADATA_ELEMENTS[metadataElementIndex].flag;
this.currentSegment[field] = true;
assertDefinedSize(size);
await this.metadataReader.reader.loadRange(this.metadataReader.pos, this.metadataReader.pos + size);
this.readContiguousElements(this.metadataReader, size);
}
else if (id === EBMLId.Cluster) {
if (!clusterEncountered) {
clusterEncountered = true;
this.currentSegment.clusterSeekStartPos = elementStartPos;
}
}
if (this.currentSegment.infoSeen && this.currentSegment.tracksSeen && this.currentSegment.cuesSeen) {
// No need to search anymore, we have everything
break;
}
if (this.currentSegment.seekHeadSeen) {
let hasInfo = this.currentSegment.infoSeen;
let hasTracks = this.currentSegment.tracksSeen;
let hasCues = this.currentSegment.cuesSeen;
for (const entry of this.currentSegment.seekEntries) {
if (entry.id === EBMLId.Info) {
hasInfo = true;
}
else if (entry.id === EBMLId.Tracks) {
hasTracks = true;
}
else if (entry.id === EBMLId.Cues) {
hasCues = true;
}
}
if (hasInfo && hasTracks && hasCues) {
// No need to search sequentially anymore, we can use the seek head
break;
}
}
if (size === null) {
break;
}
this.metadataReader.pos = dataStartPos + size;
if (!clusterEncountered) {
this.currentSegment.clusterSeekStartPos = this.metadataReader.pos;
}
}
// Use the seek head to read missing metadata elements
for (const target of METADATA_ELEMENTS) {
if (this.currentSegment[target.flag])
continue;
const seekEntry = this.currentSegment.seekEntries.find(entry => entry.id === target.id);
if (!seekEntry)
continue;
this.metadataReader.pos = segmentDataStart + seekEntry.segmentPosition;
await this.metadataReader.reader.loadRange(this.metadataReader.pos, this.metadataReader.pos + 2 ** 12);
const header = this.metadataReader.readElementHeader();
if (!header)
continue;
const { id, size } = header;
if (id !== target.id)
continue;
assertDefinedSize(size);
this.currentSegment[target.flag] = true;
await this.metadataReader.reader.loadRange(this.metadataReader.pos, this.metadataReader.pos + size);
this.readContiguousElements(this.metadataReader, size);
}
if (this.currentSegment.timestampScale === -1) {
// TimestampScale element is missing. Technically an invalid file, but let's default to the typical value,
// which is 1e6.
this.currentSegment.timestampScale = 1e6;
this.currentSegment.timestampFactor = 1e9 / 1e6;
}
// Put default tracks first
this.currentSegment.tracks.sort((a, b) => Number(b.isDefault) - Number(a.isDefault));
// Sort cue points by cluster position (required for the next algorithm)
this.currentSegment.cuePoints.sort((a, b) => a.clusterPosition - b.clusterPosition);
// Now, let's distribute the cue points to each track. Ideally, each track has their own cue point, but some
// Matroska files may only specify cue points for a single track. In this case, we still wanna use those cue
// points for all tracks.
const allTrackIds = this.currentSegment.tracks.map(x => x.id);
const remainingTrackIds = new Set();
let lastClusterPosition = null;
let lastCuePoint = null;
for (const cuePoint of this.currentSegment.cuePoints) {
if (cuePoint.clusterPosition !== lastClusterPosition) {
for (const id of remainingTrackIds) {
// These tracks didn't receive a cue point for the last cluster, so let's give them one
assert(lastCuePoint);
const track = this.currentSegment.tracks.find(x => x.id === id);
track.cuePoints.push(lastCuePoint);
}
for (const id of allTrackIds) {
remainingTrackIds.add(id);
}
}
lastCuePoint = cuePoint;
if (!remainingTrackIds.has(cuePoint.trackId)) {
continue;
}
const track = this.currentSegment.tracks.find(x => x.id === cuePoint.trackId);
track.cuePoints.push(cuePoint);
remainingTrackIds.delete(cuePoint.trackId);
lastClusterPosition = cuePoint.clusterPosition;
}
for (const id of remainingTrackIds) {
assert(lastCuePoint);
const track = this.currentSegment.tracks.find(x => x.id === id);
track.cuePoints.push(lastCuePoint);
}
for (const track of this.currentSegment.tracks) {
// Sort cue points by time
track.cuePoints.sort((a, b) => a.time - b.time);
}
this.currentSegment = null;
}
async readCluster(segment) {
await this.metadataReader.reader.loadRange(this.metadataReader.pos, this.metadataReader.pos + MAX_HEADER_SIZE);
const elementStartPos = this.metadataReader.pos;
const elementHeader = this.metadataReader.readElementHeader();
assert(elementHeader);
const id = elementHeader.id;
let size = elementHeader.size;
const dataStartPos = this.metadataReader.pos;
if (size === null) {
// The cluster's size is undefined (can happen in livestreamed files). We'd still like to know the size of
// it, so we have no other choice but to iterate over the EBML structure until we find an element at level
// 0 or 1, indicating the end of the cluster (all elements inside the cluster are at level 2).
this.clusterReader.pos = dataStartPos;
const nextElementPos = await this.clusterReader.searchForNextElementId(LEVEL_0_AND_1_EBML_IDS, segment.elementEndPos);
size = (nextElementPos ?? segment.elementEndPos) - dataStartPos;
}
assert(id === EBMLId.Cluster);
// Load the entire cluster
this.clusterReader.pos = dataStartPos;
await this.clusterReader.reader.loadRange(this.clusterReader.pos, this.clusterReader.pos + size);
const cluster = {
elementStartPos,
elementEndPos: dataStartPos + size,
dataStartPos,
timestamp: -1,
trackData: new Map(),
nextCluster: null,
isKnownToBeFirstCluster: false,
};
this.currentCluster = cluster;
this.readContiguousElements(this.clusterReader, size);
for (const [trackId, trackData] of cluster.trackData) {
const track = segment.tracks.find(x => x.id === trackId) ?? null;
// This must hold, as track datas only get created if a block for that track is encountered
assert(trackData.blocks.length > 0);
let blockReferencesExist = false;
let hasLacedBlocks = false;
for (let i = 0; i < trackData.blocks.length; i++) {
const block = trackData.blocks[i];
block.timestamp += cluster.timestamp;
blockReferencesExist ||= block.referencedTimestamps.length > 0;
hasLacedBlocks ||= block.lacing !== BlockLacing.None;
}
if (blockReferencesExist) {
trackData.blocks = sortBlocksByReferences(trackData.blocks);
}
trackData.presentationTimestamps = trackData.blocks
.map((block, i) => ({ timestamp: block.timestamp, blockIndex: i }))
.sort((a, b) => a.timestamp - b.timestamp);
for (let i = 0; i < trackData.presentationTimestamps.length; i++) {
const currentEntry = trackData.presentationTimestamps[i];
const currentBlock = trackData.blocks[currentEntry.blockIndex];
if (trackData.firstKeyFrameTimestamp === null && currentBlock.isKeyFrame) {
trackData.firstKeyFrameTimestamp = currentBlock.timestamp;
}
if (i < trackData.presentationTimestamps.length - 1) {
// Update block durations based on presentation order
const nextEntry = trackData.presentationTimestamps[i + 1];
currentBlock.duration = nextEntry.timestamp - currentBlock.timestamp;
}
else if (currentBlock.duration === 0) {
if (track?.defaultDuration != null) {
if (currentBlock.lacing === BlockLacing.None) {
currentBlock.duration = track.defaultDuration;
}
else {
// Handled by the lace resolution code
}
}
}
}
if (hasLacedBlocks) {
// Perform lace resolution. Here, we expand each laced block into multiple blocks where each contains
// one frame of the lace. We do this after determining block timestamps so we can properly distribute
// the block's duration across the laced frames.
this.expandLacedBlocks(trackData.blocks, track);
// Recompute since blocks have changed
trackData.presentationTimestamps = trackData.blocks
.map((block, i) => ({ timestamp: block.timestamp, blockIndex: i }))
.sort((a, b) => a.timestamp - b.timestamp);
}
const firstBlock = trackData.blocks[trackData.presentationTimestamps[0].blockIndex];
const lastBlock = trackData.blocks[last(trackData.presentationTimestamps).blockIndex];
trackData.startTimestamp = firstBlock.timestamp;
trackData.endTimestamp = lastBlock.timestamp + lastBlock.duration;
if (track) {
insertSorted(track.clusters, cluster, x => x.elementStartPos);
const hasKeyFrame = trackData.firstKeyFrameTimestamp !== null;
if (hasKeyFrame) {
insertSorted(track.clustersWithKeyFrame, cluster, x => x.elementStartPos);
}
}
}
insertSorted(segment.clusters, cluster, x => x.elementStartPos);
this.currentCluster = null;
return cluster;
}
getTrackDataInCluster(cluster, trackNumber) {
let trackData = cluster.trackData.get(trackNumber);
if (!trackData) {
trackData = {
startTimestamp: 0,
endTimestamp: 0,
firstKeyFrameTimestamp: null,
blocks: [],
presentationTimestamps: [],
};
cluster.trackData.set(trackNumber, trackData);
}
return trackData;
}
expandLacedBlocks(blocks, track) {
// https://www.matroska.org/technical/notes.html#block-lacing
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
const originalBlock = blocks[blockIndex];
if (originalBlock.lacing === BlockLacing.None) {
continue;
}
const data = originalBlock.data;
let pos = 0;
const frameSizes = [];
const frameCount = data[pos] + 1;
pos++;
switch (originalBlock.lacing) {
case BlockLacing.Xiph:
{
let totalUsedSize = 0;
// Xiph lacing, just like in Ogg
for (let i = 0; i < frameCount - 1; i++) {
let frameSize = 0;
while (pos < data.length) {
const value = data[pos];
frameSize += value;
pos++;
if (value < 255) {
frameSizes.push(frameSize);
totalUsedSize += frameSize;
break;
}
}
}
// Compute the last frame's size from whatever's left
frameSizes.push(data.length - (pos + totalUsedSize));
}
;
break;
case BlockLacing.FixedSize:
{
// Fixed size lacing: all frames have same size
const totalDataSize = data.length - 1; // Minus the frame count byte
const frameSize = Math.floor(totalDataSize / frameCount);
for (let i = 0; i < frameCount; i++) {
frameSizes.push(frameSize);
}
}
;
break;
case BlockLacing.Ebml:
{
// EBML lacing: first size absolute, subsequent ones are coded as signed differences from the last
const firstResult = readVarInt(data, pos);
let currentSize = firstResult.value;
frameSizes.push(currentSize);
pos += firstResult.width;
let totalUsedSize = currentSize;
for (let i = 1; i < frameCount - 1; i++) {
const diffResult = readVarInt(data, pos);
const unsignedDiff = diffResult.value;
const bias = (1 << (diffResult.width * 7 - 1)) - 1; // Typo-corrected version of 2^((7*n)-1)^-1
const diff = unsignedDiff - bias;
currentSize += diff;
frameSizes.push(currentSize);
pos += diffResult.width;
totalUsedSize += currentSize;
}
// Compute the last frame's size from whatever's left
frameSizes.push(data.length - (pos + totalUsedSize));
}
;
break;
default: assert(false);
}
assert(frameSizes.length === frameCount);
blocks.splice(blockIndex, 1); // Remove the original block
let dataOffset = pos;
// Now, let's insert each frame as its own block
for (let i = 0; i < frameCount; i++) {
const frameSize = frameSizes[i];
const frameData = data.subarray(dataOffset, dataOffset + frameSize);
const blockDuration = originalBlock.duration || (frameCount * (track?.defaultDuration ?? 0));
// Distribute timestamps evenly across the block duration
const frameTimestamp = originalBlock.timestamp + (blockDuration * i / frameCount);
const frameDuration = blockDuration / frameCount;
blocks.splice(blockIndex + i, 0, {
timestamp: frameTimestamp,
duration: frameDuration,
isKeyFrame: originalBlock.isKeyFrame,
referencedTimestamps: originalBlock.referencedTimestamps,
data: frameData,
lacing: BlockLacing.None,
});
dataOffset += frameSize;
}
blockIndex += frameCount; // Skip the blocks we just added
blockIndex--;
}
}
readContiguousElements(reader, totalSize) {
const startIndex = reader.pos;
while (reader.pos - startIndex <= totalSize - MIN_HEADER_SIZE) {
const foundElement = this.traverseElement(reader);
if (!foundElement) {
break;
}
}
}
traverseElement(reader) {
const header = reader.readElementHeader();
if (!header) {
return false;
}
const { id, size } = header;
const dataStartPos = reader.pos;
assertDefinedSize(size);
switch (id) {
case EBMLId.DocType:
{
this.isWebM = reader.readAsciiString(size) === 'webm';
}
;
break;
case EBMLId.Seek:
{
if (!this.currentSegment)
break;
const seekEntry = { id: -1, segmentPosition: -1 };
this.currentSegment.seekEntries.push(seekEntry);
this.readContiguousElements(reader, size);
if (seekEntry.id === -1 || seekEntry.segmentPosition === -1) {
this.currentSegment.seekEntries.pop();
}
}
;
break;
case EBMLId.SeekID:
{
const lastSeekEntry = this.currentSegment?.seekEntries[this.currentSegment.seekEntries.length - 1];
if (!lastSeekEntry)
break;
lastSeekEntry.id = reader.readUnsignedInt(size);
}
;
break;
case EBMLId.SeekPosition:
{
const lastSeekEntry = this.currentSegment?.seekEntries[this.currentSegment.seekEntries.length - 1];
if (!lastSeekEntry)
break;
lastSeekEntry.segmentPosition = reader.readUnsignedInt(size);
}
;
break;
case EBMLId.TimestampScale:
{
if (!this.currentSegment)
break;
this.currentSegment.timestampScale = reader.readUnsignedInt(size);
this.currentSegment.timestampFactor = 1e9 / this.currentSegment.timestampScale;
}
;
break;
case EBMLId.Duration:
{
if (!this.currentSegment)
break;
this.currentSegment.duration = reader.readFloat(size);
}
;
break;
case EBMLId.TrackEntry:
{
if (!this.currentSegment)
break;
this.currentTrack = {
id: -1,
segment: this.currentSegment,
demuxer: this,
clusters: [],
clustersWithKeyFrame: [],
cuePoints: [],
isDefault: false,
inputTrack: null,
codecId: null,
codecPrivate: null,
defaultDuration: null,
languageCode: UNDETERMINED_LANGUAGE,
info: null,
};
this.readContiguousElements(reader, size);
if (this.currentTrack
&& this.currentTrack.id !== -1
&& this.currentTrack.codecId
&& this.currentTrack.info) {
const slashIndex = this.currentTrack.codecId.indexOf('/');
const codecIdWithoutSuffix = slashIndex === -1
? this.currentTrack.codecId
: this.currentTrack.codecId.slice(0, slashIndex);
if (this.currentTrack.info.type === 'video'
&& this.currentTrack.info.width !== -1
&& this.currentTrack.info.height !== -1) {
if (this.currentTrack.codecId === CODEC_STRING_MAP.avc) {
this.currentTrack.info.codec = 'avc';
this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate;
}
else if (this.currentTrack.codecId === CODEC_STRING_MAP.hevc) {
this.currentTrack.info.codec = 'hevc';
this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate;
}
else if (codecIdWithoutSuffix === CODEC_STRING_MAP.vp8) {
this.currentTrack.info.codec = 'vp8';
}
else if (codecIdWithoutSuffix === CODEC_STRING_MAP.vp9) {
this.currentTrack.info.codec = 'vp9';
}
else if (codecIdWithoutSuffix === CODEC_STRING_MAP.av1) {
this.currentTrack.info.codec = 'av1';
}
const videoTrack = this.currentTrack;
const inputTrack = new InputVideoTrack(new MatroskaVideoTrackBacking(videoTrack));
this.currentTrack.inputTrack = inputTrack;
this.currentSegment.tracks.push(this.currentTrack);
}
else if (this.currentTrack.info.type === 'audio'
&& this.currentTrack.info.numberOfChannels !== -1
&& this.currentTrack.info.sampleRate !== -1) {
if (codecIdWithoutSuffix === CODEC_STRING_MAP.aac) {
this.currentTrack.info.codec = 'aac';
this.currentTrack.info.aacCodecInfo = {
isMpeg2: this.currentTrack.codecId.includes('MPEG2'),
};
this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate;
}
else if (this.currentTrack.codecId === CODEC_STRING_MAP.mp3) {
this.currentTrack.info.codec = 'mp3';
}
else if (codecIdWithoutSuffix === CODEC_STRING_MAP.opus) {
this.currentTrack.info.codec = 'opus';
this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate;
}
else if (codecIdWithoutSuffix === CODEC_STRING_MAP.vorbis) {
this.currentTrack.info.codec = 'vorbis';
this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate;
}
else if (codecIdWithoutSuffix === CODEC_STRING_MAP.flac) {
this.currentTrack.info.codec = 'flac';
this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate;
}
else if (this.currentTrack.codecId === 'A_PCM/INT/LIT') {
if (this.currentTrack.info.bitDepth === 8) {
this.currentTrack.info.codec = 'pcm-u8';
}
else if (this.currentTrack.info.bitDepth === 16) {
this.currentTrack.info.codec = 'pcm-s16';
}
else if (this.currentTrack.info.bitDepth === 24) {
this.currentTrack.info.codec = 'pcm-s24';
}
else if (this.currentTrack.info.bitDepth === 32) {
this.currentTrack.info.codec = 'pcm-s32';
}
}
else if (this.currentTrack.codecId === 'A_PCM/INT/BIG') {
if (this.currentTrack.info.bitDepth === 8) {
this.currentTrack.info.codec = 'pcm-u8';
}
else if (this.currentTrack.info.bitDepth === 16) {
this.currentTrack.info.codec = 'pcm-s16be';
}
else if (this.currentTrack.info.bitDepth === 24) {
this.currentTrack.info.codec = 'pcm-s24be';
}
else if (this.currentTrack.info.bitDepth === 32) {
this.currentTrack.info.codec = 'pcm-s32be';
}
}
else if (this.currentTrack.codecId === 'A_PCM/FLOAT/IEEE') {
if (this.currentTrack.info.bitDepth === 32) {
this.currentTrack.info.codec = 'pcm-f32';
}
else if (this.currentTrack.info.bitDepth === 64) {
this.currentTrack.info.codec = 'pcm-f64';
}
}
const audioTrack = this.currentTrack;
const inputTrack = new InputAudioTrack(new MatroskaAudioTrackBacking(audioTrack));
this.currentTrack.inputTrack = inputTrack;
this.currentSegment.tracks.push(this.currentTrack);
}
}
this.currentTrack = null;
}
;
break;
case EBMLId.TrackNumber:
{
if (!this.currentTrack)
break;
this.currentTrack.id = reader.readUnsignedInt(size);
}
;
break;
case EBMLId.TrackType:
{
if (!this.currentTrack)
break;
const type = reader.readUnsignedInt(size);
if (type === 1) {
this.currentTrack.info = {
type: 'video',
width: -1,
height: -1,
rotation: 0,
codec: null,
codecDescription: null,
colorSpace: null,
};
}
else if (type === 2) {
this.currentTrack.info = {
type: 'audio',
numberOfChannels: -1,
sampleRate: -1,
bitDepth: -1,
codec: null,
codecDescription: null,
aacCodecInfo: null,
};
}
}
;
break;
case EBMLId.FlagEnabled:
{
if (!this.currentTrack)
break;
const enabled = reader.readUnsignedInt(size);
if (!enabled) {
this.currentSegment.tracks.pop();
this.currentTrack = null;
}
}
;
break;
case EBMLId.FlagDefault:
{
if (!this.currentTrack)
break;
this.currentTrack.isDefault = !!reader.readUnsignedInt(size);
}
;
break;
case EBMLId.CodecID:
{
if (!this.currentTrack)
break;
this.currentTrack.codecId = reader.readAsciiString(size);
}
;
break;
case EBMLId.CodecPrivate:
{
if (!this.currentTrack)
break;
this.currentTrack.codecPrivate = reader.readBytes(size);
}
;
break;
case EBMLId.DefaultDuration:
{
if (!this.currentTrack)
break;
this.currentTrack.defaultDuration
= this.currentTrack.segment.timestampFactor * reader.readUnsignedInt(size) / 1e9;
}
;
break;
case EBMLId.Language:
{
if (!this.currentTrack)
break;
this.currentTrack.languageCode = reader.readAsciiString(size);
if (!isIso639Dash2LanguageCode(this.currentTrack.languageCode)) {
this.currentTrack.languageCode = UNDETERMINED_LANGUAGE;
}
}
;
break;
case EBMLId.Video:
{
if (this.currentTrack?.info?.type !== 'video')
break;
this.readContiguousElements(reader, size);
}
;
break;
case EBMLId.PixelWidth:
{
if (this.currentTrack?.info?.type !== 'video')
break;
this.currentTrack.info.width = reader.readUnsignedInt(size);
}
;
break;
case EBMLId.PixelHeight:
{
if (this.currentTrack?.info?.type !== 'video')
break;
this.currentTrack.info.height = reader.readUnsignedInt(size);
}
;
break;
case EBMLId.Colour:
{
if (this.currentTrack?.info?.type !== 'video')
break;
this.currentTrack.info.colorSpace = {};
this.readContiguousElements(reader, size);
}
;
break;
case EBMLId.MatrixCoefficients:
{
if (this.currentTrack?.info?.type !== 'video' || !this.currentTrack.info.colorSpace)
break;
const matrixCoefficients = reader.readUnsignedInt(size);
const mapped = MATRIX_COEFFICIENTS_MAP_INVERSE[matrixCoefficients] ?? null;
this.currentTrack.info.colorSpace.matrix = mapped;
}
;
break;
case EBMLId.Range:
{
if (this.currentTrack?.info?.type !== 'video' || !this.currentTrack.info.colorSpace)
break;
this.currentTrack.info.colorSpace.fullRange = reader.readUnsignedInt(size) === 2;
}
;
break;
case EBMLId.TransferCharacteristics:
{
if (this.currentTrack?.info?.type !== 'video' || !this.currentTrack.info.colorSpace)
break;
const transferCharacteristics = reader.readUnsignedInt(size);
const mapped = TRANSFER_CHARACTERISTICS_MAP_INVERSE[transferCharacteristics] ?? null;
this.currentTrack.info.colorSpace.transfer = mapped;
}
;
break;
case EBMLId.Primaries:
{
if (this.currentTrack?.info?.type !== 'video' || !this.currentTrack.info.colorSpace)
break;
const primaries = reader.readUnsignedInt(size);
const mapped = COLOR_PRIMARIES_MAP_INVERSE[primaries] ?? null;
this.currentTrack.info.colorSpace.primaries = mapped;
}
;
break;
case EBMLId.Projection:
{
if (this.currentTrack?.info?.type !== 'video')
break;
this.readContiguousElements(reader, size);
}
;
break;
case EBMLId.ProjectionPoseRoll:
{
if (this.currentTrack?.info?.type !== 'video')
break;
const rotation = reader.readFloat(size);
const flippedRotation = -rotation; // Convert counter-clockwise to clockwise
try {
this.currentTrack.info.rotation = normalizeRotation(flippedRotation);
}
catch {
// It wasn't a valid rotation
}
}
;
break;
case EBMLId.Audio:
{
if (this.currentTrack?.info?.type !== 'audio')
break;
this.readContiguousElements(reader, size);
}
;
break;
case EBMLId.SamplingFrequency:
{
if (this.currentTrack?.info?.type !== 'audio')
break;
this.currentTrack.info.sampleRate = reader.readFloat(size);
}
;
break;
case EBMLId.Channels:
{
if (this.currentTrack?.info?.type !== 'audio')
break;
this.currentTrack.info.numberOfChannels = reader.readUnsignedInt(size);
}
;
break;
case EBMLId.BitDepth:
{
if (this.currentTrack?.info?.type !== 'audio')
break;
this.currentTrack.info.bitDepth = reader.readUnsignedInt(size);
}
;
break;
case EBMLId.CuePoint:
{
if (!this.currentSegment)
break;
this.readContiguousElements(reader, size);
this.currentCueTime = null;
}
;
break;
case EBMLId.CueTime:
{
this.currentCueTime = reader.readUnsignedInt(size);
}
;
break;
case EBMLId.CueTrackPositions:
{
if (this.currentCueTime === null)
break;
assert(this.currentSegment);
const cuePoint = { time: this.currentCueTime, trackId: -1, clusterPosition: -1 };
this.currentSegment.cuePoints.push(cuePoint);
this.readContiguousElements(reader, size);
if (cuePoint.trackId === -1 || cuePoint.clusterPosition === -1) {
this.currentSegment.cuePoints.pop();
}
}
;
break;
case EBMLId.CueTrack:
{
const lastCuePoint = this.currentSegment?.cuePoints[this.currentSegment.cuePoints.length - 1];
if (!lastCuePoint)
break;
lastCuePoint.trackId = reader.readUnsignedInt(size);
}
;
break;
case EBMLId.CueClusterPosition:
{
const lastCuePoint = this.currentSegment?.cuePoints[this.currentSegment.cuePoints.length - 1];
if (!lastCuePoint)
break;
assert(this.currentSegment);
lastCuePoint.clusterPosition = this.currentSegment.dataStartPos + reader.readUnsignedInt(size);
}
;
break;
case EBMLId.Timestamp:
{
if (!this.currentCluster)
break;
this.currentCluster.timestamp = reader.readUnsignedInt(size);
}
;
break;
case EBMLId.SimpleBlock:
{
if (!this.currentCluster)
break;
const trackNumber = reader.readVarInt();
if (trackNumber === null)
break;
const relativeTimestamp = reader.readS16();
const flags = reader.readU8();
const isKeyFrame = !!(flags & 0x80);
const lacing = (flags >> 1) & 0x3; // If the block is laced, we'll expand it later
const trackData = this.getTrackDataInCluster(this.currentCluster, trackNumber);
trackData.blocks.push({
timestamp: relativeTimestamp, // We'll add the cluster's timestamp to this later
duration: 0, // Will set later
isKeyFrame,
referencedTimestamps: [],
data: reader.readBytes(size - (reader.pos - dataStartPos)),
lacing,
});
}
;
break;
case EBMLId.BlockGroup:
{
if (!this.currentCluster)
break;
this.readContiguousElements(reader, size);
if (this.currentBlock) {
for (let i = 0; i < this.currentBlock.referencedTimestamps.length; i++) {
this.currentBlock.referencedTimestamps[i] += this.currentBlock.timestamp;
}
this.currentBlock = null;
}
}
;
break;
case EBMLId.Block:
{
if (!this.currentCluster)
break;
const trackNumber = reader.readVarInt();
if (trackNumber === null)
break;
const relativeTimestamp = reader.readS16();
const flags = reader.readU8();
const lacing = (flags >> 1) & 0x3; // If the block is laced, we'll expand it later
const trackData = this.getTrackDataInCluster(this.currentCluster, trackNumber);
this.currentBlock = {
timestamp: relativeTimestamp, // We'll add the cluster's timestamp to this later
duration: 0, // Will set later
isKeyFrame: true,
referencedTimestamps: [],
data: reader.readBytes(size - (reader.pos - dataStartPos)),
lacing,
};
trackData.blocks.push(this.currentBlock);
}
;
break;
case EBMLId.BlockDuration:
{
if (!this.currentBlock)
break;
this.currentBlock.duration = reader.readUnsignedInt(size);
}
;
break;
case EBMLId.ReferenceBlock:
{
if (!this.currentBlock)
break;
this.currentBlock.isKeyFrame = false;
const relativeTimestamp = reader.readSignedInt(size);
// We'll offset this by the block's timestamp later
this.currentBlock.referencedTimestamps.push(relativeTimestamp);
}
;
break;
}
reader.pos = dataStartPos + size;
return true;
}
}
class MatroskaTrackBacking {
constructor(internalTrack) {
this.internalTrack = internalTrack;
this.packetToClusterLocation = new WeakMap();
}
getId() {
return this.internalTrack.id;
}
getCodec() {
throw new Error('Not implemented on base class.');
}
async computeDuration() {
const lastPacket = await this.getPacket(Infinity, { metadataOnly: true });
return (lastPacket?.timestamp ?? 0) + (lastPacket?.duration ?? 0);
}
getLanguageCode() {
return this.internalTrack.languageCode;
}
async getFirstTimestamp() {
const firstPacket = await this.getFirstPacket({ metadataOnly: true });
return firstPacket?.timestamp ?? 0;
}
getTimeResolution() {
return this.internalTrack.segment.timestampFactor;
}
async getFirstPacket(options) {
return this.performClusterLookup(() => {
const startCluster = this.internalTrack.segment.clusters[0] ?? null;
if (startCluster?.isKnownToBeFirstCluster) {
// Walk from the very first cluster in the file until we find one with our track in it
let currentCluster = startCluster;
while (currentCluster) {
const trackData = currentCluster.trackData.get(this.internalTrack.id);
if (trackData) {
return {
clusterIndex: binarySearchExact(this.internalTrack.clusters, currentCluster.elementStartPos, x => x.elementStartPos),
blockIndex: 0,
correctBlockFound: true,
};
}
currentCluster = currentCluster.nextCluster;
}
}
return {
clusterIndex: -1,
blockIndex: -1,
correctBlockFound: false,
};
}, -Infinity, // Use -Infinity as a search timestamp to avoid using the cues
Infinity, options);
}
intoTimescale(timestamp) {
// Do a little rounding to catch cases where the result is very close to an integer. If it is, it's likely
// that the number was originally an integer divided by the timescale. For stability, it's best
// to return the integer in this case.
return roundToPrecision(timestamp * this.internalTrack.segment.timestampFactor, 14);
}
async getPacket(timestamp, options) {
const timestampInTimescale = this.intoTimescale(timestamp);
return this.performClusterLookup(() => this.findBlockInClustersForTimestamp(timestampInTimescale), timestampInTimescale, timestampInTimescale, options);
}
async getNextPacket(packet, options) {
const locationInCluster = this.packetToClusterLocation.get(packet);
if (locationInCluster === undefined) {
throw new Error('Packet was not created from this track.');
}
const trackData = locationInCluster.cluster.trackData.get(this.internalTrack.id);
const clusterIndex = binarySearchExact(this.internalTrack.clusters, locationInCluster.cluster.elementStartPos, x => x.elementStartPos);
assert(clusterIndex !== -1);
return this.performClusterLookup(() => {
if (locationInCluster.blockIndex + 1 < trackData.blocks.length) {
// We can simply take the next block in the cluster
return {
clusterIndex,
blockIndex: locationInCluster.blockIndex + 1,
correctBlockFound: true,
};
}
else {
// Walk the list of clusters until we find the next cluster for this track
let currentCluster = locationInCluster.cluster;
while (currentCluster.nextCluster) {
currentCluster = currentCluster.nextCluster;
const trackData = currentCluster.trackData.get(this.internalTrack.id);
if (trackData) {
const clusterIndex = binarySearchExact(this.internalTrack.clusters, currentCluster.elementStartPos, x => x.elementStartPos);
assert(clusterIndex !== -1);
return {
clusterIndex,
blockIndex: 0,
correctBlockFound: true,
};
}
}
return {
clusterIndex,
blockIndex: -1,
correctBlockFound: false,
};
}
}, -Infinity, // Use -Infinity as a search timestamp to avoid using the cues
Infinity, options);
}
async getKeyPacket(timestamp, options) {
const timestampInTimescale = this.intoTimescale(timestamp);
return this.performClusterLookup(() => this.findKeyBlockInClustersForTimestamp(timestampInTimescale), timestampInTimescale, timestampInTimescale, options);
}
async getNextKeyPacket(packet, options) {
const locationInCluster = this.packetToClusterLocation.get(packet);
if (locationInCluster === undefined) {
throw new Error('Packet was not created from this track.');
}
const trackData = locationInCluster.cluster.trackData.get(this.internalTrack.id);
const clusterIndex = binarySearchExact(this.internalTrack.clusters, locationInCluster.cluster.elementStartPos, x => x.elementStartPos);
assert(clusterIndex !== -1);
return this.performClusterLookup(() => {
const nextKeyFrameIndex = trackData.blocks.findIndex((x, i) => x.isKeyFrame && i > locationInCluster.blockIndex);
if (nextKeyFrameIndex !== -1) {
// We can simply take the next key frame in the cluster
return {
clusterIndex,
blockIndex: nextKeyFrameIndex,
correctBlockFound: true,
};
}
else {
// Walk the list of clusters until we find the next cluster for this track with a key frame
let currentCluster = locationInCluster.cluster;
while (currentCluster.nextCluster) {
currentCluster = currentCluster.nextCluster;
const trackData = currentCluster.trackData.get(this.internalTrack.id);
if (trackData && trackData.firstKeyFrameTimestamp !== null) {
const clusterIndex = binarySearchExact(this.internalTrack.clusters, currentCluster.elementStartPos, x => x.elementStartPos);
assert(clusterIndex !== -1);
const keyFrameIndex = trackData.blocks.findIndex(x => x.isKeyFrame);
assert(keyFrameIndex !== -1); // There must be one
return {
clusterIndex,
blockIndex: keyFrameIndex,
correctBlockFound: true,
};
}
}
return {
clusterIndex,
blockIndex: -1,
correctBlockFound: false,
};
}
}, -Infinity, // Use -Infinity as a search timestamp to avoid using the cues
Infinity, options);
}
async fetchPacketInCluster(cluster, blockIndex, options) {
if (blockIndex === -1) {
return null;
}
const trackData = cluster.trackData.get(this.internalTrack.id);
const block = trackData.blocks[blockIndex];
assert(block);
const data = options.metadataOnly ? PLACEHOLDER_DATA : block.data;
const timestamp = block.timestamp / this.internalTrack.segment.timestampFactor;
const duration = block.duration / this.internalTrack.segment.timestampFactor;
const packet = new EncodedPacket(data, block.isKeyFrame ? 'key' : 'delta', timestamp, duration, cluster.dataStartPos + blockIndex, block.data.byteLength);
this.packetToClusterLocation.set(packet, { cluster, blockIndex });
return packet;
}
findBlockInClustersForTimestamp(timestampInTimescale) {
const clusterIndex = binarySearchLessOrEqual(
// This array is technically not sorted by start timestamp, but for any reasonable file, it basically is.
this.internalTrack.clusters, timestampInTimescale, x => x.trackData.get(this.internalTrack.id).startTimestamp);
let blockIndex = -1;
let correctBlockFound = false;
if (clusterIndex !== -1) {
const cluster = this.internalTrack.clusters[clusterIndex];
const trackData = cluster.trackData.get(this.internalTrack.id);
const index = binarySearchLessOrEqual(trackData.presentationTimestamps, timestampInTimescale, x => x.timestamp);
assert(index !== -1);
blockIndex = trackData.presentationTimestamps[index].blockIndex;
correctBlockFound = timestampInTimescale < trackData.endTimestamp;
}
return { clusterIndex, blockIndex, correctBlockFound };
}
findKeyBlockInClustersForTimestamp(timestampInTimescale) {
const indexInKeyFrameClusters = binarySearchLessOrEqual(
// This array is technically not sorted by start timestamp, but for any reasonable file, it basically is.
this.internalTrack.clustersWithKeyFrame, timestampInTimescale, x => x.trackData.get(this.internalTrack.id).firstKeyFrameTimestamp);
let clusterIndex = -1;
let blockIndex = -1;
let correctBlockFound = false;
if (indexInKeyFrameClusters !== -1) {
const cluster = this.internalTrack.clustersWithKeyFrame[indexInKeyFrameClusters];
// Now, let's find the actual index of the cluster in the list of ALL clusters, not just key frame ones
clusterIndex = binarySearchExact(this.internalTrack.clusters, cluster.elementStartPos, x => x.elementStartPos);
assert(clusterIndex !== -1);
const trackData = cluster.trackData.get(this.internalTrack.id);
const index = findLastIndex(trackData.presentationTimestamps, (x) => {
const block = trackData.blocks[x.blockIndex];
return block.isKeyFrame && x.timestamp <= timestampInTimescale;
});
assert(index !== -1); // It's a key frame cluster, so there must be a key frame
const entry = trackData.presentationTimestamps[index];
blockIndex = entry.blockIndex;
correctBlockFound = timestampInTimescale < trackData.endTimestamp;
}
return { clusterIndex, blockIndex, correctBlockFound };
}
/** Looks for a packet in the clusters while trying to load as few clusters as possible to retrieve it. */
async performClusterLookup(
// This function returns the best-matching block that is currently loaded. Based on this information, we know
// which clusters we need to load to find the actual match.
getBestMatch,
// The timestamp with which we can search the lookup table
searchTimestamp,
// The timestamp for which we know the correct block will not come after it
latestTimestamp, options) {
const { demuxer, segment } = this.internalTrack;
const release = await segment.clusterLookupMutex.acquire(); // The algorithm requires exclusivity
try {
const { clusterIndex, blockIndex, correctBlockFound } = getBestMatch();
if (correctBlockFound) {
// The correct block already exists, easy path.
const cluster = this.internalTrack.clusters[clusterIndex];
return this.fetchPacketInCluster(cluster, blockIndex, options);
}
// We use the metadata reader to find the cluster, but the cluster reader to load the cluster
const metadataReader = demuxer.metadataReader;
const clusterReader = demuxer.clusterReader;
let prevCluster = null;
let bestClusterIndex = clusterIndex;
let bestBlockIndex = blockIndex;
// Search for a cue point; this way, we won't need to start searching from the start of the file
// but can jump right into the correct cluster (or at least nearby).
const cuePointIndex = binarySearchLessOrEqual(this.internalTrack.cuePoints, searchTimestamp, x => x.time);
const cuePoint = cuePointIndex !== -1 ? this.internalTrack.cuePoints[cuePointIndex] : null;
let nextClusterIsFirstCluster = false;
if (clusterIndex === -1) {
metadataReader.pos = cuePoint?.clusterPosition ?? segment.clusterSeekStartPos;
nextClusterIsFirstCluster = metadataReader.pos === segment.clusterSeekStartPos;
}
else {
const cluster = this.internalTrack.clusters[clusterIndex];
if (!cuePoint || cluster.elementStartPos >= cuePoint.clusterPosition) {
metadataReader.pos = cluster.elementEndPos;
prevCluster = cluster;
}
else {
// Use the lookup entry
metadataReader.pos = cuePoint.clusterPosition;
}
}
while (metadataReader.pos <= segment.elementEndPos - MIN_HEADER_SIZE) {
if (prevCluster) {
const trackData = prevCluster.trackData.get(this.internalTrack.id);
if (trackData && trackData.startTimestamp > latestTimestamp) {
// We're already past the upper bound, no need to keep searching
break;
}
if (prevCluster.nextCluster) {
// Skip ahead quickly without needing to read the file again
metadataReader.pos = prevCluster.nextCluster.elementEndPos;
prevCluster = prevCluster.nextCluster;
continue;
}
}
// Load the header
await metadataReader.reader.loadRange(metadataReader.pos, metadataReader.pos + MAX_HEADER_SIZE);
const elementStartPos = metadataReader.pos;
const elementHeader = metadataReader.readElementHeader();
if (!elementHeader) {
break;
}
const id = elementHeader.id;
let size = elementHeader.size;
const dataStartPos = metadataReader.pos;
if (id === EBMLId.Cluster) {
const index = binarySearchExact(segment.clusters, elementStartPos, x => x.elementStartPos);
let cluster;
if (index === -1) {
// This is the first time we've seen this cluster
metadataReader.pos = elementStartPos;
cluster = await demuxer.readCluster(segment);
}
else {
// We already know this cluster
cluster = segment.clusters[index];
}
// Even if we already know the cluster, we might not yet know its predecessor, so always do this
if (prevCluster)
prevCluster.nextCluster = cluster;
prevCluster = cluster;
if (nextClusterIsFirstCluster) {
cluster.isKnownToBeFirstCluster = true;
nextClusterIsFirstCluster = false;
}
const { clusterIndex, blockIndex, correctBlockFound } = getBestMatch();
if (correctBlockFound) {
const cluster = this.internalTrack.clusters[clusterIndex];
return this.fetchPacketInCluster(cluster, blockIndex, options);
}
if (clusterIndex !== -1) {
bestClusterIndex = clusterIndex;
bestBlockIndex = blockIndex;
}
}
if (size === null) {
// Undefined element size (can happen in livestreamed files). In this case, we need to do some
// searching to determine the actual size of the element.
if (id === EBMLId.Cluster) {
// The cluster should have already computed its length, we can just copy that result
assert(prevCluster);
size = prevCluster.elementEndPos - dataStartPos;
}
else {
// Search for the next element at level 0 or 1
clusterReader.pos = dataStartPos;
const nextElementPos = await clusterReader.searchForNextElementId(LEVEL_0_AND_1_EBML_IDS, segment.elementEndPos);
size = (nextElementPos ?? segment.elementEndPos) - dataStartPos;
}
const endPos = dataStartPos + size;
if (endPos > segment.elementEndPos - MIN_HEADER_SIZE) {
// No more elements fit in this segment
break;
}
else {
// Check the next element. If it's a new segment, we know this segment ends here. The new
// segment is just ignored, since we're likely in a livestreamed file and thus only care about
// the first segment.
clusterReader.pos = endPos;
const elementId = clusterReader.readElementId();
if (elementId === EBMLId.Segment) {
segment.elementEndPos = endPos;
break;
}
}
}
metadataReader.pos = dataStartPos + size;
}
let result = null;
const bestCluster = bestClusterIndex !== -1 ? this.internalTrack.clusters[bestClusterIndex] : null;
if (bestCluster) {
// If we finished looping but didn't find a perfect match, still return the best match we found
result = await this.fetchPacketInCluster(bestCluster, bestBlockIndex, options);
}
// Catch faulty cue points
if (!result && cuePoint && (!bestCluster || bestCluster.elementStartPos < cuePoint.clusterPosition)) {
// The cue point lied to us! We found a cue point but no cluster there that satisfied the match. In this
// case, let's search again but using the cue point before that.
const previousCuePoint = this.internalTrack.cuePoints[cuePointIndex - 1];
const newSearchTimestamp = previousCuePoint?.time ?? -Infinity;
return this.performClusterLookup(getBestMatch, newSearchTimestamp, latestTimestamp, options);
}
return result;
}
finally {
release();
}
}
}
class MatroskaVideoTrackBacking extends MatroskaTrackBacking {
constructor(internalTrack) {
super(internalTrack);
this.decoderConfigPromise = null;
this.internalTrack = internalTrack;
}
getCodec() {
return this.internalTrack.info.codec;
}
getCodedWidth() {
return this.internalTrack.info.width;
}
getCodedHeight() {
return this.internalTrack.info.height;
}
getRotation() {
return this.internalTrack.info.rotation;
}
async getColorSpace() {
return {
primaries: this.internalTrack.info.colorSpace?.primaries,
transfer: this.internalTrack.info.colorSpace?.transfer,
matrix: this.internalTrack.info.colorSpace?.matrix,
fullRange: this.internalTrack.info.colorSpace?.fullRange,
};
}
async getDecoderConfig() {
if (!this.internalTrack.info.codec) {
return null;
}
return this.decoderConfigPromise ??= (async () => {
let firstPacket = null;
const needsPacketForAdditionalInfo = this.internalTrack.info.codec === 'vp9'
|| this.internalTrack.info.codec === 'av1'
// Packets are in Annex B format:
|| (this.internalTrack.info.codec === 'avc' && !this.internalTrack.info.codecDescription)
// Packets are in Annex B format:
|| (this.internalTrack.info.codec === 'hevc' && !this.internalTrack.info.codecDescription);
if (needsPacketForAdditionalInfo) {
firstPacket = await this.getFirstPacket({});
}
return {
codec: extractVideoCodecString({
width: this.internalTrack.info.width,
height: this.internalTrack.info.height,
codec: this.internalTrack.info.codec,
codecDescription: this.internalTrack.info.codecDescription,
colorSpace: this.internalTrack.info.colorSpace,
avcCodecInfo: this.internalTrack.info.codec === 'avc' && firstPacket
? extractAvcDecoderConfigurationRecord(firstPacket.data)
: null,
hevcCodecInfo: this.internalTrack.info.codec === 'hevc' && firstPacket
? extractHevcDecoderConfigurationRecord(firstPacket.data)
: null,
vp9CodecInfo: this.internalTrack.info.codec === 'vp9' && firstPacket
? extractVp9CodecInfoFromPacket(firstPacket.data)
: null,
av1CodecInfo: this.internalTrack.info.codec === 'av1' && firstPacket
? extractAv1CodecInfoFromPacket(firstPacket.data)
: null,
}),
codedWidth: this.internalTrack.info.width,
codedHeight: this.internalTrack.info.height,
description: this.internalTrack.info.codecDescription ?? undefined,
colorSpace: this.internalTrack.info.colorSpace ?? undefined,
};
})();
}
}
class MatroskaAudioTrackBacking extends MatroskaTrackBacking {
constructor(internalTrack) {
super(internalTrack);
this.decoderConfig = null;
this.internalTrack = internalTrack;
}
getCodec() {
return this.internalTrack.info.codec;
}
getNumberOfChannels() {
return this.internalTrack.info.numberOfChannels;
}
getSampleRate() {
return this.internalTrack.info.sampleRate;
}
async getDecoderConfig() {
if (!this.internalTrack.info.codec) {
return null;
}
return this.decoderConfig ??= {
codec: extractAudioCodecString({
codec: this.internalTrack.info.codec,
codecDescription: this.internalTrack.info.codecDescription,
aacCodecInfo: this.internalTrack.info.aacCodecInfo,
}),
numberOfChannels: this.internalTrack.info.numberOfChannels,
sampleRate: this.internalTrack.info.sampleRate,
description: this.internalTrack.info.codecDescription ?? undefined,
};
}
}
/** Sorts blocks such that referenced blocks come before the blocks that reference them. */
const sortBlocksByReferences = (blocks) => {
const timestampToBlock = new Map();
for (let i = 0; i < blocks.length; i++) {
const block = blocks[i];
timestampToBlock.set(block.timestamp, block);
}
const processedBlocks = new Set();
const result = [];
const processBlock = (block) => {
if (processedBlocks.has(block)) {
return;
}
// Marking the block as processed here already; prevents this algorithm from dying on cycles
processedBlocks.add(block);
for (let j = 0; j < block.referencedTimestamps.length; j++) {
const timestamp = block.referencedTimestamps[j];
const otherBlock = timestampToBlock.get(timestamp);
if (!otherBlock) {
continue;
}
processBlock(otherBlock);
}
result.push(block);
};
for (let i = 0; i < blocks.length; i++) {
processBlock(blocks[i]);
}
return result;
};