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) { ...@@ -34,6 +34,7 @@ module.exports = function(grunt) {
34 'src/segment-parser.js', 34 'src/segment-parser.js',
35 'src/m3u8/m3u8-parser.js', 35 'src/m3u8/m3u8-parser.js',
36 'src/xhr.js', 36 'src/xhr.js',
37 'src/playlist.js',
37 'src/playlist-loader.js', 38 'src/playlist-loader.js',
38 'node_modules/pkcs7/dist/pkcs7.unpad.js', 39 'node_modules/pkcs7/dist/pkcs7.unpad.js',
39 'src/decrypter.js' 40 'src/decrypter.js'
......
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
28 28
29 <!-- m3u8 handling --> 29 <!-- m3u8 handling -->
30 <script src="src/m3u8/m3u8-parser.js"></script> 30 <script src="src/m3u8/m3u8-parser.js"></script>
31 <script src="src/playlist.js"></script>
31 <script src="src/playlist-loader.js"></script> 32 <script src="src/playlist-loader.js"></script>
32 33
33 <script src="node_modules/pkcs7/dist/pkcs7.unpad.js"></script> 34 <script src="node_modules/pkcs7/dist/pkcs7.unpad.js"></script>
......
1 /**
2 * Playlist related utilities.
3 */
4 (function(window, videojs) {
5 'use strict';
6
7 var DEFAULT_TARGET_DURATION = 10;
8 var duration, seekable, segmentsDuration;
9
10 /**
11 * Calculate the media duration from the segments associated with a
12 * playlist. The duration of a subinterval of the available segments
13 * may be calculated by specifying a start and end index. The
14 * minimum recommended live buffer is automatically subtracted for
15 * the last segments of live playlists.
16 * @param playlist {object} a media playlist object
17 * @param startIndex {number} (optional) an inclusive lower
18 * boundary for the playlist. Defaults to 0.
19 * @param endIndex {number} (optional) an exclusive upper boundary
20 * for the playlist. Defaults to playlist length.
21 * @return {number} the duration between the start index and end
22 * index.
23 */
24 segmentsDuration = function(playlist, startIndex, endIndex) {
25 var targetDuration, i, segment, result = 0;
26
27 startIndex = startIndex || 0;
28 endIndex = endIndex !== undefined ? endIndex : (playlist.segments || []).length;
29 targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION;
30
31 for (i = endIndex - 1; i >= startIndex; i--) {
32 segment = playlist.segments[i];
33 result += segment.preciseDuration ||
34 segment.duration ||
35 targetDuration;
36 }
37
38 // live playlists should not expose three segment durations worth
39 // of content from the end of the playlist
40 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-6.3.3
41 if (!playlist.endList) {
42 result -= targetDuration * (3 - (playlist.segments.length - endIndex));
43 }
44
45 return result;
46 };
47
48 /**
49 * Calculates the duration of a playlist. If a start and end index
50 * are specified, the duration will be for the subset of the media
51 * timeline between those two indices. The total duration for live
52 * playlists is always Infinity.
53 * @param playlist {object} a media playlist object
54 * @param startIndex {number} (optional) an inclusive lower
55 * boundary for the playlist. Defaults to 0.
56 * @param endIndex {number} (optional) an exclusive upper boundary
57 * for the playlist. Defaults to playlist length.
58 * @return {number} the duration between the start index and end
59 * index.
60 */
61 duration = function(playlist, startIndex, endIndex) {
62 if (!playlist) {
63 return 0;
64 }
65
66 // if a slice of the total duration is not requested, use
67 // playlist-level duration indicators when they're present
68 if (startIndex === undefined && endIndex === undefined) {
69 // if present, use the duration specified in the playlist
70 if (playlist.totalDuration) {
71 return playlist.totalDuration;
72 }
73
74 // duration should be Infinity for live playlists
75 if (!playlist.endList) {
76 return window.Infinity;
77 }
78 }
79
80 // calculate the total duration based on the segment durations
81 return segmentsDuration(playlist,
82 startIndex,
83 endIndex);
84 };
85
86 /**
87 * Calculates the interval of time that is currently seekable in a
88 * playlist.
89 * @param playlist {object} a media playlist object
90 * @return {TimeRanges} the periods of time that are valid targets
91 * for seeking
92 */
93 seekable = function(playlist) {
94 var startOffset, targetDuration;
95 // without segments, there are no seekable ranges
96 if (!playlist.segments) {
97 return videojs.createTimeRange();
98 }
99 // when the playlist is complete, the entire duration is seekable
100 if (playlist.endList) {
101 return videojs.createTimeRange(0, duration(playlist));
102 }
103
104 targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION;
105 startOffset = targetDuration * (playlist.mediaSequence || 0);
106 return videojs.createTimeRange(startOffset,
107 startOffset + segmentsDuration(playlist));
108 };
109
110 // exports
111 videojs.Hls.Playlist = {
112 duration: duration,
113 seekable: seekable
114 };
115 })(window, window.videojs);
...@@ -350,18 +350,25 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) { ...@@ -350,18 +350,25 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) {
350 videojs.Hls.prototype.duration = function() { 350 videojs.Hls.prototype.duration = function() {
351 var playlists = this.playlists; 351 var playlists = this.playlists;
352 if (playlists) { 352 if (playlists) {
353 return videojs.Hls.getPlaylistTotalDuration(playlists.media()); 353 return videojs.Hls.Playlist.duration(playlists.media());
354 } 354 }
355 return 0; 355 return 0;
356 }; 356 };
357 357
358 videojs.Hls.prototype.seekable = function() {
359 if (this.playlists) {
360 return videojs.Hls.Playlist.seekable(this.playlists.media());
361 }
362 return videojs.createTimeRange();
363 };
364
358 /** 365 /**
359 * Update the player duration 366 * Update the player duration
360 */ 367 */
361 videojs.Hls.prototype.updateDuration = function(playlist) { 368 videojs.Hls.prototype.updateDuration = function(playlist) {
362 var player = this.player(), 369 var player = this.player(),
363 oldDuration = player.duration(), 370 oldDuration = player.duration(),
364 newDuration = videojs.Hls.getPlaylistTotalDuration(playlist); 371 newDuration = videojs.Hls.Playlist.duration(playlist);
365 372
366 // if the duration has changed, invalidate the cached value 373 // if the duration has changed, invalidate the cached value
367 if (oldDuration !== newDuration) { 374 if (oldDuration !== newDuration) {
...@@ -789,7 +796,7 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -789,7 +796,7 @@ videojs.Hls.prototype.drainBuffer = function(event) {
789 } 796 }
790 797
791 event = event || {}; 798 event = event || {};
792 segmentOffset = videojs.Hls.getPlaylistDuration(playlist, 0, mediaIndex) * 1000; 799 segmentOffset = videojs.Hls.Playlist.duration(playlist, 0, mediaIndex) * 1000;
793 800
794 // transmux the segment data from MP2T to FLV 801 // transmux the segment data from MP2T to FLV
795 this.segmentParser_.parseSegmentBinaryData(bytes); 802 this.segmentParser_.parseSegmentBinaryData(bytes);
...@@ -976,20 +983,9 @@ videojs.Hls.canPlaySource = function(srcObj) { ...@@ -976,20 +983,9 @@ videojs.Hls.canPlaySource = function(srcObj) {
976 * @return {number} the duration between the start index and end index. 983 * @return {number} the duration between the start index and end index.
977 */ 984 */
978 videojs.Hls.getPlaylistDuration = function(playlist, startIndex, endIndex) { 985 videojs.Hls.getPlaylistDuration = function(playlist, startIndex, endIndex) {
979 var dur = 0, 986 videojs.log.warn('videojs.Hls.getPlaylistDuration is deprecated. ' +
980 segment, 987 'Use videojs.Hls.Playlist.duration instead');
981 i; 988 return videojs.Hls.Playlist.duration(playlist, startIndex, endIndex);
982
983 startIndex = startIndex || 0;
984 endIndex = endIndex !== undefined ? endIndex : (playlist.segments || []).length;
985 i = endIndex - 1;
986
987 for (; i >= startIndex; i--) {
988 segment = playlist.segments[i];
989 dur += segment.preciseDuration || segment.duration || playlist.targetDuration || 0;
990 }
991
992 return dur;
993 }; 989 };
994 990
995 /** 991 /**
...@@ -998,21 +994,9 @@ videojs.Hls.getPlaylistDuration = function(playlist, startIndex, endIndex) { ...@@ -998,21 +994,9 @@ videojs.Hls.getPlaylistDuration = function(playlist, startIndex, endIndex) {
998 * @return {number} the currently known duration, in seconds 994 * @return {number} the currently known duration, in seconds
999 */ 995 */
1000 videojs.Hls.getPlaylistTotalDuration = function(playlist) { 996 videojs.Hls.getPlaylistTotalDuration = function(playlist) {
1001 if (!playlist) { 997 videojs.log.warn('videojs.Hls.getPlaylistTotalDuration is deprecated. ' +
1002 return 0; 998 'Use videojs.Hls.Playlist.duration instead');
1003 } 999 return videojs.Hls.Playlist.duration(playlist);
1004
1005 // if present, use the duration specified in the playlist
1006 if (playlist.totalDuration) {
1007 return playlist.totalDuration;
1008 }
1009
1010 // duration should be Infinity for live playlists
1011 if (!playlist.endList) {
1012 return window.Infinity;
1013 }
1014
1015 return videojs.Hls.getPlaylistDuration(playlist);
1016 }; 1000 };
1017 1001
1018 /** 1002 /**
......
...@@ -90,6 +90,7 @@ module.exports = function(config) { ...@@ -90,6 +90,7 @@ module.exports = function(config) {
90 '../src/segment-parser.js', 90 '../src/segment-parser.js',
91 '../src/m3u8/m3u8-parser.js', 91 '../src/m3u8/m3u8-parser.js',
92 '../src/xhr.js', 92 '../src/xhr.js',
93 '../src/playlist.js',
93 '../src/playlist-loader.js', 94 '../src/playlist-loader.js',
94 '../src/decrypter.js', 95 '../src/decrypter.js',
95 '../tmp/manifests.js', 96 '../tmp/manifests.js',
......
...@@ -54,6 +54,7 @@ module.exports = function(config) { ...@@ -54,6 +54,7 @@ module.exports = function(config) {
54 '../src/segment-parser.js', 54 '../src/segment-parser.js',
55 '../src/m3u8/m3u8-parser.js', 55 '../src/m3u8/m3u8-parser.js',
56 '../src/xhr.js', 56 '../src/xhr.js',
57 '../src/playlist.js',
57 '../src/playlist-loader.js', 58 '../src/playlist-loader.js',
58 '../src/decrypter.js', 59 '../src/decrypter.js',
59 '../tmp/manifests.js', 60 '../tmp/manifests.js',
......
...@@ -597,4 +597,5 @@ ...@@ -597,4 +597,5 @@
597 '#EXT-X-ENDLIST'); // no newline 597 '#EXT-X-ENDLIST'); // no newline
598 ok(loader.media().endList, 'flushed the final line of input'); 598 ok(loader.media().endList, 'flushed the final line of input');
599 }); 599 });
600
600 })(window); 601 })(window);
......
1 /* Tests for the playlist utilities */
2 (function(window, videojs) {
3 'use strict';
4 var Playlist = videojs.Hls.Playlist;
5
6 module('Playlist Utilities');
7
8 test('total duration for live playlists is Infinity', function() {
9 var duration = Playlist.duration({
10 segments: [{
11 duration: 4,
12 uri: '0.ts'
13 }]
14 });
15
16 equal(duration, Infinity, 'duration is infinity');
17 });
18
19 test('interval duration does not include upcoming live segments', function() {
20 var duration = Playlist.duration({
21 segments: [{
22 duration: 4,
23 uri: '0.ts'
24 }, {
25 duration: 10,
26 uri: '1.ts'
27 }, {
28 duration: 10,
29 uri: '2.ts'
30 }, {
31 duration: 10,
32 uri: '3.ts'
33 }]
34 }, 0, 3);
35
36 equal(duration, 4, 'does not include upcoming live segments');
37 });
38
39 test('calculates seekable time ranges from the available segments', function() {
40 var playlist = {
41 segments: [{
42 duration: 10,
43 uri: '0.ts'
44 }, {
45 duration: 10,
46 uri: '1.ts'
47 }],
48 endList: true
49 }, seekable = Playlist.seekable(playlist);
50
51 equal(seekable.length, 1, 'there are seekable ranges');
52 equal(seekable.start(0), 0, 'starts at zero');
53 equal(seekable.end(0), Playlist.duration(playlist), 'ends at the duration');
54 });
55
56 test('master playlists have empty seekable ranges', function() {
57 var seekable = Playlist.seekable({
58 playlists: [{
59 uri: 'low.m3u8'
60 }, {
61 uri: 'high.m3u8'
62 }]
63 });
64 equal(seekable.length, 0, 'no seekable ranges from a master playlist');
65 });
66
67 test('seekable end is three target durations from the actual end of live playlists', function() {
68 var seekable = Playlist.seekable({
69 segments: [{
70 duration: 7,
71 uri: '0.ts'
72 }, {
73 duration: 10,
74 uri: '1.ts'
75 }, {
76 duration: 10,
77 uri: '2.ts'
78 }, {
79 duration: 10,
80 uri: '3.ts'
81 }]
82 });
83 equal(seekable.length, 1, 'there are seekable ranges');
84 equal(seekable.start(0), 0, 'starts at zero');
85 equal(seekable.end(0), 7, 'ends three target durations from the last segment');
86 });
87
88 test('adjusts seekable to the live playlist window', function() {
89 var seekable = Playlist.seekable({
90 targetDuration: 10,
91 mediaSequence: 7,
92 segments: [{
93 uri: '8.ts'
94 }, {
95 uri: '9.ts'
96 }, {
97 uri: '10.ts'
98 }, {
99 uri: '11.ts'
100 }]
101 });
102 equal(seekable.length, 1, 'there are seekable ranges');
103 equal(seekable.start(0), 10 * 7, 'starts at the earliest available segment');
104 equal(seekable.end(0), 10 * 8, 'ends three target durations from the last available segment');
105 });
106
107 test('seekable end accounts for non-standard target durations', function() {
108 var seekable = Playlist.seekable({
109 targetDuration: 2,
110 segments: [{
111 duration: 2,
112 uri: '0.ts'
113 }, {
114 duration: 2,
115 uri: '1.ts'
116 }, {
117 duration: 1,
118 uri: '2.ts'
119 }, {
120 duration: 2,
121 uri: '3.ts'
122 }, {
123 duration: 2,
124 uri: '4.ts'
125 }]
126 });
127 equal(seekable.start(0), 0, 'starts at the earliest available segment');
128 equal(seekable.end(0),
129 9 - (2 * 3),
130 'allows seeking no further than three target durations from the end');
131 });
132
133 })(window, window.videojs);
...@@ -32,6 +32,7 @@ ...@@ -32,6 +32,7 @@
32 32
33 <!-- M3U8 --> 33 <!-- M3U8 -->
34 <script src="../src/m3u8/m3u8-parser.js"></script> 34 <script src="../src/m3u8/m3u8-parser.js"></script>
35 <script src="../src/playlist.js"></script>
35 <script src="../src/playlist-loader.js"></script> 36 <script src="../src/playlist-loader.js"></script>
36 <script src="../node_modules/pkcs7/dist/pkcs7.unpad.js"></script> 37 <script src="../node_modules/pkcs7/dist/pkcs7.unpad.js"></script>
37 <script src="../src/decrypter.js"></script> 38 <script src="../src/decrypter.js"></script>
...@@ -59,6 +60,7 @@ ...@@ -59,6 +60,7 @@
59 <script src="flv-tag_test.js"></script> 60 <script src="flv-tag_test.js"></script>
60 <script src="metadata-stream_test.js"></script> 61 <script src="metadata-stream_test.js"></script>
61 <script src="m3u8_test.js"></script> 62 <script src="m3u8_test.js"></script>
63 <script src="playlist_test.js"></script>
62 <script src="playlist-loader_test.js"></script> 64 <script src="playlist-loader_test.js"></script>
63 <script src="decrypter_test.js"></script> 65 <script src="decrypter_test.js"></script>
64 <script src="xhr_test.js"></script> 66 <script src="xhr_test.js"></script>
......
...@@ -1611,7 +1611,8 @@ test('clears the segment buffer on seek', function() { ...@@ -1611,7 +1611,8 @@ test('clears the segment buffer on seek', function() {
1611 '1.ts\n' + 1611 '1.ts\n' +
1612 '#EXT-X-DISCONTINUITY\n' + 1612 '#EXT-X-DISCONTINUITY\n' +
1613 '#EXTINF:10,0\n' + 1613 '#EXTINF:10,0\n' +
1614 '2.ts\n'); 1614 '2.ts\n' +
1615 '#EXT-X-ENDLIST\n');
1615 standardXHRResponse(requests.pop()); 1616 standardXHRResponse(requests.pop());
1616 1617
1617 // play to 6s to trigger the next segment request 1618 // play to 6s to trigger the next segment request
...@@ -1662,7 +1663,8 @@ test('continues playing after seek to discontinuity', function() { ...@@ -1662,7 +1663,8 @@ test('continues playing after seek to discontinuity', function() {
1662 '1.ts\n' + 1663 '1.ts\n' +
1663 '#EXT-X-DISCONTINUITY\n' + 1664 '#EXT-X-DISCONTINUITY\n' +
1664 '#EXTINF:10,0\n' + 1665 '#EXTINF:10,0\n' +
1665 '2.ts\n'); 1666 '2.ts\n' +
1667 '#EXT-X-ENDLIST\n');
1666 standardXHRResponse(requests.pop()); 1668 standardXHRResponse(requests.pop());
1667 1669
1668 currentTime = 1; 1670 currentTime = 1;
......