diff --git a/demo/common/message_ids.js b/demo/common/message_ids.js
index 8b6484045a..159eddcca1 100644
--- a/demo/common/message_ids.js
+++ b/demo/common/message_ids.js
@@ -188,6 +188,7 @@ 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',
+ IGNORE_MANIFEST_PROGRAM_DATE_TIME: 'DEMO_IGNORE_MANIFEST_PROGRAM_DATE_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 5c647f57dc..738e01719b 100644
--- a/demo/config.js
+++ b/demo/config.js
@@ -213,6 +213,8 @@ shakaDemo.Config = class {
'manifest.hls.defaultAudioCodec')
.addTextInput_(MessageIds.DEFAULT_VIDEO_CODEC,
'manifest.hls.defaultVideoCodec')
+ .addBoolInput_(MessageIds.IGNORE_MANIFEST_PROGRAM_DATE_TIME,
+ 'manifest.hls.ignoreManifestProgramDateTime')
.addNumberInput_(MessageIds.AVAILABILITY_WINDOW_OVERRIDE,
'manifest.availabilityWindowOverride',
/* canBeDecimal= */ true,
diff --git a/demo/locales/en.json b/demo/locales/en.json
index d03dcb19c2..b69622cd85 100644
--- a/demo/locales/en.json
+++ b/demo/locales/en.json
@@ -99,6 +99,7 @@
"DEMO_IMA_ASSET_KEY": "Asset key (for LIVE DAI Content)",
"DEMO_IMA_CONTENT_SRC_ID": "Content source ID (for VOD DAI Content)",
"DEMO_IMA_VIDEO_ID": "Video ID (for VOD DAI Content)",
+ "DEMO_IGNORE_MANIFEST_PROGRAM_DATE_TIME": "Ignore Program Date Time from manifest",
"DEMO_IGNORE_MIN_BUFFER_TIME": "Ignore Min Buffer Time",
"DEMO_IGNORE_TEXT_FAILURES": "Ignore Text Stream Failures",
"DEMO_INACCURATE_MANIFEST_TOLERANCE": "Inaccurate Manifest Tolerance",
diff --git a/demo/locales/source.json b/demo/locales/source.json
index 0cdc7468c5..aa2920a5fb 100644
--- a/demo/locales/source.json
+++ b/demo/locales/source.json
@@ -399,6 +399,10 @@
"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_IGNORE_MANIFEST_PROGRAM_DATE_TIME": {
+ "description": "The name of a configuration value.",
+ "message": "Ignore Program Date Time from manifest"
+ },
"DEMO_IGNORE_MIN_BUFFER_TIME": {
"description": "The name of a configuration value.",
"message": "Ignore Min Buffer Time"
diff --git a/externs/shaka/player.js b/externs/shaka/player.js
index 444d7e89b0..fca29e9848 100644
--- a/externs/shaka/player.js
+++ b/externs/shaka/player.js
@@ -747,7 +747,8 @@ shaka.extern.DashManifestConfiguration;
* ignoreTextStreamFailures: boolean,
* ignoreImageStreamFailures: boolean,
* defaultAudioCodec: string,
- * defaultVideoCodec: string
+ * defaultVideoCodec: string,
+ * ignoreManifestProgramDateTime: boolean
* }}
*
* @property {boolean} ignoreTextStreamFailures
@@ -762,6 +763,13 @@ shaka.extern.DashManifestConfiguration;
* @property {string} defaultVideoCodec
* The default video codec if it is not specified in the HLS playlist.
* Defaults to 'avc1.42E01E'
.
+ * @property {boolean} ignoreManifestProgramDateTime
+ * If true
, the HLS parser will ignore the
+ * EXT-X-PROGRAM-DATE-TIME
tags in the manifest, and always
+ * create synthetic sync times.
+ * Meant for content that should be synchronized, but has broken values for
+ * synchronization.
+ * Defaults to false
.
* @exportDoc
*/
shaka.extern.HlsManifestConfiguration;
diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js
index 05a2bcab06..b5edb1ae37 100644
--- a/lib/hls/hls_parser.js
+++ b/lib/hls/hls_parser.js
@@ -38,6 +38,7 @@ goog.require('shaka.util.OperationManager');
goog.require('shaka.util.Pssh');
goog.require('shaka.util.Timer');
goog.require('shaka.util.Platform');
+goog.require('shaka.util.XmlUtils');
goog.requireType('shaka.hls.Segment');
@@ -120,6 +121,16 @@ shaka.hls.HlsParser = class {
*/
this.updatePlaylistDelay_ = 0;
+ /**
+ * A time offset to apply to EXT-X-PROGRAM-DATE-TIME values to normalize
+ * them so that they start at 0, and are in scale with the normal time
+ * values.
+ * Starts as null, which represents how we cannot know how to properly
+ * normalize the values until we have seen each playlist.
+ * @private {?number}
+ */
+ this.syncTimeOffset_ = null;
+
/**
* This timer is used to trigger the start of a manifest update. A manifest
* update is async. Once the update is finished, the timer will be restarted
@@ -314,7 +325,7 @@ shaka.hls.HlsParser = class {
const segments = this.createSegments_(
streamInfo.verbatimMediaPlaylistUri, playlist, stream.type,
stream.mimeType, streamInfo.mediaSequenceToStartTime, mediaVariables,
- stream.codecs, stream.bandwidth);
+ stream.codecs, stream.bandwidth, streamInfo.streamStartTime).segments;
stream.segmentIndex.mergeAndEvict(
segments, this.presentationTimeline_.getSegmentAvailabilityStart());
@@ -351,6 +362,36 @@ shaka.hls.HlsParser = class {
// No-op
}
+ /**
+ * If necessary, makes sure that sync times will be normalized to 0, so that
+ * a stream does not start buffering at 50 years in because sync times are
+ * measured in time since 1970.
+ * @private
+ */
+ calculateSyncTimeOffset_() {
+ if (this.syncTimeOffset_ != null) {
+ // The offset was already calculated.
+ return;
+ }
+
+ const segments = new Set();
+ let lowestSyncTime = Infinity;
+ for (const streamInfo of this.uriToStreamInfosMap_.values()) {
+ for (const segment of (streamInfo.stream.segmentIndex || [])) {
+ if (segment.syncTime != null) {
+ lowestSyncTime = Math.min(lowestSyncTime, segment.syncTime);
+ segments.add(segment);
+ }
+ }
+ }
+ if (segments.size > 0) {
+ this.syncTimeOffset_ = -lowestSyncTime;
+ for (const segment of segments) {
+ segment.syncTime += this.syncTimeOffset_;
+ }
+ }
+ }
+
/**
* Parses the manifest.
*
@@ -433,6 +474,10 @@ shaka.hls.HlsParser = class {
shaka.util.Error.Code.OPERATION_ABORTED);
}
+ // Now that we have generated all streams, we can determine the offset to
+ // apply to sync times.
+ this.calculateSyncTimeOffset_();
+
if (this.aesEncrypted_ && variants.length == 0) {
// We do not support AES-128 encryption with HLS yet. Variants is null
// when the playlist is encrypted with AES-128.
@@ -1451,10 +1496,13 @@ shaka.hls.HlsParser = class {
const mediaSequenceToStartTime = new Map();
let segments;
+ let streamStartTime = 0;
try {
- segments = this.createSegments_(verbatimMediaPlaylistUri,
+ const ret = this.createSegments_(verbatimMediaPlaylistUri,
playlist, type, mimeType, mediaSequenceToStartTime, mediaVariables,
codecs, bandwidth);
+ segments = ret.segments;
+ streamStartTime = ret.streamStartTime;
} catch (error) {
if (error.code == shaka.util.Error.Code.HLS_INTERNAL_SKIP_STREAM) {
shaka.log.alwaysWarn('Skipping unsupported HLS stream',
@@ -1527,6 +1575,7 @@ shaka.hls.HlsParser = class {
maxTimestamp: lastEndTime,
mediaSequenceToStartTime,
canSkipSegments,
+ streamStartTime,
};
}
@@ -1706,8 +1755,8 @@ shaka.hls.HlsParser = class {
* @param {number} startTime
* @param {!Map.} variables
* @param {string} absoluteMediaPlaylistUri
- * @return {!shaka.media.SegmentReference}
* @param {string} type
+ * @return {!shaka.media.SegmentReference}
* @private
*/
createSegmentReference_(
@@ -1730,6 +1779,24 @@ shaka.hls.HlsParser = class {
'true, and see https://bit.ly/3clctcj for details.');
}
+ let syncTime = null;
+ if (!this.config_.hls.ignoreManifestProgramDateTime) {
+ const dateTimeTag =
+ shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-PROGRAM-DATE-TIME');
+ if (dateTimeTag && dateTimeTag.value) {
+ const time = shaka.util.XmlUtils.parseDate(
+ dateTimeTag.value, /* subSecond= */ true);
+ goog.asserts.assert(time != null,
+ 'EXT-X-PROGRAM-DATE-TIME format not valid');
+ // Sync time offset is null on the first go-through. This indicates that
+ // we have not yet seen every stream, and thus do not yet have enough
+ // information to determine how to normalize the sync times.
+ // For that first go-through, the sync time will be applied after the
+ // references are all created. Until then, just offset by 0.
+ syncTime = time + (this.syncTimeOffset_ || 0);
+ }
+ }
+
// Create SegmentReferences for the partial segments.
const partialSegmentRefs = [];
if (this.lowLatencyMode_ && hlsSegment.partialSegments.length) {
@@ -1839,6 +1906,7 @@ shaka.hls.HlsParser = class {
partialSegmentRefs,
tilesLayout,
tileDuration,
+ syncTime,
);
}
@@ -1894,11 +1962,15 @@ shaka.hls.HlsParser = class {
* @param {!Map.} variables
* @param {string} codecs
* @param {(number|undefined)} bandwidth
- * @return {!Array.}
+ * @param {number=} streamStartTime
+ * @return {{
+ * segments: !Array.,
+ * streamStartTime: number
+ * }}
* @private
*/
createSegments_(verbatimMediaPlaylistUri, playlist, type, mimeType,
- mediaSequenceToStartTime, variables, codecs, bandwidth) {
+ mediaSequenceToStartTime, variables, codecs, bandwidth, streamStartTime) {
/** @type {Array.} */
const hlsSegments = playlist.segments;
goog.asserts.assert(hlsSegments.length, 'Playlist should have segments!');
@@ -1910,6 +1982,8 @@ shaka.hls.HlsParser = class {
// time.
const mediaSequenceNumber = shaka.hls.Utils.getFirstTagWithNameAsNumber(
playlist.tags, 'EXT-X-MEDIA-SEQUENCE', 0);
+ const targetDuration = shaka.hls.Utils.getFirstTagWithNameAsNumber(
+ playlist.tags, 'EXT-X-TARGETDURATION', 0);
const skipTag = shaka.hls.Utils.getFirstTagWithName(playlist.tags,
'EXT-X-SKIP');
const skippedSegments =
@@ -1917,6 +1991,13 @@ shaka.hls.HlsParser = class {
let position = mediaSequenceNumber + skippedSegments;
let firstStartTime = 0;
+ if (!streamStartTime) {
+ // Make an approximation of the start time of the first segment. This is
+ // used to synthesize program date-time values if there are none, to
+ // handle the case of different streams starting at different times.
+ streamStartTime = mediaSequenceNumber * targetDuration;
+ }
+
// For live stream, use the cached value in the mediaSequenceToStartTime
// map if available.
if (this.isLive_() && mediaSequenceToStartTime.has(position)) {
@@ -1928,11 +2009,11 @@ shaka.hls.HlsParser = class {
'starts at', firstStartTime);
/** @type {!Array.} */
- const references = [];
+ const segments = [];
const enumerate = (it) => shaka.util.Iterables.enumerate(it);
for (const {i, item} of enumerate(hlsSegments)) {
- const previousReference = references[references.length - 1];
+ const previousReference = segments[segments.length - 1];
const startTime =
(i == 0) ? firstStartTime : previousReference.endTime;
position = mediaSequenceNumber + skippedSegments + i;
@@ -1963,10 +2044,58 @@ shaka.hls.HlsParser = class {
playlist.absoluteUri,
type);
- references.push(reference);
+ segments.push(reference);
+ }
+
+ // If some segments have sync times, but not all, extrapolate the sync
+ // times of the ones with none.
+ const someSyncTime = segments.some((ref) => ref.syncTime != null);
+ if (someSyncTime) {
+ for (let i = 0; i < segments.length; i++) {
+ const reference = segments[i];
+ if (reference.syncTime != null) {
+ // No need to extrapolate.
+ continue;
+ }
+ // Find the nearest segment with syncTime, in either direction.
+ // This looks forward and backward simultaneously, keeping track of what
+ // to offset the syncTime it finds by as it goes.
+ let forwardAdd = 0;
+ let backwardAdd = 0;
+ for (let j = 0; reference.syncTime == null; j++) {
+ for (const forward of [false, true]) {
+ const other = segments[i + j * (forward ? 1 : -1)];
+ if (other) {
+ if (other.syncTime != null) {
+ reference.syncTime = other.syncTime;
+ reference.syncTime += forward ? forwardAdd : backwardAdd;
+ break;
+ }
+ if (forward) {
+ forwardAdd -= other.endTime - other.startTime;
+ }
+ if (!forward) {
+ backwardAdd += other.endTime - other.startTime;
+ }
+ }
+ }
+ }
+ }
+ } else {
+ // If we want to use program date-time for sync-ing, but there is no such
+ // tag anywhere in the manifest, create synthetic sync times based on the
+ // start time of the manifest.
+ for (const reference of segments) {
+ reference.syncTime = reference.startTime;
+ // Modify this sync time by the start time of the playlist, in case
+ // different streams start at different times.
+ reference.syncTime += streamStartTime;
+ // Also apply the offset, if present.
+ reference.syncTime += this.syncTimeOffset_ || 0;
+ }
}
- return references;
+ return {segments, streamStartTime};
}
/**
@@ -2343,7 +2472,8 @@ shaka.hls.HlsParser = class {
* minTimestamp: number,
* maxTimestamp: number,
* mediaSequenceToStartTime: !Map.,
- * canSkipSegments: boolean
+ * canSkipSegments: boolean,
+ * streamStartTime: number
* }}
*
* @description
@@ -2368,6 +2498,9 @@ shaka.hls.HlsParser = class {
* @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.
+ * @property {number} streamStartTime
+ * The time of the first segment of the stream. Used to synthesize program
+ * date-time values if none are present, for audio-video synchronization.
*/
shaka.hls.HlsParser.StreamInfo;
diff --git a/lib/media/media_source_engine.js b/lib/media/media_source_engine.js
index 688147565c..3f5fe6be23 100644
--- a/lib/media/media_source_engine.js
+++ b/lib/media/media_source_engine.js
@@ -506,7 +506,6 @@ 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}
@@ -515,11 +514,11 @@ shaka.media.MediaSourceEngine = class {
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 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 (seeked) {
const timestampOffset = /** @type {number} */ (startTime);
this.enqueueOperation_(
diff --git a/lib/media/segment_index.js b/lib/media/segment_index.js
index 6147b80abe..1159a44838 100644
--- a/lib/media/segment_index.js
+++ b/lib/media/segment_index.js
@@ -348,7 +348,8 @@ shaka.media.SegmentIndex = class {
lastReference.appendWindowEnd,
lastReference.partialReferences,
lastReference.tilesLayout,
- lastReference.tileDuration);
+ lastReference.tileDuration,
+ lastReference.syncTime);
}
diff --git a/lib/media/segment_reference.js b/lib/media/segment_reference.js
index ab146015aa..1dca44a4eb 100644
--- a/lib/media/segment_reference.js
+++ b/lib/media/segment_reference.js
@@ -159,11 +159,15 @@ shaka.media.SegmentReference = class {
* The explicit duration of an individual tile within the tiles grid.
* If not provided, the duration should be automatically calculated based on
* the duration of the reference.
+ * @param {?number=} syncTime
+ * A time value, expressed in the same scale as the start and end time, which
+ * is used to synchronize between streams.
*/
constructor(
startTime, endTime, uris, startByte, endByte, initSegmentReference,
timestampOffset, appendWindowStart, appendWindowEnd,
- partialReferences = [], tilesLayout = '', tileDuration = null) {
+ partialReferences = [], tilesLayout = '', tileDuration = null,
+ syncTime = null) {
// A preload hinted Partial Segment has the same startTime and endTime.
goog.asserts.assert(startTime <= endTime,
'startTime must be less than or equal to endTime');
@@ -213,6 +217,9 @@ shaka.media.SegmentReference = class {
/** @type {?number} */
this.tileDuration = tileDuration;
+
+ /** @type {?number} */
+ this.syncTime = syncTime;
}
/**
diff --git a/lib/media/streaming_engine.js b/lib/media/streaming_engine.js
index ed10a3a7ad..7af7728a55 100644
--- a/lib/media/streaming_engine.js
+++ b/lib/media/streaming_engine.js
@@ -1595,14 +1595,15 @@ shaka.media.StreamingEngine = class {
await this.evict_(mediaState, presentationTime);
this.destroyer_.ensureNotDestroyed();
- shaka.log.v1(logPrefix, 'appending media segment');
+ shaka.log.v1(logPrefix, 'appending media segment at',
+ (reference.syncTime == null ? 'unknown' : reference.syncTime));
const seeked = mediaState.seeked;
mediaState.seeked = false;
await this.playerInterface_.mediaSourceEngine.appendBuffer(
mediaState.type,
segment,
- reference.startTime,
+ reference.syncTime == null ? reference.startTime : reference.syncTime,
reference.endTime,
hasClosedCaptions,
seeked,
diff --git a/lib/util/player_configuration.js b/lib/util/player_configuration.js
index 46e7290e58..fe4f5d27e9 100644
--- a/lib/util/player_configuration.js
+++ b/lib/util/player_configuration.js
@@ -121,6 +121,7 @@ shaka.util.PlayerConfiguration = class {
ignoreImageStreamFailures: false,
defaultAudioCodec: 'mp4a.40.2',
defaultVideoCodec: 'avc1.42E01E',
+ ignoreManifestProgramDateTime: false,
},
};
diff --git a/lib/util/xml_utils.js b/lib/util/xml_utils.js
index 84f4bf3cde..877aeeb673 100644
--- a/lib/util/xml_utils.js
+++ b/lib/util/xml_utils.js
@@ -174,10 +174,11 @@ shaka.util.XmlUtils = class {
/**
* Parses an XML date string.
* @param {string} dateString
+ * @param {boolean} subSecond Whether sub-second timing is allowed.
* @return {?number} The parsed date in seconds on success; otherwise, return
* null.
*/
- static parseDate(dateString) {
+ static parseDate(dateString, subSecond=false) {
if (!dateString) {
return null;
}
@@ -190,8 +191,12 @@ shaka.util.XmlUtils = class {
dateString += 'Z';
}
- const result = Date.parse(dateString);
- return (!isNaN(result) ? Math.floor(result / 1000.0) : null);
+ let result = Date.parse(dateString);
+ if (isNaN(result)) {
+ return null;
+ }
+ result /= 1000.0;
+ return subSecond ? result : Math.floor(result);
}
diff --git a/test/hls/hls_live_unit.js b/test/hls/hls_live_unit.js
index 7e76acda1b..e566a2aeda 100644
--- a/test/hls/hls_live_unit.js
+++ b/test/hls/hls_live_unit.js
@@ -194,8 +194,8 @@ describe('HlsParser live', () => {
describe('update', () => {
it('adds new segments when they appear', async () => {
- const ref1 = ManifestParser.makeReference('test:/main.mp4', 0, 2);
- const ref2 = ManifestParser.makeReference('test:/main2.mp4', 2, 4);
+ const ref1 = makeReference('test:/main.mp4', 0, 2);
+ const ref2 = makeReference('test:/main2.mp4', 2, 4);
await testUpdate(
master, media, [ref1], mediaWithAdditionalSegment, [ref1, ref2]);
@@ -209,8 +209,8 @@ describe('HlsParser live', () => {
].join('');
const masterWithTwoVariants = master + secondVariant;
- const ref1 = ManifestParser.makeReference('test:/main.mp4', 0, 2);
- const ref2 = ManifestParser.makeReference('test:/main2.mp4', 2, 4);
+ const ref1 = makeReference('test:/main.mp4', 0, 2);
+ const ref2 = makeReference('test:/main2.mp4', 2, 4);
await testUpdate(
masterWithTwoVariants, media, [ref1], mediaWithAdditionalSegment,
@@ -230,8 +230,8 @@ describe('HlsParser live', () => {
].join('');
const masterWithAudio = masterlist + audio;
- const ref1 = ManifestParser.makeReference('test:/main.mp4', 0, 2);
- const ref2 = ManifestParser.makeReference('test:/main2.mp4', 2, 4);
+ const ref1 = makeReference('test:/main.mp4', 0, 2);
+ const ref2 = makeReference('test:/main2.mp4', 2, 4);
await testUpdate(
masterWithAudio, media, [ref1], mediaWithAdditionalSegment,
@@ -251,9 +251,9 @@ describe('HlsParser live', () => {
const updatedMedia1 = media + newSegment1;
const updatedMedia2 = updatedMedia1 + newSegment2;
- 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);
+ const ref1 = makeReference('test:/main.mp4', 0, 2);
+ const ref2 = makeReference('test:/main2.mp4', 2, 4);
+ const ref3 = makeReference('test:/main3.mp4', 4, 6);
fakeNetEngine
.setResponseText('test:/master', master)
@@ -523,14 +523,14 @@ describe('HlsParser live', () => {
.setResponseValue('test:/main.mp4', segmentData)
.setResponseValue('test:/main2.mp4', segmentData);
- const ref1 = ManifestParser.makeReference(
+ const ref1 = makeReference(
'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(
+ const ref2 = makeReference(
'test:/main2.mp4', 2, 4,
/* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null,
/* timestampOffset= */ 0);
@@ -570,31 +570,31 @@ describe('HlsParser live', () => {
.setResponseValue('test:/partial.mp4', segmentData)
.setResponseValue('test:/partial2.mp4', segmentData);
- const partialRef = ManifestParser.makeReference(
+ const partialRef = makeReference(
'test:/partial.mp4', 0, 2,
/* baseUri= */ '', /* startByte= */ 0, /* endByte= */ 199);
- const partialRef2 = ManifestParser.makeReference(
+ const partialRef2 = makeReference(
'test:/partial2.mp4', 2, 4,
/* baseUri= */ '', /* startByte= */ 200, /* endByte= */ 429);
- const partialRef3 = ManifestParser.makeReference(
+ const partialRef3 = makeReference(
'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(
+ const preloadRef = makeReference(
'test:/partial.mp4', 6, 6,
/* baseUri= */ '', /* startByte= */ 210, /* endByte= */ null);
- const ref = ManifestParser.makeReference(
+ const ref = makeReference(
'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(
+ const ref2 = makeReference(
'', 4, 6,
/* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null,
/* timestampOffset= */ 0, [partialRef3, preloadRef]);
@@ -607,16 +607,16 @@ describe('HlsParser live', () => {
describe('update', () => {
it('adds new segments when they appear', async () => {
- const ref1 = ManifestParser.makeReference('test:/main.mp4', 0, 2);
- const ref2 = ManifestParser.makeReference('test:/main2.mp4', 2, 4);
+ const ref1 = makeReference('test:/main.mp4', 0, 2);
+ const ref2 = 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', 0, 2);
- const ref2 = ManifestParser.makeReference('test:/main2.mp4', 2, 4);
+ const ref1 = makeReference('test:/main.mp4', 0, 2);
+ const ref2 = makeReference('test:/main2.mp4', 2, 4);
await testUpdate(
master, mediaWithAdditionalSegment, [ref1, ref2],
@@ -624,12 +624,10 @@ describe('HlsParser live', () => {
});
it('handles updates with redirects', async () => {
- const oldRef1 = ManifestParser.makeReference('test:/main.mp4', 0, 2);
+ const oldRef1 = makeReference('test:/main.mp4', 0, 2);
- const newRef1 =
- ManifestParser.makeReference('test:/redirected/main.mp4', 0, 2);
- const newRef2 =
- ManifestParser.makeReference('test:/redirected/main2.mp4', 2, 4);
+ const newRef1 = makeReference('test:/redirected/main.mp4', 0, 2);
+ const newRef2 = makeReference('test:/redirected/main2.mp4', 2, 4);
let playlistFetchCount = 0;
@@ -886,4 +884,23 @@ describe('HlsParser live', () => {
});
}); // describe('update')
}); // describe('playlist type LIVE')
+
+ /**
+ * @param {string} uri A relative URI to http://example.com
+ * @param {number} start
+ * @param {number} end
+ * @param {string=} baseUri
+ * @param {number=} startByte
+ * @param {?number=} endByte
+ * @param {number=} timestampOffset
+ * @param {!Array.=} partialReferences
+ * @param {?string=} tilesLayout
+ * @return {!shaka.media.SegmentReference}
+ */
+ function makeReference(uri, start, end, baseUri, startByte, endByte,
+ timestampOffset, partialReferences, tilesLayout) {
+ return ManifestParser.makeReference(uri, start, end, baseUri, startByte,
+ endByte, timestampOffset, partialReferences, tilesLayout,
+ /* syncTime= */ start);
+ }
}); // describe('HlsParser live')
diff --git a/test/hls/hls_parser_unit.js b/test/hls/hls_parser_unit.js
index c12a32cfec..87ea9600f7 100644
--- a/test/hls/hls_parser_unit.js
+++ b/test/hls/hls_parser_unit.js
@@ -1869,6 +1869,137 @@ describe('HlsParser', () => {
expect(actual).toEqual(manifest);
});
+ describe('produces syncTime', () => {
+ /** @param {string} media */
+ async function test(media) {
+ const master = [
+ '#EXTM3U\n',
+ '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,vtt",',
+ 'RESOLUTION=960x540,FRAME-RATE=60\n',
+ 'video\n',
+ ].join('');
+
+ const initUris = () => ['test:/init.mp4'];
+ const init = new shaka.media.InitSegmentReference(initUris, 0, 615);
+ let startTime = 0;
+ const segments = [0, 5, 10, 15, 20].map((syncTime) => {
+ const uris = () => ['test:/main.mp4'];
+ const reference = new shaka.media.SegmentReference(
+ startTime, startTime + 5, uris, 0, null, init, 0, 0, Infinity,
+ [], undefined, undefined, syncTime);
+ startTime += 5;
+ return reference;
+ });
+
+ const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
+ manifest.anyTimeline();
+ manifest.addPartialVariant((variant) => {
+ variant.addPartialStream(ContentType.VIDEO, (stream) => {
+ stream.segmentIndex = new shaka.media.SegmentIndex(segments);
+ });
+ });
+ manifest.sequenceMode = true;
+ });
+
+ fakeNetEngine
+ .setResponseText('test:/master', master)
+ .setResponseText('test:/video', media)
+ .setResponseValue('test:/init.mp4', initSegmentData)
+ .setResponseValue('test:/main.mp4', segmentData);
+
+ const actual = await parser.start('test:/master', playerInterface);
+ expect(actual).toEqual(manifest);
+ }
+
+ it('from EXT-X-PROGRAM-DATE-TIME', async () => {
+ await test([
+ '#EXTM3U\n',
+ '#EXT-X-PLAYLIST-TYPE:VOD\n',
+ '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
+ '#EXT-X-PROGRAM-DATE-TIME:2000-01-01T00:00:05.00Z\n',
+ '#EXTINF:5,\n',
+ 'main.mp4\n',
+ '#EXT-X-PROGRAM-DATE-TIME:2000-01-01T00:00:10.00Z\n',
+ '#EXTINF:5,\n',
+ 'main.mp4\n',
+ '#EXT-X-PROGRAM-DATE-TIME:2000-01-01T00:00:15.00Z\n',
+ '#EXTINF:5,\n',
+ 'main.mp4\n',
+ '#EXT-X-PROGRAM-DATE-TIME:2000-01-01T00:00:20.00Z\n',
+ '#EXTINF:5,\n',
+ 'main.mp4\n',
+ '#EXT-X-PROGRAM-DATE-TIME:2000-01-01T00:00:25.00Z\n',
+ '#EXTINF:5,\n',
+ 'main.mp4',
+ ].join(''));
+ });
+
+ it('when some EXT-X-PROGRAM-DATE-TIME values are missing', async () => {
+ await test([
+ '#EXTM3U\n',
+ '#EXT-X-PLAYLIST-TYPE:VOD\n',
+ '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
+ '#EXTINF:5,\n',
+ 'main.mp4\n',
+ '#EXT-X-PROGRAM-DATE-TIME:2000-01-01T00:00:10.00Z\n',
+ '#EXTINF:5,\n',
+ 'main.mp4\n',
+ '#EXTINF:5,\n',
+ 'main.mp4\n',
+ '#EXTINF:5,\n',
+ 'main.mp4\n',
+ '#EXT-X-PROGRAM-DATE-TIME:2000-01-01T00:00:25.00Z\n',
+ '#EXTINF:5,\n',
+ 'main.mp4',
+ ].join(''));
+ });
+
+ it('when all EXT-X-PROGRAM-DATE-TIME values are missing', async () => {
+ await test([
+ '#EXTM3U\n',
+ '#EXT-X-PLAYLIST-TYPE:VOD\n',
+ '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
+ '#EXTINF:5,\n',
+ 'main.mp4\n',
+ '#EXTINF:5,\n',
+ 'main.mp4\n',
+ '#EXTINF:5,\n',
+ 'main.mp4\n',
+ '#EXTINF:5,\n',
+ 'main.mp4\n',
+ '#EXTINF:5,\n',
+ 'main.mp4',
+ ].join(''));
+ });
+
+ it('when all ignoreManifestProgramDateTime is set', async () => {
+ const config = shaka.util.PlayerConfiguration.createDefault().manifest;
+ config.hls.ignoreManifestProgramDateTime = true;
+ parser.configure(config);
+ // A manifest with purposefully bad PDT values, that we can ignore.
+ await test([
+ '#EXTM3U\n',
+ '#EXT-X-PLAYLIST-TYPE:VOD\n',
+ '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
+ '#EXT-X-PROGRAM-DATE-TIME:2000-01-01T00:00:15.00Z\n',
+ '#EXTINF:5,\n',
+ 'main.mp4\n',
+ '#EXT-X-PROGRAM-DATE-TIME:2000-01-01T00:00:07.00Z\n',
+ '#EXTINF:5,\n',
+ 'main.mp4\n',
+ '#EXT-X-PROGRAM-DATE-TIME:2000-01-01T00:00:49.00Z\n',
+ '#EXTINF:5,\n',
+ 'main.mp4\n',
+ '#EXT-X-PROGRAM-DATE-TIME:2000-01-01T00:00:12.00Z\n',
+ '#EXTINF:5,\n',
+ 'main.mp4\n',
+ '#EXT-X-PROGRAM-DATE-TIME:2000-01-01T00:00:44.00Z\n',
+ '#EXTINF:5,\n',
+ 'main.mp4',
+ ].join(''));
+ });
+ });
+
it('drops failed text streams when configured to', async () => {
const master = [
'#EXTM3U\n',
diff --git a/test/test/util/manifest_parser_util.js b/test/test/util/manifest_parser_util.js
index b25c4285cf..7ea1dabe6c 100644
--- a/test/test/util/manifest_parser_util.js
+++ b/test/test/util/manifest_parser_util.js
@@ -57,11 +57,12 @@ shaka.test.ManifestParser = class {
* @param {number=} timestampOffset
* @param {!Array.=} partialReferences
* @param {?string=} tilesLayout
+ * @param {?number=} syncTime
* @return {!shaka.media.SegmentReference}
*/
static makeReference(uri, start, end, baseUri = '',
startByte = 0, endByte = null, timestampOffset = 0,
- partialReferences = [], tilesLayout = '') {
+ partialReferences = [], tilesLayout = '', syncTime = null) {
const getUris = () => uri.length ? [baseUri + uri] : [];
// If a test wants to verify these, they can be set explicitly after
@@ -86,6 +87,7 @@ shaka.test.ManifestParser = class {
appendWindowEnd,
partialReferences,
tilesLayout,
+ syncTime,
);
}
};