/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.mkv.MatroskaIndexParser');
goog.require('shaka.mkv.BlockAddType');
goog.require('shaka.mkv.ContentCompression');
goog.require('shaka.mkv.ElementId');
goog.require('shaka.mkv.MatroskaConstants');
goog.require('shaka.util.BufferUtils');
goog.require('shaka.util.EbmlParser');
goog.require('shaka.util.Error');
goog.requireType('shaka.util.EbmlElement');
/**
* Reads the container metadata needed to index a Matroska file: the track list,
* the timeline scale, the Cues (seek index) and the Chapters. Offsets in the
* results are absolute byte positions in the file, suitable for HTTP range
* requests. The parser never inspects a Cluster payload; see
* shaka.mkv.MatroskaClusterParser for that.
*/
shaka.mkv.MatroskaIndexParser = class {
/**
* Parses the beginning of a file. The data does not need to contain the
* whole header: everything up to the first Cluster (or up to the first
* element the data cuts short) is read, and if that element is the list of
* tracks, the complete tracks that precede the cut.
*
* @param {BufferSource} data Bytes beginning at offset zero in the file.
* @return {shaka.mkv.MatroskaIndexParser.Header}
*/
static parseHeader(data) {
const ElementId = shaka.mkv.ElementId;
const MatroskaIndexParser = shaka.mkv.MatroskaIndexParser;
const bytes = shaka.util.BufferUtils.toUint8(data);
const parser = new shaka.util.EbmlParser(bytes);
const ebml = MatroskaIndexParser.parseTopLevelElement_(
parser, ElementId.EBML);
if (ebml.isPartial()) {
throw MatroskaIndexParser.invalid_();
}
let docType = null;
const ebmlChildren = ebml.createParser();
while (ebmlChildren.hasMoreData()) {
const child = ebmlChildren.parseElement();
if (child.id == ElementId.DOC_TYPE) {
docType = child.getString();
}
}
if (!shaka.mkv.MatroskaConstants.DOC_TYPES.includes(docType || '')) {
throw MatroskaIndexParser.invalid_();
}
const segmentStart = parser.getPosition();
const segment = MatroskaIndexParser.parseTopLevelElement_(
parser, ElementId.SEGMENT);
// SeekHead positions and Cue positions are relative to the first byte of
// the Segment's data.
const segmentOffset = segmentStart + segment.getHeaderSize();
/** @type {shaka.mkv.MatroskaIndexParser.Header} */
const header = {
segmentOffset,
cuesOffset: null,
chaptersOffset: null,
infoEndOffset: null,
mediaEndOffset: null,
duration: null,
timecodeScale: shaka.mkv.MatroskaConstants.DEFAULT_TIMECODE_SCALE,
tracks: [],
firstClusterOffset: null,
};
// Where the SeekHead says the top-level elements other than Clusters live.
const seekPositions = [];
let unscaledDuration = null;
const children = segment.createParser();
while (children.hasMoreData()) {
const position = children.getPosition();
let element;
try {
element = children.parseElement();
} catch (error) {
// The data ended in the middle of an element header.
if (error.code == shaka.util.Error.Code.BUFFER_READ_OUT_OF_BOUNDS) {
break;
}
throw error;
}
const offset = segmentOffset + position;
if (element.id == ElementId.CLUSTER) {
header.firstClusterOffset = offset;
break;
}
if (element.isPartial()) {
// The data ends inside this element. The tracks that precede the end
// are still worth having: the initialization segment of a track stops
// at the end of its TrackEntry.
if (element.id == ElementId.TRACKS) {
header.tracks = MatroskaIndexParser.parseTracks_(
element, offset + element.getHeaderSize());
}
break;
}
switch (element.id) {
case ElementId.SEEK_HEAD:
MatroskaIndexParser.parseSeekHead_(
element, segmentOffset, header, seekPositions);
break;
case ElementId.INFO: {
const info = MatroskaIndexParser.parseInfo_(element);
header.timecodeScale = info.timecodeScale;
unscaledDuration = info.unscaledDuration;
header.infoEndOffset = offset + element.getHeaderSize() +
element.getDeclaredSize();
break;
}
case ElementId.TRACKS:
header.tracks = MatroskaIndexParser.parseTracks_(
element, offset + element.getHeaderSize());
break;
case ElementId.CUES:
header.cuesOffset = offset;
break;
case ElementId.CHAPTERS:
header.chaptersOffset = offset;
break;
}
}
if (!header.tracks.length) {
// Either the Tracks element is not in this data, or the file has none.
// The caller reads more data when firstClusterOffset is still unknown.
if (header.firstClusterOffset != null) {
throw MatroskaIndexParser.invalid_();
}
}
if (header.firstClusterOffset != null) {
const firstCluster = header.firstClusterOffset;
// The media data ends where the first non-Cluster element that follows
// it starts (usually the Cues, at the end of the file).
const later = seekPositions.filter((position) => position > firstCluster);
header.mediaEndOffset = later.length ? Math.min(...later) : null;
}
if (unscaledDuration != null) {
header.duration = unscaledDuration * header.timecodeScale /
shaka.mkv.MatroskaConstants.NANOSECONDS_PER_SECOND;
}
return header;
}
/**
* Gets the last byte of the initialization segment of a track. That is the
* beginning of the file, up to the end of the TrackEntry of the track (and of
* the Info, which holds the timecode scale, should it come later). Nothing
* that follows is needed to tell what the track is.
*
* Besides being small, this makes the initialization segment of each track a
* different byte range, which is how the StreamingEngine knows it has to
* append it again when the track changes.
*
* @param {shaka.mkv.MatroskaIndexParser.Header} header
* @param {!shaka.mkv.MatroskaIndexParser.Track} track
* @return {number} The offset of the last byte, inclusive.
*/
static getInitEndOffset(header, track) {
return Math.max(track.offset + track.size, header.infoEndOffset || 0) - 1;
}
/**
* Parses a complete Cues element fetched with a byte range request.
*
* @param {BufferSource} data
* @param {number} segmentOffset
* @param {number} timecodeScale
* @return {!Array<shaka.mkv.MatroskaIndexParser.CuePoint>}
*/
static parseCues(data, segmentOffset, timecodeScale) {
const ElementId = shaka.mkv.ElementId;
const MatroskaIndexParser = shaka.mkv.MatroskaIndexParser;
const parser = new shaka.util.EbmlParser(data);
const cues = MatroskaIndexParser.parseTopLevelElement_(
parser, ElementId.CUES);
if (cues.isPartial()) {
throw MatroskaIndexParser.invalid_();
}
/** @type {!Array<shaka.mkv.MatroskaIndexParser.CuePoint>} */
const result = [];
const points = cues.createParser();
while (points.hasMoreData()) {
const point = points.parseElement();
if (point.id != ElementId.CUE_POINT) {
continue;
}
let ticks = null;
const positions = [];
const fields = point.createParser();
while (fields.hasMoreData()) {
const field = fields.parseElement();
if (field.id == ElementId.CUE_TIME) {
ticks = field.getUint();
} else if (field.id == ElementId.CUE_TRACK_POSITIONS) {
let track = null;
let position = null;
const values = field.createParser();
while (values.hasMoreData()) {
const value = values.parseElement();
if (value.id == ElementId.CUE_TRACK) {
track = value.getUint();
} else if (value.id == ElementId.CUE_CLUSTER_POSITION) {
position = value.getUint();
}
}
if (track != null && position != null) {
positions.push({track, position});
}
}
}
if (ticks != null) {
const time = ticks * timecodeScale /
shaka.mkv.MatroskaConstants.NANOSECONDS_PER_SECOND;
for (const {track, position} of positions) {
result.push({time, track, offset: segmentOffset + position});
}
}
}
return result;
}
/**
* Reads the total length of a top-level element (header included) from its
* first bytes. The caller can use this to fetch precisely the element with
* one more range request.
*
* @param {BufferSource} data
* @param {shaka.mkv.ElementId} expectedId
* @return {number}
*/
static getElementLength(data, expectedId) {
const MatroskaIndexParser = shaka.mkv.MatroskaIndexParser;
const element = MatroskaIndexParser.parseTopLevelElement_(
new shaka.util.EbmlParser(data), expectedId);
if (element.getDeclaredSize() == Infinity) {
throw MatroskaIndexParser.invalid_();
}
return element.getHeaderSize() + element.getDeclaredSize();
}
/**
* Parses a complete Chapters element fetched with a byte range request.
* Only the first edition is read: Shaka's chapter API has no notion of
* edition selection.
*
* @param {BufferSource} data
* @return {!Array<shaka.mkv.MatroskaIndexParser.Chapter>}
*/
static parseChapters(data) {
const ElementId = shaka.mkv.ElementId;
const MatroskaIndexParser = shaka.mkv.MatroskaIndexParser;
const chapters = MatroskaIndexParser.parseTopLevelElement_(
new shaka.util.EbmlParser(data), ElementId.CHAPTERS);
if (chapters.isPartial()) {
throw MatroskaIndexParser.invalid_();
}
const result = [];
const editions = chapters.createParser();
while (editions.hasMoreData()) {
const edition = editions.parseElement();
if (edition.id != ElementId.EDITION_ENTRY) {
continue;
}
const atoms = edition.createParser();
while (atoms.hasMoreData()) {
const atom = atoms.parseElement();
if (atom.id == ElementId.CHAPTER_ATOM) {
MatroskaIndexParser.parseChapterAtom_(atom, result);
}
}
break;
}
return result;
}
/**
* Parses the element at the parser's position and checks its ID.
*
* @param {!shaka.util.EbmlParser} parser
* @param {shaka.mkv.ElementId} expectedId
* @return {!shaka.util.EbmlElement}
* @private
*/
static parseTopLevelElement_(parser, expectedId) {
let element;
try {
element = parser.parseElement();
} catch (error) {
// A truncated or garbled element header: not something we can read.
throw shaka.mkv.MatroskaIndexParser.invalid_();
}
if (element.id != expectedId) {
throw shaka.mkv.MatroskaIndexParser.invalid_();
}
return element;
}
/**
* @param {!shaka.util.EbmlElement} seekHead
* @param {number} segmentOffset
* @param {shaka.mkv.MatroskaIndexParser.Header} header
* @param {!Array<number>} seekPositions Receives the absolute position of
* every element the SeekHead lists.
* @private
*/
static parseSeekHead_(seekHead, segmentOffset, header, seekPositions) {
const ElementId = shaka.mkv.ElementId;
const seeks = seekHead.createParser();
while (seeks.hasMoreData()) {
const seek = seeks.parseElement();
if (seek.id != ElementId.SEEK) {
continue;
}
let seekId = 0;
let seekPosition = null;
const fields = seek.createParser();
while (fields.hasMoreData()) {
const field = fields.parseElement();
if (field.id == ElementId.SEEK_ID) {
// The ID is stored as its raw bytes, i.e. as a big-endian integer.
seekId = field.getUint();
} else if (field.id == ElementId.SEEK_POSITION) {
seekPosition = field.getUint();
}
}
if (seekPosition == null) {
continue;
}
const position = segmentOffset + seekPosition;
if (seekId == ElementId.CUES) {
header.cuesOffset = position;
} else if (seekId == ElementId.CHAPTERS) {
header.chaptersOffset = position;
}
if (seekId != ElementId.CLUSTER) {
seekPositions.push(position);
}
}
}
/**
* @param {!shaka.util.EbmlElement} info
* @return {{timecodeScale: number, unscaledDuration: ?number}}
* @private
*/
static parseInfo_(info) {
const ElementId = shaka.mkv.ElementId;
let timecodeScale = shaka.mkv.MatroskaConstants.DEFAULT_TIMECODE_SCALE;
let unscaledDuration = null;
const fields = info.createParser();
while (fields.hasMoreData()) {
const field = fields.parseElement();
if (field.id == ElementId.TIMECODE_SCALE) {
timecodeScale = field.getUint();
} else if (field.id == ElementId.DURATION) {
unscaledDuration = field.getFloat();
}
}
if (!Number.isFinite(timecodeScale) || timecodeScale <= 0) {
throw shaka.mkv.MatroskaIndexParser.invalid_();
}
return {timecodeScale, unscaledDuration};
}
/**
* @param {!shaka.util.EbmlElement} tracks
* @param {number} dataOffset Absolute offset of the first byte of |tracks|'s
* data.
* @return {!Array<!shaka.mkv.MatroskaIndexParser.Track>}
* @private
*/
static parseTracks_(tracks, dataOffset) {
const result = [];
const entries = tracks.createParser();
while (entries.hasMoreData()) {
const position = entries.getPosition();
let entry;
try {
entry = entries.parseElement();
} catch (error) {
// The data ends in the middle of the header of an entry.
if (error.code == shaka.util.Error.Code.BUFFER_READ_OUT_OF_BOUNDS) {
break;
}
throw error;
}
if (entry.isPartial()) {
break;
}
if (entry.id == shaka.mkv.ElementId.TRACK_ENTRY) {
result.push(shaka.mkv.MatroskaIndexParser.parseTrackEntry_(
entry, dataOffset + position));
}
}
return result;
}
/**
* @param {!shaka.util.EbmlElement} entry
* @param {number} offset Absolute offset of the TrackEntry element.
* @return {!shaka.mkv.MatroskaIndexParser.Track}
* @private
*/
static parseTrackEntry_(entry, offset) {
const ElementId = shaka.mkv.ElementId;
const MatroskaIndexParser = shaka.mkv.MatroskaIndexParser;
/** @type {!shaka.mkv.MatroskaIndexParser.Track} */
const track = {
offset,
size: entry.getHeaderSize() + entry.getDeclaredSize(),
number: 0,
type: 0,
codecId: '',
codecPrivate: null,
codecDelay: null,
language: shaka.mkv.MatroskaConstants.DEFAULT_LANGUAGE,
name: '',
// Per the specification every flag but FlagDefault... has a default.
enabled: true,
isDefault: true,
forced: false,
hearingImpaired: false,
visualImpaired: false,
original: false,
commentary: false,
width: null,
height: null,
displayWidth: null,
displayHeight: null,
transferCharacteristics: null,
channels: null,
sampleRate: null,
outputSampleRate: null,
defaultDuration: null,
strippedHeader: null,
unsupportedEncoding: false,
dolbyVisionConfig: null,
};
let bcp47Language = null;
const fields = entry.createParser();
while (fields.hasMoreData()) {
const field = fields.parseElement();
switch (field.id) {
case ElementId.TRACK_NUMBER:
track.number = field.getUint();
break;
case ElementId.TRACK_TYPE:
track.type = field.getUint();
break;
case ElementId.CODEC_ID:
track.codecId = field.getString();
break;
case ElementId.CODEC_PRIVATE:
// Copy: the header buffer must not be kept alive by a track.
track.codecPrivate = field.getBytes().slice();
break;
case ElementId.CODEC_DELAY:
track.codecDelay = field.getUint() /
shaka.mkv.MatroskaConstants.NANOSECONDS_PER_SECOND;
break;
case ElementId.DEFAULT_DURATION:
track.defaultDuration = field.getUint() /
shaka.mkv.MatroskaConstants.NANOSECONDS_PER_SECOND;
break;
case ElementId.LANGUAGE:
track.language = field.getString();
break;
case ElementId.LANGUAGE_BCP47:
bcp47Language = field.getString();
break;
case ElementId.TRACK_NAME:
track.name = field.getString();
break;
case ElementId.FLAG_ENABLED:
track.enabled = field.getUint() != 0;
break;
case ElementId.FLAG_DEFAULT:
track.isDefault = field.getUint() != 0;
break;
case ElementId.FLAG_FORCED:
track.forced = field.getUint() != 0;
break;
case ElementId.FLAG_HEARING_IMPAIRED:
track.hearingImpaired = field.getUint() != 0;
break;
case ElementId.FLAG_VISUAL_IMPAIRED:
track.visualImpaired = field.getUint() != 0;
break;
case ElementId.FLAG_ORIGINAL:
track.original = field.getUint() != 0;
break;
case ElementId.FLAG_COMMENTARY:
track.commentary = field.getUint() != 0;
break;
case ElementId.VIDEO:
MatroskaIndexParser.parseVideoSettings_(field, track);
break;
case ElementId.AUDIO:
MatroskaIndexParser.parseAudioSettings_(field, track);
break;
case ElementId.CONTENT_ENCODINGS:
MatroskaIndexParser.parseContentEncodings_(field, track);
break;
case ElementId.BLOCK_ADDITION_MAPPING:
MatroskaIndexParser.parseBlockAdditionMapping_(field, track);
break;
}
}
// The BCP 47 form supersedes the ISO 639-2 one when both are written.
if (bcp47Language) {
track.language = bcp47Language;
}
return track;
}
/**
* @param {!shaka.util.EbmlElement} video
* @param {!shaka.mkv.MatroskaIndexParser.Track} track
* @private
*/
static parseVideoSettings_(video, track) {
const ElementId = shaka.mkv.ElementId;
const fields = video.createParser();
while (fields.hasMoreData()) {
const field = fields.parseElement();
switch (field.id) {
case ElementId.PIXEL_WIDTH:
track.width = field.getUint();
break;
case ElementId.PIXEL_HEIGHT:
track.height = field.getUint();
break;
case ElementId.DISPLAY_WIDTH:
track.displayWidth = field.getUint();
break;
case ElementId.DISPLAY_HEIGHT:
track.displayHeight = field.getUint();
break;
case ElementId.DISPLAY_UNIT:
// Display dimensions only describe an aspect ratio when they are in
// pixels (0). Other units say how big the picture is when shown.
if (field.getUint() != 0) {
track.displayWidth = null;
track.displayHeight = null;
}
break;
case ElementId.COLOUR: {
const colour = field.createParser();
while (colour.hasMoreData()) {
const value = colour.parseElement();
if (value.id == ElementId.TRANSFER_CHARACTERISTICS) {
track.transferCharacteristics = value.getUint();
}
}
break;
}
}
}
}
/**
* @param {!shaka.util.EbmlElement} audio
* @param {!shaka.mkv.MatroskaIndexParser.Track} track
* @private
*/
static parseAudioSettings_(audio, track) {
const ElementId = shaka.mkv.ElementId;
const fields = audio.createParser();
while (fields.hasMoreData()) {
const field = fields.parseElement();
switch (field.id) {
case ElementId.CHANNELS:
track.channels = field.getUint();
break;
case ElementId.SAMPLING_FREQUENCY:
track.sampleRate = field.getFloat();
break;
case ElementId.OUTPUT_SAMPLING_FREQUENCY:
track.outputSampleRate = field.getFloat();
break;
}
}
}
/**
* Reads a BlockAdditionMapping, which says what the additional data of the
* blocks, or the extra data of the track, is. The one that is looked for is
* the Dolby Vision configuration.
*
* @param {!shaka.util.EbmlElement} mapping
* @param {!shaka.mkv.MatroskaIndexParser.Track} track
* @private
*/
static parseBlockAdditionMapping_(mapping, track) {
const ElementId = shaka.mkv.ElementId;
const BlockAddType = shaka.mkv.BlockAddType;
let type = 0;
let extraData = null;
const fields = mapping.createParser();
while (fields.hasMoreData()) {
const field = fields.parseElement();
if (field.id == ElementId.BLOCK_ADD_ID_TYPE) {
type = field.getUint();
} else if (field.id == ElementId.BLOCK_ADD_ID_EXTRA_DATA) {
extraData = field.getBytes().slice();
}
}
if (extraData && (type == BlockAddType.DVCC || type == BlockAddType.DVVC)) {
track.dolbyVisionConfig = extraData;
}
}
/**
* Reads the ContentEncodings of a track. The only encoding that can be
* undone without a decompressor or a key is header stripping, in which the
* muxer removed a common prefix from every frame.
*
* @param {!shaka.util.EbmlElement} encodings
* @param {!shaka.mkv.MatroskaIndexParser.Track} track
* @private
*/
static parseContentEncodings_(encodings, track) {
const ElementId = shaka.mkv.ElementId;
// Defaults of the specification: the encoding applies to the frames only
// and is a compression (0) using zlib (0).
const scopeFrames = 1;
const typeCompression = 0;
const algoZlib = 0;
const list = encodings.createParser();
while (list.hasMoreData()) {
const encoding = list.parseElement();
if (encoding.id != ElementId.CONTENT_ENCODING) {
continue;
}
let scope = scopeFrames;
let type = typeCompression;
let algo = algoZlib;
let settings = null;
const fields = encoding.createParser();
while (fields.hasMoreData()) {
const field = fields.parseElement();
switch (field.id) {
case ElementId.CONTENT_ENCODING_SCOPE:
scope = field.getUint();
break;
case ElementId.CONTENT_ENCODING_TYPE:
type = field.getUint();
break;
case ElementId.CONTENT_COMPRESSION: {
const compression = field.createParser();
while (compression.hasMoreData()) {
const value = compression.parseElement();
if (value.id == ElementId.CONTENT_COMP_ALGO) {
algo = value.getUint();
} else if (value.id == ElementId.CONTENT_COMP_SETTINGS) {
settings = value.getBytes().slice();
}
}
break;
}
}
}
const isHeaderStripping = type == typeCompression &&
algo == shaka.mkv.ContentCompression.HEADER_STRIPPING &&
scope == scopeFrames;
if (isHeaderStripping && !track.strippedHeader) {
track.strippedHeader = settings || new Uint8Array(0);
} else {
track.unsupportedEncoding = true;
}
}
}
/**
* @param {!shaka.util.EbmlElement} atom
* @param {!Array<shaka.mkv.MatroskaIndexParser.Chapter>} result
* @private
*/
static parseChapterAtom_(atom, result) {
const ElementId = shaka.mkv.ElementId;
const MatroskaIndexParser = shaka.mkv.MatroskaIndexParser;
const nanosecondsPerSecond =
shaka.mkv.MatroskaConstants.NANOSECONDS_PER_SECOND;
let start = null;
let end = null;
const displays = [];
const nested = [];
const fields = atom.createParser();
while (fields.hasMoreData()) {
const field = fields.parseElement();
switch (field.id) {
case ElementId.CHAPTER_TIME_START:
start = field.getUint() / nanosecondsPerSecond;
break;
case ElementId.CHAPTER_TIME_END:
end = field.getUint() / nanosecondsPerSecond;
break;
case ElementId.CHAPTER_DISPLAY: {
let title = '';
let language = 'und';
const items = field.createParser();
while (items.hasMoreData()) {
const item = items.parseElement();
if (item.id == ElementId.CHAP_STRING) {
title = item.getString();
} else if (item.id == ElementId.CHAP_LANGUAGE ||
item.id == ElementId.CHAP_LANGUAGE_BCP47) {
language = item.getString();
}
}
if (title) {
displays.push({title, language});
}
break;
}
case ElementId.CHAPTER_ATOM:
nested.push(field);
break;
}
}
if (start != null) {
for (const display of displays) {
result.push({
start,
end,
title: display.title,
language: display.language,
});
}
}
for (const child of nested) {
MatroskaIndexParser.parseChapterAtom_(child, result);
}
}
/** @return {!shaka.util.Error} @private */
static invalid_() {
return new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.MKV_INVALID_FILE);
}
};
/**
* @typedef {{
* offset: number,
* size: number,
* number: number,
* type: number,
* codecId: string,
* codecPrivate: ?Uint8Array,
* codecDelay: ?number,
* language: string,
* name: string,
* enabled: boolean,
* isDefault: boolean,
* forced: boolean,
* hearingImpaired: boolean,
* visualImpaired: boolean,
* original: boolean,
* commentary: boolean,
* width: ?number,
* height: ?number,
* displayWidth: ?number,
* displayHeight: ?number,
* transferCharacteristics: ?number,
* channels: ?number,
* sampleRate: ?number,
* outputSampleRate: ?number,
* defaultDuration: ?number,
* strippedHeader: ?Uint8Array,
* unsupportedEncoding: boolean,
* dolbyVisionConfig: ?Uint8Array,
* }}
*
* @property {number} offset
* Absolute file offset of the TrackEntry element.
* @property {number} size
* Size in bytes of the whole TrackEntry element, header included.
* @property {number} number
* TrackNumber: what the blocks of the track refer to.
* @property {number} type
* TrackType; see shaka.mkv.TrackType.
* @property {?number} codecDelay
* CodecDelay in seconds.
* @property {?number} defaultDuration
* DefaultDuration (the frame duration) in seconds.
* @property {?number} transferCharacteristics
* Video Colour transfer characteristics (H.273 values).
* @property {?Uint8Array} strippedHeader
* The prefix to put back in front of every frame, when the track uses header
* stripping.
* @property {boolean} unsupportedEncoding
* True when the track is compressed or encrypted in a way that cannot be
* undone.
* @property {?Uint8Array} dolbyVisionConfig
* The Dolby Vision configuration record (the payload of dvcC or dvvC).
*/
shaka.mkv.MatroskaIndexParser.Track;
/**
* @typedef {{
* segmentOffset: number,
* cuesOffset: ?number,
* chaptersOffset: ?number,
* infoEndOffset: ?number,
* mediaEndOffset: ?number,
* duration: ?number,
* timecodeScale: number,
* tracks: !Array<!shaka.mkv.MatroskaIndexParser.Track>,
* firstClusterOffset: ?number,
* }}
*
* @property {number} segmentOffset
* Absolute file offset of the first byte of the Segment's data.
* @property {?number} cuesOffset
* Absolute file offset of the Cues element.
* @property {?number} chaptersOffset
* Absolute file offset of the Chapters element.
* @property {?number} infoEndOffset
* Absolute file offset of the byte after the Info element.
* @property {?number} mediaEndOffset
* Absolute file offset where the Clusters end, if known.
* @property {?number} duration
* Duration in seconds.
* @property {number} timecodeScale
* Nanoseconds per timecode tick.
* @property {?number} firstClusterOffset
* Absolute file offset of the first Cluster; null while it has not been
* reached in the data.
*/
shaka.mkv.MatroskaIndexParser.Header;
/**
* @typedef {{time: number, track: number, offset: number}}
*
* @property {number} time
* CueTime in seconds.
* @property {number} track
* CueTrack.
* @property {number} offset
* Absolute file offset of the Cluster the CuePoint refers to.
*/
shaka.mkv.MatroskaIndexParser.CuePoint;
/**
* @typedef {{start: number, end: ?number, title: string, language: string}}
*/
shaka.mkv.MatroskaIndexParser.Chapter;