Skip to content

Commit

Permalink
fix(HLS): Fix AV sync over ad boundaries (shaka-project#4824)
Browse files Browse the repository at this point in the history
If server-side ad segments aren't aligned, AV could get out of sync by
accumulating errors in the timestampOffset of the SourceBuffers.

This improves the issue by tracking discontinuity boundaries and
resetting timestampOffset to theoretical segment start times when a
boundary is crossed.

Issue shaka-project#4589
  • Loading branch information
joeyparrish committed Dec 13, 2022
1 parent 1bf5ef1 commit 35033bb
Show file tree
Hide file tree
Showing 5 changed files with 48 additions and 9 deletions.
16 changes: 12 additions & 4 deletions lib/hls/hls_parser.js
Expand Up @@ -2603,12 +2603,12 @@ shaka.hls.HlsParser = class {
/** @type {shaka.extern.HlsAes128Key|undefined} */
let hlsAes128Key = undefined;

// We may need to look at the media itself to determine a segment start
// time.
let discontinuitySequence = shaka.hls.Utils.getFirstTagWithNameAsNumber(
playlist.tags, 'EXT-X-DISCONTINUITY-SEQUENCE', 0);
const mediaSequenceNumber = shaka.hls.Utils.getFirstTagWithNameAsNumber(
playlist.tags, 'EXT-X-MEDIA-SEQUENCE', 0);
const skipTag = shaka.hls.Utils.getFirstTagWithName(playlist.tags,
'EXT-X-SKIP');
const skipTag = shaka.hls.Utils.getFirstTagWithName(
playlist.tags, 'EXT-X-SKIP');
const skippedSegments =
skipTag ? Number(skipTag.getAttributeValue('SKIPPED-SEGMENTS')) : 0;
let position = mediaSequenceNumber + skippedSegments;
Expand All @@ -2631,6 +2631,12 @@ shaka.hls.HlsParser = class {
(i == 0) ? firstStartTime : previousReference.endTime;
position = mediaSequenceNumber + skippedSegments + i;

const discontinuityTag = shaka.hls.Utils.getFirstTagWithName(
playlist.tags, 'EXT-X-DISCONTINUITY');
if (discontinuityTag) {
discontinuitySequence++;
}

// Apply new AES-128 tags as you see them, keeping a running total.
for (const drmTag of item.tags) {
if (drmTag.name == 'EXT-X-KEY' &&
Expand Down Expand Up @@ -2668,6 +2674,8 @@ shaka.hls.HlsParser = class {
previousReference = reference;

if (reference) {
reference.discontinuitySequence = discontinuitySequence;

if (this.config_.hls.ignoreManifestProgramDateTime &&
this.minSequenceNumber_ != null &&
position < this.minSequenceNumber_) {
Expand Down
16 changes: 16 additions & 0 deletions lib/media/media_source_engine.js
Expand Up @@ -896,6 +896,22 @@ shaka.media.MediaSourceEngine = class {
]);
}

/**
* Adjust timestamp offset to maintain AV sync across discontinuities.
* Only used in sequence mode.
*
* @param {shaka.util.ManifestParserUtils.ContentType} contentType
* @param {number} timestampOffset
* @return {!Promise}
*/
async resync(contentType, timestampOffset) {
goog.asserts.assert(this.sequenceMode_,
'resyncAudio only used with sequence mode!');
await this.enqueueOperation_(
contentType,
() => this.setTimestampOffset_(contentType, timestampOffset));
}

/**
* @param {string=} reason Valid reasons are 'network' and 'decode'.
* @return {!Promise}
Expand Down
3 changes: 3 additions & 0 deletions lib/media/segment_reference.js
Expand Up @@ -246,6 +246,9 @@ shaka.media.SegmentReference = class {

/** @type {?shaka.media.SegmentReference.ThumbnailSprite} */
this.thumbnailSprite = null;

/** @type {number} */
this.discontinuitySequence = 0;
}

/**
Expand Down
15 changes: 13 additions & 2 deletions lib/media/streaming_engine.js
Expand Up @@ -1659,6 +1659,19 @@ shaka.media.StreamingEngine = class {
}
}

if (this.manifest_.sequenceMode) {
// Across discontinuity bounds, we should resync timestamps for
// sequence mode playbacks. The next segment appended should
// land at its theoretical timestamp from the segment index.
const lastDiscontinuitySequence =
mediaState.lastSegmentReference ?
mediaState.lastSegmentReference.discontinuitySequence : null;
if (reference.discontinuitySequence != lastDiscontinuitySequence) {
operations.push(this.playerInterface_.mediaSourceEngine.resync(
mediaState.type, reference.startTime));
}
}

await Promise.all(operations);
}

Expand Down Expand Up @@ -1713,8 +1726,6 @@ shaka.media.StreamingEngine = class {

await this.evict_(mediaState, presentationTime);
this.destroyer_.ensureNotDestroyed();
shaka.log.v1(logPrefix, 'appending media segment at',
(reference.syncTime == null ? 'unknown' : reference.syncTime));

// 'seeked' or 'adaptation' triggered logic applies only to this
// appendBuffer() call.
Expand Down
7 changes: 4 additions & 3 deletions test/test/util/manifest_parser_util.js
Expand Up @@ -73,7 +73,7 @@ shaka.test.ManifestParser = class {
const appendWindowStart = /** @type {?} */(jasmine.any(Number));
const appendWindowEnd = /** @type {?} */(jasmine.any(Number));

return new shaka.media.SegmentReference(
const ref = new shaka.media.SegmentReference(
start, end, getUris, startByte, endByte,
initSegmentReference,
timestampOffset,
Expand All @@ -82,7 +82,8 @@ shaka.test.ManifestParser = class {
partialReferences,
tilesLayout,
/* tileDuration= */ undefined,
syncTime,
);
syncTime);
ref.discontinuitySequence = /** @type {?} */(jasmine.any(Number));
return ref;
}
};

0 comments on commit 35033bb

Please sign in to comment.