008e3225 by David LaPalomento

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.
1 parent 7aabe429
...@@ -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
......