UNPKG

videojs-contrib-dash

Version:

A Video.js source-handler providing MPEG-DASH playback.

442 lines (360 loc) 13.9 kB
import window from 'global/window'; import videojs from 'video.js'; import dashjs from 'dashjs'; import setupAudioTracks from './setup-audio-tracks'; import setupTextTracks from './setup-text-tracks'; import document from 'global/document'; import './ttml-text-track-display'; /** * videojs-contrib-dash * * Use Dash.js to playback DASH content inside of Video.js via a SourceHandler */ class Html5DashJS { constructor(source, tech, options) { // Get options from tech if not provided for backwards compatibility options = options || tech.options_; this.player = videojs(options.playerId); this.player.dash = this.player.dash || {}; this.tech_ = tech; this.el_ = tech.el(); this.elParent_ = this.el_.parentNode; this.hasFiniteDuration_ = false; // Do nothing if the src is falsey if (!source.src) { return; } // While the manifest is loading and Dash.js has not finished initializing // we must defer events and functions calls with isReady_ and then `triggerReady` // again later once everything is setup tech.isReady_ = false; if (Html5DashJS.updateSourceData) { videojs.log.warn('updateSourceData has been deprecated.' + ' Please switch to using hook("updatesource", callback).'); source = Html5DashJS.updateSourceData(source); } // call updatesource hooks Html5DashJS.hooks('updatesource').forEach((hook) => { source = hook(source); }); const manifestSource = source.src; this.keySystemOptions_ = Html5DashJS.buildDashJSProtData(source.keySystemOptions); this.player.dash.mediaPlayer = dashjs.MediaPlayer().create(); this.mediaPlayer_ = this.player.dash.mediaPlayer; // Log MedaPlayer messages through video.js if (Html5DashJS.useVideoJSDebug) { videojs.log.warn('useVideoJSDebug has been deprecated.' + ' Please switch to using hook("beforeinitialize", callback).'); Html5DashJS.useVideoJSDebug(this.mediaPlayer_); } if (Html5DashJS.beforeInitialize) { videojs.log.warn('beforeInitialize has been deprecated.' + ' Please switch to using hook("beforeinitialize", callback).'); Html5DashJS.beforeInitialize(this.player, this.mediaPlayer_); } Html5DashJS.hooks('beforeinitialize').forEach((hook) => { hook(this.player, this.mediaPlayer_); }); // Must run controller before these two lines or else there is no // element to bind to. this.mediaPlayer_.initialize(); // Retrigger a dash.js-specific error event as a player error // See src/streaming/utils/ErrorHandler.js in dash.js code // Handled with error (playback is stopped): // - capabilityError // - downloadError // - manifestError // - mediaSourceError // - mediaKeySessionError // Not handled: // - timedTextError (video can still play) // - mediaKeyMessageError (only fires under 'might not work' circumstances) this.retriggerError_ = (event) => { if (event.error === 'capability' && event.event === 'mediasource') { // No support for MSE this.player.error({ code: 4, message: 'The media cannot be played because it requires a feature ' + 'that your browser does not support.' }); } else if (event.error === 'manifestError' && ( // Manifest type not supported (event.event.id === 'createParser') || // Codec(s) not supported (event.event.id === 'codec') || // No streams available to stream (event.event.id === 'nostreams') || // Error creating Stream object (event.event.id === 'nostreamscomposed') || // syntax error parsing the manifest (event.event.id === 'parse') || // a stream has multiplexed audio+video (event.event.id === 'multiplexedrep') )) { // These errors have useful error messages, so we forward it on this.player.error({code: 4, message: event.event.message}); } else if (event.error === 'mediasource') { // This error happens when dash.js fails to allocate a SourceBuffer // OR the underlying video element throws a `MediaError`. // If it's a buffer allocation fail, the message states which buffer // (audio/video/text) failed allocation. // If it's a `MediaError`, dash.js inspects the error object for // additional information to append to the error type. if (event.event.match('MEDIA_ERR_ABORTED')) { this.player.error({code: 1, message: event.event}); } else if (event.event.match('MEDIA_ERR_NETWORK')) { this.player.error({code: 2, message: event.event}); } else if (event.event.match('MEDIA_ERR_DECODE')) { this.player.error({code: 3, message: event.event}); } else if (event.event.match('MEDIA_ERR_SRC_NOT_SUPPORTED')) { this.player.error({code: 4, message: event.event}); } else if (event.event.match('MEDIA_ERR_ENCRYPTED')) { this.player.error({code: 5, message: event.event}); } else if (event.event.match('UNKNOWN')) { // We shouldn't ever end up here, since this would mean a // `MediaError` thrown by the video element that doesn't comply // with the W3C spec. But, since we should handle the error, // throwing a MEDIA_ERR_SRC_NOT_SUPPORTED is probably the // most reasonable thing to do. this.player.error({code: 4, message: event.event}); } else { // Buffer allocation error this.player.error({code: 4, message: event.event}); } } else if (event.error === 'capability' && event.event === 'encryptedmedia') { // Browser doesn't support EME this.player.error({ code: 5, message: 'The media cannot be played because it requires encryption ' + 'features that your browser does not support.' }); } else if (event.error === 'key_session') { // This block handles pretty much all errors thrown by the // encryption subsystem this.player.error({ code: 5, message: event.event }); } else if (event.error === 'download') { this.player.error({ code: 2, message: 'The media playback was aborted because too many consecutive ' + 'download errors occurred.' }); } else if (event.error === 'mssError') { this.player.error({ code: 3, message: event.event }); } else { // ignore the error return; } // only reset the dash player in 10ms async, so that the rest of the // calling function finishes setTimeout(() => { this.mediaPlayer_.reset(); }, 10); }; this.mediaPlayer_.on(dashjs.MediaPlayer.events.ERROR, this.retriggerError_); this.getDuration_ = (event) => { const periods = event.data.Period_asArray; const oldHasFiniteDuration = this.hasFiniteDuration_; if (event.data.mediaPresentationDuration || periods[periods.length - 1].duration) { this.hasFiniteDuration_ = true; } else { // in case we run into a weird situation where we're VOD but then // switch to live this.hasFiniteDuration_ = false; } if (this.hasFiniteDuration_ !== oldHasFiniteDuration) { this.player.trigger('durationchange'); } }; this.mediaPlayer_.on(dashjs.MediaPlayer.events.MANIFEST_LOADED, this.getDuration_); // Apply all dash options that are set if (options.dash) { Object.keys(options.dash).forEach((key) => { if (key === 'useTTML') { return; } const dashOptionsKey = 'set' + key.charAt(0).toUpperCase() + key.slice(1); let value = options.dash[key]; if (this.mediaPlayer_.hasOwnProperty(dashOptionsKey)) { // Providing a key without `set` prefix is now deprecated. videojs.log.warn('Using dash options in videojs-contrib-dash without the set prefix ' + `has been deprecated. Change '${key}' to '${dashOptionsKey}'`); // Set key so it will still work key = dashOptionsKey; } if (!this.mediaPlayer_.hasOwnProperty(key)) { videojs.log.warn( `Warning: dash configuration option unrecognized: ${key}` ); return; } // Guarantee `value` is an array if (!Array.isArray(value)) { value = [value]; } this.mediaPlayer_[key](...value); }); } this.mediaPlayer_.attachView(this.el_); if (options.dash && options.dash.useTTML) { this.ttmlContainer_ = this.player.addChild('TTMLTextTrackDisplay'); this.mediaPlayer_.attachTTMLRenderingDiv(this.ttmlContainer_.el()); } // Dash.js autoplays by default, video.js will handle autoplay this.mediaPlayer_.setAutoPlay(false); // Setup audio tracks setupAudioTracks.call(null, this.player, tech); // Setup text tracks setupTextTracks.call(null, this.player, tech, options); // Attach the source with any protection data this.mediaPlayer_.setProtectionData(this.keySystemOptions_); this.mediaPlayer_.attachSource(manifestSource); this.tech_.triggerReady(); } /* * Iterate over the `keySystemOptions` array and convert each object into * the type of object Dash.js expects in the `protData` argument. * * Also rename 'licenseUrl' property in the options to an 'serverURL' property */ static buildDashJSProtData(keySystemOptions) { const output = {}; if (!keySystemOptions || !Array.isArray(keySystemOptions)) { return null; } for (let i = 0; i < keySystemOptions.length; i++) { const keySystem = keySystemOptions[i]; const options = videojs.mergeOptions({}, keySystem.options); if (options.licenseUrl) { options.serverURL = options.licenseUrl; delete options.licenseUrl; } output[keySystem.name] = options; } return output; } dispose() { if (this.mediaPlayer_) { this.mediaPlayer_.off(dashjs.MediaPlayer.events.ERROR, this.retriggerError_); this.mediaPlayer_.off(dashjs.MediaPlayer.events.MANIFEST_LOADED, this.getDuration_); this.mediaPlayer_.reset(); } if (this.player.dash) { delete this.player.dash; } if (this.ttmlContainer_) { this.ttmlContainer_.dispose(); this.player.removeChild('TTMLTextTrackDisplay'); } } duration() { if (this.mediaPlayer_.isDynamic() && !this.hasFiniteDuration_) { return Infinity; } return this.mediaPlayer_.duration(); } /** * Get a list of hooks for a specific lifecycle * * @param {string} type the lifecycle to get hooks from * @param {Function|Function[]} [hook] Optionally add a hook tothe lifecycle * @return {Array} an array of hooks or epty if none * @method hooks */ static hooks(type, hook) { Html5DashJS.hooks_[type] = Html5DashJS.hooks_[type] || []; if (hook) { Html5DashJS.hooks_[type] = Html5DashJS.hooks_[type].concat(hook); } return Html5DashJS.hooks_[type]; } /** * Add a function hook to a specific dash lifecycle * * @param {string} type the lifecycle to hook the function to * @param {Function|Function[]} hook the function or array of functions to attach * @method hook */ static hook(type, hook) { Html5DashJS.hooks(type, hook); } /** * Remove a hook from a specific dash lifecycle. * * @param {string} type the lifecycle that the function hooked to * @param {Function} hook The hooked function to remove * @return {boolean} True if the function was removed, false if not found * @method removeHook */ static removeHook(type, hook) { const index = Html5DashJS.hooks(type).indexOf(hook); if (index === -1) { return false; } Html5DashJS.hooks_[type] = Html5DashJS.hooks_[type].slice(); Html5DashJS.hooks_[type].splice(index, 1); return true; } } Html5DashJS.hooks_ = {}; const canHandleKeySystems = function(source) { // copy the source source = JSON.parse(JSON.stringify(source)); if (Html5DashJS.updateSourceData) { videojs.log.warn('updateSourceData has been deprecated.' + ' Please switch to using hook("updatesource", callback).'); source = Html5DashJS.updateSourceData(source); } // call updatesource hooks Html5DashJS.hooks('updatesource').forEach((hook) => { source = hook(source); }); const videoEl = document.createElement('video'); if (source.keySystemOptions && !(window.navigator.requestMediaKeySystemAccess || // IE11 Win 8.1 videoEl.msSetMediaKeys)) { return false; } return true; }; videojs.DashSourceHandler = function() { return { canHandleSource(source) { const dashExtRE = /\.mpd/i; if (!canHandleKeySystems(source)) { return ''; } if (videojs.DashSourceHandler.canPlayType(source.type)) { return 'probably'; } else if (dashExtRE.test(source.src)) { return 'maybe'; } return ''; }, handleSource(source, tech, options) { return new Html5DashJS(source, tech, options); }, canPlayType(type) { return videojs.DashSourceHandler.canPlayType(type); } }; }; videojs.DashSourceHandler.canPlayType = function(type) { const dashTypeRE = /^application\/dash\+xml/i; if (dashTypeRE.test(type)) { return 'probably'; } return ''; }; // Only add the SourceHandler if the browser supports MediaSourceExtensions if (window.MediaSource) { videojs.getTech('Html5').registerSourceHandler(videojs.DashSourceHandler(), 0); } videojs.Html5DashJS = Html5DashJS; export default Html5DashJS;