@dmlap implement seekable for live streams ([view](https://github.com/videojs/vi…
…deojs-contrib-hls/pull/295))
Showing
57 changed files
with
1081 additions
and
302 deletions
... | @@ -2,7 +2,7 @@ CHANGELOG | ... | @@ -2,7 +2,7 @@ CHANGELOG |
2 | ========= | 2 | ========= |
3 | 3 | ||
4 | ## HEAD (Unreleased) | 4 | ## HEAD (Unreleased) |
5 | _(none)_ | 5 | * @dmlap implement seekable for live streams. Fix in-band metadata timing for live streams. ([view](https://github.com/videojs/videojs-contrib-hls/pull/295)) |
6 | 6 | ||
7 | -------------------- | 7 | -------------------- |
8 | 8 | ... | ... |
... | @@ -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> --> | ... | ... |
... | @@ -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', { | ... | ... |
... | @@ -37,7 +37,8 @@ | ... | @@ -37,7 +37,8 @@ |
37 | if (tag.data[i] === 0) { | 37 | if (tag.data[i] === 0) { |
38 | // parse the text fields | 38 | // parse the text fields |
39 | tag.description = parseUtf8(tag.data, 1, i); | 39 | tag.description = parseUtf8(tag.data, 1, i); |
40 | tag.value = parseUtf8(tag.data, i + 1, tag.data.length); | 40 | // do not include the null terminator in the tag value |
41 | tag.value = parseUtf8(tag.data, i + 1, tag.data.length - 1); | ||
41 | break; | 42 | break; |
42 | } | 43 | } |
43 | } | 44 | } |
... | @@ -173,13 +174,6 @@ | ... | @@ -173,13 +174,6 @@ |
173 | (tag.data[19]); | 174 | (tag.data[19]); |
174 | } | 175 | } |
175 | 176 | ||
176 | // adjust the PTS values to align with the video and audio | ||
177 | // streams | ||
178 | if (this.timestampOffset) { | ||
179 | tag.pts -= this.timestampOffset; | ||
180 | tag.dts -= this.timestampOffset; | ||
181 | } | ||
182 | |||
183 | // parse one or more ID3 frames | 177 | // parse one or more ID3 frames |
184 | // http://id3.org/id3v2.3.0#ID3v2_frame_overview | 178 | // http://id3.org/id3v2.3.0#ID3v2_frame_overview |
185 | do { | 179 | do { | ... | ... |
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); | ... | ... |
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. | ||
14 | * | ||
15 | * @param playlist {object} a media playlist object | ||
16 | * @param startSequence {number} (optional) an inclusive lower | ||
17 | * boundary for the playlist. Defaults to 0. | ||
18 | * @param endSequence {number} (optional) an exclusive upper boundary | ||
19 | * for the playlist. Defaults to playlist length. | ||
20 | * @return {number} the duration between the start index and end | ||
21 | * index. | ||
22 | */ | ||
23 | segmentsDuration = function(playlist, startSequence, endSequence) { | ||
24 | var targetDuration, i, segment, expiredSegmentCount, result = 0; | ||
25 | |||
26 | startSequence = startSequence || 0; | ||
27 | i = startSequence; | ||
28 | endSequence = endSequence !== undefined ? endSequence : (playlist.segments || []).length; | ||
29 | targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION; | ||
30 | |||
31 | // estimate expired segment duration using the target duration | ||
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]; | ||
39 | result += segment.preciseDuration || | ||
40 | segment.duration || | ||
41 | targetDuration; | ||
42 | } | ||
43 | |||
44 | return result; | ||
45 | }; | ||
46 | |||
47 | /** | ||
48 | * Calculates the duration of a playlist. If a start and end index | ||
49 | * are specified, the duration will be for the subset of the media | ||
50 | * timeline between those two indices. The total duration for live | ||
51 | * playlists is always Infinity. | ||
52 | * @param playlist {object} a media playlist object | ||
53 | * @param startSequence {number} (optional) an inclusive lower | ||
54 | * boundary for the playlist. Defaults to 0. | ||
55 | * @param endSequence {number} (optional) an exclusive upper boundary | ||
56 | * for the playlist. Defaults to playlist length. | ||
57 | * @return {number} the duration between the start index and end | ||
58 | * index. | ||
59 | */ | ||
60 | duration = function(playlist, startSequence, endSequence) { | ||
61 | if (!playlist) { | ||
62 | return 0; | ||
63 | } | ||
64 | |||
65 | // if a slice of the total duration is not requested, use | ||
66 | // playlist-level duration indicators when they're present | ||
67 | if (startSequence === undefined && endSequence === undefined) { | ||
68 | // if present, use the duration specified in the playlist | ||
69 | if (playlist.totalDuration) { | ||
70 | return playlist.totalDuration; | ||
71 | } | ||
72 | |||
73 | // duration should be Infinity for live playlists | ||
74 | if (!playlist.endList) { | ||
75 | return window.Infinity; | ||
76 | } | ||
77 | } | ||
78 | |||
79 | // calculate the total duration based on the segment durations | ||
80 | return segmentsDuration(playlist, | ||
81 | startSequence, | ||
82 | endSequence); | ||
83 | }; | ||
84 | |||
85 | /** | ||
86 | * Calculates the interval of time that is currently seekable in a | ||
87 | * playlist. | ||
88 | * @param playlist {object} a media playlist object | ||
89 | * @return {TimeRanges} the periods of time that are valid targets | ||
90 | * for seeking | ||
91 | */ | ||
92 | seekable = function(playlist) { | ||
93 | var start, end, liveBuffer, targetDuration, segment, pending, i; | ||
94 | |||
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 | start = segmentsDuration(playlist, 0, playlist.mediaSequence); | ||
105 | end = start + segmentsDuration(playlist, | ||
106 | playlist.mediaSequence, | ||
107 | playlist.mediaSequence + playlist.segments.length); | ||
108 | targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION; | ||
109 | |||
110 | // live playlists should not expose three segment durations worth | ||
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); | ||
132 | }; | ||
133 | |||
134 | // exports | ||
135 | videojs.Hls.Playlist = { | ||
136 | duration: duration, | ||
137 | seekable: seekable | ||
138 | }; | ||
139 | })(window, window.videojs); |
... | @@ -19,10 +19,7 @@ | ... | @@ -19,10 +19,7 @@ |
19 | streamBuffer = new Uint8Array(MP2T_PACKET_LENGTH), | 19 | streamBuffer = new Uint8Array(MP2T_PACKET_LENGTH), |
20 | streamBufferByteCount = 0, | 20 | streamBufferByteCount = 0, |
21 | h264Stream = new H264Stream(), | 21 | h264Stream = new H264Stream(), |
22 | aacStream = new AacStream(), | 22 | aacStream = new AacStream(); |
23 | h264HasTimeStampOffset = false, | ||
24 | aacHasTimeStampOffset = false, | ||
25 | timeStampOffset; | ||
26 | 23 | ||
27 | // expose the stream metadata | 24 | // expose the stream metadata |
28 | self.stream = { | 25 | self.stream = { |
... | @@ -34,6 +31,15 @@ | ... | @@ -34,6 +31,15 @@ |
34 | // allow in-band metadata to be observed | 31 | // allow in-band metadata to be observed |
35 | self.metadataStream = new MetadataStream(); | 32 | self.metadataStream = new MetadataStream(); |
36 | 33 | ||
34 | this.mediaTimelineOffset = null; | ||
35 | |||
36 | // The first timestamp value encountered during parsing. This | ||
37 | // value can be used to determine the relative timing between | ||
38 | // frames and the start of the current timestamp sequence. It | ||
39 | // should be reset to null before parsing a segment with | ||
40 | // discontinuous timestamp values from previous segments. | ||
41 | self.timestampOffset = null; | ||
42 | |||
37 | // For information on the FLV format, see | 43 | // For information on the FLV format, see |
38 | // http://download.macromedia.com/f4v/video_file_format_spec_v10_1.pdf. | 44 | // http://download.macromedia.com/f4v/video_file_format_spec_v10_1.pdf. |
39 | // Technically, this function returns the header and a metadata FLV tag | 45 | // Technically, this function returns the header and a metadata FLV tag |
... | @@ -354,31 +360,18 @@ | ... | @@ -354,31 +360,18 @@ |
354 | // Skip past "optional" portion of PTS header | 360 | // Skip past "optional" portion of PTS header |
355 | offset += pesHeaderLength; | 361 | offset += pesHeaderLength; |
356 | 362 | ||
357 | // align the metadata stream PTS values with the start of | 363 | // keep track of the earliest encounted PTS value so |
358 | // the other elementary streams | 364 | // external parties can align timestamps across |
359 | if (!self.metadataStream.timestampOffset) { | 365 | // discontinuities |
360 | self.metadataStream.timestampOffset = pts; | 366 | if (self.timestampOffset === null) { |
367 | self.timestampOffset = pts; | ||
361 | } | 368 | } |
362 | 369 | ||
363 | if (pid === self.stream.programMapTable[STREAM_TYPES.h264]) { | 370 | if (pid === self.stream.programMapTable[STREAM_TYPES.h264]) { |
364 | if (!h264HasTimeStampOffset) { | ||
365 | h264HasTimeStampOffset = true; | ||
366 | if (timeStampOffset === undefined) { | ||
367 | timeStampOffset = pts; | ||
368 | } | ||
369 | h264Stream.setTimeStampOffset(timeStampOffset); | ||
370 | } | ||
371 | h264Stream.setNextTimeStamp(pts, | 371 | h264Stream.setNextTimeStamp(pts, |
372 | dts, | 372 | dts, |
373 | dataAlignmentIndicator); | 373 | dataAlignmentIndicator); |
374 | } else if (pid === self.stream.programMapTable[STREAM_TYPES.adts]) { | 374 | } else if (pid === self.stream.programMapTable[STREAM_TYPES.adts]) { |
375 | if (!aacHasTimeStampOffset) { | ||
376 | aacHasTimeStampOffset = true; | ||
377 | if (timeStampOffset === undefined) { | ||
378 | timeStampOffset = pts; | ||
379 | } | ||
380 | aacStream.setTimeStampOffset(timeStampOffset); | ||
381 | } | ||
382 | aacStream.setNextTimeStamp(pts, | 375 | aacStream.setNextTimeStamp(pts, |
383 | pesPacketSize, | 376 | pesPacketSize, |
384 | dataAlignmentIndicator); | 377 | dataAlignmentIndicator); | ... | ... |
... | @@ -39,9 +39,11 @@ videojs.Hls = videojs.Flash.extend({ | ... | @@ -39,9 +39,11 @@ 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 | ||
47 | videojs.Hls.prototype.src.call(this, options.source && options.source.src); | 49 | videojs.Hls.prototype.src.call(this, options.source && options.source.src); |
... | @@ -87,43 +89,7 @@ videojs.Hls.prototype.src = function(src) { | ... | @@ -87,43 +89,7 @@ videojs.Hls.prototype.src = function(src) { |
87 | 89 | ||
88 | // if the stream contains ID3 metadata, expose that as a metadata | 90 | // if the stream contains ID3 metadata, expose that as a metadata |
89 | // text track | 91 | // text track |
90 | (function() { | 92 | this.setupMetadataCueTranslation_(); |
91 | var | ||
92 | metadataStream = tech.segmentParser_.metadataStream, | ||
93 | textTrack; | ||
94 | |||
95 | // only expose metadata tracks to video.js versions that support | ||
96 | // dynamic text tracks (4.12+) | ||
97 | if (!tech.player().addTextTrack) { | ||
98 | return; | ||
99 | } | ||
100 | |||
101 | metadataStream.on('data', function(metadata) { | ||
102 | var i, cue, frame, time, hexDigit; | ||
103 | |||
104 | // create the metadata track if this is the first ID3 tag we've | ||
105 | // seen | ||
106 | if (!textTrack) { | ||
107 | textTrack = tech.player().addTextTrack('metadata', 'Timed Metadata'); | ||
108 | |||
109 | // build the dispatch type from the stream descriptor | ||
110 | // https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track | ||
111 | textTrack.inBandMetadataTrackDispatchType = videojs.Hls.SegmentParser.STREAM_TYPES.metadata.toString(16).toUpperCase(); | ||
112 | for (i = 0; i < metadataStream.descriptor.length; i++) { | ||
113 | hexDigit = ('00' + metadataStream.descriptor[i].toString(16).toUpperCase()).slice(-2); | ||
114 | textTrack.inBandMetadataTrackDispatchType += hexDigit; | ||
115 | } | ||
116 | } | ||
117 | |||
118 | for (i = 0; i < metadata.frames.length; i++) { | ||
119 | frame = metadata.frames[i]; | ||
120 | time = metadata.pts / 1000; | ||
121 | cue = new window.VTTCue(time, time, frame.value || frame.url || ''); | ||
122 | cue.frame = frame; | ||
123 | textTrack.addCue(cue); | ||
124 | } | ||
125 | }); | ||
126 | })(); | ||
127 | 93 | ||
128 | // load the MediaSource into the player | 94 | // load the MediaSource into the player |
129 | this.mediaSource.addEventListener('sourceopen', videojs.bind(this, this.handleSourceOpen)); | 95 | this.mediaSource.addEventListener('sourceopen', videojs.bind(this, this.handleSourceOpen)); |
... | @@ -280,6 +246,75 @@ videojs.Hls.prototype.handleSourceOpen = function() { | ... | @@ -280,6 +246,75 @@ videojs.Hls.prototype.handleSourceOpen = function() { |
280 | } | 246 | } |
281 | }; | 247 | }; |
282 | 248 | ||
249 | // register event listeners to transform in-band metadata events into | ||
250 | // VTTCues on a text track | ||
251 | videojs.Hls.prototype.setupMetadataCueTranslation_ = function() { | ||
252 | var | ||
253 | tech = this, | ||
254 | metadataStream = tech.segmentParser_.metadataStream, | ||
255 | textTrack; | ||
256 | |||
257 | // only expose metadata tracks to video.js versions that support | ||
258 | // dynamic text tracks (4.12+) | ||
259 | if (!tech.player().addTextTrack) { | ||
260 | return; | ||
261 | } | ||
262 | |||
263 | // add a metadata cue whenever a metadata event is triggered during | ||
264 | // segment parsing | ||
265 | metadataStream.on('data', function(metadata) { | ||
266 | var i, cue, frame, time, media, segmentOffset, hexDigit; | ||
267 | |||
268 | // create the metadata track if this is the first ID3 tag we've | ||
269 | // seen | ||
270 | if (!textTrack) { | ||
271 | textTrack = tech.player().addTextTrack('metadata', 'Timed Metadata'); | ||
272 | |||
273 | // build the dispatch type from the stream descriptor | ||
274 | // https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track | ||
275 | textTrack.inBandMetadataTrackDispatchType = videojs.Hls.SegmentParser.STREAM_TYPES.metadata.toString(16).toUpperCase(); | ||
276 | for (i = 0; i < metadataStream.descriptor.length; i++) { | ||
277 | hexDigit = ('00' + metadataStream.descriptor[i].toString(16).toUpperCase()).slice(-2); | ||
278 | textTrack.inBandMetadataTrackDispatchType += hexDigit; | ||
279 | } | ||
280 | } | ||
281 | |||
282 | // calculate the start time for the segment that is currently being parsed | ||
283 | media = tech.playlists.media(); | ||
284 | segmentOffset = tech.playlists.expiredPreDiscontinuity_ + tech.playlists.expiredPostDiscontinuity_; | ||
285 | segmentOffset += videojs.Hls.Playlist.duration(media, media.mediaSequence, media.mediaSequence + tech.mediaIndex); | ||
286 | |||
287 | // create cue points for all the ID3 frames in this metadata event | ||
288 | for (i = 0; i < metadata.frames.length; i++) { | ||
289 | frame = metadata.frames[i]; | ||
290 | time = tech.segmentParser_.mediaTimelineOffset + ((metadata.pts - tech.segmentParser_.timestampOffset) * 0.001); | ||
291 | cue = new window.VTTCue(time, time, frame.value || frame.url || ''); | ||
292 | cue.frame = frame; | ||
293 | textTrack.addCue(cue); | ||
294 | } | ||
295 | }); | ||
296 | |||
297 | // when seeking, clear out all cues ahead of the earliest position | ||
298 | // in the new segment. keep earlier cues around so they can still be | ||
299 | // programmatically inspected even though they've already fired | ||
300 | tech.on(tech.player(), 'seeking', function() { | ||
301 | var media, startTime, i; | ||
302 | if (!textTrack) { | ||
303 | return; | ||
304 | } | ||
305 | media = tech.playlists.media(); | ||
306 | startTime = tech.playlists.expiredPreDiscontinuity_ + tech.playlists.expiredPostDiscontinuity_; | ||
307 | startTime += videojs.Hls.Playlist.duration(media, media.mediaSequence, media.mediaSequence + tech.mediaIndex); | ||
308 | |||
309 | i = textTrack.cues.length; | ||
310 | while (i--) { | ||
311 | if (textTrack.cues[i].startTime >= startTime) { | ||
312 | textTrack.removeCue(textTrack.cues[i]); | ||
313 | } | ||
314 | } | ||
315 | }); | ||
316 | }; | ||
317 | |||
283 | /** | 318 | /** |
284 | * Reset the mediaIndex if play() is called after the video has | 319 | * Reset the mediaIndex if play() is called after the video has |
285 | * ended. | 320 | * ended. |
... | @@ -350,18 +385,37 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) { | ... | @@ -350,18 +385,37 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) { |
350 | videojs.Hls.prototype.duration = function() { | 385 | videojs.Hls.prototype.duration = function() { |
351 | var playlists = this.playlists; | 386 | var playlists = this.playlists; |
352 | if (playlists) { | 387 | if (playlists) { |
353 | return videojs.Hls.getPlaylistTotalDuration(playlists.media()); | 388 | return videojs.Hls.Playlist.duration(playlists.media()); |
354 | } | 389 | } |
355 | return 0; | 390 | return 0; |
356 | }; | 391 | }; |
357 | 392 | ||
393 | videojs.Hls.prototype.seekable = function() { | ||
394 | var absoluteSeekable, startOffset, media; | ||
395 | |||
396 | if (!this.playlists) { | ||
397 | return videojs.createTimeRange(); | ||
398 | } | ||
399 | media = this.playlists.media(); | ||
400 | if (!media) { | ||
401 | return videojs.createTimeRange(); | ||
402 | } | ||
403 | |||
404 | // report the seekable range relative to the earliest possible | ||
405 | // position when the stream was first loaded | ||
406 | absoluteSeekable = videojs.Hls.Playlist.seekable(media); | ||
407 | startOffset = this.playlists.expiredPostDiscontinuity_ - this.playlists.expiredPreDiscontinuity_; | ||
408 | return videojs.createTimeRange(startOffset, | ||
409 | startOffset + (absoluteSeekable.end(0) - absoluteSeekable.start(0))); | ||
410 | }; | ||
411 | |||
358 | /** | 412 | /** |
359 | * Update the player duration | 413 | * Update the player duration |
360 | */ | 414 | */ |
361 | videojs.Hls.prototype.updateDuration = function(playlist) { | 415 | videojs.Hls.prototype.updateDuration = function(playlist) { |
362 | var player = this.player(), | 416 | var player = this.player(), |
363 | oldDuration = player.duration(), | 417 | oldDuration = player.duration(), |
364 | newDuration = videojs.Hls.getPlaylistTotalDuration(playlist); | 418 | newDuration = videojs.Hls.Playlist.duration(playlist); |
365 | 419 | ||
366 | // if the duration has changed, invalidate the cached value | 420 | // if the duration has changed, invalidate the cached value |
367 | if (oldDuration !== newDuration) { | 421 | if (oldDuration !== newDuration) { |
... | @@ -684,9 +738,6 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) { | ... | @@ -684,9 +738,6 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) { |
684 | tech.setBandwidth(this); | 738 | tech.setBandwidth(this); |
685 | 739 | ||
686 | // package up all the work to append the segment | 740 | // package up all the work to append the segment |
687 | // if the segment is the start of a timestamp discontinuity, | ||
688 | // we have to wait until the sourcebuffer is empty before | ||
689 | // aborting the source buffer processing | ||
690 | segmentInfo = { | 741 | segmentInfo = { |
691 | // the segment's mediaIndex at the time it was received | 742 | // the segment's mediaIndex at the time it was received |
692 | mediaIndex: tech.mediaIndex, | 743 | mediaIndex: tech.mediaIndex, |
... | @@ -789,7 +840,20 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -789,7 +840,20 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
789 | } | 840 | } |
790 | 841 | ||
791 | event = event || {}; | 842 | event = event || {}; |
792 | segmentOffset = videojs.Hls.getPlaylistDuration(playlist, 0, mediaIndex) * 1000; | 843 | segmentOffset = this.playlists.expiredPreDiscontinuity_; |
844 | segmentOffset += this.playlists.expiredPostDiscontinuity_; | ||
845 | segmentOffset += videojs.Hls.Playlist.duration(playlist, playlist.mediaSequence, playlist.mediaSequence + mediaIndex); | ||
846 | segmentOffset *= 1000; | ||
847 | |||
848 | // if this segment starts is the start of a new discontinuity | ||
849 | // sequence, the segment parser's timestamp offset must be | ||
850 | // re-calculated | ||
851 | if (segment.discontinuity) { | ||
852 | this.segmentParser_.mediaTimelineOffset = segmentOffset * 0.001; | ||
853 | this.segmentParser_.timestampOffset = null; | ||
854 | } else if (this.segmentParser_.mediaTimelineOffset === null) { | ||
855 | this.segmentParser_.mediaTimelineOffset = segmentOffset * 0.001; | ||
856 | } | ||
793 | 857 | ||
794 | // transmux the segment data from MP2T to FLV | 858 | // transmux the segment data from MP2T to FLV |
795 | this.segmentParser_.parseSegmentBinaryData(bytes); | 859 | this.segmentParser_.parseSegmentBinaryData(bytes); |
... | @@ -801,10 +865,10 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -801,10 +865,10 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
801 | tags.push(this.segmentParser_.getNextTag()); | 865 | tags.push(this.segmentParser_.getNextTag()); |
802 | } | 866 | } |
803 | 867 | ||
804 | // Use the presentation timestamp of the ts segment to calculate its | ||
805 | // exact duration, since this may differ by fractions of a second | ||
806 | // from what is reported in the playlist | ||
807 | if (tags.length > 0) { | 868 | if (tags.length > 0) { |
869 | // Use the presentation timestamp of the ts segment to calculate its | ||
870 | // exact duration, since this may differ by fractions of a second | ||
871 | // from what is reported in the playlist | ||
808 | segment.preciseDuration = videojs.Hls.FlvTag.durationFromTags(tags) * 0.001; | 872 | segment.preciseDuration = videojs.Hls.FlvTag.durationFromTags(tags) * 0.001; |
809 | } | 873 | } |
810 | 874 | ||
... | @@ -976,20 +1040,9 @@ videojs.Hls.canPlaySource = function(srcObj) { | ... | @@ -976,20 +1040,9 @@ videojs.Hls.canPlaySource = function(srcObj) { |
976 | * @return {number} the duration between the start index and end index. | 1040 | * @return {number} the duration between the start index and end index. |
977 | */ | 1041 | */ |
978 | videojs.Hls.getPlaylistDuration = function(playlist, startIndex, endIndex) { | 1042 | videojs.Hls.getPlaylistDuration = function(playlist, startIndex, endIndex) { |
979 | var dur = 0, | 1043 | videojs.log.warn('videojs.Hls.getPlaylistDuration is deprecated. ' + |
980 | segment, | 1044 | 'Use videojs.Hls.Playlist.duration instead'); |
981 | i; | 1045 | 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 | }; | 1046 | }; |
994 | 1047 | ||
995 | /** | 1048 | /** |
... | @@ -998,21 +1051,9 @@ videojs.Hls.getPlaylistDuration = function(playlist, startIndex, endIndex) { | ... | @@ -998,21 +1051,9 @@ videojs.Hls.getPlaylistDuration = function(playlist, startIndex, endIndex) { |
998 | * @return {number} the currently known duration, in seconds | 1051 | * @return {number} the currently known duration, in seconds |
999 | */ | 1052 | */ |
1000 | videojs.Hls.getPlaylistTotalDuration = function(playlist) { | 1053 | videojs.Hls.getPlaylistTotalDuration = function(playlist) { |
1001 | if (!playlist) { | 1054 | videojs.log.warn('videojs.Hls.getPlaylistTotalDuration is deprecated. ' + |
1002 | return 0; | 1055 | 'Use videojs.Hls.Playlist.duration instead'); |
1003 | } | 1056 | 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 | }; | 1057 | }; |
1017 | 1058 | ||
1018 | /** | 1059 | /** | ... | ... |
... | @@ -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, | ... | ... |
... | @@ -75,7 +75,7 @@ module.exports = function(config) { | ... | @@ -75,7 +75,7 @@ module.exports = function(config) { |
75 | '../node_modules/sinon/lib/sinon/util/fake_xml_http_request.js', | 75 | '../node_modules/sinon/lib/sinon/util/fake_xml_http_request.js', |
76 | '../node_modules/sinon/lib/sinon/util/xhr_ie.js', | 76 | '../node_modules/sinon/lib/sinon/util/xhr_ie.js', |
77 | '../node_modules/sinon/lib/sinon/util/fake_timers.js', | 77 | '../node_modules/sinon/lib/sinon/util/fake_timers.js', |
78 | '../node_modules/video.js/dist/video-js/video.js', | 78 | '../node_modules/video.js/dist/video-js/video.dev.js', |
79 | '../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js', | 79 | '../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js', |
80 | '../node_modules/pkcs7/dist/pkcs7.unpad.js', | 80 | '../node_modules/pkcs7/dist/pkcs7.unpad.js', |
81 | '../test/karma-qunit-shim.js', | 81 | '../test/karma-qunit-shim.js', |
... | @@ -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', | ... | ... |
... | @@ -39,7 +39,7 @@ module.exports = function(config) { | ... | @@ -39,7 +39,7 @@ module.exports = function(config) { |
39 | '../node_modules/sinon/lib/sinon/util/fake_xml_http_request.js', | 39 | '../node_modules/sinon/lib/sinon/util/fake_xml_http_request.js', |
40 | '../node_modules/sinon/lib/sinon/util/xhr_ie.js', | 40 | '../node_modules/sinon/lib/sinon/util/xhr_ie.js', |
41 | '../node_modules/sinon/lib/sinon/util/fake_timers.js', | 41 | '../node_modules/sinon/lib/sinon/util/fake_timers.js', |
42 | '../node_modules/video.js/dist/video-js/video.js', | 42 | '../node_modules/video.js/dist/video-js/video.dev.js', |
43 | '../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js', | 43 | '../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js', |
44 | '../node_modules/pkcs7/dist/pkcs7.unpad.js', | 44 | '../node_modules/pkcs7/dist/pkcs7.unpad.js', |
45 | '../test/karma-qunit-shim.js', | 45 | '../test/karma-qunit-shim.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', | ... | ... |
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 | } | ... | ... |
... | @@ -186,28 +186,6 @@ | ... | @@ -186,28 +186,6 @@ |
186 | // too large/small tag size values | 186 | // too large/small tag size values |
187 | // too large/small frame size values | 187 | // too large/small frame size values |
188 | 188 | ||
189 | test('translates PTS and DTS values based on the timestamp offset', function() { | ||
190 | var events = []; | ||
191 | metadataStream.on('data', function(event) { | ||
192 | events.push(event); | ||
193 | }); | ||
194 | |||
195 | metadataStream.timestampOffset = 800; | ||
196 | |||
197 | metadataStream.push({ | ||
198 | trackId: 7, | ||
199 | pts: 1000, | ||
200 | dts: 900, | ||
201 | |||
202 | // header | ||
203 | data: new Uint8Array(id3Tag(id3Frame('XFFF', [0]), [0x00, 0x00])) | ||
204 | }); | ||
205 | |||
206 | equal(events.length, 1, 'emitted an event'); | ||
207 | equal(events[0].pts, 200, 'translated pts'); | ||
208 | equal(events[0].dts, 100, 'translated dts'); | ||
209 | }); | ||
210 | |||
211 | test('parses TXXX frames', function() { | 189 | test('parses TXXX frames', function() { |
212 | var events = []; | 190 | var events = []; |
213 | metadataStream.on('data', function(event) { | 191 | metadataStream.on('data', function(event) { |
... | @@ -223,7 +201,7 @@ | ... | @@ -223,7 +201,7 @@ |
223 | data: new Uint8Array(id3Tag(id3Frame('TXXX', | 201 | data: new Uint8Array(id3Tag(id3Frame('TXXX', |
224 | 0x03, // utf-8 | 202 | 0x03, // utf-8 |
225 | stringToCString('get done'), | 203 | stringToCString('get done'), |
226 | stringToInts('{ "key": "value" }')), | 204 | stringToCString('{ "key": "value" }')), |
227 | [0x00, 0x00])) | 205 | [0x00, 0x00])) |
228 | }); | 206 | }); |
229 | 207 | ||
... | @@ -231,7 +209,7 @@ | ... | @@ -231,7 +209,7 @@ |
231 | equal(events[0].frames.length, 1, 'parsed one frame'); | 209 | equal(events[0].frames.length, 1, 'parsed one frame'); |
232 | equal(events[0].frames[0].id, 'TXXX', 'parsed the frame id'); | 210 | equal(events[0].frames[0].id, 'TXXX', 'parsed the frame id'); |
233 | equal(events[0].frames[0].description, 'get done', 'parsed the description'); | 211 | equal(events[0].frames[0].description, 'get done', 'parsed the description'); |
234 | equal(events[0].frames[0].value, '{ "key": "value" }', 'parsed the value'); | 212 | deepEqual(JSON.parse(events[0].frames[0].value), { key: 'value' }, 'parsed the value'); |
235 | }); | 213 | }); |
236 | 214 | ||
237 | test('parses WXXX frames', function() { | 215 | test('parses WXXX frames', function() { |
... | @@ -275,7 +253,7 @@ | ... | @@ -275,7 +253,7 @@ |
275 | data: new Uint8Array(id3Tag(id3Frame('TXXX', | 253 | data: new Uint8Array(id3Tag(id3Frame('TXXX', |
276 | 0x03, // utf-8 | 254 | 0x03, // utf-8 |
277 | stringToCString(''), | 255 | stringToCString(''), |
278 | stringToInts(value)), | 256 | stringToCString(value)), |
279 | [0x00, 0x00])) | 257 | [0x00, 0x00])) |
280 | }); | 258 | }); |
281 | 259 | ... | ... |
... | @@ -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 = [], |
... | @@ -597,4 +710,5 @@ | ... | @@ -597,4 +710,5 @@ |
597 | '#EXT-X-ENDLIST'); // no newline | 710 | '#EXT-X-ENDLIST'); // no newline |
598 | ok(loader.media().endList, 'flushed the final line of input'); | 711 | ok(loader.media().endList, 'flushed the final line of input'); |
599 | }); | 712 | }); |
713 | |||
600 | })(window); | 714 | })(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 accounts for media sequences', function() { | ||
20 | var duration = Playlist.duration({ | ||
21 | mediaSequence: 10, | ||
22 | endList: true, | ||
23 | segments: [{ | ||
24 | duration: 10, | ||
25 | uri: '10.ts' | ||
26 | }, { | ||
27 | duration: 10, | ||
28 | uri: '11.ts' | ||
29 | }, { | ||
30 | duration: 10, | ||
31 | uri: '12.ts' | ||
32 | }, { | ||
33 | duration: 10, | ||
34 | uri: '13.ts' | ||
35 | }] | ||
36 | }, 0, 14); | ||
37 | |||
38 | equal(duration, 14 * 10, 'duration includes dropped segments'); | ||
39 | }); | ||
40 | |||
41 | test('calculates seekable time ranges from the available segments', function() { | ||
42 | var playlist = { | ||
43 | mediaSequence: 0, | ||
44 | segments: [{ | ||
45 | duration: 10, | ||
46 | uri: '0.ts' | ||
47 | }, { | ||
48 | duration: 10, | ||
49 | uri: '1.ts' | ||
50 | }], | ||
51 | endList: true | ||
52 | }, seekable = Playlist.seekable(playlist); | ||
53 | |||
54 | equal(seekable.length, 1, 'there are seekable ranges'); | ||
55 | equal(seekable.start(0), 0, 'starts at zero'); | ||
56 | equal(seekable.end(0), Playlist.duration(playlist), 'ends at the duration'); | ||
57 | }); | ||
58 | |||
59 | test('master playlists have empty seekable ranges', function() { | ||
60 | var seekable = Playlist.seekable({ | ||
61 | playlists: [{ | ||
62 | uri: 'low.m3u8' | ||
63 | }, { | ||
64 | uri: 'high.m3u8' | ||
65 | }] | ||
66 | }); | ||
67 | equal(seekable.length, 0, 'no seekable ranges from a master playlist'); | ||
68 | }); | ||
69 | |||
70 | test('seekable end is three target durations from the actual end of live playlists', function() { | ||
71 | var seekable = Playlist.seekable({ | ||
72 | mediaSequence: 0, | ||
73 | segments: [{ | ||
74 | duration: 7, | ||
75 | uri: '0.ts' | ||
76 | }, { | ||
77 | duration: 10, | ||
78 | uri: '1.ts' | ||
79 | }, { | ||
80 | duration: 10, | ||
81 | uri: '2.ts' | ||
82 | }, { | ||
83 | duration: 10, | ||
84 | uri: '3.ts' | ||
85 | }] | ||
86 | }); | ||
87 | equal(seekable.length, 1, 'there are seekable ranges'); | ||
88 | equal(seekable.start(0), 0, 'starts at zero'); | ||
89 | equal(seekable.end(0), 7, 'ends three target durations from the last segment'); | ||
90 | }); | ||
91 | |||
92 | test('adjusts seekable to the live playlist window', function() { | ||
93 | var seekable = Playlist.seekable({ | ||
94 | targetDuration: 10, | ||
95 | mediaSequence: 7, | ||
96 | segments: [{ | ||
97 | uri: '8.ts' | ||
98 | }, { | ||
99 | uri: '9.ts' | ||
100 | }, { | ||
101 | uri: '10.ts' | ||
102 | }, { | ||
103 | uri: '11.ts' | ||
104 | }] | ||
105 | }); | ||
106 | equal(seekable.length, 1, 'there are seekable ranges'); | ||
107 | equal(seekable.start(0), 10 * 7, 'starts at the earliest available segment'); | ||
108 | equal(seekable.end(0), 10 * 8, 'ends three target durations from the last available segment'); | ||
109 | }); | ||
110 | |||
111 | test('seekable end accounts for non-standard target durations', function() { | ||
112 | var seekable = Playlist.seekable({ | ||
113 | targetDuration: 2, | ||
114 | mediaSequence: 0, | ||
115 | segments: [{ | ||
116 | duration: 2, | ||
117 | uri: '0.ts' | ||
118 | }, { | ||
119 | duration: 2, | ||
120 | uri: '1.ts' | ||
121 | }, { | ||
122 | duration: 1, | ||
123 | uri: '2.ts' | ||
124 | }, { | ||
125 | duration: 2, | ||
126 | uri: '3.ts' | ||
127 | }, { | ||
128 | duration: 2, | ||
129 | uri: '4.ts' | ||
130 | }] | ||
131 | }); | ||
132 | equal(seekable.start(0), 0, 'starts at the earliest available segment'); | ||
133 | equal(seekable.end(0), | ||
134 | 9 - (2 * 3), | ||
135 | 'allows seeking no further than three target durations from the end'); | ||
136 | }); | ||
137 | |||
138 | })(window, window.videojs); |
... | @@ -15,7 +15,7 @@ | ... | @@ -15,7 +15,7 @@ |
15 | <script src="../libs/qunit/qunit.js"></script> | 15 | <script src="../libs/qunit/qunit.js"></script> |
16 | 16 | ||
17 | <!-- video.js --> | 17 | <!-- video.js --> |
18 | <script src="../node_modules/video.js/dist/video-js/video.js"></script> | 18 | <script src="../node_modules/video.js/dist/video-js/video.dev.js"></script> |
19 | <script src="../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script> | 19 | <script src="../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script> |
20 | 20 | ||
21 | <!-- HLS plugin --> | 21 | <!-- HLS plugin --> |
... | @@ -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> | ... | ... |
... | @@ -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', |
... | @@ -1224,6 +1249,226 @@ test('exposes in-band metadata events as cues', function() { | ... | @@ -1224,6 +1249,226 @@ test('exposes in-band metadata events as cues', function() { |
1224 | 'set the private data'); | 1249 | 'set the private data'); |
1225 | }); | 1250 | }); |
1226 | 1251 | ||
1252 | test('only adds in-band cues the first time they are encountered', function() { | ||
1253 | var tags = [{ pts: 0, bytes: new Uint8Array(1) }], track; | ||
1254 | player.src({ | ||
1255 | src: 'manifest/media.m3u8', | ||
1256 | type: 'application/vnd.apple.mpegurl' | ||
1257 | }); | ||
1258 | openMediaSource(player); | ||
1259 | |||
1260 | player.hls.segmentParser_.getNextTag = function() { | ||
1261 | return tags.shift(); | ||
1262 | }; | ||
1263 | player.hls.segmentParser_.tagsAvailable = function() { | ||
1264 | return tags.length; | ||
1265 | }; | ||
1266 | player.hls.segmentParser_.parseSegmentBinaryData = function() { | ||
1267 | // fake out a descriptor | ||
1268 | player.hls.segmentParser_.metadataStream.descriptor = new Uint8Array([ | ||
1269 | 1, 2, 3, 0xbb | ||
1270 | ]); | ||
1271 | // trigger a metadata event | ||
1272 | player.hls.segmentParser_.metadataStream.trigger('data', { | ||
1273 | pts: 2000, | ||
1274 | data: new Uint8Array([]), | ||
1275 | frames: [{ | ||
1276 | id: 'TXXX', | ||
1277 | value: 'cue text' | ||
1278 | }] | ||
1279 | }); | ||
1280 | }; | ||
1281 | standardXHRResponse(requests.shift()); | ||
1282 | standardXHRResponse(requests.shift()); | ||
1283 | // seek back to the first segment | ||
1284 | player.currentTime(0); | ||
1285 | player.hls.trigger('seeking'); | ||
1286 | tags.push({ pts: 0, bytes: new Uint8Array(1) }); | ||
1287 | standardXHRResponse(requests.shift()); | ||
1288 | |||
1289 | track = player.textTracks()[0]; | ||
1290 | equal(track.cues.length, 1, 'only added the cue once'); | ||
1291 | }); | ||
1292 | |||
1293 | test('clears in-band cues ahead of current time on seek', function() { | ||
1294 | var | ||
1295 | tags = [], | ||
1296 | events = [], | ||
1297 | track; | ||
1298 | player.src({ | ||
1299 | src: 'manifest/media.m3u8', | ||
1300 | type: 'application/vnd.apple.mpegurl' | ||
1301 | }); | ||
1302 | openMediaSource(player); | ||
1303 | |||
1304 | player.hls.segmentParser_.getNextTag = function() { | ||
1305 | return tags.shift(); | ||
1306 | }; | ||
1307 | player.hls.segmentParser_.tagsAvailable = function() { | ||
1308 | return tags.length; | ||
1309 | }; | ||
1310 | player.hls.segmentParser_.parseSegmentBinaryData = function() { | ||
1311 | // fake out a descriptor | ||
1312 | player.hls.segmentParser_.metadataStream.descriptor = new Uint8Array([ | ||
1313 | 1, 2, 3, 0xbb | ||
1314 | ]); | ||
1315 | // trigger a metadata event | ||
1316 | if (events.length) { | ||
1317 | player.hls.segmentParser_.metadataStream.trigger('data', events.shift()); | ||
1318 | } | ||
1319 | }; | ||
1320 | standardXHRResponse(requests.shift()); // media | ||
1321 | tags.push({ pts: 10 * 1000, bytes: new Uint8Array(1) }); | ||
1322 | events.push({ | ||
1323 | pts: 20 * 1000, | ||
1324 | data: new Uint8Array([]), | ||
1325 | frames: [{ | ||
1326 | id: 'TXXX', | ||
1327 | value: 'cue 3' | ||
1328 | }] | ||
1329 | }); | ||
1330 | events.push({ | ||
1331 | pts: 9.9 * 1000, | ||
1332 | data: new Uint8Array([]), | ||
1333 | frames: [{ | ||
1334 | id: 'TXXX', | ||
1335 | value: 'cue 1' | ||
1336 | }] | ||
1337 | }); | ||
1338 | standardXHRResponse(requests.shift()); // segment 0 | ||
1339 | tags.push({ pts: 20 * 1000, bytes: new Uint8Array(1) }); | ||
1340 | events.push({ | ||
1341 | pts: 19.9 * 1000, | ||
1342 | data: new Uint8Array([]), | ||
1343 | frames: [{ | ||
1344 | id: 'TXXX', | ||
1345 | value: 'cue 2' | ||
1346 | }] | ||
1347 | }); | ||
1348 | player.hls.checkBuffer_(); | ||
1349 | standardXHRResponse(requests.shift()); // segment 1 | ||
1350 | |||
1351 | track = player.textTracks()[0]; | ||
1352 | equal(track.cues.length, 2, 'added the cues'); | ||
1353 | |||
1354 | // seek into segment 1 | ||
1355 | player.currentTime(11); | ||
1356 | player.trigger('seeking'); | ||
1357 | equal(track.cues.length, 1, 'removed a cue'); | ||
1358 | equal(track.cues[0].startTime, 9.9, 'retained the earlier cue'); | ||
1359 | }); | ||
1360 | |||
1361 | test('translates ID3 PTS values to cue media timeline positions', function() { | ||
1362 | var tags = [{ pts: 4 * 1000, bytes: new Uint8Array(1) }], track; | ||
1363 | player.src({ | ||
1364 | src: 'manifest/media.m3u8', | ||
1365 | type: 'application/vnd.apple.mpegurl' | ||
1366 | }); | ||
1367 | openMediaSource(player); | ||
1368 | |||
1369 | player.hls.segmentParser_.getNextTag = function() { | ||
1370 | return tags.shift(); | ||
1371 | }; | ||
1372 | player.hls.segmentParser_.tagsAvailable = function() { | ||
1373 | return tags.length; | ||
1374 | }; | ||
1375 | player.hls.segmentParser_.parseSegmentBinaryData = function() { | ||
1376 | // setup the timestamp offset | ||
1377 | this.timestampOffset = tags[0].pts; | ||
1378 | |||
1379 | // fake out a descriptor | ||
1380 | player.hls.segmentParser_.metadataStream.descriptor = new Uint8Array([ | ||
1381 | 1, 2, 3, 0xbb | ||
1382 | ]); | ||
1383 | // trigger a metadata event | ||
1384 | player.hls.segmentParser_.metadataStream.trigger('data', { | ||
1385 | pts: 5 * 1000, | ||
1386 | data: new Uint8Array([]), | ||
1387 | frames: [{ | ||
1388 | id: 'TXXX', | ||
1389 | value: 'cue text' | ||
1390 | }] | ||
1391 | }); | ||
1392 | }; | ||
1393 | standardXHRResponse(requests.shift()); // media | ||
1394 | standardXHRResponse(requests.shift()); // segment 0 | ||
1395 | |||
1396 | track = player.textTracks()[0]; | ||
1397 | equal(track.cues[0].startTime, 1, 'translated startTime'); | ||
1398 | equal(track.cues[0].endTime, 1, 'translated startTime'); | ||
1399 | }); | ||
1400 | |||
1401 | test('translates ID3 PTS values across discontinuities', function() { | ||
1402 | var tags = [], events = [], track; | ||
1403 | player.src({ | ||
1404 | src: 'cues-and-discontinuities.m3u8', | ||
1405 | type: 'application/vnd.apple.mpegurl' | ||
1406 | }); | ||
1407 | openMediaSource(player); | ||
1408 | |||
1409 | player.hls.segmentParser_.getNextTag = function() { | ||
1410 | return tags.shift(); | ||
1411 | }; | ||
1412 | player.hls.segmentParser_.tagsAvailable = function() { | ||
1413 | return tags.length; | ||
1414 | }; | ||
1415 | player.hls.segmentParser_.parseSegmentBinaryData = function() { | ||
1416 | if (this.timestampOffset === null) { | ||
1417 | this.timestampOffset = tags[0].pts; | ||
1418 | } | ||
1419 | // fake out a descriptor | ||
1420 | player.hls.segmentParser_.metadataStream.descriptor = new Uint8Array([ | ||
1421 | 1, 2, 3, 0xbb | ||
1422 | ]); | ||
1423 | // trigger a metadata event | ||
1424 | if (events.length) { | ||
1425 | player.hls.segmentParser_.metadataStream.trigger('data', events.shift()); | ||
1426 | } | ||
1427 | }; | ||
1428 | |||
1429 | // media playlist | ||
1430 | requests.shift().respond(200, null, | ||
1431 | '#EXTM3U\n' + | ||
1432 | '#EXTINF:10,\n' + | ||
1433 | '0.ts\n' + | ||
1434 | '#EXT-X-DISCONTINUITY\n' + | ||
1435 | '#EXTINF:10,\n' + | ||
1436 | '1.ts\n'); | ||
1437 | |||
1438 | // segment 0 starts at PTS 14000 and has a cue point at 15000 | ||
1439 | tags.push({ pts: 14 * 1000, bytes: new Uint8Array(1) }); | ||
1440 | events.push({ | ||
1441 | pts: 15 * 1000, | ||
1442 | data: new Uint8Array([]), | ||
1443 | frames: [{ | ||
1444 | id: 'TXXX', | ||
1445 | value: 'cue 0' | ||
1446 | }] | ||
1447 | }); | ||
1448 | standardXHRResponse(requests.shift()); // segment 0 | ||
1449 | |||
1450 | // segment 1 is after a discontinuity, starts at PTS 22000 | ||
1451 | // and has a cue point at 15000 | ||
1452 | tags.push({ pts: 22 * 1000, bytes: new Uint8Array(1) }); | ||
1453 | events.push({ | ||
1454 | pts: 23 * 1000, | ||
1455 | data: new Uint8Array([]), | ||
1456 | frames: [{ | ||
1457 | id: 'TXXX', | ||
1458 | value: 'cue 0' | ||
1459 | }] | ||
1460 | }); | ||
1461 | player.hls.checkBuffer_(); | ||
1462 | standardXHRResponse(requests.shift()); | ||
1463 | |||
1464 | track = player.textTracks()[0]; | ||
1465 | equal(track.cues.length, 2, 'created cues'); | ||
1466 | equal(track.cues[0].startTime, 1, 'first cue started at the correct time'); | ||
1467 | equal(track.cues[0].endTime, 1, 'first cue ended at the correct time'); | ||
1468 | equal(track.cues[1].startTime, 11, 'second cue started at the correct time'); | ||
1469 | equal(track.cues[1].endTime, 11, 'second cue ended at the correct time'); | ||
1470 | }); | ||
1471 | |||
1227 | test('drops tags before the target timestamp when seeking', function() { | 1472 | test('drops tags before the target timestamp when seeking', function() { |
1228 | var i = 10, | 1473 | var i = 10, |
1229 | tags = [], | 1474 | tags = [], |
... | @@ -1611,7 +1856,8 @@ test('clears the segment buffer on seek', function() { | ... | @@ -1611,7 +1856,8 @@ test('clears the segment buffer on seek', function() { |
1611 | '1.ts\n' + | 1856 | '1.ts\n' + |
1612 | '#EXT-X-DISCONTINUITY\n' + | 1857 | '#EXT-X-DISCONTINUITY\n' + |
1613 | '#EXTINF:10,0\n' + | 1858 | '#EXTINF:10,0\n' + |
1614 | '2.ts\n'); | 1859 | '2.ts\n' + |
1860 | '#EXT-X-ENDLIST\n'); | ||
1615 | standardXHRResponse(requests.pop()); | 1861 | standardXHRResponse(requests.pop()); |
1616 | 1862 | ||
1617 | // play to 6s to trigger the next segment request | 1863 | // play to 6s to trigger the next segment request |
... | @@ -1662,7 +1908,8 @@ test('continues playing after seek to discontinuity', function() { | ... | @@ -1662,7 +1908,8 @@ test('continues playing after seek to discontinuity', function() { |
1662 | '1.ts\n' + | 1908 | '1.ts\n' + |
1663 | '#EXT-X-DISCONTINUITY\n' + | 1909 | '#EXT-X-DISCONTINUITY\n' + |
1664 | '#EXTINF:10,0\n' + | 1910 | '#EXTINF:10,0\n' + |
1665 | '2.ts\n'); | 1911 | '2.ts\n' + |
1912 | '#EXT-X-ENDLIST\n'); | ||
1666 | standardXHRResponse(requests.pop()); | 1913 | standardXHRResponse(requests.pop()); |
1667 | 1914 | ||
1668 | currentTime = 1; | 1915 | currentTime = 1; |
... | @@ -1751,8 +1998,7 @@ test('remove event handlers on dispose', function() { | ... | @@ -1751,8 +1998,7 @@ test('remove event handlers on dispose', function() { |
1751 | 1998 | ||
1752 | player.dispose(); | 1999 | player.dispose(); |
1753 | 2000 | ||
1754 | ok(offhandlers > onhandlers, 'more handlers were removed than were registered'); | 2001 | ok(offhandlers > onhandlers, 'removed all registered handlers'); |
1755 | equal(offhandlers - onhandlers, 1, 'one handler was registered during init'); | ||
1756 | }); | 2002 | }); |
1757 | 2003 | ||
1758 | test('aborts the source buffer on disposal', function() { | 2004 | test('aborts the source buffer on disposal', function() { | ... | ... |
-
Please register or sign in to post a comment