76b977c7 by Gary Katsevman

Merge branch 'master' into saucytravis3_thefinaldimension

Conflicts:
	package.json
2 parents 56795555 3412f5fc
1 # Live HLS Research
2 This document is a collection of notes on Live HLS implementations in the wild.
3
4 There are two varieties of Live HLS. In the first, playlists are
5 persistent and strictly appended to. In the alternative form, the
6 maximum number of segments in a playlist is relatively stable and an
7 old segment is removed every time a new segment becomes available.
8
9 On iOS devices, both stream types report a duration of `Infinity`. The
10 `currentTime` is equal to the amount of the stream that has been
11 played back on the device.
12
13 ## Akamai HD2
14
15 ## OnceLIVE
16 "Sliding window" live streams.
17
18 ### Variant Playlists
19 Once variant playlists look like standard HLS variant playlists.
20
21 ### Media Playlists
22 OnceLIVE uses "sliding window" manifests for live playback. The media
23 playlists do not have an `EXT-X-ENDLIST` and don't declare a
24 `EXT-X-PLAYLIST-TYPE`. On first request, the stream media playlist
25 returned four segment URLs with a starting media sequence of one,
26 preceded by a `EXT-X-DISCONTINUITY` tag. As playback progressed, that
27 number grew to 13 segment URLs, at which point it stabilized. That
28 would equate to a steady-state 65 second window at 5 seconds per
29 segment.
30
31 OnceLive documentation is [available on the Unicorn Media
32 website](http://www.unicornmedia.com/documents/2013/02/oncelive_implementationguide.pdf).
33
34 Here's a script to quickly parse out segment URLs:
35
36 ```shell
37 curl $ONCE_MEDIA_PLAYLIST | grep '^http'
38 ```
39
40 An example media playlist might look something like this:
41 ```m3u8
42 #EXTM3U
43 #EXT-X-TARGETDURATION:5
44 #EXT-X-MEDIA-SEQUENCE:3
45 #EXTINF:5,3
46 http://example.com/0/1/content.ts?visitguid=uuid&asseturl=http://once.example.com/asset.lrm&failoverurl=http://example.com/blank.jpg
47 #EXTINF:5,4
48 http://example.com/1/2/content.ts?visitguid=uuid&asseturl=http://once.example.com/asset.lrm&failoverurl=http://example.com/blank.jpg
49 #EXTINF:5,5
50 http://example.com/2/3/content.ts?visitguid=uuid&asseturl=http://once.example.com/asset.lrm&failoverurl=http://example.com/blank.jpg
51 #EXTINF:5,6
52 http://example.com/3/4/content.ts?visitguid=uuid&asseturl=http://once.example.com/asset.lrm&failoverurl=http://example.com/blank.jpg
53 ```
54
55 ## Zencoder Live
...@@ -61,7 +61,7 @@ ...@@ -61,7 +61,7 @@
61 type="application/x-mpegURL"> 61 type="application/x-mpegURL">
62 </video> 62 </video>
63 <script> 63 <script>
64 videojs.options.flash.swf = 'node_modules/videojs-swf/dist/video-js.swf'; 64 videojs.options.flash.swf = 'node_modules/video.js/dist/video-js/video-js.swf';
65 // initialize the player 65 // initialize the player
66 var player = videojs('video'); 66 var player = videojs('video');
67 67
......
1 { 1 {
2 "name": "videojs-contrib-hls", 2 "name": "videojs-contrib-hls",
3 "version": "0.3.1", 3 "version": "0.3.2",
4 "engines": { 4 "engines": {
5 "node": ">= 0.10.12" 5 "node": ">= 0.10.12"
6 }, 6 },
...@@ -34,10 +34,12 @@ ...@@ -34,10 +34,12 @@
34 "karma-phantomjs-launcher": "~0.1.1", 34 "karma-phantomjs-launcher": "~0.1.1",
35 "karma-safari-launcher": "~0.1.1", 35 "karma-safari-launcher": "~0.1.1",
36 "karma-qunit": "~0.1.1" 36 "karma-qunit": "~0.1.1"
37 "video.js": "^4.5"
38 },
39 "peerDependencies": {
40 "video.js": "^4.5"
37 }, 41 },
38 "dependencies": { 42 "dependencies": {
39 "video.js": "git+https://github.com/videojs/video.js.git#v4.4.1",
40 "videojs-swf": "git+https://github.com/videojs/video-js-swf.git#v4.4.0",
41 "videojs-contrib-media-sources": "git+https://github.com/videojs/videojs-contrib-media-sources.git" 43 "videojs-contrib-media-sources": "git+https://github.com/videojs/videojs-contrib-media-sources.git"
42 } 44 }
43 } 45 }
......
...@@ -344,13 +344,10 @@ ...@@ -344,13 +344,10 @@
344 byterange.offset = entry.offset; 344 byterange.offset = entry.offset;
345 } 345 }
346 }, 346 },
347 'endlist': function() {
348 this.manifest.endList = true;
349 },
347 'inf': function() { 350 'inf': function() {
348 if (!this.manifest.playlistType) {
349 this.manifest.playlistType = 'VOD';
350 this.trigger('info', {
351 message: 'defaulting playlist type to VOD'
352 });
353 }
354 if (!('mediaSequence' in this.manifest)) { 351 if (!('mediaSequence' in this.manifest)) {
355 this.manifest.mediaSequence = 0; 352 this.manifest.mediaSequence = 0;
356 this.trigger('info', { 353 this.trigger('info', {
......
...@@ -95,6 +95,44 @@ var ...@@ -95,6 +95,44 @@ var
95 }, 95 },
96 96
97 /** 97 /**
98 * Creates and sends an XMLHttpRequest.
99 * @param options {string | object} if this argument is a string, it
100 * is intrepreted as a URL and a simple GET request is
101 * inititated. If it is an object, it should contain a `url`
102 * property that indicates the URL to request and optionally a
103 * `method` which is the type of HTTP request to send.
104 * @return {object} the XMLHttpRequest that was initiated.
105 */
106 xhr = function(url, callback) {
107 var
108 options = {
109 method: 'GET'
110 },
111 request;
112 if (typeof url === 'object') {
113 options = videojs.util.mergeOptions(options, url);
114 url = options.url;
115 }
116 request = new window.XMLHttpRequest();
117 request.open(options.method, url);
118 request.onreadystatechange = function() {
119 // wait until the request completes
120 if (this.readyState !== 4) {
121 return;
122 }
123
124 // request error
125 if (this.status >= 400 || this.status === 0) {
126 return callback.call(this, true, url);
127 }
128
129 return callback.call(this, false, url);
130 };
131 request.send(null);
132 return request;
133 },
134
135 /**
98 * TODO - Document this great feature. 136 * TODO - Document this great feature.
99 * 137 *
100 * @param playlist 138 * @param playlist
...@@ -123,6 +161,42 @@ var ...@@ -123,6 +161,42 @@ var
123 }, 161 },
124 162
125 /** 163 /**
164 * Determine the media index in one playlist that corresponds to a
165 * specified media index in another. This function can be used to
166 * calculate a new segment position when a playlist is reloaded or a
167 * variant playlist is becoming active.
168 * @param mediaIndex {number} the index into the original playlist
169 * to translate
170 * @param original {object} the playlist to translate the media
171 * index from
172 * @param update {object} the playlist to translate the media index
173 * to
174 * @param {number} the corresponding media index in the updated
175 * playlist
176 */
177 translateMediaIndex = function(mediaIndex, original, update) {
178 var
179 i = update.segments.length,
180 originalSegment;
181
182 // no segments have been loaded from the original playlist
183 if (mediaIndex === 0) {
184 return 0;
185 }
186
187 // try to sync based on URI
188 originalSegment = original.segments[mediaIndex - 1];
189 while (i--) {
190 if (originalSegment.uri === update.segments[i].uri) {
191 return i + 1;
192 }
193 }
194
195 // sync on media sequence
196 return (original.mediaSequence + mediaIndex) - update.mediaSequence;
197 },
198
199 /**
126 * Calculate the total duration for a playlist based on segment metadata. 200 * Calculate the total duration for a playlist based on segment metadata.
127 * @param playlist {object} a media playlist object 201 * @param playlist {object} a media playlist object
128 * @return {number} the currently known duration, in seconds 202 * @return {number} the currently known duration, in seconds
...@@ -130,8 +204,24 @@ var ...@@ -130,8 +204,24 @@ var
130 totalDuration = function(playlist) { 204 totalDuration = function(playlist) {
131 var 205 var
132 duration = 0, 206 duration = 0,
133 i = playlist.segments.length, 207 i,
134 segment; 208 segment;
209
210 if (!playlist.segments) {
211 return 0;
212 }
213 i = playlist.segments.length;
214
215 // if present, use the duration specified in the playlist
216 if (playlist.totalDuration) {
217 return playlist.totalDuration;
218 }
219
220 // duration should be Infinity for live playlists
221 if (!playlist.endList) {
222 return window.Infinity;
223 }
224
135 while (i--) { 225 while (i--) {
136 segment = playlist.segments[i]; 226 segment = playlist.segments[i];
137 duration += segment.duration || playlist.targetDuration || 0; 227 duration += segment.duration || playlist.targetDuration || 0;
...@@ -198,9 +288,11 @@ var ...@@ -198,9 +288,11 @@ var
198 }), 288 }),
199 srcUrl, 289 srcUrl,
200 290
291 playlistXhr,
201 segmentXhr, 292 segmentXhr,
202 downloadPlaylist, 293 loadedPlaylist,
203 fillBuffer; 294 fillBuffer,
295 updateCurrentPlaylist;
204 296
205 // if the video element supports HLS natively, do nothing 297 // if the video element supports HLS natively, do nothing
206 if (videojs.hls.supportsNativeHls) { 298 if (videojs.hls.supportsNativeHls) {
...@@ -288,6 +380,36 @@ var ...@@ -288,6 +380,36 @@ var
288 fillBuffer(currentTime * 1000); 380 fillBuffer(currentTime * 1000);
289 }); 381 });
290 382
383 /**
384 * Determine whether the current media playlist should be changed
385 * and trigger a switch if necessary. If a sufficiently fresh
386 * version of the target playlist is available, the switch will take
387 * effect immediately. Otherwise, the target playlist will be
388 * refreshed.
389 */
390 updateCurrentPlaylist = function() {
391 var playlist, mediaSequence;
392 playlist = player.hls.selectPlaylist();
393 mediaSequence = player.hls.mediaIndex + (player.hls.media.mediaSequence || 0);
394 if (!playlist.segments ||
395 mediaSequence < (playlist.mediaSequence || 0) ||
396 mediaSequence > (playlist.mediaSequence || 0) + playlist.segments.length) {
397
398 if (playlistXhr) {
399 playlistXhr.abort();
400 }
401 playlistXhr = xhr(resolveUrl(srcUrl, playlist.uri), loadedPlaylist);
402 } else {
403 player.hls.mediaIndex =
404 translateMediaIndex(player.hls.mediaIndex,
405 player.hls.media,
406 playlist);
407 player.hls.media = playlist;
408
409 // update the duration
410 player.duration(totalDuration(player.hls.media));
411 }
412 };
291 413
292 /** 414 /**
293 * Chooses the appropriate media playlist based on the current 415 * Chooses the appropriate media playlist based on the current
...@@ -341,8 +463,10 @@ var ...@@ -341,8 +463,10 @@ var
341 variant = bandwidthPlaylists[i]; 463 variant = bandwidthPlaylists[i];
342 464
343 // ignore playlists without resolution information 465 // ignore playlists without resolution information
344 if (!variant.attributes || !variant.attributes.RESOLUTION || 466 if (!variant.attributes ||
345 !variant.attributes.RESOLUTION.width || !variant.attributes.RESOLUTION.height) { 467 !variant.attributes.RESOLUTION ||
468 !variant.attributes.RESOLUTION.width ||
469 !variant.attributes.RESOLUTION.height) {
346 continue; 470 continue;
347 } 471 }
348 472
...@@ -350,7 +474,7 @@ var ...@@ -350,7 +474,7 @@ var
350 // dimensions less than or equal to the player size is the 474 // dimensions less than or equal to the player size is the
351 // best 475 // best
352 if (variant.attributes.RESOLUTION.width <= player.width() && 476 if (variant.attributes.RESOLUTION.width <= player.width() &&
353 variant.attributes.RESOLUTION.height <= player.height()) { 477 variant.attributes.RESOLUTION.height <= player.height()) {
354 resolutionBestVariant = variant; 478 resolutionBestVariant = variant;
355 break; 479 break;
356 } 480 }
...@@ -361,104 +485,94 @@ var ...@@ -361,104 +485,94 @@ var
361 }; 485 };
362 486
363 /** 487 /**
364 * Download an M3U8 and update the current manifest object. If the provided 488 * Callback that is invoked when a media playlist finishes
365 * URL is a master playlist, the default variant will be downloaded and 489 * downloading. Triggers `loadedmanifest` once for each playlist
366 * parsed as well. Triggers `loadedmanifest` once for each playlist that is 490 * that is downloaded and `loadedmetadata` after at least one
367 * downloaded and `loadedmetadata` after at least one media playlist has 491 * media playlist has been parsed.
368 * been parsed. Whether multiple playlists were downloaded or not, when
369 * `loadedmetadata` fires a parsed or inferred master playlist object will
370 * be available as `player.hls.master`.
371 * 492 *
493 * @param error {*} truthy if the request was not successful
372 * @param url {string} a URL to the M3U8 file to process 494 * @param url {string} a URL to the M3U8 file to process
373 */ 495 */
374 downloadPlaylist = function(url) { 496 loadedPlaylist = function(error, url) {
375 var xhr = new window.XMLHttpRequest(); 497 var i, parser, playlist, playlistUri, refreshDelay;
376 xhr.open('GET', url); 498
377 xhr.onreadystatechange = function() { 499 // clear the current playlist XHR
378 var i, parser, playlist, playlistUri; 500 playlistXhr = null;
379 501
380 if (xhr.readyState === 4) { 502 if (error) {
381 if (xhr.status >= 400 || this.status === 0) { 503 player.hls.error = {
382 player.hls.error = { 504 status: this.status,
383 status: xhr.status, 505 message: 'HLS playlist request error at URL: ' + url,
384 message: 'HLS playlist request error at URL: ' + url, 506 code: (this.status >= 500) ? 4 : 2
385 code: (xhr.status >= 500) ? 4 : 2 507 };
386 }; 508 return player.trigger('error');
387 player.trigger('error'); 509 }
388 return; 510
511 parser = new videojs.m3u8.Parser();
512 parser.push(this.responseText);
513
514 // merge this playlist into the master
515 i = player.hls.master.playlists.length;
516 refreshDelay = (parser.manifest.targetDuration || 10) * 1000;
517 while (i--) {
518 playlist = player.hls.master.playlists[i];
519 playlistUri = resolveUrl(srcUrl, playlist.uri);
520 if (playlistUri === url) {
521 // if the playlist is unchanged since the last reload,
522 // try again after half the target duration
523 // http://tools.ietf.org/html/draft-pantos-http-live-streaming-12#section-6.3.4
524 if (playlist.segments &&
525 playlist.segments.length === parser.manifest.segments.length) {
526 refreshDelay /= 2;
389 } 527 }
390 528
391 // readystate DONE 529 player.hls.master.playlists[i] =
392 parser = new videojs.m3u8.Parser(); 530 videojs.util.mergeOptions(playlist, parser.manifest);
393 parser.push(xhr.responseText);
394 531
395 // master playlists 532 if (playlist !== player.hls.media) {
396 if (parser.manifest.playlists) { 533 continue;
397 player.hls.master = parser.manifest;
398 downloadPlaylist(resolveUrl(url, parser.manifest.playlists[0].uri));
399 player.trigger('loadedmanifest');
400 return;
401 } 534 }
402 535
403 // media playlists 536 // determine the new mediaIndex if we're updating the
404 if (player.hls.master) { 537 // current media playlist
405 // merge this playlist into the master 538 player.hls.mediaIndex =
406 i = player.hls.master.playlists.length; 539 translateMediaIndex(player.hls.mediaIndex,
407 540 playlist,
408 while (i--) { 541 parser.manifest);
409 playlist = player.hls.master.playlists[i]; 542 player.hls.media = parser.manifest;
410 playlistUri = resolveUrl(srcUrl, playlist.uri); 543 }
411 if (playlistUri === url) { 544 }
412 player.hls.master.playlists[i] = 545
413 videojs.util.mergeOptions(playlist, parser.manifest); 546 // check the playlist for updates if EXT-X-ENDLIST isn't present
414 } 547 if (!parser.manifest.endList) {
415 } 548 window.setTimeout(function() {
416 } else { 549 if (!playlistXhr &&
417 // infer a master playlist if none was previously requested 550 resolveUrl(srcUrl, player.hls.media.uri) === url) {
418 player.hls.master = { 551 playlistXhr = xhr(url, loadedPlaylist);
419 playlists: [parser.manifest]
420 };
421 } 552 }
553 }, refreshDelay);
554 }
422 555
423 // always start playback with the default rendition 556 // always start playback with the default rendition
424 if (!player.hls.media) { 557 if (!player.hls.media) {
425 player.hls.media = player.hls.master.playlists[0]; 558 player.hls.media = player.hls.master.playlists[0];
426 559
427 // update the duration 560 // update the duration
428 if (parser.manifest.totalDuration) { 561 player.duration(totalDuration(parser.manifest));
429 player.duration(parser.manifest.totalDuration);
430 } else {
431 player.duration(totalDuration(parser.manifest));
432 }
433 562
434 // periodicaly check if the buffer needs to be refilled 563 // periodicaly check if the buffer needs to be refilled
435 player.on('timeupdate', fillBuffer); 564 player.on('timeupdate', fillBuffer);
436 565
437 player.trigger('loadedmanifest'); 566 player.trigger('loadedmanifest');
438 player.trigger('loadedmetadata'); 567 player.trigger('loadedmetadata');
439 fillBuffer(); 568 fillBuffer();
440 return; 569 return;
441 } 570 }
442 571
443 // select a playlist and download its metadata if necessary 572 // select a playlist and download its metadata if necessary
444 playlist = player.hls.selectPlaylist(); 573 updateCurrentPlaylist();
445 if (!playlist.segments) {
446 downloadPlaylist(resolveUrl(srcUrl, playlist.uri));
447 } else {
448 player.hls.media = playlist;
449
450 // update the duration
451 if (player.hls.media.totalDuration) {
452 player.duration(player.hls.media.totalDuration);
453 } else {
454 player.duration(totalDuration(player.hls.media));
455 }
456 }
457 574
458 player.trigger('loadedmanifest'); 575 player.trigger('loadedmanifest');
459 }
460 };
461 xhr.send(null);
462 }; 576 };
463 577
464 /** 578 /**
...@@ -495,16 +609,19 @@ var ...@@ -495,16 +609,19 @@ var
495 return; 609 return;
496 } 610 }
497 611
498 segmentUri = resolveUrl(resolveUrl(srcUrl, player.hls.media.uri || ''), 612 // resolve the segment URL relative to the playlist
499 segment.uri); 613 if (player.hls.media.uri === srcUrl) {
614 segmentUri = resolveUrl(srcUrl, segment.uri);
615 } else {
616 segmentUri = resolveUrl(resolveUrl(srcUrl, player.hls.media.uri || ''),
617 segment.uri);
618 }
500 619
501 // request the next segment 620 // request the next segment
502 segmentXhr = new window.XMLHttpRequest(); 621 segmentXhr = new window.XMLHttpRequest();
503 segmentXhr.open('GET', segmentUri); 622 segmentXhr.open('GET', segmentUri);
504 segmentXhr.responseType = 'arraybuffer'; 623 segmentXhr.responseType = 'arraybuffer';
505 segmentXhr.onreadystatechange = function() { 624 segmentXhr.onreadystatechange = function() {
506 var playlist;
507
508 // wait until the request completes 625 // wait until the request completes
509 if (this.readyState !== 4) { 626 if (this.readyState !== 4) {
510 return; 627 return;
...@@ -559,12 +676,7 @@ var ...@@ -559,12 +676,7 @@ var
559 676
560 // figure out what stream the next segment should be downloaded from 677 // figure out what stream the next segment should be downloaded from
561 // with the updated bandwidth information 678 // with the updated bandwidth information
562 playlist = player.hls.selectPlaylist(); 679 updateCurrentPlaylist();
563 if (!playlist.segments) {
564 downloadPlaylist(resolveUrl(srcUrl, playlist.uri));
565 } else {
566 player.hls.media = playlist;
567 }
568 }; 680 };
569 startTime = +new Date(); 681 startTime = +new Date();
570 segmentXhr.send(null); 682 segmentXhr.send(null);
...@@ -578,7 +690,26 @@ var ...@@ -578,7 +690,26 @@ var
578 sourceBuffer.appendBuffer(segmentParser.getFlvHeader()); 690 sourceBuffer.appendBuffer(segmentParser.getFlvHeader());
579 691
580 player.hls.mediaIndex = 0; 692 player.hls.mediaIndex = 0;
581 downloadPlaylist(srcUrl); 693 xhr(srcUrl, function(error, url) {
694 var uri, parser = new videojs.m3u8.Parser();
695 parser.push(this.responseText);
696
697 // master playlists
698 if (parser.manifest.playlists) {
699 player.hls.master = parser.manifest;
700 playlistXhr = xhr(resolveUrl(url, parser.manifest.playlists[0].uri), loadedPlaylist);
701 return player.trigger('loadedmanifest');
702 } else {
703 // infer a master playlist if a media playlist is loaded directly
704 uri = resolveUrl(window.location.href, url);
705 player.hls.master = {
706 playlists: [{
707 uri: uri
708 }]
709 };
710 loadedPlaylist.call(this, error, uri);
711 }
712 });
582 }); 713 });
583 player.src([{ 714 player.src([{
584 src: videojs.URL.createObjectURL(mediaSource), 715 src: videojs.URL.createObjectURL(mediaSource),
......
1 (function(window, undefined) { 1 (function(window, undefined) {
2 var 2 var
3 //manifestController = this.manifestController, 3 //manifestController = this.manifestController,
4 ParseStream = window.videojs.m3u8.ParseStream, 4 m3u8 = window.videojs.m3u8,
5 ParseStream = m3u8.ParseStream,
5 parseStream, 6 parseStream,
6 LineStream = window.videojs.m3u8.LineStream, 7 LineStream = m3u8.LineStream,
7 lineStream, 8 lineStream,
8 Parser = window.videojs.m3u8.Parser, 9 Parser = m3u8.Parser,
9 parser; 10 parser;
10 11
11 /* 12 /*
...@@ -506,19 +507,15 @@ ...@@ -506,19 +507,15 @@
506 ok(!event, 'no event is triggered'); 507 ok(!event, 'no event is triggered');
507 }); 508 });
508 509
509 module('m3u8 parser', { 510 module('m3u8 parser');
510 setup: function() {
511 parser = new Parser();
512 }
513 });
514 511
515 test('should create a parser', function() { 512 test('can be constructed', function() {
516 notStrictEqual(parser, undefined, 'parser is defined'); 513 notStrictEqual(new Parser(), undefined, 'parser is defined');
517 }); 514 });
518 515
519 module('m3u8s'); 516 module('m3u8s');
520 517
521 test('parses the example manifests as expected', function() { 518 test('parses static manifests as expected', function() {
522 var key; 519 var key;
523 for (key in window.manifests) { 520 for (key in window.manifests) {
524 if (window.expected[key]) { 521 if (window.expected[key]) {
......
...@@ -20,5 +20,6 @@ ...@@ -20,5 +20,6 @@
20 "uri": "http://example.com/00004.ts" 20 "uri": "http://example.com/00004.ts"
21 } 21 }
22 ], 22 ],
23 "targetDuration": 10 23 "targetDuration": 10,
24 "endList": true
24 } 25 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -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 "targetDuration": 10 143 "targetDuration": 10,
144 "endList": true
144 } 145 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -12,5 +12,6 @@ ...@@ -12,5 +12,6 @@
12 "uri": "hls_450k_video.ts" 12 "uri": "hls_450k_video.ts"
13 } 13 }
14 ], 14 ],
15 "targetDuration": 10 15 "targetDuration": 10,
16 "endList": true
16 } 17 }
...\ No newline at end of file ...\ No newline at end of file
......
1 { 1 {
2 "allowCache": true, 2 "allowCache": true,
3 "playlists": [{ 3 "playlists": [
4 "attributes": { 4 {
5 "PROGRAM-ID": 1, 5 "attributes": {
6 "BANDWIDTH": 240000, 6 "PROGRAM-ID": 1,
7 "RESOLUTION": { 7 "BANDWIDTH": 240000,
8 "width": 396, 8 "RESOLUTION": {
9 "height": 224 9 "width": 396,
10 } 10 "height": 224
11 }
12 },
13 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001"
11 }, 14 },
12 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001" 15 {
13 }, { 16 "attributes": {
14 "attributes": { 17 "PROGRAM-ID": 1,
15 "PROGRAM-ID": 1, 18 "BANDWIDTH": 40000
16 "BANDWIDTH": 40000 19 },
20 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001"
17 }, 21 },
18 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001" 22 {
19 }, { 23 "attributes": {
20 "attributes": { 24 "PROGRAM-ID": 1,
21 "PROGRAM-ID": 1, 25 "BANDWIDTH": 440000,
22 "BANDWIDTH": 440000, 26 "RESOLUTION": {
23 "RESOLUTION": { 27 "width": 396,
24 "width": 396, 28 "height": 224
25 "height": 224 29 }
26 } 30 },
31 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001"
27 }, 32 },
28 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001" 33 {
29 }, { 34 "attributes": {
30 "attributes": { 35 "PROGRAM-ID": 1,
31 "PROGRAM-ID": 1, 36 "BANDWIDTH": 1928000,
32 "BANDWIDTH": 1928000, 37 "RESOLUTION": {
33 "RESOLUTION": { 38 "width": 960,
34 "width": 960, 39 "height": 540
35 "height": 540 40 }
36 } 41 },
37 }, 42 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001"
38 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001" 43 }
39 }] 44 ]
40 } 45 }
......
...@@ -136,5 +136,6 @@ ...@@ -136,5 +136,6 @@
136 "uri": "hls_450k_video.ts" 136 "uri": "hls_450k_video.ts"
137 } 137 }
138 ], 138 ],
139 "targetDuration": 10 139 "targetDuration": 10,
140 "endList": true
140 } 141 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -12,5 +12,6 @@ ...@@ -12,5 +12,6 @@
12 "uri": "hls_450k_video.ts" 12 "uri": "hls_450k_video.ts"
13 } 13 }
14 ], 14 ],
15 "targetDuration": 10 15 "targetDuration": 10,
16 "endList": true
16 } 17 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -20,5 +20,6 @@ ...@@ -20,5 +20,6 @@
20 "uri": "/00004.ts" 20 "uri": "/00004.ts"
21 } 21 }
22 ], 22 ],
23 "targetDuration": 10 23 "targetDuration": 10,
24 "endList": true
24 } 25 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -12,5 +12,6 @@ ...@@ -12,5 +12,6 @@
12 "uri": "hls_450k_video.ts" 12 "uri": "hls_450k_video.ts"
13 } 13 }
14 ], 14 ],
15 "targetDuration": 10 15 "targetDuration": 10,
16 "endList": true
16 } 17 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -20,5 +20,6 @@ ...@@ -20,5 +20,6 @@
20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" 20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
21 } 21 }
22 ], 22 ],
23 "targetDuration": 8 23 "targetDuration": 8,
24 "endList": true
24 } 25 }
...\ No newline at end of file ...\ No newline at end of file
......
1 { 1 {
2 "allowCache": true, 2 "allowCache": true,
3 "mediaSequence": 0, 3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [ 4 "segments": [
6 { 5 {
7 "duration": 10, 6 "duration": 10,
...@@ -28,5 +27,6 @@ ...@@ -28,5 +27,6 @@
28 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts" 27 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts"
29 } 28 }
30 ], 29 ],
31 "targetDuration": 10 30 "targetDuration": 10,
31 "endList": true
32 } 32 }
...\ No newline at end of file ...\ No newline at end of file
......
1 { 1 {
2 "allowCache": true, 2 "allowCache": true,
3 "playlists": [{ 3 "playlists": [
4 "attributes": { 4 {
5 "PROGRAM-ID": 1, 5 "attributes": {
6 "BANDWIDTH": 240000, 6 "PROGRAM-ID": 1,
7 "RESOLUTION": { 7 "BANDWIDTH": 240000,
8 "width": 396, 8 "RESOLUTION": {
9 "height": 224 9 "width": 396,
10 } 10 "height": 224
11 }
12 },
13 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001"
11 }, 14 },
12 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001" 15 {
13 }, { 16 "attributes": {
14 "attributes": { 17 "PROGRAM-ID": 1,
15 "PROGRAM-ID": 1, 18 "BANDWIDTH": 40000
16 "BANDWIDTH": 40000 19 },
20 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001"
17 }, 21 },
18 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001" 22 {
19 }, { 23 "attributes": {
20 "attributes": { 24 "PROGRAM-ID": 1,
21 "PROGRAM-ID": 1, 25 "BANDWIDTH": 440000,
22 "BANDWIDTH": 440000, 26 "RESOLUTION": {
23 "RESOLUTION": { 27 "width": 396,
24 "width": 396, 28 "height": 224
25 "height": 224 29 }
26 } 30 },
31 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001"
27 }, 32 },
28 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001" 33 {
29 }, { 34 "attributes": {
30 "attributes": { 35 "PROGRAM-ID": 1,
31 "PROGRAM-ID": 1, 36 "BANDWIDTH": 1928000,
32 "BANDWIDTH": 1928000, 37 "RESOLUTION": {
33 "RESOLUTION": { 38 "width": 960,
34 "width": 960, 39 "height": 540
35 "height": 540 40 }
36 } 41 },
37 }, 42 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001"
38 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001" 43 }
39 }] 44 ]
40 } 45 }
......
...@@ -28,5 +28,6 @@ ...@@ -28,5 +28,6 @@
28 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts" 28 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts"
29 } 29 }
30 ], 30 ],
31 "targetDuration": 10 31 "targetDuration": 10,
32 "endList": true
32 } 33 }
...\ No newline at end of file ...\ No newline at end of file
......
1 { 1 {
2 "allowCache": true, 2 "allowCache": true,
3 "mediaSequence": 1, 3 "mediaSequence": 1,
4 "playlistType": "VOD",
5 "segments": [ 4 "segments": [
6 { 5 {
7 "duration": 6.64, 6 "duration": 6.64,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts" 7 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 } 8 }
10 ], 9 ],
11 "targetDuration": 8 10 "targetDuration": 8,
11 "endList": true
12 } 12 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -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 "targetDuration": 10 143 "targetDuration": 10,
144 } 144 "endList": true
145 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -12,5 +12,6 @@ ...@@ -12,5 +12,6 @@
12 "uri": "hls_450k_video.ts" 12 "uri": "hls_450k_video.ts"
13 } 13 }
14 ], 14 ],
15 "targetDuration": 10 15 "targetDuration": 10,
16 "endList": true
16 } 17 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -20,5 +20,6 @@ ...@@ -20,5 +20,6 @@
20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" 20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
21 } 21 }
22 ], 22 ],
23 "targetDuration": 8 23 "targetDuration": 8,
24 "endList": true
24 } 25 }
...\ No newline at end of file ...\ No newline at end of file
......
1 { 1 {
2 "allowCache": true, 2 "allowCache": true,
3 "mediaSequence": 0, 3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [ 4 "segments": [
6 { 5 {
7 "duration": 10, 6 "duration": 10,
...@@ -28,5 +27,6 @@ ...@@ -28,5 +27,6 @@
28 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts" 27 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts"
29 } 28 }
30 ], 29 ],
31 "targetDuration": 10 30 "targetDuration": 10,
31 "endList": true
32 } 32 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -139,5 +139,6 @@ ...@@ -139,5 +139,6 @@
139 "duration": 1.4167, 139 "duration": 1.4167,
140 "uri": "hls_450k_video.ts" 140 "uri": "hls_450k_video.ts"
141 } 141 }
142 ] 142 ],
143 "endList": true
143 } 144 }
...\ No newline at end of file ...\ No newline at end of file
......
1 { 1 {
2 "allowCache": true, 2 "allowCache": true,
3 "mediaSequence": 0, 3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [ 4 "segments": [
6 { 5 {
7 "duration": 10, 6 "duration": 10,
8 "uri": "/test/ts-files/zencoder/gogo/00001.ts" 7 "uri": "/test/ts-files/zencoder/gogo/00001.ts"
9 } 8 }
10 ] 9 ],
10 "endList": true
11 } 11 }
...\ No newline at end of file ...\ No newline at end of file
......
1 { 1 {
2 "allowCache": true, 2 "allowCache": true,
3 "mediaSequence": 0, 3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [ 4 "segments": [
6 { 5 {
7 "duration": 10, 6 "duration": 10,
...@@ -24,5 +23,6 @@ ...@@ -24,5 +23,6 @@
24 "uri": "/test/ts-files/zencoder/gogo/00005.ts" 23 "uri": "/test/ts-files/zencoder/gogo/00005.ts"
25 } 24 }
26 ], 25 ],
27 "targetDuration": 10 26 "targetDuration": 10,
27 "endList": true
28 } 28 }
...\ No newline at end of file ...\ No newline at end of file
......
1 { 1 {
2 "allowCache": true, 2 "allowCache": true,
3 "mediaSequence": 0, 3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [ 4 "segments": [
6 { 5 {
7 "duration": 10, 6 "duration": 10,
8 "uri": "/test/ts-files/zencoder/gogo/00001.ts" 7 "uri": "/test/ts-files/zencoder/gogo/00001.ts"
9 } 8 }
10 ], 9 ],
11 "targetDuration": 10 10 "targetDuration": 10,
11 "endList": true
12 } 12 }
...\ No newline at end of file ...\ No newline at end of file
......
1 { 1 {
2 "allowCache": true, 2 "allowCache": true,
3 "playlists": [{ 3 "playlists": [
4 "attributes": { 4 {
5 "PROGRAM-ID": 1, 5 "attributes": {
6 "BANDWIDTH": 240000, 6 "PROGRAM-ID": 1,
7 "RESOLUTION": { 7 "BANDWIDTH": 240000,
8 "width": 396, 8 "RESOLUTION": {
9 "height": 224 9 "width": 396,
10 } 10 "height": 224
11 }
12 },
13 "uri": "media.m3u8"
11 }, 14 },
12 "uri": "media.m3u8" 15 {
13 }, { 16 "attributes": {
14 "attributes": { 17 "PROGRAM-ID": 1,
15 "PROGRAM-ID": 1, 18 "BANDWIDTH": 40000
16 "BANDWIDTH": 40000 19 },
20 "uri": "media1.m3u8"
17 }, 21 },
18 "uri": "media1.m3u8" 22 {
19 }, { 23 "attributes": {
20 "attributes": { 24 "PROGRAM-ID": 1,
21 "PROGRAM-ID": 1, 25 "BANDWIDTH": 440000,
22 "BANDWIDTH": 440000, 26 "RESOLUTION": {
23 "RESOLUTION": { 27 "width": 396,
24 "width": 396, 28 "height": 224
25 "height": 224 29 }
26 } 30 },
31 "uri": "media2.m3u8"
27 }, 32 },
28 "uri": "media2.m3u8" 33 {
29 }, { 34 "attributes": {
30 "attributes": { 35 "PROGRAM-ID": 1,
31 "PROGRAM-ID": 1, 36 "BANDWIDTH": 1928000,
32 "BANDWIDTH": 1928000, 37 "RESOLUTION": {
33 "RESOLUTION": { 38 "width": 960,
34 "width": 960, 39 "height": 540
35 "height": 540 40 }
36 } 41 },
37 }, 42 "uri": "media3.m3u8"
38 "uri": "media3.m3u8" 43 }
39 }] 44 ]
40 } 45 }
......
...@@ -20,5 +20,6 @@ ...@@ -20,5 +20,6 @@
20 "uri": "00004.ts" 20 "uri": "00004.ts"
21 } 21 }
22 ], 22 ],
23 "targetDuration": 10 23 "targetDuration": 10,
24 "endList": true
24 } 25 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -20,5 +20,6 @@ ...@@ -20,5 +20,6 @@
20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" 20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
21 } 21 }
22 ], 22 ],
23 "targetDuration": 8 23 "targetDuration": 8,
24 "endList": true
24 } 25 }
...\ No newline at end of file ...\ No newline at end of file
......
1 {
2 "allowCache": true,
3 "mediaSequence": 0,
4 "segments": [
5 {
6 "duration": 10,
7 "uri": "00001.ts"
8 },
9 {
10 "duration": 10,
11 "uri": "00002.ts"
12 }
13 ],
14 "targetDuration": 10
15 }
1 #EXTM3U
2 #EXT-X-TARGETDURATION:10
3 #EXTINF:10,
4 00001.ts
5 #EXTINF:10,
6 00002.ts
...@@ -16,5 +16,6 @@ ...@@ -16,5 +16,6 @@
16 "uri": "hls_450k_video.ts" 16 "uri": "hls_450k_video.ts"
17 } 17 }
18 ], 18 ],
19 "targetDuration": 10 19 "targetDuration": 10,
20 } 20 "endList": true
21 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -20,5 +20,6 @@ ...@@ -20,5 +20,6 @@
20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" 20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
21 } 21 }
22 ], 22 ],
23 "targetDuration": 8 23 "targetDuration": 8,
24 "endList": true
24 } 25 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -20,5 +20,6 @@ ...@@ -20,5 +20,6 @@
20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" 20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
21 } 21 }
22 ], 22 ],
23 "targetDuration": 8 23 "targetDuration": 8,
24 } 24 "endList": true
25 }
...\ No newline at end of file ...\ No newline at end of file
......
1 { 1 {
2 "allowCache": true, 2 "allowCache": true,
3 "mediaSequence": 0, 3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "targetDuration": 10, 4 "targetDuration": 10,
6 "segments": [{ 5 "segments": [
7 "uri": "001.ts" 6 {
8 }, { 7 "uri": "001.ts"
9 "uri": "002.ts", 8 },
10 "duration": 9 9 {
11 }, { 10 "uri": "002.ts",
12 "uri": "003.ts", 11 "duration": 9
13 "duration": 7 12 },
14 }, { 13 {
15 "uri": "004.ts", 14 "uri": "003.ts",
16 "duration": 10 15 "duration": 7
17 }] 16 },
17 {
18 "uri": "004.ts",
19 "duration": 10
20 }
21 ]
18 } 22 }
......
...@@ -20,5 +20,6 @@ ...@@ -20,5 +20,6 @@
20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" 20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
21 } 21 }
22 ], 22 ],
23 "targetDuration": 8 23 "targetDuration": 8,
24 "endList": true
24 } 25 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -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 "targetDuration": 10 143 "targetDuration": 10,
144 "endList": true
144 } 145 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -8,5 +8,6 @@ ...@@ -8,5 +8,6 @@
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts" 8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 } 9 }
10 ], 10 ],
11 "targetDuration": 8 11 "targetDuration": 8,
12 "endList": true
12 } 13 }
...\ No newline at end of file ...\ No newline at end of file
......
1 { 1 {
2 "allowCache": true, 2 "allowCache": true,
3 "playlists": [ 3 "playlists": [
4 { 4 {
5 "attributes": { 5 "attributes": {
6 "PROGRAM-ID": 1 6 "PROGRAM-ID": 1
7 },
8 "uri": "media.m3u8"
7 }, 9 },
8 "uri": "media.m3u8" 10 {
9 }, 11 "uri": "media1.m3u8"
10 { 12 }
11 "uri": "media1.m3u8"
12 }
13 ] 13 ]
14 } 14 }
......
...@@ -20,5 +20,6 @@ ...@@ -20,5 +20,6 @@
20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" 20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
21 } 21 }
22 ], 22 ],
23 "targetDuration": 8 23 "targetDuration": 8,
24 "endList": true
24 } 25 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -8,5 +8,6 @@ ...@@ -8,5 +8,6 @@
8 "uri": "hls_450k_video.ts" 8 "uri": "hls_450k_video.ts"
9 } 9 }
10 ], 10 ],
11 "targetDuration": 10 11 "targetDuration": 10,
12 "endList": true
12 } 13 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -105,6 +105,7 @@ module('HLS', { ...@@ -105,6 +105,7 @@ module('HLS', {
105 this.readyState = 4; 105 this.readyState = 4;
106 this.onreadystatechange(); 106 this.onreadystatechange();
107 }; 107 };
108 this.abort = function() {};
108 }; 109 };
109 xhrUrls = []; 110 xhrUrls = [];
110 }, 111 },
...@@ -170,7 +171,7 @@ test('sets the duration if one is available on the playlist', function() { ...@@ -170,7 +171,7 @@ test('sets the duration if one is available on the playlist', function() {
170 type: 'sourceopen' 171 type: 'sourceopen'
171 }); 172 });
172 173
173 strictEqual(1, calls, 'duration is set'); 174 strictEqual(calls, 2, 'duration is set');
174 }); 175 });
175 176
176 test('calculates the duration if needed', function() { 177 test('calculates the duration if needed', function() {
...@@ -181,13 +182,15 @@ test('calculates the duration if needed', function() { ...@@ -181,13 +182,15 @@ test('calculates the duration if needed', function() {
181 } 182 }
182 durations.push(duration); 183 durations.push(duration);
183 }; 184 };
184 player.hls('manifest/liveMissingSegmentDuration.m3u8'); 185 player.hls('http://example.com/manifest/missingExtinf.m3u8');
185 videojs.mediaSources[player.currentSrc()].trigger({ 186 videojs.mediaSources[player.currentSrc()].trigger({
186 type: 'sourceopen' 187 type: 'sourceopen'
187 }); 188 });
188 189
189 strictEqual(durations.length, 1, 'duration is set'); 190 strictEqual(durations.length, 2, 'duration is set');
190 strictEqual(durations[0], 6.64 + (2 * 8), 'duration is calculated'); 191 strictEqual(durations[0],
192 player.hls.media.segments.length * 10,
193 'duration is calculated');
191 }); 194 });
192 195
193 test('starts downloading a segment on loadedmetadata', function() { 196 test('starts downloading a segment on loadedmetadata', function() {
...@@ -400,15 +403,12 @@ test('downloads additional playlists if required', function() { ...@@ -400,15 +403,12 @@ test('downloads additional playlists if required', function() {
400 called = true; 403 called = true;
401 return playlist; 404 return playlist;
402 } 405 }
403 playlist.segments = []; 406 playlist.segments = [1, 1, 1];
404 return playlist; 407 return playlist;
405 }; 408 };
406 xhrUrls = []; 409 xhrUrls = [];
407 410
408 // the playlist selection is revisited after a new segment is downloaded 411 // the playlist selection is revisited after a new segment is downloaded
409 player.currentTime = function() {
410 return 1;
411 };
412 player.trigger('timeupdate'); 412 player.trigger('timeupdate');
413 413
414 strictEqual(2, xhrUrls.length, 'requests were made'); 414 strictEqual(2, xhrUrls.length, 'requests were made');
...@@ -867,10 +867,246 @@ test('segment 500 should trigger MEDIA_ERR_ABORTED', function () { ...@@ -867,10 +867,246 @@ test('segment 500 should trigger MEDIA_ERR_ABORTED', function () {
867 867
868 test('has no effect if native HLS is available', function() { 868 test('has no effect if native HLS is available', function() {
869 videojs.hls.supportsNativeHls = true; 869 videojs.hls.supportsNativeHls = true;
870 player.hls('manifest/master.m3u8'); 870 player.hls('http://example.com/manifest/master.m3u8');
871 871
872 ok(!(player.currentSrc() in videojs.mediaSources), 872 ok(!(player.currentSrc() in videojs.mediaSources),
873 'no media source was opened'); 873 'no media source was opened');
874 }); 874 });
875 875
876 test('reloads live playlists', function() {
877 var callbacks = [];
878 // capture timeouts
879 window.setTimeout = function(callback, timeout) {
880 callbacks.push({ callback: callback, timeout: timeout });
881 };
882 player.hls('http://example.com/manifest/missingEndlist.m3u8');
883 videojs.mediaSources[player.currentSrc()].trigger({
884 type: 'sourceopen'
885 });
886
887 strictEqual(1, callbacks.length, 'refresh was scheduled');
888 strictEqual(player.hls.media.targetDuration * 1000,
889 callbacks[0].timeout,
890 'waited one target duration');
891 });
892
893 test('duration is Infinity for live playlists', function() {
894 player.hls('http://example.com/manifest/missingEndlist.m3u8');
895 videojs.mediaSources[player.currentSrc()].trigger({
896 type: 'sourceopen'
897 });
898
899 strictEqual(Infinity, player.duration(), 'duration is infinity');
900 });
901
902 test('does not reload playlists with an endlist tag', function() {
903 var callbacks = [];
904 // capture timeouts
905 window.setTimeout = function(callback, timeout) {
906 callbacks.push({ callback: callback, timeout: timeout });
907 };
908 player.hls('manifest/media.m3u8');
909 videojs.mediaSources[player.currentSrc()].trigger({
910 type: 'sourceopen'
911 });
912
913 strictEqual(0, callbacks.length, 'no refresh was scheduled');
914 });
915
916 test('reloads a live playlist after half a target duration if it has not ' +
917 'changed since the last request', function() {
918 var callbacks = [];
919 // capture timeouts
920 window.setTimeout = function(callback, timeout) {
921 callbacks.push({ callback: callback, timeout: timeout });
922 };
923 player.hls('http://example.com/manifest/missingEndlist.m3u8');
924 videojs.mediaSources[player.currentSrc()].trigger({
925 type: 'sourceopen'
926 });
927
928 strictEqual(callbacks.length, 1, 'full-length refresh scheduled');
929 callbacks.pop().callback();
930
931 strictEqual(1, callbacks.length, 'half-length refresh was scheduled');
932 strictEqual(callbacks[0].timeout,
933 player.hls.media.targetDuration / 2 * 1000,
934 'waited half a target duration');
935 });
936
937 test('merges playlist reloads', function() {
938 var
939 oldPlaylist,
940 callback;
941 // capture timeouts
942 window.setTimeout = function(cb) {
943 callback = cb;
944 };
945
946 player.hls('http://example.com/manifest/missingEndlist.m3u8');
947 videojs.mediaSources[player.currentSrc()].trigger({
948 type: 'sourceopen'
949 });
950 oldPlaylist = player.hls.media;
951
952 callback();
953 ok(oldPlaylist !== player.hls.media, 'player.hls.media was updated');
954 });
955
956 test('updates the media index when a playlist reloads', function() {
957 var callback;
958 window.setTimeout = function(cb) {
959 callback = cb;
960 };
961 // the initial playlist
962 window.manifests['live-updating'] =
963 '#EXTM3U\n' +
964 '#EXTINF:10,\n' +
965 '0.ts\n' +
966 '#EXTINF:10,\n' +
967 '1.ts\n' +
968 '#EXTINF:10,\n' +
969 '2.ts\n';
970
971 player.hls('http://example.com/live-updating.m3u8');
972 videojs.mediaSources[player.currentSrc()].trigger({
973 type: 'sourceopen'
974 });
975
976 // play the stream until 2.ts is playing
977 player.hls.mediaIndex = 3;
978
979 // reload the updated playlist
980 window.manifests['live-updating'] =
981 '#EXTM3U\n' +
982 '#EXTINF:10,\n' +
983 '1.ts\n' +
984 '#EXTINF:10,\n' +
985 '2.ts\n' +
986 '#EXTINF:10,\n' +
987 '3.ts\n';
988 callback();
989
990 strictEqual(player.hls.mediaIndex, 2, 'mediaIndex is updated after the reload');
991 });
992
993 test('mediaIndex is zero before the first segment loads', function() {
994 window.manifests['first-seg-load'] =
995 '#EXTM3U\n' +
996 '#EXTINF:10,\n' +
997 '0.ts\n';
998 window.XMLHttpRequest = function() {
999 this.open = function() {};
1000 this.send = function() {};
1001 };
1002 player.hls('http://example.com/first-seg-load.m3u8');
1003 videojs.mediaSources[player.currentSrc()].trigger({
1004 type: 'sourceopen'
1005 });
1006
1007 strictEqual(player.hls.mediaIndex, 0, 'mediaIndex is zero');
1008 });
1009
1010 test('reloads out-of-date live playlists when switching variants', function() {
1011 player.hls('http://example.com/master.m3u8');
1012 videojs.mediaSources[player.currentSrc()].trigger({
1013 type: 'sourceopen'
1014 });
1015
1016 player.hls.master = {
1017 playlists: [{
1018 mediaSequence: 15,
1019 segments: [1, 1, 1]
1020 }, {
1021 uri: 'http://example.com/variant-update.m3u8',
1022 mediaSequence: 0,
1023 segments: [1, 1]
1024 }]
1025 };
1026 // playing segment 15 on playlist zero
1027 player.hls.media = player.hls.master.playlists[0];
1028 player.mediaIndex = 1;
1029 window.manifests['variant-update'] = '#EXTM3U\n' +
1030 '#EXT-X-MEDIA-SEQUENCE:16\n' +
1031 '#EXTINF:10,\n' +
1032 '16.ts\n' +
1033 '#EXTINF:10,\n' +
1034 '17.ts\n';
1035
1036 // switch playlists
1037 player.hls.selectPlaylist = function() {
1038 return player.hls.master.playlists[1];
1039 };
1040 // timeupdate downloads segment 16 then switches playlists
1041 player.trigger('timeupdate');
1042
1043 strictEqual(player.mediaIndex, 1, 'mediaIndex points at the next segment');
1044 });
1045
1046 test('does not reload master playlists', function() {
1047 var callbacks = [];
1048 window.setTimeout = function(callback) {
1049 callbacks.push(callback);
1050 };
1051
1052 player.hls('http://example.com/master.m3u8');
1053 videojs.mediaSources[player.currentSrc()].trigger({
1054 type: 'sourceopen'
1055 });
1056
1057 strictEqual(callbacks.length, 0, 'no reload scheduled');
1058 });
1059
1060 test('only reloads the active media playlist', function() {
1061 var callbacks = [], urls = [], responses = [];
1062 window.setTimeout = function(callback) {
1063 callbacks.push(callback);
1064 };
1065
1066 player.hls('http://example.com/missingEndlist.m3u8');
1067 videojs.mediaSources[player.currentSrc()].trigger({
1068 type: 'sourceopen'
1069 });
1070
1071 window.XMLHttpRequest = function() {
1072 this.open = function(method, url) {
1073 urls.push(url);
1074 };
1075 this.send = function() {
1076 var xhr = this;
1077 responses.push(function() {
1078 xhr.readyState = 4;
1079 xhr.responseText = '#EXTM3U\n' +
1080 '#EXT-X-MEDIA-SEQUENCE:1\n' +
1081 '#EXTINF:10,\n' +
1082 '1.ts\n';
1083 xhr.response = new Uint8Array([1]).buffer;
1084 xhr.onreadystatechange();
1085 });
1086 };
1087 };
1088 player.hls.selectPlaylist = function() {
1089 return player.hls.master.playlists[1];
1090 };
1091 player.hls.master.playlists.push({
1092 uri: 'http://example.com/switched.m3u8'
1093 });
1094
1095 player.trigger('timeupdate');
1096 strictEqual(callbacks.length, 1, 'a refresh is scheduled');
1097 strictEqual(responses.length, 1, 'segment requested');
1098
1099 responses.shift()(); // segment response
1100 responses.shift()(); // loaded switched.m3u8
1101
1102 urls = [];
1103 callbacks.shift()(); // out-of-date refresh of missingEndlist.m3u8
1104 callbacks.shift()(); // refresh switched.m3u8
1105
1106 strictEqual(urls.length, 1, 'one refresh was made');
1107 strictEqual(urls[0],
1108 'http://example.com/switched.m3u8',
1109 'refreshed the active playlist');
1110 });
1111
876 })(window, window.videojs); 1112 })(window, window.videojs);
......