UNPKG

cloudinary-video-player

Version:

Cloudinary Video Player

1,418 lines (1,314 loc) 1.32 MB
/*! * Cloudinary Video Player v3.0.1 * Built on 2025-06-09T18:04:09.938Z * https://github.com/cloudinary/cloudinary-video-player */ (self["cloudinaryVideoPlayerChunkLoading"] = self["cloudinaryVideoPlayerChunkLoading"] || []).push([["adaptive-streaming"],{ /***/ "./plugins/adaptive-streaming/abr-strategies.js": /*!******************************************************!*\ !*** ./plugins/adaptive-streaming/abr-strategies.js ***! \******************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ ADAPTIVE_STREAMING_STRATEGY: () => (/* binding */ ADAPTIVE_STREAMING_STRATEGY), /* harmony export */ abrStrategies: () => (/* binding */ abrStrategies), /* harmony export */ hdrSupported: () => (/* binding */ hdrSupported) /* harmony export */ }); const abrStrategies = { fastStart: { capLevelToPlayerSize: true, ignoreDevicePixelRatio: true, maxDevicePixelRatio: 2, abrEwmaDefaultEstimate: 4194304, abrEwmaDefaultEstimateMax: 4194304, enableWorker: false, startLevel: 0 }, balanced: { capLevelToPlayerSize: true, ignoreDevicePixelRatio: true, maxDevicePixelRatio: 2, abrEwmaDefaultEstimate: 4194304, abrEwmaDefaultEstimateMax: 4194304, enableWorker: false }, highQuality: { capLevelToPlayerSize: true, ignoreDevicePixelRatio: false, maxDevicePixelRatio: 2, abrEwmaDefaultEstimate: 4194304, abrEwmaDefaultEstimateMax: 4194304, enableWorker: false } }; const ADAPTIVE_STREAMING_STRATEGY = Object.keys(abrStrategies); const hdrSupported = window.matchMedia && window.matchMedia('(dynamic-range: high)').matches; /***/ }), /***/ "./plugins/adaptive-streaming/adaptive-streaming.js": /*!**********************************************************!*\ !*** ./plugins/adaptive-streaming/adaptive-streaming.js ***! \**********************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (/* binding */ adaptiveStreamingPlugin) /* harmony export */ }); /* harmony import */ var hls_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! hls.js */ "../node_modules/hls.js/dist/hls.mjs"); /* harmony import */ var videojs_contrib_quality_levels__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! videojs-contrib-quality-levels */ "../node_modules/videojs-contrib-quality-levels/dist/videojs-contrib-quality-levels.js"); /* harmony import */ var videojs_contrib_quality_levels__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(videojs_contrib_quality_levels__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var videojs_contrib_quality_menu__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! videojs-contrib-quality-menu */ "../node_modules/videojs-contrib-quality-menu/dist/videojs-contrib-quality-menu.es.js"); /* harmony import */ var _videojs_contrib_hlsjs__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./videojs-contrib-hlsjs */ "./plugins/adaptive-streaming/videojs-contrib-hlsjs.js"); /* harmony import */ var _quality_levels__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./quality-levels */ "./plugins/adaptive-streaming/quality-levels.js"); /* harmony import */ var _abr_strategies__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./abr-strategies */ "./plugins/adaptive-streaming/abr-strategies.js"); async function adaptiveStreamingPlugin(player, options) { const config = { ..._abr_strategies__WEBPACK_IMPORTED_MODULE_5__.abrStrategies[options.strategy], videoPreference: _abr_strategies__WEBPACK_IMPORTED_MODULE_5__.hdrSupported ? { preferHDR: true } : undefined }; player.tech_.options_.hlsjsConfig = config; player.on('loadstart', () => (0,_quality_levels__WEBPACK_IMPORTED_MODULE_4__.qualityLevels)(player, options).init()); player.qualityMenu(); player.adaptiveStreamingLoaded = true; } /***/ }), /***/ "./plugins/adaptive-streaming/quality-levels.js": /*!******************************************************!*\ !*** ./plugins/adaptive-streaming/quality-levels.js ***! \******************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ qualityLevels: () => (/* binding */ qualityLevels) /* harmony export */ }); /* harmony import */ var video_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! video.js */ "../node_modules/video.js/dist/alt/video.core-exposed.js"); /* harmony import */ var video_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(video_js__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var hls_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! hls.js */ "../node_modules/hls.js/dist/hls.mjs"); const qualityLevels = (player, options) => { const levelToRenditionHls = level => { let levelUrl = Array.isArray(level.url) && level.url.length > 1 ? level.url[level.urlId] : level.url; let rendition = { id: levelUrl, width: level.width, height: level.height, bandwidth: level.bitrate, // bitrate => bandwidth frameRate: 0, enabled: enableRendition => { var tech = player.tech({ IWillNotUseThisInPlugins: true }); if (typeof tech.sourceHandler_ != 'undefined' && typeof tech.sourceHandler_.hls != 'undefined' && tech.sourceHandler_.hls != null) { const hls = tech.sourceHandler_.hls; const levelIndex = hls.levels.findIndex(l => (Array.isArray(l.url) && l.url.length > 1 ? l.url[l.urlId] : l.url) === levelUrl); if (levelIndex >= 0 && enableRendition) { hls.currentLevel = levelIndex; } } return enableRendition; } }; return rendition; }; const levelToRenditionDash = level => ({ id: level.id, width: level.width, height: level.height, bandwidth: level.bandwidth, enabled: enableRendition => { const dash = player.dash; if (dash && dash.mediaPlayer) { if (enableRendition) { dash.mediaPlayer.updateSettings({ streaming: { abr: { autoSwitchBitrate: { video: false, audio: false } } } }); // Find the correct quality index by resolution const targetQualityIndex = findDashQualityIndex(level.width, level.height); if (targetQualityIndex >= 0) { dash.mediaPlayer.setQualityFor('video', targetQualityIndex); // Set audio quality if mapping exists if (dash.audioMapper && dash.audioMapper[targetQualityIndex] !== undefined) { dash.mediaPlayer.setQualityFor('audio', dash.audioMapper[targetQualityIndex]); } } } } return enableRendition; } }); const getRenditionsDash = () => { var dash = player.dash; if (typeof dash != 'undefined' && dash != null && typeof dash.mediaPlayer != 'undefined' && dash.mediaPlayer != null) { var streamInfo = dash.mediaPlayer.getActiveStream().getStreamInfo(); var dashAdapter = dash.mediaPlayer.getDashAdapter(); if (dashAdapter && streamInfo) { const periodIdx = streamInfo.index; var adaptation = dashAdapter.getAdaptationForType(periodIdx, 'video', streamInfo); } return adaptation.Representation_asArray; } return []; }; // Helper function to find DASH quality index by resolution const findDashQualityIndex = (targetWidth, targetHeight) => { var dash = player.dash; if (dash && dash.mediaPlayer) { const availableQualities = dash.mediaPlayer.getBitrateInfoListFor('video'); const targetQuality = availableQualities.find(q => q.width === targetWidth && q.height === targetHeight); return targetQuality ? targetQuality.qualityIndex : -1; } return -1; }; // Clean up quality levels and reset state for new source const cleanupQualityLevels = () => { let qualityLevels = player.qualityLevels; if (typeof qualityLevels === 'function') { qualityLevels = player.qualityLevels(); // Clear all existing quality levels qualityLevels.dispose(); debugLog('Quality levels cleaned up for new source'); } // Clean up audio tracks from previous source const audioTrackList = player.audioTracks(); if (audioTrackList && audioTrackList.length > 0) { // Remove all existing audio tracks for (let i = audioTrackList.length - 1; i >= 0; i--) { audioTrackList.removeTrack(audioTrackList[i]); } debugLog('Audio tracks cleaned up for new source'); } // Clean up DASH-specific state const dash = player.dash; if (dash && dash.audioMapper) { delete dash.audioMapper; debugLog('DASH audio mapper cleaned up for new source'); } // Reset previous resolution for qualitychanged events previousResolution = null; }; // Update the QualityLevels list of renditions const populateLevels = (levels, abrType) => { // Clean up existing quality levels before adding new ones cleanupQualityLevels(); let qualityLevels = player.qualityLevels; if (typeof qualityLevels === 'function') { qualityLevels = player.qualityLevels(); debugLog('QualityLevels', qualityLevels); switch (abrType) { case 'hls': for (let l = 0; l < levels.length; l++) { let level = levels[l]; let rendition = levelToRenditionHls(level); qualityLevels.addQualityLevel(rendition); } break; case 'dash': { // Set up audio mapping for DASH const dash = player.dash; if (!dash) break; const videoRates = levels; const audioRates = dash.mediaPlayer.getBitrateInfoListFor('audio') || []; const normalizeFactor = videoRates.length > 0 ? videoRates[videoRates.length - 1].bandwidth : 1; dash.audioMapper = videoRates.map(rate => Math.round(rate.bandwidth / normalizeFactor * (audioRates.length - 1))); for (let l = 0; l < levels.length; l++) { let level = levels[l]; let rendition = levelToRenditionDash(level); qualityLevels.addQualityLevel(rendition); } break; } default: return; } } else { console.warn('QualityLevels not supported'); } }; let previousResolution = null; // for qualitychanged event data // Update the selected rendition const populateQualityLevelsChange = currentLevel => { let qualityLevels = player.qualityLevels; if (typeof qualityLevels === 'function') { qualityLevels = player.qualityLevels(); if (qualityLevels.length == 0) { console.warn('ERROR - no quality levels found! Patching populate levels first'); var tech = player.tech({ IWillNotUseThisInPlugins: true }); if (typeof tech.sourceHandler_ != 'undefined' && typeof tech.sourceHandler_.hls != 'undefined' && tech.sourceHandler_.hls != null) { const hls = tech.sourceHandler_.hls; populateLevels(hls.levels, 'hls'); } } qualityLevels.selectedIndex_ = currentLevel; qualityLevels.trigger({ type: 'change', selectedIndex: currentLevel }); } }; // Custom 'qualitychanged' event const populateQualityChangedEvent = currentLevel => { if (currentLevel < 0) { return; } let qualityLevels = player.qualityLevels; let level = null; // Using videojs-contrib-quality-levels if (typeof qualityLevels === 'function') { qualityLevels = player.qualityLevels(); level = qualityLevels.levels_[currentLevel]; debugLog('Custom qualitychanged', 'using videojs-contrib-quality-levels'); } else { // hls.js directly var tech = player.tech({ IWillNotUseThisInPlugins: true }); if (typeof tech.sourceHandler_ != 'undefined' && typeof tech.sourceHandler_.hls != 'undefined' && tech.sourceHandler_.hls != null) { const hls = tech.sourceHandler_.hls; level = hls.levels[currentLevel]; debugLog('Custom qualitychanged', 'using hls.js directly'); if (currentLevel !== hls.currentLevel) { debugLog('ERROR - new level differs from hls.js'); } } else { // dash.js directly var dash = player.dash; if (typeof dash != 'undefined' && dash != null && typeof dash.mediaPlayer != 'undefined' && dash.mediaPlayer != null) { let renditions = getRenditionsDash(); level = renditions[currentLevel]; debugLog('Custom qualitychanged', 'using dash.js directly'); } } } // Add null check for level if (!level) { debugLog('Warning: Level is undefined in populateQualityChangedEvent', { currentLevel }); return; } let currentRes = { width: level.width, height: level.height }; if (previousResolution !== currentRes) { let data = { from: previousResolution, to: currentRes }; // Trigger custom 'qualitychanged' event on videojs player.trigger({ type: 'qualitychanged', eventData: data }); } previousResolution = currentRes; // Add detailed logging of current rendition debugLog('Current Rendition', { index: currentLevel, resolution: `${level.width}x${level.height}`, bitrate: `${Math.round(level.bitrate / 1000)} kbps`, url: Array.isArray(level.url) ? level.url[level.urlId] : level.url, details: level }); }; const debugLog = (label, data) => { if (options.debug) { console.log(`%c ${label}`, 'background: #3498db; color: white; padding: 2px 4px; border-radius: 2px;', data); } }; const logAudioTrackInfo = () => { var tech = player.tech({ IWillNotUseThisInPlugins: true }); if (typeof tech.sourceHandler_ != 'undefined' && typeof tech.sourceHandler_.hls != 'undefined' && tech.sourceHandler_.hls != null) { const hls = tech.sourceHandler_.hls; const audioTrackId = hls.audioTrack; const len = hls.audioTracks.length; for (let i = 0; i < len; i++) { if (audioTrackId === i) { debugLog(`audio track [${i}] ${hls.audioTracks[i].name} - enabled`, hls.audioTracks[i]); } else { debugLog(`audio track [${i}] ${hls.audioTracks[i].name} - disabled`, hls.audioTracks[i]); } } } }; // Audio track handling const addAudioTrackVideojs = track => { var vjsTrack = new (video_js__WEBPACK_IMPORTED_MODULE_0___default().AudioTrack)({ id: `${track.type}-id_${track.id}-groupId_${track.groupId}-${track.name}`, kind: 'translation', label: track.name, language: track.lang, enabled: track.enabled, default: track.default }); // Add the track to the player's audio track list. player.audioTracks().addTrack(vjsTrack); }; const initAudioTrackInfo = () => { var tech = player.tech({ IWillNotUseThisInPlugins: true }); if (typeof tech.sourceHandler_ != 'undefined' && typeof tech.sourceHandler_.hls != 'undefined' && tech.sourceHandler_.hls != null) { const hls = tech.sourceHandler_.hls; const len = hls.audioTracks.length; for (let i = 0; i < len; i++) { addAudioTrackVideojs(hls.audioTracks[i]); } } // Listen to the "change" event. var audioTrackList = player.audioTracks(); audioTrackList.addEventListener('change', function () { var tech = player.tech({ IWillNotUseThisInPlugins: true }); if (typeof tech.sourceHandler_ != 'undefined' && typeof tech.sourceHandler_.hls != 'undefined' && tech.sourceHandler_.hls != null) { const hls = tech.sourceHandler_.hls; for (var i = 0; i < audioTrackList.length; i++) { var track = audioTrackList[i]; if (track.enabled) { hls.audioTrack = i; return; } } } }); }; // Map hls.js events to QualityLevels const initQualityLevels = () => { var tech = player.tech({ IWillNotUseThisInPlugins: true }); if (typeof tech == 'undefined') { console.warn('ERROR - tech not found!'); } // HLS if (typeof tech.sourceHandler_ != 'undefined' && typeof tech.sourceHandler_.hls != 'undefined' && tech.sourceHandler_.hls != null) { const hls = tech.sourceHandler_.hls; hls.on(hls_js__WEBPACK_IMPORTED_MODULE_1__["default"].Events.MANIFEST_LOADED, (eventName, data) => { debugLog(`HLS event: ${eventName}`, data); populateLevels(hls.levels, 'hls'); }); hls.on(hls_js__WEBPACK_IMPORTED_MODULE_1__["default"].Events.LEVEL_SWITCHED, (eventName, data) => { debugLog(`HLS event: ${eventName}`, data); populateQualityLevelsChange(data.level); populateQualityChangedEvent(data.level); }); hls.on(hls_js__WEBPACK_IMPORTED_MODULE_1__["default"].Events.AUDIO_TRACKS_UPDATED, (eventName, data) => { debugLog(`HLS event: ${eventName}`, data); initAudioTrackInfo(); }); hls.on(hls_js__WEBPACK_IMPORTED_MODULE_1__["default"].Events.AUDIO_TRACK_SWITCHED, (eventName, data) => { debugLog(`HLS event: ${eventName}`, data); logAudioTrackInfo(); }); hls.on(hls_js__WEBPACK_IMPORTED_MODULE_1__["default"].Events.ERROR, (eventName, data) => { debugLog(`HLS event: ${eventName}`, data); if (data.fatal) { player.trigger({ type: 'error', eventData: data }); } }); } else { // DASH var dash = player.dash; if (typeof dash != 'undefined' && dash != null && typeof dash.mediaPlayer != 'undefined' && dash.mediaPlayer != null) { let renditions = getRenditionsDash(); populateLevels(renditions, 'dash'); dash.mediaPlayer.on('qualityChangeRendered', evt => { const currentVideoQuality = dash.mediaPlayer.getQualityFor('video'); const availableQualities = dash.mediaPlayer.getBitrateInfoListFor('video'); const currentQualityInfo = availableQualities[currentVideoQuality]; const renditionIndex = findDashQualityIndex(currentQualityInfo.width, currentQualityInfo.height); if (renditionIndex >= 0) { debugLog(`DASH event: ${evt.type}`, evt, renditions[renditionIndex]); populateQualityLevelsChange(renditionIndex); populateQualityChangedEvent(renditionIndex); } else { console.warn('Could not find matching rendition for DASH quality change'); } }); } } }; return { init: initQualityLevels }; }; /***/ }), /***/ "./plugins/adaptive-streaming/videojs-contrib-hlsjs.js": /*!*************************************************************!*\ !*** ./plugins/adaptive-streaming/videojs-contrib-hlsjs.js ***! \*************************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony import */ var hls_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! hls.js */ "../node_modules/hls.js/dist/hls.mjs"); /** * hls.js source handler * Source: https://github.com/Peer5/videojs-contrib-hls.js * @param source * @param tech * @constructor */ function Html5HlsJS(source, tech) { var options = tech.options_; var el = tech.el(); var duration = null; var hls = this.hls = new hls_js__WEBPACK_IMPORTED_MODULE_0__["default"](options.hlsjsConfig); /** * creates an error handler function * @returns {Function} */ function errorHandlerFactory() { var _recoverDecodingErrorDate = null; var _recoverAudioCodecErrorDate = null; return function () { var now = Date.now(); if (!_recoverDecodingErrorDate || now - _recoverDecodingErrorDate > 2000) { _recoverDecodingErrorDate = now; hls.recoverMediaError(); } else if (!_recoverAudioCodecErrorDate || now - _recoverAudioCodecErrorDate > 2000) { _recoverAudioCodecErrorDate = now; hls.swapAudioCodec(); hls.recoverMediaError(); } else { console.error('Error loading media: File could not be played'); } }; } // create separate error handlers for hlsjs and the video tag var hlsjsErrorHandler = errorHandlerFactory(); var videoTagErrorHandler = errorHandlerFactory(); // listen to error events coming from the video tag el.addEventListener('error', function (e) { var mediaError = e.currentTarget.error; if (mediaError.code === mediaError.MEDIA_ERR_DECODE) { videoTagErrorHandler(); } else { console.error('Error loading media: File could not be played'); } }); /** * Destroys the Hls instance */ this.dispose = function () { hls.destroy(); }; /** * returns the duration of the stream, or Infinity if live video * @returns {Infinity|number} */ this.duration = function () { return duration || el.duration || 0; }; // update live status on level load hls.on(hls_js__WEBPACK_IMPORTED_MODULE_0__["default"].Events.LEVEL_LOADED, function (event, data) { duration = data.details.live ? Infinity : data.details.totalduration; }); // try to recover on fatal errors hls.on(hls_js__WEBPACK_IMPORTED_MODULE_0__["default"].Events.ERROR, function (event, data) { if (data.fatal) { switch (data.type) { case hls_js__WEBPACK_IMPORTED_MODULE_0__["default"].ErrorTypes.NETWORK_ERROR: hls.startLoad(); break; case hls_js__WEBPACK_IMPORTED_MODULE_0__["default"].ErrorTypes.MEDIA_ERROR: hlsjsErrorHandler(); break; default: console.error('Error loading media: File could not be played'); break; } } }); Object.keys(hls_js__WEBPACK_IMPORTED_MODULE_0__["default"].Events).forEach(function (key) { var eventName = hls_js__WEBPACK_IMPORTED_MODULE_0__["default"].Events[key]; hls.on(eventName, function (event, data) { tech.trigger(eventName, data); }); }); // Intercept native TextTrack calls and route to video.js directly only // if native text tracks are not supported on this browser. if (!tech.featuresNativeTextTracks) { Object.defineProperty(el, 'textTracks', { value: tech.textTracks, writable: false }); el.addTextTrack = function () { return tech.addTextTrack.apply(tech, arguments); }; } // attach hlsjs to videotag hls.attachMedia(el); hls.loadSource(source.src); } var hlsTypeRE = /^application\/(x-mpegURL|vnd\.apple\.mpegURL)$/i; var hlsExtRE = /\.m3u8/i; var HlsSourceHandler = { canHandleSource: function (source) { if (source.skipContribHlsJs) { return ''; } else if (hlsTypeRE.test(source.type)) { return 'probably'; } else if (hlsExtRE.test(source.src)) { return 'maybe'; } else { return ''; } }, handleSource: function (source, tech) { return new Html5HlsJS(source, tech); }, canPlayType: function (type) { if (hlsTypeRE.test(type)) { return 'probably'; } return ''; } }; if (hls_js__WEBPACK_IMPORTED_MODULE_0__["default"].isSupported()) { var videojs = window.videojs; // support es6 style import videojs = videojs && videojs.default || videojs; if (videojs) { var html5Tech = videojs.getTech && videojs.getTech('Html5'); // videojs6 (partially on videojs5 too) html5Tech = html5Tech || videojs.getComponent && videojs.getComponent('Html5'); // videojs5 if (html5Tech) { html5Tech.registerSourceHandler(HlsSourceHandler, 0); } } else { console.warn('videojs-contrib-hls.js: Couldn\'t find find window.videojs nor require(\'video.js\')'); } } /***/ }), /***/ "../node_modules/videojs-contrib-quality-levels/dist/videojs-contrib-quality-levels.js": /*!*********************************************************************************************!*\ !*** ../node_modules/videojs-contrib-quality-levels/dist/videojs-contrib-quality-levels.js ***! \*********************************************************************************************/ /***/ (function(module, __unused_webpack_exports, __webpack_require__) { /*! @name videojs-contrib-quality-levels @version 4.1.0 @license Apache-2.0 */ (function (global, factory) { true ? module.exports = factory(__webpack_require__(/*! video.js */ "../node_modules/video.js/dist/alt/video.core-exposed.js")) : 0; })(this, (function (videojs) { 'use strict'; function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var videojs__default = /*#__PURE__*/_interopDefaultLegacy(videojs); /** * A single QualityLevel. * * interface QualityLevel { * readonly attribute DOMString id; * attribute DOMString label; * readonly attribute long width; * readonly attribute long height; * readonly attribute long bitrate; * attribute boolean enabled; * }; * * @class QualityLevel */ class QualityLevel { /** * Creates a QualityLevel * * @param {Representation|Object} representation The representation of the quality level * @param {string} representation.id Unique id of the QualityLevel * @param {number=} representation.width Resolution width of the QualityLevel * @param {number=} representation.height Resolution height of the QualityLevel * @param {number} representation.bandwidth Bitrate of the QualityLevel * @param {number=} representation.frameRate Frame-rate of the QualityLevel * @param {Function} representation.enabled Callback to enable/disable QualityLevel */ constructor(representation) { let level = this; // eslint-disable-line level.id = representation.id; level.label = level.id; level.width = representation.width; level.height = representation.height; level.bitrate = representation.bandwidth; level.frameRate = representation.frameRate; level.enabled_ = representation.enabled; Object.defineProperty(level, 'enabled', { /** * Get whether the QualityLevel is enabled. * * @return {boolean} True if the QualityLevel is enabled. */ get() { return level.enabled_(); }, /** * Enable or disable the QualityLevel. * * @param {boolean} enable true to enable QualityLevel, false to disable. */ set(enable) { level.enabled_(enable); } }); return level; } } /** * A list of QualityLevels. * * interface QualityLevelList : EventTarget { * getter QualityLevel (unsigned long index); * readonly attribute unsigned long length; * readonly attribute long selectedIndex; * * void addQualityLevel(QualityLevel qualityLevel) * void removeQualityLevel(QualityLevel remove) * QualityLevel? getQualityLevelById(DOMString id); * * attribute EventHandler onchange; * attribute EventHandler onaddqualitylevel; * attribute EventHandler onremovequalitylevel; * }; * * @extends videojs.EventTarget * @class QualityLevelList */ class QualityLevelList extends videojs__default["default"].EventTarget { /** * Creates a QualityLevelList. */ constructor() { super(); let list = this; // eslint-disable-line list.levels_ = []; list.selectedIndex_ = -1; /** * Get the index of the currently selected QualityLevel. * * @returns {number} The index of the selected QualityLevel. -1 if none selected. * @readonly */ Object.defineProperty(list, 'selectedIndex', { get() { return list.selectedIndex_; } }); /** * Get the length of the list of QualityLevels. * * @returns {number} The length of the list. * @readonly */ Object.defineProperty(list, 'length', { get() { return list.levels_.length; } }); list[Symbol.iterator] = () => list.levels_.values(); return list; } /** * Adds a quality level to the list. * * @param {Representation|Object} representation The representation of the quality level * @param {string} representation.id Unique id of the QualityLevel * @param {number=} representation.width Resolution width of the QualityLevel * @param {number=} representation.height Resolution height of the QualityLevel * @param {number} representation.bandwidth Bitrate of the QualityLevel * @param {number=} representation.frameRate Frame-rate of the QualityLevel * @param {Function} representation.enabled Callback to enable/disable QualityLevel * @return {QualityLevel} the QualityLevel added to the list * @method addQualityLevel */ addQualityLevel(representation) { let qualityLevel = this.getQualityLevelById(representation.id); // Do not add duplicate quality levels if (qualityLevel) { return qualityLevel; } const index = this.levels_.length; qualityLevel = new QualityLevel(representation); if (!('' + index in this)) { Object.defineProperty(this, index, { get() { return this.levels_[index]; } }); } this.levels_.push(qualityLevel); this.trigger({ qualityLevel, type: 'addqualitylevel' }); return qualityLevel; } /** * Removes a quality level from the list. * * @param {QualityLevel} qualityLevel The QualityLevel to remove from the list. * @return {QualityLevel|null} the QualityLevel removed or null if nothing removed * @method removeQualityLevel */ removeQualityLevel(qualityLevel) { let removed = null; for (let i = 0, l = this.length; i < l; i++) { if (this[i] === qualityLevel) { removed = this.levels_.splice(i, 1)[0]; if (this.selectedIndex_ === i) { this.selectedIndex_ = -1; } else if (this.selectedIndex_ > i) { this.selectedIndex_--; } break; } } if (removed) { this.trigger({ qualityLevel, type: 'removequalitylevel' }); } return removed; } /** * Searches for a QualityLevel with the given id. * * @param {string} id The id of the QualityLevel to find. * @return {QualityLevel|null} The QualityLevel with id, or null if not found. * @method getQualityLevelById */ getQualityLevelById(id) { for (let i = 0, l = this.length; i < l; i++) { const level = this[i]; if (level.id === id) { return level; } } return null; } /** * Resets the list of QualityLevels to empty * * @method dispose */ dispose() { this.selectedIndex_ = -1; this.levels_.length = 0; } } /** * change - The selected QualityLevel has changed. * addqualitylevel - A QualityLevel has been added to the QualityLevelList. * removequalitylevel - A QualityLevel has been removed from the QualityLevelList. */ QualityLevelList.prototype.allowedEvents_ = { change: 'change', addqualitylevel: 'addqualitylevel', removequalitylevel: 'removequalitylevel' }; // emulate attribute EventHandler support to allow for feature detection for (const event in QualityLevelList.prototype.allowedEvents_) { QualityLevelList.prototype['on' + event] = null; } var version = "4.1.0"; /** * Initialization function for the qualityLevels plugin. Sets up the QualityLevelList and * event handlers. * * @param {Player} player Player object. * @param {Object} options Plugin options object. * @return {QualityLevelList} a list of QualityLevels */ const initPlugin = function (player, options) { const originalPluginFn = player.qualityLevels; const qualityLevelList = new QualityLevelList(); const disposeHandler = function () { qualityLevelList.dispose(); player.qualityLevels = originalPluginFn; player.off('dispose', disposeHandler); }; player.on('dispose', disposeHandler); player.qualityLevels = () => qualityLevelList; player.qualityLevels.VERSION = version; return qualityLevelList; }; /** * A video.js plugin. * * In the plugin function, the value of `this` is a video.js `Player` * instance. You cannot rely on the player being in a "ready" state here, * depending on how the plugin is invoked. This may or may not be important * to you; if not, remove the wait for "ready"! * * @param {Object} options Plugin options object * @return {QualityLevelList} a list of QualityLevels */ const qualityLevels = function (options) { return initPlugin(this, videojs__default["default"].obj.merge({}, options)); }; // Register the plugin with video.js. videojs__default["default"].registerPlugin('qualityLevels', qualityLevels); // Include the version number. qualityLevels.VERSION = version; return qualityLevels; })); /***/ }), /***/ "../node_modules/videojs-contrib-quality-menu/dist/videojs-contrib-quality-menu.es.js": /*!********************************************************************************************!*\ !*** ../node_modules/videojs-contrib-quality-menu/dist/videojs-contrib-quality-menu.es.js ***! \********************************************************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (/* binding */ qualityMenu) /* harmony export */ }); /* harmony import */ var video_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! video.js */ "../node_modules/video.js/dist/alt/video.core-exposed.js"); /* harmony import */ var video_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(video_js__WEBPACK_IMPORTED_MODULE_0__); /*! @name videojs-contrib-quality-menu @version 1.0.4 @license Apache-2.0 */ /** * @file quality-menu-item.js */ const MenuItem = video_js__WEBPACK_IMPORTED_MODULE_0___default().getComponent('MenuItem'); const dom = (video_js__WEBPACK_IMPORTED_MODULE_0___default().dom) || (video_js__WEBPACK_IMPORTED_MODULE_0___default()); /** * The quality level menu quality * * @extends MenuItem * @class QualityMenuItem */ class QualityMenuItem extends MenuItem { /** * Creates a QualityMenuItem * * @param {Player|Object} player * Main Player * @param {Object} [options] * Options for menu item * @param {number[]} options.levels * Array of indices mapping to QualityLevels in the QualityLevelList for * this menu item * @param {string} options.label * Label for this menu item * @param {string} options.controlText * control text for this menu item * @param {string} options.subLabel * sub label text for this menu item * @param {boolean} options.active * True if the QualityLevelList.selectedIndex is contained in the levels list * for this menu * @param {boolean} options.selected * True if this menu item is the selected item in the UI * @param {boolean} options.selectable * True if this menu item should be selectable in the UI */ constructor(player, options = {}) { const selectedOption = options.selected; // We need to change options.seleted to options.active because the call to super // causes us to run MenuItem's constructor which calls this.selected(options.selected) // However, for QualityMenuItem, we change the meaning of the parameter to // this.selected() to be what we mean for 'active' which is True if the // QualityLevelList.selectedIndex is contained in the levels list for this menu options.selected = options.active; super(player, options); const qualityLevels = player.qualityLevels(); this.levels_ = options.levels; this.selected_ = selectedOption; this.handleQualityChange = this.handleQualityChange.bind(this); this.controlText(options.controlText); this.on(qualityLevels, 'change', this.handleQualityChange); this.on('dispose', () => { this.off(qualityLevels, 'change', this.handleQualityChange); }); } /** * Create the component's DOM element * * @param {string} [type] * Element type * @param {Object} [props] * Element properties * @param {Object} [attrs] * An object of attributes that should be set on the element * @return {Element} * The DOM element * @method createEl */ createEl(type, props, attrs) { const el = super.createEl(type, props, attrs); const subLabel = dom.createEl('span', { className: 'vjs-quality-menu-item-sub-label', innerHTML: this.localize(this.options_.subLabel || '') }); this.subLabel_ = subLabel; if (el) { el.appendChild(subLabel); } return el; } /** * Handle a click on the menu item, and set it to selected * * @method handleClick */ handleClick() { this.updateSiblings_(); const qualityLevels = this.player().qualityLevels(); const currentlySelected = qualityLevels.selectedIndex; for (let i = 0, l = qualityLevels.length; i < l; i++) { // do not disable the currently selected quality until the end to prevent // playlist selection from selecting something new until we've enabled/disabled // all the quality levels if (i !== currentlySelected) { qualityLevels[i].enabled = false; } } for (let i = 0, l = this.levels_.length; i < l; i++) { qualityLevels[this.levels_[i]].enabled = true; } // Disable the quality level that was selected before the click if it is not // associated with this menu item if (currentlySelected !== -1 && this.levels_.indexOf(currentlySelected) === -1) { qualityLevels[currentlySelected].enabled = false; } } /** * Handle a change event from the QualityLevelList * * @method handleQualityChange */ handleQualityChange() { const qualityLevels = this.player().qualityLevels(); const active = this.levels_.indexOf(qualityLevels.selectedIndex) > -1; this.selected(active); } /** * Set this menu item as selected or not * * @param {boolean} active * True if the active quality level is controlled by this item * @method selected */ selected(active) { if (!this.selectable) { return; } if (this.selected_) { const hasSubLabel = this.options_.subLabel; this.addClass('vjs-selected'); this.el_.setAttribute('aria-checked', 'true'); // aria-checked isn't fully supported by browsers/screen readers, // so indicate selected state to screen reader in the control text. this.controlText(this.localize(hasSubLabel ? 'selected,' : 'selected')); const controlBar = this.player().controlBar; const menuButton = controlBar.getChild('QualityMenuButton'); if (!active) { // This menu item is manually selected but the current playing quality level // is NOT associated with this menu item. This can happen if the quality hasnt // changed yet or something went wrong with rendition selection such as failed // server responses for playlists menuButton.addClass('vjs-quality-menu-button-waiting'); } else { menuButton.removeClass('vjs-quality-menu-button-waiting'); } } else { this.removeClass('vjs-selected'); this.el_.setAttribute('aria-checked', 'false'); // Indicate un-selected state to screen reader // Note that a space clears out the selected state text this.controlText(this.options_.controlText); } } /** * Sets this QualityMenuItem to be selected and deselects the other items * * @method updateSiblings_ */ updateSiblings_() { const qualityLevels = this.player().qualityLevels(); const controlBar = this.player().controlBar; const menuItems = controlBar.getChild('QualityMenuButton').items; for (let i = 0, l = menuItems.length; i < l; i++) { const item = menuItems[i]; const active = item.levels_.indexOf(qualityLevels.selectedIndex) > -1; item.selected_ = item === this; item.selected(active); } } } /** * @file quality-menu-button.js */ const MenuButton = video_js__WEBPACK_IMPORTED_MODULE_0___default().getComponent('MenuButton'); /** * Checks whether any of the QualityLevels in a QualityLevelList have resolution information * * @param {QualityLevelList} qualityLevelList * The list of QualityLevels * @return {boolean} * True if any levels have resolution information, false if none have * @function hasResolutionInfo */ const hasResolutionInfo = function (qualityLevelList) { return Array.from(qualityLevelList).some(level => level.height); }; /** * Determines the appropriate sub label for the given lines of resolution * * @param {number} lines * The horizontal lines of resolution * @return {string} * sub label for given resolution * @function getSubLabel */ const getSubLabel = function (lines) { if (lines >= 2160) { return '4K'; } if (lines >= 720) { return 'HD'; } return ''; }; /** * The component for controlling the quality menu * * @extends MenuButton * @class QualityMenuButton */ class QualityMenuButton extends MenuButton { /** * Creates a QualityMenuButton * * @param {Player|Object} player * Main Player * @param {Object} [options] * Options for QualityMenuButton */ constructor(player, options = {}) { super(player, options); this.el_.setAttribute('aria-label', this.localize('Quality Levels')); this.controlText('Quality Levels'); if (!player.options().experimentalSvgIcons) { this.$('.vjs-icon-placeholder').classList.add('vjs-icon-cog'); } this.setIcon('cog'); this.qualityLevels_ = player.qualityLevels(); this.update = this.update.bind(this); this.hide = this.hide.bind(this); this.handleQualityChange_ = this.handleQualityChange_.bind(this); this.firstChangeHandler_ = this.firstChangeHandler_.bind(this); this.enableDefaultResolution_ = this.enableDefaultResolution_.bind(this); this.on(this.qualityLevels_, 'addqualitylevel', this.update); this.on(this.qualityLevels_, 'removequalitylevel', this.update); this.on(this.qualityLevels_, 'change', this.handleQualityChange_); // TODO: Remove this and the `defaultResolution` option once videojs/http-streaming supports comparable functionality this.one(this.qualityLevels_, 'change', this.firstChangeHandler_); player.on('adstart', this.hide); player.on(['adend', 'adtimeout'], this.update); this.update(); this.on('dispose', () => { this.off(this.qualityLevels_, 'addqualitylevel', this.update); this.off(this.qualityLevels_, 'removequalitylevel', this.update); this.off(this.qualityLevels_, 'change', this.handleQualityChange_); this.off(this.qualityLevels_, 'change', this.firstChangeHandler_); player.off('adstart', this.hide); player.off(['adend', 'adtimeout'], this.update); player.off('loadedmetadata', this.enableDefaultResolution_); }); } /** * Allow sub components to stack CSS class names * * @return {string} * The constructed class name * @method buildWrapperCSSClass */ buildWrapperCSSClass() { return `vjs-quality-menu-wrapper ${super.buildWrapperCSSClass()}`; } /** * Allow sub components to stack CSS class names * * @return {string} * The constructed class name * @method buildCSSClass */ buildCSSClass() { return `vjs-quality-menu-button ${super.buildCSSClass()}`; } /** * Create the list of menu items. * * @return {Array} * The list of menu items * @method createItems */ createItems() { const items = []; if (!(this.qualityLevels_ && this.qualityLevels_.length)) { return items; } let groups; if (this.options_.useResolutionLabels && hasResolutionInfo(this.qualityLevels_)) { groups = this.groupByResolution_(); this.addClass('vjs-quality-menu-button-use-resolution'); } else { groups = this.groupByBitrate_(); this.removeClass('vjs-quality-menu-button-use-resolution'); } // if there is only 1 or 0 menu items, we should just return an empty list so // the ui does not appear when there are no options. We consider 1 to be no options // since Auto will have the same behavior as selecting the only other option, // so it is as effective as not having any options. if (groups.length <= 1) { return []; } groups.forEach(group => { if (group.levels.length) { group.selectable = true; items.push(new QualityMenuItem(this.player(), group)); } }); // Add the Auto menu item const auto = new QualityMenuItem(this.player(), { levels: Array.prototype.map.call(this.qualityLevels_, (level, i) => i), label: this.localize('Auto'), controlText: '', active: true, selected: true, selectable: true }); this.autoMenuItem_ = auto; items.push(auto); return items; } /** * Group quality levels by lines of resolution * * @return {Array} * Array of each group * @method groupByResolution_ */ groupByResolution_() { const groups = {}; const order = []; for (let i = 0, l = this.qualityLevels_.length; i < l; i++) { const level = this.qualityLevels_[i]; const active = this.qualityLevels_.selectedIndex === i; const lines = level.height; // Do not include an audio-only level if (!lines) { continue; } let label; if (this.options_.resolutionLabelBitrates) { const kbRate = Math.round(level.bitrate / 1000); label = `${lines}p @ ${kbRate} kbps`; } else { label = lines + 'p'; } if (!groups[label]) { const subLabel = getSubLabel(lines); groups[label] = { levels: [], label, controlText: '', subLabel }; order.push({ label, lines }); } if (active) { groups[label].active = true; } groups[label].levels.push(i); } // Sort from High to Low order.sort((a, b) => b.lines - a.lines); const sortedGroups = []; order.forEach(group => { sortedGroups.push(groups[group.label]); }); return sortedGroups; } /** * Group quality levels by bitrate into SD and HD buckets * * @return {Array} * Array of each group * @method groupByBitrate_ */ groupByBitrate_() { // groups[0] for HD, groups[1] for SD, since we want sorting from high to low\ const groups = [{ levels: [], label: 'HD', controlText: 'High Definition' }, { levels: [], label: 'SD', controlText: 'Standard Definition' }]; for (let i = 0, l = this.qualityLevels_.length; i < l; i++) { const level = this.qualityLevels_[i]; const active = this.qualityLevels_.selectedIndex === i; let group; if (level.bitrate < this.options_.sdBitrateLimit) { group = groups[1]; } else { group = groups[0]; } if (active) { group.active = true; } group.levels.push(i); } if (!groups[0].levels.length || !groups[1].levels.length) { // Either HD or SD do not have any quality levels, we should just return an empty // list so the ui does not appear when there are no options. We consider 1 // to be no options since Auto will have the same behavior as selecting the only // other option, so it is as effective as not having any options. return []; } return groups; } /** * Handle a change event from the QualityLevelList * * @method handleQualityChange_ */ handleQualityChange_() { const selected = this.qualityLevels_[this.qualityLevels_.selectedIndex]; const useResolution = this.options_.useResolutionLabels && hasResolutionInfo(this.qualityLevels_); let subLabel = ''; if (selected) { if (useResolution) { subLabel = getSubLabel(selected.height); } else if (selected.bitrate >= this.options_.sdBitrateLimit) { subLabel = 'HD'; } } if (subLabel === 'HD') { this.addClass('vjs-quality-menu-button-HD-flag'); this.removeClass('vjs-quality-menu-button-4K-flag'); } else if (subLabel === '4K') { this.removeClass('vjs-quality-menu-button-HD-flag'); this.addClass('vjs-quality-menu-b