diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 7474feb41f..39ad60291a 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -37,6 +37,7 @@ goog.require('shaka.util.OperationManager'); goog.require('shaka.util.Pssh'); goog.require('shaka.media.SegmentUtils'); goog.require('shaka.util.Timer'); +goog.require('shaka.util.TsParser'); goog.require('shaka.util.TXml'); goog.require('shaka.util.Platform'); goog.require('shaka.util.Uint8ArrayUtils'); @@ -753,6 +754,7 @@ shaka.hls.HlsParser = class { */ async parseManifest_(data, uri) { const Utils = shaka.hls.Utils; + const ContentType = shaka.util.ManifestParserUtils.ContentType; goog.asserts.assert(this.masterPlaylistUri_, 'Master playlist URI must be set before calling parseManifest_!'); @@ -802,26 +804,16 @@ shaka.hls.HlsParser = class { mediaVariables = this.parseMediaVariables_( variablesTags, this.masterPlaylistUri_); - // 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, getUris, mediaVariables); - mediaPlaylistType = basicInfo.type; - const mimeType = basicInfo.mimeType; - const codecs = basicInfo.codecs; - const languageValue = basicInfo.language; - const height = basicInfo.height; - const width = basicInfo.width; - const channelsCount = basicInfo.channelCount; - const sampleRate = basicInfo.sampleRate; - const closedCaptions = basicInfo.closedCaptions; - const videoRange = basicInfo.videoRange; - const colorGamut = basicInfo.colorGamut; - - // 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. + // By default we assume it is video, but in a later step the correct type + // is obtained. + mediaPlaylistType = ContentType.VIDEO; + + // These values can be obtained later so these default values are good. + const codecs = ''; + const languageValue = ''; + const channelsCount = null; + const sampleRate = null; + const closedCaptions = new Map(); const spatialAudio = false; const characteristics = null; const forced = false; // Only relevant for text. @@ -832,15 +824,10 @@ shaka.hls.HlsParser = class { const streamInfo = await this.convertParsedPlaylistIntoStreamInfo_( this.globalId_++, mediaVariables, playlist, getUris, uri, codecs, mediaPlaylistType, languageValue, primary, name, channelsCount, - closedCaptions, characteristics, forced, sampleRate, spatialAudio, - mimeType); + closedCaptions, characteristics, forced, sampleRate, spatialAudio); this.uriToStreamInfosMap_.set(uri, streamInfo); - if (mediaPlaylistType == 'video') { - this.addVideoAttributes_(streamInfo.stream, width, height, - /* frameRate= */ null, videoRange, /* videoLayout= */ null, - colorGamut); - } + mediaPlaylistType = streamInfo.stream.type; // Wrap the stream from that stream info with a variant. variants.push({ @@ -971,26 +958,21 @@ shaka.hls.HlsParser = class { } /** - * @param {shaka.hls.Playlist} playlist - * @param {function():!Array.} getUris - * @param {?Map.=} variables + * @param {!Array.} segments * @return {!Promise.} * @private */ - async getMediaPlaylistBasicInfo_(playlist, getUris, variables) { + async getBasicInfoFromSegments_(segments) { const HlsParser = shaka.hls.HlsParser; const defaultBasicInfo = shaka.media.SegmentUtils.getBasicInfoFromMimeType( this.config_.hls.mediaPlaylistFullMimeType); - if (!playlist.segments.length) { + if (!segments.length) { return defaultBasicInfo; } - const firstSegment = playlist.segments[0]; - const firstSegmentUris = shaka.hls.Utils.constructSegmentUris( - getUris(), - firstSegment.verbatimSegmentUri, - variables); - const firstSegmentUri = firstSegmentUris[0]; - const parsedUri = new goog.Uri(firstSegmentUri); + const segment = this.getAvailableSegment_(segments); + const segmentUris = segment.getUris(); + const segmentUri = segmentUris[0]; + const parsedUri = new goog.Uri(segmentUri); const extension = parsedUri.getPath().split('.').pop(); const rawMimeType = HlsParser.RAW_FORMATS_TO_MIME_TYPES_[extension]; if (rawMimeType) { @@ -1001,33 +983,28 @@ shaka.hls.HlsParser = class { const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT; let initData = null; - const initSegmentRef = this.getInitSegmentReference_( - playlist, firstSegment.tags, getUris, variables); - this.mapTagToInitSegmentRefMap_.clear(); + let initMimeType = null; + const initSegmentRef = segment.initSegmentReference; if (initSegmentRef) { const initSegmentRequest = shaka.util.Networking.createSegmentRequest( - initSegmentRef.getUris(), - initSegmentRef.getStartByte(), - initSegmentRef.getEndByte(), - this.config_.retryParameters); + initSegmentRef.getUris(), initSegmentRef.getStartByte(), + initSegmentRef.getEndByte(), this.config_.retryParameters); const initType = shaka.net.NetworkingEngine.AdvancedRequestType.INIT_SEGMENT; const initResponse = await this.makeNetworkRequest_( initSegmentRequest, requestType, {type: initType}); initData = initResponse.data; - } - let startByte = 0; - let endByte = null; - const byterangeTag = shaka.hls.Utils.getFirstTagWithName( - firstSegment.tags, 'EXT-X-BYTERANGE'); - if (byterangeTag) { - [startByte, endByte] = this.parseByteRange_( - /* previousReference= */ null, byterangeTag.value); + initMimeType = initResponse.headers['content-type']; + if (initMimeType) { + // Split the MIME type in case the server sent additional parameters. + initMimeType = initMimeType.split(';')[0].toLowerCase(); + } } const segmentRequest = shaka.util.Networking.createSegmentRequest( - firstSegmentUris, startByte, endByte, this.config_.retryParameters); + segment.getUris(), segment.getStartByte(), segment.getEndByte(), + this.config_.retryParameters); const type = shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_SEGMENT; const response = await this.makeNetworkRequest_( segmentRequest, requestType, {type}); @@ -1038,23 +1015,49 @@ shaka.hls.HlsParser = class { contentMimeType = contentMimeType.split(';')[0].toLowerCase(); } - if (extension == 'ts' || contentMimeType == 'video/mp2t') { + const validMp4Extensions = [ + 'mp4', + 'mp4a', + 'm4s', + 'm4i', + 'm4a', + 'm4f', + 'cmfa', + 'mp4v', + 'm4v', + 'cmfv', + 'fmp4', + ]; + const validMp4MimeType = [ + 'audio/mp4', + 'video/mp4', + 'video/iso.segment', + ]; + + if (shaka.util.TsParser.probe( + shaka.util.BufferUtils.toUint8(response.data))) { const basicInfo = shaka.media.SegmentUtils.getBasicInfoFromTs(response.data); if (basicInfo) { return basicInfo; } - } else if (extension == 'mp4' || extension == 'cmfv' || - extension == 'm4s' || extension == 'fmp4' || - contentMimeType == 'video/mp4' || - contentMimeType == 'audio/mp4' || - contentMimeType == 'video/iso.segment') { + } else if (validMp4Extensions.includes(extension) || + validMp4MimeType.includes(contentMimeType) || + (initMimeType && validMp4MimeType.includes(initMimeType))) { const basicInfo = shaka.media.SegmentUtils.getBasicInfoFromMp4( initData, response.data); if (basicInfo) { return basicInfo; } } + if (contentMimeType) { + return shaka.media.SegmentUtils.getBasicInfoFromMimeType( + contentMimeType); + } + if (initMimeType) { + return shaka.media.SegmentUtils.getBasicInfoFromMimeType( + initMimeType); + } return defaultBasicInfo; } @@ -2356,9 +2359,9 @@ shaka.hls.HlsParser = class { const mediaVariables = this.parseMediaVariables_(variablesTags, responseUri); - let mimeType = undefined; + const mimeType = undefined; - let closedCaptionsUpdated = false; + let requestBasicInfo = false; // If no codec info was provided in the manifest and codec guessing is // disabled we try to get necessary info from the media data. @@ -2366,40 +2369,21 @@ shaka.hls.HlsParser = class { this.config_.hls.disableCodecGuessing) || (this.needsClosedCaptionsDetection_ && type == ContentType.VIDEO && !this.config_.hls.disableClosedCaptionsDetection)) { - let canRequestBasicInfo = playlist.segments.length > 0; - if (canRequestBasicInfo) { - const segment = playlist.segments[0]; - if (shaka.hls.Utils.getFirstTagWithName(segment.tags, 'EXT-X-GAP')) { - canRequestBasicInfo = false; - } - } - if (canRequestBasicInfo) { + if (playlist.segments.length > 0) { this.needsClosedCaptionsDetection_ = false; - const basicInfo = await this.getMediaPlaylistBasicInfo_( - playlist, getUris, mediaVariables); - - goog.asserts.assert( - type === basicInfo.type, 'Media types should match!'); - - if (basicInfo.closedCaptions.size && (!closedCaptions || - closedCaptions.size != basicInfo.closedCaptions.size)) { - closedCaptions = basicInfo.closedCaptions; - closedCaptionsUpdated = true; - } - - if (!this.codecInfoInManifest_ && - this.config_.hls.disableCodecGuessing) { - mimeType = basicInfo.mimeType; - codecs = basicInfo.codecs; - } + requestBasicInfo = true; } } + const allowOverrideMimeType = !this.codecInfoInManifest_ && + this.config_.hls.disableCodecGuessing; + const wasLive = this.isLive_(); const realStreamInfo = await this.convertParsedPlaylistIntoStreamInfo_( streamId, mediaVariables, playlist, getUris, responseUri, codecs, type, languageValue, primary, name, channelsCount, closedCaptions, - characteristics, forced, sampleRate, spatialAudio, mimeType); + characteristics, forced, sampleRate, spatialAudio, mimeType, + requestBasicInfo, allowOverrideMimeType); if (abortSignal.aborted) { return; } @@ -2432,6 +2416,17 @@ shaka.hls.HlsParser = class { stream.codecs = stream.codecs || realStream.codecs; stream.closedCaptions = stream.closedCaptions || realStream.closedCaptions; + stream.width = stream.width || realStream.width; + stream.height = stream.height || realStream.height; + stream.hdr = stream.hdr || realStream.hdr; + stream.colorGamut = stream.colorGamut || realStream.colorGamut; + if (stream.language == 'und' && realStream.language != 'und') { + stream.language = realStream.language; + } + stream.language = stream.language || realStream.language; + stream.channelsCount = stream.channelsCount || realStream.channelsCount; + stream.audioSamplingRate = + stream.audioSamplingRate || realStream.audioSamplingRate; this.setFullTypeForStream_(stream); // Since we lazy-loaded this content, the player may need to create new @@ -2440,6 +2435,13 @@ shaka.hls.HlsParser = class { this.playerInterface_.newDrmInfo(stream); } + let closedCaptionsUpdated = false; + if ((!closedCaptions && stream.closedCaptions) || + (closedCaptions && stream.closedCaptions && + closedCaptions.size != stream.closedCaptions.size)) { + closedCaptionsUpdated = true; + } + if (this.manifest_ && closedCaptionsUpdated) { this.playerInterface_.makeTextStreamsForClosedCaptions(this.manifest_); } @@ -2674,13 +2676,16 @@ shaka.hls.HlsParser = class { * @param {?number} sampleRate * @param {boolean} spatialAudio * @param {(string|undefined)} mimeType + * @param {boolean=} requestBasicInfo + * @param {boolean=} allowOverrideMimeType * @return {!Promise.} * @private */ async convertParsedPlaylistIntoStreamInfo_(streamId, variables, playlist, getUris, responseUri, codecs, type, languageValue, primary, name, channelsCount, closedCaptions, characteristics, forced, sampleRate, - spatialAudio, mimeType = undefined) { + spatialAudio, mimeType = undefined, requestBasicInfo = true, + allowOverrideMimeType = true) { goog.asserts.assert(playlist.segments != null, 'Media playlist should have segments!'); @@ -2696,6 +2701,30 @@ shaka.hls.HlsParser = class { const {segments, bandwidth} = this.createSegments_( playlist, mediaSequenceToStartTime, variables, getUris, type); + let width = null; + let height = null; + let videoRange = null; + let colorGamut = null; + if (segments.length > 0 && requestBasicInfo) { + const basicInfo = await this.getBasicInfoFromSegments_(segments); + + type = basicInfo.type; + languageValue = basicInfo.language; + channelsCount = basicInfo.channelCount; + sampleRate = basicInfo.sampleRate; + closedCaptions = basicInfo.closedCaptions; + + height = basicInfo.height; + width = basicInfo.width; + videoRange = basicInfo.videoRange; + colorGamut = basicInfo.colorGamut; + + if (allowOverrideMimeType) { + mimeType = basicInfo.mimeType; + codecs = basicInfo.codecs; + } + } + if (!mimeType) { mimeType = await this.guessMimeType_(type, codecs, segments); } @@ -2722,6 +2751,13 @@ shaka.hls.HlsParser = class { } this.setFullTypeForStream_(stream); + if (type == shaka.util.ManifestParserUtils.ContentType.VIDEO && + (width || height || videoRange || colorGamut)) { + this.addVideoAttributes_(stream, width, height, + /* frameRate= */ null, videoRange, /* videoLayout= */ null, + colorGamut); + } + // This new calculation is necessary for Low Latency streams. if (this.isLive_()) { this.determineLastTargetDuration_(playlist); @@ -4118,29 +4154,41 @@ shaka.hls.HlsParser = class { } /** - * Attempts to guess stream's mime type. - * - * @param {string} contentType - * @param {string} codecs * @param {!Array.} segments - * @return {!Promise.} + * @return {!shaka.media.SegmentReference} * @private */ - async guessMimeType_(contentType, codecs, segments) { - const HlsParser = shaka.hls.HlsParser; - const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT; + getAvailableSegment_(segments) { + goog.asserts.assert(segments.length, 'Should have segments!'); // If you wait long enough, requesting the first segment can fail // because it has fallen off the left edge of DVR, so to be safer, // let's request the middle segment. - goog.asserts.assert(segments.length, 'Should have segments!'); - let segmentIndex = Math.trunc((segments.length - 1) / 2); + let segmentIndex = this.isLive_() ? + Math.trunc((segments.length - 1) / 2) : 0; let segment = segments[segmentIndex]; while (segment.status == shaka.media.SegmentReference.Status.MISSING && segmentIndex < segments.length) { segmentIndex ++; segment = segments[segmentIndex]; } + return segment; + } + + /** + * Attempts to guess stream's mime type. + * + * @param {string} contentType + * @param {string} codecs + * @param {!Array.} segments + * @return {!Promise.} + * @private + */ + async guessMimeType_(contentType, codecs, segments) { + const HlsParser = shaka.hls.HlsParser; + const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT; + + const segment = this.getAvailableSegment_(segments); if (segment.status == shaka.media.SegmentReference.Status.MISSING) { return this.guessMimeTypeFallback_(contentType);