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.
Showing
8 changed files
with
251 additions
and
60 deletions
... | @@ -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); | ... | ... |
src/stream.js
0 → 100644
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); |
1 | /* | 1 | /* |
2 | * video-js-hls | 2 | * video-js-hls |
3 | * | 3 | * |
4 | * | 4 | * |
5 | * Copyright (c) 2013 Brightcove | 5 | * Copyright (c) 2013 Brightcove |
6 | * All rights reserved. | 6 | * All rights reserved. |
7 | */ | 7 | */ |
8 | 8 | ||
9 | (function(window) { | 9 | (function(window, videojs, undefined) { |
10 | |||
11 | videojs.hls = {}; | ||
12 | |||
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'); | ||
10 | 86 | ||
11 | window.videojs.hls = {}; | 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 | }); | ||
12 | 101 | ||
13 | })(this); | 102 | })(window, window.videojs); | ... | ... |
test/manifest-loader_test.js
0 → 100644
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); |
test/manifest/media.json
0 → 100644
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 | } |
test/manifest/media.m3u8
0 → 100644
... | @@ -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> | ... | ... |
-
Please register or sign in to post a comment