Skip to content

Commit 450eb2d

Browse files
feat: add dts-based timestamp offset calculation with feature toggle (#1251)
1 parent 42fe383 commit 450eb2d

8 files changed

+229
-2
lines changed

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

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>

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
});

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_),

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_();

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) => {

test/segment-loader.test.js

+187
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
oneSecond as oneSecondSegment,
2929
audio as audioSegment,
3030
video as videoSegment,
31+
videoDiffPtsDts as videoDiffPtsDtsSegment,
3132
videoOneSecond as videoOneSecondSegment,
3233
videoOneSecond1 as videoOneSecond1Segment,
3334
videoOneSecond2 as videoOneSecond2Segment,
@@ -1145,6 +1146,192 @@ QUnit.module('SegmentLoader', function(hooks) {
11451146
});
11461147
});
11471148

1149+
QUnit.test('should use video PTS value for timestamp offset calculation when useDtsForTimestampOffset set as false', function(assert) {
1150+
loader = new SegmentLoader(LoaderCommonSettings.call(this, {
1151+
loaderType: 'main',
1152+
segmentMetadataTrack: this.segmentMetadataTrack,
1153+
useDtsForTimestampOffset: false
1154+
}), {});
1155+
1156+
const playlist = playlistWithDuration(20, { uri: 'playlist.m3u8' });
1157+
1158+
return this.setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => {
1159+
return new Promise((resolve, reject) => {
1160+
loader.one('appended', resolve);
1161+
loader.one('error', reject);
1162+
1163+
loader.playlist(playlist);
1164+
loader.load();
1165+
1166+
this.clock.tick(100);
1167+
1168+
standardXHRResponse(this.requests.shift(), videoDiffPtsDtsSegment());
1169+
});
1170+
}).then(() => {
1171+
assert.equal(
1172+
loader.sourceUpdater_.videoTimestampOffset(),
1173+
-playlist.segments[0].videoTimingInfo.transmuxedPresentationStart,
1174+
'set video timestampOffset'
1175+
);
1176+
1177+
assert.equal(
1178+
loader.sourceUpdater_.audioTimestampOffset(),
1179+
-playlist.segments[0].videoTimingInfo.transmuxedPresentationStart,
1180+
'set audio timestampOffset'
1181+
);
1182+
});
1183+
});
1184+
1185+
QUnit.test('should use video DTS value for timestamp offset calculation when useDtsForTimestampOffset set as true', function(assert) {
1186+
loader = new SegmentLoader(LoaderCommonSettings.call(this, {
1187+
loaderType: 'main',
1188+
segmentMetadataTrack: this.segmentMetadataTrack,
1189+
useDtsForTimestampOffset: true
1190+
}), {});
1191+
1192+
const playlist = playlistWithDuration(20, { uri: 'playlist.m3u8' });
1193+
1194+
return this.setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => {
1195+
return new Promise((resolve, reject) => {
1196+
loader.one('appended', resolve);
1197+
loader.one('error', reject);
1198+
1199+
loader.playlist(playlist);
1200+
loader.load();
1201+
1202+
this.clock.tick(100);
1203+
// segment
1204+
standardXHRResponse(this.requests.shift(), videoDiffPtsDtsSegment());
1205+
});
1206+
}).then(() => {
1207+
assert.equal(
1208+
loader.sourceUpdater_.videoTimestampOffset(),
1209+
-playlist.segments[0].videoTimingInfo.transmuxedDecodeStart,
1210+
'set video timestampOffset'
1211+
);
1212+
1213+
assert.equal(
1214+
loader.sourceUpdater_.audioTimestampOffset(),
1215+
-playlist.segments[0].videoTimingInfo.transmuxedDecodeStart,
1216+
'set audio timestampOffset'
1217+
);
1218+
});
1219+
});
1220+
1221+
QUnit.test('should use video DTS value as primary for muxed segments (eg: audio and video together) for timestamp offset calculation when useDtsForTimestampOffset set as true', function(assert) {
1222+
loader = new SegmentLoader(LoaderCommonSettings.call(this, {
1223+
loaderType: 'main',
1224+
segmentMetadataTrack: this.segmentMetadataTrack,
1225+
useDtsForTimestampOffset: true
1226+
}), {});
1227+
1228+
const playlist = playlistWithDuration(20, { uri: 'playlist.m3u8' });
1229+
1230+
return this.setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => {
1231+
return new Promise((resolve, reject) => {
1232+
loader.one('appended', resolve);
1233+
loader.one('error', reject);
1234+
1235+
loader.playlist(playlist);
1236+
loader.load();
1237+
1238+
this.clock.tick(100);
1239+
1240+
standardXHRResponse(this.requests.shift(), muxedSegment());
1241+
});
1242+
}).then(() => {
1243+
assert.equal(
1244+
loader.sourceUpdater_.videoTimestampOffset(),
1245+
-playlist.segments[0].videoTimingInfo.transmuxedDecodeStart,
1246+
'set video timestampOffset'
1247+
);
1248+
1249+
assert.equal(
1250+
loader.sourceUpdater_.audioTimestampOffset(),
1251+
-playlist.segments[0].videoTimingInfo.transmuxedDecodeStart,
1252+
'set audio timestampOffset'
1253+
);
1254+
});
1255+
});
1256+
1257+
QUnit.test('should use audio DTS value for timestamp offset calculation when useDtsForTimestampOffset set as true and only audio', function(assert) {
1258+
loader = new SegmentLoader(LoaderCommonSettings.call(this, {
1259+
loaderType: 'main',
1260+
segmentMetadataTrack: this.segmentMetadataTrack,
1261+
useDtsForTimestampOffset: true
1262+
}), {});
1263+
1264+
const playlist = playlistWithDuration(20, { uri: 'playlist.m3u8' });
1265+
1266+
return this.setupMediaSource(loader.mediaSource_, loader.sourceUpdater_, { isAudioOnly: true }).then(() => {
1267+
return new Promise((resolve, reject) => {
1268+
loader.one('appended', resolve);
1269+
loader.one('error', reject);
1270+
1271+
loader.playlist(playlist);
1272+
loader.load();
1273+
1274+
this.clock.tick(100);
1275+
// segment
1276+
standardXHRResponse(this.requests.shift(), audioSegment());
1277+
});
1278+
}).then(() => {
1279+
assert.equal(
1280+
loader.sourceUpdater_.audioTimestampOffset(),
1281+
-playlist.segments[0].audioTimingInfo.transmuxedDecodeStart,
1282+
'set audio timestampOffset'
1283+
);
1284+
});
1285+
});
1286+
1287+
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) {
1288+
loader = new SegmentLoader(LoaderCommonSettings.call(this, {
1289+
loaderType: 'main',
1290+
segmentMetadataTrack: this.segmentMetadataTrack,
1291+
useDtsForTimestampOffset: true
1292+
}), {});
1293+
1294+
const playlist = playlistWithDuration(10);
1295+
const ogPost = loader.transmuxer_.postMessage;
1296+
1297+
loader.transmuxer_.postMessage = (message) => {
1298+
if (message.action === 'probeMp4StartTime') {
1299+
const evt = newEvent('message');
1300+
1301+
evt.data = {action: 'probeMp4StartTime', startTime: 11, data: message.data};
1302+
1303+
loader.transmuxer_.dispatchEvent(evt);
1304+
return;
1305+
}
1306+
return ogPost.call(loader.transmuxer_, message);
1307+
};
1308+
1309+
return this.setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => {
1310+
return new Promise((resolve, reject) => {
1311+
loader.one('appended', resolve);
1312+
loader.one('error', reject);
1313+
1314+
playlist.segments.forEach((segment) => {
1315+
segment.map = {
1316+
resolvedUri: 'init.mp4',
1317+
byterange: { length: Infinity, offset: 0 }
1318+
};
1319+
});
1320+
loader.playlist(playlist);
1321+
loader.load();
1322+
1323+
this.clock.tick(100);
1324+
// init
1325+
standardXHRResponse(this.requests.shift(), mp4VideoInitSegment());
1326+
// segment
1327+
standardXHRResponse(this.requests.shift(), mp4VideoSegment());
1328+
});
1329+
}).then(() => {
1330+
assert.equal(loader.sourceUpdater_.videoTimestampOffset(), -11, 'set video timestampOffset');
1331+
assert.equal(loader.sourceUpdater_.audioTimestampOffset(), -11, 'set audio timestampOffset');
1332+
});
1333+
});
1334+
11481335
QUnit.test('updates timestamps when segments do not start at zero', function(assert) {
11491336
const playlist = playlistWithDuration(10);
11501337
const ogPost = loader.transmuxer_.postMessage;

test/segments/videoDiffPtsDts.ts

19.1 KB
Binary file not shown.

0 commit comments

Comments
 (0)