diff --git a/README.md b/README.md index c9e2c61490..5efacbc652 100644 --- a/README.md +++ b/README.md @@ -123,12 +123,11 @@ HLS features supported: - CEA-608/708 captions - Encrypted content with PlayReady and Widevine - Encrypted content with FairPlay (Safari on macOS and iOS 12+ only) + - Raw AAC, MP3, etc (without an MP4 container) HLS features **not** supported: - Key rotation: https://github.com/google/shaka-player/issues/917 - I-frame-only playlists: https://github.com/google/shaka-player/issues/742 - - Raw AAC, MP3, etc (without an MP4 container): - https://github.com/google/shaka-player/issues/2337 - Low-latency streaming with blocking playlist reload [mux.js]: https://github.com/videojs/mux.js/releases diff --git a/demo/common/message_ids.js b/demo/common/message_ids.js index 802ae86dfa..d438601f8e 100644 --- a/demo/common/message_ids.js +++ b/demo/common/message_ids.js @@ -187,7 +187,6 @@ shakaDemo.MessageIds = { IGNORE_DASH_SUGGESTED_PRESENTATION_DELAY: 'DEMO_IGNORE_DASH_SUGGESTED_PRESENTATION_DELAY', IGNORE_HLS_IMAGE_FAILURES: 'DEMO_IGNORE_HLS_IMAGE_FAILURES', IGNORE_HLS_TEXT_FAILURES: 'DEMO_IGNORE_HLS_TEXT_FAILURES', - USE_FULL_SEGMENTS_FOR_START_TIME: 'DEMO_USE_FULL_SEGMENTS_FOR_START_TIME', IGNORE_MIN_BUFFER_TIME: 'DEMO_IGNORE_MIN_BUFFER_TIME', IGNORE_TEXT_FAILURES: 'DEMO_IGNORE_TEXT_FAILURES', INACCURATE_MANIFEST_TOLERANCE: 'DEMO_INACCURATE_MANIFEST_TOLERANCE', diff --git a/demo/config.js b/demo/config.js index c29736c2d5..5c647f57dc 100644 --- a/demo/config.js +++ b/demo/config.js @@ -209,8 +209,6 @@ shakaDemo.Config = class { 'manifest.hls.ignoreTextStreamFailures') .addBoolInput_(MessageIds.IGNORE_HLS_IMAGE_FAILURES, 'manifest.hls.ignoreImageStreamFailures') - .addBoolInput_(MessageIds.USE_FULL_SEGMENTS_FOR_START_TIME, - 'manifest.hls.useFullSegmentsForStartTime') .addTextInput_(MessageIds.DEFAULT_AUDIO_CODEC, 'manifest.hls.defaultAudioCodec') .addTextInput_(MessageIds.DEFAULT_VIDEO_CODEC, diff --git a/demo/locales/en.json b/demo/locales/en.json index 680a34cb0e..9715d8ed91 100644 --- a/demo/locales/en.json +++ b/demo/locales/en.json @@ -221,7 +221,6 @@ "DEMO_UPDATE_EXPIRATION_TIME": "Update expiration time", "DEMO_UPDATE_INTERVAL_SECONDS": "Update interval seconds", "DEMO_UPLYNK": "Verizon Digital Media Services", - "DEMO_USE_FULL_SEGMENTS_FOR_START_TIME": "Use Full Segments For Start Time", "DEMO_USE_HEADERS": "Use Headers", "DEMO_USE_NATIVE_HLS_SAFARI": "Use native HLS on Safari", "DEMO_USE_PERSISTENT_LICENSES": "Use Persistent Licenses", diff --git a/demo/locales/source.json b/demo/locales/source.json index 6009153570..7a5a8ebcad 100644 --- a/demo/locales/source.json +++ b/demo/locales/source.json @@ -395,10 +395,6 @@ "description": "The label on a field that allows users to provide a video id for a custom asset.", "message": "Video ID (for VOD DAI Content)" }, - "DEMO_USE_FULL_SEGMENTS_FOR_START_TIME": { - "description": "The name of a configuration value.", - "message": "Use Full Segments For Start Time" - }, "DEMO_IGNORE_MIN_BUFFER_TIME": { "description": "The name of a configuration value.", "message": "Ignore Min Buffer Time" diff --git a/docs/tutorials/faq.md b/docs/tutorials/faq.md index 5b84d6a72e..7f85922c9a 100644 --- a/docs/tutorials/faq.md +++ b/docs/tutorials/faq.md @@ -43,17 +43,6 @@ headers in the response. Additionally, with some manifests, we will send a This can also happen with mixed-content restrictions. If the site is using `https:`, then your manifest and segments must also. -*Sending `Range` header at the start of HLS playback can be disabled using this config:* -``` -player.configure({ - manifest: { - hls: { - useFullSegmentsForStartTime: true, - }, - }, -}) -``` -
**Q:** I am getting `REQUESTED_KEY_SYSTEM_CONFIG_UNAVAILABLE` or error code diff --git a/externs/shaka/manifest.js b/externs/shaka/manifest.js index a4f055fed8..4c86e54ea9 100644 --- a/externs/shaka/manifest.js +++ b/externs/shaka/manifest.js @@ -17,7 +17,8 @@ * textStreams: !Array., * imageStreams: !Array., * offlineSessionIds: !Array., - * minBufferTime: number + * minBufferTime: number, + * sequenceMode: boolean * }} * * @description @@ -72,6 +73,9 @@ * The minimum number of seconds of content that must be buffered before * playback can begin. Can be overridden by a higher value from the Player * configuration. + * @property {boolean} sequenceMode + * If true, we will append the media segments using sequence mode; that is to + * say, ignoring any timestamps inside the media files. * * @exportDoc */ diff --git a/externs/shaka/offline.js b/externs/shaka/offline.js index e55c7bfa1d..54a7534dad 100644 --- a/externs/shaka/offline.js +++ b/externs/shaka/offline.js @@ -73,7 +73,8 @@ shaka.extern.StoredContent; * sessionIds: !Array., * drmInfo: ?shaka.extern.DrmInfo, * appMetadata: Object, - * isIncomplete: (boolean|undefined) + * isIncomplete: (boolean|undefined), + * sequenceMode: (boolean|undefined) * }} * * @property {number} creationTime @@ -98,6 +99,9 @@ shaka.extern.StoredContent; * A metadata object passed from the application. * @property {(boolean|undefined)} isIncomplete * If true, the content is still downloading. + * @property {(boolean|undefined)} sequenceMode + * If true, we will append the media segments using sequence mode; that is to + * say, ignoring any timestamps inside the media files. */ shaka.extern.ManifestDB; diff --git a/externs/shaka/player.js b/externs/shaka/player.js index 5b17095be3..444d7e89b0 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -746,7 +746,6 @@ shaka.extern.DashManifestConfiguration; * @typedef {{ * ignoreTextStreamFailures: boolean, * ignoreImageStreamFailures: boolean, - * useFullSegmentsForStartTime: boolean, * defaultAudioCodec: string, * defaultVideoCodec: string * }} @@ -757,9 +756,6 @@ shaka.extern.DashManifestConfiguration; * @property {boolean} ignoreImageStreamFailures * If true, ignore any errors in a image stream and filter out * those streams. - * @property {boolean} useFullSegmentsForStartTime - * If true, force HlsParser to use a full segment request for - * determining start time in case the server does not support partial requests * @property {string} defaultAudioCodec * The default audio codec if it is not specified in the HLS playlist. * Defaults to 'mp4a.40.2'. diff --git a/externs/sourcebuffer.js b/externs/sourcebuffer.js new file mode 100644 index 0000000000..843113da4a --- /dev/null +++ b/externs/sourcebuffer.js @@ -0,0 +1,15 @@ +/*! @license + * Shaka Player + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Externs for SourceBuffer which are missing from the Closure + * compiler. + * + * @externs + */ + +/** @type {string} */ +SourceBuffer.prototype.mode; diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 08cc1f2318..56b90d12dc 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -497,6 +497,7 @@ shaka.dash.DashParser = class { imageStreams: this.periodCombiner_.getImageStreams(), offlineSessionIds: [], minBufferTime: minBufferTime || 0, + sequenceMode: false, }; // We only need to do clock sync when we're using presentation start diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 0d60e27e7a..f7fcebc81d 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -26,7 +26,6 @@ goog.require('shaka.net.NetworkingEngine'); goog.require('shaka.util.ArrayUtils'); goog.require('shaka.util.BufferUtils'); goog.require('shaka.util.CmcdManager'); -goog.require('shaka.util.DataViewReader'); goog.require('shaka.util.Error'); goog.require('shaka.util.FakeEvent'); goog.require('shaka.util.Functional'); @@ -34,9 +33,6 @@ goog.require('shaka.util.Iterables'); goog.require('shaka.util.LanguageUtils'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.MimeUtils'); -goog.require('shaka.util.Mp4Parser'); -goog.require('shaka.util.Mp4BoxParsers'); -goog.require('shaka.util.Networking'); goog.require('shaka.util.OperationManager'); goog.require('shaka.util.Pssh'); goog.require('shaka.util.Timer'); @@ -97,9 +93,6 @@ shaka.hls.HlsParser = class { * createStreamInfoFromMediaTag_, createStreamInfoFromImageTag_ and * createStreamInfoFromVariantTag_. * - * During parsing of updates, used by getStartTime_ to determine the start - * time of the first segment from existing segment references. - * * @private {!Map.} */ this.uriToStreamInfosMap_ = new Map(); @@ -177,9 +170,6 @@ shaka.hls.HlsParser = class { /** @private {Map.} */ this.groupIdToCodecsMap_ = new Map(); - /** @private {?number} */ - this.playlistStartTime_ = null; - /** A cache mapping EXT-X-MAP tag info to the InitSegmentReference created * from the tag. * The key is a string combining the EXT-X-MAP tag's absolute uri, and @@ -187,15 +177,6 @@ shaka.hls.HlsParser = class { * {!Map.} */ this.mapTagToInitSegmentRefMap_ = new Map(); - /** - * A cache mapping a discontinuity sequence number of a segment with - * EXT-X-DISCONTINUITY tag into its timestamp offset. - * Key: the discontinuity sequence number of a segment - * Value: the segment reference's timestamp offset. - * {!Map.} - */ - this.discontinuityToTso_ = new Map(); - /** @private {boolean} */ this.lowLatencyMode_ = false; } @@ -279,8 +260,6 @@ shaka.hls.HlsParser = class { /** @type {!Array.} */ const updates = []; - // Reset the start time for the new media playlist. - this.playlistStartTime_ = null; const streamInfos = Array.from(this.uriToStreamInfosMap_.values()); // Wait for the first stream info created, so that the start time is fetched // and can be reused. @@ -331,11 +310,10 @@ shaka.hls.HlsParser = class { const stream = streamInfo.stream; - const segments = await this.createSegments_( + const segments = this.createSegments_( streamInfo.verbatimMediaPlaylistUri, playlist, stream.type, stream.mimeType, streamInfo.mediaSequenceToStartTime, mediaVariables, - streamInfo.discontinuityToMediaSequence, stream.codecs, - stream.bandwidth); + stream.codecs, stream.bandwidth); stream.segmentIndex.mergeAndEvict( segments, this.presentationTimeline_.getSegmentAvailabilityStart()); @@ -566,6 +544,7 @@ shaka.hls.HlsParser = class { imageStreams, offlineSessionIds: [], minBufferTime: 0, + sequenceMode: true, }; this.playerInterface_.makeTextStreamsForClosedCaptions(this.manifest_); } @@ -1463,31 +1442,20 @@ shaka.hls.HlsParser = class { } // MediaSource expects no codec strings combined with raw formats. - // TODO(#2337): Instead, create a Stream flag indicating a raw format. - if (shaka.hls.HlsParser.RAW_FORMATS_.includes(mimeType)) { + if (shaka.hls.HlsParser.RAW_FORMATS.includes(mimeType)) { + // TODO(#2337): Translate the raw codecs string to a corresponding + // containered version, so that audio-only raw format streams can work. codecs = ''; } /** @type {!Map.} */ const mediaSequenceToStartTime = new Map(); - /** - * A map of a discontinuity sequence number, to the first segment's media - * sequence number with the discontinuity sequence number. - * Key: the discontinuity sequence number of a few segments - * Value: the first segment's media sequence number of the segments with - * this discontinuity sequence number. - * Used to get the discontinuity sequence number with playlist delta - * updates with lowLatencyMode enabled. - * {!Map.} - */ - const discontinuityToMediaSequence = new Map(); - let segments; try { - segments = await this.createSegments_(verbatimMediaPlaylistUri, + segments = this.createSegments_(verbatimMediaPlaylistUri, playlist, type, mimeType, mediaSequenceToStartTime, mediaVariables, - discontinuityToMediaSequence, codecs, bandwidth); + codecs, bandwidth); } catch (error) { if (error.code == shaka.util.Error.Code.HLS_INTERNAL_SKIP_STREAM) { shaka.log.alwaysWarn('Skipping unsupported HLS stream', @@ -1559,7 +1527,6 @@ shaka.hls.HlsParser = class { minTimestamp, maxTimestamp: lastEndTime, mediaSequenceToStartTime, - discontinuityToMediaSequence, canSkipSegments, }; } @@ -1758,6 +1725,13 @@ shaka.hls.HlsParser = class { let startByte = 0; let endByte = null; + if (hlsSegment.partialSegments.length && !this.lowLatencyMode_) { + shaka.log.alwaysWarn('Low-latency HLS live stream detected, but ' + + 'low-latency streaming mode is not enabled in Shaka ' + + 'Player. Set streaming.lowLatencyMode configuration to ' + + 'true, and see https://bit.ly/3clctcj for details.'); + } + // Create SegmentReferences for the partial segments. const partialSegmentRefs = []; if (this.lowLatencyMode_ && hlsSegment.partialSegments.length) { @@ -1920,15 +1894,13 @@ shaka.hls.HlsParser = class { * @param {string} mimeType * @param {!Map.} mediaSequenceToStartTime * @param {!Map.} variables - * @param {!Map.} discontinuityToMediaSequence * @param {string} codecs * @param {(number|undefined)} bandwidth - * @return {!Promise>} + * @return {!Array.} * @private */ - async createSegments_(verbatimMediaPlaylistUri, playlist, type, mimeType, - mediaSequenceToStartTime, variables, discontinuityToMediaSequence, - codecs, bandwidth) { + createSegments_(verbatimMediaPlaylistUri, playlist, type, mimeType, + mediaSequenceToStartTime, variables, codecs, bandwidth) { /** @type {Array.} */ const hlsSegments = playlist.segments; goog.asserts.assert(hlsSegments.length, 'Playlist should have segments!'); @@ -1945,69 +1917,26 @@ shaka.hls.HlsParser = class { const skippedSegments = skipTag ? Number(skipTag.getAttributeValue('SKIPPED-SEGMENTS')) : 0; let position = mediaSequenceNumber + skippedSegments; - let firstStartTime; + let firstStartTime = 0; + // For live stream, use the cached value in the mediaSequenceToStartTime // map if available. - // Since createSegments_() is asynchronous and we are updating the streams - // in parallel, the global playlistStartTime_ may get updated by other - // playlist updates rather than the current one. - if (this.isLive_() && mediaSequenceToStartTime.has(position)) { firstStartTime = mediaSequenceToStartTime.get(position); - } else { - if (this.playlistStartTime_ == null) { - // For VOD and EVENT playlists, all variants must start at the same - // time, so we can fetch the start time once and reuse for the others. - // This is not guaranteed when updating a LIVE stream. We assume the - // first segment in each live playlist is no more than one segment out - // of sync with the other playlists, so we can fetch the start time for - // once. - initSegmentRef = this.getInitSegmentReference_( - playlist.absoluteUri, hlsSegments[0].tags, variables); - goog.asserts.assert( - type != shaka.util.ManifestParserUtils.ContentType.TEXT && - type != shaka.util.ManifestParserUtils.ContentType.IMAGE, - 'Should only get start time from audio or video streams'); - this.playlistStartTime_ = await this.getStartTime_( - verbatimMediaPlaylistUri, initSegmentRef, mimeType, - position, /* isDiscontinuity= */ false, - hlsSegments[0], variables, type, codecs, bandwidth); - } - firstStartTime = this.playlistStartTime_; } const firstSegmentUri = hlsSegments[0].absoluteUri; shaka.log.debug('First segment', firstSegmentUri.split('/').pop(), 'starts at', firstStartTime); - let discontintuitySequenceNum = shaka.hls.Utils.getFirstTagWithNameAsNumber( - playlist.tags, 'EXT-X-DISCONTINUITY-SEQUENCE'); - if (this.lowLatencyMode_) { - if (!discontinuityToMediaSequence.has(discontintuitySequenceNum)) { - discontinuityToMediaSequence.set(discontintuitySequenceNum, position); - } - if (skippedSegments) { - // With delta updates, the DISCONTINUITY may be skipped. Check if - // the discontintuity Sequence Number based on the media sequence - // number. - while (discontinuityToMediaSequence.has(discontintuitySequenceNum + 1) - && discontinuityToMediaSequence.get(discontintuitySequenceNum + 1) < - position) { - discontintuitySequenceNum++; - } - } - } - let timestampOffset = - this.discontinuityToTso_.get(discontintuitySequenceNum) || 0; - /** @type {!Array.} */ const references = []; const enumerate = (it) => shaka.util.Iterables.enumerate(it); for (const {i, item} of enumerate(hlsSegments)) { const previousReference = references[references.length - 1]; - const startTime = (i == 0) ? firstStartTime : - previousReference.endTime; + const startTime = + (i == 0) ? firstStartTime : previousReference.endTime; position = mediaSequenceNumber + skippedSegments + i; mediaSequenceToStartTime.set(position, startTime); @@ -2015,19 +1944,6 @@ shaka.hls.HlsParser = class { initSegmentRef = this.getInitSegmentReference_(playlist.absoluteUri, item.tags, variables); - const discontintuityTag = shaka.hls.Utils.getFirstTagWithName(item.tags, - 'EXT-X-DISCONTINUITY'); - if (discontintuityTag) { - discontintuitySequenceNum++; - discontinuityToMediaSequence.set(discontintuitySequenceNum, position); - - // eslint-disable-next-line no-await-in-loop - timestampOffset = await this.getTimestampOffset_( - discontintuitySequenceNum, verbatimMediaPlaylistUri, initSegmentRef, - mimeType, position, item, variables, startTime, type, codecs, - bandwidth); - } - // If the stream is low latency and the user has not configured the // lowLatencyMode, but if it has been configured to activate the // lowLatencyMode if a stream of this type is detected, we automatically @@ -2040,514 +1956,20 @@ shaka.hls.HlsParser = class { } } - const extinfTag = - shaka.hls.Utils.getFirstTagWithName(item.tags, 'EXTINF'); - if (this.lowLatencyMode_ || extinfTag) { - const reference = this.createSegmentReference_( - initSegmentRef, - previousReference, - item, - startTime, - timestampOffset, - variables, - playlist.absoluteUri, - type); - - references.push(reference); - } else if (!this.lowLatencyMode_) { - // If a segment has no extinfTag, it must contain partial segments. - shaka.log.alwaysWarn('Low-latency HLS live stream detected, but ' + - 'low-latency streaming mode is not enabled in Shaka Player. ' + - 'Set streaming.lowLatencyMode configuration to true, and see ' + - 'https://bit.ly/3clctcj for details.'); - } - } + const reference = this.createSegmentReference_( + initSegmentRef, + previousReference, + item, + startTime, + firstStartTime, + variables, + playlist.absoluteUri, + type); - return references; - } - - /** - * Gets the start time of the first segment of the playlist from existing - * value (if possible) or by downloading it and parsing it otherwise. - * - * @param {number} discontintuitySequenceNum - * @param {string} verbatimMediaPlaylistUri - * @param {shaka.media.InitSegmentReference} initSegmentRef - * @param {string} mimeType - * @param {number} mediaSequenceNumber - * @param {!shaka.hls.Segment} segment - * @param {!Map.} variables - * @param {number} startTime - * @param {string} type - * @param {string} codecs - * @param {number|undefined} bandwidth - * @return {!Promise.} - * @throws {shaka.util.Error} - * @private - */ - async getTimestampOffset_(discontintuitySequenceNum, - verbatimMediaPlaylistUri, initSegmentRef, - mimeType, mediaSequenceNumber, segment, variables, startTime, type, - codecs, bandwidth) { - let timestampOffset = 0; - if (this.discontinuityToTso_.has(discontintuitySequenceNum)) { - timestampOffset = - this.discontinuityToTso_.get(discontintuitySequenceNum); - } else { - const mediaStartTime = await this.getStartTime_( - verbatimMediaPlaylistUri, initSegmentRef, mimeType, - mediaSequenceNumber, /* isDiscontinuity= */ true, segment, - variables, type, codecs, bandwidth); - timestampOffset = startTime - mediaStartTime; - shaka.log.v1('Segment timestampOffset =', timestampOffset); - this.discontinuityToTso_.set( - discontintuitySequenceNum, timestampOffset); - } - return timestampOffset; - } - - /** - * Try to fetch the starting part of a segment, and fall back to a full - * segment if we have to. - * - * @param {!shaka.media.AnySegmentReference} reference - * @param {string} type - * @param {string} mimeType - * @param {string} codecs - * @param {number|undefined} bandwidth - * @return {!Promise.} - * @private - */ - async fetchStartOfSegment_(reference, type, mimeType, codecs, bandwidth) { - const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT; - - // Create two requests: - // 1. A partial request meant to fetch the smallest part of the segment - // required to get the time stamp. - // 2. A full request meant as a fallback for when the server does not - // support partial requests. - const fullRequest = shaka.util.Networking.createSegmentRequest( - reference.getUris(), - reference.startByte, - reference.endByte, - this.config_.retryParameters); - - // We can only add partial CMCD data here because the stream - // and manifest objects are still being created - this.playerInterface_.modifySegmentRequest( - fullRequest, - { - type: type, - init: reference instanceof shaka.media.InitSegmentReference, - duration: reference.endTime - reference.startTime, - mimeType: mimeType, - codecs: codecs, - bandwidth: bandwidth, - }, - ); - - if (this.config_.hls.useFullSegmentsForStartTime) { - return this.makeNetworkRequest_(fullRequest, requestType); - } - - const partialRequest = shaka.util.Networking.createSegmentRequest( - reference.getUris(), - reference.startByte, - reference.startByte + shaka.hls.HlsParser.START_OF_SEGMENT_SIZE_ - 1, - this.config_.retryParameters); - - this.playerInterface_.modifySegmentRequest( - partialRequest, - { - type: type, - init: reference instanceof shaka.media.InitSegmentReference, - duration: reference.endTime - reference.startTime, - mimeType: mimeType, - codecs: codecs, - bandwidth: bandwidth, - }, - ); - - // TODO(vaage): The need to do fall back requests is not likely to be unique - // to here. It would be nice if the fallback(s) could be included into - // the same abortable operation as the original request. - // - // What would need to change with networking engine to support requests - // with fallback(s)? - try { - const response = await this.makeNetworkRequest_( - partialRequest, requestType); - - return response; - } catch (e) { - // If the networking operation was aborted, we don't want to treat it as - // a request failure. We surface the error so that the OPERATION_ABORTED - // error will be handled correctly. - if (e.code == shaka.util.Error.Code.OPERATION_ABORTED) { - throw e; - } - - // The partial request may fail for a number of reasons. - // Some servers do not support Range requests, and others do not support - // the OPTIONS request which must be made before any cross-origin Range - // request. Since this fallback is expensive, warn the app developer. - shaka.log.alwaysWarn('Unable to fetch the starting part of HLS ' + - 'segment! Falling back to a full segment request, ' + - 'which is expensive! Your server should ' + - 'support Range requests and CORS preflights.', - partialRequest.uris[0]); - - const response = await this.makeNetworkRequest_(fullRequest, requestType); - - return response; + references.push(reference); } - } - /** - * Gets the start time of a segment from the existing manifest (if possible) - * or by downloading it and parsing it otherwise. - * - * @param {string} verbatimMediaPlaylistUri - * @param {shaka.media.InitSegmentReference} initSegmentRef - * @param {string} mimeType - * @param {number} mediaSequenceNumber - * @param {boolean} isDiscontinuity - * @param {!shaka.hls.Segment} segment - * @param {!Map.} variables - * @param {string} type - * @param {string} codecs - * @param {number|undefined} bandwidth - * @return {!Promise.} - * @private - */ - async getStartTime_( - verbatimMediaPlaylistUri, initSegmentRef, mimeType, mediaSequenceNumber, - isDiscontinuity, segment, variables, type, codecs, bandwidth) { - const segmentRef = this.createSegmentReference_( - initSegmentRef, - /* previousReference= */ null, - segment, - /* startTime= */ 0, - /* timestampOffset= */ 0, - variables, - /* absoluteMediaPlaylistUri= */ '', - type); - // If we are updating the manifest, we can usually skip fetching the segment - // by examining the references we already have. This won't be possible if - // there was some kind of lag or delay updating the manifest on the server, - // in which extreme case we would fall back to fetching a segment. This - // allows us to both avoid fetching segments when possible, and recover from - // certain server-side issues gracefully. - // Do not use cached start time for the segments with discontinuity tags. - if (this.manifest_ && !isDiscontinuity) { - const streamInfo = - this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri); - const startTime = streamInfo.mediaSequenceToStartTime.get( - mediaSequenceNumber); - if (startTime != undefined) { - // We found it! Avoid fetching and parsing the segment. - shaka.log.v1('Found segment start time in previous manifest', - startTime); - return startTime; - } - - shaka.log.debug( - 'Unable to find segment start time in previous manifest!'); - } - - // TODO: Introduce a new tag to extend HLS and provide the first segment's - // start time. This will avoid the need for these fetches in content - // packaged with Shaka Packager. This web-friendly extension to HLS can - // then be proposed to Apple for inclusion in a future version of HLS. - // See https://github.com/google/shaka-packager/issues/294 - - shaka.log.v1('Fetching segment to find start time'); - mimeType = mimeType.toLowerCase(); - - if (shaka.hls.HlsParser.RAW_FORMATS_.includes(mimeType)) { - // Raw formats contain no timestamps. Even if there is an ID3 tag with a - // timestamp, that's not going to be honored by MediaSource, which will - // use sequence mode for these segments. We don't yet support sequence - // mode, so we must reject these streams. - // TODO(#2337): Support sequence mode and align raw format timestamps to - // other streams. - shaka.log.alwaysWarn( - 'Raw formats are not yet supported. Skipping ' + mimeType); - throw new shaka.util.Error( - shaka.util.Error.Severity.RECOVERABLE, - shaka.util.Error.Category.MANIFEST, - shaka.util.Error.Code.HLS_INTERNAL_SKIP_STREAM); - } - - if (mimeType == 'video/webm') { - shaka.log.alwaysWarn('WebM in HLS is not yet supported. Skipping.'); - throw new shaka.util.Error( - shaka.util.Error.Severity.RECOVERABLE, - shaka.util.Error.Category.MANIFEST, - shaka.util.Error.Code.HLS_INTERNAL_SKIP_STREAM); - } - - if (mimeType == 'video/mp4' || mimeType == 'audio/mp4') { - // We also need the init segment to get the correct timescale. But if the - // stream is self-initializing, use the same response for both. - const fetches = [this.fetchStartOfSegment_( - segmentRef, - type, - mimeType, - codecs, - bandwidth, - )]; - - if (initSegmentRef) { - fetches.push(this.fetchStartOfSegment_( - initSegmentRef, - type, - mimeType, - codecs, - bandwidth, - )); - } - - const responses = await Promise.all(fetches); - - // If the stream is self-initializing, use the main segment in-place of - // the init segment. - const segmentResponse = responses[0]; - const initSegmentResponse = responses[1] || responses[0]; - - return this.getStartTimeFromMp4Segment_( - verbatimMediaPlaylistUri, segmentResponse.uri, - segmentResponse.data, initSegmentResponse.data); - } - - if (mimeType == 'video/mp2t') { - const response = await this.fetchStartOfSegment_( - segmentRef, - type, - mimeType, - codecs, - bandwidth, - ); - goog.asserts.assert(response.data, 'Should have a response body!'); - return this.getStartTimeFromTsSegment_( - verbatimMediaPlaylistUri, response.uri, response.data); - } - - throw new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.MANIFEST, - shaka.util.Error.Code.HLS_COULD_NOT_PARSE_SEGMENT_START_TIME, - verbatimMediaPlaylistUri); - } - - /** - * Parses an mp4 segment to get its start time. - * - * @param {string} playlistUri - * @param {string} segmentUri - * @param {BufferSource} mediaData - * @param {BufferSource} initData - * @return {number} - * @private - */ - getStartTimeFromMp4Segment_(playlistUri, segmentUri, mediaData, initData) { - const Mp4Parser = shaka.util.Mp4Parser; - - let timescale = 0; - new Mp4Parser() - .box('moov', Mp4Parser.children) - .box('trak', Mp4Parser.children) - .box('mdia', Mp4Parser.children) - .fullBox('mdhd', (box) => { - goog.asserts.assert( - box.version == 0 || box.version == 1, - 'MDHD version can only be 0 or 1'); - - const parsedMDHDBox = shaka.util.Mp4BoxParsers.parseMDHD( - box.reader, box.version); - - timescale = parsedMDHDBox.timescale; - box.parser.stop(); - }).parse(initData, /* partialOkay= */ true); - - if (!timescale) { - shaka.log.error('Unable to find timescale in init segment!'); - throw new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.MANIFEST, - shaka.util.Error.Code.HLS_COULD_NOT_PARSE_SEGMENT_START_TIME, - playlistUri, segmentUri); - } - - let startTime = 0; - let parsedMedia = false; - new Mp4Parser() - .box('moof', Mp4Parser.children) - .box('traf', Mp4Parser.children) - .fullBox('tfdt', (box) => { - goog.asserts.assert( - box.version == 0 || box.version == 1, - 'TFDT version can only be 0 or 1'); - - const parsedTFDTBox = shaka.util.Mp4BoxParsers.parseTFDT( - box.reader, box.version); - const baseTime = parsedTFDTBox.baseMediaDecodeTime; - startTime = baseTime / timescale; - parsedMedia = true; - box.parser.stop(); - }).parse(mediaData, /* partialOkay= */ true); - - if (!parsedMedia) { - throw new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.MANIFEST, - shaka.util.Error.Code.HLS_COULD_NOT_PARSE_SEGMENT_START_TIME, - playlistUri, segmentUri); - } - return startTime; - } - - /** - * Parses a TS segment to get its start time. - * - * @param {string} playlistUri - * @param {string} segmentUri - * @param {BufferSource} data - * @return {number} - * @private - */ - getStartTimeFromTsSegment_(playlistUri, segmentUri, data) { - const reader = new shaka.util.DataViewReader( - data, shaka.util.DataViewReader.Endianness.BIG_ENDIAN); - - const fail = () => { - throw new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.MANIFEST, - shaka.util.Error.Code.HLS_COULD_NOT_PARSE_SEGMENT_START_TIME, - playlistUri, segmentUri); - }; - - let packetStart = 0; - let syncByte = 0; - - const skipPacket = () => { - // 188-byte packets are standard, so assume that. - reader.seek(packetStart + 188); - syncByte = reader.readUint8(); - if (syncByte != 0x47) { - // We haven't found the sync byte, so try it as a 192-byte packet. - reader.seek(packetStart + 192); - syncByte = reader.readUint8(); - } - if (syncByte != 0x47) { - // We still haven't found the sync byte, so try as a 204-byte packet. - reader.seek(packetStart + 204); - syncByte = reader.readUint8(); - } - if (syncByte != 0x47) { - // We still haven't found the sync byte, so the packet was of a - // non-standard size. - fail(); - } - // Put the sync byte back so we can read it in the next loop. - reader.rewind(1); - }; - - // We will look a few packet-lengths forward to find the first sync byte. - // Note that we are using this method on what is already a subset of the - // file (the first |shaka.hls.HlsParser.START_OF_SEGMENT_SIZE_| bytes), so - // we can't look too far ahead to begin with. - let syncByteScanLength = Math.min(reader.getLength() - 188, 5 * 188); - - // TODO: refactor this while loop for better readability. - // eslint-disable-next-line no-constant-condition - while (true) { - // Format reference: https://bit.ly/TsPacket - packetStart = reader.getPosition(); - - syncByte = reader.readUint8(); - if (syncByte != 0x47) { - if (syncByteScanLength > 0) { - // This file could have started with a cut-off TS packet. Scan forward - // until we find a sync byte. - syncByteScanLength -= 1; - continue; - } - fail(); - } - // If we've found a sync byte, stop scanning forward for future packets. - syncByteScanLength = 0; - - const flagsAndPacketId = reader.readUint16(); - const packetId = flagsAndPacketId & 0x1fff; - if (packetId == 0x1fff) { - // A "null" TS packet. Skip this TS packet and try again. - skipPacket(); - continue; - } - - const hasPesPacket = flagsAndPacketId & 0x4000; - if (!hasPesPacket) { - // Not a PES packet yet. Skip this TS packet and try again. - skipPacket(); - continue; - } - - const flags = reader.readUint8(); - const adaptationFieldControl = (flags & 0x30) >> 4; - if (adaptationFieldControl == 0 /* reserved */ || - adaptationFieldControl == 2 /* adaptation field, no payload */) { - fail(); - } - - if (adaptationFieldControl == 3) { - // Skip over adaptation field. - const length = reader.readUint8(); - reader.skip(length); - } - - // Now we come to the PES header (hopefully). - // Format reference: https://bit.ly/TsPES - const startCode = reader.readUint32(); - const startCodePrefix = startCode >> 8; - if (startCodePrefix != 1) { - // Not a PES packet yet. Skip this TS packet and try again. - skipPacket(); - continue; - } - - // Skip the 16-bit PES length and the first 8 bits of the optional header. - reader.skip(3); - // The next 8 bits contain flags about DTS & PTS. - const ptsDtsIndicator = reader.readUint8() >> 6; - if (ptsDtsIndicator == 0 /* no timestamp */ || - ptsDtsIndicator == 1 /* forbidden */) { - fail(); - } - - const pesHeaderLengthRemaining = reader.readUint8(); - if (pesHeaderLengthRemaining == 0) { - fail(); - } - - if (ptsDtsIndicator == 2 /* PTS only */) { - goog.asserts.assert(pesHeaderLengthRemaining == 5, 'Bad PES header?'); - } else if (ptsDtsIndicator == 3 /* PTS and DTS */) { - goog.asserts.assert(pesHeaderLengthRemaining == 10, 'Bad PES header?'); - } - - const pts0 = reader.readUint8(); - const pts1 = reader.readUint16(); - const pts2 = reader.readUint16(); - // Reconstruct 33-bit PTS from the 5-byte, padded structure. - const ptsHigh3 = (pts0 & 0x0e) >> 1; - const ptsLow30 = ((pts1 & 0xfffe) << 14) | ((pts2 & 0xfffe) >> 1); - // Reconstruct the PTS as a float. Avoid bitwise operations to combine - // because bitwise ops treat the values as 32-bit ints. - const pts = ptsHigh3 * (1 << 30) + ptsLow30; - return pts / shaka.hls.HlsParser.TS_TIMESCALE_; - } + return references; } /** @@ -2924,7 +2346,6 @@ shaka.hls.HlsParser = class { * minTimestamp: number, * maxTimestamp: number, * mediaSequenceToStartTime: !Map., - * discontinuityToMediaSequence: !Map., * canSkipSegments: boolean * }} * @@ -2947,9 +2368,6 @@ shaka.hls.HlsParser = class { * The maximum timestamp found in the stream. * @property {!Map.} mediaSequenceToStartTime * A map of media sequence numbers to media start times. - * @property {!Map.} discontinuityToMediaSequence - * A map of discontinuity sequence numbers to the media sequence number of the - * segment starting with that discontinuity sequence number. * @property {boolean} canSkipSegments * True if the server supports delta playlist updates, and we can send a * request for a playlist that can skip older media segments. @@ -2995,12 +2413,10 @@ shaka.hls.HlsParser.AUDIO_EXTENSIONS_TO_MIME_TYPES_ = { /** * MIME types of raw formats. - * TODO(#2337): Support raw formats and share this list among parsers. * * @const {!Array.} - * @private */ -shaka.hls.HlsParser.RAW_FORMATS_ = [ +shaka.hls.HlsParser.RAW_FORMATS = [ 'audio/aac', 'audio/ac3', 'audio/ec3', @@ -3096,24 +2512,6 @@ shaka.hls.HlsParser.PresentationType_ = { }; -/** - * @const {number} - * @private - */ -shaka.hls.HlsParser.TS_TIMESCALE_ = 90000; - - -/** - * The amount of data from the start of a segment we will try to fetch when we - * need to know the segment start time. This allows us to avoid fetching the - * entire segment in many cases. - * - * @const {number} - * @private - */ -shaka.hls.HlsParser.START_OF_SEGMENT_SIZE_ = 2048; - - shaka.media.ManifestParser.registerParserByExtension( 'm3u8', () => new shaka.hls.HlsParser()); shaka.media.ManifestParser.registerParserByMime( diff --git a/lib/media/media_source_engine.js b/lib/media/media_source_engine.js index 6679fa34c4..6f8fd95f3c 100644 --- a/lib/media/media_source_engine.js +++ b/lib/media/media_source_engine.js @@ -310,10 +310,13 @@ shaka.media.MediaSourceEngine = class { * according to MediaSourceEngine.isStreamSupported. * @param {boolean} forceTransmuxTS * If true, this will transmux TS content even if it is natively supported. + * @param {boolean=} sequenceMode + * If true, the media segments are appended to the SourceBuffer in strict + * sequence. * * @return {!Promise} */ - async init(streamsByType, forceTransmuxTS) { + async init(streamsByType, forceTransmuxTS, sequenceMode=false) { const ContentType = shaka.util.ManifestParserUtils.ContentType; await this.mediaSourceOpen_; @@ -336,6 +339,10 @@ shaka.media.MediaSourceEngine = class { shaka.media.Transmuxer.convertTsCodecs(contentType, mimeType); } const sourceBuffer = this.mediaSource_.addSourceBuffer(mimeType); + if (sequenceMode) { + sourceBuffer.mode = + shaka.media.MediaSourceEngine.SourceBufferMode_.SEQUENCE; + } this.eventManager_.listen( sourceBuffer, 'error', () => this.onError_(contentType)); @@ -496,11 +503,28 @@ shaka.media.MediaSourceEngine = class { * @param {?number} endTime relative to the start of the presentation * @param {?boolean} hasClosedCaptions True if the buffer contains CEA closed * captions + + * @param {boolean=} seeked True if we just seeked + * @param {boolean=} sequenceMode True if sequence mode * @return {!Promise} */ - async appendBuffer(contentType, data, startTime, endTime, hasClosedCaptions) { + async appendBuffer(contentType, data, startTime, endTime, hasClosedCaptions, + seeked, sequenceMode) { const ContentType = shaka.util.ManifestParserUtils.ContentType; + // If we just cleared buffer and is on an unbuffered seek, we need to set + // the new timestampOffset of the sourceBuffer. + // Don't do this for text streams, though, since they don't use MediaSource + // anyway. + if (startTime != null && sequenceMode && contentType != ContentType.TEXT) { + if (seeked) { + const timestampOffset = /** @type {number} */ (startTime); + this.enqueueOperation_( + contentType, + () => this.setTimestampOffset_(contentType, timestampOffset)); + } + } + if (contentType == ContentType.TEXT) { await this.textEngine_.appendBuffer(data, startTime, endTime); } else if (this.transmuxers_[contentType]) { @@ -1138,3 +1162,13 @@ shaka.media.MediaSourceEngine.createObjectURL = window.URL.createObjectURL; * The PublicPromise which is associated with this operation. */ shaka.media.MediaSourceEngine.Operation; + + +/** + * @enum {string} + * @private + */ +shaka.media.MediaSourceEngine.SourceBufferMode_ = { + SEQUENCE: 'sequence', + SEGMENTS: 'segments', +}; diff --git a/lib/media/presentation_timeline.js b/lib/media/presentation_timeline.js index 04202c771a..a86a2bf366 100644 --- a/lib/media/presentation_timeline.js +++ b/lib/media/presentation_timeline.js @@ -249,8 +249,7 @@ shaka.media.PresentationTimeline = class { * @param {number} startTime * @export */ - notifyMinSegmentStartTime( - startTime) { + notifyMinSegmentStartTime(startTime) { if (this.minSegmentStartTime_ == null) { // No data yet, and Math.min(null, startTime) is always 0. So just store // startTime. diff --git a/lib/media/streaming_engine.js b/lib/media/streaming_engine.js index a501370c52..5625138d59 100644 --- a/lib/media/streaming_engine.js +++ b/lib/media/streaming_engine.js @@ -645,6 +645,10 @@ shaka.media.StreamingEngine = class { if (type === ContentType.TEXT) { this.playerInterface_.mediaSourceEngine.resetCaptionParser(); } + + // Mark the media state as having seeked, so that the new buffers know + // that they will need to be at a new position (for sequence mode). + mediaState.seeked = true; } } @@ -764,7 +768,9 @@ shaka.media.StreamingEngine = class { const mediaSourceEngine = this.playerInterface_.mediaSourceEngine; const forceTransmuxTS = this.config_.forceTransmuxTS; - await mediaSourceEngine.init(streamsByType, forceTransmuxTS); + + await mediaSourceEngine.init(streamsByType, forceTransmuxTS, + this.manifest_.sequenceMode); this.destroyer_.ensureNotDestroyed(); this.setDuration_(); @@ -805,6 +811,9 @@ shaka.media.StreamingEngine = class { clearBufferSafeMargin: 0, waitingToFlushBuffer: false, clearingBuffer: false, + // The playhead might be seeking on startup, if a start time is set, so + // start "seeked" as true. + seeked: true, recovering: false, hasError: false, operation: null, @@ -1272,6 +1281,7 @@ shaka.media.StreamingEngine = class { this.scheduleUpdate_(mediaState, 0); return; } + await this.append_( mediaState, presentationTime, stream, reference, result); } @@ -1565,8 +1575,7 @@ shaka.media.StreamingEngine = class { * @return {!Promise} * @private */ - async append_(mediaState, presentationTime, stream, reference, - segment) { + async append_(mediaState, presentationTime, stream, reference, segment) { const logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState); const hasClosedCaptions = stream.closedCaptions && @@ -1586,12 +1595,16 @@ shaka.media.StreamingEngine = class { this.destroyer_.ensureNotDestroyed(); shaka.log.v1(logPrefix, 'appending media segment'); + const seeked = mediaState.seeked; + mediaState.seeked = false; await this.playerInterface_.mediaSourceEngine.appendBuffer( mediaState.type, segment, reference.startTime, reference.endTime, - hasClosedCaptions); + hasClosedCaptions, + seeked, + this.manifest_.sequenceMode); this.destroyer_.ensureNotDestroyed(); shaka.log.v2(logPrefix, 'appended media segment'); } @@ -2017,6 +2030,7 @@ shaka.media.StreamingEngine.PlayerInterface; * waitingToFlushBuffer: boolean, * clearBufferSafeMargin: number, * clearingBuffer: boolean, + * seeked: boolean, * recovering: boolean, * hasError: boolean, * operation: shaka.net.NetworkingEngine.PendingRequest @@ -2061,6 +2075,8 @@ shaka.media.StreamingEngine.PlayerInterface; * The amount of buffer to retain when clearing the buffer after the update. * @property {boolean} clearingBuffer * True indicates that the buffer is being cleared. + * @property {boolean} seeked + * True indicates that the presentation just seeked. * @property {boolean} recovering * True indicates that the last segment was not appended because it could not * fit in the buffer. diff --git a/lib/offline/indexeddb/v1_storage_cell.js b/lib/offline/indexeddb/v1_storage_cell.js index 3634890f5e..3b17603617 100644 --- a/lib/offline/indexeddb/v1_storage_cell.js +++ b/lib/offline/indexeddb/v1_storage_cell.js @@ -99,6 +99,7 @@ shaka.offline.indexeddb.V1StorageCell = class sessionIds: old.sessionIds, drmInfo: old.drmInfo, appMetadata: old.appMetadata, + sequenceMode: false, }; } diff --git a/lib/offline/indexeddb/v2_storage_cell.js b/lib/offline/indexeddb/v2_storage_cell.js index fa59211fba..33d9645911 100644 --- a/lib/offline/indexeddb/v2_storage_cell.js +++ b/lib/offline/indexeddb/v2_storage_cell.js @@ -60,6 +60,7 @@ shaka.offline.indexeddb.V2StorageCell = class sessionIds: old.sessionIds, size: old.size, streams, + sequenceMode: false, }; } diff --git a/lib/offline/manifest_converter.js b/lib/offline/manifest_converter.js index 98df077708..e6e5179e6d 100644 --- a/lib/offline/manifest_converter.js +++ b/lib/offline/manifest_converter.js @@ -87,6 +87,7 @@ shaka.offline.ManifestConverter = class { variants: Array.from(variants.values()), textStreams: textStreams, imageStreams: imageStreams, + sequenceMode: manifestDB.sequenceMode || false, }; } diff --git a/lib/offline/storage.js b/lib/offline/storage.js index c8b3708d26..6e37360ec7 100644 --- a/lib/offline/storage.js +++ b/lib/offline/storage.js @@ -867,6 +867,7 @@ shaka.offline.Storage = class { drmInfo, appMetadata: metadata, isIncomplete: true, + sequenceMode: manifest.sequenceMode, }; return {manifestDB, toDownload}; diff --git a/lib/util/error.js b/lib/util/error.js index 6cbd56f85a..5f7a95f71e 100644 --- a/lib/util/error.js +++ b/lib/util/error.js @@ -627,12 +627,7 @@ shaka.util.Error.Code = { // RETIRED: 'HLS_LIVE_CONTENT_NOT_SUPPORTED': 4029, - /** - * The HLS parser was unable to parse segment start time from the media. - *
error.data[0] is the failed media playlist URI. - *
error.data[1] is the failed media segment URI (if any). - */ - 'HLS_COULD_NOT_PARSE_SEGMENT_START_TIME': 4030, + // RETIRED: 'HLS_COULD_NOT_PARSE_SEGMENT_START_TIME': 4030, // RETIRED: 'HLS_MEDIA_SEQUENCE_REQUIRED_IN_LIVE_STREAMS': 4031, diff --git a/lib/util/player_configuration.js b/lib/util/player_configuration.js index b10432481e..46e7290e58 100644 --- a/lib/util/player_configuration.js +++ b/lib/util/player_configuration.js @@ -119,7 +119,6 @@ shaka.util.PlayerConfiguration = class { hls: { ignoreTextStreamFailures: false, ignoreImageStreamFailures: false, - useFullSegmentsForStartTime: false, defaultAudioCodec: 'mp4a.40.2', defaultVideoCodec: 'avc1.42E01E', }, diff --git a/roadmap.md b/roadmap.md index 5b2f6f1c04..cedb809ac6 100644 --- a/roadmap.md +++ b/roadmap.md @@ -9,8 +9,6 @@ The goals of future milestones are fluid until we begin that development cycle, so the exact milestone for future features is not pre-determined. Priority features up next: - - Support containerless formats - https://github.com/google/shaka-player/issues/2337 - Preload API https://github.com/google/shaka-player/issues/880 - Codec-switching and order preference diff --git a/test/hls/hls_live_unit.js b/test/hls/hls_live_unit.js index 0595ba55d9..7e76acda1b 100644 --- a/test/hls/hls_live_unit.js +++ b/test/hls/hls_live_unit.js @@ -4,7 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -goog.require('goog.asserts'); goog.require('shaka.hls.HlsParser'); goog.require('shaka.net.NetworkingEngine'); goog.require('shaka.test.FakeNetworkingEngine'); @@ -40,14 +39,6 @@ describe('HlsParser live', () => { let segmentData; /** @type {!Uint8Array} */ let selfInitializingSegmentData; - /** @type {!Uint8Array} */ - let tsSegmentData; - /** @type {!Uint8Array} */ - let pastRolloverSegmentData; - /** @type {number} */ - let rolloverOffset; - /** @type {number} */ - let segmentDataStartTime; beforeEach(() => { // TODO: use StreamGenerator? @@ -78,36 +69,6 @@ describe('HlsParser live', () => { 0x00, 0x00, 0x00, 0x00, // baseMediaDecodeTime first 4 bytes 0x00, 0x00, 0x07, 0xd0, // baseMediaDecodeTime last 4 bytes (2000) ]); - tsSegmentData = new Uint8Array([ - 0x47, // TS sync byte (fixed value) - 0x41, 0x01, // not corrupt, payload follows, packet ID 257 - 0x10, // not scrambled, no adaptation field, payload only, seq #0 - 0x00, 0x00, 0x01, // PES start code (fixed value) - 0xe0, // stream ID (video stream 0) - 0x00, 0x00, // PES packet length (doesn't matter) - 0x80, // marker bits (fixed value), not scrambled, not priority - 0x80, // PTS only, no DTS, other flags 0 (don't matter) - 0x05, // remaining PES header length == 5 (one timestamp) - 0x21, 0x00, 0x0b, 0x7e, 0x41, // PTS = 180000, encoded into 5 bytes - ]); - // 180000 divided by TS timescale (90000) = segment starts at 2s. - segmentDataStartTime = 2; - - pastRolloverSegmentData = 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 - 0x0b, 0x60, 0xbc, 0x28, // baseMediaDecodeTime last 4 bytes (190889000) - ]); - - // The timestamp above would roll over twice, so this rollover offset should - // be applied. - rolloverOffset = (0x200000000 * 2) / 90000; selfInitializingSegmentData = shaka.util.Uint8ArrayUtils.concat(initSegmentData, segmentData); @@ -233,8 +194,8 @@ describe('HlsParser live', () => { describe('update', () => { it('adds new segments when they appear', async () => { - const ref1 = ManifestParser.makeReference('test:/main.mp4', 2, 4); - const ref2 = ManifestParser.makeReference('test:/main2.mp4', 4, 6); + const ref1 = ManifestParser.makeReference('test:/main.mp4', 0, 2); + const ref2 = ManifestParser.makeReference('test:/main2.mp4', 2, 4); await testUpdate( master, media, [ref1], mediaWithAdditionalSegment, [ref1, ref2]); @@ -248,8 +209,8 @@ describe('HlsParser live', () => { ].join(''); const masterWithTwoVariants = master + secondVariant; - const ref1 = ManifestParser.makeReference('test:/main.mp4', 2, 4); - const ref2 = ManifestParser.makeReference('test:/main2.mp4', 4, 6); + const ref1 = ManifestParser.makeReference('test:/main.mp4', 0, 2); + const ref2 = ManifestParser.makeReference('test:/main2.mp4', 2, 4); await testUpdate( masterWithTwoVariants, media, [ref1], mediaWithAdditionalSegment, @@ -269,8 +230,8 @@ describe('HlsParser live', () => { ].join(''); const masterWithAudio = masterlist + audio; - const ref1 = ManifestParser.makeReference('test:/main.mp4', 2, 4); - const ref2 = ManifestParser.makeReference('test:/main2.mp4', 4, 6); + const ref1 = ManifestParser.makeReference('test:/main.mp4', 0, 2); + const ref2 = ManifestParser.makeReference('test:/main2.mp4', 2, 4); await testUpdate( masterWithAudio, media, [ref1], mediaWithAdditionalSegment, @@ -290,9 +251,9 @@ describe('HlsParser live', () => { const updatedMedia1 = media + newSegment1; const updatedMedia2 = updatedMedia1 + newSegment2; - const ref1 = ManifestParser.makeReference('test:/main.mp4', 2, 4); - const ref2 = ManifestParser.makeReference('test:/main2.mp4', 4, 6); - const ref3 = ManifestParser.makeReference('test:/main3.mp4', 6, 8); + const ref1 = ManifestParser.makeReference('test:/main.mp4', 0, 2); + const ref2 = ManifestParser.makeReference('test:/main2.mp4', 2, 4); + const ref3 = ManifestParser.makeReference('test:/main3.mp4', 4, 6); fakeNetEngine .setResponseText('test:/master', master) @@ -398,19 +359,6 @@ describe('HlsParser live', () => { 'main.mp4\n', ].join(''); - const mediaWithByteRange = [ - '#EXTM3U\n', - '#EXT-X-TARGETDURATION:5\n', - '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', - '#EXT-X-MEDIA-SEQUENCE:0\n', - '#EXT-X-BYTERANGE:121090@616\n', - '#EXTINF:2,\n', - 'main.mp4\n', - ].join(''); - - const expectedStartByte = 616; - const expectedEndByte = 121705; - const mediaWithAdditionalSegment = [ '#EXTM3U\n', '#EXT-X-TARGETDURATION:5\n', @@ -576,16 +524,16 @@ describe('HlsParser live', () => { .setResponseValue('test:/main2.mp4', segmentData); const ref1 = ManifestParser.makeReference( - 'test:/main.mp4', segmentDataStartTime, segmentDataStartTime + 2, + 'test:/main.mp4', 0, 2, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null, /* timestampOffset= */ 0); // Expect the timestamp offset to be set for the segment after the // EXT-X-DISCONTINUITY tag. const ref2 = ManifestParser.makeReference( - 'test:/main2.mp4', segmentDataStartTime + 2, segmentDataStartTime + 4, + 'test:/main2.mp4', 2, 4, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null, - /* timestampOffset= */ 2); + /* timestampOffset= */ 0); const manifest = await parser.start('test:/master', playerInterface); const video = manifest.variants[0].video; @@ -593,57 +541,6 @@ describe('HlsParser live', () => { ManifestParser.verifySegmentIndex(video, [ref1, ref2]); }); - it('offsets VTT text with rolled over TS timestamps', async () => { - const masterWithVtt = [ - '#EXTM3U\n', - '#EXT-X-MEDIA:TYPE=SUBTITLES,LANGUAGE="fra",URI="text",', - 'GROUP-ID="sub1"\n', - '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",', - 'RESOLUTION=960x540,FRAME-RATE=60,SUBTITLES="sub1"\n', - 'video\n', - ].join(''); - - const textPlaylist = [ - '#EXTM3U\n', - '#EXT-X-TARGETDURATION:5\n', - '#EXT-X-MEDIA-SEQUENCE:0\n', - '#EXTINF:2,\n', - 'main.vtt\n', - ].join(''); - - const vtt = [ - 'WEBVTT\n', - '\n', - '00:00.000 --> 00:01.000\n', - 'Hello, world!\n', - ].join(''); - - fakeNetEngine - .setResponseText('test:/master', masterWithVtt) - .setResponseText('test:/video', media) - .setResponseText('test:/text', textPlaylist) - .setResponseValue('test:/init.mp4', initSegmentData) - .setResponseValue('test:/main.mp4', pastRolloverSegmentData) - .setResponseText('test:/main.vtt', vtt); - - const manifest = await parser.start('test:/master', playerInterface); - const textStream = manifest.textStreams[0]; - await textStream.createSegmentIndex(); - goog.asserts.assert(textStream.segmentIndex, 'Null segmentIndex!'); - - let ref = Array.from(textStream.segmentIndex)[0]; - expect(ref).not.toBe(null); - expect(ref.startTime).not.toBeLessThan(rolloverOffset); - - const videoStream = manifest.variants[0].video; - await videoStream.createSegmentIndex(); - goog.asserts.assert(videoStream.segmentIndex, 'Null segmentIndex!'); - - ref = Array.from(videoStream.segmentIndex)[0]; - expect(ref).not.toBe(null); - expect(ref.startTime).not.toBeLessThan(rolloverOffset); - }); - it('parses streams with partial and preload hinted segments', async () => { playerInterface.isLowLatencyMode = () => true; const mediaWithPartialSegments = [ @@ -674,34 +571,31 @@ describe('HlsParser live', () => { .setResponseValue('test:/partial2.mp4', segmentData); const partialRef = ManifestParser.makeReference( - 'test:/partial.mp4', segmentDataStartTime, segmentDataStartTime + 2, + 'test:/partial.mp4', 0, 2, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ 199); const partialRef2 = ManifestParser.makeReference( - 'test:/partial2.mp4', segmentDataStartTime + 2, - segmentDataStartTime + 4, /* baseUri= */ '', /* startByte= */ 200, - /* endByte= */ 429); + 'test:/partial2.mp4', 2, 4, + /* baseUri= */ '', /* startByte= */ 200, /* endByte= */ 429); const partialRef3 = ManifestParser.makeReference( - 'test:/partial.mp4', segmentDataStartTime + 4, - segmentDataStartTime + 6, + 'test:/partial.mp4', 4, 6, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ 209); // A preload hinted partial segment doesn't have duration information, // so its startTime and endTime are the same. const preloadRef = ManifestParser.makeReference( - 'test:/partial.mp4', segmentDataStartTime + 6, - segmentDataStartTime + 6, + 'test:/partial.mp4', 6, 6, /* baseUri= */ '', /* startByte= */ 210, /* endByte= */ null); const ref = ManifestParser.makeReference( - 'test:/main.mp4', segmentDataStartTime, segmentDataStartTime + 4, + 'test:/main.mp4', 0, 4, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ 429, /* timestampOffset= */ 0, [partialRef, partialRef2]); // ref2 is not fully published yet, so it doesn't have a segment uri. const ref2 = ManifestParser.makeReference( - '', segmentDataStartTime + 4, segmentDataStartTime + 6, + '', 4, 6, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null, /* timestampOffset= */ 0, [partialRef3, preloadRef]); @@ -713,16 +607,16 @@ describe('HlsParser live', () => { describe('update', () => { it('adds new segments when they appear', async () => { - const ref1 = ManifestParser.makeReference('test:/main.mp4', 2, 4); - const ref2 = ManifestParser.makeReference('test:/main2.mp4', 4, 6); + const ref1 = ManifestParser.makeReference('test:/main.mp4', 0, 2); + const ref2 = ManifestParser.makeReference('test:/main2.mp4', 2, 4); await testUpdate( master, media, [ref1], mediaWithAdditionalSegment, [ref1, ref2]); }); it('evicts removed segments', async () => { - const ref1 = ManifestParser.makeReference('test:/main.mp4', 2, 4); - const ref2 = ManifestParser.makeReference('test:/main2.mp4', 4, 6); + const ref1 = ManifestParser.makeReference('test:/main.mp4', 0, 2); + const ref2 = ManifestParser.makeReference('test:/main2.mp4', 2, 4); await testUpdate( master, mediaWithAdditionalSegment, [ref1, ref2], @@ -730,12 +624,12 @@ describe('HlsParser live', () => { }); it('handles updates with redirects', async () => { - const oldRef1 = ManifestParser.makeReference('test:/main.mp4', 2, 4); + const oldRef1 = ManifestParser.makeReference('test:/main.mp4', 0, 2); const newRef1 = - ManifestParser.makeReference('test:/redirected/main.mp4', 2, 4); + ManifestParser.makeReference('test:/redirected/main.mp4', 0, 2); const newRef2 = - ManifestParser.makeReference('test:/redirected/main2.mp4', 4, 6); + ManifestParser.makeReference('test:/redirected/main2.mp4', 2, 4); let playlistFetchCount = 0; @@ -763,7 +657,7 @@ describe('HlsParser live', () => { .setResponseValue('test:/main.mp4', segmentData); const expectedRef = ManifestParser.makeReference( - 'test:/main.mp4', segmentDataStartTime, segmentDataStartTime + 2); + 'test:/main.mp4', 0, 2); // In live content, we do not set timestampOffset. expectedRef.timestampOffset = 0; @@ -780,12 +674,9 @@ describe('HlsParser live', () => { .setResponseValue('test:/init.mp4', initSegmentData) .setResponseValue('test:/main.mp4', segmentData); - const ref1 = ManifestParser.makeReference( - 'test:/main.mp4', segmentDataStartTime, segmentDataStartTime + 2); + const ref1 = ManifestParser.makeReference('test:/main.mp4', 0, 2); - const ref2 = ManifestParser.makeReference( - 'test:/main2.mp4', segmentDataStartTime + 2, - segmentDataStartTime + 4); + const ref2 = ManifestParser.makeReference('test:/main2.mp4', 2, 4); const manifest = await parser.start('test:/master', playerInterface); const video = manifest.variants[0].video; @@ -821,11 +712,9 @@ describe('HlsParser live', () => { .setResponseValue('test:/main.mp4', segmentData) .setResponseValue('test:/main2.mp4', segmentData); - const ref1 = ManifestParser.makeReference('test:/main.mp4', - segmentDataStartTime, segmentDataStartTime + 2); + const ref1 = ManifestParser.makeReference('test:/main.mp4', 0, 2); - const ref2 = ManifestParser.makeReference('test:/main2.mp4', - segmentDataStartTime + 2, segmentDataStartTime + 4); + const ref2 = ManifestParser.makeReference('test:/main2.mp4', 2, 4); const manifest = await parser.start('test:/master', playerInterface); @@ -858,57 +747,6 @@ describe('HlsParser live', () => { shaka.net.NetworkingEngine.RequestType.SEGMENT); }); - it('parses start time from ts segments', async () => { - const tsMediaPlaylist = - mediaWithRemovedSegment.replace(/\.mp4/g, '.ts'); - - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', tsMediaPlaylist) - .setResponseValue('test:/main2.ts', tsSegmentData); - - const expectedRef = ManifestParser.makeReference( - 'test:/main2.ts', segmentDataStartTime, segmentDataStartTime + 2); - // In live content, we do not set timestampOffset. - expectedRef.timestampOffset = 0; - - const manifest = await parser.start('test:/master', playerInterface); - const video = manifest.variants[0].video; - await video.createSegmentIndex(); - ManifestParser.verifySegmentIndex(video, [expectedRef]); - }); - - it('gets start time of segments with byte range', async () => { - // Nit: this value is an implementation detail of the fix for #1106 - const partialEndByte = expectedStartByte + 2048 - 1; - - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', mediaWithByteRange) - .setResponseValue('test:/init.mp4', initSegmentData) - .setResponseValue('test:/main.mp4', segmentData); - - const expectedRef = ManifestParser.makeReference( - /* uri= */ 'test:/main.mp4', - /* start= */ segmentDataStartTime, - /* end= */ segmentDataStartTime + 2, - /* baseUri= */ '', - expectedStartByte, - expectedEndByte); // Complete segment reference - - const manifest = await parser.start('test:/master', playerInterface); - const video = manifest.variants[0].video; - await video.createSegmentIndex(); - ManifestParser.verifySegmentIndex(video, [expectedRef]); - - // There should have been a range request for this segment to get the - // start time. - fakeNetEngine.expectRangeRequest( - 'test:/main.mp4', - expectedStartByte, - partialEndByte); // partial segment request - }); - it('request playlist delta updates to skip segments', async () => { const mediaWithDeltaUpdates = [ '#EXTM3U\n', @@ -973,9 +811,9 @@ describe('HlsParser live', () => { ].join(''); playerInterface.isLowLatencyMode = () => true; - const ref1 = ManifestParser.makeReference('test:/main.mp4', 2, 4); - const ref2 = ManifestParser.makeReference('test:/main2.mp4', 4, 6); - const ref3 = ManifestParser.makeReference('test:/main3.mp4', 6, 8); + const ref1 = ManifestParser.makeReference('test:/main.mp4', 0, 2); + const ref2 = ManifestParser.makeReference('test:/main2.mp4', 2, 4); + const ref3 = ManifestParser.makeReference('test:/main3.mp4', 4, 6); // With 'SKIPPED-SEGMENTS', ref1 is skipped from the playlist, // and ref1 should be in the SegmentReferences list. // ref3 should be appended to the SegmentReferences list. @@ -1016,28 +854,28 @@ describe('HlsParser live', () => { playerInterface.isLowLatencyMode = () => true; const ref1 = ManifestParser.makeReference( - 'test:/main.mp4', segmentDataStartTime, segmentDataStartTime + 2, + 'test:/main.mp4', 0, 2, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null, /* timestampOffset= */ 0); // Expect the timestamp offset to be set for the segment after the // EXT-X-DISCONTINUITY tag. const ref2 = ManifestParser.makeReference( - 'test:/main2.mp4', segmentDataStartTime + 2, - segmentDataStartTime + 4, /* baseUri= */ '', /* startByte= */ 0, - /* endByte= */ null, /* timestampOffset= */ 2); + 'test:/main2.mp4', 2, 4, + /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null, + /* timestampOffset= */ 0); // Expect the timestamp offset to be set for the segment, with the // EXT-X-DISCONTINUITY tag skipped in the playlist. const ref3 = ManifestParser.makeReference( - 'test:/main3.mp4', segmentDataStartTime + 4, - segmentDataStartTime + 6, /* baseUri= */ '', /* startByte= */ 0, - /* endByte= */ null, /* timestampOffset= */ 2); + 'test:/main3.mp4', 4, 6, + /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null, + /* timestampOffset= */ 0); const ref4 = ManifestParser.makeReference( - 'test:/main4.mp4', segmentDataStartTime + 6, - segmentDataStartTime + 8, /* baseUri= */ '', /* startByte= */ 0, - /* endByte= */ null, /* timestampOffset= */ 2); + 'test:/main4.mp4', 6, 8, + /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null, + /* timestampOffset= */ 0); // With 'SKIPPED-SEGMENTS', ref1, ref2 are skipped from the playlist, // and ref1,ref2 should be in the SegmentReferences list. diff --git a/test/hls/hls_parser_unit.js b/test/hls/hls_parser_unit.js index 6c5afe95aa..150e58fdcb 100644 --- a/test/hls/hls_parser_unit.js +++ b/test/hls/hls_parser_unit.js @@ -7,10 +7,8 @@ goog.require('goog.asserts'); goog.require('shaka.hls.HlsParser'); goog.require('shaka.log'); -goog.require('shaka.net.NetworkingEngine'); goog.require('shaka.test.FakeNetworkingEngine'); goog.require('shaka.test.ManifestGenerator'); -goog.require('shaka.test.ManifestParser'); goog.require('shaka.test.Util'); goog.require('shaka.util.Error'); goog.require('shaka.util.ManifestParserUtils'); @@ -19,7 +17,6 @@ goog.require('shaka.util.Uint8ArrayUtils'); describe('HlsParser', () => { const ContentType = shaka.util.ManifestParserUtils.ContentType; - const ManifestParser = shaka.test.ManifestParser; const TextStreamKind = shaka.util.ManifestParserUtils.TextStreamKind; const Util = shaka.test.Util; const originalAlwaysWarn = shaka.log.alwaysWarn; @@ -169,6 +166,7 @@ describe('HlsParser', () => { ].join(''); const manifest = shaka.test.ManifestGenerator.generate((manifest) => { + manifest.sequenceMode = true; manifest.anyTimeline(); manifest.addPartialVariant((variant) => { variant.language = 'en'; @@ -239,6 +237,7 @@ describe('HlsParser', () => { stream.size(960, 540); }); }); + manifest.sequenceMode = true; }); fakeNetEngine @@ -275,6 +274,7 @@ describe('HlsParser', () => { stream.mime('video/mp4', 'avc1.4d001e'); }); }); + manifest.sequenceMode = true; }); await testHlsParser(master, media, manifest); @@ -304,6 +304,7 @@ describe('HlsParser', () => { stream.mime('video/mp4', 'avc1'); }); }); + manifest.sequenceMode = true; }); await testHlsParser(master, media, manifest); @@ -332,6 +333,7 @@ describe('HlsParser', () => { stream.mime('video/mp4', 'avc1'); }); }); + manifest.sequenceMode = true; }); await testHlsParser(master, media, manifest); @@ -363,6 +365,7 @@ describe('HlsParser', () => { stream.mime('video/mp4', 'avc1'); }); }); + manifest.sequenceMode = true; }); await testHlsParser(master, media, manifest); @@ -391,6 +394,7 @@ describe('HlsParser', () => { stream.mime('audio/mp4', 'mp4a'); }); }); + manifest.sequenceMode = true; }); await testHlsParser(master, media, manifest); @@ -425,6 +429,7 @@ describe('HlsParser', () => { stream.mime('audio/mp4', 'mp4a'); }); }); + manifest.sequenceMode = true; }); await testHlsParser(master, media, manifest); @@ -459,6 +464,35 @@ describe('HlsParser', () => { stream.mime('audio/mp4', ''); }); }); + manifest.sequenceMode = true; + }); + + await testHlsParser(master, media, manifest); + }); + + it('accepts containerless streams', async () => { + const master = [ + '#EXTM3U\n', + '#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=41457,CODECS="mp4a.40.2"\n', + 'audio\n', + ].join(''); + + const media = [ + '#EXTM3U\n', + '#EXT-X-PLAYLIST-TYPE:VOD\n', + '#EXTINF:5,\n', + '#EXT-X-BYTERANGE:121090@616\n', + 'main.aac', + ].join(''); + + const manifest = shaka.test.ManifestGenerator.generate((manifest) => { + manifest.anyTimeline(); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.AUDIO, (stream) => { + stream.mime('audio/aac', ''); + }); + }); + manifest.sequenceMode = true; }); await testHlsParser(master, media, manifest); @@ -497,6 +531,7 @@ describe('HlsParser', () => { stream.mime('audio/mp4', 'mp4a'); }); }); + manifest.sequenceMode = true; }); await testHlsParser(master, media, manifest); @@ -533,6 +568,7 @@ describe('HlsParser', () => { stream.mime('audio/mp4', 'mp4a'); }); }); + manifest.sequenceMode = true; }); await testHlsParser(master, media, manifest); @@ -563,66 +599,12 @@ describe('HlsParser', () => { stream.mime('audio/mp4', 'mp4a'); }); }); + manifest.sequenceMode = true; }); await testHlsParser(master, media, manifest); }); - it('sets seek range correctly for non-zero start', async () => { - const master = [ - '#EXTM3U\n', - '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",', - 'RESOLUTION=960x540,FRAME-RATE=60\n', - 'video', - ].join(''); - - const media = [ - '#EXTM3U\n', - '#EXT-X-PLAYLIST-TYPE:VOD\n', - '#EXT-X-MEDIA-SEQUENCE:131\n', - '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', - '#EXTINF:5,\n', - '#EXT-X-BYTERANGE:121090@616\n', - 'main.mp4', - ].join(''); - - 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, 0x0A, 0x00, 0x00, // baseMediaDecodeTime last 4 bytes (655360) - ]); - - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', media) - .setResponseValue('test:/init.mp4', initSegmentData) - .setResponseValue('test:/main.mp4', segmentData); - - const manifest = await parser.start('test:/master', playerInterface); - const presentationTimeline = manifest.presentationTimeline; - const stream = manifest.variants[0].video; - await stream.createSegmentIndex(); - goog.asserts.assert(stream.segmentIndex != null, 'Null segmentIndex!'); - - const ref = Array.from(stream.segmentIndex)[0]; - expect(ref).not.toBe(null); - if (ref) { - expect(ref.startTime).toBe(0); - // baseMediaDecodeTime (655360) / timescale (1000) - expect(ref.timestampOffset).toBe(-655.36); - } - expect(presentationTimeline.getSeekRangeStart()).toBe(0); - expect(presentationTimeline.getSeekRangeEnd()).toBe(5); - }); - it('parses multiplexed variant', async () => { const master = [ '#EXTM3U\n', @@ -647,6 +629,7 @@ describe('HlsParser', () => { stream.mime('video/mp4', 'avc1,mp4a'); }); }); + manifest.sequenceMode = true; }); await testHlsParser(master, media, manifest); @@ -676,6 +659,7 @@ describe('HlsParser', () => { stream.mime('video/mp4', /** @type {?} */ (jasmine.any(String))); }); }); + manifest.sequenceMode = true; }); await testHlsParser(master, media, manifest); @@ -710,6 +694,7 @@ describe('HlsParser', () => { stream.mime('audio/mp4', /** @type {?} */ (jasmine.any(String))); }); }); + manifest.sequenceMode = true; }); await testHlsParser(master, media, manifest); @@ -740,6 +725,7 @@ describe('HlsParser', () => { stream.mime('video/mp4', /** @type {?} */ (jasmine.any(String))); }); }); + manifest.sequenceMode = true; }); await testHlsParser(master, media, manifest); @@ -770,6 +756,7 @@ describe('HlsParser', () => { stream.mime('audio/mp4', /** @type {?} */ (jasmine.any(String))); }); }); + manifest.sequenceMode = true; }); await testHlsParser(master, media, manifest); @@ -821,6 +808,7 @@ describe('HlsParser', () => { stream.language = 'fr'; }); }); + manifest.sequenceMode = true; }); await testHlsParser(master, media, manifest); @@ -863,6 +851,7 @@ describe('HlsParser', () => { stream.language = 'fr'; }); }); + manifest.sequenceMode = true; }); await testHlsParser(master, media, manifest); @@ -910,63 +899,12 @@ describe('HlsParser', () => { ]; }); }); + manifest.sequenceMode = true; }); await testHlsParser(master, media, manifest); }); - it('fetch the start time for one audio/video stream and reuse for the others', - async () => { - const SEGMENT = shaka.net.NetworkingEngine.RequestType.SEGMENT; - const master = [ - '#EXTM3U\n', - '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",', - 'CHANNELS="2",URI="audio"\n', - '#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub1",LANGUAGE="eng",', - 'URI="text"\n', - '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",', - 'RESOLUTION=960x540,FRAME-RATE=60,AUDIO="aud1"\n', - 'video\n', - ].join(''); - - const media = [ - '#EXTM3U\n', - '#EXT-X-PLAYLIST-TYPE:VOD\n', - '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', - '#EXTINF:5,\n', - '#EXT-X-BYTERANGE:121090@616\n', - 'main.mp4', - ].join(''); - - const textMedia = [ - '#EXTM3U\n', - '#EXT-X-PLAYLIST-TYPE:VOD\n', - '#EXTINF:5,\n', - '#EXT-X-BYTERANGE:121090@616\n', - 'main.vtt', - ].join(''); - - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/audio', media) - .setResponseText('test:/video', media) - .setResponseText('test:/text', textMedia) - .setResponseText('test:/main.vtt', vttText) - .setResponseValue('test:/init.mp4', initSegmentData) - .setResponseValue('test:/main.mp4', segmentData); - - await parser.start('test:/master', playerInterface); - // The start time of audio should be fetched first, and then video and - // text streams should reuse the start time from audio. - // Thus, there should be 2 segment requests, for fetching audio init - // and main segments, and not for video and text segments. - expect(fakeNetEngine.request.calls.allArgs().filter((args) => { - return args[0] == SEGMENT; - }).length).toBe(2); - fakeNetEngine.expectRequest('test:/init.mp4', SEGMENT); - fakeNetEngine.expectRequest('test:/main.mp4', SEGMENT); - }); - it('gets mime type from header request', async () => { const master = [ '#EXTM3U\n', @@ -991,6 +929,7 @@ describe('HlsParser', () => { stream.mime('video/mp4', 'avc1'); }); }); + manifest.sequenceMode = true; }); // The extra parameters should be stripped by the parser. @@ -1048,6 +987,7 @@ describe('HlsParser', () => { stream.kind = TextStreamKind.SUBTITLE; stream.mime('text/vtt', ''); }); + manifest.sequenceMode = true; }); fakeNetEngine @@ -1117,6 +1057,7 @@ describe('HlsParser', () => { stream.kind = TextStreamKind.SUBTITLE; stream.mime('text/vtt', ''); }); + manifest.sequenceMode = true; }); fakeNetEngine @@ -1178,6 +1119,7 @@ describe('HlsParser', () => { stream.kind = TextStreamKind.SUBTITLE; stream.mime('text/vtt', ''); }); + manifest.sequenceMode = true; }); fakeNetEngine @@ -1239,6 +1181,7 @@ describe('HlsParser', () => { stream.kind = TextStreamKind.SUBTITLE; stream.mime('text/vtt', ''); }); + manifest.sequenceMode = true; }); fakeNetEngine @@ -1743,6 +1686,7 @@ describe('HlsParser', () => { stream.language = 'en'; stream.mime('application/mp4', 'stpp.ttml.im1t'); }); + manifest.sequenceMode = true; }); fakeNetEngine @@ -1792,6 +1736,7 @@ describe('HlsParser', () => { manifest.addPartialTextStream((stream) => { stream.mime('text/vtt', 'vtt'); }); + manifest.sequenceMode = true; }); fakeNetEngine @@ -1834,6 +1779,7 @@ describe('HlsParser', () => { manifest.addPartialTextStream((stream) => { stream.kind = TextStreamKind.SUBTITLE; }); + manifest.sequenceMode = true; }); fakeNetEngine @@ -1871,6 +1817,7 @@ describe('HlsParser', () => { manifest.addPartialVariant((variant) => { variant.addPartialStream(ContentType.VIDEO); }); + manifest.sequenceMode = true; }); fakeNetEngine @@ -1908,6 +1855,7 @@ describe('HlsParser', () => { manifest.addPartialVariant((variant) => { variant.addPartialStream(ContentType.VIDEO); }); + manifest.sequenceMode = true; }); fakeNetEngine @@ -1950,6 +1898,7 @@ describe('HlsParser', () => { }); variant.addPartialStream(ContentType.AUDIO); }); + manifest.sequenceMode = true; }); await testHlsParser(master, media, manifest); @@ -2034,6 +1983,7 @@ describe('HlsParser', () => { stream.mime('audio/mp4', 'mp4a'); }); }); + manifest.sequenceMode = true; }); await testHlsParser(master, media, manifest); @@ -2129,6 +2079,7 @@ describe('HlsParser', () => { stream.language = 'en'; }); }); + manifest.sequenceMode = true; }); fakeNetEngine @@ -2187,6 +2138,7 @@ describe('HlsParser', () => { }); }); }); + manifest.sequenceMode = true; }); await testHlsParser(master, media, manifest); @@ -2226,6 +2178,7 @@ describe('HlsParser', () => { }); }); }); + manifest.sequenceMode = true; }); await testHlsParser(master, media, manifest); @@ -2262,6 +2215,7 @@ describe('HlsParser', () => { }); }); }); + manifest.sequenceMode = true; }); await testHlsParser(master, media, manifest); @@ -2295,6 +2249,7 @@ describe('HlsParser', () => { stream.mime('audio/mp4', 'mp4a'); }); }); + manifest.sequenceMode = true; }); fakeNetEngine.setHeaders( @@ -2529,261 +2484,6 @@ describe('HlsParser', () => { }); }); // Errors out - describe('getStartTime_', () => { - /** @type {number} */ - let segmentDataStartTime; - /** @type {!Uint8Array} */ - let tsSegmentData; - /** @type {!Uint8Array} */ - let nullTsPacketData; - - const master = [ - '#EXTM3U\n', - '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",', - 'RESOLUTION=960x540,FRAME-RATE=60\n', - 'video', - ].join(''); - - const media = [ - '#EXTM3U\n', - '#EXT-X-PLAYLIST-TYPE:VOD\n', - '#EXT-X-MAP:URI="init.mp4"\n', - '#EXTINF:5,\n', - '#EXT-X-BYTERANGE:121090@616\n', - 'main.mp4', - ].join(''); - - // TODO: Add separate tests to cover correct handling of BYTERANGE in - // constructing references. Here it is covered incidentally. - const expectedStartByte = 616; - const expectedEndByte = 121705; - // Nit: this value is an implementation detail of the fix for #1106 - const partialEndByte = expectedStartByte + 2048 - 1; - - beforeEach(() => { - // TODO: use StreamGenerator? - 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 - 0x00, 0x00, 0x07, 0xd0, // baseMediaDecodeTime last 4 bytes (2000) - ]); - tsSegmentData = new Uint8Array([ - 0x47, // TS sync byte (fixed value) - 0x41, 0x01, // not corrupt, payload follows, packet ID 257 - 0x10, // not scrambled, no adaptation field, payload only, seq #0 - 0x00, 0x00, 0x01, // PES start code (fixed value) - 0xe0, // stream ID (video stream 0) - 0x00, 0x00, // PES packet length (doesn't matter) - 0x80, // marker bits (fixed value), not scrambled, not priority - 0x80, // PTS only, no DTS, other flags 0 (don't matter) - 0x05, // remaining PES header length == 5 (one timestamp) - 0x21, 0x00, 0x0b, 0x7e, 0x41, // PTS = 180000, encoded into 5 bytes - ]); - // 180000 (TS PTS) divided by fixed TS timescale (90000) = 2s. - // 2000 (MP4 PTS) divided by parsed MP4 timescale (1000) = 2s. - segmentDataStartTime = 2; - nullTsPacketData = new Uint8Array([ - 0x47, // TS sync byte (fixed value) - 0x1f, 0xff, // null packet (packet ID 8191) - ]); - }); - - it('parses start time from mp4 segment', async () => { - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', media) - .setResponseValue('test:/init.mp4', initSegmentData) - .setResponseValue('test:/main.mp4', segmentData); - - const expectedRef = ManifestParser.makeReference( - /* uri= */ 'test:/main.mp4', - /* startTime= */ 0, - /* endTime= */ 5, - /* baseUri= */ '', - expectedStartByte, - expectedEndByte); - // In VOD content, we set the timestampOffset to align the - // content to presentation time 0. - expectedRef.timestampOffset = -segmentDataStartTime; - - const manifest = await parser.start('test:/master', playerInterface); - const video = manifest.variants[0].video; - await video.createSegmentIndex(); - ManifestParser.verifySegmentIndex(video, [expectedRef]); - - // Make sure the segment data was fetched with the correct byte - // range. - fakeNetEngine.expectRangeRequest( - 'test:/main.mp4', - expectedStartByte, - partialEndByte); - }); - - it('parses start time from ts segments', async () => { - const tsMediaPlaylist = media.replace(/\.mp4/g, '.ts'); - - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', tsMediaPlaylist) - .setResponseValue('test:/main.ts', tsSegmentData); - - const expectedRef = ManifestParser.makeReference( - /* uri= */ 'test:/main.ts', - /* startTime= */ 0, - /* endTime= */ 5, - /* baseUri= */ '', - expectedStartByte, - expectedEndByte); - // In VOD content, we set the timestampOffset to align the - // content to presentation time 0. - expectedRef.timestampOffset = -segmentDataStartTime; - - const manifest = await parser.start('test:/master', playerInterface); - const video = manifest.variants[0].video; - await video.createSegmentIndex(); - ManifestParser.verifySegmentIndex(video, [expectedRef]); - - // Make sure the segment data was fetched with the correct byte - // range. - fakeNetEngine.expectRangeRequest( - 'test:/main.ts', - expectedStartByte, - partialEndByte); - }); - - it('parses start time from ts segments with null packets', async () => { - const tsMediaPlaylist = media.replace(/\.mp4/g, '.ts'); - - // Each packet is 188 bytes, so allocate space for 3. - const tsSegmentWithNullPackets = new Uint8Array(188 * 3); - // The first two are "null" packets. - tsSegmentWithNullPackets.set(nullTsPacketData, /* offset= */ 0); - tsSegmentWithNullPackets.set(nullTsPacketData, /* offset= */ 188); - // The third has a timestamp. - tsSegmentWithNullPackets.set(tsSegmentData, /* offset= */ 188 * 2); - - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', tsMediaPlaylist) - .setResponseValue('test:/main.ts', tsSegmentWithNullPackets); - - const expectedRef = ManifestParser.makeReference( - /* uri= */ 'test:/main.ts', - /* startTime= */ 0, - /* endTime= */ 5, - /* baseUri= */ '', - expectedStartByte, - expectedEndByte); - // In VOD content, we set the timestampOffset to align the - // content to presentation time 0. - expectedRef.timestampOffset = -segmentDataStartTime; - - const manifest = await parser.start('test:/master', playerInterface); - const video = manifest.variants[0].video; - await video.createSegmentIndex(); - ManifestParser.verifySegmentIndex(video, [expectedRef]); - - // Make sure the segment data was fetched with the correct byte - // range. - fakeNetEngine.expectRangeRequest( - 'test:/main.ts', - expectedStartByte, - partialEndByte); - }); - - // We want to make sure that we can interrupt the parser while it is getting - // the start time. This is a regression test for Issue #1788 where - // interrupting the partial network request would be misinterpreted as the - // server not supporting range requests. - it('can be interrupted', async () => { - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', media) - .setResponseValue('test:/init.mp4', initSegmentData); - - // We are assuming that the time will be pulled out of the main mp4 - // segment, so if we see a request that has a range header, we will stop - // the parser. - /** @type {!Map.} */ - const responses = new Map(); - responses.set('test:/main.mp4', segmentData); - responses.set('test:/init.mp4', initSegmentData); - - responses.forEach((data, uri) => { - fakeNetEngine.setResponse(uri, () => { - // Now that we are stopping the parser, we don't want to see any more - // requests. So if there is another request, fail the test. - responses.forEach((data, uri) => { - fakeNetEngine.setResponse(uri, fail); - }); - - // Stop the parser, but don't wait on it or else we will hit deadlock. - parser.stop(); - - return Promise.resolve(data); - }); - }); - - const expected = Util.jasmineError(new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.PLAYER, - shaka.util.Error.Code.OPERATION_ABORTED)); - await expectAsync(parser.start('test:/master', playerInterface)) - .toBeRejectedWith(expected); - }); - - it('sets duration with respect to presentation offset', async () => { - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', media) - .setResponseValue('test:/init.mp4', initSegmentData) - .setResponseValue('test:/main.mp4', segmentData); - - const manifest = await parser.start('test:/master', playerInterface); - const presentationTimeline = manifest.presentationTimeline; - - const video = manifest.variants[0].video; - await video.createSegmentIndex(); - goog.asserts.assert(video.segmentIndex != null, 'Null segmentIndex!'); - - const refs = Array.from(video.segmentIndex); - expect(refs.length).toBe(1); - - expect(refs[0].timestampOffset).toBe(-segmentDataStartTime); - // The duration should be set to the sum of the segment durations (5), - // even though the endTime of the segment is larger. - expect(refs[0].endTime - refs[0].startTime).toBe(5); - expect(presentationTimeline.getDuration()).toBe(5); - }); - - it('forces full segment request', async () => { - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', media) - .setResponseValue('test:/init.mp4', initSegmentData) - .setResponseValue('test:/main.mp4', segmentData); - - const config = shaka.util.PlayerConfiguration.createDefault().manifest; - config.hls.useFullSegmentsForStartTime = true; - parser.configure(config); - await parser.start('test:/master', playerInterface); - - // Make sure the segment data was fetched with the correct byte - // range. - fakeNetEngine.expectRangeRequest( - 'test:/main.mp4', - expectedStartByte, - expectedEndByte); - }); - }); - it('correctly detects VOD streams as non-live', async () => { const master = [ '#EXTM3U\n', @@ -3040,80 +2740,12 @@ describe('HlsParser', () => { stream.language = 'fr'; }); }); + manifest.sequenceMode = true; }); await testHlsParser(master, media, manifest); }); - it('skips raw audio formats', async () => { - const master = [ - '#EXTM3U\n', - '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",URI="audio1"\n', - '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",URI="audio2"\n', - '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",URI="audio3"\n', - '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",URI="audio4"\n', - '#EXT-X-STREAM-INF:BANDWIDTH=400,CODECS="avc1,mp4a",', - 'RESOLUTION=1280x720,AUDIO="audio"\n', - 'video\n', - ].join(''); - - const videoMedia = [ - '#EXTM3U\n', - '#EXT-X-PLAYLIST-TYPE:VOD\n', - '#EXT-X-MAP:URI="v-init.mp4"\n', - '#EXTINF:5,\n', - 'v1.mp4', - ].join(''); - - const audioMedia1 = [ - '#EXTM3U\n', - '#EXT-X-PLAYLIST-TYPE:VOD\n', - '#EXTINF:5,\n', - 'a1.mp3', - ].join(''); - - const audioMedia2 = [ - '#EXTM3U\n', - '#EXT-X-PLAYLIST-TYPE:VOD\n', - '#EXTINF:5,\n', - 'a1.aac', - ].join(''); - - const audioMedia3 = [ - '#EXTM3U\n', - '#EXT-X-PLAYLIST-TYPE:VOD\n', - '#EXTINF:5,\n', - 'a1.ac3', - ].join(''); - - const audioMedia4 = [ - '#EXTM3U\n', - '#EXT-X-PLAYLIST-TYPE:VOD\n', - '#EXTINF:5,\n', - 'a1.ec3', - ].join(''); - - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', videoMedia) - .setResponseText('test:/audio1', audioMedia1) - .setResponseText('test:/audio2', audioMedia2) - .setResponseText('test:/audio3', audioMedia3) - .setResponseText('test:/audio4', audioMedia4) - .setResponseValue('test:/v-init.mp4', initSegmentData) - .setResponseValue('test:/v1.mp4', segmentData); - - const alwaysWarnSpy = jasmine.createSpy('shaka.log.alwaysWarn'); - shaka.log.alwaysWarn = shaka.test.Util.spyFunc(alwaysWarnSpy); - - const manifest = await parser.start('test:/master', playerInterface); - expect(manifest.variants.length).toBe(1); - expect(manifest.variants[0].audio).toBe(null); - - // We should log a warning when this happens. - expect(alwaysWarnSpy).toHaveBeenCalled(); - }); - // Issue #1875 it('ignores audio groups on audio-only content', async () => { // NOTE: To reproduce the original issue accurately, the two audio playlist @@ -3149,6 +2781,7 @@ describe('HlsParser', () => { stream.mime('audio/mp4', 'mp4a'); }); }); + manifest.sequenceMode = true; }); fakeNetEngine @@ -3331,6 +2964,7 @@ describe('HlsParser', () => { stream.mime('audio/mp4', 'mp4a'); }); }); + manifest.sequenceMode = true; }); await testHlsParser(master, media, manifest); @@ -3368,6 +3002,7 @@ describe('HlsParser', () => { stream.mime('audio/mp4', 'mp4a'); }); }); + manifest.sequenceMode = true; }); await testHlsParser(master, media, manifest); @@ -3406,6 +3041,7 @@ describe('HlsParser', () => { stream.mime('audio/mp4', 'mp4a'); }); }); + manifest.sequenceMode = true; }); await testHlsParser(master, media, manifest); @@ -3445,6 +3081,7 @@ describe('HlsParser', () => { stream.mime('audio/mp4', 'mp4a'); }); }); + manifest.sequenceMode = true; }); await testHlsParser(master, media, manifest); diff --git a/test/media/playhead_unit.js b/test/media/playhead_unit.js index edbd016266..58b31673e6 100644 --- a/test/media/playhead_unit.js +++ b/test/media/playhead_unit.js @@ -152,6 +152,7 @@ describe('Playhead', () => { presentationTimeline: timeline, minBufferTime: 10, offlineSessionIds: [], + sequenceMode: false, }; config = shaka.util.PlayerConfiguration.createDefault().streaming; diff --git a/test/media/streaming_engine_integration.js b/test/media/streaming_engine_integration.js index 4ee42b7846..df866c8fb3 100644 --- a/test/media/streaming_engine_integration.js +++ b/test/media/streaming_engine_integration.js @@ -637,6 +637,7 @@ describe('StreamingEngine', () => { minBufferTime: 2, textStreams: [], imageStreams: [], + sequenceMode: false, variants: [{ id: 1, video: { diff --git a/test/media/streaming_engine_unit.js b/test/media/streaming_engine_unit.js index b88d58b34a..4523791fe8 100644 --- a/test/media/streaming_engine_unit.js +++ b/test/media/streaming_engine_unit.js @@ -520,7 +520,8 @@ describe('StreamingEngine', () => { expectedMseInit.set(ContentType.VIDEO, videoStream); expectedMseInit.set(ContentType.TEXT, textStream); - expect(mediaSourceEngine.init).toHaveBeenCalledWith(expectedMseInit, false); + expect(mediaSourceEngine.init).toHaveBeenCalledWith(expectedMseInit, + /** forceTransmuxTS= */ false, /** sequenceMode= */ false); expect(mediaSourceEngine.init).toHaveBeenCalledTimes(1); expect(mediaSourceEngine.setDuration).toHaveBeenCalledTimes(1); diff --git a/test/offline/manifest_convert_unit.js b/test/offline/manifest_convert_unit.js index 67b73f740d..62b3e04058 100644 --- a/test/offline/manifest_convert_unit.js +++ b/test/offline/manifest_convert_unit.js @@ -115,6 +115,7 @@ describe('ManifestConverter', () => { }, appMetadata: null, creationTime: 0, + sequenceMode: false, }; const manifest = createConverter().fromManifestDB(manifestDb); @@ -151,6 +152,7 @@ describe('ManifestConverter', () => { createVideoStreamDB(1, [0]), createVideoStreamDB(2, [1]), ], + sequenceMode: false, }; const manifest = createConverter().fromManifestDB(manifestDb); @@ -178,6 +180,7 @@ describe('ManifestConverter', () => { createAudioStreamDB(1, [0]), createAudioStreamDB(2, [1]), ], + sequenceMode: false, }; const manifest = createConverter().fromManifestDB(manifestDb); @@ -190,6 +193,29 @@ describe('ManifestConverter', () => { expect(manifest.variants[1].video).toBe(null); }); + it('supports containerless content', () => { + /** @type {shaka.extern.ManifestDB} */ + const manifestDb = { + originalManifestUri: 'http://example.com/foo', + duration: 60, + size: 1234, + expiration: Infinity, + sessionIds: [], + drmInfo: null, + appMetadata: null, + creationTime: 0, + streams: [ + createVideoStreamDB(1, [0]), + createAudioStreamDB(2, [0]), + ], + sequenceMode: true, + }; + + const manifest = createConverter().fromManifestDB(manifestDb); + expect(manifest.sequenceMode).toBe(true); + expect(manifest.variants.length).toBe(1); + }); + it('supports text streams', () => { /** @type {shaka.extern.ManifestDB} */ const manifestDb = { @@ -205,6 +231,7 @@ describe('ManifestConverter', () => { createVideoStreamDB(1, [0]), createTextStreamDB(2), ], + sequenceMode: false, }; const manifest = createConverter().fromManifestDB(manifestDb); @@ -243,6 +270,7 @@ describe('ManifestConverter', () => { createVideoStreamDB(video1, [variant1]), createVideoStreamDB(video2, [variant2, variant3]), ], + sequenceMode: false, }; const manifest = createConverter().fromManifestDB(manifestDb); diff --git a/test/offline/offline_manifest_parser_unit.js b/test/offline/offline_manifest_parser_unit.js index aa5aca0265..79620e90dc 100644 --- a/test/offline/offline_manifest_parser_unit.js +++ b/test/offline/offline_manifest_parser_unit.js @@ -216,6 +216,7 @@ filterDescribe('OfflineManifestParser', offlineManifestParserSupport, () => { sessionIds: [sessionId], drmInfo: null, appMetadata: {}, + sequenceMode: false, }; return manifest; diff --git a/test/test/util/manifest_generator.js b/test/test/util/manifest_generator.js index be862d3889..fbff247fe4 100644 --- a/test/test/util/manifest_generator.js +++ b/test/test/util/manifest_generator.js @@ -107,6 +107,8 @@ shaka.test.ManifestGenerator.Manifest = class { this.offlineSessionIds = []; /** @type {number} */ this.minBufferTime = 0; + /** @type {boolean} */ + this.sequenceMode = false; /** @type {shaka.extern.Manifest} */ const foo = this; diff --git a/test/test/util/offline_utils.js b/test/test/util/offline_utils.js index bba36b50ba..26fe5ecfdf 100644 --- a/test/test/util/offline_utils.js +++ b/test/test/util/offline_utils.js @@ -25,6 +25,7 @@ shaka.test.OfflineUtils = class { streams: [], sessionIds: [], size: 1024, + sequenceMode: false, }; } diff --git a/test/test/util/streaming_engine_util.js b/test/test/util/streaming_engine_util.js index a25595bf91..119743bf7a 100644 --- a/test/test/util/streaming_engine_util.js +++ b/test/test/util/streaming_engine_util.js @@ -290,6 +290,7 @@ shaka.test.StreamingEngineUtil = class { variants: [], textStreams: [], imageStreams: [], + sequenceMode: false, }; /** @type {shaka.extern.Variant} */