5e6b769b by David LaPalomento

Download the default first segment and append it to the media source

Move more of the manifest management and segment loading into a more customary location for a plugin implementation. Get a single segment downloading and playing. Add test cases for the plugin initialization and first segment download. Use a linked version of media sources to pick up an event listening implementation on SourceBuffer that doesn't require an unminified version of video.js.
1 parent faad6baa
...@@ -4,10 +4,10 @@ ...@@ -4,10 +4,10 @@
4 <meta charset="utf-8"> 4 <meta charset="utf-8">
5 <title>video.js HLS Plugin Example</title> 5 <title>video.js HLS Plugin Example</title>
6 6
7 <link href="node_modules/video.js/video-js.css" rel="stylesheet"> 7 <link href="node_modules/video.js/dist/video-js/video-js.css" rel="stylesheet">
8 8
9 <!-- video.js --> 9 <!-- video.js -->
10 <script src="node_modules/video.js/video.dev.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-media-sources/videojs-media-sources.js"></script> 13 <script src="node_modules/videojs-media-sources/videojs-media-sources.js"></script>
...@@ -24,8 +24,7 @@ ...@@ -24,8 +24,7 @@
24 <script src="src/segment-controller.js"></script> 24 <script src="src/segment-controller.js"></script>
25 25
26 <!-- m3u8 handling --> 26 <!-- m3u8 handling -->
27 <script src="src/m3u8/m3u8.js"></script> 27 <script src="src/stream.js"></script>
28 <script src="src/m3u8/m3u8-tag-types.js"></script>
29 <script src="src/m3u8/m3u8-parser.js"></script> 28 <script src="src/m3u8/m3u8-parser.js"></script>
30 <script src="src/manifest-controller.js"></script> 29 <script src="src/manifest-controller.js"></script>
31 <script src="src/segment-controller.js"></script> 30 <script src="src/segment-controller.js"></script>
...@@ -46,18 +45,12 @@ ...@@ -46,18 +45,12 @@
46 controls> 45 controls>
47 </video> 46 </video>
48 <script> 47 <script>
49 var video, mediaSource; 48 videojs.options.flash.swf = 'node_modules/video.js/dist/video-js/video-js.swf';
50
51 // initialize the player 49 // initialize the player
52 videojs.options.flash.swf = 'node_modules/videojs-media-sources/video-js-with-mse.swf'; 50 var player = videojs('video');
53 video = videojs('video',{},function(){
54 this.playbackController = new window.videojs.hls.HLSPlaybackController(this);
55 this.playbackController.loadManifest('test/fixtures/prog_index.m3u8', function(data) {
56 console.log(data);
57 });
58 });
59 </script>
60
61 51
52 // load the master playlist
53 player.hls('test/fixtures/prog_index.m3u8');
54 </script>
62 </body> 55 </body>
63 </html> 56 </html>
......
1 (function(parseInt, isFinite, mergeOptions, undefined) { 1 (function(videojs, parseInt, isFinite, mergeOptions, undefined) {
2 var 2 var
3 noop = function() {}, 3 noop = function() {},
4 parseAttributes = function(attributes) { 4 parseAttributes = function(attributes) {
...@@ -13,49 +13,11 @@ ...@@ -13,49 +13,11 @@
13 } 13 }
14 return result; 14 return result;
15 }, 15 },
16 Stream, 16 Stream = videojs.hls.Stream,
17 LineStream, 17 LineStream,
18 ParseStream, 18 ParseStream,
19 Parser; 19 Parser;
20 20
21 Stream = function() {
22 this.init = function() {
23 var listeners = {};
24 this.on = function(type, listener) {
25 if (!listeners[type]) {
26 listeners[type] = [];
27 }
28 listeners[type].push(listener);
29 };
30 this.off = function(type, listener) {
31 var index;
32 if (!listeners[type]) {
33 return false;
34 }
35 index = listeners[type].indexOf(listener);
36 listeners[type].splice(index, 1);
37 return index > -1;
38 };
39 this.trigger = function(type) {
40 var callbacks, i, length, args;
41 callbacks = listeners[type];
42 if (!callbacks) {
43 return;
44 }
45 args = Array.prototype.slice.call(arguments, 1);
46 length = callbacks.length;
47 for (i = 0; i < length; ++i) {
48 callbacks[i].apply(this, args);
49 }
50 };
51 };
52 };
53 Stream.prototype.pipe = function(destination) {
54 this.on('data', function(data) {
55 destination.push(data);
56 });
57 };
58
59 LineStream = function() { 21 LineStream = function() {
60 var buffer = ''; 22 var buffer = '';
61 LineStream.prototype.init.call(this); 23 LineStream.prototype.init.call(this);
...@@ -382,4 +344,4 @@ ...@@ -382,4 +344,4 @@
382 ParseStream: ParseStream, 344 ParseStream: ParseStream,
383 Parser: Parser 345 Parser: Parser
384 }; 346 };
385 })(window.parseInt, window.isFinite, window.videojs.util.mergeOptions); 347 })(window.videojs, window.parseInt, window.isFinite, window.videojs.util.mergeOptions);
......
1 /**
2 * A lightweight stream implemention that handles event dispatching. Objects
3 * that inherit from streams should call init in their constructors.
4 */
5 (function(videojs, undefined) {
6 var Stream = function() {
7 this.init = function() {
8 var listeners = {};
9 this.on = function(type, listener) {
10 if (!listeners[type]) {
11 listeners[type] = [];
12 }
13 listeners[type].push(listener);
14 };
15 this.off = function(type, listener) {
16 var index;
17 if (!listeners[type]) {
18 return false;
19 }
20 index = listeners[type].indexOf(listener);
21 listeners[type].splice(index, 1);
22 return index > -1;
23 };
24 this.trigger = function(type) {
25 var callbacks, i, length, args;
26 callbacks = listeners[type];
27 if (!callbacks) {
28 return;
29 }
30 args = Array.prototype.slice.call(arguments, 1);
31 length = callbacks.length;
32 for (i = 0; i < length; ++i) {
33 callbacks[i].apply(this, args);
34 }
35 };
36 };
37 };
38 Stream.prototype.pipe = function(destination) {
39 this.on('data', function(data) {
40 destination.push(data);
41 });
42 };
43
44 videojs.hls.Stream = Stream;
45 })(window.videojs);
...@@ -6,8 +6,97 @@ ...@@ -6,8 +6,97 @@
6 * All rights reserved. 6 * All rights reserved.
7 */ 7 */
8 8
9 (function(window) { 9 (function(window, videojs, undefined) {
10 10
11 window.videojs.hls = {}; 11 videojs.hls = {};
12 12
13 })(this); 13 videojs.plugin('hls', function(options) {
14 var
15 mediaSource = new videojs.MediaSource(),
16 segmentParser = new videojs.hls.SegmentParser(),
17 player = this,
18 url,
19
20 loadSegment,
21 selectPlaylist;
22
23 if (typeof options === 'string') {
24 url = options;
25 } else {
26 url = options.url;
27 }
28
29 // expose the HLS plugin state
30 player.hls.readyState = function() {
31 if (!player.hls.manifest) {
32 return 0; // HAVE_NOTHING
33 }
34 return 1; // HAVE_METADATA
35 };
36
37 // load the MediaSource into the player
38 mediaSource.addEventListener('sourceopen', function() {
39 // construct the video data buffer and set the appropriate MIME type
40 var sourceBuffer = mediaSource.addSourceBuffer('video/flv; codecs="vp6,aac"');
41 player.hls.sourceBuffer = sourceBuffer;
42 sourceBuffer.appendBuffer(segmentParser.getFlvHeader());
43
44 // Chooses the appropriate media playlist based on the current bandwidth
45 // estimate and the player size
46 selectPlaylist = function() {
47 player.hls.currentPlaylist = player.hls.manifest;
48 player.hls.currentMediaIndex = 0;
49 };
50
51 // download a new segment if one is needed
52 fillBuffer = function() {
53 var
54 xhr = new window.XMLHttpRequest(),
55 segment = player.hls.currentPlaylist.segments[player.hls.currentMediaIndex];
56 xhr.open('GET', segment.uri);
57 xhr.responseType = 'arraybuffer';
58 xhr.onreadystatechange = function() {
59 if (xhr.readyState === 4) {
60 player.hls.currentMediaIndex++;
61 segmentParser.parseSegmentBinaryData(new Uint8Array(xhr.response));
62 while (segmentParser.tagsAvailable()) {
63 player.hls.sourceBuffer.appendBuffer(segmentParser.getNextTag().bytes,
64 player);
65 }
66 }
67 };
68 xhr.send(null);
69 };
70 player.on('loadedmetadata', fillBuffer);
71
72 // download and process the manifest
73 (function() {
74 var xhr = new window.XMLHttpRequest();
75 xhr.open('GET', url);
76 xhr.onreadystatechange = function() {
77 var parser;
78
79 if (xhr.readyState === 4) {
80 // readystate DONE
81 parser = new videojs.m3u8.Parser();
82 parser.push(xhr.responseText);
83 player.hls.manifest = parser.manifest;
84
85 player.trigger('loadedmanifest');
86
87 if (parser.manifest.segments) {
88 selectPlaylist();
89 player.trigger('loadedmetadata');
90 }
91 }
92 };
93 xhr.send(null);
94 })();
95 });
96 player.src({
97 src: videojs.URL.createObjectURL(mediaSource),
98 type: "video/flv"
99 });
100 });
101
102 })(window, window.videojs);
......
1 (function(window, videojs, undefined) {
2 var player, oldXhr, oldSourceBuffer;
3
4 module('HLS', {
5 setup: function() {
6 var video = document.createElement('video');
7 document.querySelector('#qunit-fixture').appendChild(video);
8 player = videojs(video);
9
10 oldXhr = window.XMLHttpRequest;
11 oldSourceBuffer = window.videojs.SourceBuffer;
12
13 // mock out SourceBuffer since it won't be available in phantomjs
14 window.videojs.SourceBuffer = function() {
15 this.appendBuffer = function() {};
16 };
17 },
18 teardown: function() {
19 window.XMLHttpRequest = oldXhr;
20 window.videojs.SourceBuffer = oldSourceBuffer;
21 }
22 });
23
24 asyncTest('loads the specified manifest URL on init', function() {
25 var loadedmanifest = false;
26 player.on('loadedmanifest', function() {
27 loadedmanifest = true;
28 });
29 player.on('loadedmetadata', function() {
30 ok(loadedmanifest, 'loadedmanifest fires');
31 ok(player.hls.manifest, 'the manifest is available');
32 ok(player.hls.manifest.segments, 'the segments are parsed');
33 strictEqual(player.hls.manifest,
34 player.hls.currentPlaylist,
35 'a playlist is selected');
36 strictEqual(player.hls.readyState(), 1, 'the readyState is HAVE_METADATA');
37 start();
38 });
39
40 player.hls('manifest/playlist.m3u8');
41 strictEqual(player.hls.readyState(), 0, 'the readyState is HAVE_NOTHING');
42 videojs.mediaSources[player.currentSrc()].trigger({
43 type: 'sourceopen'
44 });
45 });
46
47 test('starts downloading a segment on loadedmetadata', function() {
48 var url;
49 window.XMLHttpRequest = function() {
50 this.open = function() {
51 url = arguments[1];
52 };
53 this.send = function() {
54 this.readyState = 4;
55 this.responseText = window.manifests['media'];
56 this.onreadystatechange();
57 };
58 };
59 player.hls('manifest/media.m3u8');
60 videojs.mediaSources[player.currentSrc()].trigger({
61 type: 'sourceopen'
62 });
63
64 strictEqual(url, '00001.ts', 'the first segment is requested');
65 });
66
67 })(window, window.videojs);
1 {
2 "allowCache": true,
3 "targetDuration": 10,
4 "mediaSequence": 0,
5 "playlistType": "VOD",
6 "segments": [{
7 "duration": 10,
8 "uri": "00001.ts"
9 }, {
10 "duration": 10,
11 "uri": "00002.ts"
12 }, {
13 "duration": 10,
14 "uri": "00003.ts"
15 }, {
16 "duration": 10,
17 "uri": "00004.ts"
18 }]
19 }
1 #EXTM3U
2 #EXT-X-PLAYLIST-TYPE:VOD
3 #EXT-X-TARGETDURATION:10
4 #EXTINF:10,
5 00001.ts
6 #EXTINF:10,
7 00002.ts
8 #EXTINF:10,
9 00003.ts
10 #EXTINF:10,
11 00004.ts
12 #ZEN-TOTAL-DURATION:57.9911
13 #EXT-X-ENDLIST
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
9 9
10 <!-- video.js --> 10 <!-- video.js -->
11 <script src="../node_modules/video.js/dist/video-js/video.js"></script> 11 <script src="../node_modules/video.js/dist/video-js/video.js"></script>
12 <script src="../node_modules/videojs-media-sources/videojs-media-sources.js"></script>
12 13
13 <!-- HLS plugin --> 14 <!-- HLS plugin -->
14 <script src="../src/video-js-hls.js"></script> 15 <script src="../src/video-js-hls.js"></script>
...@@ -19,6 +20,7 @@ ...@@ -19,6 +20,7 @@
19 <script src="../src/segment-parser.js"></script> 20 <script src="../src/segment-parser.js"></script>
20 21
21 <!-- M3U8 --> 22 <!-- M3U8 -->
23 <script src="../src/stream.js"></script>
22 <script src="../src/m3u8/m3u8-parser.js"></script> 24 <script src="../src/m3u8/m3u8-parser.js"></script>
23 <script src="../src/manifest-controller.js"></script> 25 <script src="../src/manifest-controller.js"></script>
24 <!-- M3U8 TEST DATA --> 26 <!-- M3U8 TEST DATA -->
...@@ -36,6 +38,7 @@ ...@@ -36,6 +38,7 @@
36 <script src="exp-golomb_test.js"></script> 38 <script src="exp-golomb_test.js"></script>
37 <script src="flv-tag_test.js"></script> 39 <script src="flv-tag_test.js"></script>
38 <script src="m3u8_test.js"></script> 40 <script src="m3u8_test.js"></script>
41 <script src="manifest-loader_test.js"></script>
39 </head> 42 </head>
40 <body> 43 <body>
41 <div id="qunit"></div> 44 <div id="qunit"></div>
......