feat(hls): Read EXT-X-PROGRAM-DATE-TIME (#4034)

This makes the HLS parser read the EXT-X-PROGRAM-DATE-TIME value
on manifests, and use it to make sure that segments are inserted at
the correct place in the timeline, when in sequence mode.

Issue #2337
This commit is contained in:
theodab
2022-03-24 15:58:53 -07:00
committed by GitHub
parent 2c5457bbec
commit 89409cee3e
16 changed files with 453 additions and 67 deletions
+1
View File
@@ -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',
+2
View File
@@ -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,
+1
View File
@@ -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",
+4
View File
@@ -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"
+7 -1
View File
@@ -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,11 @@ shaka.extern.DashManifestConfiguration;
* @property {string} defaultVideoCodec
* The default video codec if it is not specified in the HLS playlist.
* <i>Defaults to <code>'avc1.42E01E'</code>.</i>
* @property {boolean} ignoreManifestProgramDateTime
* If <code>true</code>, the HLS parser will ignore the
* <code>EXT-X-PROGRAM-DATE-TIME</code> tags in the manifest.
* Meant for tags that are incorrect or malformed.
* <i>Defaults to <code>false</code>.</i>
* @exportDoc
*/
shaka.extern.HlsManifestConfiguration;
+144 -1
View File
@@ -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,15 @@ 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. This is necessary because these times will
* be used to set presentation times for segments.
* null means we don't have enough data yet.
* @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
@@ -351,6 +361,42 @@ 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()) {
const segmentIndex = streamInfo.stream.segmentIndex;
if (segmentIndex) {
segmentIndex.forEachTopLevelReference((segment) => {
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_;
for (const partial of segment.partialReferences) {
partial.syncTime += this.syncTimeOffset_;
}
}
}
}
/**
* Parses the manifest.
*
@@ -433,6 +479,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.
@@ -1706,8 +1756,8 @@ shaka.hls.HlsParser = class {
* @param {number} startTime
* @param {!Map.<string, string>} variables
* @param {string} absoluteMediaPlaylistUri
* @return {!shaka.media.SegmentReference}
* @param {string} type
* @return {!shaka.media.SegmentReference}
* @private
*/
createSegmentReference_(
@@ -1730,6 +1780,23 @@ 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);
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,
);
}
@@ -1966,6 +2034,81 @@ shaka.hls.HlsParser = class {
references.push(reference);
}
// If some segments have sync times, but not all, extrapolate the sync
// times of the ones with none.
const someSyncTime = references.some((ref) => ref.syncTime != null);
if (someSyncTime) {
for (let i = 0; i < references.length; i++) {
const reference = references[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 forwardI = i;
/**
* Look forwards one reference at a time, summing all durations as we
* go, until we find a reference with a syncTime to use as a basis.
* This DOES count the original reference, but DOESN'T count the first
* reference with a syncTime (as we approach it from behind).
* @return {?number}
*/
const lookForward = () => {
const other = references[forwardI];
if (other) {
if (other.syncTime != null) {
return other.syncTime + forwardAdd;
}
forwardAdd -= other.endTime - other.startTime;
forwardI += 1;
}
return null;
};
let backwardAdd = 0;
let backwardI = i;
/**
* Look backwards one reference at a time, summing all durations as we
* go, until we find a reference with a syncTime to use as a basis.
* This DOESN'T count the original reference, but DOES count the first
* reference with a syncTime (as we approach it from ahead).
* @return {?number}
*/
const lookBackward = () => {
const other = references[backwardI];
if (other) {
if (other != reference) {
backwardAdd += other.endTime - other.startTime;
}
if (other.syncTime != null) {
return other.syncTime + backwardAdd;
}
backwardI -= 1;
}
return null;
};
while (reference.syncTime == null) {
reference.syncTime = lookBackward();
if (reference.syncTime == null) {
reference.syncTime = lookForward();
}
}
}
}
// Split the sync times properly among partial segments.
if (someSyncTime) {
for (const reference of references) {
let syncTime = reference.syncTime;
for (const partial of reference.partialReferences) {
partial.syncTime = syncTime;
syncTime += partial.endTime - partial.startTime;
}
}
}
return references;
}
+2 -3
View File
@@ -179,9 +179,8 @@ shaka.hls.ManifestTextParser = class {
segmentTags.push(currentMapTag);
}
// The URI appears after all of the tags describing the segment.
const segment =
new shaka.hls.Segment(absoluteSegmentUri, segmentTags,
partialSegmentTags);
const segment = new shaka.hls.Segment(absoluteSegmentUri, segmentTags,
partialSegmentTags);
segments.push(segment);
segmentTags = [];
partialSegmentTags = [];
+4 -5
View File
@@ -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_(
+13 -1
View File
@@ -101,6 +101,17 @@ shaka.media.SegmentIndex = class {
}
/**
* Iterates over all top-level segment references in this segment index.
* @param {function(!shaka.media.SegmentReference)} fn
*/
forEachTopLevelReference(fn) {
for (const reference of this.references) {
fn(reference);
}
}
/**
* Finds the position of the segment for the given time, in seconds, relative
* to the start of the presentation. Returns the position of the segment
@@ -348,7 +359,8 @@ shaka.media.SegmentIndex = class {
lastReference.appendWindowEnd,
lastReference.partialReferences,
lastReference.tilesLayout,
lastReference.tileDuration);
lastReference.tileDuration,
lastReference.syncTime);
}
+8 -1
View File
@@ -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;
}
/**
+3 -2
View File
@@ -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,
+1
View File
@@ -121,6 +121,7 @@ shaka.util.PlayerConfiguration = class {
ignoreImageStreamFailures: false,
defaultAudioCodec: 'mp4a.40.2',
defaultVideoCodec: 'avc1.42E01E',
ignoreManifestProgramDateTime: false,
},
};
+1 -1
View File
@@ -191,7 +191,7 @@ shaka.util.XmlUtils = class {
}
const result = Date.parse(dateString);
return (!isNaN(result) ? Math.floor(result / 1000.0) : null);
return isNaN(result) ? null : (result / 1000.0);
}
+91 -51
View File
@@ -194,8 +194,10 @@ 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, /* syncTime= */ null);
const ref2 = makeReference(
'test:/main2.mp4', 2, 4, /* syncTime= */ null);
await testUpdate(
master, media, [ref1], mediaWithAdditionalSegment, [ref1, ref2]);
@@ -209,8 +211,10 @@ 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, /* syncTime= */ null);
const ref2 = makeReference(
'test:/main2.mp4', 2, 4, /* syncTime= */ null);
await testUpdate(
masterWithTwoVariants, media, [ref1], mediaWithAdditionalSegment,
@@ -230,8 +234,10 @@ 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, /* syncTime= */ null);
const ref2 = makeReference(
'test:/main2.mp4', 2, 4, /* syncTime= */ null);
await testUpdate(
masterWithAudio, media, [ref1], mediaWithAdditionalSegment,
@@ -251,9 +257,12 @@ 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, /* syncTime= */ null);
const ref2 = makeReference(
'test:/main2.mp4', 2, 4, /* syncTime= */ null);
const ref3 = makeReference(
'test:/main3.mp4', 4, 6, /* syncTime= */ null);
fakeNetEngine
.setResponseText('test:/master', master)
@@ -523,15 +532,15 @@ describe('HlsParser live', () => {
.setResponseValue('test:/main.mp4', segmentData)
.setResponseValue('test:/main2.mp4', segmentData);
const ref1 = ManifestParser.makeReference(
'test:/main.mp4', 0, 2,
const ref1 = makeReference(
'test:/main.mp4', 0, 2, /* syncTime= */ null,
/* 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', 2, 4,
const ref2 = makeReference(
'test:/main2.mp4', 2, 4, /* syncTime= */ null,
/* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null,
/* timestampOffset= */ 0);
@@ -570,32 +579,32 @@ describe('HlsParser live', () => {
.setResponseValue('test:/partial.mp4', segmentData)
.setResponseValue('test:/partial2.mp4', segmentData);
const partialRef = ManifestParser.makeReference(
'test:/partial.mp4', 0, 2,
const partialRef = makeReference(
'test:/partial.mp4', 0, 2, /* syncTime= */ null,
/* baseUri= */ '', /* startByte= */ 0, /* endByte= */ 199);
const partialRef2 = ManifestParser.makeReference(
'test:/partial2.mp4', 2, 4,
const partialRef2 = makeReference(
'test:/partial2.mp4', 2, 4, /* syncTime= */ null,
/* baseUri= */ '', /* startByte= */ 200, /* endByte= */ 429);
const partialRef3 = ManifestParser.makeReference(
'test:/partial.mp4', 4, 6,
const partialRef3 = makeReference(
'test:/partial.mp4', 4, 6, /* syncTime= */ null,
/* 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', 6, 6,
const preloadRef = makeReference(
'test:/partial.mp4', 6, 6, /* syncTime= */ null,
/* baseUri= */ '', /* startByte= */ 210, /* endByte= */ null);
const ref = ManifestParser.makeReference(
'test:/main.mp4', 0, 4,
const ref = makeReference(
'test:/main.mp4', 0, 4, /* syncTime= */ null,
/* 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(
'', 4, 6,
const ref2 = makeReference(
'', 4, 6, /* syncTime= */ null,
/* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null,
/* timestampOffset= */ 0, [partialRef3, preloadRef]);
@@ -607,16 +616,20 @@ 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, /* syncTime= */ null);
const ref2 = makeReference(
'test:/main2.mp4', 2, 4, /* syncTime= */ null);
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, /* syncTime= */ null);
const ref2 = makeReference(
'test:/main2.mp4', 2, 4, /* syncTime= */ null);
await testUpdate(
master, mediaWithAdditionalSegment, [ref1, ref2],
@@ -624,12 +637,13 @@ 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, /* syncTime= */ null);
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, /* syncTime= */ null);
const newRef2 = makeReference(
'test:/redirected/main2.mp4', 2, 4, /* syncTime= */ null);
let playlistFetchCount = 0;
@@ -656,8 +670,8 @@ describe('HlsParser live', () => {
.setResponseValue('test:/init.mp4', initSegmentData)
.setResponseValue('test:/main.mp4', segmentData);
const expectedRef = ManifestParser.makeReference(
'test:/main.mp4', 0, 2);
const expectedRef = makeReference(
'test:/main.mp4', 0, 2, /* syncTime= */ null);
// In live content, we do not set timestampOffset.
expectedRef.timestampOffset = 0;
@@ -674,9 +688,11 @@ describe('HlsParser live', () => {
.setResponseValue('test:/init.mp4', initSegmentData)
.setResponseValue('test:/main.mp4', segmentData);
const ref1 = ManifestParser.makeReference('test:/main.mp4', 0, 2);
const ref1 = makeReference(
'test:/main.mp4', 0, 2, /* syncTime= */ null);
const ref2 = ManifestParser.makeReference('test:/main2.mp4', 2, 4);
const ref2 = makeReference(
'test:/main2.mp4', 2, 4, /* syncTime= */ null);
const manifest = await parser.start('test:/master', playerInterface);
const video = manifest.variants[0].video;
@@ -712,9 +728,11 @@ describe('HlsParser live', () => {
.setResponseValue('test:/main.mp4', segmentData)
.setResponseValue('test:/main2.mp4', segmentData);
const ref1 = ManifestParser.makeReference('test:/main.mp4', 0, 2);
const ref1 = makeReference(
'test:/main.mp4', 0, 2, /* syncTime= */ null);
const ref2 = ManifestParser.makeReference('test:/main2.mp4', 2, 4);
const ref2 = makeReference(
'test:/main2.mp4', 2, 4, /* syncTime= */ null);
const manifest =
await parser.start('test:/master', playerInterface);
@@ -811,9 +829,12 @@ describe('HlsParser live', () => {
].join('');
playerInterface.isLowLatencyMode = () => true;
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, /* syncTime= */ null);
const ref2 = makeReference(
'test:/main2.mp4', 2, 4, /* syncTime= */ null);
const ref3 = makeReference(
'test:/main3.mp4', 4, 6, /* syncTime= */ null);
// 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.
@@ -853,27 +874,27 @@ describe('HlsParser live', () => {
playerInterface.isLowLatencyMode = () => true;
const ref1 = ManifestParser.makeReference(
'test:/main.mp4', 0, 2,
const ref1 = makeReference(
'test:/main.mp4', 0, 2, /* syncTime= */ null,
/* 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', 2, 4,
const ref2 = makeReference(
'test:/main2.mp4', 2, 4, /* syncTime= */ null,
/* 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', 4, 6,
const ref3 = makeReference(
'test:/main3.mp4', 4, 6, /* syncTime= */ null,
/* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null,
/* timestampOffset= */ 0);
const ref4 = ManifestParser.makeReference(
'test:/main4.mp4', 6, 8,
const ref4 = makeReference(
'test:/main4.mp4', 6, 8, /* syncTime= */ null,
/* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null,
/* timestampOffset= */ 0);
@@ -886,4 +907,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 {?number} syncTime
* @param {string=} baseUri
* @param {number=} startByte
* @param {?number=} endByte
* @param {number=} timestampOffset
* @param {!Array.<!shaka.media.SegmentReference>=} partialReferences
* @param {?string=} tilesLayout
* @return {!shaka.media.SegmentReference}
*/
function makeReference(uri, start, end, syncTime, baseUri, startByte, endByte,
timestampOffset, partialReferences, tilesLayout) {
return ManifestParser.makeReference(uri, start, end, baseUri, startByte,
endByte, timestampOffset, partialReferences, tilesLayout, syncTime);
}
}); // describe('HlsParser live')
+167
View File
@@ -1869,6 +1869,173 @@ describe('HlsParser', () => {
expect(actual).toEqual(manifest);
});
describe('produces syncTime', () => {
/**
* @param {number} startTime
* @param {number} endTime
* @param {number} syncTime
* @return {!shaka.media.SegmentReference}
*/
function makeReference(startTime, endTime, syncTime) {
const initUris = () => ['test:/init.mp4'];
const init = new shaka.media.InitSegmentReference(initUris, 0, 615);
const uris = () => ['test:/main.mp4'];
return new shaka.media.SegmentReference(
startTime, endTime, uris, 0, null, init, 0, 0, Infinity,
[], undefined, undefined, syncTime);
}
/**
* @param {string} media
* @param {!Array.<number>} startTimes
* @param {(function(!shaka.media.SegmentReference))=} modifyFn
*/
async function test(media, startTimes, modifyFn) {
const master = [
'#EXTM3U\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,vtt",',
'RESOLUTION=960x540,FRAME-RATE=60\n',
'video\n',
].join('');
const segments = [];
for (let i = 0; i < startTimes.length - 1; i++) {
const startTime = startTimes[i];
const endTime = startTimes[i + 1];
const reference = makeReference(startTime, endTime, startTime);
if (modifyFn) {
modifyFn(reference);
}
segments.push(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(''), [0, 5, 10, 15, 20, 25]);
});
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:2,\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:2,\n',
'main.mp4\n',
'#EXTINF:4,\n',
'main.mp4',
].join(''), [0, 2, 7, 12, 17, 19, 23]);
});
it('except when ignoreManifestProgramDateTime is set', async () => {
const config = shaka.util.PlayerConfiguration.createDefault().manifest;
config.hls.ignoreManifestProgramDateTime = true;
parser.configure(config);
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(''), [0, 5, 10, 15, 20, 25], (reference) => {
reference.syncTime = null;
});
});
it('when there are partial segments', async () => {
playerInterface.isLowLatencyMode = () => true;
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',
'#EXT-X-PART:DURATION=2.5,URI="main.mp4"\n',
'#EXT-X-PART:DURATION=2.5,URI="main.mp4"\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(''), [0, 5, 10, 15, 20, 25], (reference) => {
if (reference.startTime == 10) {
reference.partialReferences = [
makeReference(10, 12.5, 10),
makeReference(12.5, 15, 12.5),
];
}
});
});
});
it('drops failed text streams when configured to', async () => {
const master = [
'#EXTM3U\n',
+4 -1
View File
@@ -57,11 +57,12 @@ shaka.test.ManifestParser = class {
* @param {number=} timestampOffset
* @param {!Array.<!shaka.media.SegmentReference>=} 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,8 @@ shaka.test.ManifestParser = class {
appendWindowEnd,
partialReferences,
tilesLayout,
/* tileDuration= */ undefined,
syncTime,
);
}
};