99580d5c by brandonocasey

browserify-p5: videojs-contrib-hls and bin-utils conversion

removed stub test and stub src files
updated build scripts to continue working
fixed several linting issues that were previously unnoticed
turn the linter back on
remove init function from stream
added ignore for playlist-loader as it will be finished later
1 parent c694b4b7
......@@ -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": [
......
......@@ -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) {
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) {
if (e >= 0x20 && e < 0x7e) {
return String.fromCharCode(e);
}
return '.';
},
result = '',
hex,
ascii;
for (var 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);
},
textRanges: function(ranges) {
var result = '', i;
for (i = 0; i < ranges.length; i++) {
result += textRange(ranges, i) + ' ';
}
return result;
const textRange = function(range, i) {
return range.start(i) + '-' + range.end(i);
};
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 '.';
};
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(tag) {
return utils.hexDump(tag.bytes);
},
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;
}
return result[0];
};
/**
* 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;
}
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;
};
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'),
// A fudge factor to apply to advertised playlist bitrates to account for
// temporary flucations in client bandwidth
const bandwidthVariance = 1.2;
// The amount of time to wait between checking the state of the buffer
bufferCheckInterval = 500,
// 5 minute blacklist
const blacklistDuration = 5 * 60 * 1000;
keyFailed,
resolveUrl;
// 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;
// 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);
};
};
Component.call(this, tech);
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,1409 +303,1198 @@ videojs.HlsHandler = videojs.extend(Component, {
this.on(this.tech_, 'play', this.play);
}
});
src(src) {
let oldMediaPlaylist;
// 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);
// do nothing if the src is falsey
if (!src) {
return;
}
};
};
videojs.HlsSourceHandler.canPlayType = function(type) {
var mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i;
this.mediaSource = new videojs.MediaSource({ mode: this.mode_ });
// favor native HLS support if it's available
if (videojs.Hls.supportsNativeHls) {
return false;
}
return mpegurlRE.test(type);
};
// load the MediaSource into the player
this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen.bind(this));
// 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;
this.options_ = {};
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 Hls.PlaylistLoader(this.source_.src,
this.options_.withCredentials);
videojs.HlsHandler.prototype.src = function(src) {
var oldMediaPlaylist;
this.tech_.one('canplay', this.setupFirstPlay.bind(this));
// do nothing if the src is falsey
if (!src) {
return;
}
this.playlists.on('loadedmetadata', () => {
oldMediaPlaylist = this.playlists.media();
this.mediaSource = new videojs.MediaSource({ mode: this.mode_ });
// 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';
}
// load the MediaSource into the player
this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen.bind(this));
this.setupSourceBuffer_();
this.setupFirstPlay();
this.fillBuffer();
this.tech_.trigger('loadedmetadata');
});
this.options_ = {};
if (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.on('error', () => {
this.blacklistCurrentPlaylist_(this.playlists.error);
});
this.tech_.one('canplay', this.setupFirstPlay.bind(this));
this.playlists.on('loadedplaylist', () => {
let updatedPlaylist = this.playlists.media();
let seekable;
this.playlists.on('loadedmetadata', function() {
oldMediaPlaylist = this.playlists.media();
if (!updatedPlaylist) {
// select the initial variant
this.playlists.media(this.selectPlaylist());
return;
}
// 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';
}
this.updateDuration(this.playlists.media());
this.setupSourceBuffer_();
this.setupFirstPlay();
this.fillBuffer();
this.tech_.trigger('loadedmetadata');
}.bind(this));
// update seekable
seekable = this.seekable();
if (this.duration() === Infinity &&
seekable.length !== 0) {
this.mediaSource.addSeekableRange_(seekable.start(0), seekable.end(0));
}
this.playlists.on('error', function() {
this.blacklistCurrentPlaylist_(this.playlists.error);
}.bind(this));
oldMediaPlaylist = updatedPlaylist;
});
this.playlists.on('loadedplaylist', function() {
var updatedPlaylist = this.playlists.media(), seekable;
this.playlists.on('mediachange', () => {
this.tech_.trigger({
type: 'mediachange',
bubbles: true
});
});
if (!updatedPlaylist) {
// select the initial variant
this.playlists.media(this.selectPlaylist());
// 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.updateDuration(this.playlists.media());
// update seekable
seekable = this.seekable();
if (this.duration() === Infinity &&
seekable.length !== 0) {
this.mediaSource.addSeekableRange_(seekable.start(0), seekable.end(0));
this.tech_.src(videojs.URL.createObjectURL(this.mediaSource));
}
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)
if (!this.sourceBuffer) {
this.setupSourceBuffer_();
}
oldMediaPlaylist = updatedPlaylist;
}.bind(this));
this.playlists.on('mediachange', function() {
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
if (!this.tech_.el()) {
return;
// if autoplay is enabled, begin playback. This is duplicative of
// code in video.js but is required because play() must be invoked
// *after* the media source has opened.
// NOTE: moving this invocation of play() after
// sourceBuffer.appendBuffer() below caused live streams with
// autoplay to stall
if (this.tech_.autoplay()) {
this.play();
}
}
this.tech_.src(videojs.URL.createObjectURL(this.mediaSource));
};
/**
* 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
* stall waiting for video data if you switched from a variant with
* video and audio to an audio-only one.
*
* @param media {object} a media playlist compatible with the current
* set of SourceBuffers. Variants in the current master playlist that
* do not appear to have compatible codec or stream configurations
* will be excluded from the default playlist selection algorithm
* indefinitely.
*/
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);
videoCodec = codecs.videoCodec;
audioProfile = codecs.audioProfile;
codecCount = codecs.codecCount;
}
master.playlists.forEach(function(variant) {
let variantCodecs = {
codecCount: 2,
videoCodec: null,
audioProfile: null
};
if (variant.attributes && variant.attributes.CODECS) {
variantCodecs = parseCodecs(variant.attributes.CODECS);
}
videojs.HlsHandler.prototype.handleSourceOpen = function() {
// 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)
if (!this.sourceBuffer) {
this.setupSourceBuffer_();
}
// if the streams differ in the presence or absence of audio or
// video, they are incompatible
if (variantCodecs.codecCount !== codecCount) {
variant.excludeUntil = Infinity;
}
// if autoplay is enabled, begin playback. This is duplicative of
// code in video.js but is required because play() must be invoked
// *after* the media source has opened.
// NOTE: moving this invocation of play() after
// sourceBuffer.appendBuffer() below caused live streams with
// autoplay to stall
if (this.tech_.autoplay()) {
this.play();
// if h.264 is specified on the current playlist, some flavor of
// it must be specified on all compatible variants
if (variantCodecs.videoCodec !== videoCodec) {
variant.excludeUntil = Infinity;
}
// HE-AAC ("mp4a.40.5") is incompatible with all other versions of
// AAC audio in Chrome 46. Don't mix the two.
if ((variantCodecs.audioProfile === '5' && audioProfile !== '5') ||
(audioProfile === '5' && variantCodecs.audioProfile !== '5')) {
variant.excludeUntil = Infinity;
}
});
}
};
// 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);
setupSourceBuffer_() {
let media = this.playlists.media();
let mimeType;
edges.push([start, end]);
// wait until a media playlist is available and the Media Source is
// attached
if (!media || this.mediaSource.readyState !== 'open') {
return;
}
}
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 the codecs were explicitly specified, pass them along to the
// source buffer
mimeType = 'video/mp2t';
if (media.attributes && media.attributes.CODECS) {
mimeType += '; codecs="' + media.attributes.CODECS + '"';
}
this.sourceBuffer = this.mediaSource.addSourceBuffer(mimeType);
if (edges.some(overlapsCurrentEnd)) {
continue;
}
// exclude any incompatible variant streams from future playlist
// selection
this.excludeIncompatibleVariants_(media);
// at this point it must be a unique non-shrinking end edge
result.push(end);
}
// transition the sourcebuffer to the ended state if we've hit the end of
// the playlist
this.sourceBuffer.addEventListener('updateend', this.updateEndHandler_.bind(this));
}
// 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;
}
/**
* Seek to the latest media position if this is a live video and the
* player and video are loaded and initialized.
*/
setupFirstPlay() {
let seekable;
let media = this.playlists.media();
return result[0];
};
// check that everything is ready to begin buffering
var parseCodecs = function(codecs) {
var result = {
codecCount: 0,
videoCodec: null,
audioProfile: null
};
// 1) the video is a live stream of unknown duration
if (this.duration() === Infinity &&
result.codecCount = codecs.split(',').length;
result.codecCount = result.codecCount || 2;
// 2) the player has not played before and is not paused
this.tech_.played().length === 0 &&
!this.tech_.paused() &&
// parse the video codec but ignore the version
result.videoCodec = /(^|\s|,)+(avc1)[^ ,]*/i.exec(codecs);
result.videoCodec = result.videoCodec && result.videoCodec[2];
// 3) the Media Source and Source Buffers are ready
this.sourceBuffer &&
// parse the last field of the audio codec
result.audioProfile = /(^|\s|,)+mp4a.\d+\.(\d+)/i.exec(codecs);
result.audioProfile = result.audioProfile && result.audioProfile[2];
// 4) the active media playlist is available
media &&
return result;
};
// 5) the video element or flash player is in a readyState of
// at least HAVE_FUTURE_DATA
this.tech_.readyState() >= 1) {
/**
* 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
* stall waiting for video data if you switched from a variant with
* video and audio to an audio-only one.
*
* @param media {object} a media playlist compatible with the current
* set of SourceBuffers. Variants in the current master playlist that
* do not appear to have compatible codec or stream configurations
* 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;
if (media.attributes && media.attributes.CODECS) {
codecs = parseCodecs(media.attributes.CODECS);
videoCodec = codecs.videoCodec;
audioProfile = codecs.audioProfile;
codecCount = codecs.codecCount;
}
master.playlists.forEach(function(variant) {
var variantCodecs = {
codecCount: 2,
videoCodec: null,
audioProfile: null
};
// trigger the playlist loader to start "expired time"-tracking
this.playlists.trigger('firstplay');
if (variant.attributes && variant.attributes.CODECS) {
variantCodecs = parseCodecs(variant.attributes.CODECS);
// seek to the latest media position for live videos
seekable = this.seekable();
if (seekable.length) {
this.tech_.setCurrentTime(seekable.end(0));
}
}
}
// if the streams differ in the presence or absence of audio or
// video, they are incompatible
if (variantCodecs.codecCount !== codecCount) {
variant.excludeUntil = Infinity;
}
/**
* Begin playing the video.
*/
play() {
this.loadingState_ = 'segments';
// if h.264 is specified on the current playlist, some flavor of
// it must be specified on all compatible variants
if (variantCodecs.videoCodec !== videoCodec) {
variant.excludeUntil = Infinity;
}
// HE-AAC ("mp4a.40.5") is incompatible with all other versions of
// AAC audio in Chrome 46. Don't mix the two.
if ((variantCodecs.audioProfile === '5' && audioProfile !== '5') ||
(audioProfile === '5' && variantCodecs.audioProfile !== '5')) {
variant.excludeUntil = Infinity;
if (this.tech_.ended()) {
this.tech_.setCurrentTime(0);
}
});
};
videojs.HlsHandler.prototype.setupSourceBuffer_ = function() {
var media = this.playlists.media(), mimeType;
// wait until a media playlist is available and the Media Source is
// attached
if (!media || this.mediaSource.readyState !== 'open') {
return;
}
if (this.tech_.played().length === 0) {
return this.setupFirstPlay();
}
// if the codecs were explicitly specified, pass them along to the
// source buffer
mimeType = 'video/mp2t';
if (media.attributes && media.attributes.CODECS) {
mimeType += '; codecs="' + media.attributes.CODECS + '"';
// if the viewer has paused and we fell out of the live window,
// seek forward to the earliest available position
if (this.duration() === Infinity) {
if (this.tech_.currentTime() < this.seekable().start(0)) {
this.tech_.setCurrentTime(this.seekable().start(0));
}
}
}
this.sourceBuffer = this.mediaSource.addSourceBuffer(mimeType);
// exclude any incompatible variant streams from future playlist
// selection
this.excludeIncompatibleVariants_(media);
setCurrentTime(currentTime) {
let buffered = this.findBufferedRange_();
// 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();
// check that everything is ready to begin buffering
// 1) the video is a live stream of unknown duration
if (this.duration() === Infinity &&
if (!(this.playlists && this.playlists.media())) {
// return immediately if the metadata is not ready yet
return 0;
}
// 2) the player has not played before and is not paused
this.tech_.played().length === 0 &&
!this.tech_.paused() &&
// it's clearly an edge-case but don't thrown an error if asked to
// seek within an empty playlist
if (!this.playlists.media().segments) {
return 0;
}
// 3) the Media Source and Source Buffers are ready
this.sourceBuffer &&
// if the seek location is already buffered, continue buffering as
// usual
if (buffered && buffered.length) {
return currentTime;
}
// 4) the active media playlist is available
media &&
// if we are in the middle of appending a segment, let it finish up
if (this.pendingSegment_ && this.pendingSegment_.buffered) {
return currentTime;
}
// 5) the video element or flash player is in a readyState of
// at least HAVE_FUTURE_DATA
this.tech_.readyState() >= 1) {
this.lastSegmentLoaded_ = null;
// trigger the playlist loader to start "expired time"-tracking
this.playlists.trigger('firstplay');
// cancel outstanding requests and buffer appends
this.cancelSegmentXhr();
// seek to the latest media position for live videos
seekable = this.seekable();
if (seekable.length) {
this.tech_.setCurrentTime(seekable.end(0));
// abort outstanding key requests, if necessary
if (this.keyXhr_) {
this.keyXhr_.aborted = true;
this.cancelKeyXhr();
}
}
};
/**
* Begin playing the video.
*/
videojs.HlsHandler.prototype.play = function() {
this.loadingState_ = 'segments';
if (this.tech_.ended()) {
this.tech_.setCurrentTime(0);
// begin filling the buffer at the new position
this.fillBuffer(this.playlists.getMediaIndexForTime_(currentTime));
}
if (this.tech_.played().length === 0) {
return this.setupFirstPlay();
}
duration() {
let playlists = this.playlists;
// if the viewer has paused and we fell out of the live window,
// seek forward to the earliest available position
if (this.duration() === Infinity) {
if (this.tech_.currentTime() < this.seekable().start(0)) {
this.tech_.setCurrentTime(this.seekable().start(0));
if (playlists) {
return Hls.Playlist.duration(playlists.media());
}
return 0;
}
};
videojs.HlsHandler.prototype.setCurrentTime = function(currentTime) {
var
buffered = this.findBufferedRange_();
seekable() {
let media;
let seekable;
if (!(this.playlists && this.playlists.media())) {
// return immediately if the metadata is not ready yet
return 0;
}
if (!this.playlists) {
return videojs.createTimeRanges();
}
media = this.playlists.media();
if (!media) {
return videojs.createTimeRanges();
}
// it's clearly an edge-case but don't thrown an error if asked to
// seek within an empty playlist
if (!this.playlists.media().segments) {
return 0;
}
seekable = Hls.Playlist.seekable(media);
if (seekable.length === 0) {
return seekable;
}
// if the seek location is already buffered, continue buffering as
// usual
if (buffered && buffered.length) {
return currentTime;
}
// if the seekable start is zero, it may be because the player has
// been paused for a long time and stopped buffering. in that case,
// 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)]]);
}
// if we are in the middle of appending a segment, let it finish up
if (this.pendingSegment_ && this.pendingSegment_.buffered) {
return currentTime;
// seekable has been calculated based on buffering video data so it
// can be returned directly
return seekable;
}
this.lastSegmentLoaded_ = null;
/**
* Update the player duration
*/
updateDuration(playlist) {
let oldDuration = this.mediaSource.duration;
let newDuration = Hls.Playlist.duration(playlist);
let setDuration = () => {
this.mediaSource.duration = newDuration;
this.tech_.trigger('durationchange');
// cancel outstanding requests and buffer appends
this.cancelSegmentXhr();
this.mediaSource.removeEventListener('sourceopen', setDuration);
};
// abort outstanding key requests, if necessary
if (this.keyXhr_) {
this.keyXhr_.aborted = true;
this.cancelKeyXhr();
// if the duration has changed, invalidate the cached value
if (oldDuration !== newDuration) {
// update the duration
if (this.mediaSource.readyState !== 'open') {
this.mediaSource.addEventListener('sourceopen', setDuration);
} else if (!this.sourceBuffer || !this.sourceBuffer.updating) {
this.mediaSource.duration = newDuration;
this.tech_.trigger('durationchange');
}
}
}
// begin filling the buffer at the new position
this.fillBuffer(this.playlists.getMediaIndexForTime_(currentTime));
};
/**
* 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.
*/
resetSrc_() {
this.cancelSegmentXhr();
this.cancelKeyXhr();
videojs.HlsHandler.prototype.duration = function() {
var playlists = this.playlists;
if (playlists) {
return videojs.Hls.Playlist.duration(playlists.media());
if (this.sourceBuffer && this.mediaSource.readyState === 'open') {
this.sourceBuffer.abort();
}
}
return 0;
};
videojs.HlsHandler.prototype.seekable = function() {
var media, seekable;
if (!this.playlists) {
return videojs.createTimeRanges();
}
media = this.playlists.media();
if (!media) {
return videojs.createTimeRanges();
cancelKeyXhr() {
if (this.keyXhr_) {
this.keyXhr_.onreadystatechange = null;
this.keyXhr_.abort();
this.keyXhr_ = null;
}
}
seekable = videojs.Hls.Playlist.seekable(media);
if (seekable.length === 0) {
return seekable;
}
cancelSegmentXhr() {
if (this.segmentXhr_) {
// Prevent error handler from running.
this.segmentXhr_.onreadystatechange = null;
this.segmentXhr_.abort();
this.segmentXhr_ = null;
}
// if the seekable start is zero, it may be because the player has
// been paused for a long time and stopped buffering. in that case,
// 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)
]]);
// clear out the segment being processed
this.pendingSegment_ = null;
}
// 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() {
this.mediaSource.duration = newDuration;
this.tech_.trigger('durationchange');
this.mediaSource.removeEventListener('sourceopen', setDuration);
}.bind(this);
/**
* Abort all outstanding work and cleanup.
*/
dispose() {
this.stopCheckingBuffer_();
// if the duration has changed, invalidate the cached value
if (oldDuration !== newDuration) {
// update the duration
if (this.mediaSource.readyState !== 'open') {
this.mediaSource.addEventListener('sourceopen', setDuration);
} else if (!this.sourceBuffer || !this.sourceBuffer.updating) {
this.mediaSource.duration = newDuration;
this.tech_.trigger('durationchange');
if (this.playlists) {
this.playlists.dispose();
}
}
};
/**
* 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() {
this.cancelSegmentXhr();
this.cancelKeyXhr();
if (this.sourceBuffer && this.mediaSource.readyState === 'open') {
this.sourceBuffer.abort();
this.resetSrc_();
super.dispose();
}
};
videojs.HlsHandler.prototype.cancelKeyXhr = function() {
if (this.keyXhr_) {
this.keyXhr_.onreadystatechange = null;
this.keyXhr_.abort();
this.keyXhr_ = null;
}
};
videojs.HlsHandler.prototype.cancelSegmentXhr = function() {
if (this.segmentXhr_) {
// Prevent error handler from running.
this.segmentXhr_.onreadystatechange = null;
this.segmentXhr_.abort();
this.segmentXhr_ = null;
}
/**
* 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
*/
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 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((localvariant) => {
if (typeof localvariant.excludeUntil !== 'undefined') {
return now >= localvariant.excludeUntil;
}
return true;
});
// clear out the segment being processed
this.pendingSegment_ = null;
};
// filter out any variant that has greater effective bitrate
// than the current estimated bandwidth
i = sortedPlaylists.length;
while (i--) {
variant = sortedPlaylists[i];
/**
* Abort all outstanding work and cleanup.
*/
videojs.HlsHandler.prototype.dispose = function() {
this.stopCheckingBuffer_();
// ignore playlists without bandwidth information
if (!variant.attributes || !variant.attributes.BANDWIDTH) {
continue;
}
if (this.playlists) {
this.playlists.dispose();
}
effectiveBitrate = variant.attributes.BANDWIDTH * bandwidthVariance;
this.resetSrc_();
Component.prototype.dispose.call(this);
};
if (effectiveBitrate < this.bandwidth) {
bandwidthPlaylists.push(variant);
/**
* 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);
// 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;
}
return true;
});
// filter out any variant that has greater effective bitrate
// than the current estimated bandwidth
i = sortedPlaylists.length;
while (i--) {
variant = sortedPlaylists[i];
// ignore playlists without bandwidth information
if (!variant.attributes || !variant.attributes.BANDWIDTH) {
continue;
}
effectiveBitrate = variant.attributes.BANDWIDTH * bandwidthVariance;
if (effectiveBitrate < this.bandwidth) {
bandwidthPlaylists.push(variant);
// since the playlists are sorted in ascending order by
// bandwidth, the first viable variant is the best
if (!bandwidthBestVariant) {
bandwidthBestVariant = variant;
// since the playlists are sorted in ascending order by
// bandwidth, the first viable variant is the best
if (!bandwidthBestVariant) {
bandwidthBestVariant = variant;
}
}
}
}
i = bandwidthPlaylists.length;
i = bandwidthPlaylists.length;
// sort variants by resolution
bandwidthPlaylists.sort(videojs.Hls.comparePlaylistResolution);
// sort variants by resolution
bandwidthPlaylists.sort(Hls.comparePlaylistResolution);
// 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;
// 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;
width = parseInt(getComputedStyle(this.tech_.el()).width, 10);
height = parseInt(getComputedStyle(this.tech_.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
while (i--) {
variant = bandwidthPlaylists[i];
// iterate through the bandwidth-filtered playlists and find
// best rendition by player dimension
while (i--) {
variant = bandwidthPlaylists[i];
// ignore playlists without resolution information
if (!variant.attributes ||
!variant.attributes.RESOLUTION ||
!variant.attributes.RESOLUTION.width ||
!variant.attributes.RESOLUTION.height) {
continue;
}
// ignore playlists without resolution information
if (!variant.attributes ||
!variant.attributes.RESOLUTION ||
!variant.attributes.RESOLUTION.width ||
!variant.attributes.RESOLUTION.height) {
continue;
}
// since the playlists are sorted, the first variant that has
// dimensions less than or equal to the player size is the best
// 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) {
// if we have the exact resolution as the player use it
resolutionPlusOne = null;
resolutionBestVariant = variant;
break;
} else if (variant.attributes.RESOLUTION.width < width &&
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 < width &&
variant.attributes.RESOLUTION.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)) {
// If we still haven't found a good match keep a
// reference to the previous variant for the next loop
// iteration
// By only saving variants if they are smaller than the
// previously saved variant, we ensure that we also pick
// the highest bandwidth variant that is just-larger-than
// the video player
resolutionPlusOne = variant;
// 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)) {
// If we still haven't found a good match keep a
// reference to the previous variant for the next loop
// iteration
// By only saving variants if they are smaller than the
// previously saved variant, we ensure that we also pick
// the highest bandwidth variant that is just-larger-than
// the video player
resolutionPlusOne = variant;
}
}
}
// fallback chain of variants
return resolutionPlusOne || resolutionBestVariant || bandwidthBestVariant || sortedPlaylists[0];
};
/**
* Periodically request new segments and append video data.
*/
videojs.HlsHandler.prototype.checkBuffer_ = function() {
// calling this method directly resets any outstanding buffer checks
if (this.checkBufferTimeout_) {
window.clearTimeout(this.checkBufferTimeout_);
this.checkBufferTimeout_ = null;
// fallback chain of variants
return resolutionPlusOne ||
resolutionBestVariant ||
bandwidthBestVariant ||
sortedPlaylists[0];
}
this.fillBuffer();
this.drainBuffer();
/**
* Periodically request new segments and append video data.
*/
checkBuffer_() {
// calling this method directly resets any outstanding buffer checks
if (this.checkBufferTimeout_) {
window.clearTimeout(this.checkBufferTimeout_);
this.checkBufferTimeout_ = null;
}
// wait awhile and try again
this.checkBufferTimeout_ = window.setTimeout((this.checkBuffer_).bind(this),
bufferCheckInterval);
};
this.fillBuffer();
this.drainBuffer();
/**
* Setup a periodic task to request new segments if necessary and
* append bytes into the SourceBuffer.
*/
videojs.HlsHandler.prototype.startCheckingBuffer_ = function() {
this.checkBuffer_();
};
// wait awhile and try again
this.checkBufferTimeout_ = window.setTimeout((this.checkBuffer_).bind(this),
bufferCheckInterval);
}
/**
* Stop the periodic task requesting new segments and feeding the
* SourceBuffer.
*/
videojs.HlsHandler.prototype.stopCheckingBuffer_ = function() {
if (this.checkBufferTimeout_) {
window.clearTimeout(this.checkBufferTimeout_);
this.checkBufferTimeout_ = null;
/**
* Setup a periodic task to request new segments if necessary and
* append bytes into the SourceBuffer.
*/
startCheckingBuffer_() {
this.checkBuffer_();
}
};
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();
/**
* Stop the periodic task requesting new segments and feeding the
* SourceBuffer.
*/
stopCheckingBuffer_() {
if (this.checkBufferTimeout_) {
window.clearTimeout(this.checkBufferTimeout_);
this.checkBufferTimeout_ = null;
}
}
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)]);
}
}
/**
* 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
*/
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') {
return;
}
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;
// if preload is set to "none", do not download segments until playback is requested
if (this.loadingState_ !== 'segments') {
return;
}
// if a video has not been specified, do nothing
if (!tech.currentSrc() || !this.playlists) {
return;
}
// if a video has not been specified, do nothing
if (!tech.currentSrc() || !this.playlists) {
return;
}
// if there is a request already in flight, do nothing
if (this.segmentXhr_) {
return;
}
// if there is a request already in flight, do nothing
if (this.segmentXhr_) {
return;
}
// wait until the buffer is up to date
if (this.pendingSegment_) {
return;
}
// wait until the buffer is up to date
if (this.pendingSegment_) {
return;
}
// if no segments are available, do nothing
if (this.playlists.state === "HAVE_NOTHING" ||
!this.playlists.media() ||
!this.playlists.media().segments) {
return;
}
// if no segments are available, do nothing
if (this.playlists.state === 'HAVE_NOTHING' ||
!this.playlists.media() ||
!this.playlists.media().segments) {
return;
}
// if a playlist switch is in progress, wait for it to finish
if (this.playlists.state === 'SWITCHING_MEDIA') {
return;
}
// if a playlist switch is in progress, wait for it to finish
if (this.playlists.state === 'SWITCHING_MEDIA') {
return;
}
if (mediaIndex === undefined) {
if (currentBuffered && currentBuffered.length) {
currentBufferedEnd = currentBuffered.end(0);
mediaIndex = this.playlists.getMediaIndexForTime_(currentBufferedEnd);
bufferedTime = Math.max(0, currentBufferedEnd - currentTime);
if (typeof mediaIndex === 'undefined') {
if (currentBuffered && currentBuffered.length) {
currentBufferedEnd = currentBuffered.end(0);
mediaIndex = this.playlists.getMediaIndexForTime_(currentBufferedEnd);
bufferedTime = Math.max(0, currentBufferedEnd - currentTime);
// if there is plenty of content in the buffer and we're not
// seeking, relax for awhile
if (bufferedTime >= videojs.Hls.GOAL_BUFFER_LENGTH) {
return;
// if there is plenty of content in the buffer and we're not
// seeking, relax for awhile
if (bufferedTime >= Hls.GOAL_BUFFER_LENGTH) {
return;
}
} else {
mediaIndex = this.playlists.getMediaIndexForTime_(this.tech_.currentTime());
}
} else {
mediaIndex = this.playlists.getMediaIndexForTime_(this.tech_.currentTime());
}
}
segment = this.playlists.media().segments[mediaIndex];
segment = this.playlists.media().segments[mediaIndex];
// if the video has finished downloading
if (!segment) {
return;
}
// if the video has finished downloading
if (!segment) {
return;
}
// we have entered a state where we are fetching the same segment,
// try to walk forward
if (this.lastSegmentLoaded_ &&
this.playlistUriToUrl(this.lastSegmentLoaded_.uri) === this.playlistUriToUrl(segment.uri) &&
this.lastSegmentLoaded_.byterange === segment.byterange) {
return this.fillBuffer(mediaIndex + 1);
}
// we have entered a state where we are fetching the same segment,
// try to walk forward
if (this.lastSegmentLoaded_ &&
this.playlistUriToUrl(this.lastSegmentLoaded_.uri) ===
this.playlistUriToUrl(segment.uri) &&
this.lastSegmentLoaded_.byterange === segment.byterange) {
return this.fillBuffer(mediaIndex + 1);
}
// package up all the work to append the segment
segmentInfo = {
// 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,
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,
// unencrypted bytes of the segment
bytes: null,
// when a key is defined for this segment, the encrypted bytes
encryptedBytes: null,
// optionally, the decrypter that is unencrypting the segment
decrypter: null,
// the state of the buffer before a segment is appended will be
// stored here so that the actual segment duration can be
// determined after it has been appended
buffered: null,
// The target timestampOffset for this segment when we append it
// to the source buffer
timestampOffset: null
};
// package up all the work to append the segment
segmentInfo = {
// 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,
mediaSequence: this.playlists.media().mediaSequence,
// the segment's playlist
playlist: this.playlists.media(),
// The state of the buffer when this segment was requested
currentBufferedEnd,
// unencrypted bytes of the segment
bytes: null,
// when a key is defined for this segment, the encrypted bytes
encryptedBytes: null,
// optionally, the decrypter that is unencrypting the segment
decrypter: null,
// the state of the buffer before a segment is appended will be
// stored here so that the actual segment duration can be
// determined after it has been appended
buffered: null,
// The target timestampOffset for this segment when we append it
// to the source buffer
timestampOffset: null
};
if (mediaIndex > 0) {
segmentTimestampOffset = videojs.Hls.Playlist.duration(segmentInfo.playlist,
segmentInfo.playlist.mediaSequence + mediaIndex) + this.playlists.expired_;
}
if (mediaIndex > 0) {
segmentTimestampOffset = Hls.Playlist.duration(segmentInfo.playlist,
segmentInfo.playlist.mediaSequence + mediaIndex) + this.playlists.expired_;
}
if (this.tech_.seeking() && outsideBufferedRanges) {
// If there are discontinuities in the playlist, we can't be sure of anything
// related to time so we reset the timestamp offset and start appending data
// anew on every seek
if (segmentInfo.playlist.discontinuityStarts.length) {
if (this.tech_.seeking() && outsideBufferedRanges) {
// If there are discontinuities in the playlist, we can't be sure of anything
// related to time so we reset the timestamp offset and start appending data
// anew on every seek
if (segmentInfo.playlist.discontinuityStarts.length) {
segmentInfo.timestampOffset = segmentTimestampOffset;
}
} else if (segment.discontinuity && currentBuffered.length) {
// If we aren't seeking and are crossing a discontinuity, we should set
// timestampOffset for new segments to be appended the end of the current
// buffered time-range
segmentInfo.timestampOffset = currentBuffered.end(0);
} else if (!hasBufferedContent && this.tech_.currentTime() > 0.05) {
// If we are trying to play at a position that is not zero but we aren't
// currently seeking according to the video element
segmentInfo.timestampOffset = segmentTimestampOffset;
}
} else if (segment.discontinuity && currentBuffered.length) {
// If we aren't seeking and are crossing a discontinuity, we should set
// timestampOffset for new segments to be appended the end of the current
// buffered time-range
segmentInfo.timestampOffset = currentBuffered.end(0);
} else if (!hasBufferedContent && this.tech_.currentTime() > 0.05) {
// If we are trying to play at a position that is not zero but we aren't
// currently seeking according to the video element
segmentInfo.timestampOffset = segmentTimestampOffset;
this.loadSegment(segmentInfo);
}
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);
// 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);
}
return playListUrl;
}
return playListUrl;
};
/* Turns segment byterange into a string suitable for use in
* HTTP Range requests
*/
videojs.HlsHandler.prototype.byterangeStr_ = function(byterange) {
var byterangeStart, byterangeEnd;
/*
* Turns segment byterange into a string suitable for use in
* HTTP Range requests
*/
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;
};
/* Defines headers for use in the xhr request for a particular segment.
*/
videojs.HlsHandler.prototype.segmentXhrHeaders_ = function(segment) {
var headers = {};
if ('byterange' in segment) {
headers['Range'] = this.byterangeStr_(segment.byterange);
return 'bytes=' + byterangeStart + '-' + byterangeEnd;
}
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
* * `bandwidth` - the bandwidth we want to set
* * `bytesReceived` - amount of bytes downloaded
* `bandwidth` is the only required property.
*/
videojs.HlsHandler.prototype.setBandwidth = function(xhr) {
// calculate the download bandwidth
this.segmentXhrTime = xhr.roundTripTime;
this.bandwidth = xhr.bandwidth;
this.bytesReceived += xhr.bytesReceived || 0;
/*
* Defines headers for use in the xhr request for a particular segment.
*/
segmentXhrHeaders_(segment) {
let headers = {};
this.tech_.trigger('bandwidthupdate');
};
if ('byterange' in segment) {
headers.Range = this.byterangeStr_(segment.byterange);
}
return headers;
}
/*
* 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;
// 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
// blacklisted instead of the currently selected playlist which is likely
// out-of-date in this scenario
currentPlaylist = error.playlist || this.playlists.media();
// If there is no current playlist, then an error occurred while we were
// trying to load the master OR while we were disposing of the tech
if (!currentPlaylist) {
this.error = error;
return this.mediaSource.endOfStream('network');
/*
* 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
* * `bandwidth` - the bandwidth we want to set
* * `bytesReceived` - amount of bytes downloaded
* `bandwidth` is the only required property.
*/
setBandwidth(localXhr) {
// calculate the download bandwidth
this.segmentXhrTime = localXhr.roundTripTime;
this.bandwidth = localXhr.bandwidth;
this.bytesReceived += localXhr.bytesReceived || 0;
this.tech_.trigger('bandwidthupdate');
}
// Blacklist this playlist
currentPlaylist.excludeUntil = Date.now() + blacklistDuration;
/*
* 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.
*/
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
// blacklisted instead of the currently selected playlist which is likely
// out-of-date in this scenario
currentPlaylist = error.playlist || this.playlists.media();
// If there is no current playlist, then an error occurred while we were
// trying to load the master OR while we were disposing of the tech
if (!currentPlaylist) {
this.error = error;
return this.mediaSource.endOfStream('network');
}
// Blacklist this playlist
currentPlaylist.excludeUntil = Date.now() + blacklistDuration;
// Select a new playlist
nextPlaylist = this.selectPlaylist();
// Select a new playlist
nextPlaylist = this.selectPlaylist();
if (nextPlaylist) {
videojs.log.warn('Problem encountered with the current HLS playlist. Switching to another playlist.');
if (nextPlaylist) {
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.');
return this.playlists.media(nextPlaylist);
}
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();
// 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
// otherwise remove anything older than 1 minute before the current play head
if (seekable.length && seekable.start(0) > 0) {
removeToTime = seekable.start(0);
} else {
removeToTime = this.tech_.currentTime() - 60;
}
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
// 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
// otherwise remove anything older than 1 minute before the current play head
if (seekable.length && seekable.start(0) > 0) {
removeToTime = seekable.start(0);
} else {
removeToTime = this.tech_.currentTime() - 60;
}
if (removeToTime > 0) {
this.sourceBuffer.remove(0, removeToTime);
if (removeToTime > 0) {
this.sourceBuffer.remove(0, removeToTime);
}
}
}
// if the segment is encrypted, request the key
if (segment.key) {
this.fetchKey_(segment);
}
// request the next segment
this.segmentXhr_ = videojs.Hls.xhr({
uri: segmentInfo.uri,
responseType: 'arraybuffer',
withCredentials: this.source_.withCredentials,
// Set xhr timeout to 150% of the segment duration to allow us
// some time to switch renditions in the event of a catastrophic
// decrease in network performance or a server issue.
timeout: (segment.duration * 1.5) * 1000,
headers: this.segmentXhrHeaders_(segment)
}, function(error, request) {
// This is a timeout of a previously aborted segment request
// so simply ignore it
if (!self.segmentXhr_ || request !== self.segmentXhr_) {
return;
// if the segment is encrypted, request the key
if (segment.key) {
this.fetchKey_(segment);
}
// the segment request is no longer outstanding
self.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());
}
// request the next segment
this.segmentXhr_ = Hls.xhr({
uri: segmentInfo.uri,
responseType: 'arraybuffer',
withCredentials: this.source_.withCredentials,
// Set xhr timeout to 150% of the segment duration to allow us
// some time to switch renditions in the event of a catastrophic
// decrease in network performance or a server issue.
timeout: (segment.duration * 1.5) * 1000,
headers: this.segmentXhrHeaders_(segment)
}, (error, request) => {
// This is a timeout of a previously aborted segment request
// so simply ignore it
if (!this.segmentXhr_ || request !== this.segmentXhr_) {
return;
}
// otherwise, trigger a network error
if (!request.aborted && error) {
return self.blacklistCurrentPlaylist_({
status: request.status,
message: 'HLS segment request error at URL: ' + segmentInfo.uri,
code: (request.status >= 500) ? 4 : 2
});
}
// the segment request is no longer outstanding
this.segmentXhr_ = null;
// stop processing if the request was aborted
if (!request.response) {
return;
}
// if a segment request times out, we may have better luck with another playlist
if (request.timedout) {
this.bandwidth = 1;
return this.playlists.media(this.selectPlaylist());
}
self.lastSegmentLoaded_ = segment;
self.setBandwidth(request);
// otherwise, trigger a network error
if (!request.aborted && error) {
return this.blacklistCurrentPlaylist_({
status: request.status,
message: 'HLS segment request error at URL: ' + segmentInfo.uri,
code: (request.status >= 500) ? 4 : 2
});
}
if (segment.key) {
segmentInfo.encryptedBytes = new Uint8Array(request.response);
} else {
segmentInfo.bytes = new Uint8Array(request.response);
}
// stop processing if the request was aborted
if (!request.response) {
return;
}
self.pendingSegment_ = segmentInfo;
this.lastSegmentLoaded_ = segment;
this.setBandwidth(request);
self.tech_.trigger('progress');
self.drainBuffer();
if (segment.key) {
segmentInfo.encryptedBytes = new Uint8Array(request.response);
} else {
segmentInfo.bytes = new Uint8Array(request.response);
}
// figure out what stream the next segment should be downloaded from
// with the updated bandwidth information
self.playlists.media(self.selectPlaylist());
});
this.pendingSegment_ = segmentInfo;
};
this.tech_.trigger('progress');
this.drainBuffer();
videojs.HlsHandler.prototype.drainBuffer = function() {
var
segmentInfo,
mediaIndex,
playlist,
offset,
bytes,
segment,
decrypter,
segIv;
// if the buffer is empty or the source buffer hasn't been created
// yet, do nothing
if (!this.pendingSegment_ || !this.sourceBuffer) {
return;
}
// figure out what stream the next segment should be downloaded from
// with the updated bandwidth information
this.playlists.media(this.selectPlaylist());
});
// the pending segment has already been appended and we're waiting
// for updateend to fire
if (this.pendingSegment_.buffered) {
return;
}
// we can't append more data if the source buffer is busy processing
// what we've already sent
if (this.sourceBuffer.updating) {
return;
}
drainBuffer() {
let segmentInfo;
let mediaIndex;
let playlist;
let bytes;
let segment;
let decrypter;
let segIv;
segmentInfo = this.pendingSegment_;
mediaIndex = segmentInfo.mediaIndex;
playlist = segmentInfo.playlist;
offset = segmentInfo.offset;
bytes = segmentInfo.bytes;
segment = playlist.segments[mediaIndex];
if (segment.key && !bytes) {
// this is an encrypted segment
// if the key download failed, we want to skip this segment
// but if the key hasn't downloaded yet, we want to try again later
if (keyFailed(segment.key)) {
return this.blacklistCurrentPlaylist_({
message: 'HLS segment key request error.',
code: 4
});
} else if (!segment.key.bytes) {
// if the buffer is empty or the source buffer hasn't been created
// yet, do nothing
if (!this.pendingSegment_ || !this.sourceBuffer) {
return;
}
// waiting for the key bytes, try again later
// the pending segment has already been appended and we're waiting
// for updateend to fire
if (this.pendingSegment_.buffered) {
return;
} else if (segmentInfo.decrypter) {
}
// decryption is in progress, try again later
// we can't append more data if the source buffer is busy processing
// what we've already sent
if (this.sourceBuffer.updating) {
return;
} else {
}
segmentInfo = this.pendingSegment_;
mediaIndex = segmentInfo.mediaIndex;
playlist = segmentInfo.playlist;
bytes = segmentInfo.bytes;
segment = playlist.segments[mediaIndex];
if (segment.key && !bytes) {
// this is an encrypted segment
// if the key download failed, we want to skip this segment
// but if the key hasn't downloaded yet, we want to try again later
if (keyFailed(segment.key)) {
return this.blacklistCurrentPlaylist_({
message: 'HLS segment key request error.',
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;
}
// 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,
segment.key.bytes,
segIv,
function(err, bytes) {
segmentInfo.bytes = bytes;
});
decrypter = new Hls.Decrypter(segmentInfo.encryptedBytes,
segment.key.bytes,
segIv,
function(err, localBytes) {
if (err) {
throw new Error(err);
}
segmentInfo.bytes = localBytes;
});
segmentInfo.decrypter = decrypter;
return;
}
}
this.pendingSegment_.buffered = this.tech_.buffered();
this.pendingSegment_.buffered = this.tech_.buffered();
if (segmentInfo.timestampOffset !== null) {
this.sourceBuffer.timestampOffset = segmentInfo.timestampOffset;
}
if (segmentInfo.timestampOffset !== null) {
this.sourceBuffer.timestampOffset = segmentInfo.timestampOffset;
// the segment is asynchronously added to the current buffered data
this.sourceBuffer.appendBuffer(bytes);
}
// the segment is asynchronously added to the current buffered data
this.sourceBuffer.appendBuffer(bytes);
};
updateEndHandler_() {
let segmentInfo = this.pendingSegment_;
let segment;
let segments;
let playlist;
let currentMediaIndex;
let currentBuffered;
let seekable;
let timelineUpdate;
videojs.HlsHandler.prototype.updateEndHandler_ = function () {
var
segmentInfo = this.pendingSegment_,
segment,
segments,
playlist,
currentMediaIndex,
currentBuffered,
seekable,
timelineUpdate;
this.pendingSegment_ = null;
// stop here if the update errored or was aborted
if (!segmentInfo) {
return;
}
this.pendingSegment_ = null;
playlist = this.playlists.media();
segments = playlist.segments;
currentMediaIndex = segmentInfo.mediaIndex + (segmentInfo.mediaSequence - playlist.mediaSequence);
currentBuffered = this.findBufferedRange_();
// stop here if the update errored or was aborted
if (!segmentInfo) {
return;
}
// if we switched renditions don't try to add segment timeline
// information to the playlist
if (segmentInfo.playlist.uri !== this.playlists.media().uri) {
return this.fillBuffer();
}
playlist = this.playlists.media();
segments = playlist.segments;
currentMediaIndex = segmentInfo.mediaIndex +
(segmentInfo.mediaSequence - playlist.mediaSequence);
currentBuffered = this.findBufferedRange_();
// annotate the segment with any start and end time information
// added by the media processing
segment = playlist.segments[currentMediaIndex];
// when seeking to the beginning of the seekable range, it's
// possible that imprecise timing information may cause the seek to
// end up earlier than the start of the range
// in that case, seek again
seekable = this.seekable();
if (this.tech_.seeking() &&
currentBuffered.length === 0) {
if (seekable.length &&
this.tech_.currentTime() < seekable.start(0)) {
var next = this.findNextBufferedRange_();
if (next.length) {
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);
// if we switched renditions don't try to add segment timeline
// information to the playlist
if (segmentInfo.playlist.uri !== this.playlists.media().uri) {
return this.fillBuffer();
}
// annotate the segment with any start and end time information
// added by the media processing
segment = playlist.segments[currentMediaIndex];
// when seeking to the beginning of the seekable range, it's
// possible that imprecise timing information may cause the seek to
// end up earlier than the start of the range
// in that case, seek again
seekable = this.seekable();
if (this.tech_.seeking() &&
currentBuffered.length === 0) {
if (seekable.length &&
this.tech_.currentTime() < seekable.start(0)) {
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));
this.tech_.setCurrentTime(next.start(0) + TIME_FUDGE_FACTOR);
}
}
}
}
timelineUpdate = Hls.findSoleUncommonTimeRangesEnd_(segmentInfo.buffered,
this.tech_.buffered());
timelineUpdate = videojs.Hls.findSoleUncommonTimeRangesEnd_(segmentInfo.buffered,
this.tech_.buffered());
if (timelineUpdate && segment) {
segment.end = timelineUpdate;
}
if (timelineUpdate && segment) {
segment.end = timelineUpdate;
}
// if we've buffered to the end of the video, let the MediaSource know
if (this.playlists.media().endList &&
currentBuffered.length &&
segments[segments.length - 1].end <= currentBuffered.end(0) &&
this.mediaSource.readyState === 'open') {
this.mediaSource.endOfStream();
return;
}
// if we've buffered to the end of the video, let the MediaSource know
if (this.playlists.media().endList &&
currentBuffered.length &&
segments[segments.length - 1].end <= currentBuffered.end(0) &&
this.mediaSource.readyState === 'open') {
this.mediaSource.endOfStream();
return;
}
if (timelineUpdate !== null ||
segmentInfo.buffered.length !== this.tech_.buffered().length) {
this.updateDuration(playlist);
// check if it's time to download the next segment
this.fillBuffer();
return;
}
if (timelineUpdate !== null ||
segmentInfo.buffered.length !== this.tech_.buffered().length) {
this.updateDuration(playlist);
// check if it's time to download the next segment
this.fillBuffer();
// the last segment append must have been entirely in the
// already buffered time ranges. just buffer forward until we
// find a segment that adds to the buffered time ranges and
// improves subsequent media index calculations.
this.fillBuffer(currentMediaIndex + 1);
return;
}
// the last segment append must have been entirely in the
// already buffered time ranges. just buffer forward until we
// find a segment that adds to the buffered time ranges and
// improves subsequent media index calculations.
this.fillBuffer(currentMediaIndex + 1);
return;
};
/**
* Attempt to retrieve the key for a particular media segment.
*/
fetchKey_(segment) {
let key;
let settings;
let receiveKey;
/**
* Attempt to retrieve the key for a particular media segment.
*/
videojs.HlsHandler.prototype.fetchKey_ = function(segment) {
var key, self, settings, receiveKey;
// if there is a pending XHR or no segments, don't do anything
if (this.keyXhr_) {
return;
}
// if there is a pending XHR or no segments, don't do anything
if (this.keyXhr_) {
return;
}
settings = this.options_;
self = this;
settings = this.options_;
/**
* Handle a key XHR response.
*/
receiveKey = (keyRecieved) => {
return (error, request) => {
let view;
/**
* Handle a key XHR response.
*/
receiveKey = function(key) {
return function(error, request) {
var view;
self.keyXhr_ = null;
if (error || !request.response || request.response.byteLength !== 16) {
key.retries = key.retries || 0;
key.retries++;
if (!request.aborted) {
// try fetching again
self.fetchKey_(segment);
}
return;
}
this.keyXhr_ = null;
view = new DataView(request.response);
key.bytes = new Uint32Array([
view.getUint32(0),
view.getUint32(4),
view.getUint32(8),
view.getUint32(12)
]);
if (error || !request.response || request.response.byteLength !== 16) {
keyRecieved.retries = keyRecieved.retries || 0;
keyRecieved.retries++;
if (!request.aborted) {
// try fetching again
this.fetchKey_(segment);
}
return;
}
// check to see if this allows us to make progress buffering now
self.checkBuffer_();
view = new DataView(request.response);
keyRecieved.bytes = new Uint32Array([
view.getUint32(0),
view.getUint32(4),
view.getUint32(8),
view.getUint32(12)
]);
// check to see if this allows us to make progress buffering now
this.checkBuffer_();
};
};
};
key = segment.key;
key = segment.key;
// nothing to do if this segment is unencrypted
if (!key) {
return;
}
// nothing to do if this segment is unencrypted
if (!key) {
return;
}
// request the key if the retry limit hasn't been reached
if (!key.bytes && !keyFailed(key)) {
this.keyXhr_ = videojs.Hls.xhr({
uri: this.playlistUriToUrl(key.uri),
responseType: 'arraybuffer',
withCredentials: settings.withCredentials
}, receiveKey(key));
return;
// request the key if the retry limit hasn't been reached
if (!key.bytes && !keyFailed(key)) {
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;
}
leftWidth = leftWidth || window.Number.MAX_VALUE;
if (right.attributes && right.attributes.RESOLUTION && right.attributes.RESOLUTION.width) {
rightWidth = right.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);
}
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;
}
base.href = basePath;
a.href = path;
result = a.href;
// 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'));
}
// clean up
if (oldBase) {
oldBase.href = oldHref;
} else {
docHead.removeChild(base);
}
return result;
};
videojs.HlsHandler = HlsHandler;
videojs.HlsSourceHandler = HlsSourceHandler;
videojs.Hls = Hls;
videojs.m3u8 = m3u8;
})(window, window.videojs, document);
export default {
Hls,
HlsHandler,
HlsSourceHandler
};
......
......@@ -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.