Skip to content

Commit

Permalink
fix: use a separate ProgramDateTime mapping to player time per timeli…
Browse files Browse the repository at this point in the history
…ne (#1063)

Previously, the ProgramDateTime would map to time 0 at the start of playback, and that mapping would be used for the rest of playback.

However, in the case of a discontinuity, ProgramDateTime can jump (as in, can be a larger difference in time than a single segment duration, e.g., if the encoder went down for a period of time). Because the ProgramDateTime to player time mapping never changed, this led to seeking errors.

This fix uses a mapping of ProgramDateTime to player time per timeline, allowing those "jumps" in time on discontinuities.
  • Loading branch information
gesinger authored Feb 10, 2021
1 parent 49249d5 commit 5e9b4f1
Show file tree
Hide file tree
Showing 11 changed files with 426 additions and 33 deletions.
19 changes: 19 additions & 0 deletions docs/creating-content.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,25 @@ Copy only the first two video frames, leave out audio.
$ ffmpeg -i index0.ts -vframes 2 -an -vcodec copy video.ts
```

### videoOneSecond.ts

Blank video for 1 second, MMS-Small resolution, start at 0 PTS/DTS, 2 frames per second

```
$ ffmpeg -f lavfi -i color=c=black:s=128x96:r=2:d=1 -muxdelay 0 -c:v libx264 videoOneSecond.ts
```

### videoOneSecond1.ts through videoOneSecond4.ts

Same as videoOneSecond.ts, but follows timing in sequence, with videoOneSecond.ts acting as the 0 index. Each segment starts at the second that its index indicates (e.g., videoOneSecond2.ts has a start time of 2 seconds).

```
$ ffmpeg -i videoOneSecond.ts -muxdelay 0 -output_ts_offset 1 -vcodec copy videoOneSecond1.ts
$ ffmpeg -i videoOneSecond.ts -muxdelay 0 -output_ts_offset 2 -vcodec copy videoOneSecond2.ts
$ ffmpeg -i videoOneSecond.ts -muxdelay 0 -output_ts_offset 3 -vcodec copy videoOneSecond3.ts
$ ffmpeg -i videoOneSecond.ts -muxdelay 0 -output_ts_offset 4 -vcodec copy videoOneSecond4.ts
```

### audio.ts

Copy only the first two audio frames, leave out video.
Expand Down
14 changes: 11 additions & 3 deletions src/segment-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -855,9 +855,6 @@ export default class SegmentLoader extends videojs.EventTarget {
return;
}

// not sure if this is the best place for this
this.syncController_.setDateTimeMapping(this.playlist_);

// if all the configuration is ready, initialize and begin loading
if (this.state === 'INIT' && this.couldBeginLoading_()) {
return this.init_();
Expand Down Expand Up @@ -916,6 +913,17 @@ export default class SegmentLoader extends videojs.EventTarget {
mediaSequence: newPlaylist.mediaSequence,
time: 0
};
// Setting the date time mapping means mapping the program date time (if available)
// to time 0 on the player's timeline. The playlist's syncInfo serves a similar
// purpose, mapping the initial mediaSequence to time zero. Since the syncInfo can
// be updated as the playlist is refreshed before the loader starts loading, the
// program date time mapping needs to be updated as well.
//
// This mapping is only done for the main loader because a program date time should
// map equivalently between playlists.
if (this.loaderType_ === 'main') {
this.syncController_.setDateTimeMappingForStart(newPlaylist);
}
}

let oldId = null;
Expand Down
45 changes: 32 additions & 13 deletions src/sync-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const syncPointStrategies = [
{
name: 'ProgramDateTime',
run: (syncController, playlist, duration, currentTimeline, currentTime) => {
if (!syncController.datetimeToDisplayTime) {
if (!Object.keys(syncController.timelineToDatetimeMappings).length) {
return null;
}

Expand All @@ -39,10 +39,16 @@ export const syncPointStrategies = [

for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
const datetimeMapping =
syncController.timelineToDatetimeMappings[segment.timeline];

if (!datetimeMapping) {
continue;
}

if (segment.dateTimeObject) {
const segmentTime = segment.dateTimeObject.getTime() / 1000;
const segmentStart = segmentTime + syncController.datetimeToDisplayTime;
const segmentStart = segmentTime + datetimeMapping;
const distance = Math.abs(currentTime - segmentStart);

// Once the distance begins to increase, or if distance is 0, we have passed
Expand Down Expand Up @@ -161,7 +167,7 @@ export default class SyncController extends videojs.EventTarget {
// ...for synching across variants
this.timelines = [];
this.discontinuities = [];
this.datetimeToDisplayTime = null;
this.timelineToDatetimeMappings = {};

this.logger_ = logger('SyncController');
}
Expand Down Expand Up @@ -351,19 +357,25 @@ export default class SyncController extends videojs.EventTarget {
}

/**
* Save the mapping from playlist's ProgramDateTime to display. This should
* only ever happen once at the start of playback.
* Save the mapping from playlist's ProgramDateTime to display. This should only happen
* before segments start to load.
*
* @param {Playlist} playlist - The currently active playlist
*/
setDateTimeMapping(playlist) {
if (!this.datetimeToDisplayTime &&
playlist.segments &&
setDateTimeMappingForStart(playlist) {
// It's possible for the playlist to be updated before playback starts, meaning time
// zero is not yet set. If, during these playlist refreshes, a discontinuity is
// crossed, then the old time zero mapping (for the prior timeline) would be retained
// unless the mappings are cleared.
this.timelineToDatetimeMappings = {};

if (playlist.segments &&
playlist.segments.length &&
playlist.segments[0].dateTimeObject) {
const playlistTimestamp = playlist.segments[0].dateTimeObject.getTime() / 1000;
const firstSegment = playlist.segments[0];
const playlistTimestamp = firstSegment.dateTimeObject.getTime() / 1000;

this.datetimeToDisplayTime = -playlistTimestamp;
this.timelineToDatetimeMappings[firstSegment.timeline] = -playlistTimestamp;
}
}

Expand All @@ -377,14 +389,15 @@ export default class SyncController extends videojs.EventTarget {
* The current active request information
* @param {boolean} options.shouldSaveTimelineMapping
* If there's a timeline change, determines if the timeline mapping should be
* saved in timelines.
* saved for timeline mapping and program date time mappings.
*/
saveSegmentTimingInfo({ segmentInfo, shouldSaveTimelineMapping }) {
const didCalculateSegmentTimeMapping = this.calculateSegmentTimeMapping_(
segmentInfo,
segmentInfo.timingInfo,
shouldSaveTimelineMapping
);
const segment = segmentInfo.segment;

if (didCalculateSegmentTimeMapping) {
this.saveDiscontinuitySyncInfo_(segmentInfo);
Expand All @@ -394,10 +407,16 @@ export default class SyncController extends videojs.EventTarget {
if (!segmentInfo.playlist.syncInfo) {
segmentInfo.playlist.syncInfo = {
mediaSequence: segmentInfo.playlist.mediaSequence + segmentInfo.mediaIndex,
time: segmentInfo.segment.start
time: segment.start
};
}
}

const dateTime = segment.dateTimeObject;

if (segment.discontinuity && shouldSaveTimelineMapping && dateTime) {
this.timelineToDatetimeMappings[segment.timeline] = -(dateTime.getTime() / 1000);
}
}

timestampOffsetForTimeline(timeline) {
Expand Down Expand Up @@ -433,7 +452,7 @@ export default class SyncController extends videojs.EventTarget {
const segment = segmentInfo.segment;
let mappingObj = this.timelines[segmentInfo.timeline];

if (segmentInfo.timestampOffset !== null) {
if (typeof segmentInfo.timestampOffset === 'number') {
mappingObj = {
time: segmentInfo.startOfSegment,
mapping: segmentInfo.startOfSegment - timingInfo.start
Expand Down
Loading

0 comments on commit 5e9b4f1

Please sign in to comment.