UNPKG

shaka-player

Version:
1,059 lines (967 loc) 27.1 kB
/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ goog.provide('shaka.util.Mp4BoxParsers'); goog.require('shaka.util.DataViewReader'); goog.require('shaka.util.Mp4Parser'); goog.require('shaka.util.Uint8ArrayUtils'); shaka.util.Mp4BoxParsers = class { /** * Parses a TFHD Box. * @param {!shaka.util.DataViewReader} reader * @param {number} flags * @return {!shaka.util.ParsedTFHDBox} */ static parseTFHD(reader, flags) { let defaultSampleDuration = null; let defaultSampleSize = null; let baseDataOffset = null; let sampleDescriptionIndex = null; const trackId = reader.readUint32(); // Read "track_ID" // Read "base_data_offset" if present. if (flags & 0x000001) { baseDataOffset = reader.readUint64(); } // Read "sample_description_index" if present. if (flags & 0x000002) { sampleDescriptionIndex = reader.readUint32(); } // Read "default_sample_duration" if present. if (flags & 0x000008) { defaultSampleDuration = reader.readUint32(); } // Read "default_sample_size" if present. if (flags & 0x000010) { defaultSampleSize = reader.readUint32(); } return { trackId, defaultSampleDuration, defaultSampleSize, baseDataOffset, sampleDescriptionIndex, }; } /** * Parses a TFDT Box. * @param {!shaka.util.DataViewReader} reader * @param {number} version * @return {!shaka.util.ParsedTFDTBox} */ static parseTFDT(reader, version) { const baseMediaDecodeTime = version == 1 ? reader.readUint64() : reader.readUint32(); return { baseMediaDecodeTime, }; } /** * Parses a TFDT Box, with a loss of precision beyond 53 bits. * Use only when exact integers are not required, e.g. when * dividing by the timescale. * * @param {!shaka.util.DataViewReader} reader * @param {number} version * @return {!shaka.util.ParsedTFDTBox} */ static parseTFDTInaccurate(reader, version) { if (version == 1) { const high = reader.readUint32(); const low = reader.readUint32(); return { baseMediaDecodeTime: (high * Math.pow(2, 32)) + low, }; } else { return { baseMediaDecodeTime: reader.readUint32(), }; } } /** * Parses a PRFT Box, with a loss of precision beyond 53 bits. * Use only when exact integers are not required, e.g. when * dividing by the timescale. * * @param {!shaka.util.DataViewReader} reader * @param {number} version * @return {!shaka.util.ParsedPRFTBox} */ static parsePRFTInaccurate(reader, version) { reader.readUint32(); // Ignore referenceTrackId const ntpTimestampSec = reader.readUint32(); const ntpTimestampFrac = reader.readUint32(); const ntpTimestamp = ntpTimestampSec * 1000 + ntpTimestampFrac / 2**32 * 1000; let mediaTime; if (version === 0) { mediaTime = reader.readUint32(); } else { const high = reader.readUint32(); const low = reader.readUint32(); mediaTime = (high * Math.pow(2, 32)) + low; } return { mediaTime, ntpTimestamp, }; } /** * Parses a MDHD Box. * @param {!shaka.util.DataViewReader} reader * @param {number} version * @return {!shaka.util.ParsedMDHDBox} */ static parseMDHD(reader, version) { if (version == 1) { reader.skip(8); // Skip "creation_time" reader.skip(8); // Skip "modification_time" } else { reader.skip(4); // Skip "creation_time" reader.skip(4); // Skip "modification_time" } const timescale = reader.readUint32(); reader.skip(4); // Skip "duration" const language = reader.readUint16(); // language is stored as an ISO-639-2/T code in an array of three // 5-bit fields each field is the packed difference between its ASCII // value and 0x60 const languageString = String.fromCharCode((language >> 10) + 0x60) + String.fromCharCode(((language & 0x03c0) >> 5) + 0x60) + String.fromCharCode((language & 0x1f) + 0x60); return { timescale, language: languageString, }; } /** * Parses a TREX Box. * @param {!shaka.util.DataViewReader} reader * @return {!shaka.util.ParsedTREXBox} */ static parseTREX(reader) { reader.skip(4); // Skip "track_ID" reader.skip(4); // Skip "default_sample_description_index" const defaultSampleDuration = reader.readUint32(); const defaultSampleSize = reader.readUint32(); return { defaultSampleDuration, defaultSampleSize, }; } /** * Parses a TRUN Box. * @param {!shaka.util.DataViewReader} reader * @param {number} version * @param {number} flags * @return {!shaka.util.ParsedTRUNBox} */ static parseTRUN(reader, version, flags) { const sampleCount = reader.readUint32(); const sampleData = []; let dataOffset = null; // "data_offset" if (flags & 0x000001) { dataOffset = reader.readInt32(); } // Skip "first_sample_flags" if present. if (flags & 0x000004) { reader.skip(4); } for (let i = 0; i < sampleCount; i++) { /** @type {shaka.util.ParsedTRUNSample} */ const sample = { sampleDuration: null, sampleSize: null, sampleCompositionTimeOffset: null, }; // Read "sample duration" if present. if (flags & 0x000100) { sample.sampleDuration = reader.readUint32(); } // Read "sample_size" if present. if (flags & 0x000200) { sample.sampleSize = reader.readUint32(); } // Skip "sample_flags" if present. if (flags & 0x000400) { reader.skip(4); } // Read "sample_time_offset" if present. if (flags & 0x000800) { sample.sampleCompositionTimeOffset = version == 0 ? reader.readUint32() : reader.readInt32(); } sampleData.push(sample); } return { sampleCount, sampleData, dataOffset, }; } /** * Parses a TKHD Box. * @param {!shaka.util.DataViewReader} reader * @param {number} version * @return {!shaka.util.ParsedTKHDBox} */ static parseTKHD(reader, version) { if (version == 1) { reader.skip(8); // Skip "creation_time" reader.skip(8); // Skip "modification_time" } else { reader.skip(4); // Skip "creation_time" reader.skip(4); // Skip "modification_time" } const trackId = reader.readUint32(); if (version == 1) { reader.skip(8); // Skip "reserved" } else { reader.skip(4); // Skip "reserved" } reader.skip(4); // Skip "duration" reader.skip(8); // Skip "reserved" reader.skip(2); // Skip "layer" reader.skip(2); // Skip "alternate_group" reader.skip(2); // Skip "volume" reader.skip(2); // Skip "reserved" reader.skip(36); // Skip "matrix_structure" const width = reader.readUint16() + (reader.readUint16() / 16); const height = reader.readUint16() + (reader.readUint16() / 16); return { trackId, width, height, }; } /** * Parses an visual sample entry box. * @param {!shaka.util.DataViewReader} reader * @return {!shaka.util.ParsedVisualSampleEntryBox} */ static visualSampleEntry(reader) { // Skip 6 reserved bytes. // Skip 2-byte data reference index. // Skip 16 more reserved bytes. reader.skip(24); // 4 bytes for width/height. const width = reader.readUint16(); const height = reader.readUint16(); // Skip 8 bytes for horizontal/vertical resolution. // Skip 4 more reserved bytes (0) // Skip 2-byte frame count. // Skip 32-byte compressor name (length byte, then name, then 0-padding). // Skip 2-byte depth. // Skip 2 more reserved bytes (0xff) reader.skip(50); return { width: width, height: height, }; } /** * Parses an audio sample entry box. * @param {!shaka.util.DataViewReader} reader * @return {!shaka.util.ParsedAudioSampleEntryBox} */ static audioSampleEntry(reader) { reader.skip(6); // Skip "reserved" reader.skip(2); // Skip "data_reference_index" reader.skip(8); // Skip "reserved" const channelCount = reader.readUint16(); reader.skip(2); // Skip "sample_size" reader.skip(2); // Skip "pre_defined" reader.skip(2); // Skip "reserved" const sampleRate = reader.readUint16() + (reader.readUint16() / 65536); return { channelCount, sampleRate, }; } /** * Parses a ESDS box. * @param {!shaka.util.DataViewReader} reader * @return {!shaka.util.ParsedESDSBox} */ static parseESDS(reader) { let codec = 'mp4a'; let tag; let oti; while (reader.hasMoreData()) { tag = reader.readUint8(); let byteRead = reader.readUint8(); while (byteRead & 0x80) { byteRead = reader.readUint8(); } if (tag == 0x03) { reader.readUint16(); const flags = reader.readUint8(); if (flags & 0x80) { reader.readUint16(); } if (flags & 0x40) { reader.skip(reader.readUint8()); } if (flags & 0x20) { reader.readUint16(); } } else if (tag == 0x04) { oti = reader.readUint8(); reader.skip(12); } else if (tag == 0x05) { break; } } if (oti) { codec += '.' + shaka.util.Mp4BoxParsers.toHex_(oti); if (tag == 0x05 && reader.hasMoreData()) { const firstData = reader.readUint8(); let audioObjectType = (firstData & 0xF8) >> 3; if (audioObjectType === 31 && reader.hasMoreData()) { audioObjectType = 32 + ((firstData & 0x7) << 3) + ((reader.readUint8() & 0xE0) >> 5); } codec += '.' + audioObjectType; } } return {codec}; } /** * Parses a AVCC box. * @param {string} codecBase * @param {!shaka.util.DataViewReader} reader * @param {string} boxName * @return {!shaka.util.ParsedAVCCBox} */ static parseAVCC(codecBase, reader, boxName) { const Mp4BoxParsers = shaka.util.Mp4BoxParsers; reader.skip(1); // Skip "configurationVersion" const codec = codecBase + '.' + Mp4BoxParsers.toHex_(reader.readUint8()) + Mp4BoxParsers.toHex_(reader.readUint8()) + Mp4BoxParsers.toHex_(reader.readUint8()); return {codec}; } /** * Parses a HVCC box. * @param {string} codecBase * @param {!shaka.util.DataViewReader} reader * @param {string} boxName * @return {!shaka.util.ParsedHVCCBox} */ static parseHVCC(codecBase, reader, boxName) { const Mp4BoxParsers = shaka.util.Mp4BoxParsers; reader.skip(1); // Skip "configurationVersion" const profileByte = reader.readUint8(); const profileSpace = ['', 'A', 'B', 'C'][profileByte >> 6]; const generalProfileIdc = profileByte & 0x1f; const profileCompat = reader.readUint32(); const tierFlag = (profileByte & 0x20) >> 5 ? 'H' : 'L'; const constraintIndicator = [ reader.readUint8(), reader.readUint8(), reader.readUint8(), reader.readUint8(), reader.readUint8(), reader.readUint8(), ]; const levelIDC = reader.readUint8(); let codec = codecBase; codec += '.' + profileSpace + generalProfileIdc; codec += '.' + Mp4BoxParsers.toHex_( Mp4BoxParsers.reverseBits_(profileCompat), /* removeInitialZero= */ true); codec += '.' + tierFlag + levelIDC; let constraintString = ''; for (let i = constraintIndicator.length; i--; ) { const byte = constraintIndicator[i]; if (byte || constraintString) { const encodedByte = byte.toString(16).toUpperCase(); constraintString = '.' + encodedByte + constraintString; } } codec += constraintString; return {codec}; } /** * Parses a DVCC box. * @param {string} codecBase * @param {!shaka.util.DataViewReader} reader * @param {string} boxName * @return {!shaka.util.ParsedDVCCBox} */ static parseDVCC(codecBase, reader, boxName) { const Mp4BoxParsers = shaka.util.Mp4BoxParsers; reader.skip(2); // Skip "dv_version_major" and "dv_version_minor" const thirdByte = reader.readUint8(); const fourthByte = reader.readUint8(); const profile = (thirdByte >> 1) & 0x7f; const level = ((thirdByte << 5) & 0x20) | ((fourthByte >> 3) & 0x1f); const codec = codecBase + '.' + Mp4BoxParsers.addLeadingZero_(profile) + '.' + Mp4BoxParsers.addLeadingZero_(level); return {codec}; } /** * Parses a DVVC box. * @param {string} codecBase * @param {!shaka.util.DataViewReader} reader * @param {string} boxName * @return {!shaka.util.ParsedDVVCBox} */ static parseDVVC(codecBase, reader, boxName) { const Mp4BoxParsers = shaka.util.Mp4BoxParsers; reader.skip(2); // Skip "dv_version_major" and "dv_version_minor" const thirdByte = reader.readUint8(); const fourthByte = reader.readUint8(); const profile = (thirdByte >> 1) & 0x7f; const level = ((thirdByte << 5) & 0x20) | ((fourthByte >> 3) & 0x1f); const codec = codecBase + '.' + Mp4BoxParsers.addLeadingZero_(profile) + '.' + Mp4BoxParsers.addLeadingZero_(level); return {codec}; } /** * Parses a VPCC box. * @param {string} codecBase * @param {!shaka.util.DataViewReader} reader * @param {string} boxName * @return {!shaka.util.ParsedVPCCBox} */ static parseVPCC(codecBase, reader, boxName) { const Mp4BoxParsers = shaka.util.Mp4BoxParsers; const profile = reader.readUint8(); const level = reader.readUint8(); const bitDepth = (reader.readUint8() >> 4) & 0x0f; const codec = codecBase + '.' + Mp4BoxParsers.addLeadingZero_(profile) + '.' + Mp4BoxParsers.addLeadingZero_(level) + '.' + Mp4BoxParsers.addLeadingZero_(bitDepth); return {codec}; } /** * Parses a AV1C box. * @param {string} codecBase * @param {!shaka.util.DataViewReader} reader * @param {string} boxName * @return {!shaka.util.ParsedAV1CBox} */ static parseAV1C(codecBase, reader, boxName) { const Mp4BoxParsers = shaka.util.Mp4BoxParsers; // More info https://aomediacodec.github.io/av1-isobmff/#av1codecconfigurationbox-syntax reader.skip(1); // Skip "marker" and "version" const secondByte = reader.readUint8(); const thirdByte = reader.readUint8(); const profile = secondByte >>> 5; const level = secondByte & 0x1f; const tierFlag = thirdByte >>> 7 ? 'H' : 'M'; const highBitDepth = (thirdByte & 0x40) >> 6; const twelveBit = (thirdByte & 0x20) >> 5; const bitDepth = profile === 2 && highBitDepth ? (twelveBit ? 12 : 10) : (highBitDepth ? 10 : 8); const monochrome = (thirdByte & 0x10) >> 4; const chromaSubsamplingX = (thirdByte & 0x08) >> 3; const chromaSubsamplingY = (thirdByte & 0x04) >> 2; const chromaSamplePosition = thirdByte & 0x03; // TODO: parse color_description_present_flag // default it to BT.709/limited range for now const colorPrimaries = 1; const transferCharacteristics = 1; const matrixCoefficients = 1; const videoFullRangeFlag = 0; const codec = codecBase + '.' + profile + '.' + Mp4BoxParsers.addLeadingZero_(level) + tierFlag + '.' + Mp4BoxParsers.addLeadingZero_(bitDepth) + '.' + monochrome + '.' + chromaSubsamplingX + chromaSubsamplingY + chromaSamplePosition + '.' + Mp4BoxParsers.addLeadingZero_(colorPrimaries) + '.' + Mp4BoxParsers.addLeadingZero_(transferCharacteristics) + '.' + Mp4BoxParsers.addLeadingZero_(matrixCoefficients) + '.' + videoFullRangeFlag; return {codec}; } /** * Parses a FRMA box. * @param {!shaka.util.DataViewReader} reader * @return {!shaka.util.ParsedFRMABox} */ static parseFRMA(reader) { const fourcc = reader.readUint32(); const codec = shaka.util.Mp4Parser.typeToString(fourcc); return {codec}; } /** * Parses a TENC box. * @param {!shaka.util.DataViewReader} reader * @return {!shaka.util.ParsedTENCBox} */ static parseTENC(reader) { const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils; reader.readUint8(); // reserved reader.readUint8(); reader.readUint8(); // default_isProtected reader.readUint8(); // default_Per_Sample_IV_Size const defaultKID = Uint8ArrayUtils.toHex(reader.readBytes(16)); return {defaultKID}; } /** * Parses a HDLR box. * @param {!shaka.util.DataViewReader} reader * @return {!shaka.util.ParsedHDLRBox} */ static parseHDLR(reader) { reader.skip(8); // Skip "pre_defined" const handlerType = reader.readTerminatedString(); return {handlerType}; } /** * Parses a PRJI box. * @param {!shaka.util.DataViewReader} reader * @return {!shaka.util.ParsedPRJIBox} */ static parsePRJI(reader) { const projection = reader.readTerminatedString(); return {projection}; } /** * Parses a HFOV box. * @param {!shaka.util.DataViewReader} reader * @return {!shaka.util.ParsedHFOVBox} */ static parseHFOV(reader) { const millidegrees = reader.readUint32(); return { hfov: millidegrees / 1000, }; } /** * Parses a COLR box. * @param {!shaka.util.DataViewReader} reader * @return {!shaka.util.ParsedCOLRBox} */ static parseCOLR(reader) { let colorGamut = null; let videoRange = null; const data = reader.readBytes(4); let colorType = ''; colorType += String.fromCharCode(data[0]); colorType += String.fromCharCode(data[1]); colorType += String.fromCharCode(data[2]); colorType += String.fromCharCode(data[3]); if (colorType === 'nclx') { const colorPrimaries = reader.readUint16(); switch (colorPrimaries) { case 1: case 5: case 6: case 7: colorGamut = 'srgb'; break; case 9: colorGamut = 'rec2020'; break; case 11: case 12: colorGamut = 'p3'; break; } const transferCharacteristics = reader.readUint16(); reader.readUint16(); // matrix_coefficients switch (transferCharacteristics) { case 1: case 6: case 13: case 14: case 15: videoRange = 'SDR'; break; case 16: videoRange = 'PQ'; break; case 18: videoRange = 'HLG'; break; } } return { colorGamut, videoRange, }; } /** * Parses AV1 codec string with COLR box information. * * AV1 codec info: https://aomediacodec.github.io/av1-isobmff/#codecsparam * * @param {string} codec * @param {!shaka.util.DataViewReader} reader * @return {string} */ static updateAV1CodecWithCOLRBox(codec, reader) { const Mp4BoxParsers = shaka.util.Mp4BoxParsers; const initialPosition = reader.getPosition(); const data = reader.readBytes(4); let colorType = ''; colorType += String.fromCharCode(data[0]); colorType += String.fromCharCode(data[1]); colorType += String.fromCharCode(data[2]); colorType += String.fromCharCode(data[3]); if (colorType === 'nclx') { const colorPrimaries = reader.readUint16(); const transferCharacteristics = reader.readUint16(); const matrixCoefficients = reader.readUint16(); const videoFullRangeFlag = reader.readUint8() >> 7; const codecParts = codec.split('.'); if (codecParts.length == 10) { codecParts[6] = Mp4BoxParsers.addLeadingZero_(colorPrimaries); codecParts[7] = Mp4BoxParsers.addLeadingZero_(transferCharacteristics); codecParts[8] = Mp4BoxParsers.addLeadingZero_(matrixCoefficients); codecParts[9] = String(videoFullRangeFlag); codec = codecParts.join('.'); } } reader.seek(initialPosition); return codec; } /** * Convert a number to hex * @param {number} x * @param {boolean=} removeInitialZero * @return {string} * @private */ static toHex_(x, removeInitialZero = false) { const value = x.toString(16).toUpperCase(); if (removeInitialZero) { return value; } return ('0' + value).slice(-2); } /** * Reverse a number bit to bit * @param {number} x * @return {number} * @private */ static reverseBits_(x) { let val = x; let reversed = 0; for (let i = 0; i < 32; i++) { reversed |= val & 1; if (i == 31) { break; } reversed <<= 1; val >>= 1; } return reversed; } /** * * @param {number} x * @return {string} * @private */ static addLeadingZero_(x) { return (x < 10 ? '0' : '') + x; } }; /** * @typedef {{ * trackId: number, * defaultSampleDuration: ?number, * defaultSampleSize: ?number, * baseDataOffset: ?number, * sampleDescriptionIndex: ?number * }} * * @property {number} trackId * As per the spec: an integer that uniquely identifies this * track over the entire life‐time of this presentation * @property {?number} defaultSampleDuration * If specified via flags, this overrides the default sample * duration in the Track Extends Box for this fragment * @property {?number} defaultSampleSize * If specified via flags, this overrides the default sample * size in the Track Extends Box for this fragment * @property {?number} baseDataOffset * If specified via flags, this indicate the base data offset * @property {?number} sampleDescriptionIndex * If specified via flags, this indicate the sample description index * @exportDoc */ shaka.util.ParsedTFHDBox; /** * @typedef {{ * baseMediaDecodeTime: number * }} * * @property {number} baseMediaDecodeTime * As per the spec: the absolute decode time, measured on the media * timeline, of the first sample in decode order in the track fragment * * @exportDoc */ shaka.util.ParsedTFDTBox; /** * @typedef {{ * mediaTime: number, * ntpTimestamp: number * }} * * @exportDoc */ shaka.util.ParsedPRFTBox; /** * @typedef {{ * timescale: number, * language: string * }} * * @property {number} timescale * As per the spec: an integer that specifies the time‐scale for this media; * this is the number of time units that pass in one second * @property {string} language * Language code for this media * * @exportDoc */ shaka.util.ParsedMDHDBox; /** * @typedef {{ * defaultSampleDuration: number, * defaultSampleSize: number * }} * * @property {number} defaultSampleDuration * The default sample duration to be used in track fragments * @property {number} defaultSampleSize * The default sample size to be used in track fragments * * @exportDoc */ shaka.util.ParsedTREXBox; /** * @typedef {{ * sampleCount: number, * sampleData: !Array<shaka.util.ParsedTRUNSample>, * dataOffset: ?number * }} * * @property {number} sampleCount * As per the spec: the number of samples being added in this run; * @property {!Array<shaka.util.ParsedTRUNSample>} sampleData * An array of size <sampleCount> containing data for each sample * @property {?number} dataOffset * If specified via flags, this indicate the offset of the sample in bytes. * * @exportDoc */ shaka.util.ParsedTRUNBox; /** * @typedef {{ * sampleDuration: ?number, * sampleSize: ?number, * sampleCompositionTimeOffset: ?number * }} * * @property {?number} sampleDuration * The length of the sample in timescale units. * @property {?number} sampleSize * The size of the sample in bytes. * @property {?number} sampleCompositionTimeOffset * The time since the start of the sample in timescale units. Time * offset is based of the start of the sample. If this value is * missing, the accumulated durations preceding this time sample will * be used to create the start time. * * @exportDoc */ shaka.util.ParsedTRUNSample; /** * @typedef {{ * trackId: number, * width: number, * height: number * }} * * @property {number} trackId * Unique ID indicative of this track * @property {number} width * Width of this track in pixels * @property {number} height * Height of this track in pixels. * * @exportDoc */ shaka.util.ParsedTKHDBox; /** * @typedef {{ * codec: string * }} * * @property {string} codec * A fourcc for a codec * * @exportDoc */ shaka.util.ParsedFRMABox; /** * @typedef {{ * defaultKID: string * }} * * @property {string} defaultKID * * @exportDoc */ shaka.util.ParsedTENCBox; /** * @typedef {{ * width: number, * height: number * }} * * @property {number} width * @property {number} height * * @exportDoc */ shaka.util.ParsedVisualSampleEntryBox; /** * @typedef {{ * channelCount: number, * sampleRate: number * }} * * @property {number} channelCount * @property {number} sampleRate * * @exportDoc */ shaka.util.ParsedAudioSampleEntryBox; /** * @typedef {{ * codec: string * }} * * @property {string} codec * * @exportDoc */ shaka.util.ParsedESDSBox; /** * @typedef {{ * codec: string * }} * * @property {string} codec * * @exportDoc */ shaka.util.ParsedAVCCBox; /** * @typedef {{ * codec: string * }} * * @property {string} codec * * @exportDoc */ shaka.util.ParsedHVCCBox; /** * @typedef {{ * codec: string * }} * * @property {string} codec * * @exportDoc */ shaka.util.ParsedDVCCBox; /** * @typedef {{ * codec: string * }} * * @property {string} codec * * @exportDoc */ shaka.util.ParsedDVVCBox; /** * @typedef {{ * codec: string * }} * * @property {string} codec * * @exportDoc */ shaka.util.ParsedVPCCBox; /** * @typedef {{ * codec: string * }} * * @property {string} codec * * @exportDoc */ shaka.util.ParsedAV1CBox; /** * @typedef {{ * handlerType: string * }} * * @property {string} handlerType * A four-character code that identifies the type of the media handler or * data handler. For media handlers, this field defines the type of * data—for example, 'vide' for video data, 'soun' for sound data. * * @exportDoc */ shaka.util.ParsedHDLRBox; /** * @typedef {{ * projection: string * }} * * @property {string} projection * A four-character code that identifies the type of the projection. * Possible values: * - Rectangular: ‘rect’ * - Half equirectangular: ‘hequ’ * - Equirectangular: ? * - Fisheye: ‘fish’ * * @exportDoc */ shaka.util.ParsedPRJIBox; /** * @typedef {{ * hfov: number * }} * * @property {number} hfov * * @exportDoc */ shaka.util.ParsedHFOVBox; /** * @typedef {{ * colorGamut: ?string, * videoRange: ?string * }} * * @property {?string} colorGamut * @property {?string} videoRange * * @exportDoc */ shaka.util.ParsedCOLRBox;