Browse Source

Upgrade to video.js 5 and convert to a source handler

Bump up to a vjs 5 release candidate. Make the necessary changes to work with the updated APIs. Convert the project from a subclass of the Flash tech to a source handler.
pull/6/head
David LaPalomento 10 years ago
parent
commit
7094ab9636
  1. 7
      example.html
  2. 244
      libs/qunit/qunit.css
  3. 2152
      libs/qunit/qunit.js
  4. 6
      package.json
  5. 4
      src/decrypter.js
  6. 2
      src/m3u8/m3u8-parser.js
  7. 37
      src/playlist-loader.js
  8. 400
      src/videojs-hls.js
  9. 92
      src/xhr.js
  10. 9
      test/karma.conf.js
  11. 9
      test/localkarma.conf.js
  12. 4
      test/segment-parser.js
  13. 14
      test/videojs-hls.html
  14. 847
      test/videojs-hls_test.js
  15. 34
      test/xhr_test.js

7
example.html

@ -4,10 +4,10 @@
<meta charset="utf-8">
<title>video.js HLS Plugin Example</title>
<link href="node_modules/video.js/dist/video-js/video-js.css" rel="stylesheet">
<link href="node_modules/video.js/dist/video-js.css" rel="stylesheet">
<!-- video.js -->
<script src="node_modules/video.js/dist/video-js/video.dev.js"></script>
<script src="node_modules/video.js/dist/video.js"></script>
<!-- Media Sources plugin -->
<script src="node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script>
@ -72,8 +72,7 @@
type="application/x-mpegURL">
</video>
<script>
videojs.options.flash.swf = 'node_modules/videojs-swf/dist/video-js.swf';
videojs.getGlobalOptions().flash.swf = 'node_modules/videojs-swf/dist/video-js.swf';
// initialize the player
var player = videojs('video');
</script>

244
libs/qunit/qunit.css

@ -1,244 +0,0 @@
/**
* QUnit v1.11.0 - A JavaScript Unit Testing Framework
*
* http://qunitjs.com
*
* Copyright 2012 jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*/
/** Font Family and Sizes */
#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
}
#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
#qunit-tests { font-size: smaller; }
/** Resets */
#qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
margin: 0;
padding: 0;
}
/** Header */
#qunit-header {
padding: 0.5em 0 0.5em 1em;
color: #8699a4;
background-color: #0d3349;
font-size: 1.5em;
line-height: 1em;
font-weight: normal;
border-radius: 5px 5px 0 0;
-moz-border-radius: 5px 5px 0 0;
-webkit-border-top-right-radius: 5px;
-webkit-border-top-left-radius: 5px;
}
#qunit-header a {
text-decoration: none;
color: #c2ccd1;
}
#qunit-header a:hover,
#qunit-header a:focus {
color: #fff;
}
#qunit-testrunner-toolbar label {
display: inline-block;
padding: 0 .5em 0 .1em;
}
#qunit-banner {
height: 5px;
}
#qunit-testrunner-toolbar {
padding: 0.5em 0 0.5em 2em;
color: #5E740B;
background-color: #eee;
overflow: hidden;
}
#qunit-userAgent {
padding: 0.5em 0 0.5em 2.5em;
background-color: #2b81af;
color: #fff;
text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
}
#qunit-modulefilter-container {
float: right;
}
/** Tests: Pass/Fail */
#qunit-tests {
list-style-position: inside;
}
#qunit-tests li {
padding: 0.4em 0.5em 0.4em 2.5em;
border-bottom: 1px solid #fff;
list-style-position: inside;
}
#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running {
display: none;
}
#qunit-tests li strong {
cursor: pointer;
}
#qunit-tests li a {
padding: 0.5em;
color: #c2ccd1;
text-decoration: none;
}
#qunit-tests li a:hover,
#qunit-tests li a:focus {
color: #000;
}
#qunit-tests li .runtime {
float: right;
font-size: smaller;
}
.qunit-assert-list {
margin-top: 0.5em;
padding: 0.5em;
background-color: #fff;
border-radius: 5px;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
}
.qunit-collapsed {
display: none;
}
#qunit-tests table {
border-collapse: collapse;
margin-top: .2em;
}
#qunit-tests th {
text-align: right;
vertical-align: top;
padding: 0 .5em 0 0;
}
#qunit-tests td {
vertical-align: top;
}
#qunit-tests pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
#qunit-tests del {
background-color: #e0f2be;
color: #374e0c;
text-decoration: none;
}
#qunit-tests ins {
background-color: #ffcaca;
color: #500;
text-decoration: none;
}
/*** Test Counts */
#qunit-tests b.counts { color: black; }
#qunit-tests b.passed { color: #5E740B; }
#qunit-tests b.failed { color: #710909; }
#qunit-tests li li {
padding: 5px;
background-color: #fff;
border-bottom: none;
list-style-position: inside;
}
/*** Passing Styles */
#qunit-tests li li.pass {
color: #3c510c;
background-color: #fff;
border-left: 10px solid #C6E746;
}
#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
#qunit-tests .pass .test-name { color: #366097; }
#qunit-tests .pass .test-actual,
#qunit-tests .pass .test-expected { color: #999999; }
#qunit-banner.qunit-pass { background-color: #C6E746; }
/*** Failing Styles */
#qunit-tests li li.fail {
color: #710909;
background-color: #fff;
border-left: 10px solid #EE5757;
white-space: pre;
}
#qunit-tests > li:last-child {
border-radius: 0 0 5px 5px;
-moz-border-radius: 0 0 5px 5px;
-webkit-border-bottom-right-radius: 5px;
-webkit-border-bottom-left-radius: 5px;
}
#qunit-tests .fail { color: #000000; background-color: #EE5757; }
#qunit-tests .fail .test-name,
#qunit-tests .fail .module-name { color: #000000; }
#qunit-tests .fail .test-actual { color: #EE5757; }
#qunit-tests .fail .test-expected { color: green; }
#qunit-banner.qunit-fail { background-color: #EE5757; }
/** Result */
#qunit-testresult {
padding: 0.5em 0.5em 0.5em 2.5em;
color: #2b81af;
background-color: #D2E0E6;
border-bottom: 1px solid white;
}
#qunit-testresult .module-name {
font-weight: bold;
}
/** Fixture */
#qunit-fixture {
position: absolute;
top: -10000px;
left: -10000px;
width: 1000px;
height: 1000px;
}

2152
libs/qunit/qunit.js
File diff suppressed because it is too large
View File

6
package.json

@ -42,13 +42,13 @@
"karma-qunit": "~0.1.1",
"karma-safari-launcher": "~0.1.1",
"karma-sauce-launcher": "~0.1.8",
"qunitjs": "^1.15.0",
"qunitjs": "^1.18.0",
"sinon": "1.10.2",
"video.js": "^4.12.0"
"video.js": "^5.0.0-rc.4"
},
"dependencies": {
"pkcs7": "^0.2.2",
"videojs-contrib-media-sources": "^1.0.0",
"videojs-swf": "^4.7.0"
"videojs-swf": "5.0.0-rc0"
}
}

4
src/decrypter.js

@ -299,7 +299,7 @@ AsyncStream.prototype = new videojs.Hls.Stream();
AsyncStream.prototype.processJob_ = function() {
this.jobs.shift()();
if (this.jobs.length) {
this.timeout_ = setTimeout(videojs.bind(this, this.processJob_),
this.timeout_ = setTimeout(this.processJob_.bind(this),
this.delay);
} else {
this.timeout_ = null;
@ -308,7 +308,7 @@ AsyncStream.prototype.processJob_ = function() {
AsyncStream.prototype.push = function(job) {
this.jobs.push(job);
if (!this.timeout_) {
this.timeout_ = setTimeout(videojs.bind(this, this.processJob_),
this.timeout_ = setTimeout(this.processJob_.bind(this),
this.delay);
}
};

2
src/m3u8/m3u8-parser.js

@ -585,4 +585,4 @@
ParseStream: ParseStream,
Parser: Parser
};
})(window.videojs, window.parseInt, window.isFinite, window.videojs.util.mergeOptions);
})(window.videojs, window.parseInt, window.isFinite, window.videojs.mergeOptions);

37
src/playlist-loader.js

@ -18,7 +18,7 @@
resolveUrl = videojs.Hls.resolveUrl,
xhr = videojs.Hls.xhr,
Playlist = videojs.Hls.Playlist,
mergeOptions = videojs.util.mergeOptions,
mergeOptions = videojs.mergeOptions,
/**
* Returns a new master playlist that is the result of merging an
@ -262,10 +262,10 @@
// request the new playlist
request = xhr({
url: resolveUrl(loader.master.uri, playlist.uri),
uri: resolveUrl(loader.master.uri, playlist.uri),
withCredentials: withCredentials
}, function(error) {
haveMetadata(error, this, playlist.uri);
}, function(error, request) {
haveMetadata(error, request, playlist.uri);
loader.trigger('mediachange');
});
};
@ -283,32 +283,35 @@
loader.state = 'HAVE_CURRENT_METADATA';
request = xhr({
url: resolveUrl(loader.master.uri, loader.media().uri),
uri: resolveUrl(loader.master.uri, loader.media().uri),
withCredentials: withCredentials
}, function(error) {
haveMetadata(error, this, loader.media().uri);
}, function(error, request) {
haveMetadata(error, request, loader.media().uri);
});
});
// request the specified URL
xhr({
url: srcUrl,
request = xhr({
uri: srcUrl,
withCredentials: withCredentials
}, function(error) {
}, function(error, req) {
var parser, i;
// clear the loader's request reference
request = null;
if (error) {
loader.error = {
status: this.status,
status: req.status,
message: 'HLS playlist request error at URL: ' + srcUrl,
responseText: this.responseText,
responseText: req.responseText,
code: 2 // MEDIA_ERR_NETWORK
};
return loader.trigger('error');
}
parser = new videojs.m3u8.Parser();
parser.push(this.responseText);
parser.push(req.responseText);
parser.end();
loader.state = 'HAVE_MASTER';
@ -326,12 +329,12 @@
}
request = xhr({
url: resolveUrl(srcUrl, parser.manifest.playlists[0].uri),
uri: resolveUrl(srcUrl, parser.manifest.playlists[0].uri),
withCredentials: withCredentials
}, function(error) {
}, function(error, request) {
// pass along the URL specified in the master playlist
haveMetadata(error,
this,
request,
parser.manifest.playlists[0].uri);
if (!error) {
loader.trigger('loadedmetadata');
@ -349,7 +352,7 @@
}]
};
loader.master.playlists[srcUrl] = loader.master.playlists[0];
haveMetadata(null, this, srcUrl);
haveMetadata(null, req, srcUrl);
return loader.trigger('loadedmetadata');
});
};

400
src/videojs-hls.js

@ -13,6 +13,8 @@ var
// the amount of time to wait between checking the state of the buffer
bufferCheckInterval = 500,
Component = videojs.getComponent('Component'),
keyXhr,
keyFailed,
resolveUrl;
@ -22,30 +24,39 @@ keyFailed = function(key) {
return key.retries && key.retries >= 2;
};
videojs.Hls = videojs.Flash.extend({
init: function(player, options, ready) {
var
source = options.source,
settings = player.options();
player.hls = this;
delete options.source;
options.swf = settings.flash.swf;
videojs.Flash.call(this, player, options, ready);
options.source = source;
videojs.Hls = videojs.extends(Component, {
constructor: function(tech, source) {
var self = this, _player;
Component.call(this, tech);
// tech.player() is deprecated but setup a reference to HLS for
// backwards-compatibility
if (tech.options_ && tech.options_.playerId) {
_player = videojs(tech.options_.playerId);
if (!_player.tech.hls) {
Object.defineProperty(_player, 'hls', {
get: function() {
videojs.log.warn('player.hls is deprecated. Use player.tech.hls instead.');
return self;
}
});
}
}
this.tech_ = tech;
this.source_ = source;
this.bytesReceived = 0;
this.hasPlayed_ = false;
this.on(player, 'loadstart', function() {
this.hasPlayed_ = false;
this.one(this.mediaSource, 'sourceopen', this.setupFirstPlay);
});
this.on(player, ['play', 'loadedmetadata'], this.setupFirstPlay);
// TODO: After video.js#1347 is pulled in remove these lines
this.currentTime = videojs.Hls.prototype.currentTime;
this.setCurrentTime = videojs.Hls.prototype.setCurrentTime;
// loadingState_ tracks how far along the buffering process we
// have been given permission to proceed. There are three possible
// values:
// - none: do not load playlists or segments
// - meta: load playlists but not segments
// - segments: load everything
this.loadingState_ = 'none';
if (this.tech_.preload() !== 'none') {
this.loadingState_ = 'meta';
}
// a queue of segments that need to be transmuxed and processed,
// and then fed to the source buffer
@ -54,21 +65,32 @@ videojs.Hls = videojs.Flash.extend({
// buffered data should be appended to the source buffer
this.startCheckingBuffer_();
videojs.Hls.prototype.src.call(this, options.source && options.source.src);
this.on(this.tech_, 'seeking', function() {
this.setCurrentTime(this.tech_.currentTime());
});
this.on(this.tech_, 'play', this.play);
}
});
// Add HLS to the standard tech order
videojs.options.techOrder.unshift('hls');
// add HLS as a source handler
videojs.getComponent('Flash').registerSourceHandler({
canHandleSource: function(srcObj) {
var mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i;
return mpegurlRE.test(srcObj.type);
},
handleSource: function(source, tech) {
tech.hls = new videojs.Hls(tech, source);
tech.hls.src(source.src);
return tech.hls;
}
});
// the desired length of video to maintain in the buffer, in seconds
videojs.Hls.GOAL_BUFFER_LENGTH = 30;
videojs.Hls.prototype.src = function(src) {
var
tech = this,
player = this.player(),
settings = player.options().hls || {},
mediaSource,
oldMediaPlaylist,
source;
@ -78,13 +100,6 @@ videojs.Hls.prototype.src = function(src) {
return;
}
// if there is already a source loaded, clean it up
if (this.src_) {
this.resetSrc_();
}
this.src_ = src;
mediaSource = new videojs.MediaSource();
source = {
src: videojs.URL.createObjectURL(mediaSource),
@ -100,12 +115,7 @@ videojs.Hls.prototype.src = function(src) {
this.setupMetadataCueTranslation_();
// load the MediaSource into the player
this.mediaSource.addEventListener('sourceopen', videojs.bind(this, this.handleSourceOpen));
// cleanup the old playlist loader, if necessary
if (this.playlists) {
this.playlists.dispose();
}
this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen.bind(this));
// The index of the next segment to be downloaded in the current
// media playlist. When the current media playlist is live with
@ -113,14 +123,28 @@ videojs.Hls.prototype.src = function(src) {
// sequence number for a segment.
this.mediaIndex = 0;
this.playlists = new videojs.Hls.PlaylistLoader(this.src_, settings.withCredentials);
this.options_ = {};
if (this.source_.withCredentials !== undefined) {
this.options_.withCredentials = this.source_.withCredentials;
} else if (videojs.getGlobalOptions().hls) {
this.options_.withCredentials = videojs.getGlobalOptions().hls.withCredentials;
}
this.playlists = new videojs.Hls.PlaylistLoader(this.source_.src, this.options_.withCredentials);
this.playlists.on('loadedmetadata', videojs.bind(this, function() {
this.playlists.on('loadedmetadata', function() {
var selectedPlaylist, loaderHandler, oldBitrate, newBitrate, segmentDuration,
segmentDlTime, threshold;
oldMediaPlaylist = this.playlists.media();
// if this isn't a live video and preload permits, start
// downloading segments
if (oldMediaPlaylist.endList &&
this.tech_.preload() !== 'metadata' &&
this.tech_.preload() !== 'none') {
this.loadingState_ = 'segments';
}
// the bandwidth estimate for the first segment is based on round
// trip time for the master playlist. the master playlist is
// almost always tiny so the round-trip time is dominated by
@ -157,25 +181,25 @@ videojs.Hls.prototype.src = function(src) {
if (newBitrate > oldBitrate && segmentDlTime <= threshold) {
this.playlists.media(selectedPlaylist);
loaderHandler = videojs.bind(this, function() {
loaderHandler = function() {
this.setupFirstPlay();
this.fillBuffer();
player.trigger('loadedmetadata');
this.tech_.trigger('loadedmetadata');
this.playlists.off('loadedplaylist', loaderHandler);
});
}.bind(this);
this.playlists.on('loadedplaylist', loaderHandler);
} else {
this.setupFirstPlay();
this.fillBuffer();
player.trigger('loadedmetadata');
this.tech_.trigger('loadedmetadata');
}
}));
}.bind(this));
this.playlists.on('error', videojs.bind(this, function() {
player.error(this.playlists.error);
}));
this.playlists.on('error', function() {
this.tech_.error(this.playlists.error);
}.bind(this));
this.playlists.on('loadedplaylist', videojs.bind(this, function() {
this.playlists.on('loadedplaylist', function() {
var updatedPlaylist = this.playlists.media();
if (!updatedPlaylist) {
@ -188,25 +212,23 @@ videojs.Hls.prototype.src = function(src) {
oldMediaPlaylist = updatedPlaylist;
this.fetchKeys_();
}));
}.bind(this));
this.playlists.on('mediachange', videojs.bind(this, function() {
this.playlists.on('mediachange', function() {
// abort outstanding key requests and check if new keys need to be retrieved
if (keyXhr) {
this.cancelKeyXhr();
}
player.trigger('mediachange');
}));
this.tech_.trigger({ type: 'mediachange', bubbles: true });
}.bind(this));
this.player().ready(function() {
// do nothing if the tech has been disposed already
// this can occur if someone sets the src in player.ready(), for instance
if (!tech.el()) {
return;
}
tech.el().vjs_src(source.src);
});
// do nothing if the tech has been disposed already
// this can occur if someone sets the src in player.ready(), for instance
if (!this.tech_.el()) {
return;
}
this.tech_.el().vjs_src(source.src);
};
/* Returns the media index for the live point in the current playlist, and updates
@ -231,9 +253,7 @@ videojs.Hls.getMediaIndexForLive_ = function(selectedPlaylist) {
videojs.Hls.prototype.handleSourceOpen = function() {
// construct the video data buffer and set the appropriate MIME type
var
player = this.player(),
sourceBuffer = this.mediaSource.addSourceBuffer('video/flv; codecs="vp6,aac"');
var sourceBuffer = this.mediaSource.addSourceBuffer('video/flv; codecs="vp6,aac"');
this.sourceBuffer = sourceBuffer;
@ -243,8 +263,8 @@ videojs.Hls.prototype.handleSourceOpen = function() {
// NOTE: moving this invocation of play() after
// sourceBuffer.appendBuffer() below caused live streams with
// autoplay to stall
if (player.options().autoplay) {
player.play();
if (this.tech_.autoplay()) {
this.play();
}
sourceBuffer.appendBuffer(this.segmentParser_.getFlvHeader());
@ -254,13 +274,12 @@ videojs.Hls.prototype.handleSourceOpen = function() {
// VTTCues on a text track
videojs.Hls.prototype.setupMetadataCueTranslation_ = function() {
var
tech = this,
metadataStream = tech.segmentParser_.metadataStream,
metadataStream = this.segmentParser_.metadataStream,
textTrack;
// only expose metadata tracks to video.js versions that support
// dynamic text tracks (4.12+)
if (!tech.player().addTextTrack) {
if (!this.tech_.addTextTrack) {
return;
}
@ -272,7 +291,7 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() {
// create the metadata track if this is the first ID3 tag we've
// seen
if (!textTrack) {
textTrack = tech.player().addTextTrack('metadata', 'Timed Metadata');
textTrack = this.tech_.addTextTrack('metadata', 'Timed Metadata');
// build the dispatch type from the stream descriptor
// https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track
@ -284,23 +303,23 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() {
}
// store this event for processing once the muxing has finished
tech.segmentBuffer_[0].pendingMetadata.push({
this.tech_.segmentBuffer_[0].pendingMetadata.push({
textTrack: textTrack,
metadata: metadata
});
});
}.bind(this));
// when seeking, clear out all cues ahead of the earliest position
// in the new segment. keep earlier cues around so they can still be
// programmatically inspected even though they've already fired
tech.on(tech.player(), 'seeking', function() {
this.on(this.tech_, 'seeking', function() {
var media, startTime, i;
if (!textTrack) {
return;
}
media = tech.playlists.media();
startTime = tech.playlists.expiredPreDiscontinuity_ + tech.playlists.expiredPostDiscontinuity_;
startTime += videojs.Hls.Playlist.duration(media, media.mediaSequence, media.mediaSequence + tech.mediaIndex);
media = this.playlists.media();
startTime = this.tech_.playlists.expiredPreDiscontinuity_ + this.tech_.playlists.expiredPostDiscontinuity_;
startTime += videojs.Hls.Playlist.duration(media, media.mediaSequence, media.mediaSequence + this.tech_.mediaIndex);
i = textTrack.cues.length;
while (i--) {
@ -348,20 +367,15 @@ videojs.Hls.prototype.setupFirstPlay = function() {
media = this.playlists.media();
// check that everything is ready to begin buffering
if (!this.hasPlayed_ &&
if (this.duration() === Infinity &&
this.tech_.played().length === 0 &&
this.sourceBuffer &&
media &&
this.paused() === false) {
media) {
// only run this block once per video
this.hasPlayed_ = true;
if (this.duration() === Infinity) {
// seek to the latest media position for live videos
seekable = this.seekable();
if (seekable.length) {
this.setCurrentTime(seekable.end(0));
}
// seek to the latest media position for live videos
seekable = this.seekable();
if (seekable.length) {
this.tech_.setCurrentTime(seekable.end(0));
}
}
};
@ -371,35 +385,23 @@ videojs.Hls.prototype.setupFirstPlay = function() {
* ended.
*/
videojs.Hls.prototype.play = function() {
if (this.ended()) {
this.loadingState_ = 'segments';
if (this.tech_.ended()) {
this.mediaIndex = 0;
}
if (!this.hasPlayed_) {
videojs.Flash.prototype.play.apply(this, arguments);
if (this.tech_.played().length === 0) {
return this.setupFirstPlay();
}
// if the viewer has paused and we fell out of the live window,
// seek forward to the earliest available position
if (this.duration() === Infinity &&
this.currentTime() < this.seekable().start(0)) {
this.setCurrentTime(this.seekable().start(0));
}
// delegate back to the Flash implementation
videojs.Flash.prototype.play.apply(this, arguments);
};
videojs.Hls.prototype.currentTime = function() {
if (this.lastSeekedTime_) {
return this.lastSeekedTime_;
}
// currentTime is zero while the tech is initializing
if (!this.el() || !this.el().vjs_getProperty) {
return 0;
if (this.tech_.duration() === Infinity) {
if (this.tech_.currentTime() < this.tech_.seekable().start(0)) {
this.tech_.setCurrentTime(this.tech_.seekable().start(0));
}
}
return this.el().vjs_getProperty('currentTime');
};
videojs.Hls.prototype.setCurrentTime = function(currentTime) {
@ -414,17 +416,6 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) {
return 0;
}
// clamp seeks to the available seekable time range
if (currentTime < this.seekable().start(0)) {
currentTime = this.seekable().start(0);
} else if (currentTime > this.seekable().end(0)) {
currentTime = this.seekable().end(0);
}
// save the seek target so currentTime can report it correctly
// while the seek is pending
this.lastSeekedTime_ = currentTime;
// determine the requested segment
this.mediaIndex = this.playlists.getMediaIndexForTime_(currentTime);
@ -471,6 +462,7 @@ videojs.Hls.prototype.seekable = function() {
// report the seekable range relative to the earliest possible
// position when the stream was first loaded
currentSeekable = videojs.Hls.Playlist.seekable(media);
if (!currentSeekable.length) {
return currentSeekable;
}
@ -484,13 +476,22 @@ videojs.Hls.prototype.seekable = function() {
* Update the player duration
*/
videojs.Hls.prototype.updateDuration = function(playlist) {
var player = this.player(),
oldDuration = player.duration(),
newDuration = videojs.Hls.Playlist.duration(playlist);
var oldDuration = this.mediaSource.duration(),
newDuration = videojs.Hls.Playlist.duration(playlist),
setDuration = function() {
this.mediaSource.duration(newDuration);
this.tech_.trigger('durationchange');
this.mediaSource.removeEventListener('sourceopen', setDuration);
}.bind(this);
// if the duration has changed, invalidate the cached value
if (oldDuration !== newDuration) {
player.trigger('durationchange');
if (this.mediaSource.readyState === 'open') {
this.mediaSource.duration(newDuration);
this.tech_.trigger('durationchange');
} else {
this.mediaSource.addEventListener('sourceopen', setDuration);
}
}
};
@ -536,8 +537,6 @@ videojs.Hls.prototype.dispose = function() {
}
this.resetSrc_();
videojs.Flash.prototype.dispose.call(this);
};
/**
@ -548,7 +547,6 @@ videojs.Hls.prototype.dispose = function() {
*/
videojs.Hls.prototype.selectPlaylist = function () {
var
player = this.player(),
effectiveBitrate,
sortedPlaylists = this.playlists.master.playlists.slice(),
bandwidthPlaylists = [],
@ -558,8 +556,8 @@ videojs.Hls.prototype.selectPlaylist = function () {
bandwidthBestVariant,
resolutionPlusOne,
resolutionBestVariant,
playerWidth,
playerHeight;
width,
height;
sortedPlaylists.sort(videojs.Hls.comparePlaylistBandwidth);
@ -575,7 +573,7 @@ videojs.Hls.prototype.selectPlaylist = function () {
effectiveBitrate = variant.attributes.BANDWIDTH * bandwidthVariance;
if (effectiveBitrate < player.hls.bandwidth) {
if (effectiveBitrate < this.bandwidth) {
bandwidthPlaylists.push(variant);
// since the playlists are sorted in ascending order by
@ -595,8 +593,8 @@ videojs.Hls.prototype.selectPlaylist = function () {
// (this could be the lowest bitrate rendition as we go through all of them above)
variant = null;
playerWidth = parseInt(getComputedStyle(player.el()).width, 10);
playerHeight = parseInt(getComputedStyle(player.el()).height, 10);
width = parseInt(getComputedStyle(this.tech_.el()).width, 10);
height = parseInt(getComputedStyle(this.tech_.el()).height, 10);
// iterate through the bandwidth-filtered playlists and find
// best rendition by player dimension
@ -615,14 +613,14 @@ videojs.Hls.prototype.selectPlaylist = function () {
// since the playlists are sorted, the first variant that has
// dimensions less than or equal to the player size is the best
if (variant.attributes.RESOLUTION.width === playerWidth &&
variant.attributes.RESOLUTION.height === playerHeight) {
if (variant.attributes.RESOLUTION.width === width &&
variant.attributes.RESOLUTION.height === height) {
// if we have the exact resolution as the player use it
resolutionPlusOne = null;
resolutionBestVariant = variant;
break;
} else if (variant.attributes.RESOLUTION.width < playerWidth &&
variant.attributes.RESOLUTION.height < playerHeight) {
} else if (variant.attributes.RESOLUTION.width < width &&
variant.attributes.RESOLUTION.height < height) {
// if we don't have an exact match, see if we have a good higher quality variant to use
if (oldvariant && oldvariant.attributes && oldvariant.attributes.RESOLUTION &&
oldvariant.attributes.RESOLUTION.width && oldvariant.attributes.RESOLUTION.height) {
@ -651,7 +649,7 @@ videojs.Hls.prototype.checkBuffer_ = function() {
this.drainBuffer();
// wait awhile and try again
this.checkBufferTimeout_ = window.setTimeout(videojs.bind(this, this.checkBuffer_),
this.checkBufferTimeout_ = window.setTimeout((this.checkBuffer_).bind(this),
bufferCheckInterval);
};
@ -662,7 +660,7 @@ videojs.Hls.prototype.checkBuffer_ = function() {
videojs.Hls.prototype.startCheckingBuffer_ = function() {
// if the player ever stalls, check if there is video data available
// to append immediately
this.player().on('waiting', videojs.bind(this, this.drainBuffer));
this.tech_.on('waiting', (this.drainBuffer).bind(this));
this.checkBuffer_();
};
@ -672,9 +670,11 @@ videojs.Hls.prototype.startCheckingBuffer_ = function() {
* SourceBuffer.
*/
videojs.Hls.prototype.stopCheckingBuffer_ = function() {
window.clearTimeout(this.checkBufferTimeout_);
this.checkBufferTimeout_ = null;
this.player().off('waiting', this.drainBuffer);
if (this.checkBufferTimeout_) {
window.clearTimeout(this.checkBufferTimeout_);
this.checkBufferTimeout_ = null;
}
this.tech_.off('waiting', this.drainBuffer);
};
/**
@ -685,20 +685,19 @@ videojs.Hls.prototype.stopCheckingBuffer_ = function() {
*/
videojs.Hls.prototype.fillBuffer = function(offset) {
var
player = this.player(),
buffered = player.buffered(),
tech = this.tech_,
buffered = this.tech_.buffered(),
bufferedTime = 0,
segment,
segmentUri;
// if preload is set to "none", do not download segments until playback is requested
if (!player.hasClass('vjs-has-started') &&
player.options().preload === 'none') {
if (this.loadingState_ !== 'segments') {
return;
}
// if a video has not been specified, do nothing
if (!player.currentSrc() || !this.playlists) {
if (!tech.currentSrc() || !this.playlists) {
return;
}
@ -714,15 +713,6 @@ videojs.Hls.prototype.fillBuffer = function(offset) {
return;
}
// if this is a live video wait until playback has been requested to
// being buffering so we don't preload data that will never be
// played
if (!this.playlists.media().endList &&
!player.hasClass('vjs-has-started') &&
offset === undefined) {
return;
}
// if a playlist switch is in progress, wait for it to finish
if (this.playlists.state === 'SWITCHING_MEDIA') {
return;
@ -736,7 +726,7 @@ videojs.Hls.prototype.fillBuffer = function(offset) {
if (buffered) {
// assuming a single, contiguous buffer region
bufferedTime = player.buffered().end(0) - player.currentTime();
bufferedTime = tech.buffered().end(0) - tech.currentTime();
}
// if there is plenty of content in the buffer and we're not
@ -754,10 +744,10 @@ videojs.Hls.prototype.fillBuffer = function(offset) {
videojs.Hls.prototype.playlistUriToUrl = function(segmentRelativeUrl) {
var playListUrl;
// resolve the segment URL relative to the playlist
if (this.playlists.media().uri === this.src_) {
playListUrl = resolveUrl(this.src_, segmentRelativeUrl);
if (this.playlists.media().uri === this.source_.src) {
playListUrl = resolveUrl(this.source_.src, segmentRelativeUrl);
} else {
playListUrl = resolveUrl(resolveUrl(this.src_, this.playlists.media().uri || ''), segmentRelativeUrl);
playListUrl = resolveUrl(resolveUrl(this.source_.src, this.playlists.media().uri || ''), segmentRelativeUrl);
}
return playListUrl;
};
@ -771,63 +761,59 @@ videojs.Hls.prototype.playlistUriToUrl = function(segmentRelativeUrl) {
* `bandwidth` is the only required property.
*/
videojs.Hls.prototype.setBandwidth = function(xhr) {
var tech = this;
// calculate the download bandwidth
tech.segmentXhrTime = xhr.roundTripTime;
tech.bandwidth = xhr.bandwidth;
tech.bytesReceived += xhr.bytesReceived || 0;
this.segmentXhrTime = xhr.roundTripTime;
this.bandwidth = xhr.bandwidth;
this.bytesReceived += xhr.bytesReceived || 0;
tech.trigger('bandwidthupdate');
this.tech_.trigger('bandwidthupdate');
};
videojs.Hls.prototype.loadSegment = function(segmentUri, offset) {
var
tech = this,
player = this.player(),
settings = player.options().hls || {};
var self = this;
// request the next segment
this.segmentXhr_ = videojs.Hls.xhr({
url: segmentUri,
uri: segmentUri,
responseType: 'arraybuffer',
withCredentials: settings.withCredentials
}, function(error, url) {
withCredentials: this.source_.withCredentials
}, function(error, request) {
var segmentInfo;
// the segment request is no longer outstanding
tech.segmentXhr_ = null;
self.segmentXhr_ = null;
if (error) {
// if a segment request times out, we may have better luck with another playlist
if (error === 'timeout') {
tech.bandwidth = 1;
return tech.playlists.media(tech.selectPlaylist());
if (request.timedout) {
self.bandwidth = 1;
return self.playlists.media(self.selectPlaylist());
}
// otherwise, try jumping ahead to the next segment
tech.error = {
status: this.status,
message: 'HLS segment request error at URL: ' + url,
code: (this.status >= 500) ? 4 : 2
self.error = {
status: request.status,
message: 'HLS segment request error at URL: ' + segmentUri,
code: (request.status >= 500) ? 4 : 2
};
// try moving on to the next segment
tech.mediaIndex++;
self.mediaIndex++;
return;
}
// stop processing if the request was aborted
if (!this.response) {
if (!request.response) {
return;
}
tech.setBandwidth(this);
self.setBandwidth(request);
// package up all the work to append the segment
segmentInfo = {
// the segment's mediaIndex at the time it was received
mediaIndex: tech.mediaIndex,
mediaIndex: self.mediaIndex,
// the segment's playlist
playlist: tech.playlists.media(),
playlist: self.playlists.media(),
// optionally, a time offset to seek to within the segment
offset: offset,
// unencrypted bytes of the segment
@ -841,19 +827,19 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) {
pendingMetadata: []
};
if (segmentInfo.playlist.segments[segmentInfo.mediaIndex].key) {
segmentInfo.encryptedBytes = new Uint8Array(this.response);
segmentInfo.encryptedBytes = new Uint8Array(request.response);
} else {
segmentInfo.bytes = new Uint8Array(this.response);
segmentInfo.bytes = new Uint8Array(request.response);
}
tech.segmentBuffer_.push(segmentInfo);
player.trigger('progress');
tech.drainBuffer();
self.segmentBuffer_.push(segmentInfo);
self.tech_.trigger('progress');
self.drainBuffer();
tech.mediaIndex++;
self.mediaIndex++;
// figure out what stream the next segment should be downloaded from
// with the updated bandwidth information
tech.playlists.media(tech.selectPlaylist());
self.playlists.media(self.selectPlaylist());
});
};
@ -973,18 +959,16 @@ videojs.Hls.prototype.drainBuffer = function(event) {
}
// tell the SWF the media position of the first tag we'll be delivering
this.el().vjs_setProperty('currentTime', ((tags[i].pts - ptsTime + offset) * 0.001));
this.tech_.el().vjs_setProperty('currentTime', ((tags[i].pts - ptsTime + offset) * 0.001));
tags = tags.slice(i);
}
this.lastSeekedTime_ = null;
}
// when we're crossing a discontinuity, inject metadata to indicate
// that the decoder should be reset appropriately
if (segment.discontinuity && tags.length) {
this.el().vjs_discontinuity();
this.tech_.el().vjs_discontinuity();
}
(function() {
@ -1028,26 +1012,26 @@ videojs.Hls.prototype.fetchKeys_ = function() {
tech = this;
player = this.player();
settings = player.options().hls || {};
settings = this.options_;
/**
* Handle a key XHR response. This function needs to lookup the
*/
receiveKey = function(key) {
return function(error) {
return function(error, request) {
keyXhr = null;
if (error || !this.response || this.response.byteLength !== 16) {
if (error || !request.response || request.response.byteLength !== 16) {
key.retries = key.retries || 0;
key.retries++;
if (!this.aborted) {
if (!request.aborted) {
// try fetching again
tech.fetchKeys_();
}
return;
}
view = new DataView(this.response);
view = new DataView(request.response);
key.bytes = new Uint32Array([
view.getUint32(0),
view.getUint32(4),
@ -1072,7 +1056,7 @@ videojs.Hls.prototype.fetchKeys_ = function() {
// request the key if the retry limit hasn't been reached
if (!key.bytes && !keyFailed(key)) {
keyXhr = videojs.Hls.xhr({
url: this.playlistUriToUrl(key.uri),
uri: this.playlistUriToUrl(key.uri),
responseType: 'arraybuffer',
withCredentials: settings.withCredentials
}, receiveKey(key));
@ -1091,7 +1075,7 @@ videojs.Hls.supportsNativeHls = (function() {
vndMpeg;
// native HLS is definitely not supported if HTML5 video isn't
if (!videojs.Html5.isSupported()) {
if (!videojs.getComponent('Html5').isSupported()) {
return false;
}
@ -1102,22 +1086,16 @@ videojs.Hls.supportsNativeHls = (function() {
})();
videojs.Hls.isSupported = function() {
// Only use the HLS tech if native HLS isn't available
return !videojs.Hls.supportsNativeHls &&
// Flash must be supported for the fallback to work
videojs.Flash.isSupported() &&
videojs.getComponent('Flash').isSupported() &&
// Media sources must be available to stream bytes to Flash
videojs.MediaSource &&
// Typed arrays are used to repackage the segments
window.Uint8Array;
};
videojs.Hls.canPlaySource = function(srcObj) {
var mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i;
return mpegurlRE.test(srcObj.type);
};
/**
* Calculate the duration of a playlist from a given start index to a given
* end index.

92
src/xhr.js

@ -1,86 +1,22 @@
(function(videojs){
(function(videojs) {
'use strict';
/**
* Creates and sends an XMLHttpRequest.
* TODO - expose video.js core's XHR and use that instead
*
* @param options {string | object} if this argument is a string, it
* is intrepreted as a URL and a simple GET request is
* inititated. If it is an object, it should contain a `url`
* property that indicates the URL to request and optionally a
* `method` which is the type of HTTP request to send.
* @param callback (optional) {function} a function to call when the
* request completes. If the request was not successful, the first
* argument will be falsey.
* @return {object} the XMLHttpRequest that was initiated.
* A wrapper for videojs.xhr that tracks bandwidth.
*/
videojs.Hls.xhr = function(url, callback) {
var
options = {
method: 'GET',
timeout: 45 * 1000
},
request,
abortTimeout;
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);
request.url = url;
request.requestTime = new Date().getTime();
if (options.responseType) {
request.responseType = options.responseType;
}
if (options.withCredentials) {
request.withCredentials = true;
}
if (options.timeout) {
abortTimeout = window.setTimeout(function() {
if (request.readyState !== 4) {
request.timedout = true;
request.abort();
}
}, options.timeout);
}
request.onreadystatechange = function() {
// wait until the request completes
if (this.readyState !== 4) {
return;
videojs.Hls.xhr = function(options, callback) {
var request = videojs.xhr(options, function(error, request) {
if (request.response) {
request.responseTime = (new Date()).getTime();
request.roundTripTime = request.responseTime - request.requestTime;
request.bytesReceived = request.response.byteLength || request.response.length;
request.bandwidth = Math.floor((request.bytesReceived / request.roundTripTime) * 8 * 1000);
}
// clear outstanding timeouts
window.clearTimeout(abortTimeout);
callback(error, request);
});
// request timeout
if (request.timedout) {
return callback.call(this, 'timeout', url);
}
// request aborted or errored
if (this.status >= 400 || this.status === 0) {
return callback.call(this, true, url);
}
if (this.response) {
this.responseTime = new Date().getTime();
this.roundTripTime = this.responseTime - this.requestTime;
this.bytesReceived = this.response.byteLength || this.response.length;
this.bandwidth = Math.floor((this.bytesReceived / this.roundTripTime) * 8 * 1000);
}
return callback.call(this, false, url);
};
request.send(null);
request.requestTime = (new Date()).getTime();
return request;
};
})(window.videojs);

9
test/karma.conf.js

@ -69,12 +69,9 @@ module.exports = function(config) {
// add their paths to this list.
files: [
'../node_modules/sinon/lib/sinon.js',
'../node_modules/sinon/lib/sinon/util/event.js',
'../node_modules/sinon/lib/sinon/util/fake_xml_http_request.js',
'../node_modules/sinon/lib/sinon/util/xhr_ie.js',
'../node_modules/sinon/lib/sinon/util/fake_timers.js',
'../node_modules/video.js/dist/video-js/video.dev.js',
'../node_modules/sinon/pkg/sinon.js',
'../node_modules/video.js/dist/video-js.css',
'../node_modules/video.js/dist/video.js',
'../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js',
'../node_modules/pkcs7/dist/pkcs7.unpad.js',
'../test/karma-qunit-shim.js',

9
test/localkarma.conf.js

@ -34,12 +34,9 @@ module.exports = function(config) {
// add their paths to this list.
files: [
'../node_modules/sinon/lib/sinon.js',
'../node_modules/sinon/lib/sinon/util/event.js',
'../node_modules/sinon/lib/sinon/util/fake_xml_http_request.js',
'../node_modules/sinon/lib/sinon/util/xhr_ie.js',
'../node_modules/sinon/lib/sinon/util/fake_timers.js',
'../node_modules/video.js/dist/video-js/video.dev.js',
'../node_modules/sinon/pkg/sinon.js',
'../node_modules/video.js/dist/video-js.css',
'../node_modules/video.js/dist/video.js',
'../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js',
'../node_modules/pkcs7/dist/pkcs7.unpad.js',
'../test/karma-qunit-shim.js',

4
test/segment-parser.js

@ -27,7 +27,7 @@
0x09, 0x00, 0x00, 0x00, 0x00
],
extend = window.videojs.util.mergeOptions,
mergeOptions = window.videojs.mergeOptions,
makePat,
makePsi,
@ -178,7 +178,7 @@
makePacket = function(options) {
var
result = [],
settings = extend({
settings = mergeOptions({
payloadUnitStartIndicator: true,
pid: 0x00
}, options);

14
test/videojs-hls.html

@ -4,18 +4,15 @@
<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/fake_xml_http_request.js"></script>
<script src="../node_modules/sinon/lib/sinon/util/xhr_ie.js"></script>
<script src="../node_modules/sinon/lib/sinon/util/fake_timers.js"></script>
<script src="../node_modules/sinon/pkg/sinon.js"></script>
<!-- Load local QUnit. -->
<link rel="stylesheet" href="../libs/qunit/qunit.css" media="screen">
<script src="../libs/qunit/qunit.js"></script>
<link rel="stylesheet" href="../node_modules/qunitjs/qunit/qunit.css" media="screen">
<script src="../node_modules/qunitjs/qunit/qunit.js"></script>
<!-- video.js -->
<script src="../node_modules/video.js/dist/video-js/video.dev.js"></script>
<script src="../node_modules/video.js/dist/video.js"></script>
<link rel="stylesheet" href="../node_modules/video.js/dist/video-js.css" media="screen">
<script src="../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script>
<!-- HLS plugin -->
@ -63,7 +60,6 @@
<script src="playlist_test.js"></script>
<script src="playlist-loader_test.js"></script>
<script src="decrypter_test.js"></script>
<script src="xhr_test.js"></script>
</head>
<body>
<div id="qunit"></div>

847
test/videojs-hls_test.js
File diff suppressed because it is too large
View File

34
test/xhr_test.js

@ -1,34 +0,0 @@
(function(window, videojs, undefined) {
'use strict';
/*
XHR test suite
*/
var xhr;
module('XHR', {
setup: function() {
xhr = sinon.useFakeXMLHttpRequest();
},
teardown: function() {
xhr.restore();
}
});
test('handles xhr timeouts correctly', function () {
var error;
var clock = sinon.useFakeTimers();
videojs.Hls.xhr({
url: 'http://example.com',
timeout: 1
}, function(innerError) {
error = innerError;
});
clock.tick(1);
strictEqual(error, 'timeout', 'called with timeout error');
clock.restore();
});
})(window, window.videojs);
Loading…
Cancel
Save