| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402 |
- /**
- * @file videojs-http-streaming.js
- *
- * The main file for the VHS project.
- * License: https://github.com/videojs/videojs-http-streaming/blob/main/LICENSE
- */
- import document from 'global/document';
- import window from 'global/window';
- import PlaylistLoader from './playlist-loader';
- import Playlist from './playlist';
- import xhrFactory from './xhr';
- import { simpleTypeFromSourceType } from '@videojs/vhs-utils/es/media-types.js';
- import * as utils from './bin-utils';
- import {
- getProgramTime,
- seekToProgramTime
- } from './util/time';
- import { timeRangesToArray } from './ranges';
- import videojs from 'video.js';
- import { PlaylistController } from './playlist-controller';
- import Config from './config';
- import renditionSelectionMixin from './rendition-mixin';
- import PlaybackWatcher from './playback-watcher';
- import SourceUpdater from './source-updater';
- import reloadSourceOnError from './reload-source-on-error';
- import {
- lastBandwidthSelector,
- lowestBitrateCompatibleVariantSelector,
- movingAverageBandwidthSelector,
- comparePlaylistBandwidth,
- comparePlaylistResolution
- } from './playlist-selectors.js';
- import {
- browserSupportsCodec,
- getMimeForCodec,
- parseCodecs
- } from '@videojs/vhs-utils/es/codecs.js';
- import { unwrapCodecList } from './util/codecs.js';
- import logger from './util/logger';
- import {SAFE_TIME_DELTA} from './ranges';
- import {merge} from './util/vjs-compat';
- // IMPORTANT:
- // keep these at the bottom they are replaced at build time
- // because webpack and rollup without plugins do not support json
- // and we do not want to break our users
- import {version as vhsVersion} from '../package.json';
- import {version as muxVersion} from 'mux.js/package.json';
- import {version as mpdVersion} from 'mpd-parser/package.json';
- import {version as m3u8Version} from 'm3u8-parser/package.json';
- import {version as aesVersion} from 'aes-decrypter/package.json';
- const Vhs = {
- PlaylistLoader,
- Playlist,
- utils,
- STANDARD_PLAYLIST_SELECTOR: lastBandwidthSelector,
- INITIAL_PLAYLIST_SELECTOR: lowestBitrateCompatibleVariantSelector,
- lastBandwidthSelector,
- movingAverageBandwidthSelector,
- comparePlaylistBandwidth,
- comparePlaylistResolution,
- xhr: xhrFactory()
- };
- // Define getter/setters for config properties
- Object.keys(Config).forEach((prop) => {
- Object.defineProperty(Vhs, prop, {
- get() {
- videojs.log.warn(`using Vhs.${prop} is UNSAFE be sure you know what you are doing`);
- return Config[prop];
- },
- set(value) {
- videojs.log.warn(`using Vhs.${prop} is UNSAFE be sure you know what you are doing`);
- if (typeof value !== 'number' || value < 0) {
- videojs.log.warn(`value of Vhs.${prop} must be greater than or equal to 0`);
- return;
- }
- Config[prop] = value;
- }
- });
- });
- export const LOCAL_STORAGE_KEY = 'videojs-vhs';
- /**
- * Updates the selectedIndex of the QualityLevelList when a mediachange happens in vhs.
- *
- * @param {QualityLevelList} qualityLevels The QualityLevelList to update.
- * @param {PlaylistLoader} playlistLoader PlaylistLoader containing the new media info.
- * @function handleVhsMediaChange
- */
- const handleVhsMediaChange = function(qualityLevels, playlistLoader) {
- const newPlaylist = playlistLoader.media();
- let selectedIndex = -1;
- for (let i = 0; i < qualityLevels.length; i++) {
- if (qualityLevels[i].id === newPlaylist.id) {
- 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} vhs Vhs object to listen to for media events.
- * @function handleVhsLoadedMetadata
- */
- const handleVhsLoadedMetadata = function(qualityLevels, vhs) {
- vhs.representations().forEach((rep) => {
- qualityLevels.addQualityLevel(rep);
- });
- handleVhsMediaChange(qualityLevels, vhs.playlists);
- };
- // VHS is a source handler, not a tech. Make sure attempts to use it
- // as one do not cause exceptions.
- Vhs.canPlaySource = function() {
- return videojs.log.warn('VHS is no longer a tech. Please remove it from ' +
- 'your player\'s techOrder.');
- };
- const emeKeySystems = (keySystemOptions, mainPlaylist, audioPlaylist) => {
- if (!keySystemOptions) {
- return keySystemOptions;
- }
- let codecs = {};
- if (mainPlaylist && mainPlaylist.attributes && mainPlaylist.attributes.CODECS) {
- codecs = unwrapCodecList(parseCodecs(mainPlaylist.attributes.CODECS));
- }
- if (audioPlaylist && audioPlaylist.attributes && audioPlaylist.attributes.CODECS) {
- codecs.audio = audioPlaylist.attributes.CODECS;
- }
- const videoContentType = getMimeForCodec(codecs.video);
- const audioContentType = getMimeForCodec(codecs.audio);
- // upsert the content types based on the selected playlist
- const keySystemContentTypes = {};
- for (const keySystem in keySystemOptions) {
- keySystemContentTypes[keySystem] = {};
- if (audioContentType) {
- keySystemContentTypes[keySystem].audioContentType = audioContentType;
- }
- if (videoContentType) {
- keySystemContentTypes[keySystem].videoContentType = videoContentType;
- }
- // Default to using the video playlist's PSSH even though they may be different, as
- // videojs-contrib-eme will only accept one in the options.
- //
- // This shouldn't be an issue for most cases as early intialization will handle all
- // unique PSSH values, and if they aren't, then encrypted events should have the
- // specific information needed for the unique license.
- if (mainPlaylist.contentProtection &&
- mainPlaylist.contentProtection[keySystem] &&
- mainPlaylist.contentProtection[keySystem].pssh) {
- keySystemContentTypes[keySystem].pssh =
- mainPlaylist.contentProtection[keySystem].pssh;
- }
- // videojs-contrib-eme accepts the option of specifying: 'com.some.cdm': 'url'
- // so we need to prevent overwriting the URL entirely
- if (typeof keySystemOptions[keySystem] === 'string') {
- keySystemContentTypes[keySystem].url = keySystemOptions[keySystem];
- }
- }
- return merge(keySystemOptions, keySystemContentTypes);
- };
- /**
- * @typedef {Object} KeySystems
- *
- * keySystems configuration for https://github.com/videojs/videojs-contrib-eme
- * Note: not all options are listed here.
- *
- * @property {Uint8Array} [pssh]
- * Protection System Specific Header
- */
- /**
- * Goes through all the playlists and collects an array of KeySystems options objects
- * containing each playlist's keySystems and their pssh values, if available.
- *
- * @param {Object[]} playlists
- * The playlists to look through
- * @param {string[]} keySystems
- * The keySystems to collect pssh values for
- *
- * @return {KeySystems[]}
- * An array of KeySystems objects containing available key systems and their
- * pssh values
- */
- const getAllPsshKeySystemsOptions = (playlists, keySystems) => {
- return playlists.reduce((keySystemsArr, playlist) => {
- if (!playlist.contentProtection) {
- return keySystemsArr;
- }
- const keySystemsOptions = keySystems.reduce((keySystemsObj, keySystem) => {
- const keySystemOptions = playlist.contentProtection[keySystem];
- if (keySystemOptions && keySystemOptions.pssh) {
- keySystemsObj[keySystem] = { pssh: keySystemOptions.pssh };
- }
- return keySystemsObj;
- }, {});
- if (Object.keys(keySystemsOptions).length) {
- keySystemsArr.push(keySystemsOptions);
- }
- return keySystemsArr;
- }, []);
- };
- /**
- * Returns a promise that waits for the
- * [eme plugin](https://github.com/videojs/videojs-contrib-eme) to create a key session.
- *
- * Works around https://bugs.chromium.org/p/chromium/issues/detail?id=895449 in non-IE11
- * browsers.
- *
- * As per the above ticket, this is particularly important for Chrome, where, if
- * unencrypted content is appended before encrypted content and the key session has not
- * been created, a MEDIA_ERR_DECODE will be thrown once the encrypted content is reached
- * during playback.
- *
- * @param {Object} player
- * The player instance
- * @param {Object[]} sourceKeySystems
- * The key systems options from the player source
- * @param {Object} [audioMedia]
- * The active audio media playlist (optional)
- * @param {Object[]} mainPlaylists
- * The playlists found on the main playlist object
- *
- * @return {Object}
- * Promise that resolves when the key session has been created
- */
- export const waitForKeySessionCreation = ({
- player,
- sourceKeySystems,
- audioMedia,
- mainPlaylists
- }) => {
- if (!player.eme.initializeMediaKeys) {
- return Promise.resolve();
- }
- // TODO should all audio PSSH values be initialized for DRM?
- //
- // All unique video rendition pssh values are initialized for DRM, but here only
- // the initial audio playlist license is initialized. In theory, an encrypted
- // event should be fired if the user switches to an alternative audio playlist
- // where a license is required, but this case hasn't yet been tested. In addition, there
- // may be many alternate audio playlists unlikely to be used (e.g., multiple different
- // languages).
- const playlists = audioMedia ? mainPlaylists.concat([audioMedia]) : mainPlaylists;
- const keySystemsOptionsArr = getAllPsshKeySystemsOptions(
- playlists,
- Object.keys(sourceKeySystems)
- );
- const initializationFinishedPromises = [];
- const keySessionCreatedPromises = [];
- // Since PSSH values are interpreted as initData, EME will dedupe any duplicates. The
- // only place where it should not be deduped is for ms-prefixed APIs, but
- // the existence of modern EME APIs in addition to
- // ms-prefixed APIs on Edge should prevent this from being a concern.
- // initializeMediaKeys also won't use the webkit-prefixed APIs.
- keySystemsOptionsArr.forEach((keySystemsOptions) => {
- keySessionCreatedPromises.push(new Promise((resolve, reject) => {
- player.tech_.one('keysessioncreated', resolve);
- }));
- initializationFinishedPromises.push(new Promise((resolve, reject) => {
- player.eme.initializeMediaKeys({
- keySystems: keySystemsOptions
- }, (err) => {
- if (err) {
- reject(err);
- return;
- }
- resolve();
- });
- }));
- });
- // The reasons Promise.race is chosen over Promise.any:
- //
- // * Promise.any is only available in Safari 14+.
- // * None of these promises are expected to reject. If they do reject, it might be
- // better here for the race to surface the rejection, rather than mask it by using
- // Promise.any.
- return Promise.race([
- // If a session was previously created, these will all finish resolving without
- // creating a new session, otherwise it will take until the end of all license
- // requests, which is why the key session check is used (to make setup much faster).
- Promise.all(initializationFinishedPromises),
- // Once a single session is created, the browser knows DRM will be used.
- Promise.race(keySessionCreatedPromises)
- ]);
- };
- /**
- * If the [eme](https://github.com/videojs/videojs-contrib-eme) plugin is available, and
- * there are keySystems on the source, sets up source options to prepare the source for
- * eme.
- *
- * @param {Object} player
- * The player instance
- * @param {Object[]} sourceKeySystems
- * The key systems options from the player source
- * @param {Object} media
- * The active media playlist
- * @param {Object} [audioMedia]
- * The active audio media playlist (optional)
- *
- * @return {boolean}
- * Whether or not options were configured and EME is available
- */
- const setupEmeOptions = ({
- player,
- sourceKeySystems,
- media,
- audioMedia
- }) => {
- const sourceOptions = emeKeySystems(sourceKeySystems, media, audioMedia);
- if (!sourceOptions) {
- return false;
- }
- player.currentSource().keySystems = sourceOptions;
- // eme handles the rest of the setup, so if it is missing
- // do nothing.
- if (sourceOptions && !player.eme) {
- videojs.log.warn('DRM encrypted source cannot be decrypted without a DRM plugin');
- return false;
- }
- return true;
- };
- const getVhsLocalStorage = () => {
- if (!window.localStorage) {
- return null;
- }
- const storedObject = window.localStorage.getItem(LOCAL_STORAGE_KEY);
- if (!storedObject) {
- return null;
- }
- try {
- return JSON.parse(storedObject);
- } catch (e) {
- // someone may have tampered with the value
- return null;
- }
- };
- const updateVhsLocalStorage = (options) => {
- if (!window.localStorage) {
- return false;
- }
- let objectToStore = getVhsLocalStorage();
- objectToStore = objectToStore ? merge(objectToStore, options) : options;
- try {
- window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(objectToStore));
- } catch (e) {
- // Throws if storage is full (e.g., always on iOS 5+ Safari private mode, where
- // storage is set to 0).
- // https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem#Exceptions
- // No need to perform any operation.
- return false;
- }
- return objectToStore;
- };
- /**
- * Parses VHS-supported media types from data URIs. See
- * https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
- * for information on data URIs.
- *
- * @param {string} dataUri
- * The data URI
- *
- * @return {string|Object}
- * The parsed object/string, or the original string if no supported media type
- * was found
- */
- const expandDataUri = (dataUri) => {
- if (dataUri.toLowerCase().indexOf('data:application/vnd.videojs.vhs+json,') === 0) {
- return JSON.parse(dataUri.substring(dataUri.indexOf(',') + 1));
- }
- // no known case for this data URI, return the string as-is
- return dataUri;
- };
- /**
- * Adds a request hook to an xhr object
- *
- * @param {Object} xhr object to add the onRequest hook to
- * @param {function} callback hook function for an xhr request
- */
- const addOnRequestHook = (xhr, callback) => {
- if (!xhr._requestCallbackSet) {
- xhr._requestCallbackSet = new Set();
- }
- xhr._requestCallbackSet.add(callback);
- };
- /**
- * Adds a response hook to an xhr object
- *
- * @param {Object} xhr object to add the onResponse hook to
- * @param {function} callback hook function for an xhr response
- */
- const addOnResponseHook = (xhr, callback) => {
- if (!xhr._responseCallbackSet) {
- xhr._responseCallbackSet = new Set();
- }
- xhr._responseCallbackSet.add(callback);
- };
- /**
- * Removes a request hook on an xhr object, deletes the onRequest set if empty.
- *
- * @param {Object} xhr object to remove the onRequest hook from
- * @param {function} callback hook function to remove
- */
- const removeOnRequestHook = (xhr, callback) => {
- if (!xhr._requestCallbackSet) {
- return;
- }
- xhr._requestCallbackSet.delete(callback);
- if (!xhr._requestCallbackSet.size) {
- delete xhr._requestCallbackSet;
- }
- };
- /**
- * Removes a response hook on an xhr object, deletes the onResponse set if empty.
- *
- * @param {Object} xhr object to remove the onResponse hook from
- * @param {function} callback hook function to remove
- */
- const removeOnResponseHook = (xhr, callback) => {
- if (!xhr._responseCallbackSet) {
- return;
- }
- xhr._responseCallbackSet.delete(callback);
- if (!xhr._responseCallbackSet.size) {
- delete xhr._responseCallbackSet;
- }
- };
- /**
- * Whether the browser has built-in HLS support.
- */
- Vhs.supportsNativeHls = (function() {
- if (!document || !document.createElement) {
- return false;
- }
- const 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
- const 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));
- });
- }());
- Vhs.supportsNativeDash = (function() {
- if (!document || !document.createElement || !videojs.getTech('Html5').isSupported()) {
- return false;
- }
- return (/maybe|probably/i).test(document.createElement('video').canPlayType('application/dash+xml'));
- }());
- Vhs.supportsTypeNatively = (type) => {
- if (type === 'hls') {
- return Vhs.supportsNativeHls;
- }
- if (type === 'dash') {
- return Vhs.supportsNativeDash;
- }
- return false;
- };
- /**
- * VHS is a source handler, not a tech. Make sure attempts to use it
- * as one do not cause exceptions.
- */
- Vhs.isSupported = function() {
- return videojs.log.warn('VHS is no longer a tech. Please remove it from ' +
- 'your player\'s techOrder.');
- };
- /**
- * A global function for setting an onRequest hook
- *
- * @param {function} callback for request modifiction
- */
- Vhs.xhr.onRequest = function(callback) {
- addOnRequestHook(Vhs.xhr, callback);
- };
- /**
- * A global function for setting an onResponse hook
- *
- * @param {callback} callback for response data retrieval
- */
- Vhs.xhr.onResponse = function(callback) {
- addOnResponseHook(Vhs.xhr, callback);
- };
- /**
- * Deletes a global onRequest callback if it exists
- *
- * @param {function} callback to delete from the global set
- */
- Vhs.xhr.offRequest = function(callback) {
- removeOnRequestHook(Vhs.xhr, callback);
- };
- /**
- * Deletes a global onResponse callback if it exists
- *
- * @param {function} callback to delete from the global set
- */
- Vhs.xhr.offResponse = function(callback) {
- removeOnResponseHook(Vhs.xhr, callback);
- };
- const Component = videojs.getComponent('Component');
- /**
- * The Vhs Handler object, where we orchestrate all of the parts
- * of VHS to interact with video.js
- *
- * @class VhsHandler
- * @extends videojs.Component
- * @param {Object} source the soruce object
- * @param {Tech} tech the parent tech object
- * @param {Object} options optional and required options
- */
- class VhsHandler extends Component {
- constructor(source, tech, options) {
- super(tech, options.vhs);
- // if a tech level `initialBandwidth` option was passed
- // use that over the VHS level `bandwidth` option
- if (typeof options.initialBandwidth === 'number') {
- this.options_.bandwidth = options.initialBandwidth;
- }
- this.logger_ = logger('VhsHandler');
- // we need access to the player in some cases,
- // so, get it from Video.js via the `playerId`
- if (tech.options_ && tech.options_.playerId) {
- const _player = videojs.getPlayer(tech.options_.playerId);
- this.player_ = _player;
- }
- this.tech_ = tech;
- this.source_ = source;
- this.stats = {};
- this.ignoreNextSeekingEvent_ = false;
- this.setOptions_();
- if (this.options_.overrideNative &&
- tech.overrideNativeAudioTracks &&
- tech.overrideNativeVideoTracks) {
- tech.overrideNativeAudioTracks(true);
- tech.overrideNativeVideoTracks(true);
- } else if (this.options_.overrideNative &&
- (tech.featuresNativeVideoTracks || tech.featuresNativeAudioTracks)) {
- // overriding native VHS only works if audio tracks have been emulated
- // error early if we're misconfigured
- throw new Error('Overriding native VHS 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) => {
- const fullscreenElement = document.fullscreenElement ||
- document.webkitFullscreenElement ||
- document.mozFullScreenElement ||
- document.msFullscreenElement;
- if (fullscreenElement && fullscreenElement.contains(this.tech_.el())) {
- this.playlistController_.fastQualityChange_();
- } else {
- // When leaving fullscreen, since the in page pixel dimensions should be smaller
- // than full screen, see if there should be a rendition switch down to preserve
- // bandwidth.
- this.playlistController_.checkABR_();
- }
- });
- this.on(this.tech_, 'seeking', function() {
- if (this.ignoreNextSeekingEvent_) {
- this.ignoreNextSeekingEvent_ = false;
- return;
- }
- this.setCurrentTime(this.tech_.currentTime());
- });
- this.on(this.tech_, 'error', function() {
- // verify that the error was real and we are loaded
- // enough to have pc loaded.
- if (this.tech_.error() && this.playlistController_) {
- this.playlistController_.pauseLoading();
- }
- });
- this.on(this.tech_, 'play', this.play);
- }
- /**
- * Set VHS options based on options from configuration, as well as partial
- * options to be passed at a later time.
- *
- * @param {Object} options A partial chunk of config options
- */
- setOptions_(options = {}) {
- this.options_ = merge(this.options_, options);
- // defaults
- this.options_.withCredentials = this.options_.withCredentials || false;
- this.options_.limitRenditionByPlayerDimensions = this.options_.limitRenditionByPlayerDimensions === false ? false : true;
- this.options_.useDevicePixelRatio = this.options_.useDevicePixelRatio || false;
- this.options_.useBandwidthFromLocalStorage =
- typeof this.source_.useBandwidthFromLocalStorage !== 'undefined' ?
- this.source_.useBandwidthFromLocalStorage :
- this.options_.useBandwidthFromLocalStorage || false;
- this.options_.useForcedSubtitles = this.options_.useForcedSubtitles || false;
- this.options_.useNetworkInformationApi = this.options_.useNetworkInformationApi || false;
- this.options_.useDtsForTimestampOffset = this.options_.useDtsForTimestampOffset || false;
- this.options_.customTagParsers = this.options_.customTagParsers || [];
- this.options_.customTagMappers = this.options_.customTagMappers || [];
- this.options_.cacheEncryptionKeys = this.options_.cacheEncryptionKeys || false;
- this.options_.llhls = this.options_.llhls === false ? false : true;
- this.options_.bufferBasedABR = this.options_.bufferBasedABR || false;
- if (typeof this.options_.playlistExclusionDuration !== 'number') {
- this.options_.playlistExclusionDuration = 60;
- }
- if (typeof this.options_.bandwidth !== 'number') {
- if (this.options_.useBandwidthFromLocalStorage) {
- const storedObject = getVhsLocalStorage();
- if (storedObject && storedObject.bandwidth) {
- this.options_.bandwidth = storedObject.bandwidth;
- this.tech_.trigger({type: 'usage', name: 'vhs-bandwidth-from-local-storage'});
- }
- if (storedObject && storedObject.throughput) {
- this.options_.throughput = storedObject.throughput;
- this.tech_.trigger({type: 'usage', name: 'vhs-throughput-from-local-storage'});
- }
- }
- }
- // if bandwidth was not set by options or pulled from local storage, start playlist
- // selection at a reasonable bandwidth
- if (typeof this.options_.bandwidth !== 'number') {
- this.options_.bandwidth = Config.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 === Config.INITIAL_BANDWIDTH;
- // grab options passed to player.src
- [
- 'withCredentials',
- 'useDevicePixelRatio',
- 'limitRenditionByPlayerDimensions',
- 'bandwidth',
- 'customTagParsers',
- 'customTagMappers',
- 'cacheEncryptionKeys',
- 'playlistSelector',
- 'initialPlaylistSelector',
- 'bufferBasedABR',
- 'liveRangeSafeTimeDelta',
- 'llhls',
- 'useForcedSubtitles',
- 'useNetworkInformationApi',
- 'useDtsForTimestampOffset',
- 'exactManifestTimings',
- 'leastPixelDiffSelector'
- ].forEach((option) => {
- if (typeof this.source_[option] !== 'undefined') {
- this.options_[option] = this.source_[option];
- }
- });
- this.limitRenditionByPlayerDimensions = this.options_.limitRenditionByPlayerDimensions;
- this.useDevicePixelRatio = this.options_.useDevicePixelRatio;
- }
- // alias for public method to set options
- setOptions(options = {}) {
- this.setOptions_(options);
- }
- /**
- * called when player.src gets called, handle a new source
- *
- * @param {Object} src the source object to handle
- */
- src(src, type) {
- // do nothing if the src is falsey
- if (!src) {
- return;
- }
- this.setOptions_();
- // add main playlist controller options
- this.options_.src = expandDataUri(this.source_.src);
- this.options_.tech = this.tech_;
- this.options_.externVhs = Vhs;
- this.options_.sourceType = simpleTypeFromSourceType(type);
- // Whenever we seek internally, we should update the tech
- this.options_.seekTo = (time) => {
- this.tech_.setCurrentTime(time);
- };
- this.playlistController_ = new PlaylistController(this.options_);
- const playbackWatcherOptions = merge(
- {
- liveRangeSafeTimeDelta: SAFE_TIME_DELTA
- },
- this.options_,
- {
- seekable: () => this.seekable(),
- media: () => this.playlistController_.media(),
- playlistController: this.playlistController_
- }
- );
- this.playbackWatcher_ = new PlaybackWatcher(playbackWatcherOptions);
- this.playlistController_.on('error', () => {
- const player = videojs.players[this.tech_.options_.playerId];
- let error = this.playlistController_.error;
- if (typeof error === 'object' && !error.code) {
- error.code = 3;
- } else if (typeof error === 'string') {
- error = {message: error, code: 3};
- }
- player.error(error);
- });
- const defaultSelector = this.options_.bufferBasedABR ?
- Vhs.movingAverageBandwidthSelector(0.55) : Vhs.STANDARD_PLAYLIST_SELECTOR;
- // `this` in selectPlaylist should be the VhsHandler for backwards
- // compatibility with < v2
- this.playlistController_.selectPlaylist = this.selectPlaylist ?
- this.selectPlaylist.bind(this) :
- defaultSelector.bind(this);
- this.playlistController_.selectInitialPlaylist =
- Vhs.INITIAL_PLAYLIST_SELECTOR.bind(this);
- // re-expose some internal objects for backwards compatibility with < v2
- this.playlists = this.playlistController_.mainPlaylistLoader_;
- this.mediaSource = this.playlistController_.mediaSource;
- // Proxy assignment of some properties to the main playlist
- // controller. Using a custom property for backwards compatibility
- // with < v2
- Object.defineProperties(this, {
- selectPlaylist: {
- get() {
- return this.playlistController_.selectPlaylist;
- },
- set(selectPlaylist) {
- this.playlistController_.selectPlaylist = selectPlaylist.bind(this);
- }
- },
- throughput: {
- get() {
- return this.playlistController_.mainSegmentLoader_.throughput.rate;
- },
- set(throughput) {
- this.playlistController_.mainSegmentLoader_.throughput.rate = throughput;
- // By setting `count` to 1 the throughput value becomes the starting value
- // for the cumulative average
- this.playlistController_.mainSegmentLoader_.throughput.count = 1;
- }
- },
- bandwidth: {
- get() {
- let playerBandwidthEst = this.playlistController_.mainSegmentLoader_.bandwidth;
- const networkInformation = window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection;
- const tenMbpsAsBitsPerSecond = 10e6;
- if (this.options_.useNetworkInformationApi && networkInformation) {
- // downlink returns Mbps
- // https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/downlink
- const networkInfoBandwidthEstBitsPerSec = networkInformation.downlink * 1000 * 1000;
- // downlink maxes out at 10 Mbps. In the event that both networkInformationApi and the player
- // estimate a bandwidth greater than 10 Mbps, use the larger of the two estimates to ensure that
- // high quality streams are not filtered out.
- if (networkInfoBandwidthEstBitsPerSec >= tenMbpsAsBitsPerSecond && playerBandwidthEst >= tenMbpsAsBitsPerSecond) {
- playerBandwidthEst = Math.max(playerBandwidthEst, networkInfoBandwidthEstBitsPerSec);
- } else {
- playerBandwidthEst = networkInfoBandwidthEstBitsPerSec;
- }
- }
- return playerBandwidthEst;
- },
- set(bandwidth) {
- this.playlistController_.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.playlistController_.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() {
- const invBandwidth = 1 / (this.bandwidth || 1);
- let invThroughput;
- if (this.throughput > 0) {
- invThroughput = 1 / this.throughput;
- } else {
- invThroughput = 0;
- }
- const systemBitrate = Math.floor(1 / (invBandwidth + invThroughput));
- return systemBitrate;
- },
- set() {
- videojs.log.error('The "systemBandwidth" property is read-only');
- }
- }
- });
- if (this.options_.bandwidth) {
- this.bandwidth = this.options_.bandwidth;
- }
- if (this.options_.throughput) {
- this.throughput = this.options_.throughput;
- }
- Object.defineProperties(this.stats, {
- bandwidth: {
- get: () => this.bandwidth || 0,
- enumerable: true
- },
- mediaRequests: {
- get: () => this.playlistController_.mediaRequests_() || 0,
- enumerable: true
- },
- mediaRequestsAborted: {
- get: () => this.playlistController_.mediaRequestsAborted_() || 0,
- enumerable: true
- },
- mediaRequestsTimedout: {
- get: () => this.playlistController_.mediaRequestsTimedout_() || 0,
- enumerable: true
- },
- mediaRequestsErrored: {
- get: () => this.playlistController_.mediaRequestsErrored_() || 0,
- enumerable: true
- },
- mediaTransferDuration: {
- get: () => this.playlistController_.mediaTransferDuration_() || 0,
- enumerable: true
- },
- mediaBytesTransferred: {
- get: () => this.playlistController_.mediaBytesTransferred_() || 0,
- enumerable: true
- },
- mediaSecondsLoaded: {
- get: () => this.playlistController_.mediaSecondsLoaded_() || 0,
- enumerable: true
- },
- mediaAppends: {
- get: () => this.playlistController_.mediaAppends_() || 0,
- enumerable: true
- },
- mainAppendsToLoadedData: {
- get: () => this.playlistController_.mainAppendsToLoadedData_() || 0,
- enumerable: true
- },
- audioAppendsToLoadedData: {
- get: () => this.playlistController_.audioAppendsToLoadedData_() || 0,
- enumerable: true
- },
- appendsToLoadedData: {
- get: () => this.playlistController_.appendsToLoadedData_() || 0,
- enumerable: true
- },
- timeToLoadedData: {
- get: () => this.playlistController_.timeToLoadedData_() || 0,
- enumerable: true
- },
- buffered: {
- get: () => timeRangesToArray(this.tech_.buffered()),
- enumerable: true
- },
- currentTime: {
- get: () => this.tech_.currentTime(),
- enumerable: true
- },
- currentSource: {
- get: () => this.tech_.currentSource_,
- enumerable: true
- },
- currentTech: {
- get: () => this.tech_.name_,
- enumerable: true
- },
- duration: {
- get: () => this.tech_.duration(),
- enumerable: true
- },
- main: {
- get: () => this.playlists.main,
- enumerable: true
- },
- playerDimensions: {
- get: () => this.tech_.currentDimensions(),
- enumerable: true
- },
- seekable: {
- get: () => timeRangesToArray(this.tech_.seekable()),
- enumerable: true
- },
- timestamp: {
- get: () => Date.now(),
- enumerable: true
- },
- videoPlaybackQuality: {
- get: () => this.tech_.getVideoPlaybackQuality(),
- enumerable: true
- }
- });
- this.tech_.one(
- 'canplay',
- this.playlistController_.setupFirstPlay.bind(this.playlistController_)
- );
- this.tech_.on('bandwidthupdate', () => {
- if (this.options_.useBandwidthFromLocalStorage) {
- updateVhsLocalStorage({
- bandwidth: this.bandwidth,
- throughput: Math.round(this.throughput)
- });
- }
- });
- this.playlistController_.on('selectedinitialmedia', () => {
- // Add the manual rendition mix-in to VhsHandler
- renditionSelectionMixin(this);
- });
- this.playlistController_.sourceUpdater_.on('createdsourcebuffers', () => {
- this.setupEme_();
- });
- // the bandwidth of the primary segment loader is our best
- // estimate of overall bandwidth
- this.on(this.playlistController_, '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.playlistController_, 'firstplay', function() {
- this.ignoreNextSeekingEvent_ = true;
- });
- 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.mediaSourceUrl_ = window.URL.createObjectURL(this.playlistController_.mediaSource);
- this.tech_.src(this.mediaSourceUrl_);
- }
- createKeySessions_() {
- const audioPlaylistLoader =
- this.playlistController_.mediaTypes_.AUDIO.activePlaylistLoader;
- this.logger_('waiting for EME key session creation');
- waitForKeySessionCreation({
- player: this.player_,
- sourceKeySystems: this.source_.keySystems,
- audioMedia: audioPlaylistLoader && audioPlaylistLoader.media(),
- mainPlaylists: this.playlists.main.playlists
- }).then(() => {
- this.logger_('created EME key session');
- this.playlistController_.sourceUpdater_.initializedEme();
- }).catch((err) => {
- this.logger_('error while creating EME key session', err);
- this.player_.error({
- message: 'Failed to initialize media keys for EME',
- code: 3
- });
- });
- }
- handleWaitingForKey_() {
- // If waitingforkey is fired, it's possible that the data that's necessary to retrieve
- // the key is in the manifest. While this should've happened on initial source load, it
- // may happen again in live streams where the keys change, and the manifest info
- // reflects the update.
- //
- // Because videojs-contrib-eme compares the PSSH data we send to that of PSSH data it's
- // already requested keys for, we don't have to worry about this generating extraneous
- // requests.
- this.logger_('waitingforkey fired, attempting to create any new key sessions');
- this.createKeySessions_();
- }
- /**
- * If necessary and EME is available, sets up EME options and waits for key session
- * creation.
- *
- * This function also updates the source updater so taht it can be used, as for some
- * browsers, EME must be configured before content is appended (if appending unencrypted
- * content before encrypted content).
- */
- setupEme_() {
- const audioPlaylistLoader =
- this.playlistController_.mediaTypes_.AUDIO.activePlaylistLoader;
- const didSetupEmeOptions = setupEmeOptions({
- player: this.player_,
- sourceKeySystems: this.source_.keySystems,
- media: this.playlists.media(),
- audioMedia: audioPlaylistLoader && audioPlaylistLoader.media()
- });
- this.player_.tech_.on('keystatuschange', (e) => {
- this.playlistController_.updatePlaylistByKeyStatus(e.keyId, e.status);
- });
- this.handleWaitingForKey_ = this.handleWaitingForKey_.bind(this);
- this.player_.tech_.on('waitingforkey', this.handleWaitingForKey_);
- if (!didSetupEmeOptions) {
- // If EME options were not set up, we've done all we could to initialize EME.
- this.playlistController_.sourceUpdater_.initializedEme();
- return;
- }
- this.createKeySessions_();
- }
- /**
- * Initializes the quality levels and sets listeners to update them.
- *
- * @method setupQualityLevels_
- * @private
- */
- setupQualityLevels_() {
- const player = videojs.players[this.tech_.options_.playerId];
- // if there isn't a player or there isn't a qualityLevels plugin
- // or qualityLevels_ listeners have already been setup, do nothing.
- if (!player || !player.qualityLevels || this.qualityLevels_) {
- return;
- }
- this.qualityLevels_ = player.qualityLevels();
- this.playlistController_.on('selectedinitialmedia', () => {
- handleVhsLoadedMetadata(this.qualityLevels_, this);
- });
- this.playlists.on('mediachange', () => {
- handleVhsMediaChange(this.qualityLevels_, this.playlists);
- });
- }
- /**
- * return the version
- */
- static version() {
- return {
- '@videojs/http-streaming': vhsVersion,
- 'mux.js': muxVersion,
- 'mpd-parser': mpdVersion,
- 'm3u8-parser': m3u8Version,
- 'aes-decrypter': aesVersion
- };
- }
- /**
- * return the version
- */
- version() {
- return this.constructor.version();
- }
- canChangeType() {
- return SourceUpdater.canChangeType();
- }
- /**
- * Begin playing the video.
- */
- play() {
- this.playlistController_.play();
- }
- /**
- * a wrapper around the function in PlaylistController
- */
- setCurrentTime(currentTime) {
- this.playlistController_.setCurrentTime(currentTime);
- }
- /**
- * a wrapper around the function in PlaylistController
- */
- duration() {
- return this.playlistController_.duration();
- }
- /**
- * a wrapper around the function in PlaylistController
- */
- seekable() {
- return this.playlistController_.seekable();
- }
- /**
- * Abort all outstanding work and cleanup.
- */
- dispose() {
- if (this.playbackWatcher_) {
- this.playbackWatcher_.dispose();
- }
- if (this.playlistController_) {
- this.playlistController_.dispose();
- }
- if (this.qualityLevels_) {
- this.qualityLevels_.dispose();
- }
- if (this.tech_ && this.tech_.vhs) {
- delete this.tech_.vhs;
- }
- if (this.mediaSourceUrl_ && window.URL.revokeObjectURL) {
- window.URL.revokeObjectURL(this.mediaSourceUrl_);
- this.mediaSourceUrl_ = null;
- }
- if (this.tech_) {
- this.tech_.off('waitingforkey', this.handleWaitingForKey_);
- }
- super.dispose();
- }
- convertToProgramTime(time, callback) {
- return getProgramTime({
- playlist: this.playlistController_.media(),
- time,
- callback
- });
- }
- // the player must be playing before calling this
- seekToProgramTime(programTime, callback, pauseAfterSeek = true, retryCount = 2) {
- return seekToProgramTime({
- programTime,
- playlist: this.playlistController_.media(),
- retryCount,
- pauseAfterSeek,
- seekTo: this.options_.seekTo,
- tech: this.options_.tech,
- callback
- });
- }
- /**
- * Adds the onRequest, onResponse, offRequest and offResponse functions
- * to the VhsHandler xhr Object.
- */
- setupXhrHooks_() {
- /**
- * A player function for setting an onRequest hook
- *
- * @param {function} callback for request modifiction
- */
- this.xhr.onRequest = (callback) => {
- addOnRequestHook(this.xhr, callback);
- };
- /**
- * A player function for setting an onResponse hook
- *
- * @param {callback} callback for response data retrieval
- */
- this.xhr.onResponse = (callback) => {
- addOnResponseHook(this.xhr, callback);
- };
- /**
- * Deletes a player onRequest callback if it exists
- *
- * @param {function} callback to delete from the player set
- */
- this.xhr.offRequest = (callback) => {
- removeOnRequestHook(this.xhr, callback);
- };
- /**
- * Deletes a player onResponse callback if it exists
- *
- * @param {function} callback to delete from the player set
- */
- this.xhr.offResponse = (callback) => {
- removeOnResponseHook(this.xhr, callback);
- };
- // Trigger an event on the player to notify the user that vhs is ready to set xhr hooks.
- // This allows hooks to be set before the source is set to vhs when handleSource is called.
- this.player_.trigger('xhr-hooks-ready');
- }
- }
- /**
- * 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 VhsSourceHandler = {
- name: 'videojs-http-streaming',
- VERSION: vhsVersion,
- canHandleSource(srcObj, options = {}) {
- const localOptions = merge(videojs.options, options);
- return VhsSourceHandler.canPlayType(srcObj.type, localOptions);
- },
- handleSource(source, tech, options = {}) {
- const localOptions = merge(videojs.options, options);
- tech.vhs = new VhsHandler(source, tech, localOptions);
- tech.vhs.xhr = xhrFactory();
- tech.vhs.setupXhrHooks_();
- tech.vhs.src(source.src, source.type);
- return tech.vhs;
- },
- canPlayType(type, options) {
- const simpleType = simpleTypeFromSourceType(type);
- if (!simpleType) {
- return '';
- }
- const overrideNative = VhsSourceHandler.getOverrideNative(options);
- const supportsTypeNatively = Vhs.supportsTypeNatively(simpleType);
- const canUseMsePlayback = !supportsTypeNatively || overrideNative;
- return canUseMsePlayback ? 'maybe' : '';
- },
- getOverrideNative(options = {}) {
- const { vhs = {} } = options;
- const defaultOverrideNative = !(videojs.browser.IS_ANY_SAFARI || videojs.browser.IS_IOS);
- const { overrideNative = defaultOverrideNative } = vhs;
- return overrideNative;
- }
- };
- /**
- * Check to see if the native MediaSource object exists and supports
- * an MP4 container with both H.264 video and AAC-LC audio.
- *
- * @return {boolean} if native media sources are supported
- */
- const supportsNativeMediaSources = () => {
- return browserSupportsCodec('avc1.4d400d,mp4a.40.2');
- };
- // register source handlers with the appropriate techs
- if (supportsNativeMediaSources()) {
- videojs.getTech('Html5').registerSourceHandler(VhsSourceHandler, 0);
- }
- videojs.VhsHandler = VhsHandler;
- videojs.VhsSourceHandler = VhsSourceHandler;
- videojs.Vhs = Vhs;
- if (!videojs.use) {
- videojs.registerComponent('Vhs', Vhs);
- }
- videojs.options.vhs = videojs.options.vhs || {};
- if (!videojs.getPlugin || !videojs.getPlugin('reloadSourceOnError')) {
- videojs.registerPlugin('reloadSourceOnError', reloadSourceOnError);
- }
- export {
- Vhs,
- VhsHandler,
- VhsSourceHandler,
- emeKeySystems,
- simpleTypeFromSourceType,
- expandDataUri,
- setupEmeOptions,
- getAllPsshKeySystemsOptions
- };
|