abfea271 by Gary Katsevman

Merge branch 'master' into saucelabs-take2

Conflicts:
	.travis.yml
2 parents 2279f842 58952618
.DS_Store
dist/*
/node_modules/
*~
*.iml
......
dist/*
*~
*.iml
*.swp
tmp/**
test/**
\ No newline at end of file
......@@ -9,7 +9,10 @@ notifications:
hipchat:
rooms:
secure: l5TTd5JuPAW883PtcyaIBcJI9Chr9JpsZPQAEUBKAgIEwzuS6y7t5arlkS1PwH6gi1FADzYDf+OXSIou4GkTSrIetnBcT/SAgF0gBKgIhj+eRkuCfZ4VaC7BPhfZ0hgYRE+5Ejf5BM2MJafRm0pj7OlqG4xKrQZwtuV1te5r3JY=
irc: chat.freenode.net#videojs
irc:
channels:
- "chat.freenode.net#videojs"
use_notice: true
env:
global:
- secure: dM7svnHPPu5IiUMeFWW5zg+iuWNpwt6SSDi3MmVvhSclNMRLesQoRB+7Qq5J/LiKhmjpv1/GlNVV0CTsHMRhZNwQ3fo38eEuTXv99aAflEITXwSEh/VntKViHbGFubn06EnVkJoH6MX3zJ6kbiwc2QdSQbywKzS6l6quUEpWpd0=
......
......@@ -24,7 +24,6 @@ module.exports = function(grunt) {
dist: {
nonull: true,
src: ['src/videojs-hls.js',
'src/async-queue.js',
'src/flv-tag.js',
'src/exp-golomb.js',
'src/h264-stream.js',
......
[![Build Status](https://travis-ci.org/videojs/videojs-contrib-hls.png)](https://travis-ci.org/videojs/videojs-contrib-hls)
# video.js HLS Plugin
A video.js plugin that plays HLS video on platforms that don't support it but have Flash.
[![Build Status](https://travis-ci.org/videojs/videojs-contrib-hls.svg?branch=master)](https://travis-ci.org/videojs/videojs-contrib-hls)
## Getting Started
Download the [plugin](https://github.com/videojs/videojs-contrib-hls/releases). On your web page:
......@@ -47,9 +47,26 @@ support for:
- Alternate audio and video tracks
- Subtitles
- Segment codecs _other than_ H.264 with AAC audio
- Live streams
- Internet Explorer < 10
### Plugin Options
You may pass in an options object to the hls plugin upon initialization. This
object may contain one of the following properties:
#### withCredentials
Type: `boolean`
When the `withCredentials` property is set to `true`, all XHR requests for
manifests and segments would have `withCredentials` set to `true` as well. This
enables storing and passing cookies from the server that the manifests and
segments live on. This has some implications on CORS because when set, the
`Access-Control-Allow-Origin` header cannot be set to `*`, also, the response
headers require the addition of `Access-Control-Allow-Credentials` header which
is set to `true`.
See html5rocks's [article](http://www.html5rocks.com/en/tutorials/cors/)
for more info.
### Runtime Properties
#### player.hls.master
Type: `object`
......@@ -119,6 +136,8 @@ bandwidth and viewport dimensions.
- [Best RESOLUTION variant] OR [Best BANDWIDTH variant] OR [inital playlist in manifest]
## Release History
- 0.5.0: cookie-based content protection support (see `withCredentials`)
- 0.4.0: Live stream support
- 0.3.0: Performance fixes for high-bitrate streams
- 0.2.0: Basic playback and adaptive bitrate selection
- 0.1.0: Initial release
......
......@@ -10,11 +10,10 @@
<script src="node_modules/video.js/dist/video-js/video.js"></script>
<!-- Media Sources plugin -->
<script src="node_modules/videojs-contrib-media-sources/videojs-media-sources.js"></script>
<script src="node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script>
<!-- HLS plugin -->
<script src="src/videojs-hls.js"></script>
<script src="src/async-queue.js"></script>
<!-- segment handling -->
<script src="src/flv-tag.js"></script>
......
{
"name": "videojs-contrib-hls",
"version": "0.3.2",
"version": "0.5.0",
"engines": {
"node": ">= 0.10.12"
},
......@@ -13,33 +13,34 @@
"test": "grunt test"
},
"devDependencies": {
"grunt": "~0.4.1",
"grunt-concurrent": "0.4.3",
"grunt-contrib-clean": "~0.4.0",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-connect": "~0.6.0",
"grunt-contrib-jshint": "~0.6.0",
"grunt-contrib-qunit": "~0.2.0",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-uglify": "~0.2.0",
"grunt-contrib-watch": "~0.4.0",
"grunt-contrib-clean": "~0.4.0",
"grunt-contrib-connect": "~0.6.0",
"grunt-concurrent": "0.4.3",
"grunt-karma": "~0.6.2",
"grunt-open": "0.2.3",
"grunt-shell": "0.6.1",
"grunt": "~0.4.1",
"grunt-karma": "~0.6.2",
"karma": "~0.10.0",
"karma-sauce-launcher": "~0.1.8",
"karma-chrome-launcher": "~0.1.2",
"karma-firefox-launcher": "~0.1.3",
"karma-ie-launcher": "~0.1.1",
"karma-opera-launcher": "~0.1.0",
"karma-phantomjs-launcher": "~0.1.1",
"karma-safari-launcher": "~0.1.1",
"karma-qunit": "~0.1.1",
"karma-safari-launcher": "~0.1.1",
"karma-sauce-launcher": "~0.1.8",
"sinon": "^1.9.1",
"video.js": "^4.5"
},
"peerDependencies": {
"video.js": "^4.5"
},
"dependencies": {
"videojs-contrib-media-sources": "git+https://github.com/videojs/videojs-contrib-media-sources.git"
"videojs-contrib-media-sources": "^0.2"
}
}
......
(function(window, videojs, undefined) {
'use strict';
/**
* A queue object that manages tasks that should be processed
* serially but asynchronously. Loosely adapted from
* https://github.com/caolan/async#queue.
* @param worker {function} the callback to invoke with each value
* pushed onto the queue
* @return {object} an object with an array of `tasks` that remain to
* be processed and function `push` to add new tasks
*/
videojs.hls.queue = function(worker) {
var
q = {
tasks: [],
running: false,
push: function(task) {
q.tasks.push(task);
if (!q.running) {
window.setTimeout(process, 0);
q.running = true;
}
}
},
process = function() {
var task;
if (q.tasks.length) {
task = q.tasks.shift();
worker.call(this, task);
window.setTimeout(process, 0);
} else {
q.running = false;
}
};
return q;
};
})(window, window.videojs);
......@@ -31,6 +31,9 @@ videojs.hls = {
};
var
settings,
// the desired length of video to maintain in the buffer, in seconds
goalBufferLength = 5,
......@@ -109,12 +112,26 @@ var
method: 'GET'
},
request;
if (typeof callback !== 'function') {
callback = function() {};
}
if (typeof url === 'object') {
options = videojs.util.mergeOptions(options, url);
url = options.url;
}
request = new window.XMLHttpRequest();
request.open(options.method, url);
if (options.responseType) {
request.responseType = options.responseType;
}
if (settings.withCredentials) {
request.withCredentials = true;
}
request.onreadystatechange = function() {
// wait until the request completes
if (this.readyState !== 4) {
......@@ -204,13 +221,8 @@ var
totalDuration = function(playlist) {
var
duration = 0,
i,
segment;
if (!playlist.segments) {
return 0;
}
i = playlist.segments.length;
segment,
i = (playlist.segments || []).length;
// if present, use the duration specified in the playlist
if (playlist.totalDuration) {
......@@ -277,28 +289,22 @@ var
mediaSource = new videojs.MediaSource(),
segmentParser = new videojs.hls.SegmentParser(),
player = this,
// async queue of Uint8Arrays to be appended to the SourceBuffer
tags = videojs.hls.queue(function(tag) {
player.hls.sourceBuffer.appendBuffer(tag, player);
if (player.hls.mediaIndex === player.hls.media.segments.length) {
mediaSource.endOfStream();
}
}),
srcUrl,
playlistXhr,
segmentXhr,
loadedPlaylist,
fillBuffer,
updateCurrentPlaylist;
updateCurrentPlaylist,
updateDuration;
// if the video element supports HLS natively, do nothing
if (videojs.hls.supportsNativeHls) {
return;
}
settings = videojs.util.mergeOptions({}, options);
srcUrl = (function() {
var
extname,
......@@ -312,7 +318,7 @@ var
// use the URL specified in options if one was provided
if (typeof options === 'string') {
return options;
} else if (options) {
} else if (options && options.url) {
return options.url;
}
......@@ -370,17 +376,35 @@ var
var currentTime = player.currentTime();
player.hls.mediaIndex = getMediaIndexByTime(player.hls.media, currentTime);
// abort any segments still being decoded
player.hls.sourceBuffer.abort();
// cancel outstanding requests and buffer appends
if (segmentXhr) {
segmentXhr.abort();
}
tags.tasks = [];
// begin filling the buffer at the new position
fillBuffer(currentTime * 1000);
});
/**
* Update the player duration
*/
updateDuration = function(playlist) {
var tech;
// update the duration
player.duration(totalDuration(playlist));
// tell the flash tech of the new duration
tech = player.el().querySelector('.vjs-tech');
if(tech.vjs_setProperty) {
tech.vjs_setProperty('duration', player.duration());
}
// manually fire the duration change
player.trigger('durationchange');
};
/**
* Determine whether the current media playlist should be changed
* and trigger a switch if necessary. If a sufficiently fresh
* version of the target playlist is available, the switch will take
......@@ -406,8 +430,7 @@ var
playlist);
player.hls.media = playlist;
// update the duration
player.duration(totalDuration(player.hls.media));
updateDuration(player.hls.media);
}
};
......@@ -558,7 +581,7 @@ var
player.hls.media = player.hls.master.playlists[0];
// update the duration
player.duration(totalDuration(parser.manifest));
updateDuration(parser.manifest);
// periodicaly check if the buffer needs to be refilled
player.on('timeupdate', fillBuffer);
......@@ -585,7 +608,7 @@ var
var
buffered = player.buffered(),
bufferedTime = 0,
segment = player.hls.media.segments[player.hls.mediaIndex],
segment,
segmentUri,
startTime;
......@@ -594,7 +617,13 @@ var
return;
}
// if no segments are available, do nothing
if (!player.hls.media.segments) {
return;
}
// if the video has finished downloading, stop trying to buffer
segment = player.hls.media.segments[player.hls.mediaIndex];
if (!segment) {
return;
}
......@@ -617,24 +646,20 @@ var
segment.uri);
}
// request the next segment
segmentXhr = new window.XMLHttpRequest();
segmentXhr.open('GET', segmentUri);
segmentXhr.responseType = 'arraybuffer';
segmentXhr.onreadystatechange = function() {
// wait until the request completes
if (this.readyState !== 4) {
return;
}
startTime = +new Date();
// request the next segment
segmentXhr = xhr({
url: segmentUri,
responseType: 'arraybuffer'
}, function(error, url) {
// the segment request is no longer outstanding
segmentXhr = null;
// trigger an error if the request was not successful
if (this.status >= 400) {
if (error) {
player.hls.error = {
status: this.status,
message: 'HLS segment request error at URL: ' + segmentUri,
message: 'HLS segment request error at URL: ' + url,
code: (this.status >= 500) ? 4 : 2
};
......@@ -669,17 +694,22 @@ var
// queue up the bytes to be appended to the SourceBuffer
// the queue gives control back to the browser between tags
// so that large segments don't cause a "hiccup" in playback
tags.push(segmentParser.getNextTag().bytes);
player.hls.sourceBuffer.appendBuffer(segmentParser.getNextTag().bytes,
player);
}
player.hls.mediaIndex++;
if (player.hls.mediaIndex === player.hls.media.segments.length) {
mediaSource.endOfStream();
}
// figure out what stream the next segment should be downloaded from
// with the updated bandwidth information
updateCurrentPlaylist();
};
startTime = +new Date();
segmentXhr.send(null);
});
};
// load the MediaSource into the player
......
......@@ -28,6 +28,7 @@
"strictEqual",
"notStrictEqual",
"throws",
"sinon",
"process"
]
}
......
(function(window, queue, undefined) {
var
oldSetTimeout,
callbacks;
module('async queue', {
setup: function() {
oldSetTimeout = window.setTimeout;
callbacks = [];
window.setTimeout = function(callback) {
callbacks.push(callback);
};
},
teardown: function() {
window.setTimeout = oldSetTimeout;
}
});
test('runs tasks asynchronously', function() {
var
run = false,
q = queue(function() {
run = true;
});
q.push(1);
ok(!run, 'tasks are not run immediately');
callbacks[0]();
ok(run, 'tasks are run asynchronously');
});
test('runs one task at a time', function() {
var q = queue(function() {});
q.push(1);
q.push(2);
q.push(3);
q.push(4);
q.push(5);
strictEqual(q.tasks.length, 5, 'all tasks are queued');
strictEqual(1, callbacks.length, 'one callback is registered');
});
test('tasks are scheduled until the queue is empty', function() {
var q = queue(function() {});
q.push(1);
q.push(2);
callbacks.shift()();
strictEqual(1, callbacks.length, 'the next task is scheduled');
callbacks.shift()();
strictEqual(1, callbacks.length, 'nothing is scheduled on an empty queue');
});
test('can be emptied at any time', function() {
var
runs = 0,
q = queue(function() {
runs++;
});
q.push(1);
q.push(2);
callbacks.shift()();
strictEqual(1, runs, 'task one is run');
q.tasks = [];
callbacks.shift()();
strictEqual(1, runs, 'the remaining tasks are cancelled');
});
})(window, window.videojs.hls.queue);
......@@ -40,7 +40,7 @@ module.exports = function(config) {
files: [
'../node_modules/video.js/dist/video-js/video.js',
'../node_modules/videojs-contrib-media-sources/videojs-media-sources.js',
'../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js',
'../test/karma-qunit-shim.js',
"../src/videojs-hls.js",
"../src/flv-tag.js",
......
......@@ -35,7 +35,7 @@ module.exports = function(config) {
files: [
'../node_modules/video.js/dist/video-js/video.js',
'../node_modules/videojs-contrib-media-sources/videojs-media-sources.js',
'../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js',
'../test/karma-qunit-shim.js',
"../src/videojs-hls.js",
"../src/flv-tag.js",
......
......@@ -3,13 +3,19 @@
<head>
<meta charset="utf-8">
<title>video.js HLS Plugin Test Suite</title>
<!-- Load sinon server for fakeXHR -->
<script src="../node_modules/sinon/lib/sinon.js"></script>
<script src="../node_modules/sinon/lib/sinon/util/event.js"></script>
<script src="../node_modules/sinon/lib/sinon/util/xhr_ie.js"></script>
<script src="../node_modules/sinon/lib/sinon/util/fake_xml_http_request.js"></script>
<!-- Load local QUnit. -->
<link rel="stylesheet" href="../libs/qunit/qunit.css" media="screen">
<script src="../libs/qunit/qunit.js"></script>
<!-- video.js -->
<script src="../node_modules/video.js/dist/video-js/video.js"></script>
<script src="../node_modules/videojs-contrib-media-sources/videojs-media-sources.js"></script>
<script src="../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script>
<!-- HLS plugin -->
<script src="../src/videojs-hls.js"></script>
......@@ -31,9 +37,6 @@
<script src="tsSegment-bc.js"></script>
<script src="../src/bin-utils.js"></script>
<!-- async queue -->
<script src="../src/async-queue.js"></script>
<!-- Test cases -->
<script>
module('environment');
......@@ -48,7 +51,6 @@
<script src="exp-golomb_test.js"></script>
<script src="flv-tag_test.js"></script>
<script src="m3u8_test.js"></script>
<script src="async-queue_test.js"></script>
</head>
<body>
<div id="qunit"></div>
......