0cd39e1c by David LaPalomento

@dmlap implement seekable for live streams ([view](https://github.com/videojs/vi…

…deojs-contrib-hls/pull/295))
2 parents c4a8ac25 e1e725cd
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);
......
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',
......
...@@ -21,5 +21,6 @@ ...@@ -21,5 +21,6 @@
21 } 21 }
22 ], 22 ],
23 "targetDuration": 10, 23 "targetDuration": 10,
24 "endList": true
25 }
...\ No newline at end of file ...\ No newline at end of file
24 "endList": true,
25 "discontinuitySequence": 0
26 }
......
...@@ -141,5 +141,6 @@ ...@@ -141,5 +141,6 @@
141 } 141 }
142 ], 142 ],
143 "targetDuration": 10, 143 "targetDuration": 10,
144 "endList": true
145 }
...\ No newline at end of file ...\ No newline at end of file
144 "endList": true,
145 "discontinuitySequence": 0
146 }
......
...@@ -13,5 +13,6 @@ ...@@ -13,5 +13,6 @@
13 } 13 }
14 ], 14 ],
15 "targetDuration": 10, 15 "targetDuration": 10,
16 "endList": true
17 }
...\ No newline at end of file ...\ No newline at end of file
16 "endList": true,
17 "discontinuitySequence": 0
18 }
......
...@@ -137,5 +137,6 @@ ...@@ -137,5 +137,6 @@
137 } 137 }
138 ], 138 ],
139 "targetDuration": 10, 139 "targetDuration": 10,
140 "endList": true
141 }
...\ No newline at end of file ...\ No newline at end of file
140 "endList": true,
141 "discontinuitySequence": 0
142 }
......
...@@ -13,5 +13,6 @@ ...@@ -13,5 +13,6 @@
13 } 13 }
14 ], 14 ],
15 "targetDuration": 10, 15 "targetDuration": 10,
16 "endList": true
17 }
...\ No newline at end of file ...\ No newline at end of file
16 "endList": true,
17 "discontinuitySequence": 0
18 }
......
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 }
1 #EXTM3U
2 #EXT-X-VERSION:3
3 #EXT-X-TARGETDURATION:19
4 #EXT-X-MEDIA-SEQUENCE:0
5 #EXT-X-DISCONTINUITY-SEQUENCE:3
6 #EXTINF:10,0
7 001.ts
8 #EXTINF:19,0
9 002.ts
10 #EXT-X-DISCONTINUITY
11 #EXTINF:10,0
12 003.ts
13 #EXTINF:11,0
14 004.ts
15 #EXT-X-ENDLIST
1 { 1 {
2 "allowCache": true, 2 "allowCache": true,
3 "mediaSequence": 0, 3 "mediaSequence": 0,
4 "discontinuitySequence": 0,
4 "segments": [ 5 "segments": [
5 { 6 {
6 "duration": 10, 7 "duration": 10,
......
...@@ -21,5 +21,6 @@ ...@@ -21,5 +21,6 @@
21 } 21 }
22 ], 22 ],
23 "targetDuration": 10, 23 "targetDuration": 10,
24 "endList": true
25 }
...\ No newline at end of file ...\ No newline at end of file
24 "endList": true,
25 "discontinuitySequence": 0
26 }
......
...@@ -13,5 +13,6 @@ ...@@ -13,5 +13,6 @@
13 } 13 }
14 ], 14 ],
15 "targetDuration": 10, 15 "targetDuration": 10,
16 "endList": true
17 }
...\ No newline at end of file ...\ No newline at end of file
16 "endList": true,
17 "discontinuitySequence": 0
18 }
......
...@@ -21,5 +21,6 @@ ...@@ -21,5 +21,6 @@
21 } 21 }
22 ], 22 ],
23 "targetDuration": 8, 23 "targetDuration": 8,
24 "endList": true
25 }
...\ No newline at end of file ...\ No newline at end of file
24 "endList": true,
25 "discontinuitySequence": 0
26 }
......
...@@ -28,5 +28,6 @@ ...@@ -28,5 +28,6 @@
28 } 28 }
29 ], 29 ],
30 "targetDuration": 10, 30 "targetDuration": 10,
31 "endList": true
32 }
...\ No newline at end of file ...\ No newline at end of file
31 "endList": true,
32 "discontinuitySequence": 0
33 }
......
1 { 1 {
2 "allowCache": true, 2 "allowCache": true,
3 "mediaSequence": 7794, 3 "mediaSequence": 7794,
4 "discontinuitySequence": 0,
4 "segments": [ 5 "segments": [
5 { 6 {
6 "duration": 2.833, 7 "duration": 2.833,
......
...@@ -29,5 +29,6 @@ ...@@ -29,5 +29,6 @@
29 } 29 }
30 ], 30 ],
31 "targetDuration": 10, 31 "targetDuration": 10,
32 "endList": true
33 }
...\ No newline at end of file ...\ No newline at end of file
32 "endList": true,
33 "discontinuitySequence": 0
34 }
......
...@@ -8,5 +8,6 @@ ...@@ -8,5 +8,6 @@
8 } 8 }
9 ], 9 ],
10 "targetDuration": 8, 10 "targetDuration": 8,
11 "endList": true
12 }
...\ No newline at end of file ...\ No newline at end of file
11 "endList": true,
12 "discontinuitySequence": 0
13 }
......
...@@ -141,5 +141,6 @@ ...@@ -141,5 +141,6 @@
141 } 141 }
142 ], 142 ],
143 "targetDuration": 10, 143 "targetDuration": 10,
144 "endList": true
145 }
...\ No newline at end of file ...\ No newline at end of file
144 "endList": true,
145 "discontinuitySequence": 0
146 }
......
...@@ -13,5 +13,6 @@ ...@@ -13,5 +13,6 @@
13 } 13 }
14 ], 14 ],
15 "targetDuration": 10, 15 "targetDuration": 10,
16 "endList": true
17 }
...\ No newline at end of file ...\ No newline at end of file
16 "endList": true,
17 "discontinuitySequence": 0
18 }
......
...@@ -21,5 +21,6 @@ ...@@ -21,5 +21,6 @@
21 } 21 }
22 ], 22 ],
23 "targetDuration": 8, 23 "targetDuration": 8,
24 "endList": true
25 }
...\ No newline at end of file ...\ No newline at end of file
24 "endList": true,
25 "discontinuitySequence": 0
26 }
......
...@@ -28,5 +28,6 @@ ...@@ -28,5 +28,6 @@
28 } 28 }
29 ], 29 ],
30 "targetDuration": 10, 30 "targetDuration": 10,
31 "endList": true
32 }
...\ No newline at end of file ...\ No newline at end of file
31 "endList": true,
32 "discontinuitySequence": 0
33 }
......
...@@ -140,5 +140,6 @@ ...@@ -140,5 +140,6 @@
140 "uri": "hls_450k_video.ts" 140 "uri": "hls_450k_video.ts"
141 } 141 }
142 ], 142 ],
143 "endList": true
144 }
...\ No newline at end of file ...\ No newline at end of file
143 "endList": true,
144 "discontinuitySequence": 0
145 }
......
...@@ -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 }
......
...@@ -39,5 +39,6 @@ ...@@ -39,5 +39,6 @@
39 "uri": "009.ts" 39 "uri": "009.ts"
40 } 40 }
41 ], 41 ],
42 "targetDuration": 10
43 }
...\ No newline at end of file ...\ No newline at end of file
42 "targetDuration": 10,
43 "discontinuitySequence": 0
44 }
......
...@@ -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 }
......
...@@ -24,5 +24,6 @@ ...@@ -24,5 +24,6 @@
24 } 24 }
25 ], 25 ],
26 "targetDuration": 10, 26 "targetDuration": 10,
27 "endList": true
28 }
...\ No newline at end of file ...\ No newline at end of file
27 "endList": true,
28 "discontinuitySequence": 0
29 }
......
...@@ -8,5 +8,6 @@ ...@@ -8,5 +8,6 @@
8 } 8 }
9 ], 9 ],
10 "targetDuration": 10, 10 "targetDuration": 10,
11 "endList": true
12 }
...\ No newline at end of file ...\ No newline at end of file
11 "endList": true,
12 "discontinuitySequence": 0
13 }
......
...@@ -21,5 +21,6 @@ ...@@ -21,5 +21,6 @@
21 } 21 }
22 ], 22 ],
23 "targetDuration": 10, 23 "targetDuration": 10,
24 "endList": true 24 "endList": true,
25 "discontinuitySequence": 0
25 } 26 }
......
...@@ -21,5 +21,6 @@ ...@@ -21,5 +21,6 @@
21 } 21 }
22 ], 22 ],
23 "targetDuration": 8, 23 "targetDuration": 8,
24 "endList": true
25 }
...\ No newline at end of file ...\ No newline at end of file
24 "endList": true,
25 "discontinuitySequence": 0
26 }
......
...@@ -11,5 +11,6 @@ ...@@ -11,5 +11,6 @@
11 "uri": "00002.ts" 11 "uri": "00002.ts"
12 } 12 }
13 ], 13 ],
14 "targetDuration": 10 14 "targetDuration": 10,
15 "discontinuitySequence": 0
15 } 16 }
......
...@@ -17,5 +17,6 @@ ...@@ -17,5 +17,6 @@
17 } 17 }
18 ], 18 ],
19 "targetDuration": 10, 19 "targetDuration": 10,
20 "endList": true
21 }
...\ No newline at end of file ...\ No newline at end of file
20 "endList": true,
21 "discontinuitySequence": 0
22 }
......
...@@ -21,5 +21,6 @@ ...@@ -21,5 +21,6 @@
21 } 21 }
22 ], 22 ],
23 "targetDuration": 8, 23 "targetDuration": 8,
24 "endList": true
25 }
...\ No newline at end of file ...\ No newline at end of file
24 "endList": true,
25 "discontinuitySequence": 0
26 }
......
...@@ -21,5 +21,6 @@ ...@@ -21,5 +21,6 @@
21 } 21 }
22 ], 22 ],
23 "targetDuration": 8, 23 "targetDuration": 8,
24 "endList": true
25 }
...\ No newline at end of file ...\ No newline at end of file
24 "endList": true,
25 "discontinuitySequence": 0
26 }
......
...@@ -18,5 +18,6 @@ ...@@ -18,5 +18,6 @@
18 "uri": "004.ts", 18 "uri": "004.ts",
19 "duration": 10 19 "duration": 10
20 } 20 }
21 ] 21 ],
22 "discontinuitySequence": 0
22 } 23 }
......
...@@ -21,5 +21,6 @@ ...@@ -21,5 +21,6 @@
21 } 21 }
22 ], 22 ],
23 "targetDuration": 8, 23 "targetDuration": 8,
24 "endList": true
25 }
...\ No newline at end of file ...\ No newline at end of file
24 "endList": true,
25 "discontinuitySequence": 0
26 }
......
...@@ -141,5 +141,6 @@ ...@@ -141,5 +141,6 @@
141 } 141 }
142 ], 142 ],
143 "targetDuration": 10, 143 "targetDuration": 10,
144 "endList": true
145 }
...\ No newline at end of file ...\ No newline at end of file
144 "endList": true,
145 "discontinuitySequence": 0
146 }
......
...@@ -9,5 +9,6 @@ ...@@ -9,5 +9,6 @@
9 } 9 }
10 ], 10 ],
11 "targetDuration": 8, 11 "targetDuration": 8,
12 "endList": true
13 }
...\ No newline at end of file ...\ No newline at end of file
12 "endList": true,
13 "discontinuitySequence": 0
14 }
......
...@@ -21,5 +21,6 @@ ...@@ -21,5 +21,6 @@
21 } 21 }
22 ], 22 ],
23 "targetDuration": 8, 23 "targetDuration": 8,
24 "endList": true
25 }
...\ No newline at end of file ...\ No newline at end of file
24 "endList": true,
25 "discontinuitySequence": 0
26 }
......
...@@ -9,5 +9,6 @@ ...@@ -9,5 +9,6 @@
9 } 9 }
10 ], 10 ],
11 "targetDuration": 10, 11 "targetDuration": 10,
12 "endList": true
13 }
...\ No newline at end of file ...\ No newline at end of file
12 "endList": true,
13 "discontinuitySequence": 0
14 }
......
...@@ -21,5 +21,6 @@ ...@@ -21,5 +21,6 @@
21 } 21 }
22 ], 22 ],
23 "targetDuration": 10, 23 "targetDuration": 10,
24 "endList": true
25 }
...\ No newline at end of file ...\ No newline at end of file
24 "endList": true,
25 "discontinuitySequence": 0
26 }
......
...@@ -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);
......
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() {
......