diff --git a/src/videojs-hls.js b/src/videojs-hls.js index 0c215c08b..6ef9a0de7 100644 --- a/src/videojs-hls.js +++ b/src/videojs-hls.js @@ -321,12 +321,21 @@ videojs.Hls.prototype.play = function() { this.mediaIndex = 0; } - // seek to the latest safe point in the media timeline when first - // playing live streams - if (this.duration() === Infinity && - this.playlists.media() && - !this.player().hasClass('vjs-has-started')) { - this.setCurrentTime(this.seekable().end(0)); + // we may need to seek to begin playing safely for live playlists + if (this.duration() === Infinity) { + + // if this is the first time we're playing the stream or we're + // ahead of the latest safe playback position, seek to the live + // point + if (!this.player().hasClass('vjs-has-started') || + this.currentTime() > this.seekable().end(0)) { + this.setCurrentTime(this.seekable().end(0)); + + } else if (this.currentTime() < this.seekable().start(0)) { + // if the viewer has paused and we fell out of the live window, + // seek forward to the earliest available position + this.setCurrentTime(this.seekable().start(0)); + } } // delegate back to the Flash implementation @@ -356,6 +365,13 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) { return 0; } + // clamp seeks to the available seekable time range + if (currentTime < this.seekable().start(0)) { + currentTime = this.seekable().start(0); + } else if (currentTime > this.seekable().end(0)) { + currentTime = this.seekable().end(0); + } + // save the seek target so currentTime can report it correctly // while the seek is pending this.lastSeekedTime_ = currentTime; diff --git a/test/videojs-hls_test.js b/test/videojs-hls_test.js index beb95ffac..c865bb010 100644 --- a/test/videojs-hls_test.js +++ b/test/videojs-hls_test.js @@ -1686,6 +1686,87 @@ test('live playlist starts with correct currentTime value', function() { 'currentTime is updated at playback'); }); +test('resets the time to a seekable position when resuming a live stream ' + + 'after a long break', function() { + var seekTarget; + player.src({ + src: 'live0.m3u8', + type: 'application/vnd.apple.mpegurl' + }); + openMediaSource(player); + requests.shift().respond(200, null, + '#EXTM3U\n' + + '#EXT-X-MEDIA-SEQUENCE:16\n' + + '#EXTINF:10,\n' + + '16.ts\n'); + // mock out the player to simulate a live stream that has been + // playing for awhile + player.addClass('vjs-has-started'); + player.hls.seekable = function() { + return { + start: function() { + return 160; + }, + end: function() { + return 170; + } + }; + }; + player.hls.currentTime = function() { + return 0; + }; + player.hls.setCurrentTime = function(time) { + if (time !== undefined) { + seekTarget = time; + } + }; + + player.play(); + equal(seekTarget, player.seekable().start(0), 'seeked to the start of seekable'); + + player.hls.currentTime = function() { + return 180; + }; + player.play(); + equal(seekTarget, player.seekable().end(0), 'seeked to the end of seekable'); +}); + +test('clamps seeks to the seekable window', function() { + var seekTarget; + player.src({ + src: 'live0.m3u8', + type: 'application/vnd.apple.mpegurl' + }); + openMediaSource(player); + requests.shift().respond(200, null, + '#EXTM3U\n' + + '#EXT-X-MEDIA-SEQUENCE:16\n' + + '#EXTINF:10,\n' + + '16.ts\n'); + // mock out a seekable window + player.hls.seekable = function() { + return { + start: function() { + return 160; + }, + end: function() { + return 170; + } + }; + }; + player.hls.fillBuffer = function(time) { + if (time !== undefined) { + seekTarget = time; + } + }; + + player.currentTime(180); + equal(seekTarget * 0.001, player.seekable().end(0), 'forward seeks are clamped'); + + player.currentTime(45); + equal(seekTarget * 0.001, player.seekable().start(0), 'backward seeks are clamped'); +}); + test('mediaIndex is zero before the first segment loads', function() { window.manifests['first-seg-load'] = '#EXTM3U\n' +