UNPKG

@odopod/odo-video

Version:
1,201 lines (949 loc) 37.8 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('screenfull')) : typeof define === 'function' && define.amd ? define(['screenfull'], factory) : (global.OdoVideo = factory(global.screenfull)); }(this, (function (screenfull) { 'use strict'; screenfull = screenfull && screenfull.hasOwnProperty('default') ? screenfull['default'] : screenfull; var settings = { Classes: { BASE: 'odo-video', IS_PLAYING: 'odo-video--playing', IS_FULLSCREEN: 'odo-video--fullscreen', IS_MUTED: 'odo-video--muted', IS_BUFFERING: 'odo-video--buffering', IS_IDLE: 'odo-video--idle', NO_FLEXBOX: 'odo-video--no-flexbox', CONTROLS_STACKED: 'odo-video__controls--stacked', CONTROLS_HIDDEN: 'odo-video__controls--hidden', CONTROLS: 'odo-video__controls', PLAY_TOGGLE: 'odo-video__play-toggle', PLAY_CONTROL: 'odo-video__play-control', PAUSE_CONTROL: 'odo-video__pause-control', PROGRESS_CONTAINER: 'odo-video__progress-container', PROGRESS_HOLDER: 'odo-video__progress-holder', BUFFER: 'odo-video__buffer', PROGRESS: 'odo-video__progress', CURRENT_TIME: 'odo-video__current-time', VOLUME: 'odo-video__volume', MUTE_CONTROL: 'odo-video__mute-control', UNMUTE_CONTROL: 'odo-video__unmute-control', FULLSCREEN: 'odo-video__fullscreen', FULLSCREEN_CONTROL: 'odo-video__fullscreen-control', EXIT_FULLSCREEN_CONTROL: 'odo-video__exit-fullscreen-control', FLEXIBLE_SPACE: 'odo-video__flexible-space' }, Icons: { FULLSCREEN: '<svg viewBox="0 0 16 16" enable-background="new 0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 2H8v2h2.586L8 6.587 9.414 8 12 5.415V8h2V2zM8 9.414L6.586 8 4 10.586V8H2v6h6v-2H5.415z"/></svg>', EXIT_FULLSCREEN: '<svg viewBox="0 0 16 16" enable-background="new 0 0 16 16"><path d="M15 2.4L13.6 1 11 3.6V1H9v6h6V5h-2.6L15 2.4zM5 9H1v2h2.6L1 13.6 2.4 15 5 12.4V15h2V9H5z"/></svg>', AUDIO_ON: '<svg viewBox="0 0 16 16" enable-background="new 0 0 16 16"><path d="M1 5.366v5.294c0 .177.142.32.317.32h2.89c.093 0 .18.034.254.093l4.505 3.743c.16.135.4.018.402-.19l.002-13.25c0-.21-.243-.325-.403-.193L4.47 4.923c-.077.064-.173.098-.27.098H1.33c-.235 0-.33.17-.33.346zm10.292-1.03c-.295-.296-.76-.296-1.057 0-.292.296-.292.775.002 1.07v-.002c.642.652 1.04 1.55 1.04 2.54 0 .992-.396 1.884-1.04 2.535-.294.295-.294.774 0 1.07.143.148.334.222.526.222.19 0 .388-.074.528-.22.91-.922 1.476-2.202 1.476-3.606 0-1.41-.567-2.69-1.476-3.61h.002zm1.71-1.732c-.294-.296-.76-.296-1.053 0-.294.296-.293.772 0 1.066 1.08 1.096 1.754 2.602 1.754 4.273s-.667 3.176-1.753 4.273c-.294.294-.294.77 0 1.067.142.146.337.222.53.222.19 0 .386-.076.526-.222 1.35-1.366 2.19-3.257 2.19-5.34-.008-2.08-.843-3.975-2.194-5.34z"/></svg>', AUDIO_OFF: '<svg viewBox="0 0 16 16" enable-background="new 0 0 16 16"><path d="M1 5.366v5.294c0 .177.142.32.317.32h2.89c.093 0 .18.034.254.093l4.505 3.743c.16.135.4.018.402-.19l.002-13.25c0-.21-.243-.325-.403-.193L4.47 4.923c-.077.064-.173.098-.27.098H1.33c-.235 0-.33.17-.33.346z"/></svg>' }, IDLE_TIMEOUT: 2000, Defaults: { controls: 1, layoutControls: null, updateControls: null, pauseOnClick: true }, Controls: { NONE: 0, INLINE_PROGRESS: 1, STACKED_PROGRESS: 2, CUSTOM: 3 }, VideoEvents: { LOADED_METADATA: { name: 'loadedmetadata', readyState: 1 }, LOADED_DATA: { name: 'loadeddata', readyState: 2 }, CAN_PLAY: { name: 'canplay', readyState: 3 }, CAN_PLAYTHROUGH: { name: 'canplaythrough', readyState: 4 } } }; // Determine support for videos. From Modernizr source. // https://github.com/Modernizr/Modernizr/blob/master/feature-detects/video.js // Codec values from : github.com/NielsLeenheer/html5test/blob/9106a8/index.html#L845 var supportTest = (function () { var elem = document.createElement('video'); var bool = !!elem.canPlayType; // IE9 Running on Windows Server 2008 can cause an exception to be thrown, bug #224 // End of life for 'mainstream' WS2008 was 2015-1-13. /* istanbul ignore next */ if (bool) { bool = new Boolean(bool); // eslint-disable-line no-new-wrappers bool.ogg = elem.canPlayType('video/ogg; codecs="theora"'); // Without QuickTime, this value will be `undefined`. github.com/Modernizr/Modernizr/issues/546 bool.h264 = elem.canPlayType('video/mp4; codecs="avc1.42E01E"'); bool.webm = elem.canPlayType('video/webm; codecs="vp8, vorbis"'); bool.vp9 = elem.canPlayType('video/webm; codecs="vp9"'); bool.hls = elem.canPlayType('application/x-mpegURL; codecs="avc1.42E01E"'); } return bool; }); function test(support) { // If the test has already passed, return a resolved promise. if (test.HAS_LOCAL_STORAGE && window.localStorage.getItem('odovideoautoplay') === 'true') { return Promise.resolve(true); } // Retrieve the number of autoplay test attempts. Max is 3. var tries = test.HAS_LOCAL_STORAGE && parseInt(window.localStorage.getItem('odovideoautoplaytries'), 10) || 0; if (tries > 2) { return Promise.resolve(false); } /* istanbul ignore next */ return new Promise(function (resolve) { var timeout = void 0; // Chrome can handle 300ms. IE11 requires at least 900 to avoid failing consistenly. var waitTime = 1000; var elem = document.createElement('video'); var complete = function complete(bool) { if (test.HAS_LOCAL_STORAGE) { window.localStorage.setItem('odovideoautoplay', bool); window.localStorage.setItem('odovideoautoplaytries', tries + 1); } resolve(bool); }; var testAutoplay = function testAutoplay(arg) { clearTimeout(timeout); elem.removeEventListener('playing', testAutoplay); complete(arg && arg.type === 'playing' || elem.currentTime !== 0); elem.parentNode.removeChild(elem); }; // Skip the test if video itself, or the autoplay element on it isn't supported. if (!support || !('autoplay' in elem)) { complete(false); return; } // Starting with iOS 10, video[autoplay] videos must be on screen, visible // through css, and inserted into the DOM. // https://webkit.org/blog/6784/new-video-policies-for-ios/ elem.style.cssText = 'position:fixed;top:0;left:0;height:1px;width:1px;opacity:0;'; elem.src = 'data:video/mp4;base64,AAAAFGZ0eXBNU05WAAACAE1TTlYAAAOUbW9vdgAAAGxtdmhkAAAAAM9ghv7PYIb+AAACWAAACu8AAQAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAnh0cmFrAAAAXHRraGQAAAAHz2CG/s9ghv4AAAABAAAAAAAACu8AAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAFAAAAA4AAAAAAHgbWRpYQAAACBtZGhkAAAAAM9ghv7PYIb+AAALuAAANq8AAAAAAAAAIWhkbHIAAAAAbWhscnZpZGVBVlMgAAAAAAABAB4AAAABl21pbmYAAAAUdm1oZAAAAAAAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAVdzdGJsAAAAp3N0c2QAAAAAAAAAAQAAAJdhdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAFAAOABIAAAASAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGP//AAAAEmNvbHJuY2xjAAEAAQABAAAAL2F2Y0MBTUAz/+EAGGdNQDOadCk/LgIgAAADACAAAAMA0eMGVAEABGjuPIAAAAAYc3R0cwAAAAAAAAABAAAADgAAA+gAAAAUc3RzcwAAAAAAAAABAAAAAQAAABxzdHNjAAAAAAAAAAEAAAABAAAADgAAAAEAAABMc3RzegAAAAAAAAAAAAAADgAAAE8AAAAOAAAADQAAAA0AAAANAAAADQAAAA0AAAANAAAADQAAAA0AAAANAAAADQAAAA4AAAAOAAAAFHN0Y28AAAAAAAAAAQAAA7AAAAA0dXVpZFVTTVQh0k/Ou4hpXPrJx0AAAAAcTVREVAABABIAAAAKVcQAAAAAAAEAAAAAAAAAqHV1aWRVU01UIdJPzruIaVz6ycdAAAAAkE1URFQABAAMAAAAC1XEAAACHAAeAAAABBXHAAEAQQBWAFMAIABNAGUAZABpAGEAAAAqAAAAASoOAAEAZABlAHQAZQBjAHQAXwBhAHUAdABvAHAAbABhAHkAAAAyAAAAA1XEAAEAMgAwADAANQBtAGUALwAwADcALwAwADYAMAA2ACAAMwA6ADUAOgAwAAABA21kYXQAAAAYZ01AM5p0KT8uAiAAAAMAIAAAAwDR4wZUAAAABGjuPIAAAAAnZYiAIAAR//eBLT+oL1eA2Nlb/edvwWZflzEVLlhlXtJvSAEGRA3ZAAAACkGaAQCyJ/8AFBAAAAAJQZoCATP/AOmBAAAACUGaAwGz/wDpgAAAAAlBmgQCM/8A6YEAAAAJQZoFArP/AOmBAAAACUGaBgMz/wDpgQAAAAlBmgcDs/8A6YEAAAAJQZoIBDP/AOmAAAAACUGaCQSz/wDpgAAAAAlBmgoFM/8A6YEAAAAJQZoLBbP/AOmAAAAACkGaDAYyJ/8AFBAAAAAKQZoNBrIv/4cMeQ=='; elem.setAttribute('autoplay', ''); elem.setAttribute('muted', ''); elem.setAttribute('playsinline', ''); document.documentElement.appendChild(elem); // Wait for the next tick to add the listener, otherwise the element may // not have time to play in high load situations (e.g. the test suite). setTimeout(function () { elem.addEventListener('playing', testAutoplay); timeout = setTimeout(testAutoplay, waitTime); }, 0); }); } // Support: Safari private browsing. test.HAS_LOCAL_STORAGE = function () { try { var testKey = 'test'; window.localStorage.setItem(testKey, '1'); window.localStorage.removeItem(testKey); return true; } catch (error) /* istanbul ignore next */{ return false; } }(); var classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; var Controls = function () { function Controls() { classCallCheck(this, Controls); } Controls.prototype.createElement = function createElement(type, options) { var el = document.createElement(type); Object.keys(options).forEach(function (k) { el[k] = options[k]; }); return el; }; Controls.prototype.createElements = function createElements() { return { controls: this.createElement('div', { className: settings.Classes.CONTROLS }), playToggle: this.createElement('button', { className: settings.Classes.PLAY_TOGGLE, title: Controls.LABEL.PLAY_TOGGLE }), playControl: this.createElement('span', { className: settings.Classes.PLAY_CONTROL }), pauseControl: this.createElement('span', { className: settings.Classes.PAUSE_CONTROL }), progressContainer: this.createElement('div', { className: settings.Classes.PROGRESS_CONTAINER }), progressHolder: this.createElement('div', { className: settings.Classes.PROGRESS_HOLDER }), buffer: this.createElement('div', { className: settings.Classes.BUFFER }), progress: this.createElement('div', { className: settings.Classes.PROGRESS }), currentTime: this.createElement('div', { className: settings.Classes.CURRENT_TIME }), volumeToggle: this.createElement('button', { className: settings.Classes.VOLUME, title: Controls.LABEL.VOLUME }), muteControl: this.createElement('span', { className: settings.Classes.MUTE_CONTROL, innerHTML: settings.Icons.AUDIO_ON }), unmuteControl: this.createElement('span', { className: settings.Classes.UNMUTE_CONTROL, innerHTML: settings.Icons.AUDIO_OFF }), fullScreenToggle: this.createElement('button', { className: settings.Classes.FULLSCREEN, title: Controls.LABEL.FULLSCREEN }), enterFullscreen: this.createElement('span', { className: settings.Classes.FULLSCREEN_CONTROL, innerHTML: settings.Icons.FULLSCREEN }), exitFullscreen: this.createElement('span', { className: settings.Classes.EXIT_FULLSCREEN_CONTROL, innerHTML: settings.Icons.EXIT_FULLSCREEN }), flexibleSpace: this.createElement('div', { className: settings.Classes.FLEXIBLE_SPACE }) }; }; Controls.prototype.create = function create(style, customFn) { var elements = this.createElements(); elements.playToggle.appendChild(elements.playControl); elements.playToggle.appendChild(elements.pauseControl); elements.volumeToggle.appendChild(elements.muteControl); elements.volumeToggle.appendChild(elements.unmuteControl); elements.fullScreenToggle.appendChild(elements.enterFullscreen); elements.fullScreenToggle.appendChild(elements.exitFullscreen); elements.progressHolder.appendChild(elements.buffer); elements.progressHolder.appendChild(elements.progress); elements.progressContainer.appendChild(elements.progressHolder); switch (style) { case settings.Controls.INLINE_PROGRESS: this._createInline(elements); break; case settings.Controls.NONE: // When no controls are specified, create them anyways. It's easier to hide // the controls and still have them in the DOM than it is to many if-statements // checking if the controls exist before modifying them. this._createInline(elements); elements.controls.classList.add(settings.Classes.CONTROLS_HIDDEN); break; case settings.Controls.STACKED_PROGRESS: this._createStacked(elements); break; case settings.Controls.CUSTOM: customFn(elements); break; // no default } return elements.controls; }; Controls.prototype._createInline = function _createInline(elements) { elements.controls.appendChild(elements.playToggle); elements.controls.appendChild(elements.progressContainer); elements.controls.appendChild(elements.currentTime); elements.controls.appendChild(elements.volumeToggle); elements.controls.appendChild(elements.fullScreenToggle); }; Controls.prototype._createStacked = function _createStacked(elements) { elements.controls.appendChild(elements.progressContainer); elements.controls.appendChild(elements.playToggle); elements.controls.appendChild(elements.flexibleSpace); elements.controls.appendChild(elements.currentTime); elements.controls.appendChild(elements.volumeToggle); elements.controls.appendChild(elements.fullScreenToggle); elements.controls.classList.add(settings.Classes.CONTROLS_STACKED); }; return Controls; }(); Controls.LABEL = { PLAY_TOGGLE: 'toggle video playback.', VOLUME: 'toggle mute for video.', FULLSCREEN: 'toggle video fullscreen mode.' }; /** * Features: * * Autoplay support callbacks. * Skinnable UI controls. * Fullscreen mode. * Click-to-seek. * Preload video. * Dynamically update source. * Hide controls & mouse in fullscreen when idle. * Spacebar pauses video in fullscreen mode. * Function to define control structure. * Callback for updating controls on video progress. */ var Video = function () { function Video(element, options) { classCallCheck(this, Video); /** * @type {Element} */ this.element = element; /** * @type {HTMLVideoElement} */ this.videoEl = this._findVideoElement(); /** * Random id for this instance. */ this.id = Math.random().toString(36).substring(7); /** * Options for this instance. * @type {object} */ this.options = Object.assign({}, Video.Defaults, options); this.isPlaying = this._isPlaying(); this.isFullscreen = false; this._idleTimeout = null; /** * Whether the browser can go in fullscreen mode. This will be true for iOS * because the video element can go fullscreen, but false for IE<11. * @private {boolean} */ this._noFullscreen = false; this.element.classList.toggle(Video.Classes.NO_FLEXBOX, Video.NO_FLEXBOX); // Set id attributes for each source. this._setSourceIds(); this._createControls(); this._saveElements(); this.bindEvents(); if (this._isMetadataLoaded()) { this._setProgressDisplay(); this._setBufferDisplay(); } this.autoplay = Promise.resolve(Video.autoplay); } /** * Bind the correct `this` context to each event listener. The native `bind` * method returns a different function each time `bind` is called, so the value * must be saved in order to remove the correct listener. * @protected */ Video.prototype._bindListeners = function _bindListeners() { this._onMetadataLoaded = this._handleMetadataLoaded.bind(this); this._onClick = this._handleClick.bind(this); this._onPlay = this._handlePlaying.bind(this); this._onPause = this._handlePaused.bind(this); this._onTimeUpdate = this._handleTimeUpdate.bind(this); this._onProgress = this._handleProgress.bind(this); this._onFullscreenToggle = this.toggleFullscreen.bind(this); this._onFullscreenChange = this._fullscreenChanged.bind(this); this._onVolumeToggle = this.toggleVolume.bind(this); this._onProgressClick = this._handleProgressClick.bind(this); this._onSeeking = this._handleSeeking.bind(this); this._onSeeked = this._handleSeeked.bind(this); this._onMouseMove = this._returnFromIdle.bind(this); this._onIdleTimeout = this._wentIdle.bind(this); this._onKeyboardPlaybackToggle = this._handleKeyboardPlaybackToggle.bind(this); }; /** * Add event listeners to the video and UI elements. * @protected */ Video.prototype.bindEvents = function bindEvents() { this._bindListeners(); this._waitForMetadata(); if (this.options.pauseOnClick) { this.videoEl.addEventListener('click', this._onClick); } // Sent when playback is paused. this.videoEl.addEventListener('pause', this._onPause); // Sent when the media begins to play (either for the first time, after having // been paused, or after ending and then restarting). this.videoEl.addEventListener('playing', this._onPlay); this.videoEl.addEventListener('timeupdate', this._onTimeUpdate); this.videoEl.addEventListener('progress', this._onProgress); // this.videoEl.addEventListener('volumechange', this._onVolumeChange); this.videoEl.addEventListener('seeking', this._onSeeking); this.videoEl.addEventListener('seeked', this._onSeeked); this.getElementByClass(Video.Classes.PLAY_TOGGLE).addEventListener('click', this._onClick); this.getElementByClass(Video.Classes.VOLUME).addEventListener('click', this._onVolumeToggle); this.getElementByClass(Video.Classes.PROGRESS_HOLDER).addEventListener('click', this._onProgressClick); this.getElementByClass(Video.Classes.FULLSCREEN).addEventListener('click', this._onFullscreenToggle); if (Video.screenfull.enabled) { document.addEventListener(Video.screenfull.raw.fullscreenchange, this._onFullscreenChange); document.addEventListener(Video.screenfull.raw.fullscreenerror, this._onFullscreenChange); // iOS handles full screen differently. } else if (this.videoEl.webkitSupportsFullscreen) { this.videoEl.addEventListener('webkitbeginfullscreen', this._onFullscreenChange); this.videoEl.addEventListener('webkitendfullscreen', this._onFullscreenChange); } else { this._noFullscreen = true; } }; /** * Checks the network state of the video element. Returns true if the metadata * for the video is present. * @return {boolean} * @private */ Video.prototype._isMetadataLoaded = function _isMetadataLoaded() { return this.videoEl.readyState > 0; }; /** * Add the `loadedmetadata` event listener if the video has not loaded yet, or * call the event handler directly if it has. * @private */ Video.prototype._waitForMetadata = function _waitForMetadata() { if (this._isMetadataLoaded()) { this._handleMetadataLoaded(); } else { this.videoEl.addEventListener('loadedmetadata', this._onMetadataLoaded); } }; /** * Save references to UI elements which needs to be constantly updated. * @protected */ Video.prototype._saveElements = function _saveElements() { this.currentTimeEl = this.getElementByClass(Video.Classes.CURRENT_TIME); this.progressEl = this.getElementByClass(Video.Classes.PROGRESS); this.bufferEl = this.getElementByClass(Video.Classes.BUFFER); }; /** * Separated into its own function so sinon can stub it in the tests. * @return {HTMLVideoElement} * @private */ Video.prototype._findVideoElement = function _findVideoElement() { return this.element.getElementsByTagName('video')[0]; }; /** * Whether the video is currently playing. * @return {boolean} */ Video.prototype._isPlaying = function _isPlaying() { if (this.videoEl.ended || this.videoEl.paused) { return false; } return true; }; /** * For each <source> element, set a unique id on it. * @private */ Video.prototype._setSourceIds = function _setSourceIds() { var _this = this; this.getSourceElements().forEach(function (source) { source.id = _this.id + source.type.split('/')[1]; }); }; /** * Retrieve all <source> elements within the video. * @return {Array.<HTMLSourceElement>} */ Video.prototype.getSourceElements = function getSourceElements() { return Array.from(this.videoEl.getElementsByTagName('source')); }; /** * Return the main element this component was instantiated with. * @return {Element} */ Video.prototype.getElement = function getElement() { return this.element; }; /** * Returns the <video> element associated with this component. * @return {HTMLVideoElement} */ Video.prototype.getVideoElement = function getVideoElement() { return this.videoEl; }; /** * Retrieve an element from within the main element by a class name. * @param {string} className Class name to search for. * @return {?Element} The element or null if it isn't found. */ Video.prototype.getElementByClass = function getElementByClass(className) { return this.element.getElementsByClassName(className)[0]; }; /** * Play the current video. * https://googlechrome.github.io/samples/play-return-promise/ * @return {?Promise<void>} Chrome 50+, Firefox 53+, and Safari 10+ return a * promise which is rejected if the device cannot autoplay. */ Video.prototype.play = function play() { return this.videoEl.play(); }; /** * Pause the currently playing video. */ Video.prototype.pause = function pause() { this.videoEl.pause(); }; /** * If playing, pause. If paused, play. */ Video.prototype.togglePlayback = function togglePlayback() { if (this.isPlaying) { this.pause(); } else { this.play(); } }; /** * Get the current time, in seconds. * @return {number} */ Video.prototype.getCurrentTime = function getCurrentTime() { return this.videoEl.currentTime; }; /** * Go to a specified time in the video. * @param {number} time Time in seconds. */ Video.prototype.setCurrentTime = function setCurrentTime(time) { this.videoEl.currentTime = time; }; /** * Mute the video. */ Video.prototype.mute = function mute() { this.videoEl.volume = 0; this.element.classList.add(Video.Classes.IS_MUTED); }; /** * Unmute the video. */ Video.prototype.unmute = function unmute() { this.videoEl.volume = 1; this.element.classList.remove(Video.Classes.IS_MUTED); }; /** * Whether the video is currently muted. * @return {boolean} */ Video.prototype.isMuted = function isMuted() { return this.videoEl.volume === 0; }; /** * Mute the video if it's playing audio or unmute it if it's already muted. */ Video.prototype.toggleVolume = function toggleVolume() { if (this.isMuted()) { this.unmute(); } else { this.mute(); } }; /** * Update the current video source. This method changes the appropriate <source> * `src` property. * * IE9 does not work when the <video>s `src` property or attribute is updated. * * @param {string} src Absolute or relative path to the video, without the extension. * e.g. "/videos/cool-feature". */ Video.prototype.updateSource = function updateSource(src) { var _this2 = this; var _Video$getVideoType = Video.getVideoType(), extension = _Video$getVideoType.extension; var sources = this.getSourceElements(); // Find the <source> element by Id. var source = sources.filter(function (source) { return source.id === _this2.id + extension; })[0]; var videoSource = src + '.' + extension; // Update the source's src. source.setAttribute('src', videoSource); this.videoEl.load(); // Reset progress bars. this._setProgressDisplay(); this._setBufferDisplay(); // When metadata loads, update the times. this._waitForMetadata(); }; /** * Show the video is currently buffering. * @protected */ Video.prototype.showBuffering = function showBuffering() { this.element.classList.add(Video.Classes.IS_BUFFERING); }; /** * Show the video is done buffering. * @protected */ Video.prototype.hideBuffering = function hideBuffering() { this.element.classList.remove(Video.Classes.IS_BUFFERING); }; /** * Converts a time value to MMSS, so 91 seconds is "1:31". * @param {number} currentTime Time in seconds. * @return {string} Human readable time. */ Video.getPrettyTime = function getPrettyTime(currentTime) { var hours = Math.floor(currentTime / 3600); var minutes = Math.floor((currentTime - hours * 3600) / 60); var seconds = currentTime - hours * 3600 - minutes * 60; if (seconds < 10) { seconds = '0' + seconds; } var time = minutes + ':' + seconds; if (hours > 0) { if (minutes < 10) { time = '0' + time; } time = hours + ':' + time; } return time; }; /** * Generate the controls elemenet and append it to the main element. * @private */ Video.prototype._createControls = function _createControls() { var controls = new Video.ControlsCreator(); var mainElement = controls.create(this.options.controls, this.options.layoutControls); this.element.appendChild(mainElement); }; /** * Update the controls state/display with the new current time. * @private */ Video.prototype._updateControls = function _updateControls() { var seconds = this.getCurrentTime(); this._setCurrentTimeDisplay(seconds); this._setProgressDisplay(); // Give a hook to update custom controls. if (this.options.updateControls) { this.options.updateControls(seconds); } }; /** * Sets the current time element's text to a pretty time given a float. * @param {number} seconds The current time of the video. */ Video.prototype._setCurrentTimeDisplay = function _setCurrentTimeDisplay(seconds) { this.currentTimeEl.textContent = Video.getPrettyTime(Math.round(seconds)); }; /** * Set the progress elements to a percentage. * @param {number} percentage Number between 0 and 1. If undefined, the percentage * will be calculated from the video's current time and duration. * @private */ Video.prototype._setProgressDisplay = function _setProgressDisplay() { var percentage = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.videoEl.currentTime / this.videoEl.duration; this.progressEl.style.width = percentage * 100 + '%'; }; /** * Determine the amount the video has buffered and set the buffer element's width * as that percentage. * @private */ Video.prototype._setBufferDisplay = function _setBufferDisplay() { // Progress events can be emitted before there is anything buffered. var percentage = this.videoEl.buffered.length > 0 ? this.videoEl.buffered.end(0) / this.videoEl.duration : 0; this.bufferEl.style.width = percentage * 100 + '%'; }; /** * Clicked the fullscreen icon. Request or exit from fullscreen. */ Video.prototype.toggleFullscreen = function toggleFullscreen() { if (Video.screenfull) { // Handle it the standardized way. Video.screenfull.toggle(this.element); } else if (this._noFullscreen) { // Support: IE<11. Since there won't be any entered/exited fullscreen events, // that callback must be invoked immediately. this._fullscreenChanged(); } else { // Since the native video controls will be shown and not the Video // custom controls, it can be assumed that this event handler will only // be triggered by requesting fullscreen. this.videoEl.webkitEnterFullscreen(); } }; /** * Event handler for both fullscreenchange and fullscreenerror. The state of * fullscreen has changed and styles need to be updated. * @private */ Video.prototype._fullscreenChanged = function _fullscreenChanged() { this.isFullscreen = !this.isFullscreen; this._toggleFullscreenState(); }; Video.prototype._toggleFullscreenState = function _toggleFullscreenState() { this.element.classList.toggle(Video.Classes.IS_FULLSCREEN, this.isFullscreen); if (this.isFullscreen) { this._startIdleTimer(); // Take focus off the fullscreen button because the spacebar should pause // the video, but when it's focused, it will exit fullscreen. document.activeElement.blur(); this.element.focus(); this.element.addEventListener('mousemove', this._onMouseMove); // Use keyup instead of keypress because special keys (control, shift, alt, etc.) // do not trigger the keypress event. document.addEventListener('keyup', this._onKeyboardPlaybackToggle); } else { this._returnFromIdle(); // Return from idle starts a new timer, which isn't wanted when closing fullscreen. clearTimeout(this._idleTimeout); this.element.removeEventListener('mousemove', this._onMouseMove); document.removeEventListener('keyup', this._onKeyboardPlaybackToggle); } }; /** * When the user stops interacting with the page, the cursor should disappear and * the controls should be hidden. * @private */ Video.prototype._wentIdle = function _wentIdle() { this.element.classList.add(Video.Classes.IS_IDLE); }; /** * When returning from the idle state, the cursor and controls should be shown. * Another timer is started to wait for idle state again. * @private */ Video.prototype._returnFromIdle = function _returnFromIdle() { clearTimeout(this._idleTimeout); this._startIdleTimer(); this.element.classList.remove(Video.Classes.IS_IDLE); }; /** * Sets a timeout which will trigger the `wentIdle` method when it expires. * @private */ Video.prototype._startIdleTimer = function _startIdleTimer() { this._idleTimeout = setTimeout(this._onIdleTimeout, Video.IDLE_TIMEOUT); }; /** * Calculate the position of the click relative to the element the click listener * was bound to. * @param {MouseEvent} event Event object. * @return {{x: number, y: number}} * @private */ Video._getClickOffset = function _getClickOffset(event) { if ('offsetX' in event) { return { x: event.offsetX, y: event.offsetY }; } var rect = event.currentTarget.getBoundingClientRect(); return { x: event.clientX - rect.left, y: event.clientY - rect.top }; }; /** * Event handler for the `loadedmetadata` event. Update UI with new data. * @private */ Video.prototype._handleMetadataLoaded = function _handleMetadataLoaded() { this.videoEl.removeEventListener('loadedmetadata', this._onMetadataLoaded); this._setCurrentTimeDisplay(this.videoEl.duration); }; /** * Clicked on the main element. Toggle video playback. */ Video.prototype._handleClick = function _handleClick() { this.togglePlayback(); }; /** * Event handler for the `playing` event. Add the is-playing class. * @private */ Video.prototype._handlePlaying = function _handlePlaying() { this.isPlaying = true; this.element.classList.toggle(Video.Classes.IS_PLAYING, this.isPlaying); }; /** * Event handler for the `paused` event. Remove the is-playing class. * @private */ Video.prototype._handlePaused = function _handlePaused() { this.isPlaying = false; this.element.classList.toggle(Video.Classes.IS_PLAYING, this.isPlaying); }; /** * Event handler for the `timeupdate` event. Update the UI. * @private */ Video.prototype._handleTimeUpdate = function _handleTimeUpdate() { this._updateControls(); }; /** * Event handler for the `progress` event. Update the buffer display bar. * @private */ Video.prototype._handleProgress = function _handleProgress() { this._setBufferDisplay(); }; /** * Clicked the progress bar track. Seek to the location of the click. * @param {MouseEvent} evt Event object. * @private */ Video.prototype._handleProgressClick = function _handleProgressClick(evt) { var offset = Video._getClickOffset(evt); var width = evt.currentTarget.offsetWidth; var percent = offset.x / width; this._setProgressDisplay(percent); this.setCurrentTime(percent * this.videoEl.duration); }; /** * Video started seeking. Show the user it's buffering. * @private */ Video.prototype._handleSeeking = function _handleSeeking() { // Avoid adding the buffering class when the video is looping - issue #1 if (this.videoEl.buffered.length === 0 || this.videoEl.buffered.end(0) < this.videoEl.currentTime) { this.showBuffering(); } }; /** * Video stopped seeking. Show the user it's ready to play. * @private */ Video.prototype._handleSeeked = function _handleSeeked() { this.hideBuffering(); }; /** * If the spacebar is pressed, toggle playback. * @param {KeyboardEvent} evt Event object. * @private */ Video.prototype._handleKeyboardPlaybackToggle = function _handleKeyboardPlaybackToggle(evt) { // Trigger the controls to show again. this._returnFromIdle(); // Spacebar. if (Video._getWhichKey(evt) === 32) { this.togglePlayback(); evt.preventDefault(); } }; /** * Crossbrowser way to get the key which was pressed in a keyboard event. * @param {KeyboardEvent} event Event object. * @return {number} * @private */ Video._getWhichKey = function _getWhichKey(event) { if (event.which) { return event.which; } return event.charCode || event.keyCode; }; /** * Handler for triggering events on readyState or video data events * @param {Object} event Object containing event name and readyState. * @param {function():void} cb Callback function for event. * @public */ Video.prototype.listenOnData = function listenOnData(event, cb) { var _this3 = this; var _loaded = void 0; if (this.videoEl.readyState > event.readyState) { cb(); } else { this.videoEl.addEventListener(event.name, _loaded = function loaded() { _this3.videoEl.removeEventListener(event.name, _loaded); cb(); }); } }; /** * Remove all event listeners, references to DOM elements, and generated elements. */ Video.prototype.dispose = function dispose() { this.videoEl.removeEventListener('click', this._onClick); this.videoEl.removeEventListener('pause', this._onPause); this.videoEl.removeEventListener('playing', this._onPlay); this.videoEl.removeEventListener('timeupdate', this._onTimeUpdate); this.videoEl.removeEventListener('progress', this._onProgress); this.videoEl.removeEventListener('seeking', this._onSeeking); this.videoEl.removeEventListener('seeked', this._onSeeked); this.videoEl.removeEventListener('loadedmetadata', this._onMetadataLoaded); this.getElementByClass(Video.Classes.PLAY_TOGGLE).removeEventListener('click', this._onClick); this.getElementByClass(Video.Classes.VOLUME).removeEventListener('click', this._onVolumeToggle); this.getElementByClass(Video.Classes.PROGRESS_HOLDER).removeEventListener('click', this._onProgressClick); this.getElementByClass(Video.Classes.FULLSCREEN).removeEventListener('click', this._onFullscreenToggle); var fullscreenChange = this._onFullscreenChange; if (Video.screenfull.enabled) { document.removeEventListener(Video.screenfull.raw.fullscreenchange, fullscreenChange); document.removeEventListener(Video.screenfull.raw.fullscreenerror, fullscreenChange); } else if (this.videoEl.webkitSupportsFullscreen) { this.videoEl.removeEventListener('webkitbeginfullscreen', fullscreenChange); this.videoEl.removeEventListener('webkitendfullscreen', fullscreenChange); } this.element.removeChild(this.getElementByClass(Video.Classes.CONTROLS)); this.currentTimeEl = null; this.progressEl = null; this.bufferEl = null; this.element = null; this.videoEl = null; }; /** * Get the media type and extension for the currently supported video types. * @return {{extension: string, type: string}} */ Video.getVideoType = function getVideoType() { // Webm is best. if (Video.support.webm) { return { extension: 'webm', type: 'video/webm' }; } return { extension: 'mp4', type: 'video/mp4' }; }; return Video; }(); /* Merge in settings */ Object.assign(Video, settings); /** * Determine support for videos. It is a boolean value with properties for * h264, webm, vp9, ogg, hls. * @type {Boolean} */ Video.support = supportTest(); /** * An async test for autoplay feature. * @type {Promise} */ Video.autoplay = test(Video.support); Video._autoplayTest = test; /** @type {Controls} */ Video.ControlsCreator = Controls; /** @type {boolean} IE9 is the only supported browser without flexbox. */ /* istanbul ignore next */ Video.NO_FLEXBOX = document.all && document.addEventListener && !window.atob; /** @type {boolean|Object} Expose for testing. */ Video.screenfull = screenfull; return Video; }))); //# sourceMappingURL=odo-video.js.map