Implement seekable
Override the Flash tech's seekable method to take into account live playlists.
Showing
10 changed files
with
276 additions
and
35 deletions
... | @@ -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,13 +28,14 @@ | ... | @@ -28,13 +28,14 @@ |
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> |
34 | <script src="src/decrypter.js"></script> | 35 | <script src="src/decrypter.js"></script> |
35 | 36 | ||
36 | <script src="src/bin-utils.js"></script> | 37 | <script src="src/bin-utils.js"></script> |
37 | 38 | ||
38 | <!-- example MPEG2-TS segments --> | 39 | <!-- example MPEG2-TS segments --> |
39 | <!-- bipbop --> | 40 | <!-- bipbop --> |
40 | <!-- <script src="test/tsSegment.js"></script> --> | 41 | <!-- <script src="test/tsSegment.js"></script> --> | ... | ... |
src/playlist.js
0 → 100644
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); | ... | ... |
test/playlist_test.js
0 → 100644
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; | ... | ... |
-
Please register or sign in to post a comment