diff --git a/build/types/core b/build/types/core index 2ce8a0d5c2..4031b533a2 100644 --- a/build/types/core +++ b/build/types/core @@ -106,6 +106,7 @@ +../../lib/util/player_configuration.js +../../lib/util/pssh.js +../../lib/util/public_promise.js ++../../lib/util/segment_utils.js +../../lib/util/state_history.js +../../lib/util/stats.js +../../lib/util/stream_utils.js diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 6b737202d1..02e6a4bb70 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -32,12 +32,10 @@ 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.SegmentUtils'); 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'); @@ -726,7 +724,8 @@ shaka.hls.HlsParser = class { this.parseMasterVariables_(variablesTags); /** @type {!Array.} */ - const mediaTags = Utils.filterTagsByName(playlist.tags, 'EXT-X-MEDIA'); + const mediaTags = Utils.filterTagsByName( + playlist.tags, 'EXT-X-MEDIA'); /** @type {!Array.} */ const variantTags = Utils.filterTagsByName( playlist.tags, 'EXT-X-STREAM-INF'); @@ -736,33 +735,13 @@ shaka.hls.HlsParser = class { /** @type {!Array.} */ const sessionKeyTags = Utils.filterTagsByName( playlist.tags, 'EXT-X-SESSION-KEY'); + /** @type {!Array.} */ + const sessionDataTags = Utils.filterTagsByName( + playlist.tags, 'EXT-X-SESSION-DATA'); - this.parseCodecs_(variantTags); + this.processSessionData_(sessionDataTags); - /** @type {!Array.} */ - const sesionDataTags = - Utils.filterTagsByName(playlist.tags, 'EXT-X-SESSION-DATA'); - for (const tag of sesionDataTags) { - const id = tag.getAttributeValue('DATA-ID'); - const uri = tag.getAttributeValue('URI'); - const language = tag.getAttributeValue('LANGUAGE'); - const value = tag.getAttributeValue('VALUE'); - const data = (new Map()).set('id', id); - if (uri) { - data.set('uri', shaka.hls.Utils.constructUris( - [this.masterPlaylistUri_], uri, this.globalVariables_)[0]); - } - if (language) { - data.set('language', language); - } - if (value) { - data.set('value', value); - } - const event = new shaka.util.FakeEvent('sessiondata', data); - if (this.playerInterface_) { - this.playerInterface_.onEvent(event); - } - } + this.parseCodecs_(variantTags); // Parse audio and video media tags first, so that we can extract segment // start time from audio/video streams and reuse for text streams. @@ -824,26 +803,13 @@ shaka.hls.HlsParser = class { /** * @param {shaka.hls.Playlist} playlist - * @return {!Promise.} + * @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, - }; + const defaultBasicInfo = shaka.util.SegmentUtils.getBasicInfoFromMimeType( + this.config_.hls.mediaPlaylistFullMimeType); if (!playlist.segments.length) { return defaultBasicInfo; } @@ -855,16 +821,8 @@ shaka.hls.HlsParser = class { 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, - }; + return shaka.util.SegmentUtils.getBasicInfoFromMimeType( + rawMimeType); } let segmentUris = [firstSegmentUri]; @@ -890,13 +848,13 @@ shaka.hls.HlsParser = class { } if (extension == 'ts' || contentMimeType == 'video/mp2t') { - const basicInfo = this.getBasicInfoFromTs_(response); + const basicInfo = shaka.util.SegmentUtils.getBasicInfoFromTs(response); if (basicInfo) { return basicInfo; } } else if (extension == 'mp4' || contentMimeType == 'video/mp4' || contentMimeType == 'audio/mp4') { - const basicInfo = this.getBasicInfoFromMp4_(response); + const basicInfo = shaka.util.SegmentUtils.getBasicInfoFromMp4(response); if (basicInfo) { return basicInfo; } @@ -904,236 +862,6 @@ shaka.hls.HlsParser = class { 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 videoInfo = tsParser.getVideoInfo(); - 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; - case 'ec3': - codecs.push('ec-3'); - hasAudio = true; - break; - } - switch (tsCodecs.video) { - case 'avc': - if (videoInfo.codec) { - codecs.push(videoInfo.codec); - } else { - codecs.push('avc1.42E01E'); - } - hasVideo = true; - break; - case 'hvc': - if (videoInfo.codec) { - codecs.push(videoInfo.codec); - } else { - 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: videoInfo.height, - width: videoInfo.width, - 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; - if (box.reader.hasMoreData()) { - Mp4Parser.children(box); - } else { - codecBoxParser(box); - } - }) - .box('esds', (box) => { - const parsedESDSBox = shaka.util.Mp4BoxParsers.parseESDS(box.reader); - codecs.push(parsedESDSBox.codec); - hasAudio = true; - }) - .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', (box) => { - const parsedAVCBox = - shaka.util.Mp4BoxParsers.parseAVC(box.reader, box.name); - codecs.push(parsedAVCBox.codec); - hasVideo = true; - }) - .box('avc3', (box) => { - const parsedAVCBox = - shaka.util.Mp4BoxParsers.parseAVC(box.reader, box.name); - codecs.push(parsedAVCBox.codec); - hasVideo = true; - }) - .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_, @@ -1265,6 +993,36 @@ shaka.hls.HlsParser = class { } } + /** + * Process EXT-X-SESSION-DATA tags. + * + * @param {!Array.} tags + * @private + */ + processSessionData_(tags) { + for (const tag of tags) { + const id = tag.getAttributeValue('DATA-ID'); + const uri = tag.getAttributeValue('URI'); + const language = tag.getAttributeValue('LANGUAGE'); + const value = tag.getAttributeValue('VALUE'); + const data = (new Map()).set('id', id); + if (uri) { + data.set('uri', shaka.hls.Utils.constructUris( + [this.masterPlaylistUri_], uri, this.globalVariables_)[0]); + } + if (language) { + data.set('language', language); + } + if (value) { + data.set('value', value); + } + const event = new shaka.util.FakeEvent('sessiondata', data); + if (this.playerInterface_) { + this.playerInterface_.onEvent(event); + } + } + } + /** * Parse Subtitles and Closed Captions from 'EXT-X-MEDIA' tags. * Create text streams for Subtitles, but not Closed Captions. @@ -4002,30 +3760,6 @@ 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/segment_utils.js b/lib/util/segment_utils.js new file mode 100644 index 0000000000..d7970517ee --- /dev/null +++ b/lib/util/segment_utils.js @@ -0,0 +1,315 @@ +/*! @license + * Shaka Player + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.provide('shaka.util.SegmentUtils'); + +goog.require('goog.asserts'); +goog.require('shaka.log'); +goog.require('shaka.util.BufferUtils'); +goog.require('shaka.util.MimeUtils'); +goog.require('shaka.util.Mp4BoxParsers'); +goog.require('shaka.util.Mp4Parser'); +goog.require('shaka.util.TsParser'); + + +/** + * @summary Utility functions for segment parsing. + */ +shaka.util.SegmentUtils = class { + /** + * @param {string} mimeType + * @return {shaka.util.SegmentUtils.BasicInfo} + */ + static getBasicInfoFromMimeType(mimeType) { + const baseMimeType = shaka.util.MimeUtils.getBasicType(mimeType); + const type = baseMimeType.split('/')[0]; + const codecs = shaka.util.MimeUtils.getCodecs(mimeType); + return { + type: type, + mimeType: baseMimeType, + codecs: codecs, + language: null, + height: null, + width: null, + channelCount: null, + sampleRate: null, + }; + } + + /** + * @param {shaka.extern.Response} response + * @return {?shaka.util.SegmentUtils.BasicInfo} + */ + static getBasicInfoFromTs(response) { + const uint8ArrayData = shaka.util.BufferUtils.toUint8(response.data); + const tsParser = new shaka.util.TsParser().parse(uint8ArrayData); + const tsCodecs = tsParser.getCodecs(); + const videoInfo = tsParser.getVideoInfo(); + 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; + case 'ec3': + codecs.push('ec-3'); + hasAudio = true; + break; + } + switch (tsCodecs.video) { + case 'avc': + if (videoInfo.codec) { + codecs.push(videoInfo.codec); + } else { + codecs.push('avc1.42E01E'); + } + hasVideo = true; + break; + case 'hvc': + if (videoInfo.codec) { + codecs.push(videoInfo.codec); + } else { + 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: videoInfo.height, + width: videoInfo.width, + channelCount: null, + sampleRate: null, + }; + } + + /** + * @param {shaka.extern.Response} response + * @return {?shaka.util.SegmentUtils.BasicInfo} + */ + static getBasicInfoFromMp4(response) { + const Mp4Parser = shaka.util.Mp4Parser; + const SegmentUtils = shaka.util.SegmentUtils; + + 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; + if (box.reader.hasMoreData()) { + Mp4Parser.children(box); + } else { + codecBoxParser(box); + } + }) + .box('esds', (box) => { + const parsedESDSBox = shaka.util.Mp4BoxParsers.parseESDS(box.reader); + codecs.push(parsedESDSBox.codec); + hasAudio = true; + }) + .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', (box) => { + const parsedAVCBox = + shaka.util.Mp4BoxParsers.parseAVC(box.reader, box.name); + codecs.push(parsedAVCBox.codec); + hasVideo = true; + }) + .box('avc3', (box) => { + const parsedAVCBox = + shaka.util.Mp4BoxParsers.parseAVC(box.reader, box.name); + codecs.push(parsedAVCBox.codec); + hasVideo = true; + }) + .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: SegmentUtils.filterDuplicateCodecs_(codecs).join(', '), + language: language, + height: height, + width: width, + channelCount: channelCount, + sampleRate: sampleRate, + }; + } + + /** + * @param {!Array.} codecs + * @return {!Array.} codecs + * @private + */ + static filterDuplicateCodecs_(codecs) { + // Filter out duplicate codecs. + const seen = new Set(); + const ret = []; + for (const codec of codecs) { + const shortCodec = shaka.util.MimeUtils.getCodecBase(codec); + if (!seen.has(shortCodec)) { + ret.push(codec); + seen.add(shortCodec); + } else { + shaka.log.debug('Ignoring duplicate codec'); + } + } + return ret; + } +}; + + +/** + * @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.util.SegmentUtils.BasicInfo;