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
......
...@@ -149,30 +149,25 @@ test('starts playing if autoplay is specified', function() { ...@@ -149,30 +149,25 @@ test('starts playing if autoplay is specified', function() {
149 strictEqual(1, plays, 'play was called'); 149 strictEqual(1, plays, 'play was called');
150 }); 150 });
151 151
152 test('loads the specified manifest URL on init', function() { 152 test('creates a PlaylistLoader on init', function() {
153 var loadedmanifest = false, loadedmetadata = false; 153 var loadedmetadata = false;
154 player.on('loadedmanifest', function() {
155 loadedmanifest = true;
156 });
157 player.on('loadedmetadata', function() { 154 player.on('loadedmetadata', function() {
158 loadedmetadata = true; 155 loadedmetadata = true;
159 }); 156 });
160 157
161 player.hls('manifest/playlist.m3u8'); 158 player.hls('manifest/playlist.m3u8');
162 strictEqual(player.hls.readyState(), 0, 'the readyState is HAVE_NOTHING'); 159 ok(!player.hls.playlists, 'waits for sourceopen to create the loader');
163 videojs.mediaSources[player.currentSrc()].trigger({ 160 videojs.mediaSources[player.currentSrc()].trigger({
164 type: 'sourceopen' 161 type: 'sourceopen'
165 }); 162 });
166 standardXHRResponse(requests[0]); 163 standardXHRResponse(requests[0]);
167 ok(loadedmanifest, 'loadedmanifest fires');
168 ok(loadedmetadata, 'loadedmetadata fires'); 164 ok(loadedmetadata, 'loadedmetadata fires');
169 ok(player.hls.master, 'a master is inferred'); 165 ok(player.hls.playlists.master, 'set the master playlist');
170 ok(player.hls.media, 'the manifest is available'); 166 ok(player.hls.playlists.media(), 'set the media playlist');
171 ok(player.hls.media.segments, 'the segment entries are parsed'); 167 ok(player.hls.playlists.media().segments, 'the segment entries are parsed');
172 strictEqual(player.hls.master.playlists[0], 168 strictEqual(player.hls.playlists.master.playlists[0],
173 player.hls.media, 169 player.hls.playlists.media(),
174 'the playlist is selected'); 170 'the playlist is selected');
175 strictEqual(player.hls.readyState(), 1, 'the readyState is HAVE_METADATA');
176 }); 171 });
177 172
178 test('sets the duration if one is available on the playlist', function() { 173 test('sets the duration if one is available on the playlist', function() {
...@@ -189,8 +184,9 @@ test('sets the duration if one is available on the playlist', function() { ...@@ -189,8 +184,9 @@ test('sets the duration if one is available on the playlist', function() {
189 }); 184 });
190 185
191 standardXHRResponse(requests[0]); 186 standardXHRResponse(requests[0]);
187 strictEqual(calls, 1, 'duration is set');
192 standardXHRResponse(requests[1]); 188 standardXHRResponse(requests[1]);
193 strictEqual(calls, 2, 'duration is set'); 189 strictEqual(calls, 1, 'duration is set');
194 }); 190 });
195 191
196 test('calculates the duration if needed', function() { 192 test('calculates the duration if needed', function() {
...@@ -207,10 +203,9 @@ test('calculates the duration if needed', function() { ...@@ -207,10 +203,9 @@ test('calculates the duration if needed', function() {
207 }); 203 });
208 204
209 standardXHRResponse(requests[0]); 205 standardXHRResponse(requests[0]);
210 standardXHRResponse(requests[1]); 206 strictEqual(durations.length, 1, 'duration is set');
211 strictEqual(durations.length, 2, 'duration is set');
212 strictEqual(durations[0], 207 strictEqual(durations[0],
213 player.hls.media.segments.length * 10, 208 player.hls.playlists.media().segments.length * 10,
214 'duration is calculated'); 209 'duration is calculated');
215 }); 210 });
216 211
...@@ -269,18 +264,7 @@ test('re-initializes the plugin for each source', function() { ...@@ -269,18 +264,7 @@ test('re-initializes the plugin for each source', function() {
269 }); 264 });
270 265
271 test('triggers an error when a master playlist request errors', function() { 266 test('triggers an error when a master playlist request errors', function() {
272 var 267 var error;
273 status = 0,
274 error;
275 window.XMLHttpRequest = function() {
276 this.open = function() {};
277 this.send = function() {
278 this.readyState = 4;
279 this.status = status;
280 this.onreadystatechange();
281 };
282 };
283
284 player.on('error', function() { 268 player.on('error', function() {
285 error = player.hls.error; 269 error = player.hls.error;
286 }); 270 });
...@@ -288,6 +272,7 @@ test('triggers an error when a master playlist request errors', function() { ...@@ -288,6 +272,7 @@ test('triggers an error when a master playlist request errors', function() {
288 videojs.mediaSources[player.currentSrc()].trigger({ 272 videojs.mediaSources[player.currentSrc()].trigger({
289 type: 'sourceopen' 273 type: 'sourceopen'
290 }); 274 });
275 requests.pop().respond(500);
291 276
292 ok(error, 'an error is triggered'); 277 ok(error, 'an error is triggered');
293 strictEqual(2, error.code, 'a network error is triggered'); 278 strictEqual(2, error.code, 'a network error is triggered');
...@@ -355,7 +340,7 @@ test('selects a playlist after segment downloads', function() { ...@@ -355,7 +340,7 @@ test('selects a playlist after segment downloads', function() {
355 player.hls('manifest/master.m3u8'); 340 player.hls('manifest/master.m3u8');
356 player.hls.selectPlaylist = function() { 341 player.hls.selectPlaylist = function() {
357 calls++; 342 calls++;
358 return player.hls.master.playlists[0]; 343 return player.hls.playlists.master.playlists[0];
359 }; 344 };
360 videojs.mediaSources[player.currentSrc()].trigger({ 345 videojs.mediaSources[player.currentSrc()].trigger({
361 type: 'sourceopen' 346 type: 'sourceopen'
...@@ -375,6 +360,7 @@ test('selects a playlist after segment downloads', function() { ...@@ -375,6 +360,7 @@ test('selects a playlist after segment downloads', function() {
375 player.trigger('timeupdate'); 360 player.trigger('timeupdate');
376 361
377 standardXHRResponse(requests[3]); 362 standardXHRResponse(requests[3]);
363 console.log(requests.map(function(i) { return i.url; }));
378 strictEqual(calls, 2, 'selects after additional segments'); 364 strictEqual(calls, 2, 'selects after additional segments');
379 }); 365 });
380 366
...@@ -403,14 +389,14 @@ test('updates the duration after switching playlists', function() { ...@@ -403,14 +389,14 @@ test('updates the duration after switching playlists', function() {
403 player.hls('manifest/master.m3u8'); 389 player.hls('manifest/master.m3u8');
404 player.hls.selectPlaylist = function() { 390 player.hls.selectPlaylist = function() {
405 selectedPlaylist = true; 391 selectedPlaylist = true;
406 return player.hls.master.playlists[1]; 392 return player.hls.playlists.master.playlists[1];
407 }; 393 };
408 player.duration = function(duration) { 394 player.duration = function(duration) {
409 if (duration === undefined) { 395 if (duration === undefined) {
410 return 0; 396 return 0;
411 } 397 }
412 // only track calls that occur after the playlist has been switched 398 // only track calls that occur after the playlist has been switched
413 if (player.hls.media === player.hls.master.playlists[1]) { 399 if (player.hls.playlists.media() === player.hls.playlists.master.playlists[1]) {
414 calls++; 400 calls++;
415 } 401 }
416 }; 402 };
...@@ -462,8 +448,10 @@ test('downloads additional playlists if required', function() { ...@@ -462,8 +448,10 @@ test('downloads additional playlists if required', function() {
462 '/manifest/' + 448 '/manifest/' +
463 playlist.uri, 449 playlist.uri,
464 'made playlist request'); 450 'made playlist request');
465 strictEqual(playlist, player.hls.media, 'a new playlists was selected'); 451 strictEqual(playlist.uri,
466 ok(player.hls.media.segments, 'segments are now available'); 452 player.hls.playlists.media().uri,
453 'a new playlists was selected');
454 ok(player.hls.playlists.media().segments, 'segments are now available');
467 }); 455 });
468 456
469 test('selects a playlist below the current bandwidth', function() { 457 test('selects a playlist below the current bandwidth', function() {
...@@ -476,15 +464,15 @@ test('selects a playlist below the current bandwidth', function() { ...@@ -476,15 +464,15 @@ test('selects a playlist below the current bandwidth', function() {
476 standardXHRResponse(requests[0]); 464 standardXHRResponse(requests[0]);
477 465
478 // the default playlist has a really high bitrate 466 // the default playlist has a really high bitrate
479 player.hls.master.playlists[0].attributes.BANDWIDTH = 9e10; 467 player.hls.playlists.master.playlists[0].attributes.BANDWIDTH = 9e10;
480 // playlist 1 has a very low bitrate 468 // playlist 1 has a very low bitrate
481 player.hls.master.playlists[1].attributes.BANDWIDTH = 1; 469 player.hls.playlists.master.playlists[1].attributes.BANDWIDTH = 1;
482 // but the detected client bandwidth is really low 470 // but the detected client bandwidth is really low
483 player.hls.bandwidth = 10; 471 player.hls.bandwidth = 10;
484 472
485 playlist = player.hls.selectPlaylist(); 473 playlist = player.hls.selectPlaylist();
486 strictEqual(playlist, 474 strictEqual(playlist,
487 player.hls.master.playlists[1], 475 player.hls.playlists.master.playlists[1],
488 'the low bitrate stream is selected'); 476 'the low bitrate stream is selected');
489 }); 477 });
490 478
...@@ -498,15 +486,15 @@ test('raises the minimum bitrate for a stream proportionially', function() { ...@@ -498,15 +486,15 @@ test('raises the minimum bitrate for a stream proportionially', function() {
498 standardXHRResponse(requests[0]); 486 standardXHRResponse(requests[0]);
499 487
500 // the default playlist's bandwidth + 10% is equal to the current bandwidth 488 // the default playlist's bandwidth + 10% is equal to the current bandwidth
501 player.hls.master.playlists[0].attributes.BANDWIDTH = 10; 489 player.hls.playlists.master.playlists[0].attributes.BANDWIDTH = 10;
502 player.hls.bandwidth = 11; 490 player.hls.bandwidth = 11;
503 491
504 // 9.9 * 1.1 < 11 492 // 9.9 * 1.1 < 11
505 player.hls.master.playlists[1].attributes.BANDWIDTH = 9.9; 493 player.hls.playlists.master.playlists[1].attributes.BANDWIDTH = 9.9;
506 playlist = player.hls.selectPlaylist(); 494 playlist = player.hls.selectPlaylist();
507 495
508 strictEqual(playlist, 496 strictEqual(playlist,
509 player.hls.master.playlists[1], 497 player.hls.playlists.master.playlists[1],
510 'a lower bitrate stream is selected'); 498 'a lower bitrate stream is selected');
511 }); 499 });
512 500
...@@ -525,7 +513,7 @@ test('uses the lowest bitrate if no other is suitable', function() { ...@@ -525,7 +513,7 @@ test('uses the lowest bitrate if no other is suitable', function() {
525 513
526 // playlist 1 has the lowest advertised bitrate 514 // playlist 1 has the lowest advertised bitrate
527 strictEqual(playlist, 515 strictEqual(playlist,
528 player.hls.master.playlists[1], 516 player.hls.playlists.master.playlists[1],
529 'the lowest bitrate stream is selected'); 517 'the lowest bitrate stream is selected');
530 }); 518 });
531 519
...@@ -855,30 +843,21 @@ test('clears pending buffer updates when seeking', function() { ...@@ -855,30 +843,21 @@ test('clears pending buffer updates when seeking', function() {
855 843
856 test('playlist 404 should trigger MEDIA_ERR_NETWORK', function() { 844 test('playlist 404 should trigger MEDIA_ERR_NETWORK', function() {
857 var errorTriggered = false; 845 var errorTriggered = false;
858
859 window.XMLHttpRequest = function() {
860 this.open = function(method, url) {
861 xhrUrls.push(url);
862 };
863 this.send = function() {
864 this.readyState = 4;
865 this.status = 404;
866 this.onreadystatechange();
867 };
868 };
869
870 player.hls('manifest/media.m3u8');
871
872 player.on('error', function() { 846 player.on('error', function() {
873 errorTriggered = true; 847 errorTriggered = true;
874 }); 848 });
875 849 player.hls('manifest/media.m3u8');
876 videojs.mediaSources[player.currentSrc()].trigger({ 850 videojs.mediaSources[player.currentSrc()].trigger({
877 type: 'sourceopen' 851 type: 'sourceopen'
878 }); 852 });
879 853 requests.pop().respond(404);
880 equal(true, errorTriggered, 'Missing Playlist error event should trigger'); 854
881 equal(2, player.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_NETWORK'); 855 equal(errorTriggered,
856 true,
857 'Missing Playlist error event should trigger');
858 equal(player.hls.error.code,
859 2,
860 'Player error code should be set to MediaError.MEDIA_ERR_NETWORK');
882 ok(player.hls.error.message, 'Player error type should inform user correctly'); 861 ok(player.hls.error.message, 'Player error type should inform user correctly');
883 }); 862 });
884 863
...@@ -916,24 +895,6 @@ test('has no effect if native HLS is available', function() { ...@@ -916,24 +895,6 @@ test('has no effect if native HLS is available', function() {
916 'no media source was opened'); 895 'no media source was opened');
917 }); 896 });
918 897
919 test('reloads live playlists', function() {
920 var callbacks = [];
921 // capture timeouts
922 window.setTimeout = function(callback, timeout) {
923 callbacks.push({ callback: callback, timeout: timeout });
924 };
925 player.hls('http://example.com/manifest/missingEndlist.m3u8');
926 videojs.mediaSources[player.currentSrc()].trigger({
927 type: 'sourceopen'
928 });
929 standardXHRResponse(requests[0]);
930
931 strictEqual(1, callbacks.length, 'refresh was scheduled');
932 strictEqual(player.hls.media.targetDuration * 1000,
933 callbacks[0].timeout,
934 'waited one target duration');
935 });
936
937 test('duration is Infinity for live playlists', function() { 898 test('duration is Infinity for live playlists', function() {
938 player.hls('http://example.com/manifest/missingEndlist.m3u8'); 899 player.hls('http://example.com/manifest/missingEndlist.m3u8');
939 videojs.mediaSources[player.currentSrc()].trigger({ 900 videojs.mediaSources[player.currentSrc()].trigger({
...@@ -959,88 +920,37 @@ test('does not reload playlists with an endlist tag', function() { ...@@ -959,88 +920,37 @@ test('does not reload playlists with an endlist tag', function() {
959 strictEqual(0, callbacks.length, 'no refresh was scheduled'); 920 strictEqual(0, callbacks.length, 'no refresh was scheduled');
960 }); 921 });
961 922
962 test('reloads a live playlist after half a target duration if it has not ' + 923 test('updates the media index when a playlist reloads', function() {
963 'changed since the last request', function() { 924 player.hls('http://example.com/live-updating.m3u8');
964 var callbacks = [];
965 // capture timeouts
966 window.setTimeout = function(callback, timeout) {
967 callbacks.push({ callback: callback, timeout: timeout });
968 };
969 player.hls('http://example.com/manifest/missingEndlist.m3u8');
970 videojs.mediaSources[player.currentSrc()].trigger({
971 type: 'sourceopen'
972 });
973
974 standardXHRResponse(requests[0]);
975 standardXHRResponse(requests[1]);
976 strictEqual(callbacks.length, 1, 'full-length refresh scheduled');
977 callbacks.pop().callback();
978 standardXHRResponse(requests[2]);
979
980 strictEqual(callbacks.length, 1, 'half-length refresh was scheduled');
981 strictEqual(callbacks[0].timeout,
982 player.hls.media.targetDuration / 2 * 1000,
983 'waited half a target duration');
984 });
985
986 test('merges playlist reloads', function() {
987 var oldPlaylist,
988 callback;
989
990 // capture timeouts
991 window.setTimeout = function(cb) {
992 callback = cb;
993 };
994
995 player.hls('http://example.com/manifest/missingEndlist.m3u8');
996 videojs.mediaSources[player.currentSrc()].trigger({ 925 videojs.mediaSources[player.currentSrc()].trigger({
997 type: 'sourceopen' 926 type: 'sourceopen'
998 }); 927 });
999 standardXHRResponse(requests[0]);
1000 standardXHRResponse(requests[1]);
1001 oldPlaylist = player.hls.media;
1002
1003 callback();
1004 standardXHRResponse(requests[2]);
1005 ok(oldPlaylist !== player.hls.media, 'player.hls.media was updated');
1006 });
1007 928
1008 test('updates the media index when a playlist reloads', function() { 929 requests[0].respond(200, null,
1009 var callback;
1010 window.setTimeout = function(cb) {
1011 callback = cb;
1012 };
1013 // the initial playlist
1014 window.manifests['live-updating'] =
1015 '#EXTM3U\n' + 930 '#EXTM3U\n' +
1016 '#EXTINF:10,\n' + 931 '#EXTINF:10,\n' +
1017 '0.ts\n' + 932 '0.ts\n' +
1018 '#EXTINF:10,\n' + 933 '#EXTINF:10,\n' +
1019 '1.ts\n' + 934 '1.ts\n' +
1020 '#EXTINF:10,\n' + 935 '#EXTINF:10,\n' +
1021 '2.ts\n'; 936 '2.ts\n');
1022
1023 player.hls('http://example.com/live-updating.m3u8');
1024 videojs.mediaSources[player.currentSrc()].trigger({
1025 type: 'sourceopen'
1026 });
1027
1028 standardXHRResponse(requests[0]);
1029 standardXHRResponse(requests[1]); 937 standardXHRResponse(requests[1]);
1030 // play the stream until 2.ts is playing 938 // play the stream until 2.ts is playing
1031 player.hls.mediaIndex = 3; 939 player.hls.mediaIndex = 3;
1032 940
1033 // reload the updated playlist 941 // reload the updated playlist
1034 window.manifests['live-updating'] = 942 player.hls.playlists.media = function() {
1035 '#EXTM3U\n' + 943 return {
1036 '#EXTINF:10,\n' + 944 segments: [{
1037 '1.ts\n' + 945 uri: '1.ts'
1038 '#EXTINF:10,\n' + 946 }, {
1039 '2.ts\n' + 947 uri: '2.ts'
1040 '#EXTINF:10,\n' + 948 }, {
1041 '3.ts\n'; 949 uri: '3.ts'
1042 callback(); 950 }]
1043 standardXHRResponse(requests[2]); 951 };
952 };
953 player.hls.playlists.trigger('loadedplaylist');
1044 954
1045 strictEqual(player.hls.mediaIndex, 2, 'mediaIndex is updated after the reload'); 955 strictEqual(player.hls.mediaIndex, 2, 'mediaIndex is updated after the reload');
1046 }); 956 });
...@@ -1112,64 +1022,6 @@ test('does not reload master playlists', function() { ...@@ -1112,64 +1022,6 @@ test('does not reload master playlists', function() {
1112 strictEqual(callbacks.length, 0, 'no reload scheduled'); 1022 strictEqual(callbacks.length, 0, 'no reload scheduled');
1113 }); 1023 });
1114 1024
1115 test('only reloads the active media playlist', function() {
1116 var callbacks = [],
1117 i = 0,
1118 filteredRequests = [],
1119 customResponse;
1120
1121 customResponse = function(request) {
1122 request.response = new Uint8Array([1]).buffer;
1123 request.respond(200,
1124 {'Content-Type': 'application/vnd.apple.mpegurl'},
1125 '#EXTM3U\n' +
1126 '#EXT-X-MEDIA-SEQUENCE:1\n' +
1127 '#EXTINF:10,\n' +
1128 '1.ts\n');
1129 };
1130
1131 window.setTimeout = function(callback) {
1132 callbacks.push(callback);
1133 };
1134
1135 player.hls('http://example.com/missingEndlist.m3u8');
1136 videojs.mediaSources[player.currentSrc()].trigger({
1137 type: 'sourceopen'
1138 });
1139
1140 standardXHRResponse(requests[0]);
1141 standardXHRResponse(requests[1]);
1142
1143 videojs.mediaSources[player.currentSrc()].endOfStream = function() {};
1144
1145 player.hls.selectPlaylist = function() {
1146 return player.hls.master.playlists[1];
1147 };
1148 player.hls.master.playlists.push({
1149 uri: 'http://example.com/switched.m3u8'
1150 });
1151
1152 player.trigger('timeupdate');
1153 strictEqual(callbacks.length, 1, 'a refresh is scheduled');
1154
1155 standardXHRResponse(requests[2]); // segment response
1156 customResponse(requests[3]); // loaded witched.m3u8
1157
1158 callbacks.shift()(); // out-of-date refresh of missingEndlist.m3u8
1159 callbacks.shift()(); // refresh switched.m3u8
1160
1161 for (; i < requests.length; i++) {
1162 if (/switched/.test(requests[i].url)) {
1163 filteredRequests.push(requests[i]);
1164 }
1165 }
1166 strictEqual(filteredRequests.length, 2, 'one refresh was made');
1167 strictEqual(filteredRequests[1].url,
1168 'http://example.com/switched.m3u8',
1169 'refreshed the active playlist');
1170
1171 });
1172
1173 test('if withCredentials option is used, withCredentials is set on the XHR object', function() { 1025 test('if withCredentials option is used, withCredentials is set on the XHR object', function() {
1174 player.hls({ 1026 player.hls({
1175 url: 'http://example.com/media.m3u8', 1027 url: 'http://example.com/media.m3u8',
...@@ -1182,20 +1034,15 @@ test('if withCredentials option is used, withCredentials is set on the XHR objec ...@@ -1182,20 +1034,15 @@ test('if withCredentials option is used, withCredentials is set on the XHR objec
1182 }); 1034 });
1183 1035
1184 test('does not break if the playlist has no segments', function() { 1036 test('does not break if the playlist has no segments', function() {
1185 var customResponse = function(request) {
1186 request.response = new Uint8Array([1]).buffer;
1187 request.respond(200,
1188 {'Content-Type': 'application/vnd.apple.mpegurl'},
1189 '#EXTM3U\n' +
1190 '#EXT-X-PLAYLIST-TYPE:VOD\n' +
1191 '#EXT-X-TARGETDURATION:10\n');
1192 };
1193 player.hls('manifest/master.m3u8'); 1037 player.hls('manifest/master.m3u8');
1194 try { 1038 try {
1195 videojs.mediaSources[player.currentSrc()].trigger({ 1039 videojs.mediaSources[player.currentSrc()].trigger({
1196 type: 'sourceopen' 1040 type: 'sourceopen'
1197 }); 1041 });
1198 customResponse(requests[0]); 1042 requests[0].respond(200, null,
1043 '#EXTM3U\n' +
1044 '#EXT-X-PLAYLIST-TYPE:VOD\n' +
1045 '#EXT-X-TARGETDURATION:10\n');
1199 } catch(e) { 1046 } catch(e) {
1200 ok(false, 'an error was thrown'); 1047 ok(false, 'an error was thrown');
1201 throw e; 1048 throw e;
......