d74b33c8 by David LaPalomento

Implement seekable

Override the Flash tech's seekable method to take into account live playlists.
1 parent c18e3df7
......@@ -34,6 +34,7 @@ module.exports = function(grunt) {
'src/segment-parser.js',
'src/m3u8/m3u8-parser.js',
'src/xhr.js',
'src/playlist.js',
'src/playlist-loader.js',
'node_modules/pkcs7/dist/pkcs7.unpad.js',
'src/decrypter.js'
......
......@@ -28,13 +28,14 @@
<!-- m3u8 handling -->
<script src="src/m3u8/m3u8-parser.js"></script>
<script src="src/playlist.js"></script>
<script src="src/playlist-loader.js"></script>
<script src="node_modules/pkcs7/dist/pkcs7.unpad.js"></script>
<script src="src/decrypter.js"></script>
<script src="src/bin-utils.js"></script>
<!-- example MPEG2-TS segments -->
<!-- bipbop -->
<!-- <script src="test/tsSegment.js"></script> -->
......
/**
* Playlist related utilities.
*/
(function(window, videojs) {
'use strict';
var DEFAULT_TARGET_DURATION = 10;
var duration, seekable, segmentsDuration;
/**
* Calculate the media duration from the segments associated with a
* playlist. The duration of a subinterval of the available segments
* may be calculated by specifying a start and end index. The
* minimum recommended live buffer is automatically subtracted for
* the last segments of live playlists.
* @param playlist {object} a media playlist object
* @param startIndex {number} (optional) an inclusive lower
* boundary for the playlist. Defaults to 0.
* @param endIndex {number} (optional) an exclusive upper boundary
* for the playlist. Defaults to playlist length.
* @return {number} the duration between the start index and end
* index.
*/
segmentsDuration = function(playlist, startIndex, endIndex) {
var targetDuration, i, segment, result = 0;
startIndex = startIndex || 0;
endIndex = endIndex !== undefined ? endIndex : (playlist.segments || []).length;
targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION;
for (i = endIndex - 1; i >= startIndex; i--) {
segment = playlist.segments[i];
result += segment.preciseDuration ||
segment.duration ||
targetDuration;
}
// live playlists should not expose three segment durations worth
// of content from the end of the playlist
// https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-6.3.3
if (!playlist.endList) {
result -= targetDuration * (3 - (playlist.segments.length - endIndex));
}
return result;
};
/**
* Calculates the duration of a playlist. If a start and end index
* are specified, the duration will be for the subset of the media
* timeline between those two indices. The total duration for live
* playlists is always Infinity.
* @param playlist {object} a media playlist object
* @param startIndex {number} (optional) an inclusive lower
* boundary for the playlist. Defaults to 0.
* @param endIndex {number} (optional) an exclusive upper boundary
* for the playlist. Defaults to playlist length.
* @return {number} the duration between the start index and end
* index.
*/
duration = function(playlist, startIndex, endIndex) {
if (!playlist) {
return 0;
}
// if a slice of the total duration is not requested, use
// playlist-level duration indicators when they're present
if (startIndex === undefined && endIndex === undefined) {
// if present, use the duration specified in the playlist
if (playlist.totalDuration) {
return playlist.totalDuration;
}
// duration should be Infinity for live playlists
if (!playlist.endList) {
return window.Infinity;
}
}
// calculate the total duration based on the segment durations
return segmentsDuration(playlist,
startIndex,
endIndex);
};
/**
* Calculates the interval of time that is currently seekable in a
* playlist.
* @param playlist {object} a media playlist object
* @return {TimeRanges} the periods of time that are valid targets
* for seeking
*/
seekable = function(playlist) {
var startOffset, targetDuration;
// without segments, there are no seekable ranges
if (!playlist.segments) {
return videojs.createTimeRange();
}
// when the playlist is complete, the entire duration is seekable
if (playlist.endList) {
return videojs.createTimeRange(0, duration(playlist));
}
targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION;
startOffset = targetDuration * (playlist.mediaSequence || 0);
return videojs.createTimeRange(startOffset,
startOffset + segmentsDuration(playlist));
};
// exports
videojs.Hls.Playlist = {
duration: duration,
seekable: seekable
};
})(window, window.videojs);
......@@ -350,18 +350,25 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) {
videojs.Hls.prototype.duration = function() {
var playlists = this.playlists;
if (playlists) {
return videojs.Hls.getPlaylistTotalDuration(playlists.media());
return videojs.Hls.Playlist.duration(playlists.media());
}
return 0;
};
videojs.Hls.prototype.seekable = function() {
if (this.playlists) {
return videojs.Hls.Playlist.seekable(this.playlists.media());
}
return videojs.createTimeRange();
};
/**
* Update the player duration
*/
videojs.Hls.prototype.updateDuration = function(playlist) {
var player = this.player(),
oldDuration = player.duration(),
newDuration = videojs.Hls.getPlaylistTotalDuration(playlist);
newDuration = videojs.Hls.Playlist.duration(playlist);
// if the duration has changed, invalidate the cached value
if (oldDuration !== newDuration) {
......@@ -789,7 +796,7 @@ videojs.Hls.prototype.drainBuffer = function(event) {
}
event = event || {};
segmentOffset = videojs.Hls.getPlaylistDuration(playlist, 0, mediaIndex) * 1000;
segmentOffset = videojs.Hls.Playlist.duration(playlist, 0, mediaIndex) * 1000;
// transmux the segment data from MP2T to FLV
this.segmentParser_.parseSegmentBinaryData(bytes);
......@@ -976,20 +983,9 @@ videojs.Hls.canPlaySource = function(srcObj) {
* @return {number} the duration between the start index and end index.
*/
videojs.Hls.getPlaylistDuration = function(playlist, startIndex, endIndex) {
var dur = 0,
segment,
i;
startIndex = startIndex || 0;
endIndex = endIndex !== undefined ? endIndex : (playlist.segments || []).length;
i = endIndex - 1;
for (; i >= startIndex; i--) {
segment = playlist.segments[i];
dur += segment.preciseDuration || segment.duration || playlist.targetDuration || 0;
}
return dur;
videojs.log.warn('videojs.Hls.getPlaylistDuration is deprecated. ' +
'Use videojs.Hls.Playlist.duration instead');
return videojs.Hls.Playlist.duration(playlist, startIndex, endIndex);
};
/**
......@@ -998,21 +994,9 @@ videojs.Hls.getPlaylistDuration = function(playlist, startIndex, endIndex) {
* @return {number} the currently known duration, in seconds
*/
videojs.Hls.getPlaylistTotalDuration = function(playlist) {
if (!playlist) {
return 0;
}
// if present, use the duration specified in the playlist
if (playlist.totalDuration) {
return playlist.totalDuration;
}
// duration should be Infinity for live playlists
if (!playlist.endList) {
return window.Infinity;
}
return videojs.Hls.getPlaylistDuration(playlist);
videojs.log.warn('videojs.Hls.getPlaylistTotalDuration is deprecated. ' +
'Use videojs.Hls.Playlist.duration instead');
return videojs.Hls.Playlist.duration(playlist);
};
/**
......
......@@ -90,6 +90,7 @@ module.exports = function(config) {
'../src/segment-parser.js',
'../src/m3u8/m3u8-parser.js',
'../src/xhr.js',
'../src/playlist.js',
'../src/playlist-loader.js',
'../src/decrypter.js',
'../tmp/manifests.js',
......
......@@ -54,6 +54,7 @@ module.exports = function(config) {
'../src/segment-parser.js',
'../src/m3u8/m3u8-parser.js',
'../src/xhr.js',
'../src/playlist.js',
'../src/playlist-loader.js',
'../src/decrypter.js',
'../tmp/manifests.js',
......
......@@ -597,4 +597,5 @@
'#EXT-X-ENDLIST'); // no newline
ok(loader.media().endList, 'flushed the final line of input');
});
})(window);
......
/* Tests for the playlist utilities */
(function(window, videojs) {
'use strict';
var Playlist = videojs.Hls.Playlist;
module('Playlist Utilities');
test('total duration for live playlists is Infinity', function() {
var duration = Playlist.duration({
segments: [{
duration: 4,
uri: '0.ts'
}]
});
equal(duration, Infinity, 'duration is infinity');
});
test('interval duration does not include upcoming live segments', function() {
var duration = Playlist.duration({
segments: [{
duration: 4,
uri: '0.ts'
}, {
duration: 10,
uri: '1.ts'
}, {
duration: 10,
uri: '2.ts'
}, {
duration: 10,
uri: '3.ts'
}]
}, 0, 3);
equal(duration, 4, 'does not include upcoming live segments');
});
test('calculates seekable time ranges from the available segments', function() {
var playlist = {
segments: [{
duration: 10,
uri: '0.ts'
}, {
duration: 10,
uri: '1.ts'
}],
endList: true
}, seekable = Playlist.seekable(playlist);
equal(seekable.length, 1, 'there are seekable ranges');
equal(seekable.start(0), 0, 'starts at zero');
equal(seekable.end(0), Playlist.duration(playlist), 'ends at the duration');
});
test('master playlists have empty seekable ranges', function() {
var seekable = Playlist.seekable({
playlists: [{
uri: 'low.m3u8'
}, {
uri: 'high.m3u8'
}]
});
equal(seekable.length, 0, 'no seekable ranges from a master playlist');
});
test('seekable end is three target durations from the actual end of live playlists', function() {
var seekable = Playlist.seekable({
segments: [{
duration: 7,
uri: '0.ts'
}, {
duration: 10,
uri: '1.ts'
}, {
duration: 10,
uri: '2.ts'
}, {
duration: 10,
uri: '3.ts'
}]
});
equal(seekable.length, 1, 'there are seekable ranges');
equal(seekable.start(0), 0, 'starts at zero');
equal(seekable.end(0), 7, 'ends three target durations from the last segment');
});
test('adjusts seekable to the live playlist window', function() {
var seekable = Playlist.seekable({
targetDuration: 10,
mediaSequence: 7,
segments: [{
uri: '8.ts'
}, {
uri: '9.ts'
}, {
uri: '10.ts'
}, {
uri: '11.ts'
}]
});
equal(seekable.length, 1, 'there are seekable ranges');
equal(seekable.start(0), 10 * 7, 'starts at the earliest available segment');
equal(seekable.end(0), 10 * 8, 'ends three target durations from the last available segment');
});
test('seekable end accounts for non-standard target durations', function() {
var seekable = Playlist.seekable({
targetDuration: 2,
segments: [{
duration: 2,
uri: '0.ts'
}, {
duration: 2,
uri: '1.ts'
}, {
duration: 1,
uri: '2.ts'
}, {
duration: 2,
uri: '3.ts'
}, {
duration: 2,
uri: '4.ts'
}]
});
equal(seekable.start(0), 0, 'starts at the earliest available segment');
equal(seekable.end(0),
9 - (2 * 3),
'allows seeking no further than three target durations from the end');
});
})(window, window.videojs);
......@@ -32,6 +32,7 @@
<!-- M3U8 -->
<script src="../src/m3u8/m3u8-parser.js"></script>
<script src="../src/playlist.js"></script>
<script src="../src/playlist-loader.js"></script>
<script src="../node_modules/pkcs7/dist/pkcs7.unpad.js"></script>
<script src="../src/decrypter.js"></script>
......@@ -59,6 +60,7 @@
<script src="flv-tag_test.js"></script>
<script src="metadata-stream_test.js"></script>
<script src="m3u8_test.js"></script>
<script src="playlist_test.js"></script>
<script src="playlist-loader_test.js"></script>
<script src="decrypter_test.js"></script>
<script src="xhr_test.js"></script>
......
......@@ -1611,7 +1611,8 @@ test('clears the segment buffer on seek', function() {
'1.ts\n' +
'#EXT-X-DISCONTINUITY\n' +
'#EXTINF:10,0\n' +
'2.ts\n');
'2.ts\n' +
'#EXT-X-ENDLIST\n');
standardXHRResponse(requests.pop());
// play to 6s to trigger the next segment request
......@@ -1662,7 +1663,8 @@ test('continues playing after seek to discontinuity', function() {
'1.ts\n' +
'#EXT-X-DISCONTINUITY\n' +
'#EXTINF:10,0\n' +
'2.ts\n');
'2.ts\n' +
'#EXT-X-ENDLIST\n');
standardXHRResponse(requests.pop());
currentTime = 1;
......