Skip to content

Commit

Permalink
fix: Sync each segment against EXT-X-PROGRAM-DATE-TIME (#4870)
Browse files Browse the repository at this point in the history
Closes #4589

Backported to v4.2.x:
 - patched in lowestSyncTime_ from v4.3.x in HlsParser
  • Loading branch information
joeyparrish committed Jan 13, 2023
1 parent 45b6ccb commit fadbeb6
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 20 deletions.
56 changes: 45 additions & 11 deletions lib/hls/hls_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,14 @@ shaka.hls.HlsParser = class {
*/
this.minSequenceNumber_ = null;

/**
* The lowest time value for any of the streams, as defined by the
* EXT-X-PROGRAM-DATE-TIME value. Measured in seconds since January 1, 1970.
*
* @private {number}
*/
this.lowestSyncTime_ = Infinity;

/**
* 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
Expand Down Expand Up @@ -446,7 +454,11 @@ shaka.hls.HlsParser = class {
// Now adjust timestamps back to begin at 0.
const segmentN = segmentIndex.earliestReference();
if (segmentN) {
this.offsetStream_(streamInfo, -segmentN.startTime);
const streamOffset = -segmentN.startTime;
// Modify all SegmentReferences equally.
streamInfo.stream.segmentIndex.offset(streamOffset);
// Update other parts of streamInfo the same way.
this.offsetStreamInfo_(streamInfo, streamOffset);
}
}
}
Expand All @@ -465,6 +477,10 @@ shaka.hls.HlsParser = class {
return;
}

if (this.lowestSyncTime_ != Infinity) {
return;
}

let lowestSyncTime = Infinity;

for (const streamInfo of this.uriToStreamInfosMap_.values()) {
Expand All @@ -481,28 +497,36 @@ shaka.hls.HlsParser = class {
// Nothing to sync.
return;
}
this.lowestSyncTime_ = lowestSyncTime;

shaka.log.debug('Syncing HLS streams against base time:', lowestSyncTime);

for (const streamInfo of this.uriToStreamInfosMap_.values()) {
const segmentIndex = streamInfo.stream.segmentIndex;
if (segmentIndex != null) {
// A segment's startTime should be based on its syncTime vs the lowest
// syncTime across all streams. The earliest segment sync time from
// any stream will become presentation time 0. If two streams start
// e.g. 6 seconds apart in syncTime, then their first segments will
// also start 6 seconds apart in presentation time.

const segment0 = segmentIndex.earliestReference();
if (segment0.syncTime == null) {
shaka.log.alwaysError('Missing EXT-X-PROGRAM-DATE-TIME for stream',
streamInfo.verbatimMediaPlaylistUri,
'Expect AV sync issues!');
} else {
// The first segment's target startTime should be based entirely on
// its syncTime. The rest of the stream will be based on that
// starting point. The earliest segment sync time from any stream
// will become presentation time 0. If two streams start e.g. 6
// seconds apart in syncTime, then their first segments will also
// start 6 seconds apart in presentation time.
// Stream metadata are offset by a fixed amount based on the
// first segment.
const segment0TargetTime = segment0.syncTime - lowestSyncTime;
const streamOffset = segment0TargetTime - segment0.startTime;
this.offsetStreamInfo_(streamInfo, streamOffset);

this.offsetStream_(streamInfo, streamOffset);
// This is computed across all segments separately to manage
// accumulated drift in durations.
for (const segment of segmentIndex) {
segment.syncAgainst(lowestSyncTime);
}
}
}
}
Expand All @@ -515,13 +539,13 @@ shaka.hls.HlsParser = class {
* @param {number} offset
* @private
*/
offsetStream_(streamInfo, offset) {
streamInfo.stream.segmentIndex.offset(offset);

offsetStreamInfo_(streamInfo, offset) {
// Adjust our accounting of the maximum timestamp.
streamInfo.maxTimestamp += offset;
goog.asserts.assert(streamInfo.maxTimestamp >= 0,
'Negative maxTimestamp after adjustment!');

// Update our map from sequence number to start time.
for (const [key, value] of streamInfo.mediaSequenceToStartTime) {
streamInfo.mediaSequenceToStartTime.set(key, value + offset);
}
Expand Down Expand Up @@ -2405,6 +2429,16 @@ shaka.hls.HlsParser = class {
}
}

// lowestSyncTime is a value from a previous playlist update. Use it to
// set reference start times. If this is the first playlist parse, we will
// skip this step, and wait until we have sync time across stream types.
const lowestSyncTime = this.lowestSyncTime_;
if (someSyncTime && lowestSyncTime != Infinity) {
for (const reference of references) {
reference.syncAgainst(lowestSyncTime);
}
}

return references;
}

Expand Down
10 changes: 1 addition & 9 deletions lib/media/segment_index.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,15 +183,7 @@ shaka.media.SegmentIndex = class {
offset(offset) {
if (!this.immutable_) {
for (const ref of this.references) {
ref.startTime += offset;
ref.endTime += offset;
ref.trueEndTime += offset;

for (const partial of ref.partialReferences) {
partial.startTime += offset;
partial.endTime += offset;
partial.trueEndTime += offset;
}
ref.offset(offset);
}
}
}
Expand Down
39 changes: 39 additions & 0 deletions lib/media/segment_reference.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ goog.provide('shaka.media.InitSegmentReference');
goog.provide('shaka.media.SegmentReference');

goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.util.ArrayUtils');


Expand Down Expand Up @@ -356,6 +357,44 @@ shaka.media.SegmentReference = class {
markAsUnavailable() {
this.status = shaka.media.SegmentReference.Status.UNAVAILABLE;
}

/**
* Offset the segment reference by a fixed amount.
*
* @param {number} offset The amount to add to the segment's start and end
* times.
* @export
*/
offset(offset) {
this.startTime += offset;
this.endTime += offset;
this.trueEndTime += offset;

for (const partial of this.partialReferences) {
partial.startTime += offset;
partial.endTime += offset;
partial.trueEndTime += offset;
}
}

/**
* Sync this segment against a particular sync time that will serve as "0" in
* the presentation timeline.
*
* @param {number} lowestSyncTime
* @export
*/
syncAgainst(lowestSyncTime) {
if (this.syncTime == null) {
shaka.log.alwaysError('Sync attempted without sync time!');
return;
}
const desiredStart = this.syncTime - lowestSyncTime;
const offset = desiredStart - this.startTime;
if (Math.abs(offset) >= 0.001) {
this.offset(offset);
}
}
};


Expand Down

0 comments on commit fadbeb6

Please sign in to comment.