diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 6e2eaead08..67254cdaed 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -846,6 +846,22 @@ shaka.hls.HlsParser = class { this.presentationTimeline_.setSegmentAvailabilityDuration( segmentAvailabilityDuration); } + + if (!this.presentationTimeline_.isStartTimeLocked()) { + for (const streamInfo of this.uriToStreamInfosMap_.values()) { + if (!streamInfo.stream.segmentIndex) { + continue; // Not active. + } + if (streamInfo.type != 'audio' && streamInfo.type != 'video') { + continue; + } + const firstReference = streamInfo.stream.segmentIndex.get(0); + if (firstReference && firstReference.syncTime) { + const syncTime = firstReference.syncTime; + this.presentationTimeline_.setInitialProgramDateTime(syncTime); + } + } + } } else { // Use the minimum duration as the presentation duration. this.presentationTimeline_.setDuration(this.getMinDuration_()); diff --git a/lib/media/presentation_timeline.js b/lib/media/presentation_timeline.js index 64021ff244..4200e097fe 100644 --- a/lib/media/presentation_timeline.js +++ b/lib/media/presentation_timeline.js @@ -96,6 +96,9 @@ shaka.media.PresentationTimeline = class { /** @private {boolean} */ this.startTimeLocked_ = false; + + /** @private {?number} */ + this.initialProgramDateTime_ = null; } @@ -288,6 +291,37 @@ shaka.media.PresentationTimeline = class { } + /** + * Returns if the presentation timeline's start time is locked. + * + * @return {boolean} + * @export + */ + isStartTimeLocked() { + return this.startTimeLocked_; + } + + + /** + * Sets the initial program date time. + * + * @param {number} initialProgramDateTime + * @export + */ + setInitialProgramDateTime(initialProgramDateTime) { + this.initialProgramDateTime_ = initialProgramDateTime; + } + + + /** + * @return {?number} The initial program date time in seconds. + * @export + */ + getInitialProgramDateTime() { + return this.initialProgramDateTime_; + } + + /** * Gives PresentationTimeline a Stream's minimum segment start time. * diff --git a/lib/player.js b/lib/player.js index 772be7e165..ee4e1b20fa 100644 --- a/lib/player.js +++ b/lib/player.js @@ -4527,7 +4527,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget { if (this.manifest_) { const timeline = this.manifest_.presentationTimeline; - const startTime = timeline.getPresentationStartTime(); + const startTime = timeline.getInitialProgramDateTime() || + timeline.getPresentationStartTime(); return new Date(/* ms= */ (startTime + presentationTime) * 1000); } else if (this.video_ && this.video_.getStartDate) { // Apple's native HLS gives us getStartDate(), which is only available if diff --git a/test/player_unit.js b/test/player_unit.js index bd0825e202..d4eccbc3b2 100644 --- a/test/player_unit.js +++ b/test/player_unit.js @@ -3519,11 +3519,14 @@ describe('Player', () => { }); describe('getPlayheadTimeAsDate()', () => { + /** @type {?shaka.media.PresentationTimeline} */ + let timeline; beforeEach(async () => { - const timeline = new shaka.media.PresentationTimeline(300, 0); + timeline = new shaka.media.PresentationTimeline(300, 0); timeline.setStatic(false); manifest = shaka.test.ManifestGenerator.generate((manifest) => { + goog.asserts.assert(timeline, 'timeline must be non-null'); manifest.presentationTimeline = timeline; manifest.addVariant(0, (variant) => { variant.addVideo(1); @@ -3541,6 +3544,15 @@ describe('Player', () => { // (300 (presentation start time) + 20 (playhead time)) * 1000 (ms/sec) expect(liveTimeUtc).toEqual(new Date(320 * 1000)); }); + + it('uses program date time', () => { + timeline.setInitialProgramDateTime(100); + playhead.getTime.and.returnValue(20); + + const liveTimeUtc = player.getPlayheadTimeAsDate(); + // (100 (program date time) + 20 (playhead time)) * 1000 (ms/sec) + expect(liveTimeUtc).toEqual(new Date(120 * 1000)); + }); }); it('rejects empty manifests', async () => {