Skip to content

Commit fdf277f

Browse files
feat: add dts-based timestamp offset calculation with feature toggle. more info: videojs#1247
1 parent 0964cb4 commit fdf277f

7 files changed

+187
-2
lines changed

Diff for: README.md

+6
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ Video.js Compatibility: 6.0, 7.0
5858
- [handlePartialData](#handlepartialdata)
5959
- [liveRangeSafeTimeDelta](#liverangesafetimedelta)
6060
- [useNetworkInformationApi](#usenetworkinformationapi)
61+
- [useDtsForTimestampOffset](#usedtsfortimestampoffset)
6162
- [captionServices](#captionservices)
6263
- [Format](#format)
6364
- [Example](#example)
@@ -479,6 +480,11 @@ This option defaults to `false`.
479480
* Default: `false`
480481
* Use [window.networkInformation.downlink](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/downlink) to estimate the network's bandwidth. Per mdn, _The value is never greater than 10 Mbps, as a non-standard anti-fingerprinting measure_. Given this, if bandwidth estimates from both the player and networkInfo are >= 10 Mbps, the player will use the larger of the two values as its bandwidth estimate.
481482

483+
##### useDtsForTimestampOffset
484+
* Type: `boolean`,
485+
* Default: `false`
486+
* Use [Decode Timestamp](https://www.w3.org/TR/media-source/#decode-timestamp) instead of [Presentation Timestamp](https://www.w3.org/TR/media-source/#presentation-timestamp) for [timestampOffset](https://www.w3.org/TR/media-source/#dom-sourcebuffer-timestampoffset) calculation. This option was introduced to align with DTS-based browsers. This option affects only transmuxed data (eg: transport stream). For more info please check the following [issue](https://github.com/videojs/http-streaming/issues/1247).
487+
482488
##### captionServices
483489
* Type: `object`
484490
* Default: undefined

Diff for: index.html

+5
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,11 @@
151151
<label class="form-check-label" for="network-info">Use networkInfo API for bandwidth estimations (reloads player)</label>
152152
</div>
153153

154+
<div class="form-check">
155+
<input id=dts-offset type="checkbox" class="form-check-input">
156+
<label class="form-check-label" for="dts-offset">Use DTS instead of PTS for Timestamp Offset calculation (reloads player)</label>
157+
</div>
158+
154159
<div class="form-check">
155160
<input id=llhls type="checkbox" class="form-check-input">
156161
<label class="form-check-label" for="llhls">[EXPERIMENTAL] Enables support for ll-hls (reloads player)</label>

Diff for: scripts/index.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,7 @@
448448
'exact-manifest-timings',
449449
'pixel-diff-selector',
450450
'network-info',
451+
'dts-offset',
451452
'override-native',
452453
'preload',
453454
'mirror-source'
@@ -501,6 +502,7 @@
501502
'liveui',
502503
'pixel-diff-selector',
503504
'network-info',
505+
'dts-offset',
504506
'exact-manifest-timings'
505507
].forEach(function(name) {
506508
stateEls[name].addEventListener('change', function(event) {
@@ -568,7 +570,8 @@
568570
experimentalLLHLS: getInputValue(stateEls.llhls),
569571
experimentalExactManifestTimings: getInputValue(stateEls['exact-manifest-timings']),
570572
experimentalLeastPixelDiffSelector: getInputValue(stateEls['pixel-diff-selector']),
571-
useNetworkInformationApi: getInputValue(stateEls['network-info'])
573+
useNetworkInformationApi: getInputValue(stateEls['network-info']),
574+
useDtsForTimestampOffset: getInputValue(stateEls['dts-offset']),
572575
}
573576
}
574577
});

Diff for: src/master-playlist-controller.js

+1
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ export class MasterPlaylistController extends videojs.EventTarget {
238238
const segmentLoaderSettings = {
239239
vhs: this.vhs_,
240240
parse708captions: options.parse708captions,
241+
useDtsForTimestampOffset: options.useDtsForTimestampOffset,
241242
captionServices,
242243
mediaSource: this.mediaSource,
243244
currentTime: this.tech_.currentTime.bind(this.tech_),

Diff for: src/segment-loader.js

+24-1
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,7 @@ export default class SegmentLoader extends videojs.EventTarget {
559559
this.timelineChangeController_ = settings.timelineChangeController;
560560
this.shouldSaveSegmentTimingInfo_ = true;
561561
this.parse708captions_ = settings.parse708captions;
562+
this.useDtsForTimestampOffset_ = settings.useDtsForTimestampOffset;
562563
this.captionServices_ = settings.captionServices;
563564
this.experimentalExactManifestTimings = settings.experimentalExactManifestTimings;
564565

@@ -2905,7 +2906,11 @@ export default class SegmentLoader extends videojs.EventTarget {
29052906
// the timing info here comes from video. In the event that the audio is longer than
29062907
// the video, this will trim the start of the audio.
29072908
// This also trims any offset from 0 at the beginning of the media
2908-
segmentInfo.timestampOffset -= segmentInfo.timingInfo.start;
2909+
segmentInfo.timestampOffset -= this.getSegmentStartTimeForTimestampOffsetCalculation_({
2910+
videoTimingInfo: segmentInfo.segment.videoTimingInfo,
2911+
audioTimingInfo: segmentInfo.segment.audioTimingInfo,
2912+
timingInfo: segmentInfo.timingInfo
2913+
});
29092914
// In the event that there are part segment downloads, each will try to update the
29102915
// timestamp offset. Retaining this bit of state prevents us from updating in the
29112916
// future (within the same segment), however, there may be a better way to handle it.
@@ -2926,6 +2931,24 @@ export default class SegmentLoader extends videojs.EventTarget {
29262931
}
29272932
}
29282933

2934+
getSegmentStartTimeForTimestampOffsetCalculation_ ({ videoTimingInfo, audioTimingInfo, timingInfo }) {
2935+
if (!this.useDtsForTimestampOffset_) {
2936+
return timingInfo.start;
2937+
}
2938+
2939+
if (videoTimingInfo && typeof videoTimingInfo.transmuxedDecodeStart === 'number') {
2940+
return videoTimingInfo.transmuxedDecodeStart;
2941+
}
2942+
2943+
// handle audio only
2944+
if (audioTimingInfo && typeof audioTimingInfo.transmuxedDecodeStart === 'number') {
2945+
return audioTimingInfo.transmuxedDecodeStart;
2946+
}
2947+
2948+
// handle content not transmuxed (e.g., MP4)
2949+
return timingInfo.start;
2950+
}
2951+
29292952
updateTimingInfoEnd_(segmentInfo) {
29302953
segmentInfo.timingInfo = segmentInfo.timingInfo || {};
29312954
const trackInfo = this.getMediaInfo_();

Diff for: src/videojs-http-streaming.js

+2
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,7 @@ class VhsHandler extends Component {
631631
this.source_.useBandwidthFromLocalStorage :
632632
this.options_.useBandwidthFromLocalStorage || false;
633633
this.options_.useNetworkInformationApi = this.options_.useNetworkInformationApi || false;
634+
this.options_.useDtsForTimestampOffset = this.options_.useDtsForTimestampOffset || false;
634635
this.options_.customTagParsers = this.options_.customTagParsers || [];
635636
this.options_.customTagMappers = this.options_.customTagMappers || [];
636637
this.options_.cacheEncryptionKeys = this.options_.cacheEncryptionKeys || false;
@@ -684,6 +685,7 @@ class VhsHandler extends Component {
684685
'liveRangeSafeTimeDelta',
685686
'experimentalLLHLS',
686687
'useNetworkInformationApi',
688+
'useDtsForTimestampOffset',
687689
'experimentalExactManifestTimings',
688690
'experimentalLeastPixelDiffSelector'
689691
].forEach((option) => {

Diff for: test/segment-loader.test.js

+145
Original file line numberDiff line numberDiff line change
@@ -1145,6 +1145,151 @@ QUnit.module('SegmentLoader', function(hooks) {
11451145
});
11461146
});
11471147

1148+
QUnit.test('should use video PTS value for timestamp offset calculation when useDtsForTimestampOffset set as false', function (assert) {
1149+
loader = new SegmentLoader(LoaderCommonSettings.call(this, {
1150+
loaderType: 'main',
1151+
segmentMetadataTrack: this.segmentMetadataTrack,
1152+
useDtsForTimestampOffset: false,
1153+
}), {});
1154+
1155+
const playlist = playlistWithDuration(20, { uri: 'playlist.m3u8' });
1156+
1157+
return this.setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => {
1158+
return new Promise((resolve, reject) => {
1159+
loader.one('appended', resolve);
1160+
loader.one('error', reject);
1161+
1162+
loader.playlist(playlist);
1163+
loader.load();
1164+
1165+
this.clock.tick(100);
1166+
1167+
standardXHRResponse(this.requests.shift(), videoSegment());
1168+
});
1169+
}).then(() => {
1170+
assert.equal(
1171+
loader.sourceUpdater_.videoTimestampOffset(),
1172+
-playlist.segments[0].videoTimingInfo.transmuxedPresentationStart,
1173+
'set video timestampOffset');
1174+
1175+
assert.equal(
1176+
loader.sourceUpdater_.audioTimestampOffset(),
1177+
-playlist.segments[0].videoTimingInfo.transmuxedPresentationStart,
1178+
'set audio timestampOffset');
1179+
});
1180+
});
1181+
1182+
QUnit.test('should use video DTS value for timestamp offset calculation when useDtsForTimestampOffset set as true', function (assert) {
1183+
loader = new SegmentLoader(LoaderCommonSettings.call(this, {
1184+
loaderType: 'main',
1185+
segmentMetadataTrack: this.segmentMetadataTrack,
1186+
useDtsForTimestampOffset: true,
1187+
}), {});
1188+
1189+
const playlist = playlistWithDuration(20, { uri: 'playlist.m3u8' });
1190+
1191+
return this.setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => {
1192+
return new Promise((resolve, reject) => {
1193+
loader.one('appended', resolve);
1194+
loader.one('error', reject);
1195+
1196+
loader.playlist(playlist);
1197+
loader.load();
1198+
1199+
this.clock.tick(100);
1200+
// segment
1201+
standardXHRResponse(this.requests.shift(), videoSegment());
1202+
});
1203+
}).then(() => {
1204+
assert.equal(
1205+
loader.sourceUpdater_.videoTimestampOffset(),
1206+
-playlist.segments[0].videoTimingInfo.transmuxedDecodeStart,
1207+
'set video timestampOffset');
1208+
1209+
assert.equal(
1210+
loader.sourceUpdater_.audioTimestampOffset(),
1211+
-playlist.segments[0].videoTimingInfo.transmuxedDecodeStart,
1212+
'set audio timestampOffset');
1213+
});
1214+
});
1215+
1216+
QUnit.test('should use audio DTS value for timestamp offset calculation when useDtsForTimestampOffset set as true and only audio', function (assert) {
1217+
loader = new SegmentLoader(LoaderCommonSettings.call(this, {
1218+
loaderType: 'main',
1219+
segmentMetadataTrack: this.segmentMetadataTrack,
1220+
useDtsForTimestampOffset: true,
1221+
}), {});
1222+
1223+
const playlist = playlistWithDuration(20, { uri: 'playlist.m3u8' });
1224+
1225+
return this.setupMediaSource(loader.mediaSource_, loader.sourceUpdater_, { isAudioOnly: true }).then(() => {
1226+
return new Promise((resolve, reject) => {
1227+
loader.one('appended', resolve);
1228+
loader.one('error', reject);
1229+
1230+
loader.playlist(playlist);
1231+
loader.load();
1232+
1233+
this.clock.tick(100);
1234+
// segment
1235+
standardXHRResponse(this.requests.shift(), audioSegment());
1236+
});
1237+
}).then(() => {
1238+
assert.equal(
1239+
loader.sourceUpdater_.audioTimestampOffset(),
1240+
-playlist.segments[0].audioTimingInfo.transmuxedDecodeStart,
1241+
'set audio timestampOffset');
1242+
});
1243+
});
1244+
1245+
QUnit.test('should fallback to segment\'s start time when there is no transmuxed content (eg: mp4) and useDtsForTimestampOffset is set as true', function(assert) {
1246+
loader = new SegmentLoader(LoaderCommonSettings.call(this, {
1247+
loaderType: 'main',
1248+
segmentMetadataTrack: this.segmentMetadataTrack,
1249+
useDtsForTimestampOffset: true,
1250+
}), {});
1251+
1252+
const playlist = playlistWithDuration(10);
1253+
const ogPost = loader.transmuxer_.postMessage;
1254+
1255+
loader.transmuxer_.postMessage = (message) => {
1256+
if (message.action === 'probeMp4StartTime') {
1257+
const evt = newEvent('message');
1258+
1259+
evt.data = {action: 'probeMp4StartTime', startTime: 11, data: message.data};
1260+
1261+
loader.transmuxer_.dispatchEvent(evt);
1262+
return;
1263+
}
1264+
return ogPost.call(loader.transmuxer_, message);
1265+
};
1266+
1267+
return this.setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => {
1268+
return new Promise((resolve, reject) => {
1269+
loader.one('appended', resolve);
1270+
loader.one('error', reject);
1271+
1272+
playlist.segments.forEach((segment) => {
1273+
segment.map = {
1274+
resolvedUri: 'init.mp4',
1275+
byterange: { length: Infinity, offset: 0 }
1276+
};
1277+
});
1278+
loader.playlist(playlist);
1279+
loader.load();
1280+
1281+
this.clock.tick(100);
1282+
// init
1283+
standardXHRResponse(this.requests.shift(), mp4VideoInitSegment());
1284+
// segment
1285+
standardXHRResponse(this.requests.shift(), mp4VideoSegment());
1286+
});
1287+
}).then(() => {
1288+
assert.equal(loader.sourceUpdater_.videoTimestampOffset(), -11, 'set video timestampOffset');
1289+
assert.equal(loader.sourceUpdater_.audioTimestampOffset(), -11, 'set audio timestampOffset');
1290+
});
1291+
});
1292+
11481293
QUnit.test('updates timestamps when segments do not start at zero', function(assert) {
11491294
const playlist = playlistWithDuration(10);
11501295
const ogPost = loader.transmuxer_.postMessage;

0 commit comments

Comments
 (0)