abfea271 by Gary Katsevman

Merge branch 'master' into saucelabs-take2

Conflicts:
	.travis.yml
2 parents 2279f842 58952618
1 .DS_Store
2 dist/*
1 /node_modules/ 3 /node_modules/
2 *~ 4 *~
3 *.iml 5 *.iml
......
1 dist/*
2 *~
3 *.iml
4 *.swp
5 tmp/**
6 test/**
...\ No newline at end of file ...\ No newline at end of file
...@@ -9,7 +9,10 @@ notifications: ...@@ -9,7 +9,10 @@ notifications:
9 hipchat: 9 hipchat:
10 rooms: 10 rooms:
11 secure: l5TTd5JuPAW883PtcyaIBcJI9Chr9JpsZPQAEUBKAgIEwzuS6y7t5arlkS1PwH6gi1FADzYDf+OXSIou4GkTSrIetnBcT/SAgF0gBKgIhj+eRkuCfZ4VaC7BPhfZ0hgYRE+5Ejf5BM2MJafRm0pj7OlqG4xKrQZwtuV1te5r3JY= 11 secure: l5TTd5JuPAW883PtcyaIBcJI9Chr9JpsZPQAEUBKAgIEwzuS6y7t5arlkS1PwH6gi1FADzYDf+OXSIou4GkTSrIetnBcT/SAgF0gBKgIhj+eRkuCfZ4VaC7BPhfZ0hgYRE+5Ejf5BM2MJafRm0pj7OlqG4xKrQZwtuV1te5r3JY=
12 irc: chat.freenode.net#videojs 12 irc:
13 channels:
14 - "chat.freenode.net#videojs"
15 use_notice: true
13 env: 16 env:
14 global: 17 global:
15 - secure: dM7svnHPPu5IiUMeFWW5zg+iuWNpwt6SSDi3MmVvhSclNMRLesQoRB+7Qq5J/LiKhmjpv1/GlNVV0CTsHMRhZNwQ3fo38eEuTXv99aAflEITXwSEh/VntKViHbGFubn06EnVkJoH6MX3zJ6kbiwc2QdSQbywKzS6l6quUEpWpd0= 18 - secure: dM7svnHPPu5IiUMeFWW5zg+iuWNpwt6SSDi3MmVvhSclNMRLesQoRB+7Qq5J/LiKhmjpv1/GlNVV0CTsHMRhZNwQ3fo38eEuTXv99aAflEITXwSEh/VntKViHbGFubn06EnVkJoH6MX3zJ6kbiwc2QdSQbywKzS6l6quUEpWpd0=
......
...@@ -24,7 +24,6 @@ module.exports = function(grunt) { ...@@ -24,7 +24,6 @@ module.exports = function(grunt) {
24 dist: { 24 dist: {
25 nonull: true, 25 nonull: true,
26 src: ['src/videojs-hls.js', 26 src: ['src/videojs-hls.js',
27 'src/async-queue.js',
28 'src/flv-tag.js', 27 'src/flv-tag.js',
29 'src/exp-golomb.js', 28 'src/exp-golomb.js',
30 'src/h264-stream.js', 29 'src/h264-stream.js',
......
1 [![Build Status](https://travis-ci.org/videojs/videojs-contrib-hls.png)](https://travis-ci.org/videojs/videojs-contrib-hls)
2
3 # video.js HLS Plugin 1 # video.js HLS Plugin
4 2
5 A video.js plugin that plays HLS video on platforms that don't support it but have Flash. 3 A video.js plugin that plays HLS video on platforms that don't support it but have Flash.
6 4
5 [![Build Status](https://travis-ci.org/videojs/videojs-contrib-hls.svg?branch=master)](https://travis-ci.org/videojs/videojs-contrib-hls)
6
7 ## Getting Started 7 ## Getting Started
8 Download the [plugin](https://github.com/videojs/videojs-contrib-hls/releases). On your web page: 8 Download the [plugin](https://github.com/videojs/videojs-contrib-hls/releases). On your web page:
9 9
...@@ -47,9 +47,26 @@ support for: ...@@ -47,9 +47,26 @@ support for:
47 - Alternate audio and video tracks 47 - Alternate audio and video tracks
48 - Subtitles 48 - Subtitles
49 - Segment codecs _other than_ H.264 with AAC audio 49 - Segment codecs _other than_ H.264 with AAC audio
50 - Live streams
51 - Internet Explorer < 10 50 - Internet Explorer < 10
52 51
52 ### Plugin Options
53
54 You may pass in an options object to the hls plugin upon initialization. This
55 object may contain one of the following properties:
56
57 #### withCredentials
58 Type: `boolean`
59
60 When the `withCredentials` property is set to `true`, all XHR requests for
61 manifests and segments would have `withCredentials` set to `true` as well. This
62 enables storing and passing cookies from the server that the manifests and
63 segments live on. This has some implications on CORS because when set, the
64 `Access-Control-Allow-Origin` header cannot be set to `*`, also, the response
65 headers require the addition of `Access-Control-Allow-Credentials` header which
66 is set to `true`.
67 See html5rocks's [article](http://www.html5rocks.com/en/tutorials/cors/)
68 for more info.
69
53 ### Runtime Properties 70 ### Runtime Properties
54 #### player.hls.master 71 #### player.hls.master
55 Type: `object` 72 Type: `object`
...@@ -119,6 +136,8 @@ bandwidth and viewport dimensions. ...@@ -119,6 +136,8 @@ bandwidth and viewport dimensions.
119 - [Best RESOLUTION variant] OR [Best BANDWIDTH variant] OR [inital playlist in manifest] 136 - [Best RESOLUTION variant] OR [Best BANDWIDTH variant] OR [inital playlist in manifest]
120 137
121 ## Release History 138 ## Release History
139 - 0.5.0: cookie-based content protection support (see `withCredentials`)
140 - 0.4.0: Live stream support
122 - 0.3.0: Performance fixes for high-bitrate streams 141 - 0.3.0: Performance fixes for high-bitrate streams
123 - 0.2.0: Basic playback and adaptive bitrate selection 142 - 0.2.0: Basic playback and adaptive bitrate selection
124 - 0.1.0: Initial release 143 - 0.1.0: Initial release
......
...@@ -10,11 +10,10 @@ ...@@ -10,11 +10,10 @@
10 <script src="node_modules/video.js/dist/video-js/video.js"></script> 10 <script src="node_modules/video.js/dist/video-js/video.js"></script>
11 11
12 <!-- Media Sources plugin --> 12 <!-- Media Sources plugin -->
13 <script src="node_modules/videojs-contrib-media-sources/videojs-media-sources.js"></script> 13 <script src="node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script>
14 14
15 <!-- HLS plugin --> 15 <!-- HLS plugin -->
16 <script src="src/videojs-hls.js"></script> 16 <script src="src/videojs-hls.js"></script>
17 <script src="src/async-queue.js"></script>
18 17
19 <!-- segment handling --> 18 <!-- segment handling -->
20 <script src="src/flv-tag.js"></script> 19 <script src="src/flv-tag.js"></script>
......
1 { 1 {
2 "name": "videojs-contrib-hls", 2 "name": "videojs-contrib-hls",
3 "version": "0.3.2", 3 "version": "0.5.0",
4 "engines": { 4 "engines": {
5 "node": ">= 0.10.12" 5 "node": ">= 0.10.12"
6 }, 6 },
...@@ -13,33 +13,34 @@ ...@@ -13,33 +13,34 @@
13 "test": "grunt test" 13 "test": "grunt test"
14 }, 14 },
15 "devDependencies": { 15 "devDependencies": {
16 "grunt": "~0.4.1",
17 "grunt-concurrent": "0.4.3",
18 "grunt-contrib-clean": "~0.4.0",
19 "grunt-contrib-concat": "~0.3.0",
20 "grunt-contrib-connect": "~0.6.0",
16 "grunt-contrib-jshint": "~0.6.0", 21 "grunt-contrib-jshint": "~0.6.0",
17 "grunt-contrib-qunit": "~0.2.0", 22 "grunt-contrib-qunit": "~0.2.0",
18 "grunt-contrib-concat": "~0.3.0",
19 "grunt-contrib-uglify": "~0.2.0", 23 "grunt-contrib-uglify": "~0.2.0",
20 "grunt-contrib-watch": "~0.4.0", 24 "grunt-contrib-watch": "~0.4.0",
21 "grunt-contrib-clean": "~0.4.0", 25 "grunt-karma": "~0.6.2",
22 "grunt-contrib-connect": "~0.6.0",
23 "grunt-concurrent": "0.4.3",
24 "grunt-open": "0.2.3", 26 "grunt-open": "0.2.3",
25 "grunt-shell": "0.6.1", 27 "grunt-shell": "0.6.1",
26 "grunt": "~0.4.1",
27 "grunt-karma": "~0.6.2",
28 "karma": "~0.10.0", 28 "karma": "~0.10.0",
29 "karma-sauce-launcher": "~0.1.8",
30 "karma-chrome-launcher": "~0.1.2", 29 "karma-chrome-launcher": "~0.1.2",
31 "karma-firefox-launcher": "~0.1.3", 30 "karma-firefox-launcher": "~0.1.3",
32 "karma-ie-launcher": "~0.1.1", 31 "karma-ie-launcher": "~0.1.1",
33 "karma-opera-launcher": "~0.1.0", 32 "karma-opera-launcher": "~0.1.0",
34 "karma-phantomjs-launcher": "~0.1.1", 33 "karma-phantomjs-launcher": "~0.1.1",
35 "karma-safari-launcher": "~0.1.1",
36 "karma-qunit": "~0.1.1", 34 "karma-qunit": "~0.1.1",
35 "karma-safari-launcher": "~0.1.1",
36 "karma-sauce-launcher": "~0.1.8",
37 "sinon": "^1.9.1",
37 "video.js": "^4.5" 38 "video.js": "^4.5"
38 }, 39 },
39 "peerDependencies": { 40 "peerDependencies": {
40 "video.js": "^4.5" 41 "video.js": "^4.5"
41 }, 42 },
42 "dependencies": { 43 "dependencies": {
43 "videojs-contrib-media-sources": "git+https://github.com/videojs/videojs-contrib-media-sources.git" 44 "videojs-contrib-media-sources": "^0.2"
44 } 45 }
45 } 46 }
......
1 (function(window, videojs, undefined) {
2 'use strict';
3 /**
4 * A queue object that manages tasks that should be processed
5 * serially but asynchronously. Loosely adapted from
6 * https://github.com/caolan/async#queue.
7 * @param worker {function} the callback to invoke with each value
8 * pushed onto the queue
9 * @return {object} an object with an array of `tasks` that remain to
10 * be processed and function `push` to add new tasks
11 */
12 videojs.hls.queue = function(worker) {
13 var
14 q = {
15 tasks: [],
16 running: false,
17 push: function(task) {
18 q.tasks.push(task);
19 if (!q.running) {
20 window.setTimeout(process, 0);
21 q.running = true;
22 }
23 }
24 },
25 process = function() {
26 var task;
27 if (q.tasks.length) {
28 task = q.tasks.shift();
29 worker.call(this, task);
30 window.setTimeout(process, 0);
31 } else {
32 q.running = false;
33 }
34 };
35 return q;
36 };
37 })(window, window.videojs);
...@@ -31,6 +31,9 @@ videojs.hls = { ...@@ -31,6 +31,9 @@ videojs.hls = {
31 }; 31 };
32 32
33 var 33 var
34
35 settings,
36
34 // the desired length of video to maintain in the buffer, in seconds 37 // the desired length of video to maintain in the buffer, in seconds
35 goalBufferLength = 5, 38 goalBufferLength = 5,
36 39
...@@ -109,12 +112,26 @@ var ...@@ -109,12 +112,26 @@ var
109 method: 'GET' 112 method: 'GET'
110 }, 113 },
111 request; 114 request;
115
116 if (typeof callback !== 'function') {
117 callback = function() {};
118 }
119
112 if (typeof url === 'object') { 120 if (typeof url === 'object') {
113 options = videojs.util.mergeOptions(options, url); 121 options = videojs.util.mergeOptions(options, url);
114 url = options.url; 122 url = options.url;
115 } 123 }
124
116 request = new window.XMLHttpRequest(); 125 request = new window.XMLHttpRequest();
117 request.open(options.method, url); 126 request.open(options.method, url);
127
128 if (options.responseType) {
129 request.responseType = options.responseType;
130 }
131 if (settings.withCredentials) {
132 request.withCredentials = true;
133 }
134
118 request.onreadystatechange = function() { 135 request.onreadystatechange = function() {
119 // wait until the request completes 136 // wait until the request completes
120 if (this.readyState !== 4) { 137 if (this.readyState !== 4) {
...@@ -204,13 +221,8 @@ var ...@@ -204,13 +221,8 @@ var
204 totalDuration = function(playlist) { 221 totalDuration = function(playlist) {
205 var 222 var
206 duration = 0, 223 duration = 0,
207 i, 224 segment,
208 segment; 225 i = (playlist.segments || []).length;
209
210 if (!playlist.segments) {
211 return 0;
212 }
213 i = playlist.segments.length;
214 226
215 // if present, use the duration specified in the playlist 227 // if present, use the duration specified in the playlist
216 if (playlist.totalDuration) { 228 if (playlist.totalDuration) {
...@@ -277,28 +289,22 @@ var ...@@ -277,28 +289,22 @@ var
277 mediaSource = new videojs.MediaSource(), 289 mediaSource = new videojs.MediaSource(),
278 segmentParser = new videojs.hls.SegmentParser(), 290 segmentParser = new videojs.hls.SegmentParser(),
279 player = this, 291 player = this,
280
281 // async queue of Uint8Arrays to be appended to the SourceBuffer
282 tags = videojs.hls.queue(function(tag) {
283 player.hls.sourceBuffer.appendBuffer(tag, player);
284
285 if (player.hls.mediaIndex === player.hls.media.segments.length) {
286 mediaSource.endOfStream();
287 }
288 }),
289 srcUrl, 292 srcUrl,
290 293
291 playlistXhr, 294 playlistXhr,
292 segmentXhr, 295 segmentXhr,
293 loadedPlaylist, 296 loadedPlaylist,
294 fillBuffer, 297 fillBuffer,
295 updateCurrentPlaylist; 298 updateCurrentPlaylist,
299 updateDuration;
296 300
297 // if the video element supports HLS natively, do nothing 301 // if the video element supports HLS natively, do nothing
298 if (videojs.hls.supportsNativeHls) { 302 if (videojs.hls.supportsNativeHls) {
299 return; 303 return;
300 } 304 }
301 305
306 settings = videojs.util.mergeOptions({}, options);
307
302 srcUrl = (function() { 308 srcUrl = (function() {
303 var 309 var
304 extname, 310 extname,
...@@ -312,7 +318,7 @@ var ...@@ -312,7 +318,7 @@ var
312 // use the URL specified in options if one was provided 318 // use the URL specified in options if one was provided
313 if (typeof options === 'string') { 319 if (typeof options === 'string') {
314 return options; 320 return options;
315 } else if (options) { 321 } else if (options && options.url) {
316 return options.url; 322 return options.url;
317 } 323 }
318 324
...@@ -370,17 +376,35 @@ var ...@@ -370,17 +376,35 @@ var
370 var currentTime = player.currentTime(); 376 var currentTime = player.currentTime();
371 player.hls.mediaIndex = getMediaIndexByTime(player.hls.media, currentTime); 377 player.hls.mediaIndex = getMediaIndexByTime(player.hls.media, currentTime);
372 378
379 // abort any segments still being decoded
380 player.hls.sourceBuffer.abort();
381
373 // cancel outstanding requests and buffer appends 382 // cancel outstanding requests and buffer appends
374 if (segmentXhr) { 383 if (segmentXhr) {
375 segmentXhr.abort(); 384 segmentXhr.abort();
376 } 385 }
377 tags.tasks = [];
378 386
379 // begin filling the buffer at the new position 387 // begin filling the buffer at the new position
380 fillBuffer(currentTime * 1000); 388 fillBuffer(currentTime * 1000);
381 }); 389 });
382 390
383 /** 391 /**
392 * Update the player duration
393 */
394 updateDuration = function(playlist) {
395 var tech;
396 // update the duration
397 player.duration(totalDuration(playlist));
398 // tell the flash tech of the new duration
399 tech = player.el().querySelector('.vjs-tech');
400 if(tech.vjs_setProperty) {
401 tech.vjs_setProperty('duration', player.duration());
402 }
403 // manually fire the duration change
404 player.trigger('durationchange');
405 };
406
407 /**
384 * Determine whether the current media playlist should be changed 408 * Determine whether the current media playlist should be changed
385 * and trigger a switch if necessary. If a sufficiently fresh 409 * and trigger a switch if necessary. If a sufficiently fresh
386 * version of the target playlist is available, the switch will take 410 * version of the target playlist is available, the switch will take
...@@ -406,8 +430,7 @@ var ...@@ -406,8 +430,7 @@ var
406 playlist); 430 playlist);
407 player.hls.media = playlist; 431 player.hls.media = playlist;
408 432
409 // update the duration 433 updateDuration(player.hls.media);
410 player.duration(totalDuration(player.hls.media));
411 } 434 }
412 }; 435 };
413 436
...@@ -558,7 +581,7 @@ var ...@@ -558,7 +581,7 @@ var
558 player.hls.media = player.hls.master.playlists[0]; 581 player.hls.media = player.hls.master.playlists[0];
559 582
560 // update the duration 583 // update the duration
561 player.duration(totalDuration(parser.manifest)); 584 updateDuration(parser.manifest);
562 585
563 // periodicaly check if the buffer needs to be refilled 586 // periodicaly check if the buffer needs to be refilled
564 player.on('timeupdate', fillBuffer); 587 player.on('timeupdate', fillBuffer);
...@@ -585,7 +608,7 @@ var ...@@ -585,7 +608,7 @@ var
585 var 608 var
586 buffered = player.buffered(), 609 buffered = player.buffered(),
587 bufferedTime = 0, 610 bufferedTime = 0,
588 segment = player.hls.media.segments[player.hls.mediaIndex], 611 segment,
589 segmentUri, 612 segmentUri,
590 startTime; 613 startTime;
591 614
...@@ -594,7 +617,13 @@ var ...@@ -594,7 +617,13 @@ var
594 return; 617 return;
595 } 618 }
596 619
620 // if no segments are available, do nothing
621 if (!player.hls.media.segments) {
622 return;
623 }
624
597 // if the video has finished downloading, stop trying to buffer 625 // if the video has finished downloading, stop trying to buffer
626 segment = player.hls.media.segments[player.hls.mediaIndex];
598 if (!segment) { 627 if (!segment) {
599 return; 628 return;
600 } 629 }
...@@ -617,24 +646,20 @@ var ...@@ -617,24 +646,20 @@ var
617 segment.uri); 646 segment.uri);
618 } 647 }
619 648
620 // request the next segment 649 startTime = +new Date();
621 segmentXhr = new window.XMLHttpRequest();
622 segmentXhr.open('GET', segmentUri);
623 segmentXhr.responseType = 'arraybuffer';
624 segmentXhr.onreadystatechange = function() {
625 // wait until the request completes
626 if (this.readyState !== 4) {
627 return;
628 }
629 650
651 // request the next segment
652 segmentXhr = xhr({
653 url: segmentUri,
654 responseType: 'arraybuffer'
655 }, function(error, url) {
630 // the segment request is no longer outstanding 656 // the segment request is no longer outstanding
631 segmentXhr = null; 657 segmentXhr = null;
632 658
633 // trigger an error if the request was not successful 659 if (error) {
634 if (this.status >= 400) {
635 player.hls.error = { 660 player.hls.error = {
636 status: this.status, 661 status: this.status,
637 message: 'HLS segment request error at URL: ' + segmentUri, 662 message: 'HLS segment request error at URL: ' + url,
638 code: (this.status >= 500) ? 4 : 2 663 code: (this.status >= 500) ? 4 : 2
639 }; 664 };
640 665
...@@ -669,17 +694,22 @@ var ...@@ -669,17 +694,22 @@ var
669 // queue up the bytes to be appended to the SourceBuffer 694 // queue up the bytes to be appended to the SourceBuffer
670 // the queue gives control back to the browser between tags 695 // the queue gives control back to the browser between tags
671 // so that large segments don't cause a "hiccup" in playback 696 // so that large segments don't cause a "hiccup" in playback
672 tags.push(segmentParser.getNextTag().bytes); 697
698 player.hls.sourceBuffer.appendBuffer(segmentParser.getNextTag().bytes,
699 player);
700
673 } 701 }
674 702
675 player.hls.mediaIndex++; 703 player.hls.mediaIndex++;
676 704
705 if (player.hls.mediaIndex === player.hls.media.segments.length) {
706 mediaSource.endOfStream();
707 }
708
677 // figure out what stream the next segment should be downloaded from 709 // figure out what stream the next segment should be downloaded from
678 // with the updated bandwidth information 710 // with the updated bandwidth information
679 updateCurrentPlaylist(); 711 updateCurrentPlaylist();
680 }; 712 });
681 startTime = +new Date();
682 segmentXhr.send(null);
683 }; 713 };
684 714
685 // load the MediaSource into the player 715 // load the MediaSource into the player
......
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
28 "strictEqual", 28 "strictEqual",
29 "notStrictEqual", 29 "notStrictEqual",
30 "throws", 30 "throws",
31 "sinon",
31 "process" 32 "process"
32 ] 33 ]
33 } 34 }
......
1 (function(window, queue, undefined) {
2 var
3 oldSetTimeout,
4 callbacks;
5 module('async queue', {
6 setup: function() {
7 oldSetTimeout = window.setTimeout;
8 callbacks = [];
9 window.setTimeout = function(callback) {
10 callbacks.push(callback);
11 };
12 },
13 teardown: function() {
14 window.setTimeout = oldSetTimeout;
15 }
16 });
17
18 test('runs tasks asynchronously', function() {
19 var
20 run = false,
21 q = queue(function() {
22 run = true;
23 });
24 q.push(1);
25
26 ok(!run, 'tasks are not run immediately');
27
28 callbacks[0]();
29 ok(run, 'tasks are run asynchronously');
30 });
31
32 test('runs one task at a time', function() {
33 var q = queue(function() {});
34 q.push(1);
35 q.push(2);
36 q.push(3);
37 q.push(4);
38 q.push(5);
39
40 strictEqual(q.tasks.length, 5, 'all tasks are queued');
41 strictEqual(1, callbacks.length, 'one callback is registered');
42 });
43
44 test('tasks are scheduled until the queue is empty', function() {
45 var q = queue(function() {});
46 q.push(1);
47 q.push(2);
48
49 callbacks.shift()();
50 strictEqual(1, callbacks.length, 'the next task is scheduled');
51
52 callbacks.shift()();
53 strictEqual(1, callbacks.length, 'nothing is scheduled on an empty queue');
54 });
55
56 test('can be emptied at any time', function() {
57 var
58 runs = 0,
59 q = queue(function() {
60 runs++;
61 });
62 q.push(1);
63 q.push(2);
64
65 callbacks.shift()();
66 strictEqual(1, runs, 'task one is run');
67
68 q.tasks = [];
69 callbacks.shift()();
70 strictEqual(1, runs, 'the remaining tasks are cancelled');
71 });
72 })(window, window.videojs.hls.queue);
...@@ -40,7 +40,7 @@ module.exports = function(config) { ...@@ -40,7 +40,7 @@ module.exports = function(config) {
40 40
41 files: [ 41 files: [
42 '../node_modules/video.js/dist/video-js/video.js', 42 '../node_modules/video.js/dist/video-js/video.js',
43 '../node_modules/videojs-contrib-media-sources/videojs-media-sources.js', 43 '../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js',
44 '../test/karma-qunit-shim.js', 44 '../test/karma-qunit-shim.js',
45 "../src/videojs-hls.js", 45 "../src/videojs-hls.js",
46 "../src/flv-tag.js", 46 "../src/flv-tag.js",
......
...@@ -35,7 +35,7 @@ module.exports = function(config) { ...@@ -35,7 +35,7 @@ module.exports = function(config) {
35 35
36 files: [ 36 files: [
37 '../node_modules/video.js/dist/video-js/video.js', 37 '../node_modules/video.js/dist/video-js/video.js',
38 '../node_modules/videojs-contrib-media-sources/videojs-media-sources.js', 38 '../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js',
39 '../test/karma-qunit-shim.js', 39 '../test/karma-qunit-shim.js',
40 "../src/videojs-hls.js", 40 "../src/videojs-hls.js",
41 "../src/flv-tag.js", 41 "../src/flv-tag.js",
......
...@@ -3,13 +3,19 @@ ...@@ -3,13 +3,19 @@
3 <head> 3 <head>
4 <meta charset="utf-8"> 4 <meta charset="utf-8">
5 <title>video.js HLS Plugin Test Suite</title> 5 <title>video.js HLS Plugin Test Suite</title>
6 <!-- Load sinon server for fakeXHR -->
7 <script src="../node_modules/sinon/lib/sinon.js"></script>
8 <script src="../node_modules/sinon/lib/sinon/util/event.js"></script>
9 <script src="../node_modules/sinon/lib/sinon/util/xhr_ie.js"></script>
10 <script src="../node_modules/sinon/lib/sinon/util/fake_xml_http_request.js"></script>
11
6 <!-- Load local QUnit. --> 12 <!-- Load local QUnit. -->
7 <link rel="stylesheet" href="../libs/qunit/qunit.css" media="screen"> 13 <link rel="stylesheet" href="../libs/qunit/qunit.css" media="screen">
8 <script src="../libs/qunit/qunit.js"></script> 14 <script src="../libs/qunit/qunit.js"></script>
9 15
10 <!-- video.js --> 16 <!-- video.js -->
11 <script src="../node_modules/video.js/dist/video-js/video.js"></script> 17 <script src="../node_modules/video.js/dist/video-js/video.js"></script>
12 <script src="../node_modules/videojs-contrib-media-sources/videojs-media-sources.js"></script> 18 <script src="../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script>
13 19
14 <!-- HLS plugin --> 20 <!-- HLS plugin -->
15 <script src="../src/videojs-hls.js"></script> 21 <script src="../src/videojs-hls.js"></script>
...@@ -31,9 +37,6 @@ ...@@ -31,9 +37,6 @@
31 <script src="tsSegment-bc.js"></script> 37 <script src="tsSegment-bc.js"></script>
32 <script src="../src/bin-utils.js"></script> 38 <script src="../src/bin-utils.js"></script>
33 39
34 <!-- async queue -->
35 <script src="../src/async-queue.js"></script>
36
37 <!-- Test cases --> 40 <!-- Test cases -->
38 <script> 41 <script>
39 module('environment'); 42 module('environment');
...@@ -48,7 +51,6 @@ ...@@ -48,7 +51,6 @@
48 <script src="exp-golomb_test.js"></script> 51 <script src="exp-golomb_test.js"></script>
49 <script src="flv-tag_test.js"></script> 52 <script src="flv-tag_test.js"></script>
50 <script src="m3u8_test.js"></script> 53 <script src="m3u8_test.js"></script>
51 <script src="async-queue_test.js"></script>
52 </head> 54 </head>
53 <body> 55 <body>
54 <div id="qunit"></div> 56 <div id="qunit"></div>
......
...@@ -23,12 +23,40 @@ ...@@ -23,12 +23,40 @@
23 var 23 var
24 player, 24 player,
25 oldFlashSupported, 25 oldFlashSupported,
26 oldXhr,
27 oldSegmentParser, 26 oldSegmentParser,
28 oldSetTimeout, 27 oldSetTimeout,
29 oldSourceBuffer, 28 oldSourceBuffer,
30 oldSupportsNativeHls, 29 oldSupportsNativeHls,
31 xhrUrls, 30 xhrUrls,
31 requests,
32 xhr,
33
34 standardXHRResponse = function(request) {
35 if (!request.url) {
36 return;
37 }
38
39 var contentType = "application/json",
40 // contents off the global object
41 manifestName = (/(?:.*\/)?(.*)\.m3u8/).exec(request.url);
42
43 if (manifestName) {
44 manifestName = manifestName[1];
45 } else {
46 manifestName = request.url;
47 }
48
49 if (/\.m3u8?/.test(request.url)) {
50 contentType = 'application/vnd.apple.mpegurl';
51 } else if (/\.ts/.test(request.url)) {
52 contentType = 'video/MP2T';
53 }
54
55 request.response = new Uint8Array([1]).buffer;
56 request.respond(200,
57 {'Content-Type': contentType},
58 window.manifests[manifestName]);
59 },
32 60
33 mockSegmentParser = function(tags) { 61 mockSegmentParser = function(tags) {
34 if (tags === undefined) { 62 if (tags === undefined) {
...@@ -63,6 +91,7 @@ module('HLS', { ...@@ -63,6 +91,7 @@ module('HLS', {
63 oldSourceBuffer = window.videojs.SourceBuffer; 91 oldSourceBuffer = window.videojs.SourceBuffer;
64 window.videojs.SourceBuffer = function() { 92 window.videojs.SourceBuffer = function() {
65 this.appendBuffer = function() {}; 93 this.appendBuffer = function() {};
94 this.abort = function() {};
66 }; 95 };
67 96
68 // force native HLS to be ignored 97 // force native HLS to be ignored
...@@ -87,35 +116,21 @@ module('HLS', { ...@@ -87,35 +116,21 @@ module('HLS', {
87 oldSetTimeout = window.setTimeout; 116 oldSetTimeout = window.setTimeout;
88 117
89 // make XHRs synchronous 118 // make XHRs synchronous
90 oldXhr = window.XMLHttpRequest; 119 xhr = sinon.useFakeXMLHttpRequest();
91 window.XMLHttpRequest = function() { 120 requests = [];
92 this.open = function(method, url) { 121 xhr.onCreate = function(xhr) {
93 xhrUrls.push(url); 122 requests.push(xhr);
94 };
95 this.send = function() {
96 // if the request URL looks like one of the test manifests, grab the
97 // contents off the global object
98 var manifestName = (/(?:.*\/)?(.*)\.m3u8/).exec(xhrUrls.slice(-1)[0]);
99 if (manifestName) {
100 manifestName = manifestName[1];
101 }
102 this.responseText = window.manifests[manifestName || xhrUrls.slice(-1)[0]];
103 this.response = new Uint8Array([1]).buffer;
104
105 this.readyState = 4;
106 this.onreadystatechange();
107 };
108 this.abort = function() {};
109 }; 123 };
110 xhrUrls = []; 124 xhrUrls = [];
111 }, 125 },
126
112 teardown: function() { 127 teardown: function() {
113 videojs.Flash.isSupported = oldFlashSupported; 128 videojs.Flash.isSupported = oldFlashSupported;
114 videojs.hls.supportsNativeHls = oldSupportsNativeHls; 129 videojs.hls.supportsNativeHls = oldSupportsNativeHls;
115 videojs.hls.SegmentParser = oldSegmentParser; 130 videojs.hls.SegmentParser = oldSegmentParser;
116 videojs.SourceBuffer = oldSourceBuffer; 131 videojs.SourceBuffer = oldSourceBuffer;
117 window.setTimeout = oldSetTimeout; 132 window.setTimeout = oldSetTimeout;
118 window.XMLHttpRequest = oldXhr; 133 xhr.restore();
119 } 134 }
120 }); 135 });
121 136
...@@ -130,6 +145,7 @@ test('starts playing if autoplay is specified', function() { ...@@ -130,6 +145,7 @@ test('starts playing if autoplay is specified', function() {
130 type: 'sourceopen' 145 type: 'sourceopen'
131 }); 146 });
132 147
148 standardXHRResponse(requests[0]);
133 strictEqual(1, plays, 'play was called'); 149 strictEqual(1, plays, 'play was called');
134 }); 150 });
135 151
...@@ -147,6 +163,7 @@ test('loads the specified manifest URL on init', function() { ...@@ -147,6 +163,7 @@ test('loads the specified manifest URL on init', function() {
147 videojs.mediaSources[player.currentSrc()].trigger({ 163 videojs.mediaSources[player.currentSrc()].trigger({
148 type: 'sourceopen' 164 type: 'sourceopen'
149 }); 165 });
166 standardXHRResponse(requests[0]);
150 ok(loadedmanifest, 'loadedmanifest fires'); 167 ok(loadedmanifest, 'loadedmanifest fires');
151 ok(loadedmetadata, 'loadedmetadata fires'); 168 ok(loadedmetadata, 'loadedmetadata fires');
152 ok(player.hls.master, 'a master is inferred'); 169 ok(player.hls.master, 'a master is inferred');
...@@ -171,6 +188,8 @@ test('sets the duration if one is available on the playlist', function() { ...@@ -171,6 +188,8 @@ test('sets the duration if one is available on the playlist', function() {
171 type: 'sourceopen' 188 type: 'sourceopen'
172 }); 189 });
173 190
191 standardXHRResponse(requests[0]);
192 standardXHRResponse(requests[1]);
174 strictEqual(calls, 2, 'duration is set'); 193 strictEqual(calls, 2, 'duration is set');
175 }); 194 });
176 195
...@@ -187,6 +206,8 @@ test('calculates the duration if needed', function() { ...@@ -187,6 +206,8 @@ test('calculates the duration if needed', function() {
187 type: 'sourceopen' 206 type: 'sourceopen'
188 }); 207 });
189 208
209 standardXHRResponse(requests[0]);
210 standardXHRResponse(requests[1]);
190 strictEqual(durations.length, 2, 'duration is set'); 211 strictEqual(durations.length, 2, 'duration is set');
191 strictEqual(durations[0], 212 strictEqual(durations[0],
192 player.hls.media.segments.length * 10, 213 player.hls.media.segments.length * 10,
...@@ -202,7 +223,9 @@ test('starts downloading a segment on loadedmetadata', function() { ...@@ -202,7 +223,9 @@ test('starts downloading a segment on loadedmetadata', function() {
202 type: 'sourceopen' 223 type: 'sourceopen'
203 }); 224 });
204 225
205 strictEqual(xhrUrls[1], 226 standardXHRResponse(requests[0]);
227 standardXHRResponse(requests[1]);
228 strictEqual(requests[1].url,
206 window.location.origin + 229 window.location.origin +
207 window.location.pathname.split('/').slice(0, -1).join('/') + 230 window.location.pathname.split('/').slice(0, -1).join('/') +
208 '/manifest/00001.ts', 231 '/manifest/00001.ts',
...@@ -215,7 +238,9 @@ test('recognizes absolute URIs and requests them unmodified', function() { ...@@ -215,7 +238,9 @@ test('recognizes absolute URIs and requests them unmodified', function() {
215 type: 'sourceopen' 238 type: 'sourceopen'
216 }); 239 });
217 240
218 strictEqual(xhrUrls[1], 241 standardXHRResponse(requests[0]);
242 standardXHRResponse(requests[1]);
243 strictEqual(requests[1].url,
219 'http://example.com/00001.ts', 244 'http://example.com/00001.ts',
220 'the first segment is requested'); 245 'the first segment is requested');
221 }); 246 });
...@@ -226,7 +251,9 @@ test('recognizes domain-relative URLs', function() { ...@@ -226,7 +251,9 @@ test('recognizes domain-relative URLs', function() {
226 type: 'sourceopen' 251 type: 'sourceopen'
227 }); 252 });
228 253
229 strictEqual(xhrUrls[1], 254 standardXHRResponse(requests[0]);
255 standardXHRResponse(requests[1]);
256 strictEqual(requests[1].url,
230 window.location.origin + '/00001.ts', 257 window.location.origin + '/00001.ts',
231 'the first segment is requested'); 258 'the first segment is requested');
232 }); 259 });
...@@ -272,13 +299,17 @@ test('downloads media playlists after loading the master', function() { ...@@ -272,13 +299,17 @@ test('downloads media playlists after loading the master', function() {
272 type: 'sourceopen' 299 type: 'sourceopen'
273 }); 300 });
274 301
275 strictEqual(xhrUrls[0], 'manifest/master.m3u8', 'master playlist requested'); 302 standardXHRResponse(requests[0]);
276 strictEqual(xhrUrls[1], 303 standardXHRResponse(requests[1]);
304 standardXHRResponse(requests[2]);
305
306 strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested');
307 strictEqual(requests[1].url,
277 window.location.origin + 308 window.location.origin +
278 window.location.pathname.split('/').slice(0, -1).join('/') + 309 window.location.pathname.split('/').slice(0, -1).join('/') +
279 '/manifest/media.m3u8', 310 '/manifest/media.m3u8',
280 'media playlist requested'); 311 'media playlist requested');
281 strictEqual(xhrUrls[2], 312 strictEqual(requests[2].url,
282 window.location.origin + 313 window.location.origin +
283 window.location.pathname.split('/').slice(0, -1).join('/') + 314 window.location.pathname.split('/').slice(0, -1).join('/') +
284 '/manifest/00001.ts', 315 '/manifest/00001.ts',
...@@ -309,6 +340,9 @@ test('calculates the bandwidth after downloading a segment', function() { ...@@ -309,6 +340,9 @@ test('calculates the bandwidth after downloading a segment', function() {
309 type: 'sourceopen' 340 type: 'sourceopen'
310 }); 341 });
311 342
343 standardXHRResponse(requests[0]);
344 standardXHRResponse(requests[1]);
345
312 ok(player.hls.bandwidth, 'bandwidth is calculated'); 346 ok(player.hls.bandwidth, 'bandwidth is calculated');
313 ok(player.hls.bandwidth > 0, 347 ok(player.hls.bandwidth > 0,
314 'bandwidth is positive: ' + player.hls.bandwidth); 348 'bandwidth is positive: ' + player.hls.bandwidth);
...@@ -327,6 +361,10 @@ test('selects a playlist after segment downloads', function() { ...@@ -327,6 +361,10 @@ test('selects a playlist after segment downloads', function() {
327 type: 'sourceopen' 361 type: 'sourceopen'
328 }); 362 });
329 363
364 standardXHRResponse(requests[0]);
365 standardXHRResponse(requests[1]);
366 standardXHRResponse(requests[2]);
367
330 strictEqual(calls, 1, 'selects after the initial segment'); 368 strictEqual(calls, 1, 'selects after the initial segment');
331 player.currentTime = function() { 369 player.currentTime = function() {
332 return 1; 370 return 1;
...@@ -335,28 +373,26 @@ test('selects a playlist after segment downloads', function() { ...@@ -335,28 +373,26 @@ test('selects a playlist after segment downloads', function() {
335 return videojs.createTimeRange(0, 2); 373 return videojs.createTimeRange(0, 2);
336 }; 374 };
337 player.trigger('timeupdate'); 375 player.trigger('timeupdate');
376
377 standardXHRResponse(requests[3]);
338 strictEqual(calls, 2, 'selects after additional segments'); 378 strictEqual(calls, 2, 'selects after additional segments');
339 }); 379 });
340 380
341 test('moves to the next segment if there is a network error', function() { 381 test('moves to the next segment if there is a network error', function() {
342 var mediaIndex; 382 var mediaIndex;
383
343 player.hls('manifest/master.m3u8'); 384 player.hls('manifest/master.m3u8');
344 videojs.mediaSources[player.currentSrc()].trigger({ 385 videojs.mediaSources[player.currentSrc()].trigger({
345 type: 'sourceopen' 386 type: 'sourceopen'
346 }); 387 });
347 388
348 // fail the next segment request 389 standardXHRResponse(requests[0]);
349 window.XMLHttpRequest = function() { 390 standardXHRResponse(requests[1]);
350 this.open = function() {}; 391
351 this.send = function() {
352 this.readyState = 4;
353 this.status = 400;
354 this.onreadystatechange();
355 };
356 };
357 mediaIndex = player.hls.mediaIndex; 392 mediaIndex = player.hls.mediaIndex;
358 player.trigger('timeupdate'); 393 player.trigger('timeupdate');
359 394
395 requests[2].respond(400);
360 strictEqual(mediaIndex + 1, player.hls.mediaIndex, 'media index is incremented'); 396 strictEqual(mediaIndex + 1, player.hls.mediaIndex, 'media index is incremented');
361 }); 397 });
362 398
...@@ -382,6 +418,10 @@ test('updates the duration after switching playlists', function() { ...@@ -382,6 +418,10 @@ test('updates the duration after switching playlists', function() {
382 type: 'sourceopen' 418 type: 'sourceopen'
383 }); 419 });
384 420
421 standardXHRResponse(requests[0]);
422 standardXHRResponse(requests[1]);
423 standardXHRResponse(requests[2]);
424 standardXHRResponse(requests[3]);
385 ok(selectedPlaylist, 'selected playlist'); 425 ok(selectedPlaylist, 'selected playlist');
386 strictEqual(calls, 1, 'updates the duration'); 426 strictEqual(calls, 1, 'updates the duration');
387 }); 427 });
...@@ -397,6 +437,8 @@ test('downloads additional playlists if required', function() { ...@@ -397,6 +437,8 @@ test('downloads additional playlists if required', function() {
397 type: 'sourceopen' 437 type: 'sourceopen'
398 }); 438 });
399 439
440 standardXHRResponse(requests[0]);
441 standardXHRResponse(requests[1]);
400 // before an m3u8 is downloaded, no segments are available 442 // before an m3u8 is downloaded, no segments are available
401 player.hls.selectPlaylist = function() { 443 player.hls.selectPlaylist = function() {
402 if (!called) { 444 if (!called) {
...@@ -406,13 +448,15 @@ test('downloads additional playlists if required', function() { ...@@ -406,13 +448,15 @@ test('downloads additional playlists if required', function() {
406 playlist.segments = [1, 1, 1]; 448 playlist.segments = [1, 1, 1];
407 return playlist; 449 return playlist;
408 }; 450 };
409 xhrUrls = [];
410 451
411 // the playlist selection is revisited after a new segment is downloaded 452 // the playlist selection is revisited after a new segment is downloaded
412 player.trigger('timeupdate'); 453 player.trigger('timeupdate');
413 454
414 strictEqual(2, xhrUrls.length, 'requests were made'); 455 standardXHRResponse(requests[2]);
415 strictEqual(xhrUrls[1], 456 standardXHRResponse(requests[3]);
457
458 strictEqual(4, requests.length, 'requests were made');
459 strictEqual(requests[3].url,
416 window.location.origin + 460 window.location.origin +
417 window.location.pathname.split('/').slice(0, -1).join('/') + 461 window.location.pathname.split('/').slice(0, -1).join('/') +
418 '/manifest/' + 462 '/manifest/' +
...@@ -429,6 +473,8 @@ test('selects a playlist below the current bandwidth', function() { ...@@ -429,6 +473,8 @@ test('selects a playlist below the current bandwidth', function() {
429 type: 'sourceopen' 473 type: 'sourceopen'
430 }); 474 });
431 475
476 standardXHRResponse(requests[0]);
477
432 // the default playlist has a really high bitrate 478 // the default playlist has a really high bitrate
433 player.hls.master.playlists[0].attributes.BANDWIDTH = 9e10; 479 player.hls.master.playlists[0].attributes.BANDWIDTH = 9e10;
434 // playlist 1 has a very low bitrate 480 // playlist 1 has a very low bitrate
...@@ -449,6 +495,8 @@ test('raises the minimum bitrate for a stream proportionially', function() { ...@@ -449,6 +495,8 @@ test('raises the minimum bitrate for a stream proportionially', function() {
449 type: 'sourceopen' 495 type: 'sourceopen'
450 }); 496 });
451 497
498 standardXHRResponse(requests[0]);
499
452 // the default playlist's bandwidth + 10% is equal to the current bandwidth 500 // the default playlist's bandwidth + 10% is equal to the current bandwidth
453 player.hls.master.playlists[0].attributes.BANDWIDTH = 10; 501 player.hls.master.playlists[0].attributes.BANDWIDTH = 10;
454 player.hls.bandwidth = 11; 502 player.hls.bandwidth = 11;
...@@ -469,6 +517,8 @@ test('uses the lowest bitrate if no other is suitable', function() { ...@@ -469,6 +517,8 @@ test('uses the lowest bitrate if no other is suitable', function() {
469 type: 'sourceopen' 517 type: 'sourceopen'
470 }); 518 });
471 519
520 standardXHRResponse(requests[0]);
521
472 // the lowest bitrate playlist is much greater than 1b/s 522 // the lowest bitrate playlist is much greater than 1b/s
473 player.hls.bandwidth = 1; 523 player.hls.bandwidth = 1;
474 playlist = player.hls.selectPlaylist(); 524 playlist = player.hls.selectPlaylist();
...@@ -488,6 +538,8 @@ test('selects the correct rendition by player dimensions', function() { ...@@ -488,6 +538,8 @@ test('selects the correct rendition by player dimensions', function() {
488 type: 'sourceopen' 538 type: 'sourceopen'
489 }); 539 });
490 540
541 standardXHRResponse(requests[0]);
542
491 player.width(640); 543 player.width(640);
492 player.height(360); 544 player.height(360);
493 player.hls.bandwidth = 3000000; 545 player.hls.bandwidth = 3000000;
...@@ -520,9 +572,12 @@ test('does not download the next segment if the buffer is full', function() { ...@@ -520,9 +572,12 @@ test('does not download the next segment if the buffer is full', function() {
520 videojs.mediaSources[player.currentSrc()].trigger({ 572 videojs.mediaSources[player.currentSrc()].trigger({
521 type: 'sourceopen' 573 type: 'sourceopen'
522 }); 574 });
575
576 standardXHRResponse(requests[0]);
577
523 player.trigger('timeupdate'); 578 player.trigger('timeupdate');
524 579
525 strictEqual(xhrUrls.length, 1, 'no segment request was made'); 580 strictEqual(requests.length, 1, 'no segment request was made');
526 }); 581 });
527 582
528 test('downloads the next segment if the buffer is getting low', function() { 583 test('downloads the next segment if the buffer is getting low', function() {
...@@ -530,7 +585,11 @@ test('downloads the next segment if the buffer is getting low', function() { ...@@ -530,7 +585,11 @@ test('downloads the next segment if the buffer is getting low', function() {
530 videojs.mediaSources[player.currentSrc()].trigger({ 585 videojs.mediaSources[player.currentSrc()].trigger({
531 type: 'sourceopen' 586 type: 'sourceopen'
532 }); 587 });
533 strictEqual(xhrUrls.length, 2, 'did not make a request'); 588
589 standardXHRResponse(requests[0]);
590 standardXHRResponse(requests[1]);
591
592 strictEqual(requests.length, 2, 'did not make a request');
534 player.currentTime = function() { 593 player.currentTime = function() {
535 return 15; 594 return 15;
536 }; 595 };
...@@ -539,8 +598,10 @@ test('downloads the next segment if the buffer is getting low', function() { ...@@ -539,8 +598,10 @@ test('downloads the next segment if the buffer is getting low', function() {
539 }; 598 };
540 player.trigger('timeupdate'); 599 player.trigger('timeupdate');
541 600
542 strictEqual(xhrUrls.length, 3, 'made a request'); 601 standardXHRResponse(requests[2]);
543 strictEqual(xhrUrls[2], 602
603 strictEqual(requests.length, 3, 'made a request');
604 strictEqual(requests[2].url,
544 window.location.origin + 605 window.location.origin +
545 window.location.pathname.split('/').slice(0, -1).join('/') + 606 window.location.pathname.split('/').slice(0, -1).join('/') +
546 '/manifest/00002.ts', 607 '/manifest/00002.ts',
...@@ -552,7 +613,8 @@ test('stops downloading segments at the end of the playlist', function() { ...@@ -552,7 +613,8 @@ test('stops downloading segments at the end of the playlist', function() {
552 videojs.mediaSources[player.currentSrc()].trigger({ 613 videojs.mediaSources[player.currentSrc()].trigger({
553 type: 'sourceopen' 614 type: 'sourceopen'
554 }); 615 });
555 xhrUrls = []; 616 standardXHRResponse(requests[0]);
617 requests = [];
556 player.hls.mediaIndex = 4; 618 player.hls.mediaIndex = 4;
557 player.trigger('timeupdate'); 619 player.trigger('timeupdate');
558 620
...@@ -565,6 +627,8 @@ test('only makes one segment request at a time', function() { ...@@ -565,6 +627,8 @@ test('only makes one segment request at a time', function() {
565 videojs.mediaSources[player.currentSrc()].trigger({ 627 videojs.mediaSources[player.currentSrc()].trigger({
566 type: 'sourceopen' 628 type: 'sourceopen'
567 }); 629 });
630 xhr.restore();
631 var oldXHR = window.XMLHttpRequest;
568 // mock out a long-running XHR 632 // mock out a long-running XHR
569 window.XMLHttpRequest = function() { 633 window.XMLHttpRequest = function() {
570 this.send = function() {}; 634 this.send = function() {};
...@@ -572,11 +636,14 @@ test('only makes one segment request at a time', function() { ...@@ -572,11 +636,14 @@ test('only makes one segment request at a time', function() {
572 openedXhrs++; 636 openedXhrs++;
573 }; 637 };
574 }; 638 };
639 standardXHRResponse(requests[0]);
575 player.trigger('timeupdate'); 640 player.trigger('timeupdate');
576 641
577 strictEqual(1, openedXhrs, 'one XHR is made'); 642 strictEqual(1, openedXhrs, 'one XHR is made');
578 player.trigger('timeupdate'); 643 player.trigger('timeupdate');
579 strictEqual(1, openedXhrs, 'only one XHR is made'); 644 strictEqual(1, openedXhrs, 'only one XHR is made');
645 window.XMLHttpRequest = oldXHR;
646 xhr = sinon.useFakeXMLHttpRequest();
580 }); 647 });
581 648
582 test('uses the src attribute if no options are provided and it ends in ".m3u8"', function() { 649 test('uses the src attribute if no options are provided and it ends in ".m3u8"', function() {
...@@ -587,7 +654,7 @@ test('uses the src attribute if no options are provided and it ends in ".m3u8"', ...@@ -587,7 +654,7 @@ test('uses the src attribute if no options are provided and it ends in ".m3u8"',
587 type: 'sourceopen' 654 type: 'sourceopen'
588 }); 655 });
589 656
590 strictEqual(url, xhrUrls[0], 'currentSrc is used'); 657 strictEqual(requests[0].url, url, 'currentSrc is used');
591 }); 658 });
592 659
593 test('ignores src attribute if it doesn\'t have the "m3u8" extension', function() { 660 test('ignores src attribute if it doesn\'t have the "m3u8" extension', function() {
...@@ -595,27 +662,27 @@ test('ignores src attribute if it doesn\'t have the "m3u8" extension', function( ...@@ -595,27 +662,27 @@ test('ignores src attribute if it doesn\'t have the "m3u8" extension', function(
595 tech.src = 'basdfasdfasdfliel//.m3u9'; 662 tech.src = 'basdfasdfasdfliel//.m3u9';
596 player.hls(); 663 player.hls();
597 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created'); 664 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
598 strictEqual(xhrUrls.length, 0, 'no request is made'); 665 strictEqual(requests.length, 0, 'no request is made');
599 666
600 tech.src = ''; 667 tech.src = '';
601 player.hls(); 668 player.hls();
602 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created'); 669 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
603 strictEqual(xhrUrls.length, 0, 'no request is made'); 670 strictEqual(requests.length, 0, 'no request is made');
604 671
605 tech.src = 'http://example.com/movie.mp4?q=why.m3u8'; 672 tech.src = 'http://example.com/movie.mp4?q=why.m3u8';
606 player.hls(); 673 player.hls();
607 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created'); 674 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
608 strictEqual(xhrUrls.length, 0, 'no request is made'); 675 strictEqual(requests.length, 0, 'no request is made');
609 676
610 tech.src = 'http://example.m3u8/movie.mp4'; 677 tech.src = 'http://example.m3u8/movie.mp4';
611 player.hls(); 678 player.hls();
612 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created'); 679 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
613 strictEqual(xhrUrls.length, 0, 'no request is made'); 680 strictEqual(requests.length, 0, 'no request is made');
614 681
615 tech.src = '//example.com/movie.mp4#http://tricky.com/master.m3u8'; 682 tech.src = '//example.com/movie.mp4#http://tricky.com/master.m3u8';
616 player.hls(); 683 player.hls();
617 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created'); 684 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
618 strictEqual(xhrUrls.length, 0, 'no request is made'); 685 strictEqual(requests.length, 0, 'no request is made');
619 }); 686 });
620 687
621 test('activates if the first playable source is HLS', function() { 688 test('activates if the first playable source is HLS', function() {
...@@ -639,13 +706,11 @@ test('activates if the first playable source is HLS', function() { ...@@ -639,13 +706,11 @@ test('activates if the first playable source is HLS', function() {
639 }); 706 });
640 707
641 test('cancels outstanding XHRs when seeking', function() { 708 test('cancels outstanding XHRs when seeking', function() {
642 var
643 aborted = false,
644 opened = 0;
645 player.hls('manifest/media.m3u8'); 709 player.hls('manifest/media.m3u8');
646 videojs.mediaSources[player.currentSrc()].trigger({ 710 videojs.mediaSources[player.currentSrc()].trigger({
647 type: 'sourceopen' 711 type: 'sourceopen'
648 }); 712 });
713 standardXHRResponse(requests[0]);
649 player.hls.media = { 714 player.hls.media = {
650 segments: [{ 715 segments: [{
651 uri: '0.ts', 716 uri: '0.ts',
...@@ -656,27 +721,13 @@ test('cancels outstanding XHRs when seeking', function() { ...@@ -656,27 +721,13 @@ test('cancels outstanding XHRs when seeking', function() {
656 }] 721 }]
657 }; 722 };
658 723
659 // XHR requests will never complete
660 window.XMLHttpRequest = function() {
661 this.open = function() {
662 opened++;
663 };
664 this.send = function() {};
665 this.abort = function() {
666 aborted = true;
667 this.readyState = 4;
668 this.status = 0;
669 this.onreadystatechange();
670 };
671 };
672 // trigger a segment download request 724 // trigger a segment download request
673 player.trigger('timeupdate'); 725 player.trigger('timeupdate');
674 opened = 0;
675 // attempt to seek while the download is in progress 726 // attempt to seek while the download is in progress
676 player.trigger('seeking'); 727 player.trigger('seeking');
677 728
678 ok(aborted, 'XHR aborted'); 729 ok(requests[1].aborted, 'XHR aborted');
679 strictEqual(1, opened, 'opened new XHR'); 730 strictEqual(requests.length, 3, 'opened new XHR');
680 }); 731 });
681 732
682 test('flushes the parser after each segment', function() { 733 test('flushes the parser after each segment', function() {
...@@ -698,12 +749,13 @@ test('flushes the parser after each segment', function() { ...@@ -698,12 +749,13 @@ test('flushes the parser after each segment', function() {
698 type: 'sourceopen' 749 type: 'sourceopen'
699 }); 750 });
700 751
701 strictEqual(1, flushes, 'tags are flushed at the end of a segment'); 752 standardXHRResponse(requests[0]);
753 standardXHRResponse(requests[1]);
754 strictEqual(flushes, 1, 'tags are flushed at the end of a segment');
702 }); 755 });
703 756
704 test('drops tags before the target timestamp when seeking', function() { 757 test('drops tags before the target timestamp when seeking', function() {
705 var 758 var i = 10,
706 i = 10,
707 callbacks = [], 759 callbacks = [],
708 tags = [], 760 tags = [],
709 bytes = []; 761 bytes = [];
...@@ -714,6 +766,7 @@ test('drops tags before the target timestamp when seeking', function() { ...@@ -714,6 +766,7 @@ test('drops tags before the target timestamp when seeking', function() {
714 this.appendBuffer = function(chunk) { 766 this.appendBuffer = function(chunk) {
715 bytes.push(chunk); 767 bytes.push(chunk);
716 }; 768 };
769 this.abort = function() {};
717 }; 770 };
718 // capture timeouts 771 // capture timeouts
719 window.setTimeout = function(callback) { 772 window.setTimeout = function(callback) {
...@@ -727,6 +780,8 @@ test('drops tags before the target timestamp when seeking', function() { ...@@ -727,6 +780,8 @@ test('drops tags before the target timestamp when seeking', function() {
727 videojs.mediaSources[player.currentSrc()].trigger({ 780 videojs.mediaSources[player.currentSrc()].trigger({
728 type: 'sourceopen' 781 type: 'sourceopen'
729 }); 782 });
783 standardXHRResponse(requests[0]);
784 standardXHRResponse(requests[1]);
730 while (callbacks.length) { 785 while (callbacks.length) {
731 callbacks.shift()(); 786 callbacks.shift()();
732 } 787 }
...@@ -743,6 +798,7 @@ test('drops tags before the target timestamp when seeking', function() { ...@@ -743,6 +798,7 @@ test('drops tags before the target timestamp when seeking', function() {
743 return 7; 798 return 7;
744 }; 799 };
745 player.trigger('seeking'); 800 player.trigger('seeking');
801 standardXHRResponse(requests[2]);
746 802
747 while (callbacks.length) { 803 while (callbacks.length) {
748 callbacks.shift()(); 804 callbacks.shift()();
...@@ -755,13 +811,18 @@ test('clears pending buffer updates when seeking', function() { ...@@ -755,13 +811,18 @@ test('clears pending buffer updates when seeking', function() {
755 var 811 var
756 bytes = [], 812 bytes = [],
757 callbacks = [], 813 callbacks = [],
814 aborts = 0,
758 tags = [{ pts: 0, bytes: 0 }]; 815 tags = [{ pts: 0, bytes: 0 }];
816
759 // mock out the parser and source buffer 817 // mock out the parser and source buffer
760 videojs.hls.SegmentParser = mockSegmentParser(tags); 818 videojs.hls.SegmentParser = mockSegmentParser(tags);
761 window.videojs.SourceBuffer = function() { 819 window.videojs.SourceBuffer = function() {
762 this.appendBuffer = function(chunk) { 820 this.appendBuffer = function(chunk) {
763 bytes.push(chunk); 821 bytes.push(chunk);
764 }; 822 };
823 this.abort = function() {
824 aborts++;
825 };
765 }; 826 };
766 // capture timeouts 827 // capture timeouts
767 window.setTimeout = function(callback) { 828 window.setTimeout = function(callback) {
...@@ -774,18 +835,22 @@ test('clears pending buffer updates when seeking', function() { ...@@ -774,18 +835,22 @@ test('clears pending buffer updates when seeking', function() {
774 type: 'sourceopen' 835 type: 'sourceopen'
775 }); 836 });
776 837
838 standardXHRResponse(requests[0]);
839 standardXHRResponse(requests[1]);
840
777 // seek to 7s 841 // seek to 7s
778 tags.push({ pts: 7000, bytes: 7 }); 842 tags.push({ pts: 7000, bytes: 7 });
779 player.currentTime = function() { 843 player.currentTime = function() {
780 return 7; 844 return 7;
781 }; 845 };
782 player.trigger('seeking'); 846 player.trigger('seeking');
847 standardXHRResponse(requests[2]);
783 848
784 while (callbacks.length) { 849 while (callbacks.length) {
785 callbacks.shift()(); 850 callbacks.shift()();
786 } 851 }
787 852
788 deepEqual(bytes, ['flv', 7], 'tags queued to be appended should be cancelled'); 853 strictEqual(1, aborts, 'aborted pending buffer');
789 }); 854 });
790 855
791 test('playlist 404 should trigger MEDIA_ERR_NETWORK', function() { 856 test('playlist 404 should trigger MEDIA_ERR_NETWORK', function() {
...@@ -820,23 +885,12 @@ test('playlist 404 should trigger MEDIA_ERR_NETWORK', function() { ...@@ -820,23 +885,12 @@ test('playlist 404 should trigger MEDIA_ERR_NETWORK', function() {
820 test('segment 404 should trigger MEDIA_ERR_NETWORK', function () { 885 test('segment 404 should trigger MEDIA_ERR_NETWORK', function () {
821 player.hls('manifest/media.m3u8'); 886 player.hls('manifest/media.m3u8');
822 887
823 player.on('loadedmanifest', function () {
824 window.XMLHttpRequest = function () {
825 this.open = function (method, url) {
826 xhrUrls.push(url);
827 };
828 this.send = function () {
829 this.readyState = 4;
830 this.status = 404;
831 this.onreadystatechange();
832 };
833 };
834 });
835
836 videojs.mediaSources[player.currentSrc()].trigger({ 888 videojs.mediaSources[player.currentSrc()].trigger({
837 type: 'sourceopen' 889 type: 'sourceopen'
838 }); 890 });
839 891
892 standardXHRResponse(requests[0]);
893 requests[1].respond(404);
840 ok(player.hls.error.message, 'an error message is available'); 894 ok(player.hls.error.message, 'an error message is available');
841 equal(2, player.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_NETWORK'); 895 equal(2, player.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_NETWORK');
842 }); 896 });
...@@ -844,23 +898,12 @@ test('segment 404 should trigger MEDIA_ERR_NETWORK', function () { ...@@ -844,23 +898,12 @@ test('segment 404 should trigger MEDIA_ERR_NETWORK', function () {
844 test('segment 500 should trigger MEDIA_ERR_ABORTED', function () { 898 test('segment 500 should trigger MEDIA_ERR_ABORTED', function () {
845 player.hls('manifest/media.m3u8'); 899 player.hls('manifest/media.m3u8');
846 900
847 player.on('loadedmanifest', function () {
848 window.XMLHttpRequest = function () {
849 this.open = function (method, url) {
850 xhrUrls.push(url);
851 };
852 this.send = function () {
853 this.readyState = 4;
854 this.status = 500;
855 this.onreadystatechange();
856 };
857 };
858 });
859
860 videojs.mediaSources[player.currentSrc()].trigger({ 901 videojs.mediaSources[player.currentSrc()].trigger({
861 type: 'sourceopen' 902 type: 'sourceopen'
862 }); 903 });
863 904
905 standardXHRResponse(requests[0]);
906 requests[1].respond(500);
864 ok(player.hls.error.message, 'an error message is available'); 907 ok(player.hls.error.message, 'an error message is available');
865 equal(4, player.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_ABORTED'); 908 equal(4, player.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_ABORTED');
866 }); 909 });
...@@ -883,6 +926,7 @@ test('reloads live playlists', function() { ...@@ -883,6 +926,7 @@ test('reloads live playlists', function() {
883 videojs.mediaSources[player.currentSrc()].trigger({ 926 videojs.mediaSources[player.currentSrc()].trigger({
884 type: 'sourceopen' 927 type: 'sourceopen'
885 }); 928 });
929 standardXHRResponse(requests[0]);
886 930
887 strictEqual(1, callbacks.length, 'refresh was scheduled'); 931 strictEqual(1, callbacks.length, 'refresh was scheduled');
888 strictEqual(player.hls.media.targetDuration * 1000, 932 strictEqual(player.hls.media.targetDuration * 1000,
...@@ -896,7 +940,9 @@ test('duration is Infinity for live playlists', function() { ...@@ -896,7 +940,9 @@ test('duration is Infinity for live playlists', function() {
896 type: 'sourceopen' 940 type: 'sourceopen'
897 }); 941 });
898 942
899 strictEqual(Infinity, player.duration(), 'duration is infinity'); 943 standardXHRResponse(requests[0]);
944
945 strictEqual(player.duration(), Infinity, 'duration is infinity');
900 }); 946 });
901 947
902 test('does not reload playlists with an endlist tag', function() { 948 test('does not reload playlists with an endlist tag', function() {
...@@ -925,19 +971,22 @@ test('reloads a live playlist after half a target duration if it has not ' + ...@@ -925,19 +971,22 @@ test('reloads a live playlist after half a target duration if it has not ' +
925 type: 'sourceopen' 971 type: 'sourceopen'
926 }); 972 });
927 973
974 standardXHRResponse(requests[0]);
975 standardXHRResponse(requests[1]);
928 strictEqual(callbacks.length, 1, 'full-length refresh scheduled'); 976 strictEqual(callbacks.length, 1, 'full-length refresh scheduled');
929 callbacks.pop().callback(); 977 callbacks.pop().callback();
978 standardXHRResponse(requests[2]);
930 979
931 strictEqual(1, callbacks.length, 'half-length refresh was scheduled'); 980 strictEqual(callbacks.length, 1, 'half-length refresh was scheduled');
932 strictEqual(callbacks[0].timeout, 981 strictEqual(callbacks[0].timeout,
933 player.hls.media.targetDuration / 2 * 1000, 982 player.hls.media.targetDuration / 2 * 1000,
934 'waited half a target duration'); 983 'waited half a target duration');
935 }); 984 });
936 985
937 test('merges playlist reloads', function() { 986 test('merges playlist reloads', function() {
938 var 987 var oldPlaylist,
939 oldPlaylist,
940 callback; 988 callback;
989
941 // capture timeouts 990 // capture timeouts
942 window.setTimeout = function(cb) { 991 window.setTimeout = function(cb) {
943 callback = cb; 992 callback = cb;
...@@ -947,9 +996,12 @@ test('merges playlist reloads', function() { ...@@ -947,9 +996,12 @@ test('merges playlist reloads', function() {
947 videojs.mediaSources[player.currentSrc()].trigger({ 996 videojs.mediaSources[player.currentSrc()].trigger({
948 type: 'sourceopen' 997 type: 'sourceopen'
949 }); 998 });
999 standardXHRResponse(requests[0]);
1000 standardXHRResponse(requests[1]);
950 oldPlaylist = player.hls.media; 1001 oldPlaylist = player.hls.media;
951 1002
952 callback(); 1003 callback();
1004 standardXHRResponse(requests[2]);
953 ok(oldPlaylist !== player.hls.media, 'player.hls.media was updated'); 1005 ok(oldPlaylist !== player.hls.media, 'player.hls.media was updated');
954 }); 1006 });
955 1007
...@@ -973,6 +1025,8 @@ test('updates the media index when a playlist reloads', function() { ...@@ -973,6 +1025,8 @@ test('updates the media index when a playlist reloads', function() {
973 type: 'sourceopen' 1025 type: 'sourceopen'
974 }); 1026 });
975 1027
1028 standardXHRResponse(requests[0]);
1029 standardXHRResponse(requests[1]);
976 // play the stream until 2.ts is playing 1030 // play the stream until 2.ts is playing
977 player.hls.mediaIndex = 3; 1031 player.hls.mediaIndex = 3;
978 1032
...@@ -986,6 +1040,7 @@ test('updates the media index when a playlist reloads', function() { ...@@ -986,6 +1040,7 @@ test('updates the media index when a playlist reloads', function() {
986 '#EXTINF:10,\n' + 1040 '#EXTINF:10,\n' +
987 '3.ts\n'; 1041 '3.ts\n';
988 callback(); 1042 callback();
1043 standardXHRResponse(requests[2]);
989 1044
990 strictEqual(player.hls.mediaIndex, 2, 'mediaIndex is updated after the reload'); 1045 strictEqual(player.hls.mediaIndex, 2, 'mediaIndex is updated after the reload');
991 }); 1046 });
...@@ -1058,7 +1113,21 @@ test('does not reload master playlists', function() { ...@@ -1058,7 +1113,21 @@ test('does not reload master playlists', function() {
1058 }); 1113 });
1059 1114
1060 test('only reloads the active media playlist', function() { 1115 test('only reloads the active media playlist', function() {
1061 var callbacks = [], urls = [], responses = []; 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
1062 window.setTimeout = function(callback) { 1131 window.setTimeout = function(callback) {
1063 callbacks.push(callback); 1132 callbacks.push(callback);
1064 }; 1133 };
...@@ -1068,23 +1137,11 @@ test('only reloads the active media playlist', function() { ...@@ -1068,23 +1137,11 @@ test('only reloads the active media playlist', function() {
1068 type: 'sourceopen' 1137 type: 'sourceopen'
1069 }); 1138 });
1070 1139
1071 window.XMLHttpRequest = function() { 1140 standardXHRResponse(requests[0]);
1072 this.open = function(method, url) { 1141 standardXHRResponse(requests[1]);
1073 urls.push(url); 1142
1074 }; 1143 videojs.mediaSources[player.currentSrc()].endOfStream = function() {};
1075 this.send = function() { 1144
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() { 1145 player.hls.selectPlaylist = function() {
1089 return player.hls.master.playlists[1]; 1146 return player.hls.master.playlists[1];
1090 }; 1147 };
...@@ -1094,19 +1151,57 @@ test('only reloads the active media playlist', function() { ...@@ -1094,19 +1151,57 @@ test('only reloads the active media playlist', function() {
1094 1151
1095 player.trigger('timeupdate'); 1152 player.trigger('timeupdate');
1096 strictEqual(callbacks.length, 1, 'a refresh is scheduled'); 1153 strictEqual(callbacks.length, 1, 'a refresh is scheduled');
1097 strictEqual(responses.length, 1, 'segment requested');
1098 1154
1099 responses.shift()(); // segment response 1155 standardXHRResponse(requests[2]); // segment response
1100 responses.shift()(); // loaded switched.m3u8 1156 customResponse(requests[3]); // loaded witched.m3u8
1101 1157
1102 urls = [];
1103 callbacks.shift()(); // out-of-date refresh of missingEndlist.m3u8 1158 callbacks.shift()(); // out-of-date refresh of missingEndlist.m3u8
1104 callbacks.shift()(); // refresh switched.m3u8 1159 callbacks.shift()(); // refresh switched.m3u8
1105 1160
1106 strictEqual(urls.length, 1, 'one refresh was made'); 1161 for (; i < requests.length; i++) {
1107 strictEqual(urls[0], 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,
1108 'http://example.com/switched.m3u8', 1168 'http://example.com/switched.m3u8',
1109 'refreshed the active playlist'); 1169 'refreshed the active playlist');
1170
1171 });
1172
1173 test('if withCredentials option is used, withCredentials is set on the XHR object', function() {
1174 player.hls({
1175 url: 'http://example.com/media.m3u8',
1176 withCredentials: true
1177 });
1178 videojs.mediaSources[player.currentSrc()].trigger({
1179 type: 'sourceopen'
1180 });
1181 ok(requests[0].withCredentials, "with credentials should be set to true if that option is passed in");
1182 });
1183
1184 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');
1194 try {
1195 videojs.mediaSources[player.currentSrc()].trigger({
1196 type: 'sourceopen'
1197 });
1198 customResponse(requests[0]);
1199 } catch(e) {
1200 ok(false, 'an error was thrown');
1201 throw e;
1202 }
1203 ok(true, 'no error was thrown');
1204 strictEqual(requests.length, 1, 'no requests for non-existent segments were queued');
1110 }); 1205 });
1111 1206
1112 })(window, window.videojs); 1207 })(window, window.videojs);
......