From 36d0b5484fad68dc1d640fbddf2fae3e1eb7169b Mon Sep 17 00:00:00 2001 From: Michelle Zhuo Date: Tue, 23 Nov 2021 10:19:30 -0800 Subject: [PATCH] feat(HLS): Containerless format support This adds code to allow Shaka Player to play media in sequence mode, an alternate playback mode that makes the browser ignore media timestamps, when playing HLS media. This is important for containerless media formats, as they do not contain such timestamps. Changing HLS to not require timestamps also means that we no longer need to fetch media segments in order to get the start time, which should lower bandwidth usage and startup delay. In initial tests, on a simulated 3G network, load latency went down from an average 3.16s to 2.61s on the HLS version of "Big Buck Bunny: the Dark Truths of a Video Dev Cartoon"; an improvement of about 17%. Issue #2337 Change-Id: I507898d74ae30ddfb1bddf8dce643780949fbd9b --- README.md | 3 +- demo/common/message_ids.js | 1 - demo/config.js | 2 - demo/locales/en.json | 1 - demo/locales/source.json | 4 - docs/tutorials/faq.md | 11 - externs/shaka/manifest.js | 6 +- externs/shaka/offline.js | 6 +- externs/shaka/player.js | 4 - externs/sourcebuffer.js | 15 + lib/dash/dash_parser.js | 1 + lib/hls/hls_parser.js | 670 +------------------ lib/media/media_source_engine.js | 38 +- lib/media/presentation_timeline.js | 3 +- lib/media/streaming_engine.js | 24 +- lib/offline/indexeddb/v1_storage_cell.js | 1 + lib/offline/indexeddb/v2_storage_cell.js | 1 + lib/offline/manifest_converter.js | 1 + lib/offline/storage.js | 1 + lib/util/error.js | 7 +- lib/util/player_configuration.js | 1 - roadmap.md | 2 - test/hls/hls_live_unit.js | 250 ++----- test/hls/hls_parser_unit.js | 505 ++------------ test/media/playhead_unit.js | 1 + test/media/streaming_engine_integration.js | 1 + test/media/streaming_engine_unit.js | 3 +- test/offline/manifest_convert_unit.js | 28 + test/offline/offline_manifest_parser_unit.js | 1 + test/test/util/manifest_generator.js | 2 + test/test/util/offline_utils.js | 1 + test/test/util/streaming_engine_util.js | 1 + 32 files changed, 275 insertions(+), 1321 deletions(-) create mode 100644 externs/sourcebuffer.js 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} */