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 @@
<meta charset="utf-8">
<title>video.js HLS Plugin Example</title>
<link href="node_modules/video.js/video-js.css" rel="stylesheet">
<link href="node_modules/video.js/dist/video-js/video-js.css" rel="stylesheet">
<!-- video.js -->
<script src="node_modules/video.js/video.dev.js"></script>
<script src="node_modules/video.js/dist/video-js/video.js"></script>
<!-- Media Sources plugin -->
<script src="node_modules/videojs-media-sources/videojs-media-sources.js"></script>
......@@ -24,8 +24,7 @@
<script src="src/segment-controller.js"></script>
<!-- m3u8 handling -->
<script src="src/m3u8/m3u8.js"></script>
<script src="src/m3u8/m3u8-tag-types.js"></script>
<script src="src/stream.js"></script>
<script src="src/m3u8/m3u8-parser.js"></script>
<script src="src/manifest-controller.js"></script>
<script src="src/segment-controller.js"></script>
......@@ -46,18 +45,12 @@
controls>
</video>
<script>
var video, mediaSource;
videojs.options.flash.swf = 'node_modules/video.js/dist/video-js/video-js.swf';
// initialize the player
videojs.options.flash.swf = 'node_modules/videojs-media-sources/video-js-with-mse.swf';
video = videojs('video',{},function(){
this.playbackController = new window.videojs.hls.HLSPlaybackController(this);
this.playbackController.loadManifest('test/fixtures/prog_index.m3u8', function(data) {
console.log(data);
});
});
</script>
var player = videojs('video');
// load the master playlist
player.hls('test/fixtures/prog_index.m3u8');
</script>
</body>
</html>
......
(function(parseInt, isFinite, mergeOptions, undefined) {
(function(videojs, parseInt, isFinite, mergeOptions, undefined) {
var
noop = function() {},
parseAttributes = function(attributes) {
......@@ -13,49 +13,11 @@
}
return result;
},
Stream,
Stream = videojs.hls.Stream,
LineStream,
ParseStream,
Parser;
Stream = function() {
this.init = function() {
var listeners = {};
this.on = function(type, listener) {
if (!listeners[type]) {
listeners[type] = [];
}
listeners[type].push(listener);
};
this.off = function(type, listener) {
var index;
if (!listeners[type]) {
return false;
}
index = listeners[type].indexOf(listener);
listeners[type].splice(index, 1);
return index > -1;
};
this.trigger = function(type) {
var callbacks, i, length, args;
callbacks = listeners[type];
if (!callbacks) {
return;
}
args = Array.prototype.slice.call(arguments, 1);
length = callbacks.length;
for (i = 0; i < length; ++i) {
callbacks[i].apply(this, args);
}
};
};
};
Stream.prototype.pipe = function(destination) {
this.on('data', function(data) {
destination.push(data);
});
};
LineStream = function() {
var buffer = '';
LineStream.prototype.init.call(this);
......@@ -382,4 +344,4 @@
ParseStream: ParseStream,
Parser: Parser
};
})(window.parseInt, window.isFinite, window.videojs.util.mergeOptions);
})(window.videojs, window.parseInt, window.isFinite, window.videojs.util.mergeOptions);
......
/**
* A lightweight stream implemention that handles event dispatching. Objects
* that inherit from streams should call init in their constructors.
*/
(function(videojs, undefined) {
var Stream = function() {
this.init = function() {
var listeners = {};
this.on = function(type, listener) {
if (!listeners[type]) {
listeners[type] = [];
}
listeners[type].push(listener);
};
this.off = function(type, listener) {
var index;
if (!listeners[type]) {
return false;
}
index = listeners[type].indexOf(listener);
listeners[type].splice(index, 1);
return index > -1;
};
this.trigger = function(type) {
var callbacks, i, length, args;
callbacks = listeners[type];
if (!callbacks) {
return;
}
args = Array.prototype.slice.call(arguments, 1);
length = callbacks.length;
for (i = 0; i < length; ++i) {
callbacks[i].apply(this, args);
}
};
};
};
Stream.prototype.pipe = function(destination) {
this.on('data', function(data) {
destination.push(data);
});
};
videojs.hls.Stream = Stream;
})(window.videojs);
/*
* video-js-hls
*
*
*
* Copyright (c) 2013 Brightcove
* All rights reserved.
*/
(function(window) {
(function(window, videojs, undefined) {
videojs.hls = {};
videojs.plugin('hls', function(options) {
var
mediaSource = new videojs.MediaSource(),
segmentParser = new videojs.hls.SegmentParser(),
player = this,
url,
loadSegment,
selectPlaylist;
if (typeof options === 'string') {
url = options;
} else {
url = options.url;
}
// expose the HLS plugin state
player.hls.readyState = function() {
if (!player.hls.manifest) {
return 0; // HAVE_NOTHING
}
return 1; // HAVE_METADATA
};
// load the MediaSource into the player
mediaSource.addEventListener('sourceopen', function() {
// construct the video data buffer and set the appropriate MIME type
var sourceBuffer = mediaSource.addSourceBuffer('video/flv; codecs="vp6,aac"');
player.hls.sourceBuffer = sourceBuffer;
sourceBuffer.appendBuffer(segmentParser.getFlvHeader());
// Chooses the appropriate media playlist based on the current bandwidth
// estimate and the player size
selectPlaylist = function() {
player.hls.currentPlaylist = player.hls.manifest;
player.hls.currentMediaIndex = 0;
};
// download a new segment if one is needed
fillBuffer = function() {
var
xhr = new window.XMLHttpRequest(),
segment = player.hls.currentPlaylist.segments[player.hls.currentMediaIndex];
xhr.open('GET', segment.uri);
xhr.responseType = 'arraybuffer';
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
player.hls.currentMediaIndex++;
segmentParser.parseSegmentBinaryData(new Uint8Array(xhr.response));
while (segmentParser.tagsAvailable()) {
player.hls.sourceBuffer.appendBuffer(segmentParser.getNextTag().bytes,
player);
}
}
};
xhr.send(null);
};
player.on('loadedmetadata', fillBuffer);
// download and process the manifest
(function() {
var xhr = new window.XMLHttpRequest();
xhr.open('GET', url);
xhr.onreadystatechange = function() {
var parser;
if (xhr.readyState === 4) {
// readystate DONE
parser = new videojs.m3u8.Parser();
parser.push(xhr.responseText);
player.hls.manifest = parser.manifest;
player.trigger('loadedmanifest');
window.videojs.hls = {};
if (parser.manifest.segments) {
selectPlaylist();
player.trigger('loadedmetadata');
}
}
};
xhr.send(null);
})();
});
player.src({
src: videojs.URL.createObjectURL(mediaSource),
type: "video/flv"
});
});
})(this);
})(window, window.videojs);
......
(function(window, videojs, undefined) {
var player, oldXhr, oldSourceBuffer;
module('HLS', {
setup: function() {
var video = document.createElement('video');
document.querySelector('#qunit-fixture').appendChild(video);
player = videojs(video);
oldXhr = window.XMLHttpRequest;
oldSourceBuffer = window.videojs.SourceBuffer;
// mock out SourceBuffer since it won't be available in phantomjs
window.videojs.SourceBuffer = function() {
this.appendBuffer = function() {};
};
},
teardown: function() {
window.XMLHttpRequest = oldXhr;
window.videojs.SourceBuffer = oldSourceBuffer;
}
});
asyncTest('loads the specified manifest URL on init', function() {
var loadedmanifest = false;
player.on('loadedmanifest', function() {
loadedmanifest = true;
});
player.on('loadedmetadata', function() {
ok(loadedmanifest, 'loadedmanifest fires');
ok(player.hls.manifest, 'the manifest is available');
ok(player.hls.manifest.segments, 'the segments are parsed');
strictEqual(player.hls.manifest,
player.hls.currentPlaylist,
'a playlist is selected');
strictEqual(player.hls.readyState(), 1, 'the readyState is HAVE_METADATA');
start();
});
player.hls('manifest/playlist.m3u8');
strictEqual(player.hls.readyState(), 0, 'the readyState is HAVE_NOTHING');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
});
test('starts downloading a segment on loadedmetadata', function() {
var url;
window.XMLHttpRequest = function() {
this.open = function() {
url = arguments[1];
};
this.send = function() {
this.readyState = 4;
this.responseText = window.manifests['media'];
this.onreadystatechange();
};
};
player.hls('manifest/media.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
strictEqual(url, '00001.ts', 'the first segment is requested');
});
})(window, window.videojs);
{
"allowCache": true,
"targetDuration": 10,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [{
"duration": 10,
"uri": "00001.ts"
}, {
"duration": 10,
"uri": "00002.ts"
}, {
"duration": 10,
"uri": "00003.ts"
}, {
"duration": 10,
"uri": "00004.ts"
}]
}
#EXTM3U
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:10
#EXTINF:10,
00001.ts
#EXTINF:10,
00002.ts
#EXTINF:10,
00003.ts
#EXTINF:10,
00004.ts
#ZEN-TOTAL-DURATION:57.9911
#EXT-X-ENDLIST
......@@ -9,6 +9,7 @@
<!-- video.js -->
<script src="../node_modules/video.js/dist/video-js/video.js"></script>
<script src="../node_modules/videojs-media-sources/videojs-media-sources.js"></script>
<!-- HLS plugin -->
<script src="../src/video-js-hls.js"></script>
......@@ -19,6 +20,7 @@
<script src="../src/segment-parser.js"></script>
<!-- M3U8 -->
<script src="../src/stream.js"></script>
<script src="../src/m3u8/m3u8-parser.js"></script>
<script src="../src/manifest-controller.js"></script>
<!-- M3U8 TEST DATA -->
......@@ -36,6 +38,7 @@
<script src="exp-golomb_test.js"></script>
<script src="flv-tag_test.js"></script>
<script src="m3u8_test.js"></script>
<script src="manifest-loader_test.js"></script>
</head>
<body>
<div id="qunit"></div>
......