From 0dceb5b2d8c78135e44d13071349dd03bd7f2519 Mon Sep 17 00:00:00 2001 From: Brandon Casey <2381475+brandonocasey@users.noreply.github.com> Date: Fri, 5 Mar 2021 09:48:03 -0800 Subject: [PATCH] fix: Add exclude reason and skip duplicate playlist-unchanged (#1082) --- src/dash-playlist-loader.js | 99 ++++++++++++++++++++++--------- src/master-playlist-controller.js | 14 ++++- 2 files changed, 85 insertions(+), 28 deletions(-) diff --git a/src/dash-playlist-loader.js b/src/dash-playlist-loader.js index 1feda8e1c..1c96825dd 100644 --- a/src/dash-playlist-loader.js +++ b/src/dash-playlist-loader.js @@ -19,6 +19,7 @@ import { } from './manifest'; import containerRequest from './util/container-request.js'; import {toUint8} from '@videojs/vhs-utils/es/byte-helpers'; +import logger from './util/logger'; const { EventTarget, mergeOptions } = videojs; @@ -291,6 +292,7 @@ export default class DashPlaylistLoader extends EventTarget { this.state = 'HAVE_NOTHING'; this.loadedPlaylists_ = {}; + this.logger_ = logger('DashPlaylistLoader'); // initialize the loader state // The masterPlaylistLoader will be created with a string @@ -415,6 +417,14 @@ export default class DashPlaylistLoader extends EventTarget { window.clearTimeout(this.minimumUpdatePeriodTimeout_); window.clearTimeout(this.mediaRequest_); window.clearTimeout(this.mediaUpdateTimeout); + this.mediaUpdateTimeout = null; + this.mediaRequest_ = null; + this.minimumUpdatePeriodTimeout_ = null; + + if (this.masterPlaylistLoader_.createMupOnMedia_) { + this.off('loadedmetadata', this.masterPlaylistLoader_.createMupOnMedia_); + this.masterPlaylistLoader_.createMupOnMedia_ = null; + } this.off(); } @@ -505,9 +515,15 @@ export default class DashPlaylistLoader extends EventTarget { } pause() { + if (this.masterPlaylistLoader_.createMupOnMedia_) { + this.off('loadedmetadata', this.masterPlaylistLoader_.createMupOnMedia_); + this.masterPlaylistLoader_.createMupOnMedia_ = null; + } this.stopRequest(); window.clearTimeout(this.mediaUpdateTimeout); - window.clearTimeout(this.minimumUpdatePeriodTimeout_); + window.clearTimeout(this.masterPlaylistLoader_.minimumUpdatePeriodTimeout_); + this.masterPlaylistLoader_.minimumUpdatePeriodTimeout_ = null; + this.mediaUpdateTimeout = null; if (this.state === 'HAVE_NOTHING') { // If we pause the loader before any data has been retrieved, its as if we never // started, so reset to an unstarted state. @@ -517,7 +533,7 @@ export default class DashPlaylistLoader extends EventTarget { load(isFinalRendition) { window.clearTimeout(this.mediaUpdateTimeout); - window.clearTimeout(this.minimumUpdatePeriodTimeout_); + this.mediaUpdateTimeout = null; const media = this.media(); @@ -696,8 +712,7 @@ export default class DashPlaylistLoader extends EventTarget { this.masterPlaylistLoader_.srcUrl = location; } - // if the minimumUpdatePeriod was changed, update the minimumUpdatePeriodTimeout_ - if (!oldMaster || (newMaster && oldMaster.minimumUpdatePeriod !== newMaster.minimumUpdatePeriod)) { + if (!oldMaster || (newMaster && newMaster.minimumUpdatePeriod !== oldMaster.minimumUpdatePeriod)) { this.updateMinimumUpdatePeriodTimeout_(); } @@ -705,36 +720,57 @@ export default class DashPlaylistLoader extends EventTarget { } updateMinimumUpdatePeriodTimeout_() { - // Clear existing timeout - window.clearTimeout(this.minimumUpdatePeriodTimeout_); + const mpl = this.masterPlaylistLoader_; - const createMUPTimeout = (mup) => { - this.minimumUpdatePeriodTimeout_ = window.setTimeout(() => { - this.trigger('minimumUpdatePeriod'); - createMUPTimeout(mup); - }, mup); - }; + // cancel any pending creation of mup on media + // a new one will be added if needed. + if (mpl.createMupOnMedia_) { + mpl.off('loadedmetadata', mpl.createMupOnMedia_); + mpl.createMupOnMedia_ = null; + } - const minimumUpdatePeriod = this.masterPlaylistLoader_.master && this.masterPlaylistLoader_.master.minimumUpdatePeriod; + // clear any pending timeouts + if (mpl.minimumUpdatePeriodTimeout_) { + window.clearTimeout(mpl.minimumUpdatePeriodTimeout_); + mpl.minimumUpdatePeriodTimeout_ = null; + } - if (minimumUpdatePeriod > 0) { - createMUPTimeout(minimumUpdatePeriod); + let mup = mpl.master && mpl.master.minimumUpdatePeriod; // If the minimumUpdatePeriod has a value of 0, that indicates that the current // MPD has no future validity, so a new one will need to be acquired when new // media segments are to be made available. Thus, we use the target duration // in this case - } else if (minimumUpdatePeriod === 0) { - // If we haven't yet selected a playlist, wait until then so we know the - // target duration - if (!this.media()) { - this.one('loadedplaylist', () => { - createMUPTimeout(this.media().targetDuration * 1000); - }); + if (mup === 0) { + if (mpl.media()) { + mup = mpl.media().targetDuration * 1000; } else { - createMUPTimeout(this.media().targetDuration * 1000); + mpl.createMupOnMedia_ = mpl.updateMinimumUpdatePeriodTimeout_; + mpl.one('loadedmetadata', mpl.createMupOnMedia_); } } + + // if minimumUpdatePeriod is invalid or <= zero, which + // can happen when a live video becomes VOD. skip timeout + // creation. + if (typeof mup !== 'number' || mup <= 0) { + if (mup < 0) { + this.logger_(`found invalid minimumUpdatePeriod of ${mup}, not setting a timeout`); + } + return; + } + + this.createMUPTimeout_(mup); + } + + createMUPTimeout_(mup) { + const mpl = this.masterPlaylistLoader_; + + mpl.minimumUpdatePeriodTimeout_ = window.setTimeout(() => { + mpl.minimumUpdatePeriodTimeout_ = null; + mpl.trigger('minimumUpdatePeriod'); + mpl.createMUPTimeout_(mup); + }, mup); } /** @@ -791,10 +827,19 @@ export default class DashPlaylistLoader extends EventTarget { this.trigger('playlistunchanged'); } - if (!this.media().endList) { - this.mediaUpdateTimeout = window.setTimeout(() => { - this.trigger('mediaupdatetimeout'); - }, refreshDelay(this.media(), Boolean(mediaChanged))); + if (!this.mediaUpdateTimeout) { + const createMediaUpdateTimeout = () => { + if (this.media().endList) { + return; + } + + this.mediaUpdateTimeout = window.setTimeout(() => { + this.trigger('mediaupdatetimeout'); + createMediaUpdateTimeout(); + }, refreshDelay(this.media(), Boolean(mediaChanged))); + }; + + createMediaUpdateTimeout(); } this.trigger('loadedplaylist'); diff --git a/src/master-playlist-controller.js b/src/master-playlist-controller.js index d82ebcda6..ca2ef5560 100644 --- a/src/master-playlist-controller.js +++ b/src/master-playlist-controller.js @@ -472,6 +472,14 @@ export class MasterPlaylistController extends videojs.EventTarget { this.masterPlaylistLoader_.on('playlistunchanged', () => { const updatedPlaylist = this.masterPlaylistLoader_.media(); + + // ignore unchanged playlists that have already been + // excluded for not-changing. We likely just have a really slowly updating + // playlist. + if (updatedPlaylist.lastExcludeReason_ === 'playlist-unchanged') { + return; + } + const playlistOutdated = this.stuckAtPlaylistEnd_(updatedPlaylist); if (playlistOutdated) { @@ -480,7 +488,8 @@ export class MasterPlaylistController extends videojs.EventTarget { // one is updating (and give the player a chance to re-adjust to the // safe live point). this.blacklistCurrentPlaylist({ - message: 'Playlist no longer updating.' + message: 'Playlist no longer updating.', + reason: 'playlist-unchanged' }); // useful for monitoring QoS this.tech_.trigger('playliststuck'); @@ -1066,6 +1075,9 @@ export class MasterPlaylistController extends videojs.EventTarget { // Blacklist this playlist currentPlaylist.excludeUntil = Date.now() + (blacklistDuration * 1000); + if (error.reason) { + currentPlaylist.lastExcludeReason_ = error.reason; + } this.tech_.trigger('blacklistplaylist'); this.tech_.trigger({type: 'usage', name: 'vhs-rendition-blacklisted'}); this.tech_.trigger({type: 'usage', name: 'hls-rendition-blacklisted'});