From 5353d28d06652527b8ff11e8ca1d33cddf6ef1cf Mon Sep 17 00:00:00 2001 From: Christopher Pruijsen Date: Sat, 12 Sep 2026 01:53:57 +0100 Subject: [PATCH] fix: honor maxPlaylistRetries for single-media HLS playlists Single-playlist sources skipped exclusion unless exclude duration was Infinity, so maxPlaylistRetries never stopped reload attempts. --- src/playlist-controller.js | 6 ++-- test/playlist-controller.test.js | 53 ++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/playlist-controller.js b/src/playlist-controller.js index d4181509f..a795867de 100644 --- a/src/playlist-controller.js +++ b/src/playlist-controller.js @@ -1426,8 +1426,10 @@ export class PlaylistController extends videojs.EventTarget { const isFinalRendition = enabledPlaylists.length === 1 && enabledPlaylists[0] === playlistToExclude; // Don't exclude the only playlist unless it was excluded - // forever - if (playlists.length === 1 && playlistExclusionDuration !== Infinity) { + // forever, or retries have been exhausted. + if (playlists.length === 1 && + playlistExclusionDuration !== Infinity && + playlistToExclude.playlistErrors_ <= this.maxPlaylistRetries) { videojs.log.warn(`Problem encountered with playlist ${playlistToExclude.id}. ` + 'Trying again since it is the only playlist.'); diff --git a/test/playlist-controller.test.js b/test/playlist-controller.test.js index 77dfe527e..0b1a9b837 100644 --- a/test/playlist-controller.test.js +++ b/test/playlist-controller.test.js @@ -6689,6 +6689,59 @@ QUnit.test('Playlist is excluded indefinitely if number of playlistErrors_ excee this.env.log.warn.callCount = 0; }); +QUnit.test('single media playlist is excluded indefinitely once playlistErrors_ exceeds maxPlaylistRetries', function(assert) { + this.requests.length = 0; + this.player.dispose(); + this.player = createPlayer({ html5: { vhs: { maxPlaylistRetries: 1 } } }); + this.player.src({ + src: 'manifest/media.m3u8', + type: 'application/vnd.apple.mpegurl' + }); + + this.clock.tick(1); + + this.playlistController = this.player.tech_.vhs.playlistController_; + + // media playlist with no master + this.standardXHRResponse(this.requests.shift()); + + const pc = this.playlistController; + const mpl = pc.mainPlaylistLoader_; + const playlist = mpl.main.playlists[0]; + let loadCalled = false; + let errorTriggered = false; + + mpl.load = () => { + loadCalled = true; + }; + pc.on('error', () => { + errorTriggered = true; + }); + + assert.equal(mpl.main.playlists.length, 1, 'source is a single media playlist'); + assert.equal(playlist.playlistErrors_, 0, 'playlistErrors_ starts at zero'); + + pc.excludePlaylist({}); + + assert.notOk('excludeUntil' in playlist, 'playlist was not excluded on first error'); + assert.equal(playlist.playlistErrors_, 1, 'we incremented playlistErrors_'); + assert.ok(loadCalled, 'retried the only playlist'); + assert.notOk(errorTriggered, 'did not error before retries were exceeded'); + assert.equal(this.env.log.warn.callCount, 1, 'logged a warning'); + + loadCalled = false; + pc.excludePlaylist({}); + + assert.equal(playlist.playlistErrors_, 2, 'we incremented playlistErrors_'); + assert.equal(playlist.excludeUntil, Infinity, 'The playlist was excluded indefinitely'); + assert.ok(errorTriggered, 'we triggered a playback error'); + assert.notOk(loadCalled, 'did not retry after exceeding maxPlaylistRetries'); + assert.equal(this.env.log.error.callCount, 1, 'logged an error'); + + this.env.log.warn.callCount = 0; + this.env.log.error.callCount = 0; +}); + QUnit.test('should delay loading of new playlist if lastRequest was less than half target duration', function(assert) { this.requests.length = 0; this.player.dispose();