UNPKG

videojs-shaka

Version:
730 lines (582 loc) 20.1 kB
/*! @name videojs-shaka @version 1.1.2 @license MIT */ import videojs from 'video.js'; function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; } function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } function _getQuality(tech, shakaPlayer) { var tracks = []; var levels = shakaPlayer.getVariantTracks().filter(function (t) { return t.type === 'variant'; }); if (levels.length > 1) { var autoLevel = { id: -1, label: 'auto', selected: true }; tracks.push(autoLevel); } levels.forEach(function (level, index) { var track = level; var label = ''; if (level.height >= 2160) { label = ' (4k)'; } else if (level.height >= 1440) { label = ' (2k)'; } else if (level.height >= 720) { label = ' (HD)'; } track.label = level.height + 'p' + label; tracks.push(track); }); // group tracks by langugage b/c we will need to only display the tracks associated with the current audio track var sortedTracks = tracks.sort(function (track1, track2) { if (track1.language > track2.language) { return -1; } if (track2.language > track1.language) { return 1; } if (track1.height > track2.height) { return -1; } if (track2.height > track1.height) { return 1; } if (track1.bandwidth > track2.bandwidth) { return -1; } if (track2.bandwidth > track1.bandwidth) { return 1; } return 0; }).reduce(function (accumulator, track) { if (track.height !== accumulator.previousHeight || track.height === accumulator.previousHeight && track.language !== accumulator.previousLanguage) { accumulator.previousHeight = track.height; accumulator.previousLanguage = track.language; accumulator.list.push(track); } return accumulator; }, { previousHeight: null, previousLanguage: null, list: [] }).list; return sortedTracks; } function setupQualityTracks(tech, shakaPlayer) { tech.trigger('loadedqualitydata', { qualityData: { video: _getQuality(tech, shakaPlayer) }, qualitySwitchCallback: function qualitySwitchCallback(id, type) { // Update the adaptation. shakaPlayer.configure({ abr: { enabled: id === -1 } }); // Is auto? if (id === -1) return; var tracks = shakaPlayer.getVariantTracks().filter(function (t) { return t.id === id && t.type === 'variant'; }); shakaPlayer.selectVariantTrack(tracks[0], /* clearBuffer */ true); // fire `variantchanged` event - only supports debug mode right now // todo - need to figure out how to do this in non debug mode if (shaka.util.FakeEvent) { var event = new shaka.util.FakeEvent('variantchanged'); shakaPlayer.dispatchEvent(event); } } }); } function find(l, f) { for (var i = 0; i < l.length; i++) { if (f(l[i])) { return l[i]; } } } /* * Attach text tracks from dash.js to videojs * * @param {videojs} tech the videojs player tech instance * @param {array} tracks the tracks loaded by dash.js to attach to videojs * * @private */ function attachDashTextTracksToVideojs(tech, shakaPlayer, tracks) { var trackDictionary = []; // Add remote tracks var tracksAttached = tracks // Map input data to match HTMLTrackElement spec // https://developer.mozilla.org/en-US/docs/Web/API/HTMLTrackElement .map(function (track) { return { dashTrack: track, trackConfig: { label: track.label || track.language, language: track.language, srclang: track.language, kind: track.kind } }; }) // Add track to videojs track list .map(function (_ref) { var trackConfig = _ref.trackConfig, dashTrack = _ref.dashTrack; var remoteTextTrack = tech.addRemoteTextTrack(trackConfig, false); trackDictionary.push({ textTrack: remoteTextTrack.track, dashTrack: dashTrack }); // Don't add the cues becuase we're going to let dash handle it natively. This will ensure // that dash handle external time text files and fragmented text tracks. // // Example file with external time text files: // https://storage.googleapis.com/shaka-demo-assets/sintel-mp4-wvtt/dash.mpd return remoteTextTrack; }); /* * Scan `videojs.textTracks()` to find one that is showing. Set the dash text track. */ function updateActiveDashTextTrack() { var dashTrackToActivate; var textTracks = tech.textTracks(); // Iterate through the tracks and find the one marked as showing. If none are showing, // disable text tracks. var _loop = function _loop(i) { var textTrack = textTracks[i]; if (textTrack.mode === 'showing') { // Find the dash track we want to use /* jshint loopfunc: true */ var dictionaryLookupResult = find(trackDictionary, function (track) { return track.textTrack === textTrack; }); /* jshint loopfunc: false */ dashTrackToActivate = dictionaryLookupResult ? dictionaryLookupResult.dashTrack : null; } }; for (var i = 0; i < textTracks.length; i += 1) { _loop(i); } // If the text track has changed, then set it in shaka if (dashTrackToActivate) { shakaPlayer.selectTextTrack(dashTrackToActivate); shakaPlayer.setTextTrackVisibility(true); } else { shakaPlayer.setTextTrackVisibility(false); } } // Update dash when videojs's selected text track changes. tech.textTracks().on('change', updateActiveDashTextTrack); // Cleanup event listeners whenever we start loading a new source shakaPlayer.addEventListener('unloading', function () { tech.textTracks().off('change', updateActiveDashTextTrack); }); // Initialize the text track on our first run-through updateActiveDashTextTrack(); return tracksAttached; } function setupTextTracks(tech, shakaPlayer) { // Store the tracks that we've added so we can remove them later. var dashTracksAttachedToVideoJs = []; // Clear the tracks that we added. We don't clear them all because someone else can add tracks. function clearDashTracks() { dashTracksAttachedToVideoJs.forEach(tech.removeRemoteTextTrack.bind(tech)); dashTracksAttachedToVideoJs = []; } function handleTextTracksAdded(tracks) { // Cleanup old tracks clearDashTracks(); // Don't try to add text tracks if there aren't any or if the app is sideloading webvtt files if (!tracks.length || tech.options_.sideload) { shakaPlayer.setTextTrackVisibility(false); return; } // Save the tracks so we can remove them later dashTracksAttachedToVideoJs = attachDashTextTracksToVideojs(tech, shakaPlayer, tracks); } handleTextTracksAdded(shakaPlayer.getTextTracks()); } /** * Setup audio tracks. Take the tracks from dash and add the tracks to videojs. Listen for when * videojs changes tracks and apply that to the dash player because videojs doesn't do this * natively. * * @private * @param {videojs} tech the videojs player tech instance * @param {videojs.tech} tech the videojs tech being used */ function handleAudioTracksAdded(tech, shakaPlayer, tracks) { var videojsAudioTracks = tech.audioTracks(); function generateIdFromTrackIndex(index) { return "dash-audio-" + index; } function generateLabelFromTrack(track) { var label = track.language; if (track.role) { label += " (" + track.role + ")"; } return label; } function findDashAudioTrack(subDashAudioTracks, videojsAudioTrack) { return subDashAudioTracks.find(function (track) { return generateLabelFromTrack(track) === videojsAudioTrack.label; }); } // Safari creates a single native `AudioTrack` (not `videojs.AudioTrack`) when loading. Clear all // automatically generated audio tracks so we can create them all ourself. if (videojsAudioTracks.length) { tech.clearTracks(['audio']); } var currentAudioTrack = tracks[0]; tracks.forEach(function (dashTrack, index) { var label = generateLabelFromTrack(dashTrack); if (dashTrack === currentAudioTrack) { tech.trigger('shakaaudiotrackchange', { language: dashTrack.language }); } // Add the track to the player's audio track list. videojsAudioTracks.addTrack(new videojs.AudioTrack({ enabled: dashTrack === currentAudioTrack, id: generateIdFromTrackIndex(index), kind: 'main', label: label, language: dashTrack.language })); }); var audioTracksChangeHandler = function audioTracksChangeHandler() { for (var i = 0; i < videojsAudioTracks.length; i++) { var track = videojsAudioTracks[i]; if (track.enabled) { // Find the audio track we just selected by the id var dashAudioTrack = findDashAudioTrack(tracks, track); if (dashAudioTrack) { // Set is as the current track tech.trigger('shakaaudiotrackchange', { language: dashAudioTrack.language }); shakaPlayer.selectAudioLanguage(dashAudioTrack.language, dashAudioTrack.role); // Stop looping continue; } } } }; videojsAudioTracks.addEventListener('change', audioTracksChangeHandler); shakaPlayer.addEventListener('unloading', function () { videojsAudioTracks.removeEventListener('change', audioTracksChangeHandler); }); } function setupAudioTracks(tech, shakaPlayer) { handleAudioTracksAdded(tech, shakaPlayer, shakaPlayer.getAudioLanguagesAndRoles()); } var version = "1.1.2"; var Html5 = videojs.getTech('Html5'); // Default options for the plugin. // const defaults = {}; /** * Shaka Media Controller - Wrapper for HTML5 Media API * * @mixes Html5~SourceHandlerAdditions * @extends Html5 */ var Shaka = /*#__PURE__*/ function (_Html) { _inheritsLoose(Shaka, _Html); /** * Create a Shaka plugin instance. * * @param {Player} player * A Video.js Player instance. * * @param {Object} [options] * An optional options object. * * While not a core part of the Video.js plugin architecture, a * second argument of options is a convenient way to accept inputs * from your plugin's caller. */ /** * Create an instance of this Tech. * * @param {Object} [options] * The key/value store of player options. * * @param {Component~ReadyCallback} ready * Callback function to call when the `HTML5` Tech is ready. */ function Shaka(options, ready) { var _this; _this = _Html.call(this, options, ready) || this; _this.vjsPlayer = videojs(options.playerId); _this.player_.ready(function () { _this.player_.addClass('vjs-shaka'); }); return _this; } var _proto = Shaka.prototype; _proto.createEl = function createEl() { this.el_ = Html5.prototype.createEl.apply(this, arguments); // Install built-in polyfills to patch browser incompatibilities. shaka.polyfill.installAll(); // set debug log level if (shaka.log) { if (this.options_.debug) { shaka.log.setLevel(shaka.log.Level.DEBUG); } else { shaka.log.setLevel(shaka.log.Level.ERROR); } } this.shaka_ = new shaka.Player(this.el_); this.el_.tech = this; return this.el_; }; _proto.setSrc = function setSrc(src) { var me = this; var shakaOptions = this.options_.configuration || {}; if (typeof shakaOptions.drm === 'function') { shakaOptions.drm = shakaOptions.drm(); } else { shakaOptions.drm = shakaOptions.drm || {}; } if (!shakaOptions.abr) { shakaOptions.abr = { enabled: true }; } this.shaka_.configure(shakaOptions); if (this.options_.licenseServerAuth) { this.shaka_.getNetworkingEngine().registerRequestFilter(this.options_.licenseServerAuth); } this.shaka_.addEventListener('buffering', function (event) { if (event.buffering) { me.vjsPlayer.trigger('waiting'); } else { me.vjsPlayer.trigger('playing'); } }); this.shaka_.addEventListener('error', function (event) { me.retriggerError(event.detail); }); this.shaka_.load(src).then(function () { me.initShakaMenus(); }).catch(me.retriggerError.bind(this)); }; _proto.dispose = function dispose() { if (this.shaka_) { this.shaka_.unload(); this.shaka_.destroy(); } }; _proto.initShakaMenus = function initShakaMenus() { setupQualityTracks(this, this.shaka_); setupTextTracks(this, this.shaka_); setupAudioTracks(this, this.shaka_); }; _proto.retriggerError = function retriggerError(event) { var _this2 = this; var code; // map the shaka player error to the appropriate video.js error if (event.message && (event.message.indexOf('UNSUPPORTED') > -1 || event.message.indexOf('NOT_SUPPORTED') > -1)) { code = 4; } else { switch (event.category) { case 1: code = 2; break; case 2: case 3: case 4: code = 3; break; case 5: code = 1; break; case 6: code = 5; break; case 7: case 8: case 9: code = 0; break; } } this.vjsPlayer.error({ code: code, message: event.code + " - " + event.message }); // only reset the shaka player in 10ms async, so that the rest of the // calling function finishes setTimeout(function () { _this2.dispose(); }, 10); }; return Shaka; }(Html5); // Define default values for the plugin's `state` object here. Shaka.defaultState = {}; // Include the version number. Shaka.VERSION = version; Shaka.isSupported = function () { return !!window.MediaSource; }; Shaka.canPlaySource = function (source, tech) { var dashTypeRE = /^(application\/dash\+xml|application\/x-mpegURL)/i; if (dashTypeRE.test(source.type)) { return 'probably'; } return ''; }; var VjsMenu = videojs.getComponent('Menu'); var QualityMenu = /*#__PURE__*/ function (_VjsMenu) { _inheritsLoose(QualityMenu, _VjsMenu); function QualityMenu(player, options) { var _this; _this = _VjsMenu.call(this, player, options) || this; var me = _assertThisInitialized(_assertThisInitialized(_this)); player.tech_.on('shakaaudiotrackchange', function (event, _ref) { var language = _ref.language; me.children().forEach(function (menuItem) { if (menuItem.options_.label === 'auto') { menuItem.selected(true); menuItem.show(); } else if (menuItem.options_.language === language) { menuItem.selected(false); menuItem.show(); } else { menuItem.selected(false); menuItem.hide(); } }); }); player.on('qualitytrackchange', function () { me.hide(); }); return _this; } var _proto = QualityMenu.prototype; _proto.addItem = function addItem(component) { var _this2 = this; _VjsMenu.prototype.addItem.call(this, component); component.on(['tap', 'click'], function () { var children = _this2.children(); for (var i = 0; i < children.length; i++) { var child = children[i]; if (component !== child) { child.selected(false); } } }); }; return QualityMenu; }(VjsMenu); var VjsMenuItem = videojs.getComponent('MenuItem'); var QualityMenuItem = /*#__PURE__*/ function (_VjsMenuItem) { _inheritsLoose(QualityMenuItem, _VjsMenuItem); function QualityMenuItem() { return _VjsMenuItem.apply(this, arguments) || this; } var _proto = QualityMenuItem.prototype; _proto.handleClick = function handleClick() { _VjsMenuItem.prototype.handleClick.call(this); this.player_.trigger('qualitytrackchange', this.options_); this.options_.qualitySwitchCallback(this.options_.id, this.options_.trackType); }; return QualityMenuItem; }(VjsMenuItem); var VjsButton = videojs.getComponent('MenuButton'); var TRACK_CLASS = { video: 'vjs-icon-hd', audio: 'vjs-icon-cog', subtitle: 'vjs-icon-subtitles' }; var QualityPickerButton = /*#__PURE__*/ function (_VjsButton) { _inheritsLoose(QualityPickerButton, _VjsButton); function QualityPickerButton(player, options) { return _VjsButton.call(this, player, options) || this; } var _proto = QualityPickerButton.prototype; _proto.createMenu = function createMenu() { var menu = new QualityMenu(this.player_, this.options_); var menuItem; var options; for (var i = 0; i < this.options_.qualityList.length; i++) { var quality = this.options_.qualityList[i]; var _this$options_ = this.options_, qualitySwitchCallback = _this$options_.qualitySwitchCallback, trackType = _this$options_.trackType; options = _extends({ qualitySwitchCallback: qualitySwitchCallback, trackType: trackType }, quality, { selectable: true }); menuItem = new QualityMenuItem(this.player_, options); menu.addItem(menuItem); } return menu; }; _proto.buildCSSClass = function buildCSSClass() { return "vjs-quality-button " + TRACK_CLASS[this.options_.trackType] + " vjs-icon-placeholder " + _VjsButton.prototype.buildCSSClass.call(this); }; _proto.buildWrapperCSSClass = function buildWrapperCSSClass() { return "vjs-quality-button " + _VjsButton.prototype.buildWrapperCSSClass.call(this); }; return QualityPickerButton; }(VjsButton); function qualityPickerPlugin() { var player = this; var SUPPORTED_TRACKS = ['video', 'audio', 'subtitle']; // On later versions `player.tech` is undefined before this... if (player.tech_) { player.tech_.on('loadedqualitydata', onQualityData); } else { player.ready(function () { player.tech_.on('loadedqualitydata', onQualityData); }, true); } function onQualityData(event, _ref) { var qualityData = _ref.qualityData, qualitySwitchCallback = _ref.qualitySwitchCallback; var fullscreenToggle = player.controlBar.getChild('fullscreenToggle'); player.controlBar.removeChild(fullscreenToggle); for (var i = 0; i < SUPPORTED_TRACKS.length; i++) { var track = SUPPORTED_TRACKS[i]; var name = track + 'PickerButton'; // videojs.utils.toTitleCase name = name.charAt(0).toUpperCase() + name.slice(1); var qualityPickerButton = player.controlBar.getChild(name); if (qualityPickerButton) { qualityPickerButton.dispose(); player.controlBar.removeChild(qualityPickerButton); } if (qualityData[track] && qualityData[track].length > 1) { qualityPickerButton = new QualityPickerButton(player, { name: name, qualityList: qualityData[track], qualitySwitchCallback: qualitySwitchCallback, trackType: track }); player.controlBar.addChild(qualityPickerButton); } } if (fullscreenToggle) { player.controlBar.addChild(fullscreenToggle); } } } var Tech = videojs.getTech('Tech'); // Register Shaka as a Tech; Tech.registerTech('Shaka', Shaka); // Register quality picker plugin var registerPlugin = videojs.registerPlugin || videojs.plugin; registerPlugin('qualityPickerPlugin', qualityPickerPlugin); export default Shaka;