123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613 |
- /**
- * @file videojs-contrib-hls.js
- *
- * The main file for the HLS project.
- * License: https://github.com/videojs/videojs-contrib-hls/blob/master/LICENSE
- */
- import document from 'global/document';
- import PlaylistLoader from './playlist-loader';
- import Playlist from './playlist';
- import xhrFactory from './xhr';
- import {Decrypter, AsyncStream, decrypt} from 'aes-decrypter';
- import utils from './bin-utils';
- import {MediaSource, URL} from 'videojs-contrib-media-sources';
- import m3u8 from 'm3u8-parser';
- import videojs from 'video.js';
- import { MasterPlaylistController } from './master-playlist-controller';
- import Config from './config';
- import renditionSelectionMixin from './rendition-mixin';
- import window from 'global/window';
- import PlaybackWatcher from './playback-watcher';
- import reloadSourceOnError from './reload-source-on-error';
- import {
- lastBandwidthSelector,
- lowestBitrateCompatibleVariantSelector,
- comparePlaylistBandwidth,
- comparePlaylistResolution
- } from './playlist-selectors.js';
- const Hls = {
- PlaylistLoader,
- Playlist,
- Decrypter,
- AsyncStream,
- decrypt,
- utils,
- STANDARD_PLAYLIST_SELECTOR: lastBandwidthSelector,
- INITIAL_PLAYLIST_SELECTOR: lowestBitrateCompatibleVariantSelector,
- comparePlaylistBandwidth,
- comparePlaylistResolution,
- xhr: xhrFactory()
- };
- // 0.5 MB/s
- const INITIAL_BANDWIDTH = 4194304;
- // Define getter/setters for config properites
- [
- 'GOAL_BUFFER_LENGTH',
- 'MAX_GOAL_BUFFER_LENGTH',
- 'GOAL_BUFFER_LENGTH_RATE',
- 'BUFFER_LOW_WATER_LINE',
- 'MAX_BUFFER_LOW_WATER_LINE',
- 'BUFFER_LOW_WATER_LINE_RATE',
- 'BANDWIDTH_VARIANCE'
- ].forEach((prop) => {
- Object.defineProperty(Hls, prop, {
- get() {
- videojs.log.warn(`using Hls.${prop} is UNSAFE be sure you know what you are doing`);
- return Config[prop];
- },
- set(value) {
- videojs.log.warn(`using Hls.${prop} is UNSAFE be sure you know what you are doing`);
- if (typeof value !== 'number' || value < 0) {
- videojs.log.warn(`value of Hls.${prop} must be greater than or equal to 0`);
- return;
- }
- Config[prop] = value;
- }
- });
- });
- /**
- * Updates the selectedIndex of the QualityLevelList when a mediachange happens in hls.
- *
- * @param {QualityLevelList} qualityLevels The QualityLevelList to update.
- * @param {PlaylistLoader} playlistLoader PlaylistLoader containing the new media info.
- * @function handleHlsMediaChange
- */
- const handleHlsMediaChange = function(qualityLevels, playlistLoader) {
- let newPlaylist = playlistLoader.media();
- let selectedIndex = -1;
- for (let i = 0; i < qualityLevels.length; i++) {
- if (qualityLevels[i].id === newPlaylist.uri) {
- selectedIndex = i;
- break;
- }
- }
- qualityLevels.selectedIndex_ = selectedIndex;
- qualityLevels.trigger({
- selectedIndex,
- type: 'change'
- });
- };
- /**
- * Adds quality levels to list once playlist metadata is available
- *
- * @param {QualityLevelList} qualityLevels The QualityLevelList to attach events to.
- * @param {Object} hls Hls object to listen to for media events.
- * @function handleHlsLoadedMetadata
- */
- const handleHlsLoadedMetadata = function(qualityLevels, hls) {
- hls.representations().forEach((rep) => {
- qualityLevels.addQualityLevel(rep);
- });
- handleHlsMediaChange(qualityLevels, hls.playlists);
- };
- // 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.');
- };
- /**
- * Whether the browser has built-in HLS support.
- */
- Hls.supportsNativeHls = (function() {
- let video = document.createElement('video');
- // native HLS is definitely not supported if HTML5 video isn't
- if (!videojs.getTech('Html5').isSupported()) {
- return false;
- }
- // HLS manifests can go by many mime-types
- let canPlay = [
- // Apple santioned
- 'application/vnd.apple.mpegurl',
- // Apple sanctioned for backwards compatibility
- 'audio/mpegurl',
- // Very common
- 'audio/x-mpegurl',
- // Very common
- 'application/x-mpegurl',
- // Included for completeness
- 'video/x-mpegurl',
- 'video/mpegurl',
- 'application/mpegurl'
- ];
- return canPlay.some(function(canItPlay) {
- return (/maybe|probably/i).test(video.canPlayType(canItPlay));
- });
- }());
- /**
- * 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.');
- };
- const Component = videojs.getComponent('Component');
- /**
- * The Hls Handler object, where we orchestrate all of the parts
- * of HLS to interact with video.js
- *
- * @class HlsHandler
- * @extends videojs.Component
- * @param {Object} source the soruce object
- * @param {Tech} tech the parent tech object
- * @param {Object} options optional and required options
- */
- class HlsHandler extends Component {
- constructor(source, tech, options) {
- super(tech, options.hls);
- // tech.player() is deprecated but setup a reference to HLS for
- // backwards-compatibility
- if (tech.options_ && tech.options_.playerId) {
- let _player = videojs(tech.options_.playerId);
- if (!_player.hasOwnProperty('hls')) {
- Object.defineProperty(_player, 'hls', {
- get: () => {
- videojs.log.warn('player.hls is deprecated. Use player.tech_.hls instead.');
- tech.trigger({type: 'usage', name: 'hls-player-access'});
- return this;
- }
- });
- }
- }
- this.tech_ = tech;
- this.source_ = source;
- this.stats = {};
- this.ignoreNextSeekingEvent_ = false;
- this.setOptions_();
- // overriding native HLS only works if audio tracks have been emulated
- // error early if we're misconfigured:
- if (this.options_.overrideNative &&
- (tech.featuresNativeVideoTracks || tech.featuresNativeAudioTracks)) {
- throw new Error('Overriding native HLS requires emulated tracks. ' +
- 'See https://git.io/vMpjB');
- }
- // listen for fullscreenchange events for this player so that we
- // can adjust our quality selection quickly
- this.on(document, [
- 'fullscreenchange', 'webkitfullscreenchange',
- 'mozfullscreenchange', 'MSFullscreenChange'
- ], (event) => {
- let fullscreenElement = document.fullscreenElement ||
- document.webkitFullscreenElement ||
- document.mozFullScreenElement ||
- document.msFullscreenElement;
- if (fullscreenElement && fullscreenElement.contains(this.tech_.el())) {
- this.masterPlaylistController_.fastQualityChange_();
- }
- });
- this.on(this.tech_, 'seeking', function() {
- if (this.ignoreNextSeekingEvent_) {
- this.ignoreNextSeekingEvent_ = false;
- return;
- }
- this.setCurrentTime(this.tech_.currentTime());
- });
- this.on(this.tech_, 'error', function() {
- if (this.masterPlaylistController_) {
- this.masterPlaylistController_.pauseLoading();
- }
- });
- this.on(this.tech_, 'play', this.play);
- }
- setOptions_() {
- // defaults
- this.options_.withCredentials = this.options_.withCredentials || false;
- if (typeof this.options_.blacklistDuration !== 'number') {
- this.options_.blacklistDuration = 5 * 60;
- }
- // start playlist selection at a reasonable bandwidth for
- // broadband internet (0.5 MB/s) or mobile (0.0625 MB/s)
- if (typeof this.options_.bandwidth !== 'number') {
- this.options_.bandwidth = INITIAL_BANDWIDTH;
- }
- // If the bandwidth number is unchanged from the initial setting
- // then this takes precedence over the enableLowInitialPlaylist option
- this.options_.enableLowInitialPlaylist =
- this.options_.enableLowInitialPlaylist &&
- this.options_.bandwidth === INITIAL_BANDWIDTH;
- // grab options passed to player.src
- ['withCredentials', 'bandwidth', 'handleManifestRedirects'].forEach((option) => {
- if (typeof this.source_[option] !== 'undefined') {
- this.options_[option] = this.source_[option];
- }
- });
- this.bandwidth = this.options_.bandwidth;
- }
- /**
- * called when player.src gets called, handle a new source
- *
- * @param {Object} src the source object to handle
- */
- src(src) {
- // do nothing if the src is falsey
- if (!src) {
- return;
- }
- this.setOptions_();
- // add master playlist controller options
- this.options_.url = this.source_.src;
- this.options_.tech = this.tech_;
- this.options_.externHls = Hls;
- this.masterPlaylistController_ = new MasterPlaylistController(this.options_);
- this.playbackWatcher_ = new PlaybackWatcher(
- videojs.mergeOptions(this.options_, {
- seekable: () => this.seekable()
- }));
- this.masterPlaylistController_.on('error', () => {
- let player = videojs.players[this.tech_.options_.playerId];
- player.error(this.masterPlaylistController_.error);
- });
- // `this` in selectPlaylist should be the HlsHandler for backwards
- // compatibility with < v2
- this.masterPlaylistController_.selectPlaylist =
- this.selectPlaylist ?
- this.selectPlaylist.bind(this) : Hls.STANDARD_PLAYLIST_SELECTOR.bind(this);
- this.masterPlaylistController_.selectInitialPlaylist =
- Hls.INITIAL_PLAYLIST_SELECTOR.bind(this);
- // re-expose some internal objects for backwards compatibility with < v2
- this.playlists = this.masterPlaylistController_.masterPlaylistLoader_;
- this.mediaSource = this.masterPlaylistController_.mediaSource;
- // Proxy assignment of some properties to the master playlist
- // controller. Using a custom property for backwards compatibility
- // with < v2
- Object.defineProperties(this, {
- selectPlaylist: {
- get() {
- return this.masterPlaylistController_.selectPlaylist;
- },
- set(selectPlaylist) {
- this.masterPlaylistController_.selectPlaylist = selectPlaylist.bind(this);
- }
- },
- throughput: {
- get() {
- return this.masterPlaylistController_.mainSegmentLoader_.throughput.rate;
- },
- set(throughput) {
- this.masterPlaylistController_.mainSegmentLoader_.throughput.rate = throughput;
- // By setting `count` to 1 the throughput value becomes the starting value
- // for the cumulative average
- this.masterPlaylistController_.mainSegmentLoader_.throughput.count = 1;
- }
- },
- bandwidth: {
- get() {
- return this.masterPlaylistController_.mainSegmentLoader_.bandwidth;
- },
- set(bandwidth) {
- this.masterPlaylistController_.mainSegmentLoader_.bandwidth = bandwidth;
- // setting the bandwidth manually resets the throughput counter
- // `count` is set to zero that current value of `rate` isn't included
- // in the cumulative average
- this.masterPlaylistController_.mainSegmentLoader_.throughput = {
- rate: 0,
- count: 0
- };
- }
- },
- /**
- * `systemBandwidth` is a combination of two serial processes bit-rates. The first
- * is the network bitrate provided by `bandwidth` and the second is the bitrate of
- * the entire process after that - decryption, transmuxing, and appending - provided
- * by `throughput`.
- *
- * Since the two process are serial, the overall system bandwidth is given by:
- * sysBandwidth = 1 / (1 / bandwidth + 1 / throughput)
- */
- systemBandwidth: {
- get() {
- let invBandwidth = 1 / (this.bandwidth || 1);
- let invThroughput;
- if (this.throughput > 0) {
- invThroughput = 1 / this.throughput;
- } else {
- invThroughput = 0;
- }
- let systemBitrate = Math.floor(1 / (invBandwidth + invThroughput));
- return systemBitrate;
- },
- set() {
- videojs.log.error('The "systemBandwidth" property is read-only');
- }
- }
- });
- Object.defineProperties(this.stats, {
- bandwidth: {
- get: () => this.bandwidth || 0,
- enumerable: true
- },
- mediaRequests: {
- get: () => this.masterPlaylistController_.mediaRequests_() || 0,
- enumerable: true
- },
- mediaRequestsAborted: {
- get: () => this.masterPlaylistController_.mediaRequestsAborted_() || 0,
- enumerable: true
- },
- mediaRequestsTimedout: {
- get: () => this.masterPlaylistController_.mediaRequestsTimedout_() || 0,
- enumerable: true
- },
- mediaRequestsErrored: {
- get: () => this.masterPlaylistController_.mediaRequestsErrored_() || 0,
- enumerable: true
- },
- mediaTransferDuration: {
- get: () => this.masterPlaylistController_.mediaTransferDuration_() || 0,
- enumerable: true
- },
- mediaBytesTransferred: {
- get: () => this.masterPlaylistController_.mediaBytesTransferred_() || 0,
- enumerable: true
- },
- mediaSecondsLoaded: {
- get: () => this.masterPlaylistController_.mediaSecondsLoaded_() || 0,
- enumerable: true
- }
- });
- this.tech_.one('canplay',
- this.masterPlaylistController_.setupFirstPlay.bind(this.masterPlaylistController_));
- this.masterPlaylistController_.on('selectedinitialmedia', () => {
- // Add the manual rendition mix-in to HlsHandler
- renditionSelectionMixin(this);
- });
- // the bandwidth of the primary segment loader is our best
- // estimate of overall bandwidth
- this.on(this.masterPlaylistController_, 'progress', function() {
- this.tech_.trigger('progress');
- });
- // In the live case, we need to ignore the very first `seeking` event since
- // that will be the result of the seek-to-live behavior
- this.on(this.masterPlaylistController_, 'firstplay', function() {
- this.ignoreNextSeekingEvent_ = true;
- });
- this.tech_.ready(() => this.setupQualityLevels_());
- // do nothing if the tech has been disposed already
- // this can occur if someone sets the src in player.ready(), for instance
- if (!this.tech_.el()) {
- return;
- }
- this.tech_.src(videojs.URL.createObjectURL(
- this.masterPlaylistController_.mediaSource));
- }
- /**
- * Initializes the quality levels and sets listeners to update them.
- *
- * @method setupQualityLevels_
- * @private
- */
- setupQualityLevels_() {
- let player = videojs.players[this.tech_.options_.playerId];
- if (player && player.qualityLevels) {
- this.qualityLevels_ = player.qualityLevels();
- this.masterPlaylistController_.on('selectedinitialmedia', () => {
- handleHlsLoadedMetadata(this.qualityLevels_, this);
- });
- this.playlists.on('mediachange', () => {
- handleHlsMediaChange(this.qualityLevels_, this.playlists);
- });
- }
- }
- /**
- * Begin playing the video.
- */
- play() {
- this.masterPlaylistController_.play();
- }
- /**
- * a wrapper around the function in MasterPlaylistController
- */
- setCurrentTime(currentTime) {
- this.masterPlaylistController_.setCurrentTime(currentTime);
- }
- /**
- * a wrapper around the function in MasterPlaylistController
- */
- duration() {
- return this.masterPlaylistController_.duration();
- }
- /**
- * a wrapper around the function in MasterPlaylistController
- */
- seekable() {
- return this.masterPlaylistController_.seekable();
- }
- /**
- * Abort all outstanding work and cleanup.
- */
- dispose() {
- if (this.playbackWatcher_) {
- this.playbackWatcher_.dispose();
- }
- if (this.masterPlaylistController_) {
- this.masterPlaylistController_.dispose();
- }
- if (this.qualityLevels_) {
- this.qualityLevels_.dispose();
- }
- super.dispose();
- }
- }
- /**
- * 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.
- */
- const HlsSourceHandler = function(mode) {
- return {
- canHandleSource(srcObj, options = {}) {
- let localOptions = videojs.mergeOptions(videojs.options, options);
- // this forces video.js to skip this tech/mode if its not the one we have been
- // overriden to use, by returing that we cannot handle the source.
- if (localOptions.hls &&
- localOptions.hls.mode &&
- localOptions.hls.mode !== mode) {
- return false;
- }
- return HlsSourceHandler.canPlayType(srcObj.type, localOptions);
- },
- handleSource(source, tech, options = {}) {
- let localOptions = videojs.mergeOptions(videojs.options, options, {hls: {mode}});
- 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(source, tech, localOptions);
- tech.hls.xhr = xhrFactory();
- tech.hls.src(source.src);
- return tech.hls;
- },
- canPlayType(type, options = {}) {
- let localOptions = videojs.mergeOptions(videojs.options, options);
- if (HlsSourceHandler.canPlayType(type, localOptions)) {
- return 'maybe';
- }
- return '';
- }
- };
- };
- HlsSourceHandler.canPlayType = function(type, options) {
- // No support for IE 10 or below
- if (videojs.browser.IE_VERSION && videojs.browser.IE_VERSION <= 10) {
- return false;
- }
- let mpegurlRE = /^(audio|video|application)\/(x-|vnd\.apple\.)?mpegurl/i;
- // favor native HLS support if it's available
- if (!options.hls.overrideNative && Hls.supportsNativeHls) {
- return false;
- }
- return mpegurlRE.test(type);
- };
- if (typeof videojs.MediaSource === 'undefined' ||
- typeof videojs.URL === 'undefined') {
- videojs.MediaSource = MediaSource;
- videojs.URL = URL;
- }
- const flashTech = videojs.getTech('Flash');
- // register source handlers with the appropriate techs
- if (MediaSource.supportsNativeMediaSources()) {
- videojs.getTech('Html5').registerSourceHandler(HlsSourceHandler('html5'), 0);
- }
- if (window.Uint8Array && flashTech) {
- flashTech.registerSourceHandler(HlsSourceHandler('flash'));
- }
- videojs.HlsHandler = HlsHandler;
- videojs.HlsSourceHandler = HlsSourceHandler;
- videojs.Hls = Hls;
- if (!videojs.use) {
- videojs.registerComponent('Hls', Hls);
- }
- videojs.m3u8 = m3u8;
- videojs.options.hls = videojs.options.hls || {};
- if (videojs.registerPlugin) {
- videojs.registerPlugin('reloadSourceOnError', reloadSourceOnError);
- } else {
- videojs.plugin('reloadSourceOnError', reloadSourceOnError);
- }
- module.exports = {
- Hls,
- HlsHandler,
- HlsSourceHandler
- };
|