Merge branch 'master' into saucelabs-take2
Conflicts: .travis.yml
Showing
15 changed files
with
352 additions
and
304 deletions
.npmignore
0 → 100644
... | @@ -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 | } | ... | ... |
src/async-queue.js
deleted
100644 → 0
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 | ... | ... |
test/async-queue_test.js
deleted
100644 → 0
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", |
... | @@ -93,4 +93,4 @@ module.exports = function(config) { | ... | @@ -93,4 +93,4 @@ module.exports = function(config) { |
93 | // If browser does not capture in given timeout [ms], kill it | 93 | // If browser does not capture in given timeout [ms], kill it |
94 | captureTimeout: 60000 | 94 | captureTimeout: 60000 |
95 | }); | 95 | }); |
96 | }; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
96 | }; | ... | ... |
... | @@ -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,15 +749,16 @@ test('flushes the parser after each segment', function() { | ... | @@ -698,15 +749,16 @@ 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, | 759 | callbacks = [], |
707 | callbacks = [], | 760 | tags = [], |
708 | tags = [], | 761 | bytes = []; |
709 | bytes = []; | ||
710 | 762 | ||
711 | // mock out the parser and source buffer | 763 | // mock out the parser and source buffer |
712 | videojs.hls.SegmentParser = mockSegmentParser(tags); | 764 | videojs.hls.SegmentParser = mockSegmentParser(tags); |
... | @@ -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, | 988 | callback; |
940 | 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); | ... | ... |
-
Please register or sign in to post a comment