b27cedcc by David LaPalomento

Report seekable relative to the earliest possible position at load start

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