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, ); } };