77aaebc4 by David LaPalomento

Merge pull request #91 from videojs/discontinuity

Support for EXT-X-DISCONTINUITY
2 parents 18928e94 25232346
...@@ -298,6 +298,9 @@ hls.FlvTag = function(type, extraData) { ...@@ -298,6 +298,9 @@ hls.FlvTag = function(type, extraData) {
298 this.bytes[ 9] = 0; 298 this.bytes[ 9] = 0;
299 this.bytes[10] = 0; 299 this.bytes[10] = 0;
300 300
301 // Sometimes we're at the end of the view and have one slot to write a
302 // uint32, so, prepareWrite of count 4, since, view is uint8
303 prepareWrite(this, 4);
301 this.view.setUint32(this.length, this.length); 304 this.view.setUint32(this.length, this.length);
302 this.length += 4; 305 this.length += 4;
303 this.position += 4; 306 this.position += 4;
......
...@@ -302,6 +302,7 @@ ...@@ -302,6 +302,7 @@
302 302
303 h264Frame.endNalUnit(); 303 h264Frame.endNalUnit();
304 this.tags.push(h264Frame); 304 this.tags.push(h264Frame);
305
305 } 306 }
306 307
307 h264Frame = null; 308 h264Frame = null;
...@@ -427,7 +428,9 @@ ...@@ -427,7 +428,9 @@
427 428
428 // We did not find any start codes. Try again next packet 429 // We did not find any start codes. Try again next packet
429 state = 1; 430 state = 1;
430 h264Frame.writeBytes(data, start, length); 431 if (h264Frame) {
432 h264Frame.writeBytes(data, start, length);
433 }
431 return; 434 return;
432 case 3: 435 case 3:
433 // The next byte is the first byte of a NAL Unit 436 // The next byte is the first byte of a NAL Unit
......
...@@ -273,6 +273,14 @@ ...@@ -273,6 +273,14 @@
273 }); 273 });
274 return; 274 return;
275 } 275 }
276 match = (/^#EXT-X-DISCONTINUITY/).exec(line);
277 if (match) {
278 this.trigger('data', {
279 type: 'tag',
280 tagType: 'discontinuity'
281 });
282 return;
283 }
276 284
277 // unknown tag type 285 // unknown tag type
278 this.trigger('data', { 286 this.trigger('data', {
...@@ -399,6 +407,9 @@ ...@@ -399,6 +407,9 @@
399 currentUri.attributes = mergeOptions(currentUri.attributes, 407 currentUri.attributes = mergeOptions(currentUri.attributes,
400 entry.attributes); 408 entry.attributes);
401 }, 409 },
410 'discontinuity': function() {
411 currentUri.discontinuity = true;
412 },
402 'targetduration': function() { 413 'targetduration': function() {
403 if (!isFinite(entry.duration) || entry.duration < 0) { 414 if (!isFinite(entry.duration) || entry.duration < 0) {
404 this.trigger('warn', { 415 this.trigger('warn', {
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
7 */ 7 */
8 8
9 (function(window, videojs, document, undefined) { 9 (function(window, videojs, document, undefined) {
10 'use strict';
10 11
11 var 12 var
12 13
...@@ -145,22 +146,42 @@ var ...@@ -145,22 +146,42 @@ var
145 }, 146 },
146 147
147 /** 148 /**
149 * Calculate the duration of a playlist from a given start index to a given
150 * end index.
151 * @param playlist {object} a media playlist object
152 * @param startIndex {number} an inclusive lower boundary for the playlist.
153 * Defaults to 0.
154 * @param endIndex {number} an exclusive upper boundary for the playlist.
155 * Defaults to playlist length.
156 * @return {number} the duration between the start index and end index.
157 */
158 duration = function(playlist, startIndex, endIndex) {
159 var dur = 0,
160 segment,
161 i;
162
163 startIndex = startIndex || 0;
164 endIndex = endIndex !== undefined ? endIndex : (playlist.segments || []).length;
165 i = endIndex - 1;
166
167 for (; i >= startIndex; i--) {
168 segment = playlist.segments[i];
169 dur += segment.duration || playlist.targetDuration || 0;
170 }
171
172 return dur;
173 },
174
175 /**
148 * Calculate the total duration for a playlist based on segment metadata. 176 * Calculate the total duration for a playlist based on segment metadata.
149 * @param playlist {object} a media playlist object 177 * @param playlist {object} a media playlist object
150 * @return {number} the currently known duration, in seconds 178 * @return {number} the currently known duration, in seconds
151 */ 179 */
152 totalDuration = function(playlist) { 180 totalDuration = function(playlist) {
153 var
154 duration = 0,
155 segment,
156 i;
157
158 if (!playlist) { 181 if (!playlist) {
159 return 0; 182 return 0;
160 } 183 }
161 184
162 i = (playlist.segments || []).length;
163
164 // if present, use the duration specified in the playlist 185 // if present, use the duration specified in the playlist
165 if (playlist.totalDuration) { 186 if (playlist.totalDuration) {
166 return playlist.totalDuration; 187 return playlist.totalDuration;
...@@ -171,11 +192,7 @@ var ...@@ -171,11 +192,7 @@ var
171 return window.Infinity; 192 return window.Infinity;
172 } 193 }
173 194
174 while (i--) { 195 return duration(playlist);
175 segment = playlist.segments[i];
176 duration += segment.duration || playlist.targetDuration || 0;
177 }
178 return duration;
179 }, 196 },
180 197
181 resolveUrl, 198 resolveUrl,
...@@ -184,10 +201,12 @@ var ...@@ -184,10 +201,12 @@ var
184 var 201 var
185 segmentParser = new videojs.Hls.SegmentParser(), 202 segmentParser = new videojs.Hls.SegmentParser(),
186 settings = videojs.util.mergeOptions({}, player.options().hls), 203 settings = videojs.util.mergeOptions({}, player.options().hls),
204 segmentBuffer = [],
187 205
188 lastSeekedTime, 206 lastSeekedTime,
189 segmentXhr, 207 segmentXhr,
190 fillBuffer, 208 fillBuffer,
209 drainBuffer,
191 updateDuration; 210 updateDuration;
192 211
193 212
...@@ -197,6 +216,7 @@ var ...@@ -197,6 +216,7 @@ var
197 } 216 }
198 return this.el().vjs_getProperty('currentTime'); 217 return this.el().vjs_getProperty('currentTime');
199 }; 218 };
219
200 player.hls.setCurrentTime = function(currentTime) { 220 player.hls.setCurrentTime = function(currentTime) {
201 if (!(this.playlists && this.playlists.media())) { 221 if (!(this.playlists && this.playlists.media())) {
202 // return immediately if the metadata is not ready yet 222 // return immediately if the metadata is not ready yet
...@@ -219,6 +239,9 @@ var ...@@ -219,6 +239,9 @@ var
219 segmentXhr.abort(); 239 segmentXhr.abort();
220 } 240 }
221 241
242 // clear out any buffered segments
243 segmentBuffer = [];
244
222 // begin filling the buffer at the new position 245 // begin filling the buffer at the new position
223 fillBuffer(currentTime * 1000); 246 fillBuffer(currentTime * 1000);
224 }; 247 };
...@@ -367,6 +390,8 @@ var ...@@ -367,6 +390,8 @@ var
367 responseType: 'arraybuffer', 390 responseType: 'arraybuffer',
368 withCredentials: settings.withCredentials 391 withCredentials: settings.withCredentials
369 }, function(error, url) { 392 }, function(error, url) {
393 var tags;
394
370 // the segment request is no longer outstanding 395 // the segment request is no longer outstanding
371 segmentXhr = null; 396 segmentXhr = null;
372 397
...@@ -395,45 +420,101 @@ var ...@@ -395,45 +420,101 @@ var
395 segmentParser.parseSegmentBinaryData(new Uint8Array(this.response)); 420 segmentParser.parseSegmentBinaryData(new Uint8Array(this.response));
396 segmentParser.flushTags(); 421 segmentParser.flushTags();
397 422
398 // if we're refilling the buffer after a seek, scan through the muxed 423 // package up all the work to append the segment
399 // FLV tags until we find the one that is closest to the desired 424 // if the segment is the start of a timestamp discontinuity,
400 // playback time 425 // we have to wait until the sourcebuffer is empty before
401 if (typeof offset === 'number') { 426 // aborting the source buffer processing
402 (function() { 427 tags = [];
403 var tag = segmentParser.getTags()[0];
404
405 for (; tag.pts < offset; tag = segmentParser.getTags()[0]) {
406 segmentParser.getNextTag();
407 }
408
409 // tell the SWF where we will be seeking to
410 player.hls.el().vjs_setProperty('currentTime', tag.pts * 0.001);
411 lastSeekedTime = null;
412 })();
413 }
414
415 while (segmentParser.tagsAvailable()) { 428 while (segmentParser.tagsAvailable()) {
416 // queue up the bytes to be appended to the SourceBuffer 429 tags.push(segmentParser.getNextTag());
417 // the queue gives control back to the browser between tags
418 // so that large segments don't cause a "hiccup" in playback
419
420 player.hls.sourceBuffer.appendBuffer(segmentParser.getNextTag().bytes,
421 player);
422
423 } 430 }
431 segmentBuffer.push({
432 mediaIndex: player.hls.mediaIndex,
433 playlist: player.hls.playlists.media(),
434 offset: offset,
435 tags: tags
436 });
437 drainBuffer();
424 438
425 player.hls.mediaIndex++; 439 player.hls.mediaIndex++;
426 440
427 if (player.hls.mediaIndex === player.hls.playlists.media().segments.length) {
428 mediaSource.endOfStream();
429 }
430
431 // figure out what stream the next segment should be downloaded from 441 // figure out what stream the next segment should be downloaded from
432 // with the updated bandwidth information 442 // with the updated bandwidth information
433 player.hls.playlists.media(player.hls.selectPlaylist()); 443 player.hls.playlists.media(player.hls.selectPlaylist());
434 }); 444 });
435 }; 445 };
436 446
447 drainBuffer = function(event) {
448 var
449 i = 0,
450 mediaIndex,
451 playlist,
452 offset,
453 tags,
454 segment,
455
456 ptsTime,
457 segmentOffset;
458
459 if (!segmentBuffer.length) {
460 return;
461 }
462
463 mediaIndex = segmentBuffer[0].mediaIndex;
464 playlist = segmentBuffer[0].playlist;
465 offset = segmentBuffer[0].offset;
466 tags = segmentBuffer[0].tags;
467 segment = playlist.segments[mediaIndex];
468
469 event = event || {};
470 segmentOffset = duration(playlist, 0, mediaIndex) * 1000;
471
472 // abort() clears any data queued in the source buffer so wait
473 // until it empties before calling it when a discontinuity is
474 // next in the buffer
475 if (segment.discontinuity) {
476 if (event.type !== 'waiting') {
477 return;
478 }
479 player.hls.sourceBuffer.abort();
480 // tell the SWF where playback is continuing in the stitched timeline
481 player.hls.el().vjs_setProperty('currentTime', segmentOffset * 0.001);
482 }
483
484 // if we're refilling the buffer after a seek, scan through the muxed
485 // FLV tags until we find the one that is closest to the desired
486 // playback time
487 if (typeof offset === 'number') {
488 ptsTime = offset - segmentOffset + tags[0].pts;
489
490 while (tags[i].pts < ptsTime) {
491 i++;
492 }
493
494 // tell the SWF where we will be seeking to
495 player.hls.el().vjs_setProperty('currentTime', (tags[i].pts - tags[0].pts + segmentOffset) * 0.001);
496
497 tags = tags.slice(i);
498
499 lastSeekedTime = null;
500 }
501
502 for (i = 0; i < tags.length; i++) {
503 // queue up the bytes to be appended to the SourceBuffer
504 // the queue gives control back to the browser between tags
505 // so that large segments don't cause a "hiccup" in playback
506
507 player.hls.sourceBuffer.appendBuffer(tags[i].bytes, player);
508 }
509
510 // we're done processing this segment
511 segmentBuffer.shift();
512
513 if (mediaIndex === playlist.segments.length) {
514 mediaSource.endOfStream();
515 }
516 };
517
437 // load the MediaSource into the player 518 // load the MediaSource into the player
438 mediaSource.addEventListener('sourceopen', function() { 519 mediaSource.addEventListener('sourceopen', function() {
439 // construct the video data buffer and set the appropriate MIME type 520 // construct the video data buffer and set the appropriate MIME type
...@@ -450,9 +531,12 @@ var ...@@ -450,9 +531,12 @@ var
450 player.hls.playlists.on('loadedmetadata', function() { 531 player.hls.playlists.on('loadedmetadata', function() {
451 oldMediaPlaylist = player.hls.playlists.media(); 532 oldMediaPlaylist = player.hls.playlists.media();
452 533
453 // periodicaly check if the buffer needs to be refilled 534 // periodically check if new data needs to be downloaded or
535 // buffered data should be appended to the source buffer
454 fillBuffer(); 536 fillBuffer();
455 player.on('timeupdate', fillBuffer); 537 player.on('timeupdate', fillBuffer);
538 player.on('timeupdate', drainBuffer);
539 player.on('waiting', drainBuffer);
456 540
457 player.trigger('loadedmetadata'); 541 player.trigger('loadedmetadata');
458 }); 542 });
......
1 {
2 "allowCache": true,
3 "mediaSequence": 0,
4 "segments": [
5 {
6 "duration": 10,
7 "uri": "001.ts"
8 },
9 {
10 "duration": 19,
11 "uri": "002.ts"
12 },
13 {
14 "discontinuity": true,
15 "duration": 10,
16 "uri": "003.ts"
17 },
18 {
19 "duration": 11,
20 "uri": "004.ts"
21 },
22 {
23 "discontinuity": true,
24 "duration": 10,
25 "uri": "005.ts"
26 },
27 {
28 "duration": 10,
29 "uri": "006.ts"
30 },
31 {
32 "duration": 10,
33 "uri": "007.ts"
34 },
35 {
36 "discontinuity": true,
37 "duration": 10,
38 "uri": "008.ts"
39 },
40 {
41 "duration": 16,
42 "uri": "009.ts"
43 }
44 ],
45 "targetDuration": 19,
46 "endList": true
47 }
1 #EXTM3U
2 #EXT-X-VERSION:3
3 #EXT-X-TARGETDURATION:19
4 #EXT-X-MEDIA-SEQUENCE:0
5 #EXTINF:10,0
6 001.ts
7 #EXTINF:19,0
8 002.ts
9 #EXT-X-DISCONTINUITY
10 #EXTINF:10,0
11 003.ts
12 #EXTINF:11,0
13 004.ts
14 #EXT-X-DISCONTINUITY
15 #EXTINF:10,0
16 005.ts
17 #EXTINF:10,0
18 006.ts
19 #EXTINF:10,0
20 007.ts
21 #EXT-X-DISCONTINUITY
22 #EXTINF:10,0
23 008.ts
24 #EXTINF:16,0
25 009.ts
26 #EXT-X-ENDLIST
...@@ -124,6 +124,8 @@ ...@@ -124,6 +124,8 @@
124 0x12: 'metadata' 124 0x12: 'metadata'
125 }; 125 };
126 126
127 videojs.log = console.log.bind(console);
128
127 original.addEventListener('change', function() { 129 original.addEventListener('change', function() {
128 var reader = new FileReader(); 130 var reader = new FileReader();
129 reader.addEventListener('loadend', function() { 131 reader.addEventListener('loadend', function() {
......
...@@ -840,8 +840,11 @@ test('calls abort() on the SourceBuffer before seeking', function() { ...@@ -840,8 +840,11 @@ test('calls abort() on the SourceBuffer before seeking', function() {
840 standardXHRResponse(requests[0]); 840 standardXHRResponse(requests[0]);
841 standardXHRResponse(requests[1]); 841 standardXHRResponse(requests[1]);
842 842
843 // seek to 7s 843 // drainBuffer() uses the first PTS value to account for any timestamp discontinuities in the stream
844 // adding a tag with a PTS of zero looks like a stream with no discontinuities
845 tags.push({ pts: 0, bytes: 0 });
844 tags.push({ pts: 7000, bytes: 7 }); 846 tags.push({ pts: 7000, bytes: 7 });
847 // seek to 7s
845 player.currentTime(7); 848 player.currentTime(7);
846 standardXHRResponse(requests[2]); 849 standardXHRResponse(requests[2]);
847 850
...@@ -1047,6 +1050,106 @@ test('does not break if the playlist has no segments', function() { ...@@ -1047,6 +1050,106 @@ test('does not break if the playlist has no segments', function() {
1047 strictEqual(requests.length, 1, 'no requests for non-existent segments were queued'); 1050 strictEqual(requests.length, 1, 'no requests for non-existent segments were queued');
1048 }); 1051 });
1049 1052
1053 test('waits until the buffer is empty before appending bytes at a discontinuity', function() {
1054 var aborts = 0, setTime, currentTime, bufferEnd;
1055
1056 player.src({
1057 src: 'disc.m3u8',
1058 type: 'application/vnd.apple.mpegurl'
1059 });
1060 player.hls.mediaSource.trigger({
1061 type: 'sourceopen'
1062 });
1063 player.currentTime = function() { return currentTime; };
1064 player.buffered = function() {
1065 return videojs.createTimeRange(0, bufferEnd);
1066 };
1067 player.hls.sourceBuffer.abort = function() {
1068 aborts++;
1069 };
1070 player.hls.el().vjs_setProperty = function(name, value) {
1071 if (name === 'currentTime') {
1072 return setTime = value;
1073 }
1074 };
1075
1076 requests.pop().respond(200, null,
1077 '#EXTM3U\n' +
1078 '#EXTINF:10,0\n' +
1079 '1.ts\n' +
1080 '#EXT-X-DISCONTINUITY\n' +
1081 '#EXTINF:10,0\n' +
1082 '2.ts\n');
1083 standardXHRResponse(requests.pop());
1084
1085 // play to 6s to trigger the next segment request
1086 currentTime = 6;
1087 bufferEnd = 10;
1088 player.trigger('timeupdate');
1089 strictEqual(aborts, 0, 'no aborts before the buffer empties');
1090
1091 standardXHRResponse(requests.pop());
1092 strictEqual(aborts, 0, 'no aborts before the buffer empties');
1093
1094 // pretend the buffer has emptied
1095 player.trigger('waiting');
1096 strictEqual(aborts, 1, 'aborted before appending the new segment');
1097 strictEqual(setTime, 10, 'updated the time after crossing the discontinuity');
1098 });
1099
1100 test('clears the segment buffer on seek', function() {
1101 var aborts = 0, tags = [], currentTime, bufferEnd, oldCurrentTime;
1102
1103 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1104
1105 player.src({
1106 src: 'disc.m3u8',
1107 type: 'application/vnd.apple.mpegurl'
1108 });
1109 player.hls.mediaSource.trigger({
1110 type: 'sourceopen'
1111 });
1112 oldCurrentTime = player.currentTime;
1113 player.currentTime = function(time) {
1114 if (time !== undefined) {
1115 return oldCurrentTime.call(player, time);
1116 }
1117 return currentTime;
1118 };
1119 player.buffered = function() {
1120 return videojs.createTimeRange(0, bufferEnd);
1121 };
1122 player.hls.sourceBuffer.abort = function() {
1123 aborts++;
1124 };
1125
1126 requests.pop().respond(200, null,
1127 '#EXTM3U\n' +
1128 '#EXTINF:10,0\n' +
1129 '1.ts\n' +
1130 '#EXT-X-DISCONTINUITY\n' +
1131 '#EXTINF:10,0\n' +
1132 '2.ts\n');
1133 standardXHRResponse(requests.pop());
1134
1135 // play to 6s to trigger the next segment request
1136 currentTime = 6;
1137 bufferEnd = 10;
1138 player.trigger('timeupdate');
1139
1140 standardXHRResponse(requests.pop());
1141
1142 // seek back to the beginning
1143 player.currentTime(0);
1144 tags.push({ pts: 0, bytes: 0 });
1145 standardXHRResponse(requests.pop());
1146 strictEqual(aborts, 1, 'aborted once for the seek');
1147
1148 // the source buffer empties. is 2.ts still in the segment buffer?
1149 player.trigger('waiting');
1150 strictEqual(aborts, 1, 'cleared the segment buffer on a seek');
1151 });
1152
1050 test('disposes the playlist loader', function() { 1153 test('disposes the playlist loader', function() {
1051 var disposes = 0, player, loaderDispose; 1154 var disposes = 0, player, loaderDispose;
1052 player = createPlayer(); 1155 player = createPlayer();
......