diff --git a/README.md b/README.md index d581d4958..a04d1cbc9 100644 --- a/README.md +++ b/README.md @@ -463,6 +463,12 @@ This option defaults to `false`. * Default: `false` * 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). +##### calculateTimestampOffsetForEachSegment +* Type: `boolean`, +* Default: `false` +* Calculate timestampOffset for each segment, regardless of its timeline. Sometimes it is helpful when you have corrupted DTS/PTS timestamps during discontinuities. + + ##### useForcedSubtitles * Type: `boolean` * Default: `false` diff --git a/index.html b/index.html index 8946221a5..0db8a913c 100644 --- a/index.html +++ b/index.html @@ -144,6 +144,11 @@ +
+ + +
+
diff --git a/scripts/index.js b/scripts/index.js index 65ef4e672..301555b0d 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -470,6 +470,7 @@ 'pixel-diff-selector', 'network-info', 'dts-offset', + 'offset-each-segment', 'override-native', 'preload', 'mirror-source', @@ -525,6 +526,7 @@ 'pixel-diff-selector', 'network-info', 'dts-offset', + 'offset-each-segment', 'exact-manifest-timings', 'forced-subtitles' ].forEach(function(name) { @@ -609,6 +611,7 @@ leastPixelDiffSelector: getInputValue(stateEls['pixel-diff-selector']), useNetworkInformationApi: getInputValue(stateEls['network-info']), useDtsForTimestampOffset: getInputValue(stateEls['dts-offset']), + calculateTimestampOffsetForEachSegment: getInputValue(stateEls['offset-each-segment']), useForcedSubtitles: getInputValue(stateEls['forced-subtitles']) } } diff --git a/src/media-segment-request.js b/src/media-segment-request.js index 086f0fe48..bc43779fd 100644 --- a/src/media-segment-request.js +++ b/src/media-segment-request.js @@ -389,15 +389,6 @@ const transmuxAndNotify = ({ isMuxed }); trackInfoFn = null; - - if (probeResult.hasAudio && !isMuxed) { - audioStartFn(probeResult.audioStart); - } - if (probeResult.hasVideo) { - videoStartFn(probeResult.videoStart); - } - audioStartFn = null; - videoStartFn = null; } finish(); diff --git a/src/playlist-controller.js b/src/playlist-controller.js index 1767409c3..98ac9149d 100644 --- a/src/playlist-controller.js +++ b/src/playlist-controller.js @@ -239,6 +239,7 @@ export class PlaylistController extends videojs.EventTarget { vhs: this.vhs_, parse708captions: options.parse708captions, useDtsForTimestampOffset: options.useDtsForTimestampOffset, + calculateTimestampOffsetForEachSegment: options.calculateTimestampOffsetForEachSegment, captionServices, mediaSource: this.mediaSource, currentTime: this.tech_.currentTime.bind(this.tech_), diff --git a/src/segment-loader.js b/src/segment-loader.js index 6ccc9b7a6..b67c4984c 100644 --- a/src/segment-loader.js +++ b/src/segment-loader.js @@ -179,6 +179,10 @@ export const segmentInfoString = (segmentInfo) => { const timingInfoPropertyForMedia = (mediaType) => `${mediaType}TimingInfo`; +const getBufferedEndOrFallback = (buffered, fallback) => buffered.length ? + buffered.end(buffered.length - 1) : + fallback; + /** * Returns the timestamp offset to use for the segment. * @@ -190,6 +194,8 @@ const timingInfoPropertyForMedia = (mediaType) => `${mediaType}TimingInfo`; * The estimated segment start * @param {TimeRange[]} buffered * The loader's buffer + * @param {boolean} calculateTimestampOffsetForEachSegment + * Feature flag to always calculate timestampOffset * @param {boolean} overrideCheck * If true, no checks are made to see if the timestamp offset value should be set, * but sets it directly to a value. @@ -203,8 +209,13 @@ export const timestampOffsetForSegment = ({ currentTimeline, startOfSegment, buffered, + calculateTimestampOffsetForEachSegment, overrideCheck }) => { + if (calculateTimestampOffsetForEachSegment) { + return getBufferedEndOrFallback(buffered, startOfSegment); + } + // Check to see if we are crossing a discontinuity to see if we need to set the // timestamp offset on the transmuxer and source buffer. // @@ -248,7 +259,7 @@ export const timestampOffsetForSegment = ({ // should often be correct, it's better to rely on the buffered end, as the new // content post discontinuity should line up with the buffered end as if it were // time 0 for the new content. - return buffered.length ? buffered.end(buffered.length - 1) : startOfSegment; + return getBufferedEndOrFallback(buffered, startOfSegment); }; /** @@ -559,6 +570,7 @@ export default class SegmentLoader extends videojs.EventTarget { this.shouldSaveSegmentTimingInfo_ = true; this.parse708captions_ = settings.parse708captions; this.useDtsForTimestampOffset_ = settings.useDtsForTimestampOffset; + this.calculateTimestampOffsetForEachSegment_ = settings.calculateTimestampOffsetForEachSegment; this.captionServices_ = settings.captionServices; this.exactManifestTimings = settings.exactManifestTimings; this.addMetadataToTextTrack = settings.addMetadataToTextTrack; @@ -1586,6 +1598,7 @@ export default class SegmentLoader extends videojs.EventTarget { currentTimeline: this.currentTimeline_, startOfSegment, buffered: this.buffered_(), + calculateTimestampOffsetForEachSegment: this.calculateTimestampOffsetForEachSegment_, overrideCheck }); diff --git a/src/source-updater.js b/src/source-updater.js index a7f3f11da..12c635914 100644 --- a/src/source-updater.js +++ b/src/source-updater.js @@ -9,7 +9,7 @@ import {getMimeForCodec} from '@videojs/vhs-utils/es/codecs.js'; import window from 'global/window'; import toTitleCase from './util/to-title-case.js'; import { QUOTA_EXCEEDED_ERR } from './error-codes'; -import {createTimeRanges} from './util/vjs-compat'; +import {createTimeRanges, prettyBuffered} from './util/vjs-compat'; const bufferTypes = [ 'video', @@ -297,6 +297,11 @@ const pushQueue = ({type, sourceUpdater, action, doneFn, name}) => { }; const onUpdateend = (type, sourceUpdater) => (e) => { + const buffered = sourceUpdater[`${type}Buffered`](); + const bufferedAsString = prettyBuffered(buffered); + + sourceUpdater.logger_(`${type} source buffer update end. Buffered: \n`, bufferedAsString); + // Although there should, in theory, be a pending action for any updateend receieved, // there are some actions that may trigger updateend events without set definitions in // the w3c spec. For instance, setting the duration on the media source may trigger diff --git a/src/util/vjs-compat.js b/src/util/vjs-compat.js index d51a4b963..e545ffc33 100644 --- a/src/util/vjs-compat.js +++ b/src/util/vjs-compat.js @@ -24,3 +24,28 @@ export function createTimeRanges(...args) { return fn.apply(context, args); } + +/** + * Converts any buffered time range to a descriptive string + * + * @param {TimeRanges} buffered - time ranges + * @return {string} - descriptive string + */ +export function prettyBuffered(buffered) { + let result = ''; + + for (let i = 0; i < buffered.length; i++) { + const start = buffered.start(i); + const end = buffered.end(i); + + const duration = end - start; + + if (result.length) { + result += '\n'; + } + + result += `[${duration}](${start} -> ${end})`; + } + + return result || 'empty'; +} diff --git a/src/videojs-http-streaming.js b/src/videojs-http-streaming.js index f484d13c5..a1f62bc20 100644 --- a/src/videojs-http-streaming.js +++ b/src/videojs-http-streaming.js @@ -690,6 +690,7 @@ class VhsHandler extends Component { this.options_.useForcedSubtitles = this.options_.useForcedSubtitles || false; this.options_.useNetworkInformationApi = this.options_.useNetworkInformationApi || false; this.options_.useDtsForTimestampOffset = this.options_.useDtsForTimestampOffset || false; + this.options_.calculateTimestampOffsetForEachSegment = this.options_.calculateTimestampOffsetForEachSegment || false; this.options_.customTagParsers = this.options_.customTagParsers || []; this.options_.customTagMappers = this.options_.customTagMappers || []; this.options_.cacheEncryptionKeys = this.options_.cacheEncryptionKeys || false; @@ -743,6 +744,7 @@ class VhsHandler extends Component { 'useForcedSubtitles', 'useNetworkInformationApi', 'useDtsForTimestampOffset', + 'calculateTimestampOffsetForEachSegment', 'exactManifestTimings', 'leastPixelDiffSelector' ].forEach((option) => { diff --git a/test/segment-loader.test.js b/test/segment-loader.test.js index 5a75b407d..62d61e02a 100644 --- a/test/segment-loader.test.js +++ b/test/segment-loader.test.js @@ -222,6 +222,54 @@ QUnit.test('illegalMediaSwitch detects illegal media switches', function(assert) QUnit.module('timestampOffsetForSegment'); +QUnit.test('returns startOfSegment when calculateTimestampOffsetForEachSegment is enabled and the buffer is empty with the same timeline', function(assert) { + const timestampOffset = timestampOffsetForSegment({ + calculateTimestampOffsetForEachSegment: true, + segmentTimeline: 0, + currentTimeline: 0, + startOfSegment: 3, + buffered: createTimeRanges() + }); + + assert.equal(timestampOffset, 3, 'returned startOfSegment'); +}); + +QUnit.test('returns startOfSegment when calculateTimestampOffsetForEachSegment is enabled and the buffer is empty with different timeline', function(assert) { + const timestampOffset = timestampOffsetForSegment({ + calculateTimestampOffsetForEachSegment: true, + segmentTimeline: 1, + currentTimeline: 0, + startOfSegment: 3, + buffered: createTimeRanges() + }); + + assert.equal(timestampOffset, 3, 'returned startOfSegment'); +}); + +QUnit.test('returns buffered.end when calculateTimestampOffsetForEachSegment is enabled and there exists buffered content with the same timeline', function(assert) { + const timestampOffset = timestampOffsetForSegment({ + calculateTimestampOffsetForEachSegment: true, + segmentTimeline: 0, + currentTimeline: 0, + startOfSegment: 3, + buffered: createTimeRanges([[1, 5], [7, 8]]) + }); + + assert.equal(timestampOffset, 8, 'returned buffered.end'); +}); + +QUnit.test('returns buffered.end when calculateTimestampOffsetForEachSegment is enabled and there exists buffered content with different timeline', function(assert) { + const timestampOffset = timestampOffsetForSegment({ + calculateTimestampOffsetForEachSegment: true, + segmentTimeline: 1, + currentTimeline: 0, + startOfSegment: 3, + buffered: createTimeRanges([[1, 5], [7, 8]]) + }); + + assert.equal(timestampOffset, 8, 'returned buffered.end'); +}); + QUnit.test('returns startOfSegment when timeline changes and the buffer is empty', function(assert) { assert.equal( timestampOffsetForSegment({ diff --git a/test/source-updater.test.js b/test/source-updater.test.js index 4ca5d57f2..b79fc2150 100644 --- a/test/source-updater.test.js +++ b/test/source-updater.test.js @@ -216,14 +216,14 @@ QUnit.test('verifies that sourcebuffer is in source buffers list before attempti assert.deepEqual(actionCalls, { audioAbort: 1, audioAppendBuffer: 1, - audioBuffered: 8, + audioBuffered: 12, audioChangeType: 1, audioRemove: 1, audioRemoveSourceBuffer: 1, audioTimestampOffset: 1, videoAbort: 1, videoAppendBuffer: 1, - videoBuffered: 8, + videoBuffered: 12, videoChangeType: 1, videoRemove: 1, videoRemoveSourceBuffer: 1,