d8cf74c3 by David LaPalomento

Integrate playlist loader

Remove old playlist download code and use the playlist loader. Update test cases.
1 parent 2c6ddb6e
...@@ -47,7 +47,7 @@ ...@@ -47,7 +47,7 @@
47 return changed ? result : null; 47 return changed ? result : null;
48 }, 48 },
49 49
50 PlaylistLoader = function(srcUrl) { 50 PlaylistLoader = function(srcUrl, withCredentials) {
51 var 51 var
52 loader = this, 52 loader = this,
53 media, 53 media,
...@@ -92,6 +92,8 @@ ...@@ -92,6 +92,8 @@
92 loader.trigger('mediaupdatetimeout'); 92 loader.trigger('mediaupdatetimeout');
93 }, refreshDelay); 93 }, refreshDelay);
94 } 94 }
95
96 loader.trigger('loadedplaylist');
95 }; 97 };
96 98
97 PlaylistLoader.prototype.init.call(this); 99 PlaylistLoader.prototype.init.call(this);
...@@ -122,13 +124,6 @@ ...@@ -122,13 +124,6 @@
122 if (loader.state === 'HAVE_NOTHING' || loader.state === 'HAVE_MASTER') { 124 if (loader.state === 'HAVE_NOTHING' || loader.state === 'HAVE_MASTER') {
123 throw new Error('Cannot switch media playlist from ' + loader.state); 125 throw new Error('Cannot switch media playlist from ' + loader.state);
124 } 126 }
125 loader.state = 'SWITCHING_MEDIA';
126
127 // abort any outstanding playlist refreshes
128 if (request) {
129 request.abort();
130 request = null;
131 }
132 127
133 // find the playlist object if the target playlist has been 128 // find the playlist object if the target playlist has been
134 // specified by URI 129 // specified by URI
...@@ -139,8 +134,24 @@ ...@@ -139,8 +134,24 @@
139 playlist = loader.master.playlists[playlist]; 134 playlist = loader.master.playlists[playlist];
140 } 135 }
141 136
137 if (playlist.uri === media.uri) {
138 // switching to the currently active playlist is a no-op
139 return;
140 }
141
142 loader.state = 'SWITCHING_MEDIA';
143
144 // abort any outstanding playlist refreshes
145 if (request) {
146 request.abort();
147 request = null;
148 }
149
142 // request the new playlist 150 // request the new playlist
143 request = xhr(resolveUrl(loader.master.uri, playlist.uri), function(error) { 151 request = xhr({
152 url: resolveUrl(loader.master.uri, playlist.uri),
153 withCredentials: withCredentials
154 }, function(error) {
144 haveMetadata(error, this, playlist.uri); 155 haveMetadata(error, this, playlist.uri);
145 }); 156 });
146 }; 157 };
...@@ -153,21 +164,26 @@ ...@@ -153,21 +164,26 @@
153 } 164 }
154 165
155 loader.state = 'HAVE_CURRENT_METADATA'; 166 loader.state = 'HAVE_CURRENT_METADATA';
156 request = xhr(resolveUrl(loader.master.uri, loader.media().uri), 167 request = xhr({
157 function(error) { 168 url: resolveUrl(loader.master.uri, loader.media().uri),
169 withCredentials: withCredentials
170 }, function(error) {
158 haveMetadata(error, this, loader.media().uri); 171 haveMetadata(error, this, loader.media().uri);
159 }); 172 });
160 }); 173 });
161 174
162 // request the specified URL 175 // request the specified URL
163 xhr(srcUrl, function(error) { 176 xhr({
177 url: srcUrl,
178 withCredentials: withCredentials
179 }, function(error) {
164 var parser, i; 180 var parser, i;
165 181
166 if (error) { 182 if (error) {
167 loader.error = { 183 loader.error = {
168 status: this.status, 184 status: this.status,
169 message: 'HLS playlist request error at URL: ' + srcUrl, 185 message: 'HLS playlist request error at URL: ' + srcUrl,
170 code: (this.status >= 500) ? 4 : 2 186 code: 2 // MEDIA_ERR_NETWORK
171 }; 187 };
172 return loader.trigger('error'); 188 return loader.trigger('error');
173 } 189 }
...@@ -189,12 +205,15 @@ ...@@ -189,12 +205,15 @@
189 loader.master.playlists[loader.master.playlists[i].uri] = loader.master.playlists[i]; 205 loader.master.playlists[loader.master.playlists[i].uri] = loader.master.playlists[i];
190 } 206 }
191 207
192 request = xhr(resolveUrl(srcUrl, parser.manifest.playlists[0].uri), 208 request = xhr({
193 function(error) { 209 url: resolveUrl(srcUrl, parser.manifest.playlists[0].uri),
210 withCredentials: withCredentials
211 }, function(error) {
194 // pass along the URL specified in the master playlist 212 // pass along the URL specified in the master playlist
195 haveMetadata(error, 213 haveMetadata(error,
196 this, 214 this,
197 parser.manifest.playlists[0].uri); 215 parser.manifest.playlists[0].uri);
216 loader.trigger('loadedmetadata');
198 }); 217 });
199 return loader.trigger('loadedplaylist'); 218 return loader.trigger('loadedplaylist');
200 } 219 }
...@@ -208,7 +227,8 @@ ...@@ -208,7 +227,8 @@
208 }] 227 }]
209 }; 228 };
210 loader.master.playlists[srcUrl] = loader.master.playlists[0]; 229 loader.master.playlists[srcUrl] = loader.master.playlists[0];
211 return haveMetadata(null, this, srcUrl); 230 haveMetadata(null, this, srcUrl);
231 return loader.trigger('loadedmetadata');
212 }); 232 });
213 }; 233 };
214 PlaylistLoader.prototype = new videojs.hls.Stream(); 234 PlaylistLoader.prototype = new videojs.hls.Stream();
......
...@@ -194,15 +194,20 @@ var ...@@ -194,15 +194,20 @@ var
194 */ 194 */
195 translateMediaIndex = function(mediaIndex, original, update) { 195 translateMediaIndex = function(mediaIndex, original, update) {
196 var 196 var
197 i = update.segments.length, 197 i,
198 originalSegment; 198 originalSegment;
199 199
200 // no segments have been loaded from the original playlist 200 // no segments have been loaded from the original playlist
201 if (mediaIndex === 0) { 201 if (mediaIndex === 0) {
202 return 0; 202 return 0;
203 } 203 }
204 if (!(update && update.segments)) {
205 // let the media index be zero when there are no segments defined
206 return 0;
207 }
204 208
205 // try to sync based on URI 209 // try to sync based on URI
210 i = update.segments.length;
206 originalSegment = original.segments[mediaIndex - 1]; 211 originalSegment = original.segments[mediaIndex - 1];
207 while (i--) { 212 while (i--) {
208 if (originalSegment.uri === update.segments[i].uri) { 213 if (originalSegment.uri === update.segments[i].uri) {
...@@ -292,12 +297,9 @@ var ...@@ -292,12 +297,9 @@ var
292 player = this, 297 player = this,
293 srcUrl, 298 srcUrl,
294 299
295 playlistXhr,
296 segmentXhr, 300 segmentXhr,
297 settings, 301 settings,
298 loadedPlaylist,
299 fillBuffer, 302 fillBuffer,
300 updateCurrentPlaylist,
301 updateDuration; 303 updateDuration;
302 304
303 // if the video element supports HLS natively, do nothing 305 // if the video element supports HLS natively, do nothing
...@@ -376,7 +378,8 @@ var ...@@ -376,7 +378,8 @@ var
376 378
377 player.on('seeking', function() { 379 player.on('seeking', function() {
378 var currentTime = player.currentTime(); 380 var currentTime = player.currentTime();
379 player.hls.mediaIndex = getMediaIndexByTime(player.hls.media, currentTime); 381 player.hls.mediaIndex = getMediaIndexByTime(player.hls.playlists.media(),
382 currentTime);
380 383
381 // abort any segments still being decoded 384 // abort any segments still being decoded
382 player.hls.sourceBuffer.abort(); 385 player.hls.sourceBuffer.abort();
...@@ -407,39 +410,6 @@ var ...@@ -407,39 +410,6 @@ var
407 }; 410 };
408 411
409 /** 412 /**
410 * Determine whether the current media playlist should be changed
411 * and trigger a switch if necessary. If a sufficiently fresh
412 * version of the target playlist is available, the switch will take
413 * effect immediately. Otherwise, the target playlist will be
414 * refreshed.
415 */
416 updateCurrentPlaylist = function() {
417 var playlist, mediaSequence;
418 playlist = player.hls.selectPlaylist();
419 mediaSequence = player.hls.mediaIndex + (player.hls.media.mediaSequence || 0);
420 if (!playlist.segments ||
421 mediaSequence < (playlist.mediaSequence || 0) ||
422 mediaSequence > (playlist.mediaSequence || 0) + playlist.segments.length) {
423
424 if (playlistXhr) {
425 playlistXhr.abort();
426 }
427 playlistXhr = xhr({
428 url: resolveUrl(srcUrl, playlist.uri),
429 withCredentials: settings.withCredentials
430 }, loadedPlaylist);
431 } else {
432 player.hls.mediaIndex =
433 translateMediaIndex(player.hls.mediaIndex,
434 player.hls.media,
435 playlist);
436 player.hls.media = playlist;
437
438 updateDuration(player.hls.media);
439 }
440 };
441
442 /**
443 * Chooses the appropriate media playlist based on the current 413 * Chooses the appropriate media playlist based on the current
444 * bandwidth estimate and the player size. 414 * bandwidth estimate and the player size.
445 * @return the highest bitrate playlist less than the currently detected 415 * @return the highest bitrate playlist less than the currently detected
...@@ -448,7 +418,7 @@ var ...@@ -448,7 +418,7 @@ var
448 player.hls.selectPlaylist = function () { 418 player.hls.selectPlaylist = function () {
449 var 419 var
450 effectiveBitrate, 420 effectiveBitrate,
451 sortedPlaylists = player.hls.master.playlists.slice(), 421 sortedPlaylists = player.hls.playlists.master.playlists.slice(),
452 bandwidthPlaylists = [], 422 bandwidthPlaylists = [],
453 i = sortedPlaylists.length, 423 i = sortedPlaylists.length,
454 variant, 424 variant,
...@@ -513,97 +483,6 @@ var ...@@ -513,97 +483,6 @@ var
513 }; 483 };
514 484
515 /** 485 /**
516 * Callback that is invoked when a media playlist finishes
517 * downloading. Triggers `loadedmanifest` once for each playlist
518 * that is downloaded and `loadedmetadata` after at least one
519 * media playlist has been parsed.
520 *
521 * @param error {*} truthy if the request was not successful
522 * @param url {string} a URL to the M3U8 file to process
523 */
524 loadedPlaylist = function(error, url) {
525 var i, parser, playlist, playlistUri, refreshDelay;
526
527 // clear the current playlist XHR
528 playlistXhr = null;
529
530 if (error) {
531 player.hls.error = {
532 status: this.status,
533 message: 'HLS playlist request error at URL: ' + url,
534 code: (this.status >= 500) ? 4 : 2
535 };
536 return player.trigger('error');
537 }
538
539 parser = new videojs.m3u8.Parser();
540 parser.push(this.responseText);
541
542 // merge this playlist into the master
543 i = player.hls.master.playlists.length;
544 refreshDelay = (parser.manifest.targetDuration || 10) * 1000;
545 while (i--) {
546 playlist = player.hls.master.playlists[i];
547 playlistUri = resolveUrl(srcUrl, playlist.uri);
548 if (playlistUri === url) {
549 // if the playlist is unchanged since the last reload,
550 // try again after half the target duration
551 // http://tools.ietf.org/html/draft-pantos-http-live-streaming-12#section-6.3.4
552 if (playlist.segments &&
553 playlist.segments.length === parser.manifest.segments.length) {
554 refreshDelay /= 2;
555 }
556
557 player.hls.master.playlists[i] =
558 videojs.util.mergeOptions(playlist, parser.manifest);
559
560 if (playlist !== player.hls.media) {
561 continue;
562 }
563
564 // determine the new mediaIndex if we're updating the
565 // current media playlist
566 player.hls.mediaIndex =
567 translateMediaIndex(player.hls.mediaIndex,
568 playlist,
569 parser.manifest);
570 player.hls.media = parser.manifest;
571 }
572 }
573
574 // check the playlist for updates if EXT-X-ENDLIST isn't present
575 if (!parser.manifest.endList) {
576 window.setTimeout(function() {
577 if (!playlistXhr &&
578 resolveUrl(srcUrl, player.hls.media.uri) === url) {
579 playlistXhr = xhr(url, loadedPlaylist);
580 }
581 }, refreshDelay);
582 }
583
584 // always start playback with the default rendition
585 if (!player.hls.media) {
586 player.hls.media = player.hls.master.playlists[0];
587
588 // update the duration
589 updateDuration(parser.manifest);
590
591 // periodicaly check if the buffer needs to be refilled
592 player.on('timeupdate', fillBuffer);
593
594 player.trigger('loadedmanifest');
595 player.trigger('loadedmetadata');
596 fillBuffer();
597 return;
598 }
599
600 // select a playlist and download its metadata if necessary
601 updateCurrentPlaylist();
602
603 player.trigger('loadedmanifest');
604 };
605
606 /**
607 * Determines whether there is enough video data currently in the buffer 486 * Determines whether there is enough video data currently in the buffer
608 * and downloads a new segment if the buffered time is less than the goal. 487 * and downloads a new segment if the buffered time is less than the goal.
609 * @param offset (optional) {number} the offset into the downloaded segment 488 * @param offset (optional) {number} the offset into the downloaded segment
...@@ -623,12 +502,12 @@ var ...@@ -623,12 +502,12 @@ var
623 } 502 }
624 503
625 // if no segments are available, do nothing 504 // if no segments are available, do nothing
626 if (!player.hls.media.segments) { 505 if (!player.hls.playlists.media().segments) {
627 return; 506 return;
628 } 507 }
629 508
630 // if the video has finished downloading, stop trying to buffer 509 // if the video has finished downloading, stop trying to buffer
631 segment = player.hls.media.segments[player.hls.mediaIndex]; 510 segment = player.hls.playlists.media().segments[player.hls.mediaIndex];
632 if (!segment) { 511 if (!segment) {
633 return; 512 return;
634 } 513 }
...@@ -644,10 +523,10 @@ var ...@@ -644,10 +523,10 @@ var
644 } 523 }
645 524
646 // resolve the segment URL relative to the playlist 525 // resolve the segment URL relative to the playlist
647 if (player.hls.media.uri === srcUrl) { 526 if (player.hls.playlists.media().uri === srcUrl) {
648 segmentUri = resolveUrl(srcUrl, segment.uri); 527 segmentUri = resolveUrl(srcUrl, segment.uri);
649 } else { 528 } else {
650 segmentUri = resolveUrl(resolveUrl(srcUrl, player.hls.media.uri || ''), 529 segmentUri = resolveUrl(resolveUrl(srcUrl, player.hls.playlists.media().uri || ''),
651 segment.uri); 530 segment.uri);
652 } 531 }
653 532
...@@ -708,49 +587,55 @@ var ...@@ -708,49 +587,55 @@ var
708 587
709 player.hls.mediaIndex++; 588 player.hls.mediaIndex++;
710 589
711 if (player.hls.mediaIndex === player.hls.media.segments.length) { 590 if (player.hls.mediaIndex === player.hls.playlists.media().segments.length) {
712 mediaSource.endOfStream(); 591 mediaSource.endOfStream();
713 } 592 }
714 593
715 // figure out what stream the next segment should be downloaded from 594 // figure out what stream the next segment should be downloaded from
716 // with the updated bandwidth information 595 // with the updated bandwidth information
717 updateCurrentPlaylist(); 596 player.hls.playlists.media(player.hls.selectPlaylist());
718 }); 597 });
719 }; 598 };
720 599
721 // load the MediaSource into the player 600 // load the MediaSource into the player
722 mediaSource.addEventListener('sourceopen', function() { 601 mediaSource.addEventListener('sourceopen', function() {
723 // construct the video data buffer and set the appropriate MIME type 602 // construct the video data buffer and set the appropriate MIME type
724 var sourceBuffer = mediaSource.addSourceBuffer('video/flv; codecs="vp6,aac"'); 603 var
604 sourceBuffer = mediaSource.addSourceBuffer('video/flv; codecs="vp6,aac"'),
605 oldMediaPlaylist;
606
725 player.hls.sourceBuffer = sourceBuffer; 607 player.hls.sourceBuffer = sourceBuffer;
726 sourceBuffer.appendBuffer(segmentParser.getFlvHeader()); 608 sourceBuffer.appendBuffer(segmentParser.getFlvHeader());
727 609
728 player.hls.mediaIndex = 0; 610 player.hls.mediaIndex = 0;
729 xhr({ 611 player.hls.playlists =
730 url: srcUrl, 612 new videojs.hls.PlaylistLoader(srcUrl, settings.withCredentials);
731 withCredentials: settings.withCredentials 613 player.hls.playlists.on('loadedmetadata', function() {
732 }, function(error, url) { 614 oldMediaPlaylist = player.hls.playlists.media();
733 var uri, parser = new videojs.m3u8.Parser(); 615
734 parser.push(this.responseText); 616 // periodicaly check if the buffer needs to be refilled
735 617 fillBuffer();
736 // master playlists 618 player.on('timeupdate', fillBuffer);
737 if (parser.manifest.playlists) { 619
738 player.hls.master = parser.manifest; 620 player.trigger('loadedmetadata');
739 playlistXhr = xhr({ 621 });
740 url: resolveUrl(url, parser.manifest.playlists[0].uri), 622 player.hls.playlists.on('error', function() {
741 withCredentials: settings.withCredentials 623 player.hls.error = player.hls.playlists.error;
742 }, loadedPlaylist); 624 player.trigger('error');
743 return player.trigger('loadedmanifest'); 625 });
744 } else { 626 player.hls.playlists.on('loadedplaylist', function() {
745 // infer a master playlist if a media playlist is loaded directly 627 var updatedPlaylist = player.hls.playlists.media();
746 uri = resolveUrl(window.location.href, url); 628
747 player.hls.master = { 629 if (!updatedPlaylist) {
748 playlists: [{ 630 // do nothing before an initial media playlist has been activated
749 uri: uri 631 return;
750 }]
751 };
752 loadedPlaylist.call(this, error, uri);
753 } 632 }
633
634 updateDuration(player.hls.playlists.media());
635 player.hls.mediaIndex = translateMediaIndex(player.hls.mediaIndex,
636 oldMediaPlaylist,
637 updatedPlaylist);
638 oldMediaPlaylist = updatedPlaylist;
754 }); 639 });
755 }); 640 });
756 player.src([{ 641 player.src([{
......
...@@ -65,7 +65,12 @@ ...@@ -65,7 +65,12 @@
65 }); 65 });
66 66
67 test('jumps to HAVE_METADATA when initialized with a media playlist', function() { 67 test('jumps to HAVE_METADATA when initialized with a media playlist', function() {
68 var loader = new videojs.hls.PlaylistLoader('media.m3u8'); 68 var
69 loadedmetadatas = 0,
70 loader = new videojs.hls.PlaylistLoader('media.m3u8');
71 loader.on('loadedmetadata', function() {
72 loadedmetadatas++;
73 });
69 requests.pop().respond(200, null, 74 requests.pop().respond(200, null,
70 '#EXTM3U\n' + 75 '#EXTM3U\n' +
71 '#EXTINF:10,\n' + 76 '#EXTINF:10,\n' +
...@@ -75,7 +80,8 @@ ...@@ -75,7 +80,8 @@
75 ok(loader.media(), 'sets the media playlist'); 80 ok(loader.media(), 'sets the media playlist');
76 ok(loader.media().uri, 'sets the media playlist URI'); 81 ok(loader.media().uri, 'sets the media playlist URI');
77 strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); 82 strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
78 strictEqual(0, requests.length, 'no more requests are made'); 83 strictEqual(requests.length, 0, 'no more requests are made');
84 strictEqual(loadedmetadatas, 1, 'fired one loadedmetadata');
79 }); 85 });
80 86
81 test('jumps to HAVE_METADATA when initialized with a live media playlist', function() { 87 test('jumps to HAVE_METADATA when initialized with a live media playlist', function() {
...@@ -91,17 +97,22 @@ ...@@ -91,17 +97,22 @@
91 97
92 test('moves to HAVE_METADATA after loading a media playlist', function() { 98 test('moves to HAVE_METADATA after loading a media playlist', function() {
93 var 99 var
94 loadedPlaylist = false, 100 loadedPlaylist = 0,
101 loadedMetadata = 0,
95 loader = new videojs.hls.PlaylistLoader('master.m3u8'); 102 loader = new videojs.hls.PlaylistLoader('master.m3u8');
96 loader.on('loadedplaylist', function() { 103 loader.on('loadedplaylist', function() {
97 loadedPlaylist = true; 104 loadedPlaylist++;
105 });
106 loader.on('loadedmetadata', function() {
107 loadedMetadata++;
98 }); 108 });
99 requests.pop().respond(200, null, 109 requests.pop().respond(200, null,
100 '#EXTM3U\n' + 110 '#EXTM3U\n' +
101 '#EXT-X-STREAM-INF:\n' + 111 '#EXT-X-STREAM-INF:\n' +
102 'media.m3u8\n' + 112 'media.m3u8\n' +
103 'alt.m3u8\n'); 113 'alt.m3u8\n');
104 ok(loadedPlaylist, 'loadedplaylist fired'); 114 strictEqual(loadedPlaylist, 1, 'fired loadedplaylist once');
115 strictEqual(loadedMetadata, 0, 'did not fire loadedmetadata');
105 strictEqual(requests.length, 1, 'requests the media playlist'); 116 strictEqual(requests.length, 1, 'requests the media playlist');
106 strictEqual(requests[0].method, 'GET', 'GETs the media playlist'); 117 strictEqual(requests[0].method, 'GET', 'GETs the media playlist');
107 strictEqual(requests[0].url, 118 strictEqual(requests[0].url,
...@@ -114,6 +125,8 @@ ...@@ -114,6 +125,8 @@
114 '0.ts\n'); 125 '0.ts\n');
115 ok(loader.master, 'sets the master playlist'); 126 ok(loader.master, 'sets the master playlist');
116 ok(loader.media(), 'sets the media playlist'); 127 ok(loader.media(), 'sets the media playlist');
128 strictEqual(loadedPlaylist, 2, 'fired loadedplaylist twice');
129 strictEqual(loadedMetadata, 1, 'fired loadedmetadata once');
117 strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); 130 strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
118 }); 131 });
119 132
...@@ -317,6 +330,25 @@ ...@@ -317,6 +330,25 @@
317 strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state'); 330 strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state');
318 }); 331 });
319 332
333 test('switching to the active playlist is a no-op', function() {
334 var loader = new videojs.hls.PlaylistLoader('master.m3u8');
335 requests.pop().respond(200, null,
336 '#EXTM3U\n' +
337 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
338 'low.m3u8\n' +
339 '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
340 'high.m3u8\n');
341 requests.pop().respond(200, null,
342 '#EXTM3U\n' +
343 '#EXT-X-MEDIA-SEQUENCE:0\n' +
344 '#EXTINF:10,\n' +
345 'low-0.ts\n' +
346 '#EXT-X-ENDLIST\n');
347 loader.media('low.m3u8');
348
349 strictEqual(requests.length, 0, 'no requests is sent');
350 });
351
320 test('throws an error if a media switch is initiated too early', function() { 352 test('throws an error if a media switch is initiated too early', function() {
321 var loader = new videojs.hls.PlaylistLoader('master.m3u8'); 353 var loader = new videojs.hls.PlaylistLoader('master.m3u8');
322 354
......