mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-26 17:46:26 +03:00
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:
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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_(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -121,6 +121,7 @@ shaka.util.PlayerConfiguration = class {
|
||||
ignoreImageStreamFailures: false,
|
||||
defaultAudioCodec: 'mp4a.40.2',
|
||||
defaultVideoCodec: 'avc1.42E01E',
|
||||
ignoreManifestProgramDateTime: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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')
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user