2db4b64c by Jon-Carlos Rivera

Merge pull request #568 from videojs/browserify-p5

Browserify p5
2 parents c694b4b7 10562674
sudo: false
language: node_js
addons:
firefox: "latest"
node_js:
- 'node'
- '4.2'
- '0.12'
- '0.10'
- "stable"
notifications:
hipchat:
rooms:
......
......@@ -46,10 +46,7 @@
</ul>
<script src="/node_modules/video.js/dist/video.js"></script>
<script src="/node_modules/videojs-contrib-media-sources/dist/videojs-media-sources.js"></script>
<script src="/src/videojs-contrib-hls.js"></script>
<script src="/dist/videojs-contrib-hls.js"></script>
<script src="/src/bin-utils.js"></script>
<script>
(function(window, videojs) {
var player = window.player = videojs('videojs-contrib-hls-player');
......
......@@ -2,7 +2,7 @@
"name": "videojs-contrib-hls",
"version": "1.3.5",
"description": "Play back HLS with video.js, even where it's not natively supported",
"main": "es5/stub.js",
"main": "es5/videojs-contrib-hls.js",
"engines": {
"node": ">= 0.10.12"
},
......@@ -27,7 +27,7 @@
"docs": "npm-run-all docs:*",
"docs:api": "jsdoc src -r -d docs/api",
"docs:toc": "doctoc README.md",
"lint": "vjsstandard :",
"lint": "vjsstandard",
"prestart": "npm-run-all docs build",
"start": "npm-run-all -p start:* watch:*",
"start:serve": "babel-node scripts/server.js",
......@@ -40,7 +40,7 @@
"preversion": "npm test",
"version": "npm run build",
"watch": "npm-run-all -p watch:*",
"watch:js": "watchify src/stub.js -t babelify -v -o dist/videojs-contrib-hls.js",
"watch:js": "watchify src/videojs-contrib-hls.js -t babelify -v -o dist/videojs-contrib-hls.js",
"watch:test": "npm-run-all -p watch:test:*",
"watch:test:js": "node scripts/watch-test.js",
"watch:test:manifest": "node -e \"var b=require('./scripts/manifest-data.js'); b.watch();\"",
......@@ -72,7 +72,8 @@
"scripts",
"utils",
"test/test-manifests.js",
"test/test-expected.js"
"test/test-expected.js",
"src/playlist-loader.js"
]
},
"files": [
......@@ -88,10 +89,10 @@
"utils/"
],
"dependencies": {
"pkcs7": "^0.2.3",
"video.js": "^5.0.0",
"videojs-contrib-media-sources": "^2.4.4",
"videojs-swf": "^5.0.1"
"pkcs7": "^0.2.2",
"video.js": "^5.2.1",
"videojs-contrib-media-sources": "^3.0.0",
"videojs-swf": "^5.0.0"
},
"devDependencies": {
"babel": "^5.8.0",
......@@ -117,12 +118,13 @@
"minimist": "^1.2.0",
"npm-run-all": "^1.2.0",
"portscanner": "^1.0.0",
"qunitjs": "^1.0.0",
"qunitjs": "^1.18.0",
"serve-static": "^1.10.0",
"shelljs": "^0.5.3",
"sinon": "1.10.2",
"uglify-js": "^2.5.0",
"videojs-standard": "^4.0.0",
"watchify": "^3.6.0"
"watchify": "^3.6.0",
"webworkify": "^1.1.0"
}
}
......
......@@ -2,7 +2,7 @@ var browserify = require('browserify');
var fs = require('fs');
var glob = require('glob');
glob('test/{playlist*,decryper,m3u8,stub}.test.js', function(err, files) {
glob('test/**/*.test.js', function(err, files) {
browserify(files)
.transform('babelify')
.bundle()
......
......@@ -3,7 +3,7 @@ var fs = require('fs');
var glob = require('glob');
var watchify = require('watchify');
glob('test/{playlist*,decrypter,m3u8,stub}.test.js', function(err, files) {
glob('test/**/*.test.js', function(err, files) {
var b = browserify(files, {
cache: {},
packageCache: {},
......
(function(window) {
var textRange = function(range, i) {
const textRange = function(range, i) {
return range.start(i) + '-' + range.end(i);
};
var module = {
hexDump: function(data) {
var
bytes = Array.prototype.slice.call(data),
step = 16,
formatHexString = function(e, i) {
var value = e.toString(16);
return "00".substring(0, 2 - value.length) + value + (i % 2 ? ' ' : '');
},
formatAsciiString = function(e) {
};
const formatHexString = function(e, i) {
let value = e.toString(16);
return '00'.substring(0, 2 - value.length) + value + (i % 2 ? ' ' : '');
};
const formatAsciiString = function(e) {
if (e >= 0x20 && e < 0x7e) {
return String.fromCharCode(e);
}
return '.';
},
result = '',
hex,
ascii;
for (var j = 0; j < bytes.length / step; j++) {
};
const utils = {
hexDump(data) {
let bytes = Array.prototype.slice.call(data);
let step = 16;
let result = '';
let hex;
let ascii;
for (let j = 0; j < bytes.length / step; j++) {
hex = bytes.slice(j * step, j * step + step).map(formatHexString).join('');
ascii = bytes.slice(j * step, j * step + step).map(formatAsciiString).join('');
result += hex + ' ' + ascii + '\n';
}
return result;
},
tagDump: function(tag) {
return module.hexDump(tag.bytes);
tagDump(tag) {
return utils.hexDump(tag.bytes);
},
textRanges: function(ranges) {
var result = '', i;
textRanges(ranges) {
let result = '';
let i;
for (i = 0; i < ranges.length; i++) {
result += textRange(ranges, i) + ' ';
}
return result;
}
};
};
window.videojs.Hls.utils = module;
})(this);
export default utils;
......
......@@ -89,10 +89,9 @@ const precompute = function() {
decTable[i] = decTable[i].slice(0);
}
return tables;
}
};
let aesTables = null;
/**
* Schedule out an AES key for both encryption and decryption. This
* is a low-level class. Use a cipher mode to do bulk encryption.
......@@ -116,7 +115,7 @@ export default class AES {
*/
// if we have yet to precompute the S-box tables
// do so now
if(!aesTables) {
if (!aesTables) {
aesTables = precompute();
}
// then make a copy of that object for use
......
/**
* A lightweight readable stream implemention that handles event dispatching.
* Objects that inherit from streams should call init in their constructors.
*/
export default class Stream {
constructor() {
this.init();
}
init() {
this.listeners = {};
}
......
import m3u8 from './m3u8';
import Stream from './stream';
import videojs from 'video.js';
import {Decrypter, decrypt, AsyncStream} from './decrypter';
import Playlist from './playlist';
import PlaylistLoader from './playlist-loader';
import xhr from './xhr';
if(typeof window.videojs.Hls === 'undefined') {
videojs.Hls = {};
}
videojs.Hls.Stream = Stream;
videojs.m3u8 = m3u8;
videojs.Hls.decrypt = decrypt;
videojs.Hls.Decrypter = Decrypter;
videojs.Hls.AsyncStream = AsyncStream;
videojs.Hls.xhr = xhr;
videojs.Hls.Playlist = Playlist;
videojs.Hls.PlaylistLoader = PlaylistLoader;
/*
/**
* videojs-hls
* The main file for the HLS project.
* License: https://github.com/videojs/videojs-contrib-hls/blob/master/LICENSE
*/
(function(window, videojs, document, undefined) {
'use strict';
import PlaylistLoader from './playlist-loader';
import Playlist from './playlist';
import xhr from './xhr';
import {Decrypter, AsyncStream, decrypt} from './decrypter';
import utils from './bin-utils';
import {MediaSource, URL} from 'videojs-contrib-media-sources';
import m3u8 from './m3u8';
import videojs from 'video.js';
import resolveUrl from './resolve-url';
const Hls = {
PlaylistLoader,
Playlist,
Decrypter,
AsyncStream,
decrypt,
utils,
xhr
};
// the desired length of video to maintain in the buffer, in seconds
Hls.GOAL_BUFFER_LENGTH = 30;
// HLS is a source handler, not a tech. Make sure attempts to use it
// as one do not cause exceptions.
Hls.canPlaySource = function() {
return videojs.log.warn('HLS is no longer a tech. Please remove it from ' +
'your player\'s techOrder.');
};
// Search for a likely end time for the segment that was just appened
// based on the state of the `buffered` property before and after the
// append.
// If we found only one such uncommon end-point return it.
Hls.findSoleUncommonTimeRangesEnd_ = function(original, update) {
let i;
let start;
let end;
let result = [];
let edges = [];
// In order to qualify as a possible candidate, the end point must:
// 1) Not have already existed in the `original` ranges
// 2) Not result from the shrinking of a range that already existed
// in the `original` ranges
// 3) Not be contained inside of a range that existed in `original`
let overlapsCurrentEnd = function(span) {
return (span[0] <= end && span[1] >= end);
};
if (original) {
// Save all the edges in the `original` TimeRanges object
for (i = 0; i < original.length; i++) {
start = original.start(i);
end = original.end(i);
edges.push([start, end]);
}
}
if (update) {
// Save any end-points in `update` that are not in the `original`
// TimeRanges object
for (i = 0; i < update.length; i++) {
start = update.start(i);
end = update.end(i);
if (edges.some(overlapsCurrentEnd)) {
continue;
}
// at this point it must be a unique non-shrinking end edge
result.push(end);
}
}
// we err on the side of caution and return null if didn't find
// exactly *one* differing end edge in the search above
if (result.length !== 1) {
return null;
}
var
// A fudge factor to apply to advertised playlist bitrates to account for
// temporary flucations in client bandwidth
bandwidthVariance = 1.2,
blacklistDuration = 5 * 60 * 1000, // 5 minute blacklist
TIME_FUDGE_FACTOR = 1 / 30, // Fudge factor to account for TimeRanges rounding
Component = videojs.getComponent('Component'),
return result[0];
};
// The amount of time to wait between checking the state of the buffer
bufferCheckInterval = 500,
/**
* Whether the browser has built-in HLS support.
*/
Hls.supportsNativeHls = (function() {
let video = document.createElement('video');
let xMpegUrl;
let vndMpeg;
// native HLS is definitely not supported if HTML5 video isn't
if (!videojs.getComponent('Html5').isSupported()) {
return false;
}
keyFailed,
resolveUrl;
xMpegUrl = video.canPlayType('application/x-mpegURL');
vndMpeg = video.canPlayType('application/vnd.apple.mpegURL');
return (/probably|maybe/).test(xMpegUrl) ||
(/probably|maybe/).test(vndMpeg);
}());
// HLS is a source handler, not a tech. Make sure attempts to use it
// as one do not cause exceptions.
Hls.isSupported = function() {
return videojs.log.warn('HLS is no longer a tech. Please remove it from ' +
'your player\'s techOrder.');
};
/**
* A comparator function to sort two playlist object by bandwidth.
* @param left {object} a media playlist object
* @param right {object} a media playlist object
* @return {number} Greater than zero if the bandwidth attribute of
* left is greater than the corresponding attribute of right. Less
* than zero if the bandwidth of right is greater than left and
* exactly zero if the two are equal.
*/
Hls.comparePlaylistBandwidth = function(left, right) {
let leftBandwidth;
let rightBandwidth;
if (left.attributes && left.attributes.BANDWIDTH) {
leftBandwidth = left.attributes.BANDWIDTH;
}
leftBandwidth = leftBandwidth || window.Number.MAX_VALUE;
if (right.attributes && right.attributes.BANDWIDTH) {
rightBandwidth = right.attributes.BANDWIDTH;
}
rightBandwidth = rightBandwidth || window.Number.MAX_VALUE;
return leftBandwidth - rightBandwidth;
};
/**
* A comparator function to sort two playlist object by resolution (width).
* @param left {object} a media playlist object
* @param right {object} a media playlist object
* @return {number} Greater than zero if the resolution.width attribute of
* left is greater than the corresponding attribute of right. Less
* than zero if the resolution.width of right is greater than left and
* exactly zero if the two are equal.
*/
Hls.comparePlaylistResolution = function(left, right) {
let leftWidth;
let rightWidth;
if (left.attributes &&
left.attributes.RESOLUTION &&
left.attributes.RESOLUTION.width) {
leftWidth = left.attributes.RESOLUTION.width;
}
leftWidth = leftWidth || window.Number.MAX_VALUE;
if (right.attributes &&
right.attributes.RESOLUTION &&
right.attributes.RESOLUTION.width) {
rightWidth = right.attributes.RESOLUTION.width;
}
rightWidth = rightWidth || window.Number.MAX_VALUE;
// NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions
// have the same media dimensions/ resolution
if (leftWidth === rightWidth &&
left.attributes.BANDWIDTH &&
right.attributes.BANDWIDTH) {
return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH;
}
return leftWidth - rightWidth;
};
// A fudge factor to apply to advertised playlist bitrates to account for
// temporary flucations in client bandwidth
const bandwidthVariance = 1.2;
// 5 minute blacklist
const blacklistDuration = 5 * 60 * 1000;
// Fudge factor to account for TimeRanges rounding
const TIME_FUDGE_FACTOR = 1 / 30;
const Component = videojs.getComponent('Component');
// The amount of time to wait between checking the state of the buffer
const bufferCheckInterval = 500;
// returns true if a key has failed to download within a certain amount of retries
keyFailed = function(key) {
const keyFailed = function(key) {
return key.retries && key.retries >= 2;
};
videojs.Hls = {};
videojs.HlsHandler = videojs.extend(Component, {
constructor: function(tech, options) {
var self = this, _player;
const parseCodecs = function(codecs) {
let result = {
codecCount: 0,
videoCodec: null,
audioProfile: null
};
result.codecCount = codecs.split(',').length;
result.codecCount = result.codecCount || 2;
Component.call(this, tech);
// parse the video codec but ignore the version
result.videoCodec = (/(^|\s|,)+(avc1)[^ ,]*/i).exec(codecs);
result.videoCodec = result.videoCodec && result.videoCodec[2];
// parse the last field of the audio codec
result.audioProfile = (/(^|\s|,)+mp4a.\d+\.(\d+)/i).exec(codecs);
result.audioProfile = result.audioProfile && result.audioProfile[2];
return result;
};
const filterBufferedRanges = function(predicate) {
return function(time) {
let i;
let ranges = [];
let tech = this.tech_;
// !!The order of the next two assignments is important!!
// `currentTime` must be equal-to or greater-than the start of the
// buffered range. Flash executes out-of-process so, every value can
// change behind the scenes from line-to-line. By reading `currentTime`
// after `buffered`, we ensure that it is always a current or later
// value during playback.
let buffered = tech.buffered();
if (typeof time === 'undefined') {
time = tech.currentTime();
}
if (buffered && buffered.length) {
// Search for a range containing the play-head
for (i = 0; i < buffered.length; i++) {
if (predicate(buffered.start(i), buffered.end(i), time)) {
ranges.push([buffered.start(i), buffered.end(i)]);
}
}
}
return videojs.createTimeRanges(ranges);
};
};
export default class HlsHandler extends Component {
constructor(tech, options) {
super(tech);
let _player;
// tech.player() is deprecated but setup a reference to HLS for
// backwards-compatibility
......@@ -38,9 +259,9 @@ videojs.HlsHandler = videojs.extend(Component, {
_player = videojs(tech.options_.playerId);
if (!_player.hls) {
Object.defineProperty(_player, 'hls', {
get: function() {
get: () => {
videojs.log.warn('player.hls is deprecated. Use player.tech.hls instead.');
return self;
return this;
}
});
}
......@@ -54,7 +275,8 @@ videojs.HlsHandler = videojs.extend(Component, {
// start playlist selection at a reasonable bandwidth for
// broadband internet
this.bandwidth = options.bandwidth || 4194304; // 0.5 Mbps
// 0.5 Mbps
this.bandwidth = options.bandwidth || 4194304;
this.bytesReceived = 0;
// loadingState_ tracks how far along the buffering process we
......@@ -81,71 +303,8 @@ videojs.HlsHandler = videojs.extend(Component, {
this.on(this.tech_, 'play', this.play);
}
});
// HLS is a source handler, not a tech. Make sure attempts to use it
// as one do not cause exceptions.
videojs.Hls.canPlaySource = function() {
return videojs.log.warn('HLS is no longer a tech. Please remove it from ' +
'your player\'s techOrder.');
};
/**
* The Source Handler object, which informs video.js what additional
* MIME types are supported and sets up playback. It is registered
* automatically to the appropriate tech based on the capabilities of
* the browser it is running in. It is not necessary to use or modify
* this object in normal usage.
*/
videojs.HlsSourceHandler = function(mode) {
return {
canHandleSource: function(srcObj) {
return videojs.HlsSourceHandler.canPlayType(srcObj.type);
},
handleSource: function(source, tech) {
if (mode === 'flash') {
// We need to trigger this asynchronously to give others the chance
// to bind to the event when a source is set at player creation
tech.setTimeout(function() {
tech.trigger('loadstart');
}, 1);
}
tech.hls = new videojs.HlsHandler(tech, {
source: source,
mode: mode
});
tech.hls.src(source.src);
return tech.hls;
},
canPlayType: function(type) {
return videojs.HlsSourceHandler.canPlayType(type);
}
};
};
videojs.HlsSourceHandler.canPlayType = function(type) {
var mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i;
// favor native HLS support if it's available
if (videojs.Hls.supportsNativeHls) {
return false;
}
return mpegurlRE.test(type);
};
// register source handlers with the appropriate techs
if (videojs.MediaSource.supportsNativeMediaSources()) {
videojs.getComponent('Html5').registerSourceHandler(videojs.HlsSourceHandler('html5'));
}
if (window.Uint8Array) {
videojs.getComponent('Flash').registerSourceHandler(videojs.HlsSourceHandler('flash'));
}
// the desired length of video to maintain in the buffer, in seconds
videojs.Hls.GOAL_BUFFER_LENGTH = 30;
videojs.HlsHandler.prototype.src = function(src) {
var oldMediaPlaylist;
src(src) {
let oldMediaPlaylist;
// do nothing if the src is falsey
if (!src) {
......@@ -158,16 +317,17 @@ videojs.HlsHandler.prototype.src = function(src) {
this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen.bind(this));
this.options_ = {};
if (this.source_.withCredentials !== undefined) {
if (typeof this.source_.withCredentials !== 'undefined') {
this.options_.withCredentials = this.source_.withCredentials;
} else if (videojs.options.hls) {
this.options_.withCredentials = videojs.options.hls.withCredentials;
}
this.playlists = new videojs.Hls.PlaylistLoader(this.source_.src, this.options_.withCredentials);
this.playlists = new Hls.PlaylistLoader(this.source_.src,
this.options_.withCredentials);
this.tech_.one('canplay', this.setupFirstPlay.bind(this));
this.playlists.on('loadedmetadata', function() {
this.playlists.on('loadedmetadata', () => {
oldMediaPlaylist = this.playlists.media();
// if this isn't a live video and preload permits, start
......@@ -182,14 +342,15 @@ videojs.HlsHandler.prototype.src = function(src) {
this.setupFirstPlay();
this.fillBuffer();
this.tech_.trigger('loadedmetadata');
}.bind(this));
});
this.playlists.on('error', function() {
this.playlists.on('error', () => {
this.blacklistCurrentPlaylist_(this.playlists.error);
}.bind(this));
});
this.playlists.on('loadedplaylist', function() {
var updatedPlaylist = this.playlists.media(), seekable;
this.playlists.on('loadedplaylist', () => {
let updatedPlaylist = this.playlists.media();
let seekable;
if (!updatedPlaylist) {
// select the initial variant
......@@ -207,14 +368,14 @@ videojs.HlsHandler.prototype.src = function(src) {
}
oldMediaPlaylist = updatedPlaylist;
}.bind(this));
});
this.playlists.on('mediachange', function() {
this.playlists.on('mediachange', () => {
this.tech_.trigger({
type: 'mediachange',
bubbles: true
});
}.bind(this));
});
// do nothing if the tech has been disposed already
// this can occur if someone sets the src in player.ready(), for instance
......@@ -223,9 +384,8 @@ videojs.HlsHandler.prototype.src = function(src) {
}
this.tech_.src(videojs.URL.createObjectURL(this.mediaSource));
};
videojs.HlsHandler.prototype.handleSourceOpen = function() {
}
handleSourceOpen() {
// Only attempt to create the source buffer if none already exist.
// handleSourceOpen is also called when we are "re-opening" a source buffer
// after `endOfStream` has been called (in response to a seek for instance)
......@@ -242,83 +402,9 @@ videojs.HlsHandler.prototype.handleSourceOpen = function() {
if (this.tech_.autoplay()) {
this.play();
}
};
// Search for a likely end time for the segment that was just appened
// based on the state of the `buffered` property before and after the
// append.
// If we found only one such uncommon end-point return it.
videojs.Hls.findSoleUncommonTimeRangesEnd_ = function(original, update) {
var
i, start, end,
result = [],
edges = [],
// In order to qualify as a possible candidate, the end point must:
// 1) Not have already existed in the `original` ranges
// 2) Not result from the shrinking of a range that already existed
// in the `original` ranges
// 3) Not be contained inside of a range that existed in `original`
overlapsCurrentEnd = function(span) {
return (span[0] <= end && span[1] >= end);
};
if (original) {
// Save all the edges in the `original` TimeRanges object
for (i = 0; i < original.length; i++) {
start = original.start(i);
end = original.end(i);
edges.push([start, end]);
}
}
if (update) {
// Save any end-points in `update` that are not in the `original`
// TimeRanges object
for (i = 0; i < update.length; i++) {
start = update.start(i);
end = update.end(i);
if (edges.some(overlapsCurrentEnd)) {
continue;
}
// at this point it must be a unique non-shrinking end edge
result.push(end);
}
}
// we err on the side of caution and return null if didn't find
// exactly *one* differing end edge in the search above
if (result.length !== 1) {
return null;
}
return result[0];
};
var parseCodecs = function(codecs) {
var result = {
codecCount: 0,
videoCodec: null,
audioProfile: null
};
result.codecCount = codecs.split(',').length;
result.codecCount = result.codecCount || 2;
// parse the video codec but ignore the version
result.videoCodec = /(^|\s|,)+(avc1)[^ ,]*/i.exec(codecs);
result.videoCodec = result.videoCodec && result.videoCodec[2];
// parse the last field of the audio codec
result.audioProfile = /(^|\s|,)+mp4a.\d+\.(\d+)/i.exec(codecs);
result.audioProfile = result.audioProfile && result.audioProfile[2];
return result;
};
}
/**
/**
* Blacklist playlists that are known to be codec or
* stream-incompatible with the SourceBuffer configuration. For
* instance, Media Source Extensions would cause the video element to
......@@ -331,13 +417,12 @@ var parseCodecs = function(codecs) {
* will be excluded from the default playlist selection algorithm
* indefinitely.
*/
videojs.HlsHandler.prototype.excludeIncompatibleVariants_ = function(media) {
var
master = this.playlists.master,
codecCount = 2,
videoCodec = null,
audioProfile = null,
codecs;
excludeIncompatibleVariants_(media) {
let master = this.playlists.master;
let codecCount = 2;
let videoCodec = null;
let audioProfile = null;
let codecs;
if (media.attributes && media.attributes.CODECS) {
codecs = parseCodecs(media.attributes.CODECS);
......@@ -346,7 +431,7 @@ videojs.HlsHandler.prototype.excludeIncompatibleVariants_ = function(media) {
codecCount = codecs.codecCount;
}
master.playlists.forEach(function(variant) {
var variantCodecs = {
let variantCodecs = {
codecCount: 2,
videoCodec: null,
audioProfile: null
......@@ -374,10 +459,11 @@ videojs.HlsHandler.prototype.excludeIncompatibleVariants_ = function(media) {
variant.excludeUntil = Infinity;
}
});
};
}
videojs.HlsHandler.prototype.setupSourceBuffer_ = function() {
var media = this.playlists.media(), mimeType;
setupSourceBuffer_() {
let media = this.playlists.media();
let mimeType;
// wait until a media playlist is available and the Media Source is
// attached
......@@ -400,16 +486,15 @@ videojs.HlsHandler.prototype.setupSourceBuffer_ = function() {
// transition the sourcebuffer to the ended state if we've hit the end of
// the playlist
this.sourceBuffer.addEventListener('updateend', this.updateEndHandler_.bind(this));
};
}
/**
/**
* Seek to the latest media position if this is a live video and the
* player and video are loaded and initialized.
*/
videojs.HlsHandler.prototype.setupFirstPlay = function() {
var seekable, media;
media = this.playlists.media();
setupFirstPlay() {
let seekable;
let media = this.playlists.media();
// check that everything is ready to begin buffering
......@@ -439,12 +524,12 @@ videojs.HlsHandler.prototype.setupFirstPlay = function() {
this.tech_.setCurrentTime(seekable.end(0));
}
}
};
}
/**
/**
* Begin playing the video.
*/
videojs.HlsHandler.prototype.play = function() {
play() {
this.loadingState_ = 'segments';
if (this.tech_.ended()) {
......@@ -462,11 +547,10 @@ videojs.HlsHandler.prototype.play = function() {
this.tech_.setCurrentTime(this.seekable().start(0));
}
}
};
}
videojs.HlsHandler.prototype.setCurrentTime = function(currentTime) {
var
buffered = this.findBufferedRange_();
setCurrentTime(currentTime) {
let buffered = this.findBufferedRange_();
if (!(this.playlists && this.playlists.media())) {
// return immediately if the metadata is not ready yet
......@@ -503,18 +587,20 @@ videojs.HlsHandler.prototype.setCurrentTime = function(currentTime) {
// begin filling the buffer at the new position
this.fillBuffer(this.playlists.getMediaIndexForTime_(currentTime));
};
}
duration() {
let playlists = this.playlists;
videojs.HlsHandler.prototype.duration = function() {
var playlists = this.playlists;
if (playlists) {
return videojs.Hls.Playlist.duration(playlists.media());
return Hls.Playlist.duration(playlists.media());
}
return 0;
};
}
videojs.HlsHandler.prototype.seekable = function() {
var media, seekable;
seekable() {
let media;
let seekable;
if (!this.playlists) {
return videojs.createTimeRanges();
......@@ -524,7 +610,7 @@ videojs.HlsHandler.prototype.seekable = function() {
return videojs.createTimeRanges();
}
seekable = videojs.Hls.Playlist.seekable(media);
seekable = Hls.Playlist.seekable(media);
if (seekable.length === 0) {
return seekable;
}
......@@ -534,29 +620,27 @@ videojs.HlsHandler.prototype.seekable = function() {
// fall back to the playlist loader's running estimate of expired
// time
if (seekable.start(0) === 0) {
return videojs.createTimeRanges([[
this.playlists.expired_,
this.playlists.expired_ + seekable.end(0)
]]);
return videojs.createTimeRanges([[this.playlists.expired_,
this.playlists.expired_ + seekable.end(0)]]);
}
// seekable has been calculated based on buffering video data so it
// can be returned directly
return seekable;
};
}
/**
/**
* Update the player duration
*/
videojs.HlsHandler.prototype.updateDuration = function(playlist) {
var oldDuration = this.mediaSource.duration,
newDuration = videojs.Hls.Playlist.duration(playlist),
setDuration = function() {
updateDuration(playlist) {
let oldDuration = this.mediaSource.duration;
let newDuration = Hls.Playlist.duration(playlist);
let setDuration = () => {
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) {
......@@ -568,31 +652,31 @@ videojs.HlsHandler.prototype.updateDuration = function(playlist) {
this.tech_.trigger('durationchange');
}
}
};
}
/**
/**
* Clear all buffers and reset any state relevant to the current
* source. After this function is called, the tech should be in a
* state suitable for switching to a different video.
*/
videojs.HlsHandler.prototype.resetSrc_ = function() {
resetSrc_() {
this.cancelSegmentXhr();
this.cancelKeyXhr();
if (this.sourceBuffer && this.mediaSource.readyState === 'open') {
this.sourceBuffer.abort();
}
};
}
videojs.HlsHandler.prototype.cancelKeyXhr = function() {
cancelKeyXhr() {
if (this.keyXhr_) {
this.keyXhr_.onreadystatechange = null;
this.keyXhr_.abort();
this.keyXhr_ = null;
}
};
}
videojs.HlsHandler.prototype.cancelSegmentXhr = function() {
cancelSegmentXhr() {
if (this.segmentXhr_) {
// Prevent error handler from running.
this.segmentXhr_.onreadystatechange = null;
......@@ -602,12 +686,12 @@ videojs.HlsHandler.prototype.cancelSegmentXhr = function() {
// clear out the segment being processed
this.pendingSegment_ = null;
};
}
/**
/**
* Abort all outstanding work and cleanup.
*/
videojs.HlsHandler.prototype.dispose = function() {
dispose() {
this.stopCheckingBuffer_();
if (this.playlists) {
......@@ -615,36 +699,36 @@ videojs.HlsHandler.prototype.dispose = function() {
}
this.resetSrc_();
Component.prototype.dispose.call(this);
};
super.dispose();
}
/**
/**
* Chooses the appropriate media playlist based on the current
* bandwidth estimate and the player size.
* @return the highest bitrate playlist less than the currently detected
* bandwidth, accounting for some amount of bandwidth variance
*/
videojs.HlsHandler.prototype.selectPlaylist = function () {
var
effectiveBitrate,
sortedPlaylists = this.playlists.master.playlists.slice(),
bandwidthPlaylists = [],
now = +new Date(),
i,
variant,
bandwidthBestVariant,
resolutionPlusOne,
resolutionBestVariant,
width,
height;
sortedPlaylists.sort(videojs.Hls.comparePlaylistBandwidth);
selectPlaylist() {
let effectiveBitrate;
let sortedPlaylists = this.playlists.master.playlists.slice();
let bandwidthPlaylists = [];
let now = +new Date();
let i;
let variant;
let bandwidthBestVariant;
let resolutionPlusOne;
let resolutionPlusOneAttribute;
let resolutionBestVariant;
let width;
let height;
sortedPlaylists.sort(Hls.comparePlaylistBandwidth);
// filter out any playlists that have been excluded due to
// incompatible configurations or playback errors
sortedPlaylists = sortedPlaylists.filter(function(variant) {
if (variant.excludeUntil !== undefined) {
return now >= variant.excludeUntil;
sortedPlaylists = sortedPlaylists.filter((localVariant) => {
if (typeof localVariant.excludeUntil !== 'undefined') {
return now >= localVariant.excludeUntil;
}
return true;
});
......@@ -676,9 +760,10 @@ videojs.HlsHandler.prototype.selectPlaylist = function () {
i = bandwidthPlaylists.length;
// sort variants by resolution
bandwidthPlaylists.sort(videojs.Hls.comparePlaylistResolution);
bandwidthPlaylists.sort(Hls.comparePlaylistResolution);
// forget our old variant from above, or we might choose that in high-bandwidth scenarios
// forget our old variant from above,
// or we might choose that in high-bandwidth scenarios
// (this could be the lowest bitrate rendition as we go through all of them above)
variant = null;
......@@ -701,20 +786,22 @@ videojs.HlsHandler.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 === width &&
variant.attributes.RESOLUTION.height === height) {
let variantResolution = variant.attributes.RESOLUTION;
if (variantResolution.width === width &&
variantResolution.height === height) {
// if we have the exact resolution as the player use it
resolutionPlusOne = null;
resolutionBestVariant = variant;
break;
} else if (variant.attributes.RESOLUTION.width < width &&
variant.attributes.RESOLUTION.height < height) {
} else if (variantResolution.width < width &&
variantResolution.height < height) {
// if both dimensions are less than the player use the
// previous (next-largest) variant
break;
} else if (!resolutionPlusOne ||
(variant.attributes.RESOLUTION.width < resolutionPlusOne.attributes.RESOLUTION.width &&
variant.attributes.RESOLUTION.height < resolutionPlusOne.attributes.RESOLUTION.height)) {
(variantResolution.width < resolutionPlusOneAttribute.width &&
variantResolution.height < resolutionPlusOneAttribute.height)) {
// If we still haven't found a good match keep a
// reference to the previous variant for the next loop
// iteration
......@@ -724,17 +811,21 @@ videojs.HlsHandler.prototype.selectPlaylist = function () {
// the highest bandwidth variant that is just-larger-than
// the video player
resolutionPlusOne = variant;
resolutionPlusOneAttribute = resolutionPlusOne.attributes.RESOLUTION;
}
}
// fallback chain of variants
return resolutionPlusOne || resolutionBestVariant || bandwidthBestVariant || sortedPlaylists[0];
};
return resolutionPlusOne ||
resolutionBestVariant ||
bandwidthBestVariant ||
sortedPlaylists[0];
}
/**
/**
* Periodically request new segments and append video data.
*/
videojs.HlsHandler.prototype.checkBuffer_ = function() {
checkBuffer_() {
// calling this method directly resets any outstanding buffer checks
if (this.checkBufferTimeout_) {
window.clearTimeout(this.checkBufferTimeout_);
......@@ -747,101 +838,44 @@ videojs.HlsHandler.prototype.checkBuffer_ = function() {
// wait awhile and try again
this.checkBufferTimeout_ = window.setTimeout((this.checkBuffer_).bind(this),
bufferCheckInterval);
};
}
/**
/**
* Setup a periodic task to request new segments if necessary and
* append bytes into the SourceBuffer.
*/
videojs.HlsHandler.prototype.startCheckingBuffer_ = function() {
startCheckingBuffer_() {
this.checkBuffer_();
};
}
/**
/**
* Stop the periodic task requesting new segments and feeding the
* SourceBuffer.
*/
videojs.HlsHandler.prototype.stopCheckingBuffer_ = function() {
stopCheckingBuffer_() {
if (this.checkBufferTimeout_) {
window.clearTimeout(this.checkBufferTimeout_);
this.checkBufferTimeout_ = null;
}
};
var filterBufferedRanges = function(predicate) {
return function(time) {
var
i,
ranges = [],
tech = this.tech_,
// !!The order of the next two assignments is important!!
// `currentTime` must be equal-to or greater-than the start of the
// buffered range. Flash executes out-of-process so, every value can
// change behind the scenes from line-to-line. By reading `currentTime`
// after `buffered`, we ensure that it is always a current or later
// value during playback.
buffered = tech.buffered();
if (time === undefined) {
time = tech.currentTime();
}
if (buffered && buffered.length) {
// Search for a range containing the play-head
for (i = 0; i < buffered.length; i++) {
if (predicate(buffered.start(i), buffered.end(i), time)) {
ranges.push([buffered.start(i), buffered.end(i)]);
}
}
}
return videojs.createTimeRanges(ranges);
};
};
/**
* Attempts to find the buffered TimeRange that contains the specified
* time, or where playback is currently happening if no specific time
* is specified.
* @param time (optional) {number} the time to filter on. Defaults to
* currentTime.
* @return a new TimeRanges object.
*/
videojs.HlsHandler.prototype.findBufferedRange_ = filterBufferedRanges(function(start, end, time) {
return start - TIME_FUDGE_FACTOR <= time &&
end + TIME_FUDGE_FACTOR >= time;
});
/**
* Returns the TimeRanges that begin at or later than the specified
* time.
* @param time (optional) {number} the time to filter on. Defaults to
* currentTime.
* @return a new TimeRanges object.
*/
videojs.HlsHandler.prototype.findNextBufferedRange_ = filterBufferedRanges(function(start, end, time) {
return start - TIME_FUDGE_FACTOR >= time;
});
/**
/**
* Determines whether there is enough video data currently in the buffer
* and downloads a new segment if the buffered time is less than the goal.
* @param seekToTime (optional) {number} the offset into the downloaded segment
* to seek to, in seconds
*/
videojs.HlsHandler.prototype.fillBuffer = function(mediaIndex) {
var
tech = this.tech_,
currentTime = tech.currentTime(),
hasBufferedContent = (this.tech_.buffered().length !== 0),
currentBuffered = this.findBufferedRange_(),
outsideBufferedRanges = !(currentBuffered && currentBuffered.length),
currentBufferedEnd = 0,
bufferedTime = 0,
segment,
segmentInfo,
segmentTimestampOffset;
fillBuffer(mediaIndex) {
let tech = this.tech_;
let currentTime = tech.currentTime();
let hasBufferedContent = (this.tech_.buffered().length !== 0);
let currentBuffered = this.findBufferedRange_();
let outsideBufferedRanges = !(currentBuffered && currentBuffered.length);
let currentBufferedEnd = 0;
let bufferedTime = 0;
let segment;
let segmentInfo;
let segmentTimestampOffset;
// if preload is set to "none", do not download segments until playback is requested
if (this.loadingState_ !== 'segments') {
......@@ -864,7 +898,7 @@ videojs.HlsHandler.prototype.fillBuffer = function(mediaIndex) {
}
// if no segments are available, do nothing
if (this.playlists.state === "HAVE_NOTHING" ||
if (this.playlists.state === 'HAVE_NOTHING' ||
!this.playlists.media() ||
!this.playlists.media().segments) {
return;
......@@ -875,7 +909,7 @@ videojs.HlsHandler.prototype.fillBuffer = function(mediaIndex) {
return;
}
if (mediaIndex === undefined) {
if (typeof mediaIndex === 'undefined') {
if (currentBuffered && currentBuffered.length) {
currentBufferedEnd = currentBuffered.end(0);
mediaIndex = this.playlists.getMediaIndexForTime_(currentBufferedEnd);
......@@ -883,7 +917,7 @@ videojs.HlsHandler.prototype.fillBuffer = function(mediaIndex) {
// if there is plenty of content in the buffer and we're not
// seeking, relax for awhile
if (bufferedTime >= videojs.Hls.GOAL_BUFFER_LENGTH) {
if (bufferedTime >= Hls.GOAL_BUFFER_LENGTH) {
return;
}
} else {
......@@ -910,12 +944,12 @@ videojs.HlsHandler.prototype.fillBuffer = function(mediaIndex) {
// resolve the segment URL relative to the playlist
uri: this.playlistUriToUrl(segment.uri),
// the segment's mediaIndex & mediaSequence at the time it was requested
mediaIndex: mediaIndex,
mediaIndex,
mediaSequence: this.playlists.media().mediaSequence,
// the segment's playlist
playlist: this.playlists.media(),
// The state of the buffer when this segment was requested
currentBufferedEnd: currentBufferedEnd,
currentBufferedEnd,
// unencrypted bytes of the segment
bytes: null,
// when a key is defined for this segment, the encrypted bytes
......@@ -932,7 +966,7 @@ videojs.HlsHandler.prototype.fillBuffer = function(mediaIndex) {
};
if (mediaIndex > 0) {
segmentTimestampOffset = videojs.Hls.Playlist.duration(segmentInfo.playlist,
segmentTimestampOffset = Hls.Playlist.duration(segmentInfo.playlist,
segmentInfo.playlist.mediaSequence + mediaIndex) + this.playlists.expired_;
}
......@@ -955,43 +989,50 @@ videojs.HlsHandler.prototype.fillBuffer = function(mediaIndex) {
}
this.loadSegment(segmentInfo);
};
}
playlistUriToUrl(segmentRelativeUrl) {
let playListUrl;
videojs.HlsHandler.prototype.playlistUriToUrl = function(segmentRelativeUrl) {
var playListUrl;
// resolve the segment URL relative to the playlist
if (this.playlists.media().uri === this.source_.src) {
playListUrl = resolveUrl(this.source_.src, segmentRelativeUrl);
} else {
playListUrl = resolveUrl(resolveUrl(this.source_.src, this.playlists.media().uri || ''), segmentRelativeUrl);
playListUrl =
resolveUrl(resolveUrl(this.source_.src, this.playlists.media().uri || ''),
segmentRelativeUrl);
}
return playListUrl;
};
}
/* Turns segment byterange into a string suitable for use in
/*
* Turns segment byterange into a string suitable for use in
* HTTP Range requests
*/
videojs.HlsHandler.prototype.byterangeStr_ = function(byterange) {
var byterangeStart, byterangeEnd;
byterangeStr_(byterange) {
let byterangeStart;
let byterangeEnd;
// `byterangeEnd` is one less than `offset + length` because the HTTP range
// header uses inclusive ranges
byterangeEnd = byterange.offset + byterange.length - 1;
byterangeStart = byterange.offset;
return "bytes=" + byterangeStart + "-" + byterangeEnd;
};
return 'bytes=' + byterangeStart + '-' + byterangeEnd;
}
/* Defines headers for use in the xhr request for a particular segment.
/*
* Defines headers for use in the xhr request for a particular segment.
*/
videojs.HlsHandler.prototype.segmentXhrHeaders_ = function(segment) {
var headers = {};
segmentXhrHeaders_(segment) {
let headers = {};
if ('byterange' in segment) {
headers['Range'] = this.byterangeStr_(segment.byterange);
headers.Range = this.byterangeStr_(segment.byterange);
}
return headers;
};
}
/*
/*
* Sets `bandwidth`, `segmentXhrTime`, and appends to the `bytesReceived.
* Expects an object with:
* * `roundTripTime` - the round trip time for the request we're setting the time for
......@@ -999,22 +1040,23 @@ videojs.HlsHandler.prototype.segmentXhrHeaders_ = function(segment) {
* * `bytesReceived` - amount of bytes downloaded
* `bandwidth` is the only required property.
*/
videojs.HlsHandler.prototype.setBandwidth = function(xhr) {
setBandwidth(localXhr) {
// calculate the download bandwidth
this.segmentXhrTime = xhr.roundTripTime;
this.bandwidth = xhr.bandwidth;
this.bytesReceived += xhr.bytesReceived || 0;
this.segmentXhrTime = localXhr.roundTripTime;
this.bandwidth = localXhr.bandwidth;
this.bytesReceived += localXhr.bytesReceived || 0;
this.tech_.trigger('bandwidthupdate');
};
}
/*
/*
* Blacklists a playlist when an error occurs for a set amount of time
* making it unavailable for selection by the rendition selection algorithm
* and then forces a new playlist (rendition) selection.
*/
videojs.HlsHandler.prototype.blacklistCurrentPlaylist_ = function(error) {
var currentPlaylist, nextPlaylist;
blacklistCurrentPlaylist_(error) {
let currentPlaylist;
let nextPlaylist;
// If the `error` was generated by the playlist loader, it will contain
// the playlist we were trying to load (but failed) and that should be
......@@ -1036,26 +1078,27 @@ videojs.HlsHandler.prototype.blacklistCurrentPlaylist_ = function(error) {
nextPlaylist = this.selectPlaylist();
if (nextPlaylist) {
videojs.log.warn('Problem encountered with the current HLS playlist. Switching to another playlist.');
videojs.log.warn('Problem encountered with the current ' +
'HLS playlist. Switching to another playlist.');
return this.playlists.media(nextPlaylist);
} else {
videojs.log.warn('Problem encountered with the current HLS playlist. No suitable alternatives found.');
}
videojs.log.warn('Problem encountered with the current ' +
'HLS playlist. No suitable alternatives found.');
// We have no more playlists we can select so we must fail
this.error = error;
return this.mediaSource.endOfStream('network');
}
};
videojs.HlsHandler.prototype.loadSegment = function(segmentInfo) {
var
self = this,
segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex],
removeToTime = 0,
seekable = this.seekable();
loadSegment(segmentInfo) {
let segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
let removeToTime = 0;
let seekable = this.seekable();
// Chrome has a hard limit of 150mb of buffer and a very conservative "garbage collector"
// We manually clear out the old buffer to ensure we don't trigger the QuotaExceeded error
// Chrome has a hard limit of 150mb of
// buffer and a very conservative "garbage collector"
// We manually clear out the old buffer to ensure
// we don't trigger the QuotaExceeded error
// on the source buffer during subsequent appends
if (this.sourceBuffer && !this.sourceBuffer.updating) {
// If we have a seekable range use that as the limit for what can be removed safely
......@@ -1077,7 +1120,7 @@ videojs.HlsHandler.prototype.loadSegment = function(segmentInfo) {
}
// request the next segment
this.segmentXhr_ = videojs.Hls.xhr({
this.segmentXhr_ = Hls.xhr({
uri: segmentInfo.uri,
responseType: 'arraybuffer',
withCredentials: this.source_.withCredentials,
......@@ -1086,25 +1129,25 @@ videojs.HlsHandler.prototype.loadSegment = function(segmentInfo) {
// decrease in network performance or a server issue.
timeout: (segment.duration * 1.5) * 1000,
headers: this.segmentXhrHeaders_(segment)
}, function(error, request) {
}, (error, request) => {
// This is a timeout of a previously aborted segment request
// so simply ignore it
if (!self.segmentXhr_ || request !== self.segmentXhr_) {
if (!this.segmentXhr_ || request !== this.segmentXhr_) {
return;
}
// the segment request is no longer outstanding
self.segmentXhr_ = null;
this.segmentXhr_ = null;
// if a segment request times out, we may have better luck with another playlist
if (request.timedout) {
self.bandwidth = 1;
return self.playlists.media(self.selectPlaylist());
this.bandwidth = 1;
return this.playlists.media(this.selectPlaylist());
}
// otherwise, trigger a network error
if (!request.aborted && error) {
return self.blacklistCurrentPlaylist_({
return this.blacklistCurrentPlaylist_({
status: request.status,
message: 'HLS segment request error at URL: ' + segmentInfo.uri,
code: (request.status >= 500) ? 4 : 2
......@@ -1116,8 +1159,8 @@ videojs.HlsHandler.prototype.loadSegment = function(segmentInfo) {
return;
}
self.lastSegmentLoaded_ = segment;
self.setBandwidth(request);
this.lastSegmentLoaded_ = segment;
this.setBandwidth(request);
if (segment.key) {
segmentInfo.encryptedBytes = new Uint8Array(request.response);
......@@ -1125,28 +1168,26 @@ videojs.HlsHandler.prototype.loadSegment = function(segmentInfo) {
segmentInfo.bytes = new Uint8Array(request.response);
}
self.pendingSegment_ = segmentInfo;
this.pendingSegment_ = segmentInfo;
self.tech_.trigger('progress');
self.drainBuffer();
this.tech_.trigger('progress');
this.drainBuffer();
// figure out what stream the next segment should be downloaded from
// with the updated bandwidth information
self.playlists.media(self.selectPlaylist());
this.playlists.media(this.selectPlaylist());
});
};
}
videojs.HlsHandler.prototype.drainBuffer = function() {
var
segmentInfo,
mediaIndex,
playlist,
offset,
bytes,
segment,
decrypter,
segIv;
drainBuffer() {
let segmentInfo;
let mediaIndex;
let playlist;
let bytes;
let segment;
let decrypter;
let segIv;
// if the buffer is empty or the source buffer hasn't been created
// yet, do nothing
......@@ -1169,7 +1210,6 @@ videojs.HlsHandler.prototype.drainBuffer = function() {
segmentInfo = this.pendingSegment_;
mediaIndex = segmentInfo.mediaIndex;
playlist = segmentInfo.playlist;
offset = segmentInfo.offset;
bytes = segmentInfo.bytes;
segment = playlist.segments[mediaIndex];
......@@ -1183,30 +1223,30 @@ videojs.HlsHandler.prototype.drainBuffer = function() {
code: 4
});
} else if (!segment.key.bytes) {
// waiting for the key bytes, try again later
return;
} else if (segmentInfo.decrypter) {
// decryption is in progress, try again later
return;
} else {
}
// if the media sequence is greater than 2^32, the IV will be incorrect
// assuming 10s segments, that would be about 1300 years
segIv = segment.key.iv || new Uint32Array([0, 0, 0, mediaIndex + playlist.mediaSequence]);
segIv = segment.key.iv ||
new Uint32Array([0, 0, 0, mediaIndex + playlist.mediaSequence]);
// create a decrypter to incrementally decrypt the segment
decrypter = new videojs.Hls.Decrypter(segmentInfo.encryptedBytes,
decrypter = new Hls.Decrypter(segmentInfo.encryptedBytes,
segment.key.bytes,
segIv,
function(err, bytes) {
segmentInfo.bytes = bytes;
function(error, localBytes) {
if (error) {
videojs.log.warn(error);
}
segmentInfo.bytes = localBytes;
});
segmentInfo.decrypter = decrypter;
return;
}
}
this.pendingSegment_.buffered = this.tech_.buffered();
......@@ -1216,18 +1256,17 @@ videojs.HlsHandler.prototype.drainBuffer = function() {
// the segment is asynchronously added to the current buffered data
this.sourceBuffer.appendBuffer(bytes);
};
}
videojs.HlsHandler.prototype.updateEndHandler_ = function () {
var
segmentInfo = this.pendingSegment_,
segment,
segments,
playlist,
currentMediaIndex,
currentBuffered,
seekable,
timelineUpdate;
updateEndHandler_() {
let segmentInfo = this.pendingSegment_;
let segment;
let segments;
let playlist;
let currentMediaIndex;
let currentBuffered;
let seekable;
let timelineUpdate;
this.pendingSegment_ = null;
......@@ -1238,7 +1277,8 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () {
playlist = this.playlists.media();
segments = playlist.segments;
currentMediaIndex = segmentInfo.mediaIndex + (segmentInfo.mediaSequence - playlist.mediaSequence);
currentMediaIndex = segmentInfo.mediaIndex +
(segmentInfo.mediaSequence - playlist.mediaSequence);
currentBuffered = this.findBufferedRange_();
// if we switched renditions don't try to add segment timeline
......@@ -1260,16 +1300,17 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () {
currentBuffered.length === 0) {
if (seekable.length &&
this.tech_.currentTime() < seekable.start(0)) {
var next = this.findNextBufferedRange_();
let next = this.findNextBufferedRange_();
if (next.length) {
videojs.log('tried seeking to', this.tech_.currentTime(), 'but that was too early, retrying at', next.start(0));
videojs.log('tried seeking to', this.tech_.currentTime(),
'but that was too early, retrying at', next.start(0));
this.tech_.setCurrentTime(next.start(0) + TIME_FUDGE_FACTOR);
}
}
}
timelineUpdate = videojs.Hls.findSoleUncommonTimeRangesEnd_(segmentInfo.buffered,
timelineUpdate = Hls.findSoleUncommonTimeRangesEnd_(segmentInfo.buffered,
this.tech_.buffered());
if (timelineUpdate && segment) {
......@@ -1299,42 +1340,44 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () {
// improves subsequent media index calculations.
this.fillBuffer(currentMediaIndex + 1);
return;
};
}
/**
/**
* Attempt to retrieve the key for a particular media segment.
*/
videojs.HlsHandler.prototype.fetchKey_ = function(segment) {
var key, self, settings, receiveKey;
fetchKey_(segment) {
let key;
let settings;
let receiveKey;
// if there is a pending XHR or no segments, don't do anything
if (this.keyXhr_) {
return;
}
self = this;
settings = this.options_;
/**
* Handle a key XHR response.
*/
receiveKey = function(key) {
return function(error, request) {
var view;
self.keyXhr_ = null;
receiveKey = (keyRecieved) => {
return (error, request) => {
let view;
this.keyXhr_ = null;
if (error || !request.response || request.response.byteLength !== 16) {
key.retries = key.retries || 0;
key.retries++;
keyRecieved.retries = keyRecieved.retries || 0;
keyRecieved.retries++;
if (!request.aborted) {
// try fetching again
self.fetchKey_(segment);
this.fetchKey_(segment);
}
return;
}
view = new DataView(request.response);
key.bytes = new Uint32Array([
keyRecieved.bytes = new Uint32Array([
view.getUint32(0),
view.getUint32(4),
view.getUint32(8),
......@@ -1342,7 +1385,7 @@ videojs.HlsHandler.prototype.fetchKey_ = function(segment) {
]);
// check to see if this allows us to make progress buffering now
self.checkBuffer_();
this.checkBuffer_();
};
};
......@@ -1355,135 +1398,105 @@ videojs.HlsHandler.prototype.fetchKey_ = function(segment) {
// request the key if the retry limit hasn't been reached
if (!key.bytes && !keyFailed(key)) {
this.keyXhr_ = videojs.Hls.xhr({
this.keyXhr_ = Hls.xhr({
uri: this.playlistUriToUrl(key.uri),
responseType: 'arraybuffer',
withCredentials: settings.withCredentials
}, receiveKey(key));
return;
}
};
}
}
/**
* Whether the browser has built-in HLS support.
* Attempts to find the buffered TimeRange that contains the specified
* time, or where playback is currently happening if no specific time
* is specified.
* @param time (optional) {number} the time to filter on. Defaults to
* currentTime.
* @return a new TimeRanges object.
*/
videojs.Hls.supportsNativeHls = (function() {
var
video = document.createElement('video'),
xMpegUrl,
vndMpeg;
// native HLS is definitely not supported if HTML5 video isn't
if (!videojs.getComponent('Html5').isSupported()) {
return false;
}
xMpegUrl = video.canPlayType('application/x-mpegURL');
vndMpeg = video.canPlayType('application/vnd.apple.mpegURL');
return (/probably|maybe/).test(xMpegUrl) ||
(/probably|maybe/).test(vndMpeg);
})();
// HLS is a source handler, not a tech. Make sure attempts to use it
// as one do not cause exceptions.
videojs.Hls.isSupported = function() {
return videojs.log.warn('HLS is no longer a tech. Please remove it from ' +
'your player\'s techOrder.');
};
HlsHandler.prototype.findBufferedRange_ =
filterBufferedRanges(function(start, end, time) {
return start - TIME_FUDGE_FACTOR <= time &&
end + TIME_FUDGE_FACTOR >= time;
});
/**
* A comparator function to sort two playlist object by bandwidth.
* @param left {object} a media playlist object
* @param right {object} a media playlist object
* @return {number} Greater than zero if the bandwidth attribute of
* left is greater than the corresponding attribute of right. Less
* than zero if the bandwidth of right is greater than left and
* exactly zero if the two are equal.
* Returns the TimeRanges that begin at or later than the specified
* time.
* @param time (optional) {number} the time to filter on. Defaults to
* currentTime.
* @return a new TimeRanges object.
*/
videojs.Hls.comparePlaylistBandwidth = function(left, right) {
var leftBandwidth, rightBandwidth;
if (left.attributes && left.attributes.BANDWIDTH) {
leftBandwidth = left.attributes.BANDWIDTH;
}
leftBandwidth = leftBandwidth || window.Number.MAX_VALUE;
if (right.attributes && right.attributes.BANDWIDTH) {
rightBandwidth = right.attributes.BANDWIDTH;
}
rightBandwidth = rightBandwidth || window.Number.MAX_VALUE;
return leftBandwidth - rightBandwidth;
};
HlsHandler.prototype.findNextBufferedRange_ =
filterBufferedRanges(function(start, end, time) {
return start - TIME_FUDGE_FACTOR >= time;
});
/**
* A comparator function to sort two playlist object by resolution (width).
* @param left {object} a media playlist object
* @param right {object} a media playlist object
* @return {number} Greater than zero if the resolution.width attribute of
* left is greater than the corresponding attribute of right. Less
* than zero if the resolution.width of right is greater than left and
* exactly zero if the two are equal.
* The Source Handler object, which informs video.js what additional
* MIME types are supported and sets up playback. It is registered
* automatically to the appropriate tech based on the capabilities of
* the browser it is running in. It is not necessary to use or modify
* this object in normal usage.
*/
videojs.Hls.comparePlaylistResolution = function(left, right) {
var leftWidth, rightWidth;
if (left.attributes && left.attributes.RESOLUTION && left.attributes.RESOLUTION.width) {
leftWidth = left.attributes.RESOLUTION.width;
const HlsSourceHandler = function(mode) {
return {
canHandleSource(srcObj) {
return HlsSourceHandler.canPlayType(srcObj.type);
},
handleSource(source, tech) {
if (mode === 'flash') {
// We need to trigger this asynchronously to give others the chance
// to bind to the event when a source is set at player creation
tech.setTimeout(function() {
tech.trigger('loadstart');
}, 1);
}
leftWidth = leftWidth || window.Number.MAX_VALUE;
if (right.attributes && right.attributes.RESOLUTION && right.attributes.RESOLUTION.width) {
rightWidth = right.attributes.RESOLUTION.width;
tech.hls = new HlsHandler(tech, {
source,
mode
});
tech.hls.src(source.src);
return tech.hls;
},
canPlayType(type) {
return HlsSourceHandler.canPlayType(type);
}
};
};
rightWidth = rightWidth || window.Number.MAX_VALUE;
HlsSourceHandler.canPlayType = function(type) {
let mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i;
// NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions
// have the same media dimensions/ resolution
if (leftWidth === rightWidth && left.attributes.BANDWIDTH && right.attributes.BANDWIDTH) {
return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH;
} else {
return leftWidth - rightWidth;
// favor native HLS support if it's available
if (Hls.supportsNativeHls) {
return false;
}
return mpegurlRE.test(type);
};
/**
* Constructs a new URI by interpreting a path relative to another
* URI.
* @param basePath {string} a relative or absolute URI
* @param path {string} a path part to combine with the base
* @return {string} a URI that is equivalent to composing `base`
* with `path`
* @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue
*/
resolveUrl = videojs.Hls.resolveUrl = function(basePath, path) {
// use the base element to get the browser to handle URI resolution
var
oldBase = document.querySelector('base'),
docHead = document.querySelector('head'),
a = document.createElement('a'),
base = oldBase,
oldHref,
result;
// prep the document
if (oldBase) {
oldHref = oldBase.href;
} else {
base = docHead.appendChild(document.createElement('base'));
}
if (typeof videojs.MediaSource === 'undefined' ||
typeof videojs.URL === 'undefined') {
videojs.MediaSource = MediaSource;
videojs.URL = URL;
}
// register source handlers with the appropriate techs
if (MediaSource.supportsNativeMediaSources()) {
videojs.getComponent('Html5').registerSourceHandler(HlsSourceHandler('html5'));
}
if (window.Uint8Array) {
videojs.getComponent('Flash').registerSourceHandler(HlsSourceHandler('flash'));
}
base.href = basePath;
a.href = path;
result = a.href;
videojs.HlsHandler = HlsHandler;
videojs.HlsSourceHandler = HlsSourceHandler;
videojs.Hls = Hls;
videojs.m3u8 = m3u8;
// clean up
if (oldBase) {
oldBase.href = oldHref;
} else {
docHead.removeChild(base);
}
return result;
export default {
Hls,
HlsHandler,
HlsSourceHandler
};
})(window, window.videojs, document);
......
......@@ -90,7 +90,6 @@ function() {
});
QUnit.module('Incremental Processing', {
beforeEach() {
this.clock = sinon.useFakeTimers();
......
......@@ -13,13 +13,6 @@
<script src="/node_modules/sinon/pkg/sinon.js"></script>
<script src="/node_modules/qunitjs/qunit/qunit.js"></script>
<script src="/node_modules/video.js/dist/video.js"></script>
<script src="/node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script>
<script src="/src/videojs-contrib-hls.js"></script>
<script src="/dist/videojs-contrib-hls.js"></script>
<script src="/src/bin-utils.js"></script>
<script src="/test/videojs-contrib-hls.test.js"></script>
<script src="/dist-test/videojs-contrib-hls.js"></script>
</body>
......
......@@ -11,29 +11,11 @@ var DEFAULTS = {
'node_modules/video.js/dist/video.js',
'node_modules/video.js/dist/video-js.css',
// REMOVE ME WHEN BROWSERIFIED
'node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js',
// these two stub old functionality
'src/videojs-contrib-hls.js',
'dist/videojs-contrib-hls.js',
'src/bin-utils.js',
'test/stub.test.js',
'test/videojs-contrib-hls.test.js',
'test/m3u8.test.js',
'test/playlist.test.js',
'test/playlist-loader.test.js',
'test/decrypter.test.js',
// END REMOVE ME
// 'test/**/*.js'
'test/**/*.test.js'
],
exclude: [
'test/bundle.js',
// 'test/data/**'
'test/data/**'
],
plugins: [
......@@ -42,7 +24,7 @@ var DEFAULTS = {
],
preprocessors: {
'test/{playlist*,decrypter,stub,m3u8}.test.js': ['browserify']
'test/**/*.test.js': ['browserify']
},
reporters: ['dots'],
......
......@@ -29,9 +29,11 @@ QUnit.test('the environment is sane', function(assert) {
assert.strictEqual(typeof Array.isArray, 'function', 'es5 exists');
assert.strictEqual(typeof sinon, 'object', 'sinon exists');
assert.strictEqual(typeof videojs, 'function', 'videojs exists');
assert.strictEqual(typeof videojs.MediaSource, 'object', 'MediaSource is an object');
assert.strictEqual(typeof videojs.MediaSource, 'function', 'MediaSource is an object');
assert.strictEqual(typeof videojs.URL, 'object', 'URL is an object');
assert.strictEqual(typeof videojs.Hls, 'object', 'Hls is an object');
assert.strictEqual(typeof videojs.HlsSourceHandler,'function', 'HlsSourceHandler is a function');
assert.strictEqual(typeof videojs.HlsSourceHandler,
'function',
'HlsSourceHandler is a function');
assert.strictEqual(typeof videojs.HlsHandler, 'function', 'HlsHandler is a function');
});
......
import manifests from './test-manifests';
import expected from './test-expected';
window.manifests = manifests;
window.expected = expected;
This diff could not be displayed because it is too large.