From e316e5b093956f152013767d58ea1293cfa64ceb Mon Sep 17 00:00:00 2001 From: Matthew Neil Date: Fri, 29 Sep 2017 17:54:43 -0400 Subject: [PATCH 1/3] fix InvalidStateError for live playback in IE11 --- src/master-playlist-controller.js | 37 +++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/src/master-playlist-controller.js b/src/master-playlist-controller.js index 6fb766fb7..7dad6f2f0 100644 --- a/src/master-playlist-controller.js +++ b/src/master-playlist-controller.js @@ -698,6 +698,8 @@ export class MasterPlaylistController extends videojs.EventTarget { // 3) the player has not started playing !this.hasPlayed_()) { + let setHasPlayed = false; + // when the video is a live stream if (!media.endList) { this.trigger('firstplay'); @@ -705,10 +707,32 @@ export class MasterPlaylistController extends videojs.EventTarget { // seek to the latest media position for live videos seekable = this.seekable(); if (seekable.length) { - this.tech_.setCurrentTime(seekable.end(0)); + if (videojs.browser.IE_VERSION && + this.mode_ === 'html5' && + this.tech_.readyState() === 0) { + // IE11 throws an InvalidStateError if you try to set currentTime while the + // readyState is 0, so it must be delayed. + this.tech_.one('loadedmetadata', () => { + // we hold off on setting hasPlayed on true until after we seek to prevent + // segment loaders from requesting more than 1 segment before the seek to + // live point + this.tech_.setCurrentTime(seekable.end(0)); + this.hasPlayed_ = () => true; + }); + } else { + setHasPlayed = true; + this.tech_.setCurrentTime(seekable.end(0)); + } } + } else { + // always set hasPlayed to true for first play of VOD + setHasPlayed = true; } - this.hasPlayed_ = () => true; + + if (setHasPlayed) { + this.hasPlayed_ = () => true; + } + // now that we are ready, load the segment this.load(); return true; @@ -893,14 +917,9 @@ export class MasterPlaylistController extends videojs.EventTarget { } // In flash playback, the segment loaders should be reset on every seek, even - // in buffer seeks - const isFlash = - (this.mode_ === 'flash') || - (this.mode_ === 'auto' && !videojs.MediaSource.supportsNativeMediaSources()); - - // if the seek location is already buffered, continue buffering as + // in buffer seeks. If the seek location is already buffered, continue buffering as // usual - if (buffered && buffered.length && !isFlash) { + if (buffered && buffered.length && this.mode_ !== 'flash') { return currentTime; } From e81541642b29fbf846d0279c7e3fae6de5ce0884 Mon Sep 17 00:00:00 2001 From: Matthew Neil Date: Wed, 4 Oct 2017 12:29:31 -0400 Subject: [PATCH 2/3] add test --- test/videojs-contrib-hls.test.js | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/test/videojs-contrib-hls.test.js b/test/videojs-contrib-hls.test.js index 2124ee81f..6ff3724b2 100644 --- a/test/videojs-contrib-hls.test.js +++ b/test/videojs-contrib-hls.test.js @@ -275,7 +275,37 @@ QUnit.test('autoplay seeks to the live point after media source open', function( assert.notEqual(currentTime, 0, 'seeked on autoplay'); }); -QUnit.test('duration is set when the source opens after the playlist is loaded', function(assert) { +QUnit.test('autoplay seeks to the live point after tech fires loadedmetadata in ie11', +function(assert) { + videojs.browser.IE_VERSION = 11; + let currentTime = 0; + + this.player.autoplay(true); + this.player.on('seeking', () => { + currentTime = this.player.currentTime(); + }); + this.player.src({ + src: 'liveStart30sBefore.m3u8', + type: 'application/vnd.apple.mpegurl' + }); + + this.clock.tick(1); + + openMediaSource(this.player, this.clock); + this.player.tech_.trigger('play'); + this.standardXHRResponse(this.requests.shift()); + this.clock.tick(1); + + assert.equal(currentTime, 0, 'have not played yet'); + + this.player.tech_.trigger('loadedmetadata'); + this.clock.tick(1); + + assert.notEqual(currentTime, 0, 'seeked after tech is ready'); +}); + +QUnit.test('duration is set when the source opens after the playlist is loaded', +function(assert) { this.player.src({ src: 'media.m3u8', type: 'application/vnd.apple.mpegurl' From 310d06d58beb1338502b5b75c527a9dd50a2529b Mon Sep 17 00:00:00 2001 From: Matthew Neil Date: Wed, 4 Oct 2017 17:03:29 -0400 Subject: [PATCH 3/3] make setupFirstPlay more readable --- src/master-playlist-controller.js | 83 ++++++++++++++----------------- 1 file changed, 38 insertions(+), 45 deletions(-) diff --git a/src/master-playlist-controller.js b/src/master-playlist-controller.js index 7dad6f2f0..5fc84df40 100644 --- a/src/master-playlist-controller.js +++ b/src/master-playlist-controller.js @@ -686,58 +686,51 @@ export class MasterPlaylistController extends videojs.EventTarget { * player and video are loaded and initialized. */ setupFirstPlay() { - let seekable; let media = this.masterPlaylistLoader_.media(); - // check that everything is ready to begin buffering in the live - // scenario - // 1) the active media playlist is available - if (media && - // 2) the player is not paused - !this.tech_.paused() && - // 3) the player has not started playing - !this.hasPlayed_()) { - - let setHasPlayed = false; - - // when the video is a live stream - if (!media.endList) { - this.trigger('firstplay'); - - // seek to the latest media position for live videos - seekable = this.seekable(); - if (seekable.length) { - if (videojs.browser.IE_VERSION && - this.mode_ === 'html5' && - this.tech_.readyState() === 0) { - // IE11 throws an InvalidStateError if you try to set currentTime while the - // readyState is 0, so it must be delayed. - this.tech_.one('loadedmetadata', () => { - // we hold off on setting hasPlayed on true until after we seek to prevent - // segment loaders from requesting more than 1 segment before the seek to - // live point - this.tech_.setCurrentTime(seekable.end(0)); - this.hasPlayed_ = () => true; - }); - } else { - setHasPlayed = true; - this.tech_.setCurrentTime(seekable.end(0)); - } - } - } else { - // always set hasPlayed to true for first play of VOD - setHasPlayed = true; + // Check that everything is ready to begin buffering for the first call to play + // If 1) there is no active media + // 2) the player is paused + // 3) the first play has already been setup + // then exit early + if (!media || this.tech_.paused() || this.hasPlayed_()) { + return false; + } + + // when the video is a live stream + if (!media.endList) { + const seekable = this.seekable(); + + if (!seekable.length) { + // without a seekable range, the player cannot seek to begin buffering at the live + // point + return false; } - if (setHasPlayed) { - this.hasPlayed_ = () => true; + if (videojs.browser.IE_VERSION && + this.mode_ === 'html5' && + this.tech_.readyState() === 0) { + // IE11 throws an InvalidStateError if you try to set currentTime while the + // readyState is 0, so it must be delayed until the tech fires loadedmetadata. + this.tech_.one('loadedmetadata', () => { + this.trigger('firstplay'); + this.tech_.setCurrentTime(seekable.end(0)); + this.hasPlayed_ = () => true; + }); + + return false; } - // now that we are ready, load the segment - this.load(); - return true; + // trigger firstplay to inform the source handler to ignore the next seek event + this.trigger('firstplay'); + // seek to the live point + this.tech_.setCurrentTime(seekable.end(0)); } - return false; + + this.hasPlayed_ = () => true; + // we can begin loading now that everything is ready + this.load(); + return true; } /**