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} */