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({
options.source = source;
this.bytesReceived = 0;
this.hasPlayed_ = false;
this.on(player, 'loadstart', function() {
this.hasPlayed_ = false;
this.one(this.mediaSource, 'sourceopen', this.setupFirstPlay);
});
this.on(player, ['play', 'loadedmetadata'], this.setupFirstPlay);
// TODO: After video.js#1347 is pulled in remove these lines
this.currentTime = videojs.Hls.prototype.currentTime;
this.setCurrentTime = videojs.Hls.prototype.setCurrentTime;
......@@ -109,12 +117,7 @@ videojs.Hls.prototype.src = function(src) {
this.playlists.on('loadedmetadata', videojs.bind(this, function() {
var selectedPlaylist, loaderHandler, oldBitrate, newBitrate, segmentDuration,
segmentDlTime, setupEvents, threshold;
setupEvents = function() {
this.fillBuffer();
player.trigger('loadedmetadata');
};
segmentDlTime, threshold;
oldMediaPlaylist = this.playlists.media();
......@@ -155,12 +158,16 @@ videojs.Hls.prototype.src = function(src) {
if (newBitrate > oldBitrate && segmentDlTime <= threshold) {
this.playlists.media(selectedPlaylist);
loaderHandler = videojs.bind(this, function() {
setupEvents.call(this);
this.setupFirstPlay();
this.fillBuffer();
player.trigger('loadedmetadata');
this.playlists.off('loadedplaylist', loaderHandler);
});
this.playlists.on('loadedplaylist', loaderHandler);
} else {
setupEvents.call(this);
this.setupFirstPlay();
this.fillBuffer();
player.trigger('loadedmetadata');
}
}));
......@@ -329,6 +336,33 @@ videojs.Hls.prototype.addCuesForMetadata_ = function(segmentInfo) {
};
/**
* Seek to the latest media position if this is a live video and the
* player and video are loaded and initialized.
*/
videojs.Hls.prototype.setupFirstPlay = function() {
var seekable, media;
media = this.playlists.media();
// check that everything is ready to begin buffering
if (!this.hasPlayed_ &&
this.sourceBuffer &&
media &&
this.paused() === false) {
// only run this block once per video
this.hasPlayed_ = true;
if (this.duration() === Infinity) {
// seek to the latest media position for live videos
seekable = this.seekable();
if (seekable.length) {
this.setCurrentTime(seekable.end(0));
}
}
}
};
/**
* Reset the mediaIndex if play() is called after the video has
* ended.
*/
......@@ -337,25 +371,20 @@ videojs.Hls.prototype.play = function() {
this.mediaIndex = 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));
if (!this.hasPlayed_) {
videojs.Flash.prototype.play.apply(this, arguments);
return this.setupFirstPlay();
}
} 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));
}
// if the viewer has paused and we fell out of the live window,
// seek forward to the earliest available position
if (this.duration() === Infinity &&
this.currentTime() < this.seekable().start(0)) {
this.setCurrentTime(this.seekable().start(0));
}
// delegate back to the Flash implementation
return videojs.Flash.prototype.play.apply(this, arguments);
videojs.Flash.prototype.play.apply(this, arguments);
};
videojs.Hls.prototype.currentTime = function() {
......@@ -396,7 +425,9 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) {
this.mediaIndex = this.playlists.getMediaIndexForTime_(currentTime);
// abort any segments still being decoded
this.sourceBuffer.abort();
if (this.sourceBuffer) {
this.sourceBuffer.abort();
}
// cancel outstanding requests and buffer appends
this.cancelSegmentXhr();
......@@ -436,6 +467,10 @@ videojs.Hls.prototype.seekable = function() {
// report the seekable range relative to the earliest possible
// position when the stream was first loaded
currentSeekable = videojs.Hls.Playlist.seekable(media);
if (!currentSeekable.length) {
return currentSeekable;
}
startOffset = this.playlists.expiredPostDiscontinuity_ - this.playlists.expiredPreDiscontinuity_;
return videojs.createTimeRange(startOffset,
startOffset + (currentSeekable.end(0) - currentSeekable.start(0)));
......@@ -679,7 +714,7 @@ videojs.Hls.prototype.fillBuffer = function(offset) {
// being buffering so we don't preload data that will never be
// played
if (!this.playlists.media().endList &&
!this.player().hasClass('vjs-has-started') &&
!player.hasClass('vjs-has-started') &&
offset === undefined) {
return;
}
......@@ -920,22 +955,24 @@ videojs.Hls.prototype.drainBuffer = function(event) {
// FLV tags until we find the one that is closest to the desired
// playback time
if (typeof offset === 'number') {
// determine the offset within this segment we're seeking to
segmentOffset = this.playlists.expiredPostDiscontinuity_ + this.playlists.expiredPreDiscontinuity_;
segmentOffset += videojs.Hls.Playlist.duration(playlist,
playlist.mediaSequence,
playlist.mediaSequence + mediaIndex);
segmentOffset = offset - (segmentOffset * 1000);
ptsTime = segmentOffset + tags[0].pts;
while (tags[i + 1] && tags[i].pts < ptsTime) {
i++;
}
if (tags.length) {
// determine the offset within this segment we're seeking to
segmentOffset = this.playlists.expiredPostDiscontinuity_ + this.playlists.expiredPreDiscontinuity_;
segmentOffset += videojs.Hls.Playlist.duration(playlist,
playlist.mediaSequence,
playlist.mediaSequence + mediaIndex);
segmentOffset = offset - (segmentOffset * 1000);
ptsTime = segmentOffset + tags[0].pts;
while (tags[i + 1] && tags[i].pts < ptsTime) {
i++;
}
// tell the SWF the media position of the first tag we'll be delivering
this.el().vjs_setProperty('currentTime', ((tags[i].pts - ptsTime + offset) * 0.001));
// tell the SWF the media position of the first tag we'll be delivering
this.el().vjs_setProperty('currentTime', ((tags[i].pts - ptsTime + offset) * 0.001));
tags = tags.slice(i);
tags = tags.slice(i);
}
this.lastSeekedTime_ = null;
}
......
......@@ -50,10 +50,19 @@ var
};
tech = player.el().querySelector('.vjs-tech');
tech.vjs_getProperty = function() {};
tech.vjs_getProperty = function(name) {
if (name === 'paused') {
return this.paused_;
}
};
tech.vjs_setProperty = function() {};
tech.vjs_src = function() {};
tech.vjs_play = function() {};
tech.vjs_play = function() {
this.paused_ = false;
};
tech.vjs_pause = function() {
this.paused_ = true;
};
tech.vjs_discontinuity = function() {};
videojs.Flash.onReady(tech.id);
......@@ -226,6 +235,46 @@ test('starts playing if autoplay is specified', function() {
strictEqual(1, plays, 'play was called');
});
test('autoplay seeks to the live point after playlist load', function() {
var currentTime = 0;
player.options().autoplay = true;
player.hls.setCurrentTime = function(time) {
currentTime = time;
return currentTime;
};
player.hls.currentTime = function() {
return currentTime;
};
player.src({
src: 'liveStart30sBefore.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
standardXHRResponse(requests.shift());
notEqual(currentTime, 0, 'seeked on autoplay');
});
test('autoplay seeks to the live point after media source open', function() {
var currentTime = 0;
player.options().autoplay = true;
player.hls.setCurrentTime = function(time) {
currentTime = time;
return currentTime;
};
player.hls.currentTime = function() {
return currentTime;
};
player.src({
src: 'liveStart30sBefore.m3u8',
type: 'application/vnd.apple.mpegurl'
});
standardXHRResponse(requests.shift());
openMediaSource(player);
notEqual(currentTime, 0, 'seeked on autoplay');
});
test('creates a PlaylistLoader on init', function() {
var loadedmetadata = false;
player.on('loadedmetadata', function() {
......@@ -1699,6 +1748,7 @@ test('live playlist starts three target durations before live', function() {
equal(player.hls.mediaIndex, 0, 'waits for the first play to start buffering');
equal(requests.length, 0, 'no outstanding segment request');
player.hls.paused = function() { return false; };
player.play();
mediaPlaylist = player.hls.playlists.media();
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 ' +
'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.hasPlayed_ = true;
player.hls.seekable = function() {
return {
start: function() {
......@@ -1766,7 +1816,8 @@ test('resets the time to a seekable position when resuming a live stream ' +
},
end: function() {
return 170;
}
},
length: 1
};
};
player.hls.currentTime = function() {
......@@ -1780,12 +1831,6 @@ test('resets the time to a seekable position when resuming a live stream ' +
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() {
......@@ -2015,6 +2060,18 @@ test('clears the segment buffer on seek', function() {
strictEqual(aborts, 1, 'cleared the segment buffer on a seek');
});
test('can seek before the source buffer opens', function() {
player.src({
src: 'media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
standardXHRResponse(requests.shift());
player.triggerReady();
player.currentTime(1);
equal(player.currentTime(), 1, 'seeked');
});
test('continues playing after seek to discontinuity', function() {
var aborts = 0, tags = [], currentTime, bufferEnd, oldCurrentTime;
......