Wait for everything to be ready before seeking on first live play
Depending on the order the media source opened, the media playlist was recieved, and play was invoked, live streams may or may not have been seeked. Consolidate the logic that checks for this condition to ensure we always seek to the live point. Add a check so that if someone seeks before the media source opens, the player does not throw an exception. For #351.
Showing
2 changed files
with
143 additions
and
49 deletions
... | @@ -35,6 +35,14 @@ videojs.Hls = videojs.Flash.extend({ | ... | @@ -35,6 +35,14 @@ videojs.Hls = videojs.Flash.extend({ |
35 | options.source = source; | 35 | options.source = source; |
36 | this.bytesReceived = 0; | 36 | this.bytesReceived = 0; |
37 | 37 | ||
38 | this.hasPlayed_ = false; | ||
39 | this.on(player, 'loadstart', function() { | ||
40 | this.hasPlayed_ = false; | ||
41 | this.one(this.mediaSource, 'sourceopen', this.setupFirstPlay); | ||
42 | }); | ||
43 | this.on(player, ['play', 'loadedmetadata'], this.setupFirstPlay); | ||
44 | |||
45 | |||
38 | // TODO: After video.js#1347 is pulled in remove these lines | 46 | // TODO: After video.js#1347 is pulled in remove these lines |
39 | this.currentTime = videojs.Hls.prototype.currentTime; | 47 | this.currentTime = videojs.Hls.prototype.currentTime; |
40 | this.setCurrentTime = videojs.Hls.prototype.setCurrentTime; | 48 | this.setCurrentTime = videojs.Hls.prototype.setCurrentTime; |
... | @@ -109,12 +117,7 @@ videojs.Hls.prototype.src = function(src) { | ... | @@ -109,12 +117,7 @@ videojs.Hls.prototype.src = function(src) { |
109 | 117 | ||
110 | this.playlists.on('loadedmetadata', videojs.bind(this, function() { | 118 | this.playlists.on('loadedmetadata', videojs.bind(this, function() { |
111 | var selectedPlaylist, loaderHandler, oldBitrate, newBitrate, segmentDuration, | 119 | var selectedPlaylist, loaderHandler, oldBitrate, newBitrate, segmentDuration, |
112 | segmentDlTime, setupEvents, threshold; | 120 | segmentDlTime, threshold; |
113 | |||
114 | setupEvents = function() { | ||
115 | this.fillBuffer(); | ||
116 | player.trigger('loadedmetadata'); | ||
117 | }; | ||
118 | 121 | ||
119 | oldMediaPlaylist = this.playlists.media(); | 122 | oldMediaPlaylist = this.playlists.media(); |
120 | 123 | ||
... | @@ -155,12 +158,16 @@ videojs.Hls.prototype.src = function(src) { | ... | @@ -155,12 +158,16 @@ videojs.Hls.prototype.src = function(src) { |
155 | if (newBitrate > oldBitrate && segmentDlTime <= threshold) { | 158 | if (newBitrate > oldBitrate && segmentDlTime <= threshold) { |
156 | this.playlists.media(selectedPlaylist); | 159 | this.playlists.media(selectedPlaylist); |
157 | loaderHandler = videojs.bind(this, function() { | 160 | loaderHandler = videojs.bind(this, function() { |
158 | setupEvents.call(this); | 161 | this.setupFirstPlay(); |
162 | this.fillBuffer(); | ||
163 | player.trigger('loadedmetadata'); | ||
159 | this.playlists.off('loadedplaylist', loaderHandler); | 164 | this.playlists.off('loadedplaylist', loaderHandler); |
160 | }); | 165 | }); |
161 | this.playlists.on('loadedplaylist', loaderHandler); | 166 | this.playlists.on('loadedplaylist', loaderHandler); |
162 | } else { | 167 | } else { |
163 | setupEvents.call(this); | 168 | this.setupFirstPlay(); |
169 | this.fillBuffer(); | ||
170 | player.trigger('loadedmetadata'); | ||
164 | } | 171 | } |
165 | })); | 172 | })); |
166 | 173 | ||
... | @@ -329,6 +336,33 @@ videojs.Hls.prototype.addCuesForMetadata_ = function(segmentInfo) { | ... | @@ -329,6 +336,33 @@ videojs.Hls.prototype.addCuesForMetadata_ = function(segmentInfo) { |
329 | }; | 336 | }; |
330 | 337 | ||
331 | /** | 338 | /** |
339 | * Seek to the latest media position if this is a live video and the | ||
340 | * player and video are loaded and initialized. | ||
341 | */ | ||
342 | videojs.Hls.prototype.setupFirstPlay = function() { | ||
343 | var seekable, media; | ||
344 | media = this.playlists.media(); | ||
345 | |||
346 | // check that everything is ready to begin buffering | ||
347 | if (!this.hasPlayed_ && | ||
348 | this.sourceBuffer && | ||
349 | media && | ||
350 | this.paused() === false) { | ||
351 | |||
352 | // only run this block once per video | ||
353 | this.hasPlayed_ = true; | ||
354 | |||
355 | if (this.duration() === Infinity) { | ||
356 | // seek to the latest media position for live videos | ||
357 | seekable = this.seekable(); | ||
358 | if (seekable.length) { | ||
359 | this.setCurrentTime(seekable.end(0)); | ||
360 | } | ||
361 | } | ||
362 | } | ||
363 | }; | ||
364 | |||
365 | /** | ||
332 | * Reset the mediaIndex if play() is called after the video has | 366 | * Reset the mediaIndex if play() is called after the video has |
333 | * ended. | 367 | * ended. |
334 | */ | 368 | */ |
... | @@ -337,25 +371,20 @@ videojs.Hls.prototype.play = function() { | ... | @@ -337,25 +371,20 @@ videojs.Hls.prototype.play = function() { |
337 | this.mediaIndex = 0; | 371 | this.mediaIndex = 0; |
338 | } | 372 | } |
339 | 373 | ||
340 | // we may need to seek to begin playing safely for live playlists | 374 | if (!this.hasPlayed_) { |
341 | if (this.duration() === Infinity) { | 375 | videojs.Flash.prototype.play.apply(this, arguments); |
342 | 376 | return this.setupFirstPlay(); | |
343 | // if this is the first time we're playing the stream or we're | 377 | } |
344 | // ahead of the latest safe playback position, seek to the live | ||
345 | // point | ||
346 | if (!this.player().hasClass('vjs-has-started') || | ||
347 | this.currentTime() > this.seekable().end(0)) { | ||
348 | this.setCurrentTime(this.seekable().end(0)); | ||
349 | 378 | ||
350 | } else if (this.currentTime() < this.seekable().start(0)) { | 379 | // if the viewer has paused and we fell out of the live window, |
351 | // if the viewer has paused and we fell out of the live window, | 380 | // seek forward to the earliest available position |
352 | // seek forward to the earliest available position | 381 | if (this.duration() === Infinity && |
353 | this.setCurrentTime(this.seekable().start(0)); | 382 | this.currentTime() < this.seekable().start(0)) { |
354 | } | 383 | this.setCurrentTime(this.seekable().start(0)); |
355 | } | 384 | } |
356 | 385 | ||
357 | // delegate back to the Flash implementation | 386 | // delegate back to the Flash implementation |
358 | return videojs.Flash.prototype.play.apply(this, arguments); | 387 | videojs.Flash.prototype.play.apply(this, arguments); |
359 | }; | 388 | }; |
360 | 389 | ||
361 | videojs.Hls.prototype.currentTime = function() { | 390 | videojs.Hls.prototype.currentTime = function() { |
... | @@ -396,7 +425,9 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) { | ... | @@ -396,7 +425,9 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) { |
396 | this.mediaIndex = this.playlists.getMediaIndexForTime_(currentTime); | 425 | this.mediaIndex = this.playlists.getMediaIndexForTime_(currentTime); |
397 | 426 | ||
398 | // abort any segments still being decoded | 427 | // abort any segments still being decoded |
399 | this.sourceBuffer.abort(); | 428 | if (this.sourceBuffer) { |
429 | this.sourceBuffer.abort(); | ||
430 | } | ||
400 | 431 | ||
401 | // cancel outstanding requests and buffer appends | 432 | // cancel outstanding requests and buffer appends |
402 | this.cancelSegmentXhr(); | 433 | this.cancelSegmentXhr(); |
... | @@ -436,6 +467,10 @@ videojs.Hls.prototype.seekable = function() { | ... | @@ -436,6 +467,10 @@ videojs.Hls.prototype.seekable = function() { |
436 | // report the seekable range relative to the earliest possible | 467 | // report the seekable range relative to the earliest possible |
437 | // position when the stream was first loaded | 468 | // position when the stream was first loaded |
438 | currentSeekable = videojs.Hls.Playlist.seekable(media); | 469 | currentSeekable = videojs.Hls.Playlist.seekable(media); |
470 | if (!currentSeekable.length) { | ||
471 | return currentSeekable; | ||
472 | } | ||
473 | |||
439 | startOffset = this.playlists.expiredPostDiscontinuity_ - this.playlists.expiredPreDiscontinuity_; | 474 | startOffset = this.playlists.expiredPostDiscontinuity_ - this.playlists.expiredPreDiscontinuity_; |
440 | return videojs.createTimeRange(startOffset, | 475 | return videojs.createTimeRange(startOffset, |
441 | startOffset + (currentSeekable.end(0) - currentSeekable.start(0))); | 476 | startOffset + (currentSeekable.end(0) - currentSeekable.start(0))); |
... | @@ -679,7 +714,7 @@ videojs.Hls.prototype.fillBuffer = function(offset) { | ... | @@ -679,7 +714,7 @@ videojs.Hls.prototype.fillBuffer = function(offset) { |
679 | // being buffering so we don't preload data that will never be | 714 | // being buffering so we don't preload data that will never be |
680 | // played | 715 | // played |
681 | if (!this.playlists.media().endList && | 716 | if (!this.playlists.media().endList && |
682 | !this.player().hasClass('vjs-has-started') && | 717 | !player.hasClass('vjs-has-started') && |
683 | offset === undefined) { | 718 | offset === undefined) { |
684 | return; | 719 | return; |
685 | } | 720 | } |
... | @@ -920,22 +955,24 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -920,22 +955,24 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
920 | // FLV tags until we find the one that is closest to the desired | 955 | // FLV tags until we find the one that is closest to the desired |
921 | // playback time | 956 | // playback time |
922 | if (typeof offset === 'number') { | 957 | if (typeof offset === 'number') { |
923 | // determine the offset within this segment we're seeking to | 958 | if (tags.length) { |
924 | segmentOffset = this.playlists.expiredPostDiscontinuity_ + this.playlists.expiredPreDiscontinuity_; | 959 | // determine the offset within this segment we're seeking to |
925 | segmentOffset += videojs.Hls.Playlist.duration(playlist, | 960 | segmentOffset = this.playlists.expiredPostDiscontinuity_ + this.playlists.expiredPreDiscontinuity_; |
926 | playlist.mediaSequence, | 961 | segmentOffset += videojs.Hls.Playlist.duration(playlist, |
927 | playlist.mediaSequence + mediaIndex); | 962 | playlist.mediaSequence, |
928 | segmentOffset = offset - (segmentOffset * 1000); | 963 | playlist.mediaSequence + mediaIndex); |
929 | ptsTime = segmentOffset + tags[0].pts; | 964 | segmentOffset = offset - (segmentOffset * 1000); |
930 | 965 | ptsTime = segmentOffset + tags[0].pts; | |
931 | while (tags[i + 1] && tags[i].pts < ptsTime) { | 966 | |
932 | i++; | 967 | while (tags[i + 1] && tags[i].pts < ptsTime) { |
933 | } | 968 | i++; |
969 | } | ||
934 | 970 | ||
935 | // tell the SWF the media position of the first tag we'll be delivering | 971 | // tell the SWF the media position of the first tag we'll be delivering |
936 | this.el().vjs_setProperty('currentTime', ((tags[i].pts - ptsTime + offset) * 0.001)); | 972 | this.el().vjs_setProperty('currentTime', ((tags[i].pts - ptsTime + offset) * 0.001)); |
937 | 973 | ||
938 | tags = tags.slice(i); | 974 | tags = tags.slice(i); |
975 | } | ||
939 | 976 | ||
940 | this.lastSeekedTime_ = null; | 977 | this.lastSeekedTime_ = null; |
941 | } | 978 | } | ... | ... |
... | @@ -50,10 +50,19 @@ var | ... | @@ -50,10 +50,19 @@ var |
50 | }; | 50 | }; |
51 | 51 | ||
52 | tech = player.el().querySelector('.vjs-tech'); | 52 | tech = player.el().querySelector('.vjs-tech'); |
53 | tech.vjs_getProperty = function() {}; | 53 | tech.vjs_getProperty = function(name) { |
54 | if (name === 'paused') { | ||
55 | return this.paused_; | ||
56 | } | ||
57 | }; | ||
54 | tech.vjs_setProperty = function() {}; | 58 | tech.vjs_setProperty = function() {}; |
55 | tech.vjs_src = function() {}; | 59 | tech.vjs_src = function() {}; |
56 | tech.vjs_play = function() {}; | 60 | tech.vjs_play = function() { |
61 | this.paused_ = false; | ||
62 | }; | ||
63 | tech.vjs_pause = function() { | ||
64 | this.paused_ = true; | ||
65 | }; | ||
57 | tech.vjs_discontinuity = function() {}; | 66 | tech.vjs_discontinuity = function() {}; |
58 | videojs.Flash.onReady(tech.id); | 67 | videojs.Flash.onReady(tech.id); |
59 | 68 | ||
... | @@ -226,6 +235,46 @@ test('starts playing if autoplay is specified', function() { | ... | @@ -226,6 +235,46 @@ test('starts playing if autoplay is specified', function() { |
226 | strictEqual(1, plays, 'play was called'); | 235 | strictEqual(1, plays, 'play was called'); |
227 | }); | 236 | }); |
228 | 237 | ||
238 | test('autoplay seeks to the live point after playlist load', function() { | ||
239 | var currentTime = 0; | ||
240 | player.options().autoplay = true; | ||
241 | player.hls.setCurrentTime = function(time) { | ||
242 | currentTime = time; | ||
243 | return currentTime; | ||
244 | }; | ||
245 | player.hls.currentTime = function() { | ||
246 | return currentTime; | ||
247 | }; | ||
248 | player.src({ | ||
249 | src: 'liveStart30sBefore.m3u8', | ||
250 | type: 'application/vnd.apple.mpegurl' | ||
251 | }); | ||
252 | openMediaSource(player); | ||
253 | standardXHRResponse(requests.shift()); | ||
254 | |||
255 | notEqual(currentTime, 0, 'seeked on autoplay'); | ||
256 | }); | ||
257 | |||
258 | test('autoplay seeks to the live point after media source open', function() { | ||
259 | var currentTime = 0; | ||
260 | player.options().autoplay = true; | ||
261 | player.hls.setCurrentTime = function(time) { | ||
262 | currentTime = time; | ||
263 | return currentTime; | ||
264 | }; | ||
265 | player.hls.currentTime = function() { | ||
266 | return currentTime; | ||
267 | }; | ||
268 | player.src({ | ||
269 | src: 'liveStart30sBefore.m3u8', | ||
270 | type: 'application/vnd.apple.mpegurl' | ||
271 | }); | ||
272 | standardXHRResponse(requests.shift()); | ||
273 | openMediaSource(player); | ||
274 | |||
275 | notEqual(currentTime, 0, 'seeked on autoplay'); | ||
276 | }); | ||
277 | |||
229 | test('creates a PlaylistLoader on init', function() { | 278 | test('creates a PlaylistLoader on init', function() { |
230 | var loadedmetadata = false; | 279 | var loadedmetadata = false; |
231 | player.on('loadedmetadata', function() { | 280 | player.on('loadedmetadata', function() { |
... | @@ -1699,6 +1748,7 @@ test('live playlist starts three target durations before live', function() { | ... | @@ -1699,6 +1748,7 @@ test('live playlist starts three target durations before live', function() { |
1699 | equal(player.hls.mediaIndex, 0, 'waits for the first play to start buffering'); | 1748 | equal(player.hls.mediaIndex, 0, 'waits for the first play to start buffering'); |
1700 | equal(requests.length, 0, 'no outstanding segment request'); | 1749 | equal(requests.length, 0, 'no outstanding segment request'); |
1701 | 1750 | ||
1751 | player.hls.paused = function() { return false; }; | ||
1702 | player.play(); | 1752 | player.play(); |
1703 | mediaPlaylist = player.hls.playlists.media(); | 1753 | mediaPlaylist = player.hls.playlists.media(); |
1704 | equal(player.hls.mediaIndex, 1, 'mediaIndex is updated at play'); | 1754 | equal(player.hls.mediaIndex, 1, 'mediaIndex is updated at play'); |
... | @@ -1758,7 +1808,7 @@ test('resets the time to a seekable position when resuming a live stream ' + | ... | @@ -1758,7 +1808,7 @@ test('resets the time to a seekable position when resuming a live stream ' + |
1758 | '16.ts\n'); | 1808 | '16.ts\n'); |
1759 | // mock out the player to simulate a live stream that has been | 1809 | // mock out the player to simulate a live stream that has been |
1760 | // playing for awhile | 1810 | // playing for awhile |
1761 | player.addClass('vjs-has-started'); | 1811 | player.hls.hasPlayed_ = true; |
1762 | player.hls.seekable = function() { | 1812 | player.hls.seekable = function() { |
1763 | return { | 1813 | return { |
1764 | start: function() { | 1814 | start: function() { |
... | @@ -1766,7 +1816,8 @@ test('resets the time to a seekable position when resuming a live stream ' + | ... | @@ -1766,7 +1816,8 @@ test('resets the time to a seekable position when resuming a live stream ' + |
1766 | }, | 1816 | }, |
1767 | end: function() { | 1817 | end: function() { |
1768 | return 170; | 1818 | return 170; |
1769 | } | 1819 | }, |
1820 | length: 1 | ||
1770 | }; | 1821 | }; |
1771 | }; | 1822 | }; |
1772 | player.hls.currentTime = function() { | 1823 | player.hls.currentTime = function() { |
... | @@ -1780,12 +1831,6 @@ test('resets the time to a seekable position when resuming a live stream ' + | ... | @@ -1780,12 +1831,6 @@ test('resets the time to a seekable position when resuming a live stream ' + |
1780 | 1831 | ||
1781 | player.play(); | 1832 | player.play(); |
1782 | equal(seekTarget, player.seekable().start(0), 'seeked to the start of seekable'); | 1833 | equal(seekTarget, player.seekable().start(0), 'seeked to the start of seekable'); |
1783 | |||
1784 | player.hls.currentTime = function() { | ||
1785 | return 180; | ||
1786 | }; | ||
1787 | player.play(); | ||
1788 | equal(seekTarget, player.seekable().end(0), 'seeked to the end of seekable'); | ||
1789 | }); | 1834 | }); |
1790 | 1835 | ||
1791 | test('clamps seeks to the seekable window', function() { | 1836 | test('clamps seeks to the seekable window', function() { |
... | @@ -2015,6 +2060,18 @@ test('clears the segment buffer on seek', function() { | ... | @@ -2015,6 +2060,18 @@ test('clears the segment buffer on seek', function() { |
2015 | strictEqual(aborts, 1, 'cleared the segment buffer on a seek'); | 2060 | strictEqual(aborts, 1, 'cleared the segment buffer on a seek'); |
2016 | }); | 2061 | }); |
2017 | 2062 | ||
2063 | test('can seek before the source buffer opens', function() { | ||
2064 | player.src({ | ||
2065 | src: 'media.m3u8', | ||
2066 | type: 'application/vnd.apple.mpegurl' | ||
2067 | }); | ||
2068 | standardXHRResponse(requests.shift()); | ||
2069 | player.triggerReady(); | ||
2070 | |||
2071 | player.currentTime(1); | ||
2072 | equal(player.currentTime(), 1, 'seeked'); | ||
2073 | }); | ||
2074 | |||
2018 | test('continues playing after seek to discontinuity', function() { | 2075 | test('continues playing after seek to discontinuity', function() { |
2019 | var aborts = 0, tags = [], currentTime, bufferEnd, oldCurrentTime; | 2076 | var aborts = 0, tags = [], currentTime, bufferEnd, oldCurrentTime; |
2020 | 2077 | ... | ... |
-
Please register or sign in to post a comment