From d465942c4393e6c891d6a230bea90a44d90cc70b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Velad=20Galv=C3=A1n?= Date: Fri, 27 Jan 2023 08:46:46 +0100 Subject: [PATCH] feat(HLS): Improve detection of basic info from Media Playlist (#4809) --- lib/hls/hls_parser.js | 365 +++++++++++++++++++++++++++--- lib/util/mp4_box_parsers.js | 83 ++++++- test/hls/hls_parser_unit.js | 48 ++-- test/util/mp4_box_parsers_unit.js | 43 +++- 4 files changed, 460 insertions(+), 79 deletions(-) diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 089894ec6d..d0c62cc3b5 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -33,9 +33,12 @@ goog.require('shaka.util.Functional'); goog.require('shaka.util.LanguageUtils'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.MimeUtils'); +goog.require('shaka.util.Mp4BoxParsers'); +goog.require('shaka.util.Mp4Parser'); goog.require('shaka.util.OperationManager'); goog.require('shaka.util.Pssh'); goog.require('shaka.util.Timer'); +goog.require('shaka.util.TsParser'); goog.require('shaka.util.Platform'); goog.require('shaka.util.Uint8ArrayUtils'); goog.require('shaka.util.XmlUtils'); @@ -594,7 +597,6 @@ shaka.hls.HlsParser = class { * @private */ async parseManifest_(data, uri) { - const HlsParser = shaka.hls.HlsParser; const Utils = shaka.hls.Utils; goog.asserts.assert(this.masterPlaylistUri_, @@ -617,44 +619,22 @@ shaka.hls.HlsParser = class { // Parsing a media playlist results in a single-variant stream. if (playlist.type == shaka.hls.PlaylistType.MEDIA) { - // Get necessary info for this stream, from the config. These are things - // we would normally find from the master playlist (e.g. from values on - // EXT-X-MEDIA tags). - let fullMimeType = this.config_.hls.mediaPlaylistFullMimeType; - // Try to infer the full mimetype better. - if (playlist.segments.length) { - const parsedUri = new goog.Uri(playlist.segments[0].absoluteUri); - const extension = parsedUri.getPath().split('.').pop(); - let mimeType = HlsParser.RAW_FORMATS_TO_MIME_TYPES_[extension]; - if (mimeType) { - fullMimeType = mimeType; - } else if (extension === 'ts') { - // TODO: Fetch one segment a use the TsParser to analize if there is - // video, audio or both. - } else if (extension === 'mp4') { - // TODO: Fetch one segment a use the Mp4Parser to analize if there is - // video, audio or both. - } else if (HlsParser.AUDIO_EXTENSIONS_TO_MIME_TYPES_[extension]) { - mimeType = HlsParser.AUDIO_EXTENSIONS_TO_MIME_TYPES_[extension]; - const defaultAudioCodec = this.config_.hls.defaultAudioCodec; - fullMimeType = `${mimeType}; codecs="${defaultAudioCodec}"`; - } else if (HlsParser.VIDEO_EXTENSIONS_TO_MIME_TYPES_[extension]) { - mimeType = HlsParser.VIDEO_EXTENSIONS_TO_MIME_TYPES_[extension]; - const defaultVideoCodec = this.config_.hls.defaultVideoCodec; - fullMimeType = `${mimeType}; codecs="${defaultVideoCodec}"`; - } - } - - const mimeType = shaka.util.MimeUtils.getBasicType(fullMimeType); - const type = mimeType.split('/')[0]; - const codecs = shaka.util.MimeUtils.getCodecs(fullMimeType); + // Get necessary info for this stream. These are things we would normally + // find from the master playlist (e.g. from values on EXT-X-MEDIA tags). + const basicInfo = await this.getMediaPlaylistBasicInfo_(playlist); + const type = basicInfo.type; + const mimeType = basicInfo.mimeType; + const codecs = basicInfo.codecs; + const language = basicInfo.language || 'und'; + const height = basicInfo.height; + const width = basicInfo.width; + const channelsCount = basicInfo.channelCount; + const sampleRate = basicInfo.sampleRate; // Some values we cannot figure out, and aren't important enough to ask // the user to provide through config values. A lot of these are only // relevant to ABR, which isn't necessary if there's only one variant. // So these unknowns should be set to false or null, largely. - const language = ''; - const channelsCount = null; const spatialAudio = false; const characteristics = null; const closedCaptions = new Map(); @@ -669,10 +649,17 @@ shaka.hls.HlsParser = class { mimeType); this.uriToStreamInfosMap_.set(uri, streamInfo); + if (type == 'video') { + this.addVideoAttributes_(streamInfo.stream, width, height, + /* frameRate= */ null, /* videoRange= */ null); + } else if (type == 'audio') { + streamInfo.stream.audioSamplingRate = sampleRate; + } + // Wrap the stream from that stream info with a variant. variants.push({ id: 0, - language: 'und', + language: language, disabledUntilTime: 0, primary: true, audio: type == 'audio' ? streamInfo.stream : null, @@ -776,6 +763,280 @@ shaka.hls.HlsParser = class { this.playerInterface_.makeTextStreamsForClosedCaptions(this.manifest_); } + /** + * @param {shaka.hls.Playlist} playlist + * @return {!Promise.} + * @private + */ + async getMediaPlaylistBasicInfo_(playlist) { + const HlsParser = shaka.hls.HlsParser; + const defaultFullMimeType = this.config_.hls.mediaPlaylistFullMimeType; + const defaultMimeType = + shaka.util.MimeUtils.getBasicType(defaultFullMimeType); + const defaultType = defaultMimeType.split('/')[0]; + const defaultCodecs = shaka.util.MimeUtils.getCodecs(defaultFullMimeType); + const defaultBasicInfo = { + type: defaultType, + mimeType: defaultMimeType, + codecs: defaultCodecs, + language: null, + height: null, + width: null, + channelCount: null, + sampleRate: null, + }; + if (!playlist.segments.length) { + return defaultBasicInfo; + } + const firstSegment = playlist.segments[0]; + const parsedUri = new goog.Uri(firstSegment.absoluteUri); + const extension = parsedUri.getPath().split('.').pop(); + const rawMimeType = HlsParser.RAW_FORMATS_TO_MIME_TYPES_[extension]; + if (rawMimeType) { + return { + type: 'audio', + mimeType: rawMimeType, + codecs: '', + language: null, + height: null, + width: null, + channelCount: null, + sampleRate: null, + }; + } + + let segmentUris = [firstSegment.absoluteUri]; + const initSegmentRef = this.getInitSegmentReference_( + playlist.absoluteUri, firstSegment.tags, new Map()); + if (initSegmentRef) { + segmentUris = initSegmentRef.getUris(); + } + + const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT; + const segmentRequest = shaka.net.NetworkingEngine.makeRequest( + segmentUris, this.config_.retryParameters); + const response = await this.makeNetworkRequest_( + segmentRequest, requestType); + + let contentMimeType = response.headers['content-type']; + if (contentMimeType) { + // Split the MIME type in case the server sent additional parameters. + contentMimeType = contentMimeType.split(';')[0].toLowerCase(); + } + + if (extension == 'ts' || contentMimeType == 'video/mp2t') { + const basicInfo = this.getBasicInfoFromTs_(response); + if (basicInfo) { + return basicInfo; + } + } else if (extension == 'mp4' || + contentMimeType == 'video/mp4' || contentMimeType == 'audio/mp4') { + const basicInfo = this.getBasicInfoFromMp4_(response); + if (basicInfo) { + return basicInfo; + } + } + return defaultBasicInfo; + } + + /** + * @param {shaka.extern.Response} response + * @return {?shaka.hls.HlsParser.BasicInfo} + * @private + */ + getBasicInfoFromTs_(response) { + const uint8ArrayData = shaka.util.BufferUtils.toUint8(response.data); + const tsParser = new shaka.util.TsParser().parse(uint8ArrayData); + const tsCodecs = tsParser.getCodecs(); + const codecs = []; + let hasAudio = false; + let hasVideo = false; + switch (tsCodecs.audio) { + case 'aac': + codecs.push('mp4a.40.2'); + hasAudio = true; + break; + case 'mp3': + codecs.push('mp4a.40.34'); + hasAudio = true; + break; + case 'ac3': + codecs.push('ac-3'); + hasAudio = true; + break; + } + switch (tsCodecs.video) { + case 'avc': + codecs.push('avc1.42E01E'); + hasVideo = true; + break; + case 'hvc': + codecs.push('hvc1.1.6.L93.90'); + hasVideo = true; + break; + } + if (!codecs.length) { + return null; + } + const onlyAudio = hasAudio && !hasVideo; + return { + type: onlyAudio ? 'audio' : 'video', + mimeType: 'video/mp2t', + codecs: codecs.join(', '), + language: null, + height: null, + width: null, + channelCount: null, + sampleRate: null, + }; + } + + /** + * @param {shaka.extern.Response} response + * @return {?shaka.hls.HlsParser.BasicInfo} + * @private + */ + getBasicInfoFromMp4_(response) { + const Mp4Parser = shaka.util.Mp4Parser; + + const codecs = []; + + let hasAudio = false; + let hasVideo = false; + + const addCodec = (codec) => { + const codecLC = codec.toLowerCase(); + switch (codecLC) { + case 'avc1': + case 'avc3': + codecs.push(codecLC + '.42E01E'); + hasVideo = true; + break; + case 'hev1': + case 'hvc1': + codecs.push(codecLC + '.1.6.L93.90'); + hasVideo = true; + break; + case 'dvh1': + case 'dvhe': + codecs.push(codecLC + '.05.04'); + hasVideo = true; + break; + case 'vp09': + codecs.push(codecLC + '.00.10.08'); + hasVideo = true; + break; + case 'av01': + codecs.push(codecLC + '.0.01M.08'); + hasVideo = true; + break; + case 'mp4a': + // We assume AAC, but this can be wrong since mp4a supports + // others codecs + codecs.push('mp4a.40.2'); + hasAudio = true; + break; + case 'ac-3': + case 'ec-3': + case 'opus': + case 'flac': + codecs.push(codecLC); + hasAudio = true; + break; + } + }; + + const codecBoxParser = (box) => addCodec(box.name); + + /** @type {?string} */ + let language = null; + /** @type {?string} */ + let height = null; + /** @type {?string} */ + let width = null; + /** @type {?number} */ + let channelCount = null; + /** @type {?number} */ + let sampleRate = null; + + new Mp4Parser() + .box('moov', Mp4Parser.children) + .box('trak', Mp4Parser.children) + .fullBox('tkhd', (box) => { + goog.asserts.assert( + box.version != null, + 'TKHD is a full box and should have a valid version.'); + const parsedTKHDBox = shaka.util.Mp4BoxParsers.parseTKHD( + box.reader, box.version); + height = String(parsedTKHDBox.height); + width = String(parsedTKHDBox.width); + }) + .box('mdia', Mp4Parser.children) + .fullBox('mdhd', (box) => { + goog.asserts.assert( + box.version != null, + 'MDHD is a full box and should have a valid version.'); + const parsedMDHDBox = shaka.util.Mp4BoxParsers.parseMDHD( + box.reader, box.version); + language = parsedMDHDBox.language; + }) + .box('minf', Mp4Parser.children) + .box('stbl', Mp4Parser.children) + .fullBox('stsd', Mp4Parser.sampleDescription) + + // AUDIO + // These are the various boxes that signal a codec. + .box('mp4a', (box) => { + const parsedMP4ABox = shaka.util.Mp4BoxParsers.parseMP4A(box.reader); + channelCount = parsedMP4ABox.channelCount; + sampleRate = parsedMP4ABox.sampleRate; + codecBoxParser(box); + }) + .box('ac-3', codecBoxParser) + .box('ec-3', codecBoxParser) + .box('opus', codecBoxParser) + .box('Opus', codecBoxParser) + .box('fLaC', codecBoxParser) + + // VIDEO + // These are the various boxes that signal a codec. + .box('avc1', codecBoxParser) + .box('avc3', codecBoxParser) + .box('hev1', codecBoxParser) + .box('hvc1', codecBoxParser) + .box('dvh1', codecBoxParser) + .box('dvhe', codecBoxParser) + .box('vp09', codecBoxParser) + .box('av01', codecBoxParser) + + // This signals an encrypted sample, which we can go inside of to + // find the codec used. + // Note: If encrypted, you can only have audio or video, not both. + .box('enca', Mp4Parser.visualSampleEntry) + .box('encv', Mp4Parser.visualSampleEntry) + .box('sinf', Mp4Parser.children) + .box('frma', (box) => { + const {codec} = shaka.util.Mp4BoxParsers.parseFRMA(box.reader); + addCodec(codec); + }) + + .parse(response.data, /* partialOkay= */ true); + if (!codecs.length) { + return null; + } + const onlyAudio = hasAudio && !hasVideo; + return { + type: onlyAudio ? 'audio' : 'video', + mimeType: onlyAudio ? 'audio/mp4' : 'video/mp4', + codecs: this.filterDuplicateCodecs_(codecs).join(', '), + language: language, + height: height, + width: width, + channelCount: channelCount, + sampleRate: sampleRate, + }; + } + /** @private */ determineDuration_() { goog.asserts.assert(this.presentationTimeline_, @@ -1178,6 +1439,16 @@ shaka.hls.HlsParser = class { /** @type {!Array.} */ const codecs = codecsString.split(/\s*,\s*/); + return this.filterDuplicateCodecs_(codecs); + } + + + /** + * @param {!Array.} codecs + * @return {!Array.} codecs + * @private + */ + filterDuplicateCodecs_(codecs) { // Filter out duplicate codecs. const seen = new Set(); const ret = []; @@ -3311,6 +3582,30 @@ shaka.hls.HlsParser.StreamInfo; shaka.hls.HlsParser.StreamInfos; +/** + * @typedef {{ + * type: string, + * mimeType: string, + * codecs: string, + * language: ?string, + * height: ?string, + * width: ?string, + * channelCount: ?number, + * sampleRate: ?number + * }} + * + * @property {string} type + * @property {string} mimeType + * @property {string} codecs + * @property {?string} language + * @property {?string} height + * @property {?string} width + * @property {?number} channelCount + * @property {?number} sampleRate + */ +shaka.hls.HlsParser.BasicInfo; + + /** * @const {!Object.} * @private diff --git a/lib/util/mp4_box_parsers.js b/lib/util/mp4_box_parsers.js index 6fc41db21f..49b2d2e318 100644 --- a/lib/util/mp4_box_parsers.js +++ b/lib/util/mp4_box_parsers.js @@ -83,8 +83,21 @@ shaka.util.Mp4BoxParsers = class { 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, }; } @@ -174,19 +187,57 @@ shaka.util.Mp4BoxParsers = class { * @return {!shaka.util.ParsedTKHDBox} */ static parseTKHD(reader, version) { - let trackId = 0; if (version == 1) { reader.skip(8); // Skip "creation_time" reader.skip(8); // Skip "modification_time" - trackId = reader.readUint32(); } else { reader.skip(4); // Skip "creation_time" reader.skip(4); // Skip "modification_time" - trackId = reader.readUint32(); } + 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 a MP4A box. + * @param {!shaka.util.DataViewReader} reader + * @return {!shaka.util.ParsedMP4ABox} + */ + static parseMP4A(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 "samplesize" + reader.skip(2); // Skip "pre_defined" + reader.skip(2); // Skip "reserved" + const sampleRate = reader.readUint16() + (reader.readUint16() / 65536); + + return { + channelCount, + sampleRate, }; } @@ -242,12 +293,15 @@ shaka.util.ParsedTFDTBox; /** * @typedef {{ - * timescale: number + * 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 */ @@ -309,11 +363,17 @@ shaka.util.ParsedTRUNSample; /** * @typedef {{ - * trackId: number + * 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 */ @@ -330,3 +390,16 @@ shaka.util.ParsedTKHDBox; * @exportDoc */ shaka.util.ParsedFRMABox; + +/** + * @typedef {{ + * channelCount: number, + * sampleRate: number + * }} + * + * @property {number} channelCount + * @property {number} sampleRate + * + * @exportDoc + */ +shaka.util.ParsedMP4ABox; diff --git a/test/hls/hls_parser_unit.js b/test/hls/hls_parser_unit.js index efcd93abc3..95f0df16db 100644 --- a/test/hls/hls_parser_unit.js +++ b/test/hls/hls_parser_unit.js @@ -10,6 +10,9 @@ describe('HlsParser', () => { const Util = shaka.test.Util; const originalAlwaysWarn = shaka.log.alwaysWarn; + const videoInitSegmentUri = '/base/test/test/assets/sintel-video-init.mp4'; + const videoSegmentUri = '/base/test/test/assets/sintel-video-segment.mp4'; + const vttText = [ 'WEBVTT\n', '\n', @@ -43,39 +46,13 @@ describe('HlsParser', () => { parser.stop(); }); - beforeEach(() => { - // TODO: use StreamGenerator? - initSegmentData = new Uint8Array([ - 0x00, 0x00, 0x00, 0x30, // size (48) - 0x6D, 0x6F, 0x6F, 0x76, // type (moov) - 0x00, 0x00, 0x00, 0x28, // trak size (40) - 0x74, 0x72, 0x61, 0x6B, // type (trak) - 0x00, 0x00, 0x00, 0x20, // mdia size (32) - 0x6D, 0x64, 0x69, 0x61, // type (mdia) - - 0x00, 0x00, 0x00, 0x18, // mdhd size (24) - 0x6D, 0x64, 0x68, 0x64, // type (mdhd) - 0x00, 0x00, 0x00, 0x00, // version and flags - - 0x00, 0x00, 0x00, 0x00, // creation time (0) - 0x00, 0x00, 0x00, 0x00, // modification time (0) - 0x00, 0x00, 0x03, 0xe8, // timescale (1000) - ]); - - segmentData = new Uint8Array([ - 0x00, 0x00, 0x00, 0x24, // size (36) - 0x6D, 0x6F, 0x6F, 0x66, // type (moof) - 0x00, 0x00, 0x00, 0x1C, // traf size (28) - 0x74, 0x72, 0x61, 0x66, // type (traf) - - 0x00, 0x00, 0x00, 0x14, // tfdt size (20) - 0x74, 0x66, 0x64, 0x74, // type (tfdt) - 0x01, 0x00, 0x00, 0x00, // version and flags - - 0x00, 0x00, 0x00, 0x00, // baseMediaDecodeTime first 4 bytes (0) - 0x00, 0x00, 0x00, 0x00, // baseMediaDecodeTime last 4 bytes (0) + beforeEach(async () => { + const responses = await Promise.all([ + shaka.test.Util.fetch(videoInitSegmentUri), + shaka.test.Util.fetch(videoSegmentUri), ]); - // segment starts at 0s. + initSegmentData = responses[0]; + segmentData = responses[1]; selfInitializingSegmentData = shaka.util.Uint8ArrayUtils.concat(initSegmentData, segmentData); @@ -129,6 +106,7 @@ describe('HlsParser', () => { .setResponseText('test:/main.vtt', vttText) .setResponseValue('test:/init.mp4', initSegmentData) .setResponseValue('test:/init2.mp4', initSegmentData) + .setResponseValue('test:/init.test', initSegmentData) .setResponseValue('test:/main.mp4', segmentData) .setResponseValue('test:/main2.mp4', segmentData) .setResponseValue('test:/main.test', segmentData) @@ -4127,7 +4105,7 @@ describe('HlsParser', () => { manifest.anyTimeline(); manifest.addPartialVariant((variant) => { variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.mime('video/mp2t', 'avc1.42E01E, mp4a.40.2'); + stream.mime('video/mp4', 'avc1.42E01E'); }); }); }); @@ -4141,10 +4119,10 @@ describe('HlsParser', () => { const media = [ '#EXTM3U\n', '#EXT-X-PLAYLIST-TYPE:VOD\n', - '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', + '#EXT-X-MAP:URI="init.test",BYTERANGE="616@0"\n', '#EXTINF:5,\n', '#EXT-X-BYTERANGE:121090@616\n', - 'main.mp4', + 'main.test', ].join(''); const config = shaka.util.PlayerConfiguration.createDefault().manifest; diff --git a/test/util/mp4_box_parsers_unit.js b/test/util/mp4_box_parsers_unit.js index 71af16e503..fab41ee073 100644 --- a/test/util/mp4_box_parsers_unit.js +++ b/test/util/mp4_box_parsers_unit.js @@ -29,12 +29,18 @@ describe('Mp4BoxParsers', () => { let defaultSampleDuration; let defaultSampleSize; let trackId; + let width; + let height; let timescale; + let language; const expectedDefaultSampleDuration = 512; const expectedDefaultSampleSize = 0; const expectedTrackId = 1; + const expectedWidth = 1685.9375; + const expectedHeight = 110; const expectedTimescale = 12288; + const expectedLanguage = 'eng'; const Mp4Parser = shaka.util.Mp4Parser; new Mp4Parser() @@ -56,6 +62,8 @@ describe('Mp4BoxParsers', () => { const parsedTKHDBox = shaka.util.Mp4BoxParsers.parseTKHD( box.reader, box.version); trackId = parsedTKHDBox.trackId; + width = parsedTKHDBox.width; + height = parsedTKHDBox.height; tkhdParsed = true; }) .box('mdia', Mp4Parser.children) @@ -66,6 +74,7 @@ describe('Mp4BoxParsers', () => { const parsedMDHDBox = shaka.util.Mp4BoxParsers.parseMDHD( box.reader, box.version); timescale = parsedMDHDBox.timescale; + language = parsedMDHDBox.language; mdhdParsed = true; }) .parse(videoInitSegment, /* partialOkay= */ true); @@ -76,7 +85,10 @@ describe('Mp4BoxParsers', () => { expect(defaultSampleDuration).toBe(expectedDefaultSampleDuration); expect(defaultSampleSize).toBe(expectedDefaultSampleSize); expect(trackId).toBe(expectedTrackId); + expect(width).toBe(expectedWidth); + expect(height).toBe(expectedHeight); expect(timescale).toBe(expectedTimescale); + expect(language).toBe(expectedLanguage); }); it('parses video segment', () => { @@ -146,14 +158,22 @@ describe('Mp4BoxParsers', () => { }); /** - * Test on parsing an incomplete TKHD V1 box, since the parser doesn't - * parse the other fields * * Explanation on the Uint8Array: * [ * , * , - * + * , + * , + * , + * , + * , + * , + * , + * , + * , + * , + * * ] * * Time is a 32B integer expressed in seconds since Jan 1, 1904, 0000 UTC @@ -164,12 +184,27 @@ describe('Mp4BoxParsers', () => { 0x00, 0x00, 0x00, 0x00, 0xDC, 0xBF, 0x0F, 0xD7, // Creation time 0x00, 0x00, 0x00, 0x00, 0xDC, 0xBF, 0x0F, 0xD7, // Modification time 0x00, 0x00, 0x00, 0x01, // Track ID - // Remaining fields are not processed in parseTKHD() + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Reserved + 0x00, 0x00, 0x00, 0x00, // Duration + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Reserved + 0x00, 0x00, // Layer + 0x00, 0x00, // Alternate Group + 0x00, 0x00, // Volume + 0x00, 0x00, // Reserved + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Matrix Structure + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Matrix Structure + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Matrix Structure + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Matrix Structure + 0x00, 0x00, 0x00, 0x00, // Matrix Structure + 0x00, 0x40, 0x00, 0x00, // Width + 0x00, 0x40, 0x00, 0x00, // Height ]); const reader = new shaka.util.DataViewReader( tkhdBox, shaka.util.DataViewReader.Endianness.BIG_ENDIAN); const parsedTkhd = shaka.util.Mp4BoxParsers .parseTKHD(reader, /* version= */ 1); expect(parsedTkhd.trackId).toBe(1); + expect(parsedTkhd.width).toBe(64); + expect(parsedTkhd.height).toBe(64); }); });