Report seekable relative to the earliest possible position at load start
Keep track of the accurate durations of expired segments in the playlist loader so that it's possible to accurately calculate the start and end points of the seekable ranges relative to media timeline position zero, even if we switch variant streams or seek within a live stream. Track the media timeline position of the last discontinuity to allow for PTS-based variant stream synchronization instead of the incorrect media sequence based method we're currently using. Stop rewriting timestamps in the transmuxed FLV tags for that reason as well. Add m3u8 parser support for EXT-X-DISCONTINUITY-SEQUENCE.
Showing
48 changed files
with
521 additions
and
207 deletions
... | @@ -12,7 +12,6 @@ var | ... | @@ -12,7 +12,6 @@ var |
12 | window.videojs.Hls.AacStream = function() { | 12 | window.videojs.Hls.AacStream = function() { |
13 | var | 13 | var |
14 | next_pts, // :uint | 14 | next_pts, // :uint |
15 | pts_offset, // :int | ||
16 | state, // :uint | 15 | state, // :uint |
17 | pes_length, // :int | 16 | pes_length, // :int |
18 | lastMetaPts, | 17 | lastMetaPts, |
... | @@ -32,7 +31,6 @@ window.videojs.Hls.AacStream = function() { | ... | @@ -32,7 +31,6 @@ window.videojs.Hls.AacStream = function() { |
32 | 31 | ||
33 | // (pts:uint):void | 32 | // (pts:uint):void |
34 | this.setTimeStampOffset = function(pts) { | 33 | this.setTimeStampOffset = function(pts) { |
35 | pts_offset = pts; | ||
36 | 34 | ||
37 | // keep track of the last time a metadata tag was written out | 35 | // keep track of the last time a metadata tag was written out |
38 | // set the initial value so metadata will be generated before any | 36 | // set the initial value so metadata will be generated before any |
... | @@ -42,7 +40,7 @@ window.videojs.Hls.AacStream = function() { | ... | @@ -42,7 +40,7 @@ window.videojs.Hls.AacStream = function() { |
42 | 40 | ||
43 | // (pts:uint, pes_size:int, dataAligned:Boolean):void | 41 | // (pts:uint, pes_size:int, dataAligned:Boolean):void |
44 | this.setNextTimeStamp = function(pts, pes_size, dataAligned) { | 42 | this.setNextTimeStamp = function(pts, pes_size, dataAligned) { |
45 | next_pts = pts - pts_offset; | 43 | next_pts = pts; |
46 | pes_length = pes_size; | 44 | pes_length = pes_size; |
47 | 45 | ||
48 | // If data is aligned, flush all internal buffers | 46 | // If data is aligned, flush all internal buffers | ... | ... |
... | @@ -37,7 +37,6 @@ | ... | @@ -37,7 +37,6 @@ |
37 | window.videojs.Hls.H264Stream = H264Stream = function() { | 37 | window.videojs.Hls.H264Stream = H264Stream = function() { |
38 | this._next_pts = 0; // :uint; | 38 | this._next_pts = 0; // :uint; |
39 | this._next_dts = 0; // :uint; | 39 | this._next_dts = 0; // :uint; |
40 | this._pts_offset = 0; // :int | ||
41 | 40 | ||
42 | this._h264Frame = null; // :FlvTag | 41 | this._h264Frame = null; // :FlvTag |
43 | 42 | ||
... | @@ -52,15 +51,13 @@ | ... | @@ -52,15 +51,13 @@ |
52 | }; | 51 | }; |
53 | 52 | ||
54 | //(pts:uint):void | 53 | //(pts:uint):void |
55 | H264Stream.prototype.setTimeStampOffset = function(pts) { | 54 | H264Stream.prototype.setTimeStampOffset = function() {}; |
56 | this._pts_offset = pts; | ||
57 | }; | ||
58 | 55 | ||
59 | //(pts:uint, dts:uint, dataAligned:Boolean):void | 56 | //(pts:uint, dts:uint, dataAligned:Boolean):void |
60 | H264Stream.prototype.setNextTimeStamp = function(pts, dts, dataAligned) { | 57 | H264Stream.prototype.setNextTimeStamp = function(pts, dts, dataAligned) { |
61 | // We could end up with a DTS less than 0 here. We need to deal with that! | 58 | // We could end up with a DTS less than 0 here. We need to deal with that! |
62 | this._next_pts = pts - this._pts_offset; | 59 | this._next_pts = pts; |
63 | this._next_dts = dts - this._pts_offset; | 60 | this._next_dts = dts; |
64 | 61 | ||
65 | // If data is aligned, flush all internal buffers | 62 | // If data is aligned, flush all internal buffers |
66 | if (dataAligned) { | 63 | if (dataAligned) { | ... | ... |
... | @@ -210,6 +210,18 @@ | ... | @@ -210,6 +210,18 @@ |
210 | this.trigger('data', event); | 210 | this.trigger('data', event); |
211 | return; | 211 | return; |
212 | } | 212 | } |
213 | match = (/^#EXT-X-DISCONTINUITY-SEQUENCE:?(\-?[0-9.]*)?/).exec(line); | ||
214 | if (match) { | ||
215 | event = { | ||
216 | type: 'tag', | ||
217 | tagType: 'discontinuity-sequence' | ||
218 | }; | ||
219 | if (match[1]) { | ||
220 | event.number = parseInt(match[1], 10); | ||
221 | } | ||
222 | this.trigger('data', event); | ||
223 | return; | ||
224 | } | ||
213 | match = (/^#EXT-X-PLAYLIST-TYPE:?(.*)?$/).exec(line); | 225 | match = (/^#EXT-X-PLAYLIST-TYPE:?(.*)?$/).exec(line); |
214 | if (match) { | 226 | if (match) { |
215 | event = { | 227 | event = { |
... | @@ -308,7 +320,7 @@ | ... | @@ -308,7 +320,7 @@ |
308 | event.attributes = parseAttributes(match[1]); | 320 | event.attributes = parseAttributes(match[1]); |
309 | // parse the IV string into a Uint32Array | 321 | // parse the IV string into a Uint32Array |
310 | if (event.attributes.IV) { | 322 | if (event.attributes.IV) { |
311 | if (event.attributes.IV.substring(0,2) === '0x') { | 323 | if (event.attributes.IV.substring(0,2) === '0x') { |
312 | event.attributes.IV = event.attributes.IV.substring(2); | 324 | event.attributes.IV = event.attributes.IV.substring(2); |
313 | } | 325 | } |
314 | 326 | ||
... | @@ -409,6 +421,12 @@ | ... | @@ -409,6 +421,12 @@ |
409 | message: 'defaulting media sequence to zero' | 421 | message: 'defaulting media sequence to zero' |
410 | }); | 422 | }); |
411 | } | 423 | } |
424 | if (!('discontinuitySequence' in this.manifest)) { | ||
425 | this.manifest.discontinuitySequence = 0; | ||
426 | this.trigger('info', { | ||
427 | message: 'defaulting discontinuity sequence to zero' | ||
428 | }); | ||
429 | } | ||
412 | if (entry.duration >= 0) { | 430 | if (entry.duration >= 0) { |
413 | currentUri.duration = entry.duration; | 431 | currentUri.duration = entry.duration; |
414 | } | 432 | } |
... | @@ -459,6 +477,15 @@ | ... | @@ -459,6 +477,15 @@ |
459 | } | 477 | } |
460 | this.manifest.mediaSequence = entry.number; | 478 | this.manifest.mediaSequence = entry.number; |
461 | }, | 479 | }, |
480 | 'discontinuity-sequence': function() { | ||
481 | if (!isFinite(entry.number)) { | ||
482 | this.trigger('warn', { | ||
483 | message: 'ignoring invalid discontinuity sequence: ' + entry.number | ||
484 | }); | ||
485 | return; | ||
486 | } | ||
487 | this.manifest.discontinuitySequence = entry.number; | ||
488 | }, | ||
462 | 'playlist-type': function() { | 489 | 'playlist-type': function() { |
463 | if (!(/VOD|EVENT/).test(entry.playlistType)) { | 490 | if (!(/VOD|EVENT/).test(entry.playlistType)) { |
464 | this.trigger('warn', { | 491 | this.trigger('warn', { | ... | ... |
1 | /** | 1 | /** |
2 | * A state machine that manages the loading, caching, and updating of | 2 | * A state machine that manages the loading, caching, and updating of |
3 | * M3U8 playlists. | 3 | * M3U8 playlists. When tracking a live playlist, loaders will keep |
4 | * track of the duration of content that expired since the loader was | ||
5 | * initialized and when the current discontinuity sequence was | ||
6 | * encountered. A complete media timeline for a live playlist with | ||
7 | * expiring segments and discontinuities looks like this: | ||
8 | * | ||
9 | * |-- expiredPreDiscontinuity --|-- expiredPostDiscontinuity --|-- segments --| | ||
10 | * | ||
11 | * You can use these values to calculate how much time has elapsed | ||
12 | * since the stream began loading or how long it has been since the | ||
13 | * most recent discontinuity was encountered, for instance. | ||
4 | */ | 14 | */ |
5 | (function(window, videojs) { | 15 | (function(window, videojs) { |
6 | 'use strict'; | 16 | 'use strict'; |
7 | var | 17 | var |
8 | resolveUrl = videojs.Hls.resolveUrl, | 18 | resolveUrl = videojs.Hls.resolveUrl, |
9 | xhr = videojs.Hls.xhr, | 19 | xhr = videojs.Hls.xhr, |
20 | Playlist = videojs.Hls.Playlist, | ||
10 | 21 | ||
11 | /** | 22 | /** |
12 | * Returns a new master playlist that is the result of merging an | 23 | * Returns a new master playlist that is the result of merging an |
... | @@ -51,66 +62,84 @@ | ... | @@ -51,66 +62,84 @@ |
51 | var | 62 | var |
52 | loader = this, | 63 | loader = this, |
53 | dispose, | 64 | dispose, |
54 | media, | ||
55 | mediaUpdateTimeout, | 65 | mediaUpdateTimeout, |
56 | request, | 66 | request, |
67 | haveMetadata; | ||
57 | 68 | ||
58 | haveMetadata = function(error, xhr, url) { | 69 | PlaylistLoader.prototype.init.call(this); |
59 | var parser, refreshDelay, update; | ||
60 | 70 | ||
61 | loader.setBandwidth(request || xhr); | 71 | if (!srcUrl) { |
72 | throw new Error('A non-empty playlist URL is required'); | ||
73 | } | ||
62 | 74 | ||
63 | // any in-flight request is now finished | 75 | // update the playlist loader's state in response to a new or |
64 | request = null; | 76 | // updated playlist. |
77 | haveMetadata = function(error, xhr, url) { | ||
78 | var parser, refreshDelay, update; | ||
65 | 79 | ||
66 | if (error) { | 80 | loader.setBandwidth(request || xhr); |
67 | loader.error = { | ||
68 | status: xhr.status, | ||
69 | message: 'HLS playlist request error at URL: ' + url, | ||
70 | responseText: xhr.responseText, | ||
71 | code: (xhr.status >= 500) ? 4 : 2 | ||
72 | }; | ||
73 | return loader.trigger('error'); | ||
74 | } | ||
75 | 81 | ||
76 | loader.state = 'HAVE_METADATA'; | 82 | // any in-flight request is now finished |
83 | request = null; | ||
77 | 84 | ||
78 | parser = new videojs.m3u8.Parser(); | 85 | if (error) { |
79 | parser.push(xhr.responseText); | 86 | loader.error = { |
80 | parser.end(); | 87 | status: xhr.status, |
81 | parser.manifest.uri = url; | 88 | message: 'HLS playlist request error at URL: ' + url, |
82 | 89 | responseText: xhr.responseText, | |
83 | // merge this playlist into the master | 90 | code: (xhr.status >= 500) ? 4 : 2 |
84 | update = updateMaster(loader.master, parser.manifest); | 91 | }; |
85 | refreshDelay = (parser.manifest.targetDuration || 10) * 1000; | 92 | return loader.trigger('error'); |
86 | if (update) { | 93 | } |
87 | loader.master = update; | ||
88 | media = loader.master.playlists[url]; | ||
89 | } else { | ||
90 | // if the playlist is unchanged since the last reload, | ||
91 | // try again after half the target duration | ||
92 | refreshDelay /= 2; | ||
93 | } | ||
94 | 94 | ||
95 | // refresh live playlists after a target duration passes | 95 | loader.state = 'HAVE_METADATA'; |
96 | if (!loader.media().endList) { | ||
97 | window.clearTimeout(mediaUpdateTimeout); | ||
98 | mediaUpdateTimeout = window.setTimeout(function() { | ||
99 | loader.trigger('mediaupdatetimeout'); | ||
100 | }, refreshDelay); | ||
101 | } | ||
102 | 96 | ||
103 | loader.trigger('loadedplaylist'); | 97 | parser = new videojs.m3u8.Parser(); |
104 | }; | 98 | parser.push(xhr.responseText); |
99 | parser.end(); | ||
100 | parser.manifest.uri = url; | ||
101 | |||
102 | // merge this playlist into the master | ||
103 | update = updateMaster(loader.master, parser.manifest); | ||
104 | refreshDelay = (parser.manifest.targetDuration || 10) * 1000; | ||
105 | if (update) { | ||
106 | loader.master = update; | ||
107 | loader.updateMediaPlaylist_(parser.manifest); | ||
108 | } else { | ||
109 | // if the playlist is unchanged since the last reload, | ||
110 | // try again after half the target duration | ||
111 | refreshDelay /= 2; | ||
112 | } | ||
105 | 113 | ||
106 | PlaylistLoader.prototype.init.call(this); | 114 | // refresh live playlists after a target duration passes |
115 | if (!loader.media().endList) { | ||
116 | window.clearTimeout(mediaUpdateTimeout); | ||
117 | mediaUpdateTimeout = window.setTimeout(function() { | ||
118 | loader.trigger('mediaupdatetimeout'); | ||
119 | }, refreshDelay); | ||
120 | } | ||
107 | 121 | ||
108 | if (!srcUrl) { | 122 | loader.trigger('loadedplaylist'); |
109 | throw new Error('A non-empty playlist URL is required'); | 123 | }; |
110 | } | ||
111 | 124 | ||
125 | // initialize the loader state | ||
112 | loader.state = 'HAVE_NOTHING'; | 126 | loader.state = 'HAVE_NOTHING'; |
113 | 127 | ||
128 | // the total duration of all segments that expired and have been | ||
129 | // removed from the current playlist after the last | ||
130 | // #EXT-X-DISCONTINUITY. In a live playlist without | ||
131 | // discontinuities, this is the total amount of time that has | ||
132 | // been removed from the stream since the playlist loader began | ||
133 | // tracking it. | ||
134 | loader.expiredPostDiscontinuity_ = 0; | ||
135 | |||
136 | // the total duration of all segments that expired and have been | ||
137 | // removed from the current playlist before the last | ||
138 | // #EXT-X-DISCONTINUITY. The total amount of time that has | ||
139 | // expired is always the sum of expiredPreDiscontinuity_ and | ||
140 | // expiredPostDiscontinuity_. | ||
141 | loader.expiredPreDiscontinuity_ = 0; | ||
142 | |||
114 | // capture the prototype dispose function | 143 | // capture the prototype dispose function |
115 | dispose = this.dispose; | 144 | dispose = this.dispose; |
116 | 145 | ||
... | @@ -141,7 +170,7 @@ | ... | @@ -141,7 +170,7 @@ |
141 | var mediaChange = false; | 170 | var mediaChange = false; |
142 | // getter | 171 | // getter |
143 | if (!playlist) { | 172 | if (!playlist) { |
144 | return media; | 173 | return loader.media_; |
145 | } | 174 | } |
146 | 175 | ||
147 | // setter | 176 | // setter |
... | @@ -158,7 +187,7 @@ | ... | @@ -158,7 +187,7 @@ |
158 | playlist = loader.master.playlists[playlist]; | 187 | playlist = loader.master.playlists[playlist]; |
159 | } | 188 | } |
160 | 189 | ||
161 | mediaChange = playlist.uri !== media.uri; | 190 | mediaChange = playlist.uri !== loader.media_.uri; |
162 | 191 | ||
163 | // switch to fully loaded playlists immediately | 192 | // switch to fully loaded playlists immediately |
164 | if (loader.master.playlists[playlist.uri].endList) { | 193 | if (loader.master.playlists[playlist.uri].endList) { |
... | @@ -169,7 +198,7 @@ | ... | @@ -169,7 +198,7 @@ |
169 | request = null; | 198 | request = null; |
170 | } | 199 | } |
171 | loader.state = 'HAVE_METADATA'; | 200 | loader.state = 'HAVE_METADATA'; |
172 | media = playlist; | 201 | loader.media_ = playlist; |
173 | 202 | ||
174 | // trigger media change if the active media has been updated | 203 | // trigger media change if the active media has been updated |
175 | if (mediaChange) { | 204 | if (mediaChange) { |
... | @@ -292,5 +321,46 @@ | ... | @@ -292,5 +321,46 @@ |
292 | }; | 321 | }; |
293 | PlaylistLoader.prototype = new videojs.Hls.Stream(); | 322 | PlaylistLoader.prototype = new videojs.Hls.Stream(); |
294 | 323 | ||
324 | /** | ||
325 | * Update the PlaylistLoader state to reflect the changes in an | ||
326 | * update to the current media playlist. | ||
327 | * @param update {object} the updated media playlist object | ||
328 | */ | ||
329 | PlaylistLoader.prototype.updateMediaPlaylist_ = function(update) { | ||
330 | var lastDiscontinuity, expiredCount, i; | ||
331 | |||
332 | if (this.media_) { | ||
333 | expiredCount = update.mediaSequence - this.media_.mediaSequence; | ||
334 | |||
335 | // setup the index for duration calculations so that the newly | ||
336 | // expired time will be accumulated after the last | ||
337 | // discontinuity, unless we discover otherwise | ||
338 | lastDiscontinuity = this.media_.mediaSequence; | ||
339 | |||
340 | if (this.media_.discontinuitySequence !== update.discontinuitySequence) { | ||
341 | i = expiredCount; | ||
342 | while (i--) { | ||
343 | if (this.media_.segments[i].discontinuity) { | ||
344 | // a segment that begins a new discontinuity sequence has expired | ||
345 | lastDiscontinuity = i + this.media_.mediaSequence; | ||
346 | this.expiredPreDiscontinuity_ += this.expiredPostDiscontinuity_; | ||
347 | this.expiredPostDiscontinuity_ = 0; | ||
348 | break; | ||
349 | } | ||
350 | } | ||
351 | } | ||
352 | |||
353 | // update the expirated durations | ||
354 | this.expiredPreDiscontinuity_ += Playlist.duration(this.media_, | ||
355 | this.media_.mediaSequence, | ||
356 | lastDiscontinuity); | ||
357 | this.expiredPostDiscontinuity_ += Playlist.duration(this.media_, | ||
358 | lastDiscontinuity, | ||
359 | this.media_.mediaSequence + expiredCount); | ||
360 | } | ||
361 | |||
362 | this.media_ = this.master.playlists[update.uri]; | ||
363 | }; | ||
364 | |||
295 | videojs.Hls.PlaylistLoader = PlaylistLoader; | 365 | videojs.Hls.PlaylistLoader = PlaylistLoader; |
296 | })(window, window.videojs); | 366 | })(window, window.videojs); | ... | ... |
... | @@ -10,38 +10,37 @@ | ... | @@ -10,38 +10,37 @@ |
10 | /** | 10 | /** |
11 | * Calculate the media duration from the segments associated with a | 11 | * Calculate the media duration from the segments associated with a |
12 | * playlist. The duration of a subinterval of the available segments | 12 | * playlist. The duration of a subinterval of the available segments |
13 | * may be calculated by specifying a start and end index. The | 13 | * may be calculated by specifying a start and end index. |
14 | * minimum recommended live buffer is automatically subtracted for | 14 | * |
15 | * the last segments of live playlists. | ||
16 | * @param playlist {object} a media playlist object | 15 | * @param playlist {object} a media playlist object |
17 | * @param startIndex {number} (optional) an inclusive lower | 16 | * @param startSequence {number} (optional) an inclusive lower |
18 | * boundary for the playlist. Defaults to 0. | 17 | * boundary for the playlist. Defaults to 0. |
19 | * @param endIndex {number} (optional) an exclusive upper boundary | 18 | * @param endSequence {number} (optional) an exclusive upper boundary |
20 | * for the playlist. Defaults to playlist length. | 19 | * for the playlist. Defaults to playlist length. |
21 | * @return {number} the duration between the start index and end | 20 | * @return {number} the duration between the start index and end |
22 | * index. | 21 | * index. |
23 | */ | 22 | */ |
24 | segmentsDuration = function(playlist, startIndex, endIndex) { | 23 | segmentsDuration = function(playlist, startSequence, endSequence) { |
25 | var targetDuration, i, segment, result = 0; | 24 | var targetDuration, i, segment, expiredSegmentCount, result = 0; |
26 | 25 | ||
27 | startIndex = startIndex || 0; | 26 | startSequence = startSequence || 0; |
28 | endIndex = endIndex !== undefined ? endIndex : (playlist.segments || []).length; | 27 | i = startSequence; |
28 | endSequence = endSequence !== undefined ? endSequence : (playlist.segments || []).length; | ||
29 | targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION; | 29 | targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION; |
30 | 30 | ||
31 | for (i = endIndex - 1; i >= startIndex; i--) { | 31 | // estimate expired segment duration using the target duration |
32 | segment = playlist.segments[i]; | 32 | expiredSegmentCount = Math.max(playlist.mediaSequence - startSequence, 0); |
33 | result += expiredSegmentCount * targetDuration; | ||
34 | i += expiredSegmentCount; | ||
35 | |||
36 | // accumulate the segment durations into the result | ||
37 | for (; i < endSequence; i++) { | ||
38 | segment = playlist.segments[i - playlist.mediaSequence]; | ||
33 | result += segment.preciseDuration || | 39 | result += segment.preciseDuration || |
34 | segment.duration || | 40 | segment.duration || |
35 | targetDuration; | 41 | targetDuration; |
36 | } | 42 | } |
37 | 43 | ||
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; | 44 | return result; |
46 | }; | 45 | }; |
47 | 46 | ||
... | @@ -51,21 +50,21 @@ | ... | @@ -51,21 +50,21 @@ |
51 | * timeline between those two indices. The total duration for live | 50 | * timeline between those two indices. The total duration for live |
52 | * playlists is always Infinity. | 51 | * playlists is always Infinity. |
53 | * @param playlist {object} a media playlist object | 52 | * @param playlist {object} a media playlist object |
54 | * @param startIndex {number} (optional) an inclusive lower | 53 | * @param startSequence {number} (optional) an inclusive lower |
55 | * boundary for the playlist. Defaults to 0. | 54 | * boundary for the playlist. Defaults to 0. |
56 | * @param endIndex {number} (optional) an exclusive upper boundary | 55 | * @param endSequence {number} (optional) an exclusive upper boundary |
57 | * for the playlist. Defaults to playlist length. | 56 | * for the playlist. Defaults to playlist length. |
58 | * @return {number} the duration between the start index and end | 57 | * @return {number} the duration between the start index and end |
59 | * index. | 58 | * index. |
60 | */ | 59 | */ |
61 | duration = function(playlist, startIndex, endIndex) { | 60 | duration = function(playlist, startSequence, endSequence) { |
62 | if (!playlist) { | 61 | if (!playlist) { |
63 | return 0; | 62 | return 0; |
64 | } | 63 | } |
65 | 64 | ||
66 | // if a slice of the total duration is not requested, use | 65 | // if a slice of the total duration is not requested, use |
67 | // playlist-level duration indicators when they're present | 66 | // playlist-level duration indicators when they're present |
68 | if (startIndex === undefined && endIndex === undefined) { | 67 | if (startSequence === undefined && endSequence === undefined) { |
69 | // if present, use the duration specified in the playlist | 68 | // if present, use the duration specified in the playlist |
70 | if (playlist.totalDuration) { | 69 | if (playlist.totalDuration) { |
71 | return playlist.totalDuration; | 70 | return playlist.totalDuration; |
... | @@ -79,8 +78,8 @@ | ... | @@ -79,8 +78,8 @@ |
79 | 78 | ||
80 | // calculate the total duration based on the segment durations | 79 | // calculate the total duration based on the segment durations |
81 | return segmentsDuration(playlist, | 80 | return segmentsDuration(playlist, |
82 | startIndex, | 81 | startSequence, |
83 | endIndex); | 82 | endSequence); |
84 | }; | 83 | }; |
85 | 84 | ||
86 | /** | 85 | /** |
... | @@ -91,7 +90,8 @@ | ... | @@ -91,7 +90,8 @@ |
91 | * for seeking | 90 | * for seeking |
92 | */ | 91 | */ |
93 | seekable = function(playlist) { | 92 | seekable = function(playlist) { |
94 | var startOffset, targetDuration; | 93 | var start, end, liveBuffer, targetDuration, segment, pending, i; |
94 | |||
95 | // without segments, there are no seekable ranges | 95 | // without segments, there are no seekable ranges |
96 | if (!playlist.segments) { | 96 | if (!playlist.segments) { |
97 | return videojs.createTimeRange(); | 97 | return videojs.createTimeRange(); |
... | @@ -101,10 +101,34 @@ | ... | @@ -101,10 +101,34 @@ |
101 | return videojs.createTimeRange(0, duration(playlist)); | 101 | return videojs.createTimeRange(0, duration(playlist)); |
102 | } | 102 | } |
103 | 103 | ||
104 | start = segmentsDuration(playlist, 0, playlist.mediaSequence); | ||
105 | end = start + segmentsDuration(playlist, | ||
106 | playlist.mediaSequence, | ||
107 | playlist.mediaSequence + playlist.segments.length); | ||
104 | targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION; | 108 | targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION; |
105 | startOffset = targetDuration * (playlist.mediaSequence || 0); | 109 | |
106 | return videojs.createTimeRange(startOffset, | 110 | // live playlists should not expose three segment durations worth |
107 | startOffset + segmentsDuration(playlist)); | 111 | // of content from the end of the playlist |
112 | // https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-6.3.3 | ||
113 | if (!playlist.endList) { | ||
114 | liveBuffer = targetDuration * 3; | ||
115 | // walk backward from the last available segment and track how | ||
116 | // much media time has elapsed until three target durations have | ||
117 | // been traversed. if a segment is part of the interval being | ||
118 | // reported, subtract the overlapping portion of its duration | ||
119 | // from the result. | ||
120 | for (i = playlist.segments.length - 1; i >= 0 && liveBuffer > 0; i--) { | ||
121 | segment = playlist.segments[i]; | ||
122 | pending = Math.min(segment.preciseDuration || | ||
123 | segment.duration || | ||
124 | targetDuration, | ||
125 | liveBuffer); | ||
126 | liveBuffer -= pending; | ||
127 | end -= pending; | ||
128 | } | ||
129 | } | ||
130 | |||
131 | return videojs.createTimeRange(start, end); | ||
108 | }; | 132 | }; |
109 | 133 | ||
110 | // exports | 134 | // exports | ... | ... |
... | @@ -39,11 +39,20 @@ videojs.Hls = videojs.Flash.extend({ | ... | @@ -39,11 +39,20 @@ videojs.Hls = videojs.Flash.extend({ |
39 | this.currentTime = videojs.Hls.prototype.currentTime; | 39 | this.currentTime = videojs.Hls.prototype.currentTime; |
40 | this.setCurrentTime = videojs.Hls.prototype.setCurrentTime; | 40 | this.setCurrentTime = videojs.Hls.prototype.setCurrentTime; |
41 | 41 | ||
42 | // a queue of segments that need to be transmuxed and processed, | ||
43 | // and then fed to the source buffer | ||
44 | this.segmentBuffer_ = []; | ||
42 | // periodically check if new data needs to be downloaded or | 45 | // periodically check if new data needs to be downloaded or |
43 | // buffered data should be appended to the source buffer | 46 | // buffered data should be appended to the source buffer |
44 | this.segmentBuffer_ = []; | ||
45 | this.startCheckingBuffer_(); | 47 | this.startCheckingBuffer_(); |
46 | 48 | ||
49 | // the earliest presentation timestamp (PTS) encountered since the | ||
50 | // last #EXT-X-DISCONTINUITY. In a playlist without | ||
51 | // discontinuities, this will be the PTS value for the first frame | ||
52 | // in the video. PTS values are necessary to properly synchronize | ||
53 | // playback when switching to a variant stream. | ||
54 | this.lastStartingPts_ = undefined; | ||
55 | |||
47 | videojs.Hls.prototype.src.call(this, options.source && options.source.src); | 56 | videojs.Hls.prototype.src.call(this, options.source && options.source.src); |
48 | } | 57 | } |
49 | }); | 58 | }); |
... | @@ -356,8 +365,16 @@ videojs.Hls.prototype.duration = function() { | ... | @@ -356,8 +365,16 @@ videojs.Hls.prototype.duration = function() { |
356 | }; | 365 | }; |
357 | 366 | ||
358 | videojs.Hls.prototype.seekable = function() { | 367 | videojs.Hls.prototype.seekable = function() { |
368 | var absoluteSeekable, startOffset, media; | ||
369 | |||
359 | if (this.playlists) { | 370 | if (this.playlists) { |
360 | return videojs.Hls.Playlist.seekable(this.playlists.media()); | 371 | // report the seekable range relative to the earliest possible |
372 | // position when the stream was first loaded | ||
373 | media = this.playlists.media(); | ||
374 | absoluteSeekable = videojs.Hls.Playlist.seekable(media); | ||
375 | startOffset = this.playlists.expiredPostDiscontinuity_ - this.playlists.expiredPreDiscontinuity_; | ||
376 | return videojs.createTimeRange(startOffset, | ||
377 | startOffset + (absoluteSeekable.end(0) - absoluteSeekable.start(0))); | ||
361 | } | 378 | } |
362 | return videojs.createTimeRange(); | 379 | return videojs.createTimeRange(); |
363 | }; | 380 | }; |
... | @@ -691,9 +708,6 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) { | ... | @@ -691,9 +708,6 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) { |
691 | tech.setBandwidth(this); | 708 | tech.setBandwidth(this); |
692 | 709 | ||
693 | // package up all the work to append the segment | 710 | // package up all the work to append the segment |
694 | // if the segment is the start of a timestamp discontinuity, | ||
695 | // we have to wait until the sourcebuffer is empty before | ||
696 | // aborting the source buffer processing | ||
697 | segmentInfo = { | 711 | segmentInfo = { |
698 | // the segment's mediaIndex at the time it was received | 712 | // the segment's mediaIndex at the time it was received |
699 | mediaIndex: tech.mediaIndex, | 713 | mediaIndex: tech.mediaIndex, |
... | @@ -796,7 +810,10 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -796,7 +810,10 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
796 | } | 810 | } |
797 | 811 | ||
798 | event = event || {}; | 812 | event = event || {}; |
799 | segmentOffset = videojs.Hls.Playlist.duration(playlist, 0, mediaIndex) * 1000; | 813 | segmentOffset = this.playlists.expiredPreDiscontinuity_; |
814 | segmentOffset += this.playlists.expiredPostDiscontinuity_; | ||
815 | segmentOffset += videojs.Hls.Playlist.duration(playlist, playlist.mediaSequence, playlist.mediaSequence + mediaIndex); | ||
816 | segmentOffset *= 1000; | ||
800 | 817 | ||
801 | // transmux the segment data from MP2T to FLV | 818 | // transmux the segment data from MP2T to FLV |
802 | this.segmentParser_.parseSegmentBinaryData(bytes); | 819 | this.segmentParser_.parseSegmentBinaryData(bytes); |
... | @@ -808,10 +825,10 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -808,10 +825,10 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
808 | tags.push(this.segmentParser_.getNextTag()); | 825 | tags.push(this.segmentParser_.getNextTag()); |
809 | } | 826 | } |
810 | 827 | ||
811 | // Use the presentation timestamp of the ts segment to calculate its | ||
812 | // exact duration, since this may differ by fractions of a second | ||
813 | // from what is reported in the playlist | ||
814 | if (tags.length > 0) { | 828 | if (tags.length > 0) { |
829 | // Use the presentation timestamp of the ts segment to calculate its | ||
830 | // exact duration, since this may differ by fractions of a second | ||
831 | // from what is reported in the playlist | ||
815 | segment.preciseDuration = videojs.Hls.FlvTag.durationFromTags(tags) * 0.001; | 832 | segment.preciseDuration = videojs.Hls.FlvTag.durationFromTags(tags) * 0.001; |
816 | } | 833 | } |
817 | 834 | ... | ... |
... | @@ -61,45 +61,6 @@ test('metadata is generated for IDRs after a full NAL unit is written', function | ... | @@ -61,45 +61,6 @@ test('metadata is generated for IDRs after a full NAL unit is written', function |
61 | ok(h264Stream.tags[2].keyFrame, 'key frame is written'); | 61 | ok(h264Stream.tags[2].keyFrame, 'key frame is written'); |
62 | }); | 62 | }); |
63 | 63 | ||
64 | test('starting PTS values can be negative', function() { | ||
65 | var | ||
66 | H264ExtraData = videojs.Hls.H264ExtraData, | ||
67 | oldExtraData = H264ExtraData.prototype.extraDataTag, | ||
68 | oldMetadata = H264ExtraData.prototype.metaDataTag, | ||
69 | h264Stream; | ||
70 | |||
71 | H264ExtraData.prototype.extraDataTag = function() { | ||
72 | return 'extraDataTag'; | ||
73 | }; | ||
74 | H264ExtraData.prototype.metaDataTag = function() { | ||
75 | return 'metaDataTag'; | ||
76 | }; | ||
77 | |||
78 | h264Stream = new videojs.Hls.H264Stream(); | ||
79 | |||
80 | h264Stream.setTimeStampOffset(-100); | ||
81 | h264Stream.setNextTimeStamp(-100, -100, true); | ||
82 | h264Stream.writeBytes(accessUnitDelimiter, 0, accessUnitDelimiter.byteLength); | ||
83 | h264Stream.setNextTimeStamp(-99, -99, true); | ||
84 | h264Stream.writeBytes(accessUnitDelimiter, 0, accessUnitDelimiter.byteLength); | ||
85 | h264Stream.setNextTimeStamp(0, 0, true); | ||
86 | h264Stream.writeBytes(accessUnitDelimiter, 0, accessUnitDelimiter.byteLength); | ||
87 | // flush out the last tag | ||
88 | h264Stream.writeBytes(accessUnitDelimiter, 0, accessUnitDelimiter.byteLength); | ||
89 | |||
90 | strictEqual(h264Stream.tags.length, 3, 'three tags are ready'); | ||
91 | strictEqual(h264Stream.tags[0].pts, 0, 'the first PTS is zero'); | ||
92 | strictEqual(h264Stream.tags[0].dts, 0, 'the first DTS is zero'); | ||
93 | strictEqual(h264Stream.tags[1].pts, 1, 'the second PTS is one'); | ||
94 | strictEqual(h264Stream.tags[1].dts, 1, 'the second DTS is one'); | ||
95 | |||
96 | strictEqual(h264Stream.tags[2].pts, 100, 'the third PTS is 100'); | ||
97 | strictEqual(h264Stream.tags[2].dts, 100, 'the third DTS is 100'); | ||
98 | |||
99 | H264ExtraData.prototype.extraDataTag = oldExtraData; | ||
100 | H264ExtraData.prototype.metaDataTag = oldMetadata; | ||
101 | }); | ||
102 | |||
103 | test('make sure we add metadata and extra data at the beginning of a stream', function() { | 64 | test('make sure we add metadata and extra data at the beginning of a stream', function() { |
104 | var | 65 | var |
105 | H264ExtraData = videojs.Hls.H264ExtraData, | 66 | H264ExtraData = videojs.Hls.H264ExtraData, | ... | ... |
test/manifest/disc-sequence.js
0 → 100644
1 | { | ||
2 | "allowCache": true, | ||
3 | "mediaSequence": 0, | ||
4 | "discontinuitySequence": 3, | ||
5 | "segments": [ | ||
6 | { | ||
7 | "duration": 10, | ||
8 | "uri": "001.ts" | ||
9 | }, | ||
10 | { | ||
11 | "duration": 19, | ||
12 | "uri": "002.ts" | ||
13 | }, | ||
14 | { | ||
15 | "discontinuity": true, | ||
16 | "duration": 10, | ||
17 | "uri": "003.ts" | ||
18 | }, | ||
19 | { | ||
20 | "duration": 11, | ||
21 | "uri": "004.ts" | ||
22 | } | ||
23 | ], | ||
24 | "targetDuration": 19, | ||
25 | "endList": true | ||
26 | } |
test/manifest/disc-sequence.m3u8
0 → 100644
... | @@ -16,5 +16,6 @@ | ... | @@ -16,5 +16,6 @@ |
16 | "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts" | 16 | "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts" |
17 | } | 17 | } |
18 | ], | 18 | ], |
19 | "targetDuration": 8 | 19 | "targetDuration": 8, |
20 | "discontinuitySequence": 0 | ||
20 | } | 21 | } | ... | ... |
... | @@ -7,5 +7,6 @@ | ... | @@ -7,5 +7,6 @@ |
7 | "uri": "/test/ts-files/zencoder/gogo/00001.ts" | 7 | "uri": "/test/ts-files/zencoder/gogo/00001.ts" |
8 | } | 8 | } |
9 | ], | 9 | ], |
10 | "endList": true | ||
11 | } | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
10 | "endList": true, | ||
11 | "discontinuitySequence": 0 | ||
12 | } | ... | ... |
... | @@ -50,6 +50,20 @@ | ... | @@ -50,6 +50,20 @@ |
50 | strictEqual(loader.state, 'HAVE_NOTHING', 'no metadata has loaded yet'); | 50 | strictEqual(loader.state, 'HAVE_NOTHING', 'no metadata has loaded yet'); |
51 | }); | 51 | }); |
52 | 52 | ||
53 | test('starts with no expired time', function() { | ||
54 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); | ||
55 | requests.pop().respond(200, null, | ||
56 | '#EXTM3U\n' + | ||
57 | '#EXTINF:10,\n' + | ||
58 | '0.ts\n'); | ||
59 | equal(loader.expiredPreDiscontinuity_, | ||
60 | 0, | ||
61 | 'zero seconds expired pre-discontinuity'); | ||
62 | equal(loader.expiredPostDiscontinuity_, | ||
63 | 0, | ||
64 | 'zero seconds expired post-discontinuity'); | ||
65 | }); | ||
66 | |||
53 | test('requests the initial playlist immediately', function() { | 67 | test('requests the initial playlist immediately', function() { |
54 | new videojs.Hls.PlaylistLoader('master.m3u8'); | 68 | new videojs.Hls.PlaylistLoader('master.m3u8'); |
55 | strictEqual(requests.length, 1, 'made a request'); | 69 | strictEqual(requests.length, 1, 'made a request'); |
... | @@ -160,6 +174,105 @@ | ... | @@ -160,6 +174,105 @@ |
160 | strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); | 174 | strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); |
161 | }); | 175 | }); |
162 | 176 | ||
177 | test('increments expired seconds after a segment is removed', function() { | ||
178 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
179 | requests.pop().respond(200, null, | ||
180 | '#EXTM3U\n' + | ||
181 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
182 | '#EXTINF:10,\n' + | ||
183 | '0.ts\n' + | ||
184 | '#EXTINF:10,\n' + | ||
185 | '1.ts\n' + | ||
186 | '#EXTINF:10,\n' + | ||
187 | '2.ts\n' + | ||
188 | '#EXTINF:10,\n' + | ||
189 | '3.ts\n'); | ||
190 | clock.tick(10 * 1000); // 10s, one target duration | ||
191 | requests.pop().respond(200, null, | ||
192 | '#EXTM3U\n' + | ||
193 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | ||
194 | '#EXTINF:10,\n' + | ||
195 | '1.ts\n' + | ||
196 | '#EXTINF:10,\n' + | ||
197 | '2.ts\n' + | ||
198 | '#EXTINF:10,\n' + | ||
199 | '3.ts\n' + | ||
200 | '#EXTINF:10,\n' + | ||
201 | '4.ts\n'); | ||
202 | equal(loader.expiredPostDiscontinuity_, 10, 'expired one segment'); | ||
203 | }); | ||
204 | |||
205 | test('increments expired seconds after a discontinuity', function() { | ||
206 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
207 | requests.pop().respond(200, null, | ||
208 | '#EXTM3U\n' + | ||
209 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
210 | '#EXTINF:10,\n' + | ||
211 | '0.ts\n' + | ||
212 | '#EXTINF:3,\n' + | ||
213 | '1.ts\n' + | ||
214 | '#EXT-X-DISCONTINUITY\n' + | ||
215 | '#EXTINF:4,\n' + | ||
216 | '2.ts\n'); | ||
217 | clock.tick(10 * 1000); // 10s, one target duration | ||
218 | requests.pop().respond(200, null, | ||
219 | '#EXTM3U\n' + | ||
220 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | ||
221 | '#EXTINF:3,\n' + | ||
222 | '1.ts\n' + | ||
223 | '#EXT-X-DISCONTINUITY\n' + | ||
224 | '#EXTINF:4,\n' + | ||
225 | '2.ts\n'); | ||
226 | equal(loader.expiredPreDiscontinuity_, 0, 'identifies pre-discontinuity time'); | ||
227 | equal(loader.expiredPostDiscontinuity_, 10, 'expired one segment'); | ||
228 | |||
229 | clock.tick(10 * 1000); // 10s, one target duration | ||
230 | requests.pop().respond(200, null, | ||
231 | '#EXTM3U\n' + | ||
232 | '#EXT-X-MEDIA-SEQUENCE:2\n' + | ||
233 | '#EXT-X-DISCONTINUITY\n' + | ||
234 | '#EXTINF:4,\n' + | ||
235 | '2.ts\n'); | ||
236 | equal(loader.expiredPreDiscontinuity_, 0, 'tracked time across the discontinuity'); | ||
237 | equal(loader.expiredPostDiscontinuity_, 13, 'no expirations after the discontinuity yet'); | ||
238 | |||
239 | clock.tick(10 * 1000); // 10s, one target duration | ||
240 | requests.pop().respond(200, null, | ||
241 | '#EXTM3U\n' + | ||
242 | '#EXT-X-MEDIA-SEQUENCE:3\n' + | ||
243 | '#EXT-X-DISCONTINUITY-SEQUENCE:1\n' + | ||
244 | '#EXTINF:10,\n' + | ||
245 | '3.ts\n'); | ||
246 | equal(loader.expiredPreDiscontinuity_, 13, 'did not increment pre-discontinuity'); | ||
247 | equal(loader.expiredPostDiscontinuity_, 4, 'expired post-discontinuity'); | ||
248 | }); | ||
249 | |||
250 | test('tracks expired seconds properly when two discontinuities expire at once', function() { | ||
251 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
252 | requests.pop().respond(200, null, | ||
253 | '#EXTM3U\n' + | ||
254 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
255 | '#EXTINF:4,\n' + | ||
256 | '0.ts\n' + | ||
257 | '#EXT-X-DISCONTINUITY\n' + | ||
258 | '#EXTINF:5,\n' + | ||
259 | '1.ts\n' + | ||
260 | '#EXT-X-DISCONTINUITY\n' + | ||
261 | '#EXTINF:6,\n' + | ||
262 | '2.ts\n' + | ||
263 | '#EXTINF:7,\n' + | ||
264 | '3.ts\n'); | ||
265 | clock.tick(10 * 1000); | ||
266 | requests.pop().respond(200, null, | ||
267 | '#EXTM3U\n' + | ||
268 | '#EXT-X-MEDIA-SEQUENCE:3\n' + | ||
269 | '#EXT-X-DISCONTINUITY-SEQUENCE:2\n' + | ||
270 | '#EXTINF:7,\n' + | ||
271 | '3.ts\n'); | ||
272 | equal(loader.expiredPreDiscontinuity_, 4 + 5, 'tracked pre-discontinuity time'); | ||
273 | equal(loader.expiredPostDiscontinuity_, 6, 'tracked post-discontinuity time'); | ||
274 | }); | ||
275 | |||
163 | test('emits an error when an initial playlist request fails', function() { | 276 | test('emits an error when an initial playlist request fails', function() { |
164 | var | 277 | var |
165 | errors = [], | 278 | errors = [], | ... | ... |
... | @@ -16,28 +16,31 @@ | ... | @@ -16,28 +16,31 @@ |
16 | equal(duration, Infinity, 'duration is infinity'); | 16 | equal(duration, Infinity, 'duration is infinity'); |
17 | }); | 17 | }); |
18 | 18 | ||
19 | test('interval duration does not include upcoming live segments', function() { | 19 | test('interval duration accounts for media sequences', function() { |
20 | var duration = Playlist.duration({ | 20 | var duration = Playlist.duration({ |
21 | mediaSequence: 10, | ||
22 | endList: true, | ||
21 | segments: [{ | 23 | segments: [{ |
22 | duration: 4, | 24 | duration: 10, |
23 | uri: '0.ts' | 25 | uri: '10.ts' |
24 | }, { | 26 | }, { |
25 | duration: 10, | 27 | duration: 10, |
26 | uri: '1.ts' | 28 | uri: '11.ts' |
27 | }, { | 29 | }, { |
28 | duration: 10, | 30 | duration: 10, |
29 | uri: '2.ts' | 31 | uri: '12.ts' |
30 | }, { | 32 | }, { |
31 | duration: 10, | 33 | duration: 10, |
32 | uri: '3.ts' | 34 | uri: '13.ts' |
33 | }] | 35 | }] |
34 | }, 0, 3); | 36 | }, 0, 14); |
35 | 37 | ||
36 | equal(duration, 4, 'does not include upcoming live segments'); | 38 | equal(duration, 14 * 10, 'duration includes dropped segments'); |
37 | }); | 39 | }); |
38 | 40 | ||
39 | test('calculates seekable time ranges from the available segments', function() { | 41 | test('calculates seekable time ranges from the available segments', function() { |
40 | var playlist = { | 42 | var playlist = { |
43 | mediaSequence: 0, | ||
41 | segments: [{ | 44 | segments: [{ |
42 | duration: 10, | 45 | duration: 10, |
43 | uri: '0.ts' | 46 | uri: '0.ts' |
... | @@ -66,6 +69,7 @@ | ... | @@ -66,6 +69,7 @@ |
66 | 69 | ||
67 | test('seekable end is three target durations from the actual end of live playlists', function() { | 70 | test('seekable end is three target durations from the actual end of live playlists', function() { |
68 | var seekable = Playlist.seekable({ | 71 | var seekable = Playlist.seekable({ |
72 | mediaSequence: 0, | ||
69 | segments: [{ | 73 | segments: [{ |
70 | duration: 7, | 74 | duration: 7, |
71 | uri: '0.ts' | 75 | uri: '0.ts' |
... | @@ -107,6 +111,7 @@ | ... | @@ -107,6 +111,7 @@ |
107 | test('seekable end accounts for non-standard target durations', function() { | 111 | test('seekable end accounts for non-standard target durations', function() { |
108 | var seekable = Playlist.seekable({ | 112 | var seekable = Playlist.seekable({ |
109 | targetDuration: 2, | 113 | targetDuration: 2, |
114 | mediaSequence: 0, | ||
110 | segments: [{ | 115 | segments: [{ |
111 | duration: 2, | 116 | duration: 2, |
112 | uri: '0.ts' | 117 | uri: '0.ts' | ... | ... |
... | @@ -293,6 +293,31 @@ test('calculates the duration if needed', function() { | ... | @@ -293,6 +293,31 @@ test('calculates the duration if needed', function() { |
293 | 'duration is calculated'); | 293 | 'duration is calculated'); |
294 | }); | 294 | }); |
295 | 295 | ||
296 | test('translates seekable by the starting time for live playlists', function() { | ||
297 | var seekable; | ||
298 | player.src({ | ||
299 | src: 'media.m3u8', | ||
300 | type: 'application/vnd.apple.mpegurl' | ||
301 | }); | ||
302 | openMediaSource(player); | ||
303 | requests.shift().respond(200, null, | ||
304 | '#EXTM3U\n' + | ||
305 | '#EXT-X-MEDIA-SEQUENCE:15\n' + | ||
306 | '#EXTINF:10,\n' + | ||
307 | '0.ts\n' + | ||
308 | '#EXTINF:10,\n' + | ||
309 | '1.ts\n' + | ||
310 | '#EXTINF:10,\n' + | ||
311 | '2.ts\n' + | ||
312 | '#EXTINF:10,\n' + | ||
313 | '3.ts\n'); | ||
314 | |||
315 | seekable = player.seekable(); | ||
316 | equal(seekable.length, 1, 'one seekable range'); | ||
317 | equal(seekable.start(0), 0, 'the earliest possible position is at zero'); | ||
318 | equal(seekable.end(0), 10, 'end is relative to the start'); | ||
319 | }); | ||
320 | |||
296 | test('starts downloading a segment on loadedmetadata', function() { | 321 | test('starts downloading a segment on loadedmetadata', function() { |
297 | player.src({ | 322 | player.src({ |
298 | src: 'manifest/media.m3u8', | 323 | src: 'manifest/media.m3u8', | ... | ... |
-
Please register or sign in to post a comment