50a78066 by David LaPalomento

Merge pull request #62 from videojs/tech2

HLS Tech
2 parents fafd0563 d30df95f
...@@ -45,9 +45,6 @@ module.exports = function(grunt) { ...@@ -45,9 +45,6 @@ module.exports = function(grunt) {
45 dest: 'dist/videojs.hls.min.js' 45 dest: 'dist/videojs.hls.min.js'
46 } 46 }
47 }, 47 },
48 qunit: {
49 files: ['test/**/*.html', '!test/perf.html', '!test/muxer/**']
50 },
51 jshint: { 48 jshint: {
52 gruntfile: { 49 gruntfile: {
53 options: { 50 options: {
...@@ -93,11 +90,11 @@ module.exports = function(grunt) { ...@@ -93,11 +90,11 @@ module.exports = function(grunt) {
93 }, 90 },
94 src: { 91 src: {
95 files: '<%= jshint.src.src %>', 92 files: '<%= jshint.src.src %>',
96 tasks: ['jshint:src', 'qunit'] 93 tasks: ['jshint:src', 'test']
97 }, 94 },
98 test: { 95 test: {
99 files: '<%= jshint.test.src %>', 96 files: '<%= jshint.test.src %>',
100 tasks: ['jshint:test', 'qunit'] 97 tasks: ['jshint:test', 'test']
101 } 98 }
102 }, 99 },
103 concurrent: { 100 concurrent: {
...@@ -194,7 +191,6 @@ module.exports = function(grunt) { ...@@ -194,7 +191,6 @@ module.exports = function(grunt) {
194 grunt.loadNpmTasks('grunt-contrib-clean'); 191 grunt.loadNpmTasks('grunt-contrib-clean');
195 grunt.loadNpmTasks('grunt-contrib-concat'); 192 grunt.loadNpmTasks('grunt-contrib-concat');
196 grunt.loadNpmTasks('grunt-contrib-uglify'); 193 grunt.loadNpmTasks('grunt-contrib-uglify');
197 grunt.loadNpmTasks('grunt-contrib-qunit');
198 grunt.loadNpmTasks('grunt-contrib-jshint'); 194 grunt.loadNpmTasks('grunt-contrib-jshint');
199 grunt.loadNpmTasks('grunt-contrib-watch'); 195 grunt.loadNpmTasks('grunt-contrib-watch');
200 grunt.loadNpmTasks('grunt-contrib-connect'); 196 grunt.loadNpmTasks('grunt-contrib-connect');
...@@ -255,7 +251,7 @@ module.exports = function(grunt) { ...@@ -255,7 +251,7 @@ module.exports = function(grunt) {
255 ['clean', 251 ['clean',
256 'jshint', 252 'jshint',
257 'manifests-to-js', 253 'manifests-to-js',
258 'qunit', 254 'test',
259 'concat', 255 'concat',
260 'uglify']); 256 'uglify']);
261 257
......
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
31 <!-- bipbop --> 31 <!-- bipbop -->
32 <!-- <script src="test/tsSegment.js"></script> --> 32 <!-- <script src="test/tsSegment.js"></script> -->
33 <!-- bunnies --> 33 <!-- bunnies -->
34 <script src="test/tsSegment-bc.js"></script> 34 <!--<script src="test/tsSegment-bc.js"></script>-->
35 35
36 <style> 36 <style>
37 body { 37 body {
...@@ -63,10 +63,12 @@ ...@@ -63,10 +63,12 @@
63 <script> 63 <script>
64 videojs.options.flash.swf = 'node_modules/video.js/dist/video-js/video-js.swf'; 64 videojs.options.flash.swf = 'node_modules/video.js/dist/video-js/video-js.swf';
65 // initialize the player 65 // initialize the player
66 var player = videojs('video'); 66 var player = videojs('video', {
67 techOrder: ['hls']
68 });
67 69
68 // initialize the plugin 70 // initialize the plugin
69 player.hls(); 71 //player.hls()
70 </script> 72 </script>
71 </body> 73 </body>
72 </html> 74 </html>
......
...@@ -19,7 +19,6 @@ ...@@ -19,7 +19,6 @@
19 "grunt-contrib-concat": "~0.3.0", 19 "grunt-contrib-concat": "~0.3.0",
20 "grunt-contrib-connect": "~0.6.0", 20 "grunt-contrib-connect": "~0.6.0",
21 "grunt-contrib-jshint": "~0.6.0", 21 "grunt-contrib-jshint": "~0.6.0",
22 "grunt-contrib-qunit": "~0.2.0",
23 "grunt-contrib-uglify": "~0.2.0", 22 "grunt-contrib-uglify": "~0.2.0",
24 "grunt-contrib-watch": "~0.4.0", 23 "grunt-contrib-watch": "~0.4.0",
25 "grunt-karma": "~0.6.2", 24 "grunt-karma": "~0.6.2",
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
8 8
9 (function(window) { 9 (function(window) {
10 var 10 var
11 FlvTag = window.videojs.hls.FlvTag, 11 FlvTag = window.videojs.Hls.FlvTag,
12 adtsSampleingRates = [ 12 adtsSampleingRates = [
13 96000, 88200, 13 96000, 88200,
14 64000, 48000, 14 64000, 48000,
...@@ -17,7 +17,7 @@ var ...@@ -17,7 +17,7 @@ var
17 16000, 12000 17 16000, 12000
18 ]; 18 ];
19 19
20 window.videojs.hls.AacStream = function() { 20 window.videojs.Hls.AacStream = function() {
21 var 21 var
22 next_pts, // :uint 22 next_pts, // :uint
23 pts_offset, // :int 23 pts_offset, // :int
......
...@@ -29,5 +29,5 @@ ...@@ -29,5 +29,5 @@
29 } 29 }
30 }; 30 };
31 31
32 window.videojs.hls.utils = module; 32 window.videojs.Hls.utils = module;
33 })(this); 33 })(this);
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
4 * Parser for exponential Golomb codes, a variable-bitwidth number encoding 4 * Parser for exponential Golomb codes, a variable-bitwidth number encoding
5 * scheme used by h264. 5 * scheme used by h264.
6 */ 6 */
7 window.videojs.hls.ExpGolomb = function(workingData) { 7 window.videojs.Hls.ExpGolomb = function(workingData) {
8 var 8 var
9 // the number of bytes left to examine in workingData 9 // the number of bytes left to examine in workingData
10 workingBytesAvailable = workingData.byteLength, 10 workingBytesAvailable = workingData.byteLength,
......
1 (function(window) { 1 (function(window) {
2 2
3 window.videojs = window.videojs || {}; 3 window.videojs = window.videojs || {};
4 window.videojs.hls = window.videojs.hls || {}; 4 window.videojs.Hls = window.videojs.Hls || {};
5 5
6 var hls = window.videojs.hls; 6 var hls = window.videojs.Hls;
7 7
8 // (type:uint, extraData:Boolean = false) extends ByteArray 8 // (type:uint, extraData:Boolean = false) extends ByteArray
9 hls.FlvTag = function(type, extraData) { 9 hls.FlvTag = function(type, extraData) {
......
...@@ -8,8 +8,8 @@ ...@@ -8,8 +8,8 @@
8 8
9 (function(window) { 9 (function(window) {
10 var 10 var
11 ExpGolomb = window.videojs.hls.ExpGolomb, 11 ExpGolomb = window.videojs.Hls.ExpGolomb,
12 FlvTag = window.videojs.hls.FlvTag, 12 FlvTag = window.videojs.Hls.FlvTag,
13 13
14 H264ExtraData = function() { 14 H264ExtraData = function() {
15 this.sps = []; // :Array 15 this.sps = []; // :Array
...@@ -234,7 +234,7 @@ ...@@ -234,7 +234,7 @@
234 * an h264 stream. Exactly one byte. 234 * an h264 stream. Exactly one byte.
235 */ 235 */
236 // incomplete, see Table 7.1 of ITU-T H.264 for 12-32 236 // incomplete, see Table 7.1 of ITU-T H.264 for 12-32
237 window.videojs.hls.NALUnitType = NALUnitType = { 237 window.videojs.Hls.NALUnitType = NALUnitType = {
238 unspecified: 0, 238 unspecified: 0,
239 slice_layer_without_partitioning_rbsp_non_idr: 1, 239 slice_layer_without_partitioning_rbsp_non_idr: 1,
240 slice_data_partition_a_layer_rbsp: 2, 240 slice_data_partition_a_layer_rbsp: 2,
...@@ -249,7 +249,7 @@ ...@@ -249,7 +249,7 @@
249 end_of_stream_rbsp: 11 249 end_of_stream_rbsp: 11
250 }; 250 };
251 251
252 window.videojs.hls.H264Stream = function() { 252 window.videojs.Hls.H264Stream = function() {
253 var 253 var
254 next_pts, // :uint; 254 next_pts, // :uint;
255 next_dts, // :uint; 255 next_dts, // :uint;
......
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
32 } 32 }
33 return result; 33 return result;
34 }, 34 },
35 Stream = videojs.hls.Stream, 35 Stream = videojs.Hls.Stream,
36 LineStream, 36 LineStream,
37 ParseStream, 37 ParseStream,
38 Parser; 38 Parser;
......
...@@ -5,8 +5,8 @@ ...@@ -5,8 +5,8 @@
5 (function(window, videojs) { 5 (function(window, videojs) {
6 'use strict'; 6 'use strict';
7 var 7 var
8 resolveUrl = videojs.hls.resolveUrl, 8 resolveUrl = videojs.Hls.resolveUrl,
9 xhr = videojs.hls.xhr, 9 xhr = videojs.Hls.xhr,
10 10
11 /** 11 /**
12 * Returns a new master playlist that is the result of merging an 12 * Returns a new master playlist that is the result of merging an
...@@ -51,6 +51,7 @@ ...@@ -51,6 +51,7 @@
51 var 51 var
52 loader = this, 52 loader = this,
53 media, 53 media,
54 mediaUpdateTimeout,
54 request, 55 request,
55 56
56 haveMetadata = function(error, xhr, url) { 57 haveMetadata = function(error, xhr, url) {
...@@ -88,7 +89,7 @@ ...@@ -88,7 +89,7 @@
88 89
89 // refresh live playlists after a target duration passes 90 // refresh live playlists after a target duration passes
90 if (!loader.media().endList) { 91 if (!loader.media().endList) {
91 window.setTimeout(function() { 92 mediaUpdateTimeout = window.setTimeout(function() {
92 loader.trigger('mediaupdatetimeout'); 93 loader.trigger('mediaupdatetimeout');
93 }, refreshDelay); 94 }, refreshDelay);
94 } 95 }
...@@ -104,6 +105,13 @@ ...@@ -104,6 +105,13 @@
104 105
105 loader.state = 'HAVE_NOTHING'; 106 loader.state = 'HAVE_NOTHING';
106 107
108 loader.dispose = function() {
109 if (request) {
110 request.abort();
111 }
112 window.clearTimeout(mediaUpdateTimeout);
113 };
114
107 /** 115 /**
108 * When called without any arguments, returns the currently 116 * When called without any arguments, returns the currently
109 * active media playlist. When called with a single argument, 117 * active media playlist. When called with a single argument,
...@@ -213,7 +221,9 @@ ...@@ -213,7 +221,9 @@
213 haveMetadata(error, 221 haveMetadata(error,
214 this, 222 this,
215 parser.manifest.playlists[0].uri); 223 parser.manifest.playlists[0].uri);
216 loader.trigger('loadedmetadata'); 224 if (!error) {
225 loader.trigger('loadedmetadata');
226 }
217 }); 227 });
218 return loader.trigger('loadedplaylist'); 228 return loader.trigger('loadedplaylist');
219 } 229 }
...@@ -231,7 +241,7 @@ ...@@ -231,7 +241,7 @@
231 return loader.trigger('loadedmetadata'); 241 return loader.trigger('loadedmetadata');
232 }); 242 });
233 }; 243 };
234 PlaylistLoader.prototype = new videojs.hls.Stream(); 244 PlaylistLoader.prototype = new videojs.Hls.Stream();
235 245
236 videojs.hls.PlaylistLoader = PlaylistLoader; 246 videojs.Hls.PlaylistLoader = PlaylistLoader;
237 })(window, window.videojs); 247 })(window, window.videojs);
......
1 (function(window) { 1 (function(window) {
2 var 2 var
3 videojs = window.videojs, 3 videojs = window.videojs,
4 FlvTag = videojs.hls.FlvTag, 4 FlvTag = videojs.Hls.FlvTag,
5 H264Stream = videojs.hls.H264Stream, 5 H264Stream = videojs.Hls.H264Stream,
6 AacStream = videojs.hls.AacStream, 6 AacStream = videojs.Hls.AacStream,
7 MP2T_PACKET_LENGTH, 7 MP2T_PACKET_LENGTH,
8 STREAM_TYPES; 8 STREAM_TYPES;
9 9
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
11 * An object that incrementally transmuxes MPEG2 Trasport Stream 11 * An object that incrementally transmuxes MPEG2 Trasport Stream
12 * chunks into an FLV. 12 * chunks into an FLV.
13 */ 13 */
14 videojs.hls.SegmentParser = function() { 14 videojs.Hls.SegmentParser = function() {
15 var 15 var
16 self = this, 16 self = this,
17 parseTSPacket, 17 parseTSPacket,
...@@ -432,8 +432,8 @@ ...@@ -432,8 +432,8 @@
432 }; 432 };
433 433
434 // MPEG2-TS constants 434 // MPEG2-TS constants
435 videojs.hls.SegmentParser.MP2T_PACKET_LENGTH = MP2T_PACKET_LENGTH = 188; 435 videojs.Hls.SegmentParser.MP2T_PACKET_LENGTH = MP2T_PACKET_LENGTH = 188;
436 videojs.hls.SegmentParser.STREAM_TYPES = STREAM_TYPES = { 436 videojs.Hls.SegmentParser.STREAM_TYPES = STREAM_TYPES = {
437 h264: 0x1b, 437 h264: 0x1b,
438 adts: 0x0f 438 adts: 0x0f
439 }; 439 };
......
...@@ -65,5 +65,5 @@ ...@@ -65,5 +65,5 @@
65 }); 65 });
66 }; 66 };
67 67
68 videojs.hls.Stream = Stream; 68 videojs.Hls.Stream = Stream;
69 })(window.videojs); 69 })(window.videojs);
......
...@@ -8,28 +8,6 @@ ...@@ -8,28 +8,6 @@
8 8
9 (function(window, videojs, document, undefined) { 9 (function(window, videojs, document, undefined) {
10 10
11 videojs.hls = {
12 /**
13 * Whether the browser has built-in HLS support.
14 */
15 supportsNativeHls: (function() {
16 var
17 video = document.createElement('video'),
18 xMpegUrl,
19 vndMpeg;
20
21 // native HLS is definitely not supported if HTML5 video isn't
22 if (!videojs.Html5.isSupported()) {
23 return false;
24 }
25
26 xMpegUrl = video.canPlayType('application/x-mpegURL');
27 vndMpeg = video.canPlayType('application/vnd.apple.mpegURL');
28 return (/probably|maybe/).test(xMpegUrl) ||
29 (/probably|maybe/).test(vndMpeg);
30 })()
31 };
32
33 var 11 var
34 12
35 // the desired length of video to maintain in the buffer, in seconds 13 // the desired length of video to maintain in the buffer, in seconds
...@@ -95,60 +73,7 @@ var ...@@ -95,60 +73,7 @@ var
95 } 73 }
96 }, 74 },
97 75
98 /** 76 xhr,
99 * Creates and sends an XMLHttpRequest.
100 * @param options {string | object} if this argument is a string, it
101 * is intrepreted as a URL and a simple GET request is
102 * inititated. If it is an object, it should contain a `url`
103 * property that indicates the URL to request and optionally a
104 * `method` which is the type of HTTP request to send.
105 * @param callback (optional) {function} a function to call when the
106 * request completes. If the request was not successful, the first
107 * argument will be falsey.
108 * @return {object} the XMLHttpRequest that was initiated.
109 */
110 xhr = videojs.hls.xhr = function(url, callback) {
111 var
112 options = {
113 method: 'GET'
114 },
115 request;
116
117 if (typeof callback !== 'function') {
118 callback = function() {};
119 }
120
121 if (typeof url === 'object') {
122 options = videojs.util.mergeOptions(options, url);
123 url = options.url;
124 }
125
126 request = new window.XMLHttpRequest();
127 request.open(options.method, url);
128
129 if (options.responseType) {
130 request.responseType = options.responseType;
131 }
132 if (options.withCredentials) {
133 request.withCredentials = true;
134 }
135
136 request.onreadystatechange = function() {
137 // wait until the request completes
138 if (this.readyState !== 4) {
139 return;
140 }
141
142 // request error
143 if (this.status >= 400 || this.status === 0) {
144 return callback.call(this, true, url);
145 }
146
147 return callback.call(this, false, url);
148 };
149 request.send(null);
150 return request;
151 },
152 77
153 /** 78 /**
154 * TODO - Document this great feature. 79 * TODO - Document this great feature.
...@@ -228,7 +153,13 @@ var ...@@ -228,7 +153,13 @@ var
228 var 153 var
229 duration = 0, 154 duration = 0,
230 segment, 155 segment,
231 i = (playlist.segments || []).length; 156 i;
157
158 if (!playlist) {
159 return 0;
160 }
161
162 i = (playlist.segments || []).length;
232 163
233 // if present, use the duration specified in the playlist 164 // if present, use the duration specified in the playlist
234 if (playlist.totalDuration) { 165 if (playlist.totalDuration) {
...@@ -247,134 +178,17 @@ var ...@@ -247,134 +178,17 @@ var
247 return duration; 178 return duration;
248 }, 179 },
249 180
250 /** 181 resolveUrl,
251 * Constructs a new URI by interpreting a path relative to another
252 * URI.
253 * @param basePath {string} a relative or absolute URI
254 * @param path {string} a path part to combine with the base
255 * @return {string} a URI that is equivalent to composing `base`
256 * with `path`
257 * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue
258 */
259 resolveUrl = videojs.hls.resolveUrl = function(basePath, path) {
260 // use the base element to get the browser to handle URI resolution
261 var
262 oldBase = document.querySelector('base'),
263 docHead = document.querySelector('head'),
264 a = document.createElement('a'),
265 base = oldBase,
266 oldHref,
267 result;
268
269 // prep the document
270 if (oldBase) {
271 oldHref = oldBase.href;
272 } else {
273 base = docHead.appendChild(document.createElement('base'));
274 }
275
276 base.href = basePath;
277 a.href = path;
278 result = a.href;
279 182
280 // clean up 183 initSource = function(player, mediaSource, srcUrl) {
281 if (oldBase) {
282 oldBase.href = oldHref;
283 } else {
284 docHead.removeChild(base);
285 }
286 return result;
287 },
288
289 /**
290 * Initializes the HLS plugin.
291 * @param options {mixed} the URL to an HLS playlist
292 */
293 init = function(options) {
294 var 184 var
295 mediaSource = new videojs.MediaSource(), 185 segmentParser = new videojs.Hls.SegmentParser(),
296 segmentParser = new videojs.hls.SegmentParser(),
297 player = this,
298 srcUrl,
299 186
300 segmentXhr, 187 segmentXhr,
301 settings, 188 settings = videojs.util.mergeOptions({}, player.options().hls),
302 fillBuffer, 189 fillBuffer,
303 updateDuration; 190 updateDuration;
304 191
305 // if the video element supports HLS natively, do nothing
306 if (videojs.hls.supportsNativeHls) {
307 return;
308 }
309
310 settings = videojs.util.mergeOptions({}, options);
311
312 srcUrl = (function() {
313 var
314 extname,
315 i = 0,
316 j = 0,
317 src = player.el().querySelector('.vjs-tech').src,
318 sources = player.options().sources,
319 techName,
320 length = sources.length;
321
322 // use the URL specified in options if one was provided
323 if (typeof options === 'string') {
324 return options;
325 } else if (options && options.url) {
326 return options.url;
327 }
328
329 // src attributes take precedence over source children
330 if (src) {
331
332 // assume files with the m3u8 extension are HLS
333 extname = (/[^#?]*(?:\/[^#?]*\.([^#?]*))/).exec(src);
334 if (extname && extname[1] === 'm3u8') {
335 return src;
336 }
337 return;
338 }
339
340 // find the first playable source
341 for (; i < length; i++) {
342
343 // ignore sources without a specified type
344 if (!sources[i].type) {
345 continue;
346 }
347
348 // do nothing if the source is handled by one of the standard techs
349 for (j in player.options().techOrder) {
350 techName = player.options().techOrder[j];
351 techName = techName[0].toUpperCase() + techName.substring(1);
352 if (videojs[techName].canPlaySource({ type: sources[i].type })) {
353 return;
354 }
355 }
356
357 // use the plugin if the MIME type specifies HLS
358 if ((/application\/x-mpegURL/).test(sources[i].type) ||
359 (/application\/vnd\.apple\.mpegURL/).test(sources[i].type)) {
360 return sources[i].src;
361 }
362 }
363 })();
364
365 if (!srcUrl) {
366 // do nothing until the plugin is initialized with a valid URL
367 videojs.log('hls: no valid playlist URL specified');
368 return;
369 }
370
371 // expose the HLS plugin state
372 player.hls.readyState = function() {
373 if (!player.hls.media) {
374 return 0; // HAVE_NOTHING
375 }
376 return 1; // HAVE_METADATA
377 };
378 192
379 player.on('seeking', function() { 193 player.on('seeking', function() {
380 var currentTime = player.currentTime(); 194 var currentTime = player.currentTime();
...@@ -397,16 +211,8 @@ var ...@@ -397,16 +211,8 @@ var
397 * Update the player duration 211 * Update the player duration
398 */ 212 */
399 updateDuration = function(playlist) { 213 updateDuration = function(playlist) {
400 var tech;
401 // update the duration 214 // update the duration
402 player.duration(totalDuration(playlist)); 215 player.duration(totalDuration(playlist));
403 // tell the flash tech of the new duration
404 tech = player.el().querySelector('.vjs-tech');
405 if(tech.vjs_setProperty) {
406 tech.vjs_setProperty('duration', player.duration());
407 }
408 // manually fire the duration change
409 player.trigger('durationchange');
410 }; 216 };
411 217
412 /** 218 /**
...@@ -502,7 +308,8 @@ var ...@@ -502,7 +308,8 @@ var
502 } 308 }
503 309
504 // if no segments are available, do nothing 310 // if no segments are available, do nothing
505 if (!player.hls.playlists.media().segments) { 311 if (player.hls.playlists.state === "HAVE_NOTHING" ||
312 !player.hls.playlists.media().segments) {
506 return; 313 return;
507 } 314 }
508 315
...@@ -609,7 +416,7 @@ var ...@@ -609,7 +416,7 @@ var
609 416
610 player.hls.mediaIndex = 0; 417 player.hls.mediaIndex = 0;
611 player.hls.playlists = 418 player.hls.playlists =
612 new videojs.hls.PlaylistLoader(srcUrl, settings.withCredentials); 419 new videojs.Hls.PlaylistLoader(srcUrl, settings.withCredentials);
613 player.hls.playlists.on('loadedmetadata', function() { 420 player.hls.playlists.on('loadedmetadata', function() {
614 oldMediaPlaylist = player.hls.playlists.media(); 421 oldMediaPlaylist = player.hls.playlists.media();
615 422
...@@ -638,28 +445,160 @@ var ...@@ -638,28 +445,160 @@ var
638 oldMediaPlaylist = updatedPlaylist; 445 oldMediaPlaylist = updatedPlaylist;
639 }); 446 });
640 }); 447 });
641 player.src([{ 448 };
449
450 var mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i;
451
452 videojs.Hls = videojs.Flash.extend({
453 init: function(player, options, ready) {
454 var
455 source = options.source,
456 settings = player.options();
457
458 player.hls = this;
459 delete options.source;
460 options.swf = settings.flash.swf;
461 videojs.Flash.call(this, player, options, ready);
462 options.source = source;
463 videojs.Hls.prototype.src.call(this, options.source && options.source.src);
464 }
465 });
466
467 videojs.Hls.prototype.src = function(src) {
468 var
469 player = this.player(),
470 mediaSource,
471 source;
472
473 if (src) {
474 mediaSource = new videojs.MediaSource();
475 source = {
642 src: videojs.URL.createObjectURL(mediaSource), 476 src: videojs.URL.createObjectURL(mediaSource),
643 type: "video/flv" 477 type: "video/flv"
644 }]); 478 };
479 this.mediaSource = mediaSource;
480 initSource(player, mediaSource, src);
481 this.ready(function() {
482 this.el().vjs_src(source.src);
483 });
484 }
485 };
486
487 videojs.Hls.prototype.duration = function() {
488 var playlists = this.playlists;
489 if (playlists) {
490 return totalDuration(playlists.media());
491 }
492 return 0;
493 };
645 494
646 if (player.options().autoplay) { 495 videojs.Hls.prototype.dispose = function() {
647 player.play(); 496 if (this.playlists) {
497 this.playlists.dispose();
498 }
499 videojs.Flash.prototype.dispose.call(this);
500 };
501
502 videojs.Hls.isSupported = function() {
503 return videojs.Flash.isSupported() && videojs.MediaSource;
504 };
505
506 videojs.Hls.canPlaySource = function(srcObj) {
507 return mpegurlRE.test(srcObj.type) || videojs.Flash.canPlaySource.call(this, srcObj);
508 };
509
510 /**
511 * Creates and sends an XMLHttpRequest.
512 * @param options {string | object} if this argument is a string, it
513 * is intrepreted as a URL and a simple GET request is
514 * inititated. If it is an object, it should contain a `url`
515 * property that indicates the URL to request and optionally a
516 * `method` which is the type of HTTP request to send.
517 * @param callback (optional) {function} a function to call when the
518 * request completes. If the request was not successful, the first
519 * argument will be falsey.
520 * @return {object} the XMLHttpRequest that was initiated.
521 */
522 xhr = videojs.Hls.xhr = function(url, callback) {
523 var
524 options = {
525 method: 'GET'
526 },
527 request;
528
529 if (typeof callback !== 'function') {
530 callback = function() {};
531 }
532
533 if (typeof url === 'object') {
534 options = videojs.util.mergeOptions(options, url);
535 url = options.url;
536 }
537
538 request = new window.XMLHttpRequest();
539 request.open(options.method, url);
540
541 if (options.responseType) {
542 request.responseType = options.responseType;
543 }
544 if (options.withCredentials) {
545 request.withCredentials = true;
546 }
547
548 request.onreadystatechange = function() {
549 // wait until the request completes
550 if (this.readyState !== 4) {
551 return;
648 } 552 }
553
554 // request error
555 if (this.status >= 400 || this.status === 0) {
556 return callback.call(this, true, url);
557 }
558
559 return callback.call(this, false, url);
649 }; 560 };
561 request.send(null);
562 return request;
563 };
650 564
651 videojs.plugin('hls', function() { 565 /**
652 if (typeof Uint8Array === 'undefined') { 566 * Constructs a new URI by interpreting a path relative to another
653 return; 567 * URI.
568 * @param basePath {string} a relative or absolute URI
569 * @param path {string} a path part to combine with the base
570 * @return {string} a URI that is equivalent to composing `base`
571 * with `path`
572 * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue
573 */
574 resolveUrl = videojs.Hls.resolveUrl = function(basePath, path) {
575 // use the base element to get the browser to handle URI resolution
576 var
577 oldBase = document.querySelector('base'),
578 docHead = document.querySelector('head'),
579 a = document.createElement('a'),
580 base = oldBase,
581 oldHref,
582 result;
583
584 // prep the document
585 if (oldBase) {
586 oldHref = oldBase.href;
587 } else {
588 base = docHead.appendChild(document.createElement('base'));
654 } 589 }
655 590
656 var initialize = function() { 591 base.href = basePath;
657 return function() { 592 a.href = path;
658 this.hls = initialize(); 593 result = a.href;
659 init.apply(this, arguments); 594
660 }; 595 // clean up
661 }; 596 if (oldBase) {
662 initialize().apply(this, arguments); 597 oldBase.href = oldHref;
663 }); 598 } else {
599 docHead.removeChild(base);
600 }
601 return result;
602 };
664 603
665 })(window, window.videojs, document); 604 })(window, window.videojs, document);
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
21 */ 21 */
22 var 22 var
23 buffer, 23 buffer,
24 ExpGolomb = window.videojs.hls.ExpGolomb, 24 ExpGolomb = window.videojs.Hls.ExpGolomb,
25 expGolomb; 25 expGolomb;
26 26
27 module('Exponential Golomb coding'); 27 module('Exponential Golomb coding');
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
19 notStrictEqual(actual, expected, [message]) 19 notStrictEqual(actual, expected, [message])
20 throws(block, [expected], [message]) 20 throws(block, [expected], [message])
21 */ 21 */
22 var FlvTag = window.videojs.hls.FlvTag; 22 var FlvTag = window.videojs.Hls.FlvTag;
23 23
24 module('FLV tag'); 24 module('FLV tag');
25 25
......
...@@ -2,12 +2,12 @@ ...@@ -2,12 +2,12 @@
2 module('H264 Stream'); 2 module('H264 Stream');
3 3
4 var 4 var
5 nalUnitTypes = window.videojs.hls.NALUnitType, 5 nalUnitTypes = window.videojs.Hls.NALUnitType,
6 FlvTag = window.videojs.hls.FlvTag; 6 FlvTag = window.videojs.Hls.FlvTag;
7 7
8 test('metadata is generated for IDRs after a full NAL unit is written', function() { 8 test('metadata is generated for IDRs after a full NAL unit is written', function() {
9 var 9 var
10 h264Stream = new videojs.hls.H264Stream(), 10 h264Stream = new videojs.Hls.H264Stream(),
11 accessUnitDelimiter = new Uint8Array([ 11 accessUnitDelimiter = new Uint8Array([
12 0x00, 12 0x00,
13 0x00, 13 0x00,
...@@ -62,7 +62,7 @@ test('metadata is generated for IDRs after a full NAL unit is written', function ...@@ -62,7 +62,7 @@ test('metadata is generated for IDRs after a full NAL unit is written', function
62 62
63 test('starting PTS values can be negative', function() { 63 test('starting PTS values can be negative', function() {
64 var 64 var
65 h264Stream = new videojs.hls.H264Stream(), 65 h264Stream = new videojs.Hls.H264Stream(),
66 accessUnitDelimiter = new Uint8Array([ 66 accessUnitDelimiter = new Uint8Array([
67 0x00, 67 0x00,
68 0x00, 68 0x00,
......
...@@ -127,7 +127,7 @@ ...@@ -127,7 +127,7 @@
127 original.addEventListener('change', function() { 127 original.addEventListener('change', function() {
128 var reader = new FileReader(); 128 var reader = new FileReader();
129 reader.addEventListener('loadend', function() { 129 reader.addEventListener('loadend', function() {
130 var parser = new videojs.hls.SegmentParser(), 130 var parser = new videojs.Hls.SegmentParser(),
131 tags = [parser.getFlvHeader()], 131 tags = [parser.getFlvHeader()],
132 tag, 132 tag,
133 hex, 133 hex,
...@@ -164,7 +164,7 @@ ...@@ -164,7 +164,7 @@
164 } 164 }
165 165
166 hex = '<pre>' 166 hex = '<pre>'
167 hex += videojs.hls.utils.hexDump(data); 167 hex += videojs.Hls.utils.hexDump(data);
168 hex += '</pre>' 168 hex += '</pre>'
169 169
170 vjsOutput.innerHTML = hex; 170 vjsOutput.innerHTML = hex;
...@@ -201,7 +201,7 @@ ...@@ -201,7 +201,7 @@
201 } 201 }
202 202
203 // output the hex dump 203 // output the hex dump
204 hex += videojs.hls.utils.hexDump(bytes); 204 hex += videojs.Hls.utils.hexDump(bytes);
205 hex += '</pre>'; 205 hex += '</pre>';
206 workingOutput.innerHTML = hex; 206 workingOutput.innerHTML = hex;
207 }); 207 });
......
...@@ -36,26 +36,26 @@ ...@@ -36,26 +36,26 @@
36 36
37 test('throws if the playlist url is empty or undefined', function() { 37 test('throws if the playlist url is empty or undefined', function() {
38 throws(function() { 38 throws(function() {
39 videojs.hls.PlaylistLoader(); 39 videojs.Hls.PlaylistLoader();
40 }, 'requires an argument'); 40 }, 'requires an argument');
41 throws(function() { 41 throws(function() {
42 videojs.hls.PlaylistLoader(''); 42 videojs.Hls.PlaylistLoader('');
43 }, 'does not accept the empty string'); 43 }, 'does not accept the empty string');
44 }); 44 });
45 45
46 test('starts without any metadata', function() { 46 test('starts without any metadata', function() {
47 var loader = new videojs.hls.PlaylistLoader('master.m3u8'); 47 var loader = new videojs.Hls.PlaylistLoader('master.m3u8');
48 strictEqual(loader.state, 'HAVE_NOTHING', 'no metadata has loaded yet'); 48 strictEqual(loader.state, 'HAVE_NOTHING', 'no metadata has loaded yet');
49 }); 49 });
50 50
51 test('requests the initial playlist immediately', function() { 51 test('requests the initial playlist immediately', function() {
52 new videojs.hls.PlaylistLoader('master.m3u8'); 52 new videojs.Hls.PlaylistLoader('master.m3u8');
53 strictEqual(requests.length, 1, 'made a request'); 53 strictEqual(requests.length, 1, 'made a request');
54 strictEqual(requests[0].url, 'master.m3u8', 'requested the initial playlist'); 54 strictEqual(requests[0].url, 'master.m3u8', 'requested the initial playlist');
55 }); 55 });
56 56
57 test('moves to HAVE_MASTER after loading a master playlist', function() { 57 test('moves to HAVE_MASTER after loading a master playlist', function() {
58 var loader = new videojs.hls.PlaylistLoader('master.m3u8'); 58 var loader = new videojs.Hls.PlaylistLoader('master.m3u8');
59 requests.pop().respond(200, null, 59 requests.pop().respond(200, null,
60 '#EXTM3U\n' + 60 '#EXTM3U\n' +
61 '#EXT-X-STREAM-INF:\n' + 61 '#EXT-X-STREAM-INF:\n' +
...@@ -67,7 +67,7 @@ ...@@ -67,7 +67,7 @@
67 test('jumps to HAVE_METADATA when initialized with a media playlist', function() { 67 test('jumps to HAVE_METADATA when initialized with a media playlist', function() {
68 var 68 var
69 loadedmetadatas = 0, 69 loadedmetadatas = 0,
70 loader = new videojs.hls.PlaylistLoader('media.m3u8'); 70 loader = new videojs.Hls.PlaylistLoader('media.m3u8');
71 loader.on('loadedmetadata', function() { 71 loader.on('loadedmetadata', function() {
72 loadedmetadatas++; 72 loadedmetadatas++;
73 }); 73 });
...@@ -85,7 +85,7 @@ ...@@ -85,7 +85,7 @@
85 }); 85 });
86 86
87 test('jumps to HAVE_METADATA when initialized with a live media playlist', function() { 87 test('jumps to HAVE_METADATA when initialized with a live media playlist', function() {
88 var loader = new videojs.hls.PlaylistLoader('media.m3u8'); 88 var loader = new videojs.Hls.PlaylistLoader('media.m3u8');
89 requests.pop().respond(200, null, 89 requests.pop().respond(200, null,
90 '#EXTM3U\n' + 90 '#EXTM3U\n' +
91 '#EXTINF:10,\n' + 91 '#EXTINF:10,\n' +
...@@ -99,7 +99,7 @@ ...@@ -99,7 +99,7 @@
99 var 99 var
100 loadedPlaylist = 0, 100 loadedPlaylist = 0,
101 loadedMetadata = 0, 101 loadedMetadata = 0,
102 loader = new videojs.hls.PlaylistLoader('master.m3u8'); 102 loader = new videojs.Hls.PlaylistLoader('master.m3u8');
103 loader.on('loadedplaylist', function() { 103 loader.on('loadedplaylist', function() {
104 loadedPlaylist++; 104 loadedPlaylist++;
105 }); 105 });
...@@ -131,7 +131,7 @@ ...@@ -131,7 +131,7 @@
131 }); 131 });
132 132
133 test('moves to HAVE_CURRENT_METADATA when refreshing the playlist', function() { 133 test('moves to HAVE_CURRENT_METADATA when refreshing the playlist', function() {
134 var loader = new videojs.hls.PlaylistLoader('live.m3u8'); 134 var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
135 requests.pop().respond(200, null, 135 requests.pop().respond(200, null,
136 '#EXTM3U\n' + 136 '#EXTM3U\n' +
137 '#EXTINF:10,\n' + 137 '#EXTINF:10,\n' +
...@@ -145,7 +145,7 @@ ...@@ -145,7 +145,7 @@
145 }); 145 });
146 146
147 test('returns to HAVE_METADATA after refreshing the playlist', function() { 147 test('returns to HAVE_METADATA after refreshing the playlist', function() {
148 var loader = new videojs.hls.PlaylistLoader('live.m3u8'); 148 var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
149 requests.pop().respond(200, null, 149 requests.pop().respond(200, null,
150 '#EXTM3U\n' + 150 '#EXTM3U\n' +
151 '#EXTINF:10,\n' + 151 '#EXTINF:10,\n' +
...@@ -161,7 +161,7 @@ ...@@ -161,7 +161,7 @@
161 test('emits an error when an initial playlist request fails', function() { 161 test('emits an error when an initial playlist request fails', function() {
162 var 162 var
163 errors = [], 163 errors = [],
164 loader = new videojs.hls.PlaylistLoader('master.m3u8'); 164 loader = new videojs.Hls.PlaylistLoader('master.m3u8');
165 165
166 loader.on('error', function() { 166 loader.on('error', function() {
167 errors.push(loader.error); 167 errors.push(loader.error);
...@@ -175,7 +175,7 @@ ...@@ -175,7 +175,7 @@
175 test('errors when an initial media playlist request fails', function() { 175 test('errors when an initial media playlist request fails', function() {
176 var 176 var
177 errors = [], 177 errors = [],
178 loader = new videojs.hls.PlaylistLoader('master.m3u8'); 178 loader = new videojs.Hls.PlaylistLoader('master.m3u8');
179 179
180 loader.on('error', function() { 180 loader.on('error', function() {
181 errors.push(loader.error); 181 errors.push(loader.error);
...@@ -197,7 +197,7 @@ ...@@ -197,7 +197,7 @@
197 // http://tools.ietf.org/html/draft-pantos-http-live-streaming-12#section-6.3.4 197 // http://tools.ietf.org/html/draft-pantos-http-live-streaming-12#section-6.3.4
198 test('halves the refresh timeout if a playlist is unchanged' + 198 test('halves the refresh timeout if a playlist is unchanged' +
199 'since the last reload', function() { 199 'since the last reload', function() {
200 new videojs.hls.PlaylistLoader('live.m3u8'); 200 new videojs.Hls.PlaylistLoader('live.m3u8');
201 requests.pop().respond(200, null, 201 requests.pop().respond(200, null,
202 '#EXTM3U\n' + 202 '#EXTM3U\n' +
203 '#EXT-X-MEDIA-SEQUENCE:0\n' + 203 '#EXT-X-MEDIA-SEQUENCE:0\n' +
...@@ -218,7 +218,7 @@ ...@@ -218,7 +218,7 @@
218 }); 218 });
219 219
220 test('media-sequence updates are considered a playlist change', function() { 220 test('media-sequence updates are considered a playlist change', function() {
221 new videojs.hls.PlaylistLoader('live.m3u8'); 221 new videojs.Hls.PlaylistLoader('live.m3u8');
222 requests.pop().respond(200, null, 222 requests.pop().respond(200, null,
223 '#EXTM3U\n' + 223 '#EXTM3U\n' +
224 '#EXT-X-MEDIA-SEQUENCE:0\n' + 224 '#EXT-X-MEDIA-SEQUENCE:0\n' +
...@@ -238,7 +238,7 @@ ...@@ -238,7 +238,7 @@
238 test('emits an error if a media refresh fails', function() { 238 test('emits an error if a media refresh fails', function() {
239 var 239 var
240 errors = 0, 240 errors = 0,
241 loader = new videojs.hls.PlaylistLoader('live.m3u8'); 241 loader = new videojs.Hls.PlaylistLoader('live.m3u8');
242 242
243 loader.on('error', function() { 243 loader.on('error', function() {
244 errors++; 244 errors++;
...@@ -256,7 +256,7 @@ ...@@ -256,7 +256,7 @@
256 }); 256 });
257 257
258 test('switches media playlists when requested', function() { 258 test('switches media playlists when requested', function() {
259 var loader = new videojs.hls.PlaylistLoader('master.m3u8'); 259 var loader = new videojs.Hls.PlaylistLoader('master.m3u8');
260 requests.pop().respond(200, null, 260 requests.pop().respond(200, null,
261 '#EXTM3U\n' + 261 '#EXTM3U\n' +
262 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + 262 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
...@@ -284,7 +284,7 @@ ...@@ -284,7 +284,7 @@
284 }); 284 });
285 285
286 test('can switch media playlists based on URI', function() { 286 test('can switch media playlists based on URI', function() {
287 var loader = new videojs.hls.PlaylistLoader('master.m3u8'); 287 var loader = new videojs.Hls.PlaylistLoader('master.m3u8');
288 requests.pop().respond(200, null, 288 requests.pop().respond(200, null,
289 '#EXTM3U\n' + 289 '#EXTM3U\n' +
290 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + 290 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
...@@ -312,7 +312,7 @@ ...@@ -312,7 +312,7 @@
312 }); 312 });
313 313
314 test('aborts in-flight playlist refreshes when switching', function() { 314 test('aborts in-flight playlist refreshes when switching', function() {
315 var loader = new videojs.hls.PlaylistLoader('master.m3u8'); 315 var loader = new videojs.Hls.PlaylistLoader('master.m3u8');
316 requests.pop().respond(200, null, 316 requests.pop().respond(200, null,
317 '#EXTM3U\n' + 317 '#EXTM3U\n' +
318 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + 318 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
...@@ -331,7 +331,7 @@ ...@@ -331,7 +331,7 @@
331 }); 331 });
332 332
333 test('switching to the active playlist is a no-op', function() { 333 test('switching to the active playlist is a no-op', function() {
334 var loader = new videojs.hls.PlaylistLoader('master.m3u8'); 334 var loader = new videojs.Hls.PlaylistLoader('master.m3u8');
335 requests.pop().respond(200, null, 335 requests.pop().respond(200, null,
336 '#EXTM3U\n' + 336 '#EXTM3U\n' +
337 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + 337 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
...@@ -350,7 +350,7 @@ ...@@ -350,7 +350,7 @@
350 }); 350 });
351 351
352 test('throws an error if a media switch is initiated too early', function() { 352 test('throws an error if a media switch is initiated too early', function() {
353 var loader = new videojs.hls.PlaylistLoader('master.m3u8'); 353 var loader = new videojs.Hls.PlaylistLoader('master.m3u8');
354 354
355 throws(function() { 355 throws(function() {
356 loader.media('high.m3u8'); 356 loader.media('high.m3u8');
...@@ -368,7 +368,7 @@ ...@@ -368,7 +368,7 @@
368 }); 368 });
369 369
370 test('throws an error if a switch to an unrecognized playlist is requested', function() { 370 test('throws an error if a switch to an unrecognized playlist is requested', function() {
371 var loader = new videojs.hls.PlaylistLoader('master.m3u8'); 371 var loader = new videojs.Hls.PlaylistLoader('master.m3u8');
372 requests.pop().respond(200, null, 372 requests.pop().respond(200, null,
373 '#EXTM3U\n' + 373 '#EXTM3U\n' +
374 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + 374 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
...@@ -378,4 +378,31 @@ ...@@ -378,4 +378,31 @@
378 loader.media('unrecognized.m3u8'); 378 loader.media('unrecognized.m3u8');
379 }, 'throws an error'); 379 }, 'throws an error');
380 }); 380 });
381
382 test('dispose cancels the refresh timeout', function() {
383 var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
384 requests.pop().respond(200, null,
385 '#EXTM3U\n' +
386 '#EXT-X-MEDIA-SEQUENCE:0\n' +
387 '#EXTINF:10,\n' +
388 '0.ts\n');
389 loader.dispose();
390 // a lot of time passes...
391 clock.tick(15 * 1000);
392
393 strictEqual(requests.length, 0, 'no refresh request was made');
394 });
395
396 test('dispose aborts pending refresh requests', function() {
397 var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
398 requests.pop().respond(200, null,
399 '#EXTM3U\n' +
400 '#EXT-X-MEDIA-SEQUENCE:0\n' +
401 '#EXTINF:10,\n' +
402 '0.ts\n');
403 clock.tick(10 * 1000);
404
405 loader.dispose();
406 ok(requests[0].aborted, 'refresh request aborted');
407 });
381 })(window); 408 })(window);
......
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
39 39
40 module('segment parser', { 40 module('segment parser', {
41 setup: function() { 41 setup: function() {
42 parser = new window.videojs.hls.SegmentParser(); 42 parser = new window.videojs.Hls.SegmentParser();
43 } 43 }
44 }); 44 });
45 45
...@@ -168,11 +168,11 @@ ...@@ -168,11 +168,11 @@
168 result = result.concat(makePsi(settings)); 168 result = result.concat(makePsi(settings));
169 169
170 // ensure the resulting packet is the correct size 170 // ensure the resulting packet is the correct size
171 result.length = window.videojs.hls.SegmentParser.MP2T_PACKET_LENGTH; 171 result.length = window.videojs.Hls.SegmentParser.MP2T_PACKET_LENGTH;
172 return result; 172 return result;
173 }, 173 },
174 h264Type = window.videojs.hls.SegmentParser.STREAM_TYPES.h264, 174 h264Type = window.videojs.Hls.SegmentParser.STREAM_TYPES.h264,
175 adtsType = window.videojs.hls.SegmentParser.STREAM_TYPES.adts; 175 adtsType = window.videojs.Hls.SegmentParser.STREAM_TYPES.adts;
176 176
177 parser.parseSegmentBinaryData(new Uint8Array(makePacket({ 177 parser.parseSegmentBinaryData(new Uint8Array(makePacket({
178 programs: { 178 programs: {
......
1 (function(window, videojs, undefined) { 1 (function(window, videojs, undefined) {
2 'use strict';
2 /* 3 /*
3 ======== A Handy Little QUnit Reference ======== 4 ======== A Handy Little QUnit Reference ========
4 http://api.qunitjs.com/ 5 http://api.qunitjs.com/
...@@ -22,15 +23,37 @@ ...@@ -22,15 +23,37 @@
22 23
23 var 24 var
24 player, 25 player,
25 oldFlashSupported, 26 oldMediaSourceOpen,
26 oldSegmentParser, 27 oldSegmentParser,
27 oldSetTimeout, 28 oldSetTimeout,
28 oldSourceBuffer, 29 oldSourceBuffer,
29 oldSupportsNativeHls, 30 oldFlashSupported,
30 xhrUrls,
31 requests, 31 requests,
32 xhr, 32 xhr,
33 33
34 createPlayer = function(options) {
35 var tech, video, player;
36 video = document.createElement('video');
37 document.querySelector('#qunit-fixture').appendChild(video);
38 player = videojs(video, {
39 flash: {
40 swf: ''
41 },
42 techOrder: ['hls'],
43 hls: options || {}
44 });
45
46 player.buffered = function() {
47 return videojs.createTimeRange(0, 0);
48 };
49
50 tech = player.el().querySelector('.vjs-tech');
51 tech.vjs_getProperty = function() {};
52 tech.vjs_src = function() {};
53 videojs.Flash.onReady(tech.id);
54
55 return player;
56 },
34 standardXHRResponse = function(request) { 57 standardXHRResponse = function(request) {
35 if (!request.url) { 58 if (!request.url) {
36 return; 59 return;
...@@ -82,37 +105,23 @@ var ...@@ -82,37 +105,23 @@ var
82 105
83 module('HLS', { 106 module('HLS', {
84 setup: function() { 107 setup: function() {
108 oldMediaSourceOpen = videojs.MediaSource.open;
109 videojs.MediaSource.open = function() {};
85 110
86 // mock out Flash features for phantomjs 111 // mock out Flash features for phantomjs
87 oldFlashSupported = videojs.Flash.isSupported; 112 oldFlashSupported = videojs.Flash.isSupported;
88 videojs.Flash.isSupported = function() { 113 videojs.Flash.isSupported = function() {
89 return true; 114 return true;
90 }; 115 };
116
91 oldSourceBuffer = window.videojs.SourceBuffer; 117 oldSourceBuffer = window.videojs.SourceBuffer;
92 window.videojs.SourceBuffer = function() { 118 window.videojs.SourceBuffer = function() {
93 this.appendBuffer = function() {}; 119 this.appendBuffer = function() {};
94 this.abort = function() {}; 120 this.abort = function() {};
95 }; 121 };
96 122
97 // force native HLS to be ignored
98 oldSupportsNativeHls = videojs.hls.supportsNativeHls;
99 videojs.hls.supportsNativeHls = false;
100
101 // create the test player
102 var video = document.createElement('video');
103 document.querySelector('#qunit-fixture').appendChild(video);
104 player = videojs(video, {
105 flash: {
106 swf: '../node_modules/video.js/dist/video-js/video-js.swf'
107 },
108 techOrder: ['flash']
109 });
110 player.buffered = function() {
111 return videojs.createTimeRange(0, 0);
112 };
113
114 // store functionality that some tests need to mock 123 // store functionality that some tests need to mock
115 oldSegmentParser = videojs.hls.SegmentParser; 124 oldSegmentParser = videojs.Hls.SegmentParser;
116 oldSetTimeout = window.setTimeout; 125 oldSetTimeout = window.setTimeout;
117 126
118 // fake XHRs 127 // fake XHRs
...@@ -121,13 +130,16 @@ module('HLS', { ...@@ -121,13 +130,16 @@ module('HLS', {
121 xhr.onCreate = function(xhr) { 130 xhr.onCreate = function(xhr) {
122 requests.push(xhr); 131 requests.push(xhr);
123 }; 132 };
124 xhrUrls = []; 133
134 // create the test player
135 player = createPlayer();
125 }, 136 },
126 137
127 teardown: function() { 138 teardown: function() {
139 player.dispose();
128 videojs.Flash.isSupported = oldFlashSupported; 140 videojs.Flash.isSupported = oldFlashSupported;
129 videojs.hls.supportsNativeHls = oldSupportsNativeHls; 141 videojs.MediaSource.open = oldMediaSourceOpen;
130 videojs.hls.SegmentParser = oldSegmentParser; 142 videojs.Hls.SegmentParser = oldSegmentParser;
131 videojs.SourceBuffer = oldSourceBuffer; 143 videojs.SourceBuffer = oldSourceBuffer;
132 window.setTimeout = oldSetTimeout; 144 window.setTimeout = oldSetTimeout;
133 xhr.restore(); 145 xhr.restore();
...@@ -140,8 +152,11 @@ test('starts playing if autoplay is specified', function() { ...@@ -140,8 +152,11 @@ test('starts playing if autoplay is specified', function() {
140 plays++; 152 plays++;
141 }; 153 };
142 player.options().autoplay = true; 154 player.options().autoplay = true;
143 player.hls('manifest/playlist.m3u8'); 155 player.src({
144 videojs.mediaSources[player.currentSrc()].trigger({ 156 src: 'manifest/playlist.m3u8',
157 type: 'application/vnd.apple.mpegurl'
158 });
159 player.hls.mediaSource.trigger({
145 type: 'sourceopen' 160 type: 'sourceopen'
146 }); 161 });
147 162
...@@ -155,9 +170,12 @@ test('creates a PlaylistLoader on init', function() { ...@@ -155,9 +170,12 @@ test('creates a PlaylistLoader on init', function() {
155 loadedmetadata = true; 170 loadedmetadata = true;
156 }); 171 });
157 172
158 player.hls('manifest/playlist.m3u8'); 173 ok(!player.hls.playlists, 'waits for set src to create the loader');
159 ok(!player.hls.playlists, 'waits for sourceopen to create the loader'); 174 player.src({
160 videojs.mediaSources[player.currentSrc()].trigger({ 175 src:'manifest/playlist.m3u8',
176 type: 'application/vnd.apple.mpegurl'
177 });
178 player.hls.mediaSource.trigger({
161 type: 'sourceopen' 179 type: 'sourceopen'
162 }); 180 });
163 standardXHRResponse(requests[0]); 181 standardXHRResponse(requests[0]);
...@@ -178,8 +196,11 @@ test('sets the duration if one is available on the playlist', function() { ...@@ -178,8 +196,11 @@ test('sets the duration if one is available on the playlist', function() {
178 } 196 }
179 calls++; 197 calls++;
180 }; 198 };
181 player.hls('manifest/media.m3u8'); 199 player.src({
182 videojs.mediaSources[player.currentSrc()].trigger({ 200 src: 'manifest/media.m3u8',
201 type: 'application/vnd.apple.mpegurl'
202 });
203 player.hls.mediaSource.trigger({
183 type: 'sourceopen' 204 type: 'sourceopen'
184 }); 205 });
185 206
...@@ -197,8 +218,11 @@ test('calculates the duration if needed', function() { ...@@ -197,8 +218,11 @@ test('calculates the duration if needed', function() {
197 } 218 }
198 durations.push(duration); 219 durations.push(duration);
199 }; 220 };
200 player.hls('http://example.com/manifest/missingExtinf.m3u8'); 221 player.src({
201 videojs.mediaSources[player.currentSrc()].trigger({ 222 src: 'http://example.com/manifest/missingExtinf.m3u8',
223 type: 'application/vnd.apple.mpegurl'
224 });
225 player.hls.mediaSource.trigger({
202 type: 'sourceopen' 226 type: 'sourceopen'
203 }); 227 });
204 228
...@@ -210,11 +234,14 @@ test('calculates the duration if needed', function() { ...@@ -210,11 +234,14 @@ test('calculates the duration if needed', function() {
210 }); 234 });
211 235
212 test('starts downloading a segment on loadedmetadata', function() { 236 test('starts downloading a segment on loadedmetadata', function() {
213 player.hls('manifest/media.m3u8'); 237 player.src({
238 src: 'manifest/media.m3u8',
239 type: 'application/vnd.apple.mpegurl'
240 });
214 player.buffered = function() { 241 player.buffered = function() {
215 return videojs.createTimeRange(0, 0); 242 return videojs.createTimeRange(0, 0);
216 }; 243 };
217 videojs.mediaSources[player.currentSrc()].trigger({ 244 player.hls.mediaSource.trigger({
218 type: 'sourceopen' 245 type: 'sourceopen'
219 }); 246 });
220 247
...@@ -228,8 +255,11 @@ test('starts downloading a segment on loadedmetadata', function() { ...@@ -228,8 +255,11 @@ test('starts downloading a segment on loadedmetadata', function() {
228 }); 255 });
229 256
230 test('recognizes absolute URIs and requests them unmodified', function() { 257 test('recognizes absolute URIs and requests them unmodified', function() {
231 player.hls('manifest/absoluteUris.m3u8'); 258 player.src({
232 videojs.mediaSources[player.currentSrc()].trigger({ 259 src: 'manifest/absoluteUris.m3u8',
260 type: 'application/vnd.apple.mpegurl'
261 });
262 player.hls.mediaSource.trigger({
233 type: 'sourceopen' 263 type: 'sourceopen'
234 }); 264 });
235 265
...@@ -241,8 +271,11 @@ test('recognizes absolute URIs and requests them unmodified', function() { ...@@ -241,8 +271,11 @@ test('recognizes absolute URIs and requests them unmodified', function() {
241 }); 271 });
242 272
243 test('recognizes domain-relative URLs', function() { 273 test('recognizes domain-relative URLs', function() {
244 player.hls('manifest/domainUris.m3u8'); 274 player.src({
245 videojs.mediaSources[player.currentSrc()].trigger({ 275 src: 'manifest/domainUris.m3u8',
276 type: 'application/vnd.apple.mpegurl'
277 });
278 player.hls.mediaSource.trigger({
246 type: 'sourceopen' 279 type: 'sourceopen'
247 }); 280 });
248 281
...@@ -253,14 +286,31 @@ test('recognizes domain-relative URLs', function() { ...@@ -253,14 +286,31 @@ test('recognizes domain-relative URLs', function() {
253 'the first segment is requested'); 286 'the first segment is requested');
254 }); 287 });
255 288
256 test('re-initializes the plugin for each source', function() { 289 test('re-initializes the tech for each source', function() {
257 var firstInit, secondInit; 290 var firstPlaylists, secondPlaylists, firstMSE, secondMSE;
258 player.hls('manifest/master.m3u8'); 291
259 firstInit = player.hls; 292 player.src({
260 player.hls('manifest/master.m3u8'); 293 src: 'manifest/master.m3u8',
261 secondInit = player.hls; 294 type: 'application/vnd.apple.mpegurl'
295 });
296 player.hls.mediaSource.trigger({
297 type: 'sourceopen'
298 });
299 firstPlaylists = player.hls.playlists;
300 firstMSE = player.hls.mediaSource;
301
302 player.src({
303 src: 'manifest/master.m3u8',
304 type: 'application/vnd.apple.mpegurl'
305 });
306 player.hls.mediaSource.trigger({
307 type: 'sourceopen'
308 });
309 secondPlaylists = player.hls.playlists;
310 secondMSE = player.hls.mediaSource;
262 311
263 notStrictEqual(firstInit, secondInit, 'the plugin object is replaced'); 312 notStrictEqual(firstPlaylists, secondPlaylists, 'the playlist object is not reused');
313 notStrictEqual(firstMSE, secondMSE, 'the media source object is not reused');
264 }); 314 });
265 315
266 test('triggers an error when a master playlist request errors', function() { 316 test('triggers an error when a master playlist request errors', function() {
...@@ -268,8 +318,11 @@ test('triggers an error when a master playlist request errors', function() { ...@@ -268,8 +318,11 @@ test('triggers an error when a master playlist request errors', function() {
268 player.on('error', function() { 318 player.on('error', function() {
269 error = player.hls.error; 319 error = player.hls.error;
270 }); 320 });
271 player.hls('manifest/master.m3u8'); 321 player.src({
272 videojs.mediaSources[player.currentSrc()].trigger({ 322 src: 'manifest/master.m3u8',
323 type: 'application/vnd.apple.mpegurl'
324 });
325 player.hls.mediaSource.trigger({
273 type: 'sourceopen' 326 type: 'sourceopen'
274 }); 327 });
275 requests.pop().respond(500); 328 requests.pop().respond(500);
...@@ -279,8 +332,11 @@ test('triggers an error when a master playlist request errors', function() { ...@@ -279,8 +332,11 @@ test('triggers an error when a master playlist request errors', function() {
279 }); 332 });
280 333
281 test('downloads media playlists after loading the master', function() { 334 test('downloads media playlists after loading the master', function() {
282 player.hls('manifest/master.m3u8'); 335 player.src({
283 videojs.mediaSources[player.currentSrc()].trigger({ 336 src: 'manifest/master.m3u8',
337 type: 'application/vnd.apple.mpegurl'
338 });
339 player.hls.mediaSource.trigger({
284 type: 'sourceopen' 340 type: 'sourceopen'
285 }); 341 });
286 342
...@@ -309,8 +365,11 @@ test('timeupdates do not check to fill the buffer until a media playlist is read ...@@ -309,8 +365,11 @@ test('timeupdates do not check to fill the buffer until a media playlist is read
309 }; 365 };
310 this.send = function() {}; 366 this.send = function() {};
311 }; 367 };
312 player.hls('manifest/media.m3u8'); 368 player.src({
313 videojs.mediaSources[player.currentSrc()].trigger({ 369 src: 'manifest/media.m3u8',
370 type: 'application/vnd.apple.mpegurl'
371 });
372 player.hls.mediaSource.trigger({
314 type: 'sourceopen' 373 type: 'sourceopen'
315 }); 374 });
316 player.trigger('timeupdate'); 375 player.trigger('timeupdate');
...@@ -320,8 +379,11 @@ test('timeupdates do not check to fill the buffer until a media playlist is read ...@@ -320,8 +379,11 @@ test('timeupdates do not check to fill the buffer until a media playlist is read
320 }); 379 });
321 380
322 test('calculates the bandwidth after downloading a segment', function() { 381 test('calculates the bandwidth after downloading a segment', function() {
323 player.hls('manifest/media.m3u8'); 382 player.src({
324 videojs.mediaSources[player.currentSrc()].trigger({ 383 src: 'manifest/media.m3u8',
384 type: 'application/vnd.apple.mpegurl'
385 });
386 player.hls.mediaSource.trigger({
325 type: 'sourceopen' 387 type: 'sourceopen'
326 }); 388 });
327 389
...@@ -337,12 +399,15 @@ test('calculates the bandwidth after downloading a segment', function() { ...@@ -337,12 +399,15 @@ test('calculates the bandwidth after downloading a segment', function() {
337 399
338 test('selects a playlist after segment downloads', function() { 400 test('selects a playlist after segment downloads', function() {
339 var calls = 0; 401 var calls = 0;
340 player.hls('manifest/master.m3u8'); 402 player.src({
403 src: 'manifest/master.m3u8',
404 type: 'application/vnd.apple.mpegurl'
405 });
341 player.hls.selectPlaylist = function() { 406 player.hls.selectPlaylist = function() {
342 calls++; 407 calls++;
343 return player.hls.playlists.master.playlists[0]; 408 return player.hls.playlists.master.playlists[0];
344 }; 409 };
345 videojs.mediaSources[player.currentSrc()].trigger({ 410 player.hls.mediaSource.trigger({
346 type: 'sourceopen' 411 type: 'sourceopen'
347 }); 412 });
348 413
...@@ -367,8 +432,11 @@ test('selects a playlist after segment downloads', function() { ...@@ -367,8 +432,11 @@ test('selects a playlist after segment downloads', function() {
367 test('moves to the next segment if there is a network error', function() { 432 test('moves to the next segment if there is a network error', function() {
368 var mediaIndex; 433 var mediaIndex;
369 434
370 player.hls('manifest/master.m3u8'); 435 player.src({
371 videojs.mediaSources[player.currentSrc()].trigger({ 436 src: 'manifest/master.m3u8',
437 type: 'application/vnd.apple.mpegurl'
438 });
439 player.hls.mediaSource.trigger({
372 type: 'sourceopen' 440 type: 'sourceopen'
373 }); 441 });
374 442
...@@ -386,7 +454,10 @@ test('updates the duration after switching playlists', function() { ...@@ -386,7 +454,10 @@ test('updates the duration after switching playlists', function() {
386 var 454 var
387 calls = 0, 455 calls = 0,
388 selectedPlaylist = false; 456 selectedPlaylist = false;
389 player.hls('manifest/master.m3u8'); 457 player.src({
458 src: 'manifest/master.m3u8',
459 type: 'application/vnd.apple.mpegurl'
460 });
390 player.hls.selectPlaylist = function() { 461 player.hls.selectPlaylist = function() {
391 selectedPlaylist = true; 462 selectedPlaylist = true;
392 return player.hls.playlists.master.playlists[1]; 463 return player.hls.playlists.master.playlists[1];
...@@ -400,7 +471,7 @@ test('updates the duration after switching playlists', function() { ...@@ -400,7 +471,7 @@ test('updates the duration after switching playlists', function() {
400 calls++; 471 calls++;
401 } 472 }
402 }; 473 };
403 videojs.mediaSources[player.currentSrc()].trigger({ 474 player.hls.mediaSource.trigger({
404 type: 'sourceopen' 475 type: 'sourceopen'
405 }); 476 });
406 477
...@@ -418,8 +489,11 @@ test('downloads additional playlists if required', function() { ...@@ -418,8 +489,11 @@ test('downloads additional playlists if required', function() {
418 playlist = { 489 playlist = {
419 uri: 'media3.m3u8' 490 uri: 'media3.m3u8'
420 }; 491 };
421 player.hls('manifest/master.m3u8'); 492 player.src({
422 videojs.mediaSources[player.currentSrc()].trigger({ 493 src: 'manifest/master.m3u8',
494 type: 'application/vnd.apple.mpegurl'
495 });
496 player.hls.mediaSource.trigger({
423 type: 'sourceopen' 497 type: 'sourceopen'
424 }); 498 });
425 499
...@@ -456,8 +530,11 @@ test('downloads additional playlists if required', function() { ...@@ -456,8 +530,11 @@ test('downloads additional playlists if required', function() {
456 530
457 test('selects a playlist below the current bandwidth', function() { 531 test('selects a playlist below the current bandwidth', function() {
458 var playlist; 532 var playlist;
459 player.hls('manifest/master.m3u8'); 533 player.src({
460 videojs.mediaSources[player.currentSrc()].trigger({ 534 src: 'manifest/master.m3u8',
535 type: 'application/vnd.apple.mpegurl'
536 });
537 player.hls.mediaSource.trigger({
461 type: 'sourceopen' 538 type: 'sourceopen'
462 }); 539 });
463 540
...@@ -478,8 +555,11 @@ test('selects a playlist below the current bandwidth', function() { ...@@ -478,8 +555,11 @@ test('selects a playlist below the current bandwidth', function() {
478 555
479 test('raises the minimum bitrate for a stream proportionially', function() { 556 test('raises the minimum bitrate for a stream proportionially', function() {
480 var playlist; 557 var playlist;
481 player.hls('manifest/master.m3u8'); 558 player.src({
482 videojs.mediaSources[player.currentSrc()].trigger({ 559 src: 'manifest/master.m3u8',
560 type: 'application/vnd.apple.mpegurl'
561 });
562 player.hls.mediaSource.trigger({
483 type: 'sourceopen' 563 type: 'sourceopen'
484 }); 564 });
485 565
...@@ -500,8 +580,11 @@ test('raises the minimum bitrate for a stream proportionially', function() { ...@@ -500,8 +580,11 @@ test('raises the minimum bitrate for a stream proportionially', function() {
500 580
501 test('uses the lowest bitrate if no other is suitable', function() { 581 test('uses the lowest bitrate if no other is suitable', function() {
502 var playlist; 582 var playlist;
503 player.hls('manifest/master.m3u8'); 583 player.src({
504 videojs.mediaSources[player.currentSrc()].trigger({ 584 src: 'manifest/master.m3u8',
585 type: 'application/vnd.apple.mpegurl'
586 });
587 player.hls.mediaSource.trigger({
505 type: 'sourceopen' 588 type: 'sourceopen'
506 }); 589 });
507 590
...@@ -520,9 +603,12 @@ test('uses the lowest bitrate if no other is suitable', function() { ...@@ -520,9 +603,12 @@ test('uses the lowest bitrate if no other is suitable', function() {
520 test('selects the correct rendition by player dimensions', function() { 603 test('selects the correct rendition by player dimensions', function() {
521 var playlist; 604 var playlist;
522 605
523 player.hls('manifest/master.m3u8'); 606 player.src({
607 src: 'manifest/master.m3u8',
608 type: 'application/vnd.apple.mpegurl'
609 });
524 610
525 videojs.mediaSources[player.currentSrc()].trigger({ 611 player.hls.mediaSource.trigger({
526 type: 'sourceopen' 612 type: 'sourceopen'
527 }); 613 });
528 614
...@@ -550,14 +636,17 @@ test('selects the correct rendition by player dimensions', function() { ...@@ -550,14 +636,17 @@ test('selects the correct rendition by player dimensions', function() {
550 636
551 637
552 test('does not download the next segment if the buffer is full', function() { 638 test('does not download the next segment if the buffer is full', function() {
553 player.hls('manifest/media.m3u8'); 639 player.src({
640 src: 'manifest/media.m3u8',
641 type: 'application/vnd.apple.mpegurl'
642 });
554 player.currentTime = function() { 643 player.currentTime = function() {
555 return 15; 644 return 15;
556 }; 645 };
557 player.buffered = function() { 646 player.buffered = function() {
558 return videojs.createTimeRange(0, 20); 647 return videojs.createTimeRange(0, 20);
559 }; 648 };
560 videojs.mediaSources[player.currentSrc()].trigger({ 649 player.hls.mediaSource.trigger({
561 type: 'sourceopen' 650 type: 'sourceopen'
562 }); 651 });
563 652
...@@ -569,8 +658,11 @@ test('does not download the next segment if the buffer is full', function() { ...@@ -569,8 +658,11 @@ test('does not download the next segment if the buffer is full', function() {
569 }); 658 });
570 659
571 test('downloads the next segment if the buffer is getting low', function() { 660 test('downloads the next segment if the buffer is getting low', function() {
572 player.hls('manifest/media.m3u8'); 661 player.src({
573 videojs.mediaSources[player.currentSrc()].trigger({ 662 src: 'manifest/media.m3u8',
663 type: 'application/vnd.apple.mpegurl'
664 });
665 player.hls.mediaSource.trigger({
574 type: 'sourceopen' 666 type: 'sourceopen'
575 }); 667 });
576 668
...@@ -597,8 +689,11 @@ test('downloads the next segment if the buffer is getting low', function() { ...@@ -597,8 +689,11 @@ test('downloads the next segment if the buffer is getting low', function() {
597 }); 689 });
598 690
599 test('stops downloading segments at the end of the playlist', function() { 691 test('stops downloading segments at the end of the playlist', function() {
600 player.hls('manifest/media.m3u8'); 692 player.src({
601 videojs.mediaSources[player.currentSrc()].trigger({ 693 src: 'manifest/media.m3u8',
694 type: 'application/vnd.apple.mpegurl'
695 });
696 player.hls.mediaSource.trigger({
602 type: 'sourceopen' 697 type: 'sourceopen'
603 }); 698 });
604 standardXHRResponse(requests[0]); 699 standardXHRResponse(requests[0]);
...@@ -606,13 +701,16 @@ test('stops downloading segments at the end of the playlist', function() { ...@@ -606,13 +701,16 @@ test('stops downloading segments at the end of the playlist', function() {
606 player.hls.mediaIndex = 4; 701 player.hls.mediaIndex = 4;
607 player.trigger('timeupdate'); 702 player.trigger('timeupdate');
608 703
609 strictEqual(xhrUrls.length, 0, 'no request is made'); 704 strictEqual(requests.length, 0, 'no request is made');
610 }); 705 });
611 706
612 test('only makes one segment request at a time', function() { 707 test('only makes one segment request at a time', function() {
613 var openedXhrs = 0; 708 var openedXhrs = 0;
614 player.hls('manifest/media.m3u8'); 709 player.src({
615 videojs.mediaSources[player.currentSrc()].trigger({ 710 src: 'manifest/media.m3u8',
711 type: 'application/vnd.apple.mpegurl'
712 });
713 player.hls.mediaSource.trigger({
616 type: 'sourceopen' 714 type: 'sourceopen'
617 }); 715 });
618 xhr.restore(); 716 xhr.restore();
...@@ -634,68 +732,12 @@ test('only makes one segment request at a time', function() { ...@@ -634,68 +732,12 @@ test('only makes one segment request at a time', function() {
634 xhr = sinon.useFakeXMLHttpRequest(); 732 xhr = sinon.useFakeXMLHttpRequest();
635 }); 733 });
636 734
637 test('uses the src attribute if no options are provided and it ends in ".m3u8"', function() {
638 var url = 'http://example.com/services/mobile/streaming/index/master.m3u8?videoId=1824650741001';
639 player.el().querySelector('.vjs-tech').src = url;
640 player.hls();
641 videojs.mediaSources[player.currentSrc()].trigger({
642 type: 'sourceopen'
643 });
644
645 strictEqual(requests[0].url, url, 'currentSrc is used');
646 });
647
648 test('ignores src attribute if it doesn\'t have the "m3u8" extension', function() {
649 var tech = player.el().querySelector('.vjs-tech');
650 tech.src = 'basdfasdfasdfliel//.m3u9';
651 player.hls();
652 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
653 strictEqual(requests.length, 0, 'no request is made');
654
655 tech.src = '';
656 player.hls();
657 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
658 strictEqual(requests.length, 0, 'no request is made');
659
660 tech.src = 'http://example.com/movie.mp4?q=why.m3u8';
661 player.hls();
662 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
663 strictEqual(requests.length, 0, 'no request is made');
664
665 tech.src = 'http://example.m3u8/movie.mp4';
666 player.hls();
667 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
668 strictEqual(requests.length, 0, 'no request is made');
669
670 tech.src = '//example.com/movie.mp4#http://tricky.com/master.m3u8';
671 player.hls();
672 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
673 strictEqual(requests.length, 0, 'no request is made');
674 });
675
676 test('activates if the first playable source is HLS', function() {
677 var video;
678 document.querySelector('#qunit-fixture').innerHTML =
679 '<video controls>' +
680 '<source type="slartibartfast$%" src="movie.slarti">' +
681 '<source type="application/x-mpegURL" src="movie.m3u8">' +
682 '<source type="video/mp4" src="movie.mp4">' +
683 '</video>';
684 video = document.querySelector('#qunit-fixture video');
685 player = videojs(video, {
686 flash: {
687 swf: '../node_modules/video.js/dist/video-js/video-js.swf'
688 },
689 techOrder: ['flash']
690 });
691 player.hls();
692
693 ok(player.currentSrc() in videojs.mediaSources, 'media source created');
694 });
695
696 test('cancels outstanding XHRs when seeking', function() { 735 test('cancels outstanding XHRs when seeking', function() {
697 player.hls('manifest/media.m3u8'); 736 player.src({
698 videojs.mediaSources[player.currentSrc()].trigger({ 737 src: 'manifest/media.m3u8',
738 type: 'application/vnd.apple.mpegurl'
739 });
740 player.hls.mediaSource.trigger({
699 type: 'sourceopen' 741 type: 'sourceopen'
700 }); 742 });
701 standardXHRResponse(requests[0]); 743 standardXHRResponse(requests[0]);
...@@ -721,7 +763,7 @@ test('cancels outstanding XHRs when seeking', function() { ...@@ -721,7 +763,7 @@ test('cancels outstanding XHRs when seeking', function() {
721 test('flushes the parser after each segment', function() { 763 test('flushes the parser after each segment', function() {
722 var flushes = 0; 764 var flushes = 0;
723 // mock out the segment parser 765 // mock out the segment parser
724 videojs.hls.SegmentParser = function() { 766 videojs.Hls.SegmentParser = function() {
725 this.getFlvHeader = function() { 767 this.getFlvHeader = function() {
726 return []; 768 return [];
727 }; 769 };
...@@ -732,8 +774,11 @@ test('flushes the parser after each segment', function() { ...@@ -732,8 +774,11 @@ test('flushes the parser after each segment', function() {
732 this.tagsAvailable = function() {}; 774 this.tagsAvailable = function() {};
733 }; 775 };
734 776
735 player.hls('manifest/media.m3u8'); 777 player.src({
736 videojs.mediaSources[player.currentSrc()].trigger({ 778 src: 'manifest/media.m3u8',
779 type: 'application/vnd.apple.mpegurl'
780 });
781 player.hls.mediaSource.trigger({
737 type: 'sourceopen' 782 type: 'sourceopen'
738 }); 783 });
739 784
...@@ -749,7 +794,7 @@ test('drops tags before the target timestamp when seeking', function() { ...@@ -749,7 +794,7 @@ test('drops tags before the target timestamp when seeking', function() {
749 bytes = []; 794 bytes = [];
750 795
751 // mock out the parser and source buffer 796 // mock out the parser and source buffer
752 videojs.hls.SegmentParser = mockSegmentParser(tags); 797 videojs.Hls.SegmentParser = mockSegmentParser(tags);
753 window.videojs.SourceBuffer = function() { 798 window.videojs.SourceBuffer = function() {
754 this.appendBuffer = function(chunk) { 799 this.appendBuffer = function(chunk) {
755 bytes.push(chunk); 800 bytes.push(chunk);
...@@ -764,8 +809,11 @@ test('drops tags before the target timestamp when seeking', function() { ...@@ -764,8 +809,11 @@ test('drops tags before the target timestamp when seeking', function() {
764 // push a tag into the buffer 809 // push a tag into the buffer
765 tags.push({ pts: 0, bytes: 0 }); 810 tags.push({ pts: 0, bytes: 0 });
766 811
767 player.hls('manifest/media.m3u8'); 812 player.src({
768 videojs.mediaSources[player.currentSrc()].trigger({ 813 src: 'manifest/media.m3u8',
814 type: 'application/vnd.apple.mpegurl'
815 });
816 player.hls.mediaSource.trigger({
769 type: 'sourceopen' 817 type: 'sourceopen'
770 }); 818 });
771 standardXHRResponse(requests[0]); 819 standardXHRResponse(requests[0]);
...@@ -803,7 +851,7 @@ test('clears pending buffer updates when seeking', function() { ...@@ -803,7 +851,7 @@ test('clears pending buffer updates when seeking', function() {
803 tags = [{ pts: 0, bytes: 0 }]; 851 tags = [{ pts: 0, bytes: 0 }];
804 852
805 // mock out the parser and source buffer 853 // mock out the parser and source buffer
806 videojs.hls.SegmentParser = mockSegmentParser(tags); 854 videojs.Hls.SegmentParser = mockSegmentParser(tags);
807 window.videojs.SourceBuffer = function() { 855 window.videojs.SourceBuffer = function() {
808 this.appendBuffer = function(chunk) { 856 this.appendBuffer = function(chunk) {
809 bytes.push(chunk); 857 bytes.push(chunk);
...@@ -818,8 +866,11 @@ test('clears pending buffer updates when seeking', function() { ...@@ -818,8 +866,11 @@ test('clears pending buffer updates when seeking', function() {
818 }; 866 };
819 867
820 // queue up a tag to be pushed into the buffer (but don't push it yet!) 868 // queue up a tag to be pushed into the buffer (but don't push it yet!)
821 player.hls('manifest/media.m3u8'); 869 player.src({
822 videojs.mediaSources[player.currentSrc()].trigger({ 870 src: 'manifest/media.m3u8',
871 type: 'application/vnd.apple.mpegurl'
872 });
873 player.hls.mediaSource.trigger({
823 type: 'sourceopen' 874 type: 'sourceopen'
824 }); 875 });
825 876
...@@ -846,8 +897,11 @@ test('playlist 404 should trigger MEDIA_ERR_NETWORK', function() { ...@@ -846,8 +897,11 @@ test('playlist 404 should trigger MEDIA_ERR_NETWORK', function() {
846 player.on('error', function() { 897 player.on('error', function() {
847 errorTriggered = true; 898 errorTriggered = true;
848 }); 899 });
849 player.hls('manifest/media.m3u8'); 900 player.src({
850 videojs.mediaSources[player.currentSrc()].trigger({ 901 src: 'manifest/media.m3u8',
902 type: 'application/vnd.apple.mpegurl'
903 });
904 player.hls.mediaSource.trigger({
851 type: 'sourceopen' 905 type: 'sourceopen'
852 }); 906 });
853 requests.pop().respond(404); 907 requests.pop().respond(404);
...@@ -862,9 +916,12 @@ test('playlist 404 should trigger MEDIA_ERR_NETWORK', function() { ...@@ -862,9 +916,12 @@ test('playlist 404 should trigger MEDIA_ERR_NETWORK', function() {
862 }); 916 });
863 917
864 test('segment 404 should trigger MEDIA_ERR_NETWORK', function () { 918 test('segment 404 should trigger MEDIA_ERR_NETWORK', function () {
865 player.hls('manifest/media.m3u8'); 919 player.src({
920 src: 'manifest/media.m3u8',
921 type: 'application/vnd.apple.mpegurl'
922 });
866 923
867 videojs.mediaSources[player.currentSrc()].trigger({ 924 player.hls.mediaSource.trigger({
868 type: 'sourceopen' 925 type: 'sourceopen'
869 }); 926 });
870 927
...@@ -875,9 +932,12 @@ test('segment 404 should trigger MEDIA_ERR_NETWORK', function () { ...@@ -875,9 +932,12 @@ test('segment 404 should trigger MEDIA_ERR_NETWORK', function () {
875 }); 932 });
876 933
877 test('segment 500 should trigger MEDIA_ERR_ABORTED', function () { 934 test('segment 500 should trigger MEDIA_ERR_ABORTED', function () {
878 player.hls('manifest/media.m3u8'); 935 player.src({
936 src: 'manifest/media.m3u8',
937 type: 'application/vnd.apple.mpegurl'
938 });
879 939
880 videojs.mediaSources[player.currentSrc()].trigger({ 940 player.hls.mediaSource.trigger({
881 type: 'sourceopen' 941 type: 'sourceopen'
882 }); 942 });
883 943
...@@ -887,17 +947,12 @@ test('segment 500 should trigger MEDIA_ERR_ABORTED', function () { ...@@ -887,17 +947,12 @@ test('segment 500 should trigger MEDIA_ERR_ABORTED', function () {
887 equal(4, player.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_ABORTED'); 947 equal(4, player.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_ABORTED');
888 }); 948 });
889 949
890 test('has no effect if native HLS is available', function() {
891 videojs.hls.supportsNativeHls = true;
892 player.hls('http://example.com/manifest/master.m3u8');
893
894 ok(!(player.currentSrc() in videojs.mediaSources),
895 'no media source was opened');
896 });
897
898 test('duration is Infinity for live playlists', function() { 950 test('duration is Infinity for live playlists', function() {
899 player.hls('http://example.com/manifest/missingEndlist.m3u8'); 951 player.src({
900 videojs.mediaSources[player.currentSrc()].trigger({ 952 src: 'http://example.com/manifest/missingEndlist.m3u8',
953 type: 'application/vnd.apple.mpegurl'
954 });
955 player.hls.mediaSource.trigger({
901 type: 'sourceopen' 956 type: 'sourceopen'
902 }); 957 });
903 958
...@@ -912,8 +967,11 @@ test('does not reload playlists with an endlist tag', function() { ...@@ -912,8 +967,11 @@ test('does not reload playlists with an endlist tag', function() {
912 window.setTimeout = function(callback, timeout) { 967 window.setTimeout = function(callback, timeout) {
913 callbacks.push({ callback: callback, timeout: timeout }); 968 callbacks.push({ callback: callback, timeout: timeout });
914 }; 969 };
915 player.hls('manifest/media.m3u8'); 970 player.src({
916 videojs.mediaSources[player.currentSrc()].trigger({ 971 src: 'manifest/media.m3u8',
972 type: 'application/vnd.apple.mpegurl'
973 });
974 player.hls.mediaSource.trigger({
917 type: 'sourceopen' 975 type: 'sourceopen'
918 }); 976 });
919 977
...@@ -921,8 +979,11 @@ test('does not reload playlists with an endlist tag', function() { ...@@ -921,8 +979,11 @@ test('does not reload playlists with an endlist tag', function() {
921 }); 979 });
922 980
923 test('updates the media index when a playlist reloads', function() { 981 test('updates the media index when a playlist reloads', function() {
924 player.hls('http://example.com/live-updating.m3u8'); 982 player.src({
925 videojs.mediaSources[player.currentSrc()].trigger({ 983 src: 'http://example.com/live-updating.m3u8',
984 type: 'application/vnd.apple.mpegurl'
985 });
986 player.hls.mediaSource.trigger({
926 type: 'sourceopen' 987 type: 'sourceopen'
927 }); 988 });
928 989
...@@ -964,8 +1025,11 @@ test('mediaIndex is zero before the first segment loads', function() { ...@@ -964,8 +1025,11 @@ test('mediaIndex is zero before the first segment loads', function() {
964 this.open = function() {}; 1025 this.open = function() {};
965 this.send = function() {}; 1026 this.send = function() {};
966 }; 1027 };
967 player.hls('http://example.com/first-seg-load.m3u8'); 1028 player.src({
968 videojs.mediaSources[player.currentSrc()].trigger({ 1029 src: 'http://example.com/first-seg-load.m3u8',
1030 type: 'application/vnd.apple.mpegurl'
1031 });
1032 player.hls.mediaSource.trigger({
969 type: 'sourceopen' 1033 type: 'sourceopen'
970 }); 1034 });
971 1035
...@@ -973,8 +1037,11 @@ test('mediaIndex is zero before the first segment loads', function() { ...@@ -973,8 +1037,11 @@ test('mediaIndex is zero before the first segment loads', function() {
973 }); 1037 });
974 1038
975 test('reloads out-of-date live playlists when switching variants', function() { 1039 test('reloads out-of-date live playlists when switching variants', function() {
976 player.hls('http://example.com/master.m3u8'); 1040 player.src({
977 videojs.mediaSources[player.currentSrc()].trigger({ 1041 src: 'http://example.com/master.m3u8',
1042 type: 'application/vnd.apple.mpegurl'
1043 });
1044 player.hls.mediaSource.trigger({
978 type: 'sourceopen' 1045 type: 'sourceopen'
979 }); 1046 });
980 1047
...@@ -1014,29 +1081,40 @@ test('does not reload master playlists', function() { ...@@ -1014,29 +1081,40 @@ test('does not reload master playlists', function() {
1014 callbacks.push(callback); 1081 callbacks.push(callback);
1015 }; 1082 };
1016 1083
1017 player.hls('http://example.com/master.m3u8'); 1084 player.src({
1018 videojs.mediaSources[player.currentSrc()].trigger({ 1085 src: 'http://example.com/master.m3u8',
1086 type: 'application/vnd.apple.mpegurl'
1087 });
1088 player.hls.mediaSource.trigger({
1019 type: 'sourceopen' 1089 type: 'sourceopen'
1020 }); 1090 });
1021 1091
1022 strictEqual(callbacks.length, 0, 'no reload scheduled'); 1092 strictEqual(callbacks.length,
1093 0, 'no reload scheduled');
1023 }); 1094 });
1024 1095
1025 test('if withCredentials option is used, withCredentials is set on the XHR object', function() { 1096 test('if withCredentials option is used, withCredentials is set on the XHR object', function() {
1026 player.hls({ 1097 player.dispose();
1027 url: 'http://example.com/media.m3u8', 1098 player = createPlayer({
1028 withCredentials: true 1099 withCredentials: true
1029 }); 1100 });
1030 videojs.mediaSources[player.currentSrc()].trigger({ 1101 player.src({
1102 src: 'http://example.com/media.m3u8',
1103 type: 'application/vnd.apple.mpegurl'
1104 });
1105 player.hls.mediaSource.trigger({
1031 type: 'sourceopen' 1106 type: 'sourceopen'
1032 }); 1107 });
1033 ok(requests[0].withCredentials, "with credentials should be set to true if that option is passed in"); 1108 ok(requests[0].withCredentials, "with credentials should be set to true if that option is passed in");
1034 }); 1109 });
1035 1110
1036 test('does not break if the playlist has no segments', function() { 1111 test('does not break if the playlist has no segments', function() {
1037 player.hls('manifest/master.m3u8'); 1112 player.src({
1113 src: 'manifest/master.m3u8',
1114 type: 'application/vnd.apple.mpegurl'
1115 });
1038 try { 1116 try {
1039 videojs.mediaSources[player.currentSrc()].trigger({ 1117 player.hls.mediaSource.trigger({
1040 type: 'sourceopen' 1118 type: 'sourceopen'
1041 }); 1119 });
1042 requests[0].respond(200, null, 1120 requests[0].respond(200, null,
...@@ -1051,4 +1129,22 @@ test('does not break if the playlist has no segments', function() { ...@@ -1051,4 +1129,22 @@ test('does not break if the playlist has no segments', function() {
1051 strictEqual(requests.length, 1, 'no requests for non-existent segments were queued'); 1129 strictEqual(requests.length, 1, 'no requests for non-existent segments were queued');
1052 }); 1130 });
1053 1131
1132 test('disposes the playlist loader', function() {
1133 var disposes = 0, player;
1134 player = createPlayer();
1135 player.src({
1136 src: 'manifest/master.m3u8',
1137 type: 'application/vnd.apple.mpegurl'
1138 });
1139 player.hls.mediaSource.trigger({
1140 type: 'sourceopen'
1141 });
1142 player.hls.playlists.dispose = function() {
1143 disposes++;
1144 };
1145
1146 player.dispose();
1147 strictEqual(disposes, 1, 'disposed playlist loader');
1148 });
1149
1054 })(window, window.videojs); 1150 })(window, window.videojs);
......