Skip to content

Commit 2355ddc

Browse files
dzianis-dashkevichDzianis Dashkevich
andauthored
feat: Add feature flag to calculate timestampOffset for each segment to handle streams with corrupted pts or dts timestamps (#1426)
* Use audio/video start time info from Transmuxer instead of probeTs because Transmuxer aligns time between audio and video in the timestampRollover stream. * Add a feature flag to calculate timestampOffset for each segment, regardless of its timeline, so timestampOffset should always be valid. --------- Co-authored-by: Dzianis Dashkevich <ddashkevich@brightcove.com>
1 parent 04451d4 commit 2355ddc

11 files changed

+112
-13
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,12 @@ This option defaults to `false`.
463463
* Default: `false`
464464
* 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).
465465

466+
##### calculateTimestampOffsetForEachSegment
467+
* Type: `boolean`,
468+
* Default: `false`
469+
* Calculate timestampOffset for each segment, regardless of its timeline. Sometimes it is helpful when you have corrupted DTS/PTS timestamps during discontinuities.
470+
471+
466472
##### useForcedSubtitles
467473
* Type: `boolean`
468474
* Default: `false`

index.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,11 @@
144144
<label class="form-check-label" for="dts-offset">Use DTS instead of PTS for Timestamp Offset calculation (reloads player)</label>
145145
</div>
146146

147+
<div class="form-check">
148+
<input id=offset-each-segment type="checkbox" class="form-check-input">
149+
<label class="form-check-label" for="offset-each-segment">Calculate timestampOffset for each segment, regardless of its timeline (reloads player)</label>
150+
</div>
151+
147152
<div class="form-check">
148153
<input id=llhls type="checkbox" class="form-check-input">
149154
<label class="form-check-label" for="llhls">[EXPERIMENTAL] Enables support for ll-hls (reloads player)</label>

scripts/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@
470470
'pixel-diff-selector',
471471
'network-info',
472472
'dts-offset',
473+
'offset-each-segment',
473474
'override-native',
474475
'preload',
475476
'mirror-source',
@@ -525,6 +526,7 @@
525526
'pixel-diff-selector',
526527
'network-info',
527528
'dts-offset',
529+
'offset-each-segment',
528530
'exact-manifest-timings',
529531
'forced-subtitles'
530532
].forEach(function(name) {
@@ -609,6 +611,7 @@
609611
leastPixelDiffSelector: getInputValue(stateEls['pixel-diff-selector']),
610612
useNetworkInformationApi: getInputValue(stateEls['network-info']),
611613
useDtsForTimestampOffset: getInputValue(stateEls['dts-offset']),
614+
calculateTimestampOffsetForEachSegment: getInputValue(stateEls['offset-each-segment']),
612615
useForcedSubtitles: getInputValue(stateEls['forced-subtitles'])
613616
}
614617
}

src/media-segment-request.js

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -389,15 +389,6 @@ const transmuxAndNotify = ({
389389
isMuxed
390390
});
391391
trackInfoFn = null;
392-
393-
if (probeResult.hasAudio && !isMuxed) {
394-
audioStartFn(probeResult.audioStart);
395-
}
396-
if (probeResult.hasVideo) {
397-
videoStartFn(probeResult.videoStart);
398-
}
399-
audioStartFn = null;
400-
videoStartFn = null;
401392
}
402393

403394
finish();

src/playlist-controller.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ export class PlaylistController extends videojs.EventTarget {
239239
vhs: this.vhs_,
240240
parse708captions: options.parse708captions,
241241
useDtsForTimestampOffset: options.useDtsForTimestampOffset,
242+
calculateTimestampOffsetForEachSegment: options.calculateTimestampOffsetForEachSegment,
242243
captionServices,
243244
mediaSource: this.mediaSource,
244245
currentTime: this.tech_.currentTime.bind(this.tech_),

src/segment-loader.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,10 @@ export const segmentInfoString = (segmentInfo) => {
179179

180180
const timingInfoPropertyForMedia = (mediaType) => `${mediaType}TimingInfo`;
181181

182+
const getBufferedEndOrFallback = (buffered, fallback) => buffered.length ?
183+
buffered.end(buffered.length - 1) :
184+
fallback;
185+
182186
/**
183187
* Returns the timestamp offset to use for the segment.
184188
*
@@ -190,6 +194,8 @@ const timingInfoPropertyForMedia = (mediaType) => `${mediaType}TimingInfo`;
190194
* The estimated segment start
191195
* @param {TimeRange[]} buffered
192196
* The loader's buffer
197+
* @param {boolean} calculateTimestampOffsetForEachSegment
198+
* Feature flag to always calculate timestampOffset
193199
* @param {boolean} overrideCheck
194200
* If true, no checks are made to see if the timestamp offset value should be set,
195201
* but sets it directly to a value.
@@ -203,8 +209,13 @@ export const timestampOffsetForSegment = ({
203209
currentTimeline,
204210
startOfSegment,
205211
buffered,
212+
calculateTimestampOffsetForEachSegment,
206213
overrideCheck
207214
}) => {
215+
if (calculateTimestampOffsetForEachSegment) {
216+
return getBufferedEndOrFallback(buffered, startOfSegment);
217+
}
218+
208219
// Check to see if we are crossing a discontinuity to see if we need to set the
209220
// timestamp offset on the transmuxer and source buffer.
210221
//
@@ -248,7 +259,7 @@ export const timestampOffsetForSegment = ({
248259
// should often be correct, it's better to rely on the buffered end, as the new
249260
// content post discontinuity should line up with the buffered end as if it were
250261
// time 0 for the new content.
251-
return buffered.length ? buffered.end(buffered.length - 1) : startOfSegment;
262+
return getBufferedEndOrFallback(buffered, startOfSegment);
252263
};
253264

254265
/**
@@ -559,6 +570,7 @@ export default class SegmentLoader extends videojs.EventTarget {
559570
this.shouldSaveSegmentTimingInfo_ = true;
560571
this.parse708captions_ = settings.parse708captions;
561572
this.useDtsForTimestampOffset_ = settings.useDtsForTimestampOffset;
573+
this.calculateTimestampOffsetForEachSegment_ = settings.calculateTimestampOffsetForEachSegment;
562574
this.captionServices_ = settings.captionServices;
563575
this.exactManifestTimings = settings.exactManifestTimings;
564576
this.addMetadataToTextTrack = settings.addMetadataToTextTrack;
@@ -1586,6 +1598,7 @@ export default class SegmentLoader extends videojs.EventTarget {
15861598
currentTimeline: this.currentTimeline_,
15871599
startOfSegment,
15881600
buffered: this.buffered_(),
1601+
calculateTimestampOffsetForEachSegment: this.calculateTimestampOffsetForEachSegment_,
15891602
overrideCheck
15901603
});
15911604

src/source-updater.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {getMimeForCodec} from '@videojs/vhs-utils/es/codecs.js';
99
import window from 'global/window';
1010
import toTitleCase from './util/to-title-case.js';
1111
import { QUOTA_EXCEEDED_ERR } from './error-codes';
12-
import {createTimeRanges} from './util/vjs-compat';
12+
import {createTimeRanges, prettyBuffered} from './util/vjs-compat';
1313

1414
const bufferTypes = [
1515
'video',
@@ -297,6 +297,11 @@ const pushQueue = ({type, sourceUpdater, action, doneFn, name}) => {
297297
};
298298

299299
const onUpdateend = (type, sourceUpdater) => (e) => {
300+
const buffered = sourceUpdater[`${type}Buffered`]();
301+
const bufferedAsString = prettyBuffered(buffered);
302+
303+
sourceUpdater.logger_(`${type} source buffer update end. Buffered: \n`, bufferedAsString);
304+
300305
// Although there should, in theory, be a pending action for any updateend receieved,
301306
// there are some actions that may trigger updateend events without set definitions in
302307
// the w3c spec. For instance, setting the duration on the media source may trigger

src/util/vjs-compat.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,28 @@ export function createTimeRanges(...args) {
2424

2525
return fn.apply(context, args);
2626
}
27+
28+
/**
29+
* Converts any buffered time range to a descriptive string
30+
*
31+
* @param {TimeRanges} buffered - time ranges
32+
* @return {string} - descriptive string
33+
*/
34+
export function prettyBuffered(buffered) {
35+
let result = '';
36+
37+
for (let i = 0; i < buffered.length; i++) {
38+
const start = buffered.start(i);
39+
const end = buffered.end(i);
40+
41+
const duration = end - start;
42+
43+
if (result.length) {
44+
result += '\n';
45+
}
46+
47+
result += `[${duration}](${start} -> ${end})`;
48+
}
49+
50+
return result || 'empty';
51+
}

src/videojs-http-streaming.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,7 @@ class VhsHandler extends Component {
690690
this.options_.useForcedSubtitles = this.options_.useForcedSubtitles || false;
691691
this.options_.useNetworkInformationApi = this.options_.useNetworkInformationApi || false;
692692
this.options_.useDtsForTimestampOffset = this.options_.useDtsForTimestampOffset || false;
693+
this.options_.calculateTimestampOffsetForEachSegment = this.options_.calculateTimestampOffsetForEachSegment || false;
693694
this.options_.customTagParsers = this.options_.customTagParsers || [];
694695
this.options_.customTagMappers = this.options_.customTagMappers || [];
695696
this.options_.cacheEncryptionKeys = this.options_.cacheEncryptionKeys || false;
@@ -743,6 +744,7 @@ class VhsHandler extends Component {
743744
'useForcedSubtitles',
744745
'useNetworkInformationApi',
745746
'useDtsForTimestampOffset',
747+
'calculateTimestampOffsetForEachSegment',
746748
'exactManifestTimings',
747749
'leastPixelDiffSelector'
748750
].forEach((option) => {

test/segment-loader.test.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,54 @@ QUnit.test('illegalMediaSwitch detects illegal media switches', function(assert)
222222

223223
QUnit.module('timestampOffsetForSegment');
224224

225+
QUnit.test('returns startOfSegment when calculateTimestampOffsetForEachSegment is enabled and the buffer is empty with the same timeline', function(assert) {
226+
const timestampOffset = timestampOffsetForSegment({
227+
calculateTimestampOffsetForEachSegment: true,
228+
segmentTimeline: 0,
229+
currentTimeline: 0,
230+
startOfSegment: 3,
231+
buffered: createTimeRanges()
232+
});
233+
234+
assert.equal(timestampOffset, 3, 'returned startOfSegment');
235+
});
236+
237+
QUnit.test('returns startOfSegment when calculateTimestampOffsetForEachSegment is enabled and the buffer is empty with different timeline', function(assert) {
238+
const timestampOffset = timestampOffsetForSegment({
239+
calculateTimestampOffsetForEachSegment: true,
240+
segmentTimeline: 1,
241+
currentTimeline: 0,
242+
startOfSegment: 3,
243+
buffered: createTimeRanges()
244+
});
245+
246+
assert.equal(timestampOffset, 3, 'returned startOfSegment');
247+
});
248+
249+
QUnit.test('returns buffered.end when calculateTimestampOffsetForEachSegment is enabled and there exists buffered content with the same timeline', function(assert) {
250+
const timestampOffset = timestampOffsetForSegment({
251+
calculateTimestampOffsetForEachSegment: true,
252+
segmentTimeline: 0,
253+
currentTimeline: 0,
254+
startOfSegment: 3,
255+
buffered: createTimeRanges([[1, 5], [7, 8]])
256+
});
257+
258+
assert.equal(timestampOffset, 8, 'returned buffered.end');
259+
});
260+
261+
QUnit.test('returns buffered.end when calculateTimestampOffsetForEachSegment is enabled and there exists buffered content with different timeline', function(assert) {
262+
const timestampOffset = timestampOffsetForSegment({
263+
calculateTimestampOffsetForEachSegment: true,
264+
segmentTimeline: 1,
265+
currentTimeline: 0,
266+
startOfSegment: 3,
267+
buffered: createTimeRanges([[1, 5], [7, 8]])
268+
});
269+
270+
assert.equal(timestampOffset, 8, 'returned buffered.end');
271+
});
272+
225273
QUnit.test('returns startOfSegment when timeline changes and the buffer is empty', function(assert) {
226274
assert.equal(
227275
timestampOffsetForSegment({

test/source-updater.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,14 +216,14 @@ QUnit.test('verifies that sourcebuffer is in source buffers list before attempti
216216
assert.deepEqual(actionCalls, {
217217
audioAbort: 1,
218218
audioAppendBuffer: 1,
219-
audioBuffered: 8,
219+
audioBuffered: 12,
220220
audioChangeType: 1,
221221
audioRemove: 1,
222222
audioRemoveSourceBuffer: 1,
223223
audioTimestampOffset: 1,
224224
videoAbort: 1,
225225
videoAppendBuffer: 1,
226-
videoBuffered: 8,
226+
videoBuffered: 12,
227227
videoChangeType: 1,
228228
videoRemove: 1,
229229
videoRemoveSourceBuffer: 1,

0 commit comments

Comments
 (0)