Skip to content

Commit 6fe7d9c

Browse files
dzianis-dashkevichDzianis Dashkevich
andauthored
fix: multi-period DASH VOD fixes (#1551)
* chore: clear previous dash media request timeout everytime we clear or update it * chore: call fast-quality-switch only when we enable playlist from quality selector * chore: add isPaused for dash playlist loader to mitigate duplicate playlist trigger for the main segment loader * chore: fix fastQualityChange_ tests * chore: add fast quality change debounce * chore: add debounce tick to the tests * chore: restart all loaders when we hit fix bad timeline change * chore: update segment loader * chore: update run-fast-quality-switch and fix bad timeline changes * fix test * chore: fix lint issues --------- Co-authored-by: Dzianis Dashkevich <ddashkevich@brightcove.com>
1 parent 2f8d0af commit 6fe7d9c

9 files changed

+117
-77
lines changed

src/dash-playlist-loader.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,8 @@ export default class DashPlaylistLoader extends EventTarget {
304304
constructor(srcUrlOrPlaylist, vhs, options = { }, mainPlaylistLoader) {
305305
super();
306306

307+
this.isPaused_ = true;
308+
307309
this.mainPlaylistLoader_ = mainPlaylistLoader || this;
308310
if (!mainPlaylistLoader) {
309311
this.isMain_ = true;
@@ -345,6 +347,10 @@ export default class DashPlaylistLoader extends EventTarget {
345347
}
346348
}
347349

350+
get isPaused() {
351+
return this.isPaused_;
352+
}
353+
348354
requestErrored_(err, request, startingState) {
349355
// disposed
350356
if (!this.request) {
@@ -384,6 +390,7 @@ export default class DashPlaylistLoader extends EventTarget {
384390
// playlist lacks sidx or sidx segments were added to this playlist already.
385391
if (!playlist.sidx || !sidxKey || this.mainPlaylistLoader_.sidxMapping_[sidxKey]) {
386392
// keep this function async
393+
window.clearTimeout(this.mediaRequest_);
387394
this.mediaRequest_ = window.setTimeout(() => cb(false), 0);
388395
return;
389396
}
@@ -465,6 +472,7 @@ export default class DashPlaylistLoader extends EventTarget {
465472
}
466473

467474
dispose() {
475+
this.isPaused_ = true;
468476
this.trigger('dispose');
469477
this.stopRequest();
470478
this.loadedPlaylists_ = {};
@@ -553,6 +561,7 @@ export default class DashPlaylistLoader extends EventTarget {
553561
haveMetadata({startingState, playlist}) {
554562
this.state = 'HAVE_METADATA';
555563
this.loadedPlaylists_[playlist.id] = playlist;
564+
window.clearTimeout(this.mediaRequest_);
556565
this.mediaRequest_ = null;
557566

558567
// This will trigger loadedplaylist
@@ -569,6 +578,8 @@ export default class DashPlaylistLoader extends EventTarget {
569578
}
570579

571580
pause() {
581+
this.isPaused_ = true;
582+
572583
if (this.mainPlaylistLoader_.createMupOnMedia_) {
573584
this.off('loadedmetadata', this.mainPlaylistLoader_.createMupOnMedia_);
574585
this.mainPlaylistLoader_.createMupOnMedia_ = null;
@@ -588,6 +599,8 @@ export default class DashPlaylistLoader extends EventTarget {
588599
}
589600

590601
load(isFinalRendition) {
602+
this.isPaused_ = false;
603+
591604
window.clearTimeout(this.mediaUpdateTimeout);
592605
this.mediaUpdateTimeout = null;
593606

@@ -629,6 +642,7 @@ export default class DashPlaylistLoader extends EventTarget {
629642
// We don't need to request the main manifest again
630643
// Call this asynchronously to match the xhr request behavior below
631644
if (!this.isMain_) {
645+
window.clearTimeout(this.mediaRequest_);
632646
this.mediaRequest_ = window.setTimeout(() => this.haveMain_(), 0);
633647
return;
634648
}
@@ -773,6 +787,7 @@ export default class DashPlaylistLoader extends EventTarget {
773787

774788
handleMain_() {
775789
// clear media request
790+
window.clearTimeout(this.mediaRequest_);
776791
this.mediaRequest_ = null;
777792

778793
const oldMain = this.mainPlaylistLoader_.main;

src/playlist-controller.js

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {merge, createTimeRanges} from './util/vjs-compat';
2929
import { addMetadata, createMetadataTrackIfNotExists, addDateRangeMetadata } from './util/text-tracks';
3030
import ContentSteeringController from './content-steering-controller';
3131
import { bufferToHexString } from './util/string.js';
32+
import {debounce} from './util/fn';
3233

3334
const ABORT_EARLY_EXCLUSION_SECONDS = 10;
3435

@@ -152,6 +153,11 @@ export class PlaylistController extends videojs.EventTarget {
152153
constructor(options) {
153154
super();
154155

156+
// Adding a slight debounce to avoid duplicate calls during rapid quality changes, for example:
157+
// When selecting quality from the quality list,
158+
// where we may have multiple bandwidth profiles for the same vertical resolution.
159+
this.fastQualityChange_ = debounce(this.fastQualityChange_.bind(this), 100);
160+
155161
const {
156162
src,
157163
withCredentials,
@@ -701,7 +707,16 @@ export class PlaylistController extends videojs.EventTarget {
701707

702708
if (this.sourceType_ === 'dash') {
703709
// we don't want to re-request the same hls playlist right after it was changed
704-
this.mainPlaylistLoader_.load();
710+
711+
// Initially it was implemented as workaround to restart playlist loader for live
712+
// when playlist loader is paused because of playlist exclusions:
713+
// see: https://github.com/videojs/http-streaming/pull/1339
714+
// but this introduces duplicate "loadedplaylist" event.
715+
// Ideally we want to re-think playlist loader life-cycle events,
716+
// but simply checking "paused" state should help a lot
717+
if (this.mainPlaylistLoader_.isPaused) {
718+
this.mainPlaylistLoader_.load();
719+
}
705720
}
706721

707722
// TODO: Create a new event on the PlaylistLoader that signals
@@ -962,6 +977,24 @@ export class PlaylistController extends videojs.EventTarget {
962977
this.tech_.setCurrentTime(newTime);
963978
});
964979

980+
this.timelineChangeController_.on('fixBadTimelineChange', () => {
981+
// pause, reset-everything and load for all segment-loaders
982+
this.logger_('Fix bad timeline change. Restarting al segment loaders...');
983+
this.mainSegmentLoader_.pause();
984+
this.mainSegmentLoader_.resetEverything();
985+
if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
986+
this.audioSegmentLoader_.pause();
987+
this.audioSegmentLoader_.resetEverything();
988+
}
989+
if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) {
990+
this.subtitleSegmentLoader_.pause();
991+
this.subtitleSegmentLoader_.resetEverything();
992+
}
993+
994+
// start segment loader loading in case they are paused
995+
this.load();
996+
});
997+
965998
this.mainSegmentLoader_.on('earlyabort', (event) => {
966999
// never try to early abort with the new ABR algorithm
9671000
if (this.bufferBasedABR) {
@@ -1109,13 +1142,19 @@ export class PlaylistController extends videojs.EventTarget {
11091142

11101143
runFastQualitySwitch_() {
11111144
this.waitingForFastQualityPlaylistReceived_ = false;
1112-
// Delete all buffered data to allow an immediate quality switch.
11131145
this.mainSegmentLoader_.pause();
1114-
this.mainSegmentLoader_.resetEverything(() => {
1115-
this.mainSegmentLoader_.load();
1116-
});
1146+
this.mainSegmentLoader_.resetEverything();
1147+
if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
1148+
this.audioSegmentLoader_.pause();
1149+
this.audioSegmentLoader_.resetEverything();
1150+
}
1151+
if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) {
1152+
this.subtitleSegmentLoader_.pause();
1153+
this.subtitleSegmentLoader_.resetEverything();
1154+
}
11171155

1118-
// don't need to reset audio as it is reset when media changes
1156+
// start segment loader loading in case they are paused
1157+
this.load();
11191158
}
11201159

11211160
/**

src/rendition-mixin.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ const enableFunction = (loader, playlistID, changePlaylistFn) => (enable) => {
3939

4040
if (enable !== currentlyEnabled && !incompatible) {
4141
// Ensure the outside world knows about our changes
42-
changePlaylistFn(playlist);
4342
if (enable) {
43+
// call fast quality change only when the playlist is enabled
44+
changePlaylistFn(playlist);
4445
loader.trigger({ type: 'renditionenabled', metadata});
4546
} else {
4647
loader.trigger({ type: 'renditiondisabled', metadata});

src/segment-loader.js

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -423,21 +423,6 @@ export const shouldFixBadTimelineChanges = (timelineChangeController) => {
423423
return false;
424424
};
425425

426-
/**
427-
* Fixes certain bad timeline scenarios by resetting the loader.
428-
*
429-
* @param {SegmentLoader} segmentLoader
430-
*/
431-
export const fixBadTimelineChange = (segmentLoader) => {
432-
if (!segmentLoader) {
433-
return;
434-
}
435-
436-
segmentLoader.pause();
437-
segmentLoader.resetEverything();
438-
segmentLoader.load();
439-
};
440-
441426
/**
442427
* Check if the pending audio timeline change is behind the
443428
* pending main timeline change.
@@ -480,7 +465,7 @@ const checkAndFixTimelines = (segmentLoader) => {
480465
return;
481466
}
482467

483-
fixBadTimelineChange(segmentLoader);
468+
segmentLoader.timelineChangeController_.trigger('fixBadTimelineChange');
484469
}
485470
};
486471

@@ -872,6 +857,7 @@ export default class SegmentLoader extends videojs.EventTarget {
872857
if (this.pendingSegment_) {
873858
this.pendingSegment_ = null;
874859
}
860+
this.timelineChangeController_.clearPendingTimelineChange(this.loaderType_);
875861
return;
876862
}
877863

@@ -1118,6 +1104,14 @@ export default class SegmentLoader extends videojs.EventTarget {
11181104
return;
11191105
}
11201106

1107+
if (this.playlist_ &&
1108+
this.playlist_.endList &&
1109+
newPlaylist.endList &&
1110+
this.playlist_.uri === newPlaylist.uri) {
1111+
// skip update if both prev and new are vod and have the same URI
1112+
return;
1113+
}
1114+
11211115
const oldPlaylist = this.playlist_;
11221116
const segmentInfo = this.pendingSegment_;
11231117

src/util/fn.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export const debounce = (callback, wait) => {
2+
let timeoutId = null;
3+
4+
return (...args) => {
5+
clearTimeout(timeoutId);
6+
7+
timeoutId = setTimeout(() => {
8+
callback.apply(null, args);
9+
}, wait);
10+
};
11+
};

test/playlist-controller.test.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -726,8 +726,9 @@ QUnit.test('resets everything for a fast quality change then calls load', functi
726726

727727
segmentLoader.remove = (start, end, doneFn) => {
728728
assert.equal(end, Infinity, 'on a remove all, end should be Infinity');
729-
assert.ok(doneFn);
730-
doneFn();
729+
if (doneFn) {
730+
doneFn();
731+
}
731732
origRemove.call(segmentLoader, start, end, doneFn);
732733
};
733734

@@ -4871,6 +4872,7 @@ QUnit.test('can pass or select a playlist for fastQualityChange', function(asser
48714872
};
48724873

48734874
pc.fastQualityChange_(pc.main().playlists[1]);
4875+
this.clock.tick(110);
48744876
pc.runFastQualitySwitch_();
48754877
assert.deepEqual(calls, {
48764878
resetEverything: 1,
@@ -4880,6 +4882,7 @@ QUnit.test('can pass or select a playlist for fastQualityChange', function(asser
48804882
}, 'calls expected function when passed a playlist');
48814883

48824884
pc.fastQualityChange_();
4885+
this.clock.tick(110);
48834886
pc.runFastQualitySwitch_();
48844887
assert.deepEqual(calls, {
48854888
resetEverything: 2,

test/rendition-mixin.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ QUnit.test(
294294

295295
renditions[1].enabled(false);
296296

297-
assert.equal(pc.fastQualityChange_.calls, 2, 'fastQualityChange_ was called twice');
297+
assert.equal(pc.fastQualityChange_.calls, 1, 'fastQualityChange_ was called once');
298298
}
299299
);
300300

0 commit comments

Comments
 (0)