Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add allowSeeksWithinUnsafeLiveWindow property #320

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Video.js Compatibility: 6.0, 7.0
- [enableLowInitialPlaylist](#enablelowinitialplaylist)
- [limitRenditionByPlayerDimensions](#limitrenditionbyplayerdimensions)
- [smoothQualityChange](#smoothqualitychange)
- [allowSeeksWithinUnsafeLiveWindow](#allowSeeksWithinUnsafeLiveWindow)
- [Runtime Properties](#runtime-properties)
- [hls.playlists.master](#hlsplaylistsmaster)
- [hls.playlists.media](#hlsplaylistsmedia)
Expand Down Expand Up @@ -374,6 +375,25 @@ Note that this _only_ affects quality changes triggered via the representations
API; automatic quality switches based on available bandwidth will always be
smooth switches.

##### allowSeeksWithinUnsafeLiveWindow
* Type: `boolean`
* can be used as a source option

When `allowSeeksWithinUnsafeLiveWindow` is set to `true`, if the active playlist is live
and a seek is made to a time between the safe live point (end of manifest minus three
times the target duration,
see [the hls spec](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-6.3.3)
for details) and the end of the playlist, the seek is allowed, rather than corrected to
the safe live point.

This option can help in instances where the live stream's target duration is greater than
the segment durations, playback ends up in the unsafe live window, and there are gaps in
the content. In this case the player will attempt to seek past the gaps but end up seeking
inside of the unsafe range, leading to a correction and seek back into a previously played
content.

The property defaults to `false`.

### Runtime Properties
Runtime properties are attached to the tech object when HLS is in
use. You can get a reference to the HLS source handler like this:
Expand Down
29 changes: 25 additions & 4 deletions src/playback-watcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export default class PlaybackWatcher {
this.tech_ = options.tech;
this.seekable = options.seekable;
this.seekTo = options.seekTo;
this.allowSeeksWithinUnsafeLiveWindow = options.allowSeeksWithinUnsafeLiveWindow;
this.media = options.media;

this.consecutiveUpdates = 0;
this.lastRecordedTime = null;
Expand Down Expand Up @@ -153,18 +155,29 @@ export default class PlaybackWatcher {
*/
fixesBadSeeks_() {
const seeking = this.tech_.seeking();

if (!seeking) {
return false;
}

const seekable = this.seekable();
const currentTime = this.tech_.currentTime();
const isAfterSeekableRange = this.afterSeekableWindow_(
seekable,
currentTime,
this.media(),
this.allowSeeksWithinUnsafeLiveWindow
);
let seekTo;

if (seeking && this.afterSeekableWindow_(seekable, currentTime)) {
if (isAfterSeekableRange) {
const seekableEnd = seekable.end(seekable.length - 1);

// sync to live point (if VOD, our seekable was updated and we're simply adjusting)
seekTo = seekableEnd;
}

if (seeking && this.beforeSeekableWindow_(seekable, currentTime)) {
if (this.beforeSeekableWindow_(seekable, currentTime)) {
const seekableStart = seekable.start(0);

// sync to the beginning of the live window
Expand Down Expand Up @@ -290,13 +303,21 @@ export default class PlaybackWatcher {
return false;
}

afterSeekableWindow_(seekable, currentTime) {
afterSeekableWindow_(
seekable, currentTime, playlist, allowSeeksWithinUnsafeLiveWindow = false) {
if (!seekable.length) {
// we can't make a solid case if there's no seekable, default to false
return false;
}

if (currentTime > seekable.end(seekable.length - 1) + Ranges.SAFE_TIME_DELTA) {
let allowedEnd = seekable.end(seekable.length - 1) + Ranges.SAFE_TIME_DELTA;
const isLive = !playlist.endList;

if (isLive && allowSeeksWithinUnsafeLiveWindow) {
allowedEnd = seekable.end(seekable.length - 1) + (playlist.targetDuration * 3);
}

if (currentTime > allowedEnd) {
return true;
}

Expand Down
3 changes: 2 additions & 1 deletion src/videojs-http-streaming.js
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,8 @@ class HlsHandler extends Component {
this.masterPlaylistController_ = new MasterPlaylistController(this.options_);
this.playbackWatcher_ = new PlaybackWatcher(
videojs.mergeOptions(this.options_, {
seekable: () => this.seekable()
seekable: () => this.seekable(),
media: () => this.masterPlaylistController_.media()
}));

this.masterPlaylistController_.on('error', () => {
Expand Down
206 changes: 191 additions & 15 deletions test/playback-watcher.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,65 @@ QUnit.test('corrects seek outside of seekable', function(assert) {
assert.equal(seeks.length, 4, 'did not seek');
});

QUnit.test('corrected seeks respect allowSeeksWithinUnsafeLiveWindow flag',
function(assert) {
// set an arbitrary live source
this.player.src({
src: 'liveStart30sBefore.m3u8',
type: 'application/vnd.apple.mpegurl'
});

// start playback normally
this.player.tech_.triggerReady();
this.clock.tick(1);
standardXHRResponse(this.requests.shift());
openMediaSource(this.player, this.clock);
this.player.tech_.trigger('play');
this.player.tech_.trigger('playing');
this.clock.tick(1);

let playbackWatcher = this.player.tech_.hls.playbackWatcher_;
let seeks = [];
let seekable;
let seeking;
let currentTime;

playbackWatcher.seekable = () => seekable;
playbackWatcher.tech_ = {
off: () => {},
seeking: () => seeking,
currentTime: () => currentTime,
// mocked out
paused: () => false,
buffered: () => videojs.createTimeRanges()
};
this.player.vhs.setCurrentTime = (time) => seeks.push(time);

playbackWatcher.allowSeeksWithinUnsafeLiveWindow = true;

// waiting

seekable = videojs.createTimeRanges([[1, 45]]);
seeking = true;

// target duration of 10, seekable end of 45
// 45 + 3 * 10 = 75
currentTime = 75;
this.player.tech_.trigger('waiting');
assert.equal(seeks.length, 0, 'did not seek');

currentTime = 75.1;
this.player.tech_.trigger('waiting');
assert.equal(seeks.length, 1, 'seeked');
assert.equal(seeks[0], 45, 'player seeked to live point');

playbackWatcher.allowSeeksWithinUnsafeLiveWindow = true;

currentTime = 75;
this.player.tech_.trigger('waiting');
assert.equal(seeks.length, 1, 'did not seek');
});

QUnit.test('calls fixesBadSeeks_ on seekablechanged', function(assert) {
// set an arbitrary live source
this.player.src({
Expand Down Expand Up @@ -675,36 +734,153 @@ QUnit.test('detects live window falloff', function(assert) {
'true if current time is 0 and earlier than seekable range');
});

QUnit.test('detects beyond seekable window', function(assert) {
QUnit.test('detects beyond seekable window for VOD', function(assert) {
const playlist = {
endList: true,
targetDuration: 7
};
let afterSeekableWindow_ =
this.playbackWatcher.afterSeekableWindow_.bind(this.playbackWatcher);

assert.ok(
!afterSeekableWindow_(videojs.createTimeRanges([[11, 20]]), 10.8),
assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[11, 20]]), 10.8, playlist),
'false if before seekable range');
assert.ok(
afterSeekableWindow_(videojs.createTimeRanges([[11, 20]]), 20.2),
afterSeekableWindow_(videojs.createTimeRanges([[11, 20]]), 20.2, playlist),
'true if after seekable range');
assert.ok(
!afterSeekableWindow_(videojs.createTimeRanges([[11, 20]]), 10.9),
assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[11, 20]]), 10.9, playlist),
'false if within starting seekable range buffer');
assert.ok(
!afterSeekableWindow_(videojs.createTimeRanges([[11, 20]]), 20.1),
assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[11, 20]]), 20.1, playlist),
'false if within ending seekable range buffer');

assert.ok(
!afterSeekableWindow_(videojs.createTimeRanges(), 10),
assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges(), 10, playlist),
'false if no seekable range');
assert.ok(
!afterSeekableWindow_(videojs.createTimeRanges([[0, 10]]), -0.2),
assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[0, 10]]), -0.2, playlist),
'false if current time is negative');
assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[0, 10]]), 5, playlist),
'false if within seekable range');
assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[0, 10]]), 0, playlist),
'false if within seekable range');
assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[0, 10]]), 10, playlist),
'false if within seekable range');
});

QUnit.test('detects beyond seekable window for LIVE', function(assert) {
// no endList means live
const playlist = {
targetDuration: 7
};
let afterSeekableWindow_ =
this.playbackWatcher.afterSeekableWindow_.bind(this.playbackWatcher);

assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[11, 20]]), 10.8, playlist),
'false if before seekable range');
assert.ok(
!afterSeekableWindow_(videojs.createTimeRanges([[0, 10]]), 5),
afterSeekableWindow_(videojs.createTimeRanges([[11, 20]]), 20.2, playlist),
'true if after seekable range');
assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[11, 20]]), 10.9, playlist),
'false if within starting seekable range buffer');
assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[11, 20]]), 20.1, playlist),
'false if within ending seekable range buffer');

assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges(), 10, playlist),
'false if no seekable range');
assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[0, 10]]), -0.2, playlist),
'false if current time is negative');
assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[0, 10]]), 5, playlist),
'false if within seekable range');
assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[0, 10]]), 0, playlist),
'false if within seekable range');
assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[0, 10]]), 10, playlist),
'false if within seekable range');
});

QUnit.test('respects allowSeeksWithinUnsafeLiveWindow flag', function(assert) {
// no endList means live
const playlist = {
targetDuration: 7
};
let afterSeekableWindow_ =
this.playbackWatcher.afterSeekableWindow_.bind(this.playbackWatcher);

assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[11, 20]]), 10.8, playlist, true),
'false if before seekable range');
assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[11, 20]]), 20.2, playlist, true),
'false if after seekable range but within unsafe live window');
assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[11, 20]]), 40.9, playlist, true),
'false if after seekable range but within unsafe live window');
assert.ok(
!afterSeekableWindow_(videojs.createTimeRanges([[0, 10]]), 0),
afterSeekableWindow_(videojs.createTimeRanges([[11, 20]]), 41.1, playlist, true),
'true if after seekable range and unsafe live window');
assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[11, 20]]), 10.9, playlist, true),
'false if within starting seekable range buffer');
assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[11, 20]]), 20.1, playlist, true),
'false if within ending seekable range buffer');

assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges(), 10, playlist, true),
'false if no seekable range');
assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[0, 10]]), -0.2, playlist, true),
'false if current time is negative');
assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[0, 10]]), 5, playlist, true),
'false if within seekable range');
assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[0, 10]]), 0, playlist, true),
'false if within seekable range');
assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[0, 10]]), 10, playlist, true),
'false if within seekable range');

playlist.endList = true;

assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[11, 20]]), 10.8, playlist, true),
'false if before seekable range');
assert.ok(
!afterSeekableWindow_(videojs.createTimeRanges([[0, 10]]), 10),
afterSeekableWindow_(videojs.createTimeRanges([[11, 20]]), 20.2, playlist, true),
'true if after seekable range');
assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[11, 20]]), 10.9, playlist, true),
'false if within starting seekable range buffer');
assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[11, 20]]), 20.1, playlist, true),
'false if within ending seekable range buffer');

assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges(), 10, playlist, true),
'false if no seekable range');
assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[0, 10]]), -0.2, playlist, true),
'false if current time is negative');
assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[0, 10]]), 5, playlist, true),
'false if within seekable range');
assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[0, 10]]), 0, playlist, true),
'false if within seekable range');
assert.notOk(
afterSeekableWindow_(videojs.createTimeRanges([[0, 10]]), 10, playlist, true),
'false if within seekable range');
});