diff options
Diffstat (limited to 'dist/plyr.js')
-rw-r--r-- | dist/plyr.js | 2943 |
1 files changed, 1503 insertions, 1440 deletions
diff --git a/dist/plyr.js b/dist/plyr.js index 71aa50d4..13286229 100644 --- a/dist/plyr.js +++ b/dist/plyr.js @@ -216,7 +216,7 @@ var defaults = { 'statechange', 'qualitychange', 'qualityrequested', // Ads - 'adsloaded', 'adscontentpause', 'adsconentresume', 'adstarted', 'adsmidpoint', 'adscomplete', 'adsallcomplete', 'adsimpression', 'adsclick'], + 'adsloaded', 'adscontentpause', 'adscontentresume', 'adstarted', 'adsmidpoint', 'adscomplete', 'adsallcomplete', 'adsimpression', 'adsclick'], // Selectors // Change these to match your template if using custom HTML @@ -323,9 +323,10 @@ var defaults = { }, // Advertisements plugin - // Tag is not required as publisher is determined by vi.ai using the domain + // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio ads: { - enabled: false + enabled: false, + publisherId: null } }; @@ -690,51 +691,51 @@ var utils = { // Load an external script - loadScript: function loadScript(url, callback, error) { - var current = document.querySelector('script[src="' + url + '"]'); + loadScript: function loadScript(url) { + return new Promise(function (resolve, reject) { + var current = document.querySelector('script[src="' + url + '"]'); - // Check script is not already referenced, if so wait for load - if (current !== null) { - current.callbacks = current.callbacks || []; - current.callbacks.push(callback); - return; - } + // Check script is not already referenced, if so wait for load + if (current !== null) { + current.callbacks = current.callbacks || []; + current.callbacks.push(resolve); + return; + } - // Build the element - var element = document.createElement('script'); + // Build the element + var element = document.createElement('script'); - // Callback queue - element.callbacks = element.callbacks || []; - element.callbacks.push(callback); + // Callback queue + element.callbacks = element.callbacks || []; + element.callbacks.push(resolve); - // Error queue - element.errors = element.errors || []; - element.errors.push(error); + // Error queue + element.errors = element.errors || []; + element.errors.push(reject); - // Bind callback - if (utils.is.function(callback)) { + // Bind callback element.addEventListener('load', function (event) { element.callbacks.forEach(function (cb) { return cb.call(null, event); }); element.callbacks = null; }, false); - } - // Bind error handling - element.addEventListener('error', function (event) { - element.errors.forEach(function (err) { - return err.call(null, event); - }); - element.errors = null; - }, false); + // Bind error handling + element.addEventListener('error', function (event) { + element.errors.forEach(function (err) { + return err.call(null, event); + }); + element.errors = null; + }, false); - // Set the URL after binding callback - element.src = url; + // Set the URL after binding callback + element.src = url; - // Inject - var first = document.getElementsByTagName('script')[0]; - first.parentNode.insertBefore(element, first); + // Inject + var first = document.getElementsByTagName('script')[0]; + first.parentNode.insertBefore(element, first); + }); }, @@ -1916,1300 +1917,236 @@ var Fullscreen = function () { }(); // ========================================================================== -// Plyr storage -// ========================================================================== - -var Storage = function () { - function Storage(player) { - classCallCheck(this, Storage); - - this.enabled = player.config.storage.enabled; - this.key = player.config.storage.key; - } - - // Check for actual support (see if we can use it) - - - createClass(Storage, [{ - key: 'get', - value: function get$$1(key) { - var store = window.localStorage.getItem(this.key); - - if (!Storage.supported || utils.is.empty(store)) { - return null; - } - - var json = JSON.parse(store); - - return utils.is.string(key) && key.length ? json[key] : json; - } - }, { - key: 'set', - value: function set$$1(object) { - // Bail if we don't have localStorage support or it's disabled - if (!Storage.supported || !this.enabled) { - return; - } - - // Can only store objectst - if (!utils.is.object(object)) { - return; - } - - // Get current storage - var storage = this.get(); - - // Default to empty object - if (utils.is.empty(storage)) { - storage = {}; - } - - // Update the working copy of the values - utils.extend(storage, object); - - // Update storage - window.localStorage.setItem(this.key, JSON.stringify(storage)); - } - }], [{ - key: 'supported', - get: function get$$1() { - if (!('localStorage' in window)) { - return false; - } - - var test = '___test'; - - // Try to use it (it might be disabled, e.g. user is in private mode) - // see: https://github.com/sampotts/plyr/issues/131 - try { - window.localStorage.setItem(test, test); - window.localStorage.removeItem(test); - return true; - } catch (e) { - return false; - } - } - }]); - return Storage; -}(); - -// ========================================================================== -// Advertisement plugin using Google IMA HTML5 SDK -// Create an account with our ad partner, vi here: -// https://www.vi.ai/publisher-video-monetization/ +// Plyr Captions +// TODO: Create as class // ========================================================================== -/* global google */ - -var getTagUrl = function getTagUrl() { - var params = { - AV_PUBLISHERID: '58c25bb0073ef448b1087ad6', - AV_CHANNELID: '5a0458dc28a06145e4519d21', - AV_URL: '127.0.0.1:3000', - cb: 1, - AV_WIDTH: 640, - AV_HEIGHT: 480 - }; - - var base = 'https://go.aniview.com/api/adserver6/vast/'; - - return base + '?' + utils.buildUrlParams(params); -}; - -var Ads = function () { - /** - * Ads constructor. - * @param {object} player - * @return {Ads} - */ - function Ads(player) { - var _this = this; - - classCallCheck(this, Ads); - - this.player = player; - this.enabled = player.config.ads.enabled; - this.playing = false; - this.initialized = false; - this.blocked = false; - this.enabled = utils.is.url(player.config.ads.tag); - - // Check if a tag URL is provided. - if (!this.enabled) { +var captions = { + // Setup captions + setup: function setup() { + // Requires UI support + if (!this.supported.ui) { return; } - // Check if the Google IMA3 SDK is loaded or load it ourselves - if (!utils.is.object(window.google)) { - utils.loadScript(player.config.urls.googleIMA.api, function () { - _this.ready(); - }, function () { - // Script failed to load or is blocked - _this.blocked = true; - _this.player.debug.log('Ads error: Google IMA SDK failed to load'); - }); - } else { - this.ready(); - } - } - - /** - * Get the ads instance ready. - */ - - - createClass(Ads, [{ - key: 'ready', - value: function ready() { - var _this2 = this; - - this.elements = { - container: null, - displayContainer: null - }; - this.manager = null; - this.loader = null; - this.cuePoints = null; - this.events = {}; - this.safetyTimer = null; - this.countdownTimer = null; - - // Set listeners on the Plyr instance - this.listeners(); - - // Start ticking our safety timer. If the whole advertisement - // thing doesn't resolve within our set time; we bail - this.startSafetyTimer(12000, 'ready()'); - - // Setup a simple promise to resolve if the IMA loader is ready - this.loaderPromise = new Promise(function (resolve) { - _this2.on('ADS_LOADER_LOADED', function () { - return resolve(); - }); - }); - - // Setup a promise to resolve if the IMA manager is ready - this.managerPromise = new Promise(function (resolve) { - _this2.on('ADS_MANAGER_LOADED', function () { - return resolve(); - }); - }); - - // Clear the safety timer - this.managerPromise.then(function () { - _this2.clearSafetyTimer('onAdsManagerLoaded()'); - }); + // Set default language if not set + var stored = this.storage.get('language'); - // Setup the IMA SDK - this.setupIMA(); + if (!utils.is.empty(stored)) { + this.captions.language = stored; } - /** - * In order for the SDK to display ads for our video, we need to tell it where to put them, - * so here we define our ad container. This div is set up to render on top of the video player. - * Using the code below, we tell the SDK to render ads within that div. We also provide a - * handle to the content video player - the SDK will poll the current time of our player to - * properly place mid-rolls. After we create the ad display container, we initialize it. On - * mobile devices, this initialization is done as the result of a user action. - */ - - }, { - key: 'setupIMA', - value: function setupIMA() { - // Create the container for our advertisements - this.elements.container = utils.createElement('div', { - class: this.player.config.classNames.ads, - hidden: '' - }); - this.player.elements.container.appendChild(this.elements.container); - - // So we can run VPAID2 - google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED); - - // Set language - google.ima.settings.setLocale(this.player.config.ads.language); - - // We assume the adContainer is the video container of the plyr element - // that will house the ads - this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container); - - // Request video ads to be pre-loaded - this.requestAds(); + if (utils.is.empty(this.captions.language)) { + this.captions.language = this.config.captions.language.toLowerCase(); } - /** - * Request advertisements - */ - - }, { - key: 'requestAds', - value: function requestAds() { - var _this3 = this; - - var container = this.player.elements.container; - - - try { - // Create ads loader - this.loader = new google.ima.AdsLoader(this.elements.displayContainer); - - // Listen and respond to ads loaded and error events - this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, function (event) { - return _this3.onAdsManagerLoaded(event); - }, false); - this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, function (error) { - return _this3.onAdError(error); - }, false); - - // Request video ads - var request = new google.ima.AdsRequest(); - request.adTagUrl = getTagUrl(); - - // Specify the linear and nonlinear slot sizes. This helps the SDK - // to select the correct creative if multiple are returned - request.linearAdSlotWidth = container.offsetWidth; - request.linearAdSlotHeight = container.offsetHeight; - request.nonLinearAdSlotWidth = container.offsetWidth; - request.nonLinearAdSlotHeight = container.offsetHeight; - - // We only overlay ads as we only support video. - request.forceNonLinearFullSlot = false; - - this.loader.requestAds(request); + // Set captions enabled state if not set + if (!utils.is.boolean(this.captions.active)) { + var active = this.storage.get('captions'); - this.handleEventListeners('ADS_LOADER_LOADED'); - } catch (e) { - this.onAdError(e); + if (utils.is.boolean(active)) { + this.captions.active = active; + } else { + this.captions.active = this.config.captions.active; } } - /** - * Update the ad countdown - * @param {boolean} start - */ - - }, { - key: 'pollCountdown', - value: function pollCountdown() { - var _this4 = this; - - var start = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; - - if (!start) { - window.clearInterval(this.countdownTimer); - this.elements.container.removeAttribute('data-badge-text'); - return; + // Only Vimeo and HTML5 video supported at this point + if (!this.isVideo || this.isYouTube || this.isHTML5 && !support.textTracks) { + // Clear menu and hide + if (utils.is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) { + controls.setCaptionsMenu.call(this); } - var update = function update() { - var time = utils.formatTime(_this4.manager.getRemainingTime()); - var label = _this4.player.config.i18n.advertisement + ' - ' + time; - _this4.elements.container.setAttribute('data-badge-text', label); - }; - - this.countdownTimer = window.setInterval(update, 100); - } - - /** - * This method is called whenever the ads are ready inside the AdDisplayContainer - * @param {Event} adsManagerLoadedEvent - */ - - }, { - key: 'onAdsManagerLoaded', - value: function onAdsManagerLoaded(adsManagerLoadedEvent) { - var _this5 = this; - - // Get the ads manager - var settings = new google.ima.AdsRenderingSettings(); - - // Tell the SDK to save and restore content video state on our behalf - settings.restoreCustomPlaybackStateOnAdBreakComplete = true; - settings.enablePreloading = true; - - // The SDK is polling currentTime on the contentPlayback. And needs a duration - // so it can determine when to start the mid- and post-roll - this.manager = adsManagerLoadedEvent.getAdsManager(this.player, settings); - - // Get the cue points for any mid-rolls by filtering out the pre- and post-roll - this.cuePoints = this.manager.getCuePoints(); - - // Add advertisement cue's within the time line if available - this.cuePoints.forEach(function (cuePoint) { - if (cuePoint !== 0 && cuePoint !== -1) { - var seekElement = _this5.player.elements.progress; - - if (seekElement) { - var cuePercentage = 100 / _this5.player.duration * cuePoint; - var cue = utils.createElement('span', { - class: _this5.player.config.classNames.cues - }); - - cue.style.left = cuePercentage.toString() + '%'; - seekElement.appendChild(cue); - } - } - }); - - // Get skippable state - // TODO: Skip button - // this.manager.getAdSkippableState(); - - // Set volume to match player - this.manager.setVolume(this.player.volume); - - // Add listeners to the required events - // Advertisement error events - this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, function (error) { - return _this5.onAdError(error); - }); - - // Advertisement regular events - Object.keys(google.ima.AdEvent.Type).forEach(function (type) { - _this5.manager.addEventListener(google.ima.AdEvent.Type[type], function (event) { - return _this5.onAdEvent(event); - }); - }); - - // Resolve our adsManager - this.handleEventListeners('ADS_MANAGER_LOADED'); - } - - /** - * This is where all the event handling takes place. Retrieve the ad from the event. Some - * events (e.g. ALL_ADS_COMPLETED) don't have the ad object associated - * https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type - * @param {Event} event - */ - - }, { - key: 'onAdEvent', - value: function onAdEvent(event) { - var _this6 = this; - - var container = this.player.elements.container; - - // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED) - // don't have ad object associated - - var ad = event.getAd(); - - // Proxy event - var dispatchEvent = function dispatchEvent(type) { - utils.dispatchEvent.call(_this6.player, _this6.player.media, 'ads' + type); - }; - - switch (event.type) { - case google.ima.AdEvent.Type.LOADED: - // This is the first event sent for an ad - it is possible to determine whether the - // ad is a video ad or an overlay - this.handleEventListeners('LOADED'); - - // Bubble event - dispatchEvent('loaded'); - - // Start countdown - this.pollCountdown(true); - - if (!ad.isLinear()) { - // Position AdDisplayContainer correctly for overlay - ad.width = container.offsetWidth; - ad.height = container.offsetHeight; - } - - // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex()); - // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset()); - break; - - case google.ima.AdEvent.Type.ALL_ADS_COMPLETED: - // All ads for the current videos are done. We can now request new advertisements - // in case the video is re-played - this.handleEventListeners('ALL_ADS_COMPLETED'); - - // Fire event - dispatchEvent('allcomplete'); - - // TODO: Example for what happens when a next video in a playlist would be loaded. - // So here we load a new video when all ads are done. - // Then we load new ads within a new adsManager. When the video - // Is started - after - the ads are loaded, then we get ads. - // You can also easily test cancelling and reloading by running - // player.ads.cancel() and player.ads.play from the console I guess. - // this.player.source = { - // type: 'video', - // title: 'View From A Blue Moon', - // sources: [{ - // src: - // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', type: - // 'video/mp4', }], poster: - // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', tracks: - // [ { kind: 'captions', label: 'English', srclang: 'en', src: - // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt', - // default: true, }, { kind: 'captions', label: 'French', srclang: 'fr', src: - // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', }, ], - // }; - - // TODO: So there is still this thing where a video should only be allowed to start - // playing when the IMA SDK is ready or has failed - - this.loadAds(); - break; - - case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED: - // This event indicates the ad has started - the video player can adjust the UI, - // for example display a pause button and remaining time. Fired when content should - // be paused. This usually happens right before an ad is about to cover the content - this.handleEventListeners('CONTENT_PAUSE_REQUESTED'); - - dispatchEvent('contentpause'); - - this.pauseContent(); - - break; - - case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED: - // This event indicates the ad has finished - the video player can perform - // appropriate UI actions, such as removing the timer for remaining time detection. - // Fired when content should be resumed. This usually happens when an ad finishes - // or collapses - this.handleEventListeners('CONTENT_RESUME_REQUESTED'); - - dispatchEvent('contentresume'); - - this.pollCountdown(); - - this.resumeContent(); - - break; - - case google.ima.AdEvent.Type.STARTED: - dispatchEvent('started'); - break; - - case google.ima.AdEvent.Type.MIDPOINT: - dispatchEvent('midpoint'); - break; - - case google.ima.AdEvent.Type.COMPLETE: - dispatchEvent('complete'); - break; - - case google.ima.AdEvent.Type.IMPRESSION: - dispatchEvent('impression'); - break; - - case google.ima.AdEvent.Type.CLICK: - dispatchEvent('click'); - break; - - default: - break; - } + return; } + // Inject the container + if (!utils.is.element(this.elements.captions)) { + this.elements.captions = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.captions)); - /** - * Any ad error handling comes through here - * @param {Event} event - */ - - }, { - key: 'onAdError', - value: function onAdError(event) { - this.cancel(); - this.player.debug.log('Ads error', event); + utils.insertAfter(this.elements.captions, this.elements.wrapper); } - /** - * Setup hooks for Plyr and window events. This ensures - * the mid- and post-roll launch at the correct time. And - * resize the advertisement when the player resizes - */ - - }, { - key: 'listeners', - value: function listeners() { - var _this7 = this; - - var container = this.player.elements.container; - - var time = void 0; - - // Add listeners to the required events - this.player.on('ended', function () { - _this7.loader.contentComplete(); - }); - - this.player.on('seeking', function () { - time = _this7.player.currentTime; - return time; - }); - - this.player.on('seeked', function () { - var seekedTime = _this7.player.currentTime; + // Set the class hook + utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(captions.getTracks.call(this))); - _this7.cuePoints.forEach(function (cuePoint, index) { - if (time < cuePoint && cuePoint < seekedTime) { - _this7.manager.discardAdBreak(); - _this7.cuePoints.splice(index, 1); - } - }); - }); + // Get tracks + var tracks = captions.getTracks.call(this); - // Listen to the resizing of the window. And resize ad accordingly - // TODO: eventually implement ResizeObserver - window.addEventListener('resize', function () { - _this7.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL); - }); + // If no caption file exists, hide container for caption text + if (utils.is.empty(tracks)) { + return; } - /** - * Initialize the adsManager and start playing advertisements - */ - - }, { - key: 'play', - value: function play() { - var _this8 = this; - - var container = this.player.elements.container; - - - if (!this.managerPromise) { - return; - } - - // Play the requested advertisement whenever the adsManager is ready - this.managerPromise.then(function () { - // Initialize the container. Must be done via a user action on mobile devices - _this8.elements.displayContainer.initialize(); + // Get browser info + var browser = utils.getBrowser(); - try { - if (!_this8.initialized) { - // Initialize the ads manager. Ad rules playlist will start at this time - _this8.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL); + // Fix IE captions if CORS is used + // Fetch captions and inject as blobs instead (data URIs not supported!) + if (browser.isIE && window.URL) { + var elements = this.media.querySelectorAll('track'); - // Call play to start showing the ad. Single video and overlay ads will - // start at this time; the call will be ignored for ad rules - _this8.manager.start(); - } + Array.from(elements).forEach(function (track) { + var src = track.getAttribute('src'); + var href = utils.parseUrl(src); - _this8.initialized = true; - } catch (adError) { - // An error may be thrown if there was a problem with the - // VAST response - _this8.onAdError(adError); + if (href.hostname !== window.location.href.hostname && ['http:', 'https:'].includes(href.protocol)) { + utils.fetch(src, 'blob').then(function (blob) { + track.setAttribute('src', window.URL.createObjectURL(blob)); + }).catch(function () { + utils.removeElement(track); + }); } }); } - /** - * Resume our video. - */ - - }, { - key: 'resumeContent', - value: function resumeContent() { - // Hide our ad container - utils.toggleHidden(this.elements.container, true); - - // Ad is stopped - this.playing = false; - - // Play our video - if (this.player.currentTime < this.player.duration) { - this.player.play(); - } - } - - /** - * Pause our video - */ - - }, { - key: 'pauseContent', - value: function pauseContent() { - // Show our ad container. - utils.toggleHidden(this.elements.container, false); - - // Ad is playing. - this.playing = true; - - // Pause our video. - this.player.pause(); - } - - /** - * Destroy the adsManager so we can grab new ads after this. If we don't then we're not - * allowed to call new ads based on google policies, as they interpret this as an accidental - * video requests. https://developers.google.com/interactive- - * media-ads/docs/sdks/android/faq#8 - */ - - }, { - key: 'cancel', - value: function cancel() { - // Pause our video - if (this.initialized) { - this.resumeContent(); - } + // Set language + captions.setLanguage.call(this); - // Tell our instance that we're done for now - this.handleEventListeners('ERROR'); + // Enable UI + captions.show.call(this); - // Re-create our adsManager - this.loadAds(); + // Set available languages in list + if (utils.is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) { + controls.setCaptionsMenu.call(this); } + }, - /** - * Re-create our adsManager - */ - - }, { - key: 'loadAds', - value: function loadAds() { - var _this9 = this; - // Tell our adsManager to go bye bye - this.managerPromise.then(function () { - // Destroy our adsManager - if (_this9.manager) { - _this9.manager.destroy(); - } + // Set the captions language + setLanguage: function setLanguage() { + var _this = this; - // Re-set our adsManager promises - _this9.managerPromise = new Promise(function (resolve) { - _this9.on('ADS_MANAGER_LOADED', function () { - return resolve(); - }); - _this9.player.debug.log(_this9.manager); + // Setup HTML5 track rendering + if (this.isHTML5 && this.isVideo) { + captions.getTracks.call(this).forEach(function (track) { + // Show track + utils.on(track, 'cuechange', function (event) { + return captions.setCue.call(_this, event); }); - // Now request some new advertisements - _this9.requestAds(); + // Turn off native caption rendering to avoid double captions + // eslint-disable-next-line + track.mode = 'hidden'; }); - } - - /** - * Handles callbacks after an ad event was invoked - * @param {string} event - Event type - */ - - }, { - key: 'handleEventListeners', - value: function handleEventListeners(event) { - if (utils.is.function(this.events[event])) { - this.events[event].call(this); - } - } - - /** - * Add event listeners - * @param {string} event - Event type - * @param {function} callback - Callback for when event occurs - * @return {Ads} - */ - - }, { - key: 'on', - value: function on(event, callback) { - this.events[event] = callback; - return this; - } - - /** - * Setup a safety timer for when the ad network doesn't respond for whatever reason. - * The advertisement has 12 seconds to get its things together. We stop this timer when the - * advertisement is playing, or when a user action is required to start, then we clear the - * timer on ad ready - * @param {number} time - * @param {string} from - */ - - }, { - key: 'startSafetyTimer', - value: function startSafetyTimer(time, from) { - var _this10 = this; - - this.player.debug.log('Safety timer invoked from: ' + from); - - this.safetyTimer = setTimeout(function () { - _this10.cancel(); - _this10.clearSafetyTimer('startSafetyTimer()'); - }, time); - } - - /** - * Clear our safety timer(s) - * @param {string} from - */ - - }, { - key: 'clearSafetyTimer', - value: function clearSafetyTimer(from) { - if (!utils.is.nullOrUndefined(this.safetyTimer)) { - this.player.debug.log('Safety timer cleared from: ' + from); - - clearTimeout(this.safetyTimer); - this.safetyTimer = null; - } - } - }]); - return Ads; -}(); - -// ========================================================================== -// Plyr Event Listeners -// ========================================================================== - -var browser$2 = utils.getBrowser(); - -var listeners = { - // Global listeners - global: function global() { - var _this = this; - - var last = null; - - // Get the key code for an event - var getKeyCode = function getKeyCode(event) { - return event.keyCode ? event.keyCode : event.which; - }; - - // Handle key press - var handleKey = function handleKey(event) { - var code = getKeyCode(event); - var pressed = event.type === 'keydown'; - var repeat = pressed && code === last; - - // Bail if a modifier key is set - if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) { - return; - } - - // If the event is bubbled from the media element - // Firefox doesn't get the keycode for whatever reason - if (!utils.is.number(code)) { - return; - } - - // Seek by the number keys - var seekByKey = function seekByKey() { - // Divide the max duration into 10th's and times by the number value - _this.currentTime = _this.duration / 10 * (code - 48); - }; - - // Handle the key on keydown - // Reset on keyup - if (pressed) { - // Which keycodes should we prevent default - var preventDefault = [48, 49, 50, 51, 52, 53, 54, 56, 57, 32, 75, 38, 40, 77, 39, 37, 70, 67, 73, 76, 79]; - - // Check focused element - // and if the focused element is not editable (e.g. text input) - // and any that accept key input http://webaim.org/techniques/keyboard/ - var focused = utils.getFocusElement(); - if (utils.is.element(focused) && utils.matches(focused, _this.config.selectors.editable)) { - return; - } - - // If the code is found prevent default (e.g. prevent scrolling for arrows) - if (preventDefault.includes(code)) { - event.preventDefault(); - event.stopPropagation(); - } - - switch (code) { - case 48: - case 49: - case 50: - case 51: - case 52: - case 53: - case 54: - case 55: - case 56: - case 57: - // 0-9 - if (!repeat) { - seekByKey(); - } - break; - - case 32: - case 75: - // Space and K key - if (!repeat) { - _this.togglePlay(); - } - break; - - case 38: - // Arrow up - _this.increaseVolume(0.1); - break; - - case 40: - // Arrow down - _this.decreaseVolume(0.1); - break; - - case 77: - // M key - if (!repeat) { - _this.muted = !_this.muted; - } - break; - - case 39: - // Arrow forward - _this.forward(); - break; - - case 37: - // Arrow back - _this.rewind(); - break; - - case 70: - // F key - _this.fullscreen.toggle(); - break; - - case 67: - // C key - if (!repeat) { - _this.toggleCaptions(); - } - break; - - case 76: - // L key - _this.loop = !_this.loop; - break; - /* case 73: - this.setLoop('start'); - break; - case 76: - this.setLoop(); - break; - case 79: - this.setLoop('end'); - break; */ - - default: - break; - } + // Get current track + var currentTrack = captions.getCurrentTrack.call(this); - // Escape is handle natively when in full screen - // So we only need to worry about non native - if (!_this.fullscreen.enabled && _this.fullscreen.active && code === 27) { - _this.fullscreen.toggle(); + // Check if suported kind + if (utils.is.track(currentTrack)) { + // If we change the active track while a cue is already displayed we need to update it + if (Array.from(currentTrack.activeCues || []).length) { + captions.setCue.call(this, currentTrack); } - - // Store last code for next cycle - last = code; - } else { - last = null; } - }; - - // Keyboard shortcuts - if (this.config.keyboard.global) { - utils.on(window, 'keydown keyup', handleKey, false); - } else if (this.config.keyboard.focused) { - utils.on(this.elements.container, 'keydown keyup', handleKey, false); + } else if (this.isVimeo && this.captions.active) { + this.embed.enableTextTrack(this.language); } + }, - // Detect tab focus - // Remove class on blur/focusout - utils.on(this.elements.container, 'focusout', function (event) { - utils.toggleClass(event.target, _this.config.classNames.tabFocus, false); - }); - // Add classname to tabbed elements - utils.on(this.elements.container, 'keydown', function (event) { - if (event.keyCode !== 9) { - return; - } + // Get the tracks + getTracks: function getTracks() { + // Return empty array at least + if (utils.is.nullOrUndefined(this.media)) { + return []; + } - // Delay the adding of classname until the focus has changed - // This event fires before the focusin event - setTimeout(function () { - utils.toggleClass(utils.getFocusElement(), _this.config.classNames.tabFocus, true); - }, 0); + // Only get accepted kinds + return Array.from(this.media.textTracks || []).filter(function (track) { + return ['captions', 'subtitles'].includes(track.kind); }); - - // Toggle controls visibility based on mouse movement - if (this.config.hideControls) { - // Toggle controls on mouse events and entering fullscreen - utils.on(this.elements.container, 'mouseenter mouseleave mousemove touchstart touchend touchmove enterfullscreen exitfullscreen', function (event) { - _this.toggleControls(event); - }); - } }, - // Listen for media events - media: function media() { + // Get the current track for the current language + getCurrentTrack: function getCurrentTrack() { var _this2 = this; - // Time change on media - utils.on(this.media, 'timeupdate seeking', function (event) { - return ui.timeUpdate.call(_this2, event); - }); - - // Display duration - utils.on(this.media, 'durationchange loadedmetadata', function (event) { - return ui.durationUpdate.call(_this2, event); - }); - - // Check for audio tracks on load - // We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point - utils.on(this.media, 'loadeddata', function () { - utils.toggleHidden(_this2.elements.volume, !_this2.hasAudio); - utils.toggleHidden(_this2.elements.buttons.mute, !_this2.hasAudio); - }); - - // Handle the media finishing - utils.on(this.media, 'ended', function () { - // Show poster on end - if (_this2.isHTML5 && _this2.isVideo && _this2.config.showPosterOnEnd) { - // Restart - _this2.restart(); - - // Re-load media - _this2.media.load(); - } - }); - - // Check for buffer progress - utils.on(this.media, 'progress playing', function (event) { - return ui.updateProgress.call(_this2, event); - }); - - // Handle native mute - utils.on(this.media, 'volumechange', function (event) { - return ui.updateVolume.call(_this2, event); - }); - - // Handle native play/pause - utils.on(this.media, 'playing play pause ended', function (event) { - return ui.checkPlaying.call(_this2, event); - }); - - // Loading - utils.on(this.media, 'waiting canplay seeked playing', function (event) { - return ui.checkLoading.call(_this2, event); + return captions.getTracks.call(this).find(function (track) { + return track.language.toLowerCase() === _this2.language; }); + }, - // Check if media failed to load - // utils.on(this.media, 'play', event => ui.checkFailed.call(this, event)); - - // Click video - if (this.supported.ui && this.config.clickToPlay && !this.isAudio) { - // Re-fetch the wrapper - var wrapper = utils.getElement.call(this, '.' + this.config.classNames.video); - // Bail if there's no wrapper (this should never happen) - if (!utils.is.element(wrapper)) { - return; - } + // Display active caption if it contains text + setCue: function setCue(input) { + // Get the track from the event if needed + var track = utils.is.event(input) ? input.target : input; + var activeCues = track.activeCues; - // On click play, pause ore restart - utils.on(wrapper, 'click', function () { - // Touch devices will just show controls (if we're hiding controls) - if (_this2.config.hideControls && support.touch && !_this2.paused) { - return; - } + var active = activeCues.length && activeCues[0]; + var currentTrack = captions.getCurrentTrack.call(this); - if (_this2.paused) { - _this2.play(); - } else if (_this2.ended) { - _this2.restart(); - _this2.play(); - } else { - _this2.pause(); - } - }); + // Only display current track + if (track !== currentTrack) { + return; } - // Disable right click - if (this.supported.ui && this.config.disableContextMenu) { - utils.on(this.media, 'contextmenu', function (event) { - event.preventDefault(); - }, false); + // Display a cue, if there is one + if (utils.is.cue(active)) { + captions.setText.call(this, active.getCueAsHTML()); + } else { + captions.setText.call(this, null); } - // Volume change - utils.on(this.media, 'volumechange', function () { - // Save to storage - _this2.storage.set({ volume: _this2.volume, muted: _this2.muted }); - }); - - // Speed change - utils.on(this.media, 'ratechange', function () { - // Update UI - controls.updateSetting.call(_this2, 'speed'); - - // Save to storage - _this2.storage.set({ speed: _this2.speed }); - }); - - // Quality change - utils.on(this.media, 'qualitychange', function () { - // Update UI - controls.updateSetting.call(_this2, 'quality'); - - // Save to storage - _this2.storage.set({ quality: _this2.quality }); - }); - - // Caption language change - utils.on(this.media, 'languagechange', function () { - // Update UI - controls.updateSetting.call(_this2, 'captions'); - - // Save to storage - _this2.storage.set({ language: _this2.language }); - }); - - // Captions toggle - utils.on(this.media, 'captionsenabled captionsdisabled', function () { - // Update UI - controls.updateSetting.call(_this2, 'captions'); - - // Save to storage - _this2.storage.set({ captions: _this2.captions.active }); - }); - - // Proxy events to container - // Bubble up key events for Edge - utils.on(this.media, this.config.events.concat(['keyup', 'keydown']).join(' '), function (event) { - var detail = {}; - - // Get error details from media - if (event.type === 'error') { - detail = _this2.media.error; - } - - utils.dispatchEvent.call(_this2, _this2.elements.container, event.type, true, detail); - }); + utils.dispatchEvent.call(this, this.media, 'cuechange'); }, - // Listen for control events - controls: function controls$$1() { - var _this3 = this; - - // IE doesn't support input event, so we fallback to change - var inputEvent = browser$2.isIE ? 'change' : 'input'; - - // Trigger custom and default handlers - var proxy = function proxy(event, handlerKey, defaultHandler) { - var customHandler = _this3.config.listeners[handlerKey]; - - // Execute custom handler - if (utils.is.function(customHandler)) { - customHandler.call(_this3, event); - } - - // Only call default handler if not prevented in custom handler - if (!event.defaultPrevented && utils.is.function(defaultHandler)) { - defaultHandler.call(_this3, event); - } - }; - - // Play/pause toggle - utils.on(this.elements.buttons.play, 'click', function (event) { - return proxy(event, 'play', function () { - _this3.togglePlay(); - }); - }); - - // Pause - utils.on(this.elements.buttons.restart, 'click', function (event) { - return proxy(event, 'restart', function () { - _this3.restart(); - }); - }); - - // Rewind - utils.on(this.elements.buttons.rewind, 'click', function (event) { - return proxy(event, 'rewind', function () { - _this3.rewind(); - }); - }); - - // Rewind - utils.on(this.elements.buttons.forward, 'click', function (event) { - return proxy(event, 'forward', function () { - _this3.forward(); - }); - }); - - // Mute toggle - utils.on(this.elements.buttons.mute, 'click', function (event) { - return proxy(event, 'mute', function () { - _this3.muted = !_this3.muted; - }); - }); - - // Captions toggle - utils.on(this.elements.buttons.captions, 'click', function (event) { - return proxy(event, 'captions', function () { - _this3.toggleCaptions(); - }); - }); - - // Fullscreen toggle - utils.on(this.elements.buttons.fullscreen, 'click', function (event) { - return proxy(event, 'fullscreen', function () { - _this3.fullscreen.toggle(); - }); - }); - - // Picture-in-Picture - utils.on(this.elements.buttons.pip, 'click', function (event) { - return proxy(event, 'pip', function () { - _this3.pip = 'toggle'; - }); - }); - - // Airplay - utils.on(this.elements.buttons.airplay, 'click', function (event) { - return proxy(event, 'airplay', function () { - _this3.airplay(); - }); - }); + // Set the current caption + setText: function setText(input) { + // Requires UI + if (!this.supported.ui) { + return; + } - // Settings menu - utils.on(this.elements.buttons.settings, 'click', function (event) { - controls.toggleMenu.call(_this3, event); - }); + if (utils.is.element(this.elements.captions)) { + var content = utils.createElement('span'); - // Click anywhere closes menu - utils.on(document.documentElement, 'click', function (event) { - controls.toggleMenu.call(_this3, event); - }); + // Empty the container + utils.emptyElement(this.elements.captions); - // Settings menu - utils.on(this.elements.settings.form, 'click', function (event) { - event.stopPropagation(); + // Default to empty + var caption = !utils.is.nullOrUndefined(input) ? input : ''; - // Settings menu items - use event delegation as items are added/removed - if (utils.matches(event.target, _this3.config.selectors.inputs.language)) { - proxy(event, 'language', function () { - _this3.language = event.target.value; - }); - } else if (utils.matches(event.target, _this3.config.selectors.inputs.quality)) { - proxy(event, 'quality', function () { - _this3.quality = event.target.value; - }); - } else if (utils.matches(event.target, _this3.config.selectors.inputs.speed)) { - proxy(event, 'speed', function () { - _this3.speed = parseFloat(event.target.value); - }); + // Set the span content + if (utils.is.string(caption)) { + content.textContent = caption.trim(); } else { - controls.showTab.call(_this3, event); + content.appendChild(caption); } - }); - - // Seek - utils.on(this.elements.inputs.seek, inputEvent, function (event) { - return proxy(event, 'seek', function () { - _this3.currentTime = event.target.value / event.target.max * _this3.duration; - }); - }); - - // Current time invert - // Only if one time element is used for both currentTime and duration - if (this.config.toggleInvert && !utils.is.element(this.elements.display.duration)) { - utils.on(this.elements.display.currentTime, 'click', function () { - // Do nothing if we're at the start - if (_this3.currentTime === 0) { - return; - } - _this3.config.invertTime = !_this3.config.invertTime; - ui.timeUpdate.call(_this3); - }); + // Set new caption text + this.elements.captions.appendChild(content); + } else { + this.debug.warn('No captions element to render to'); } + }, - // Volume - utils.on(this.elements.inputs.volume, inputEvent, function (event) { - return proxy(event, 'volume', function () { - _this3.volume = event.target.value; - }); - }); - // Polyfill for lower fill in <input type="range"> for webkit - if (browser$2.isWebkit) { - utils.on(utils.getElements.call(this, 'input[type="range"]'), 'input', function (event) { - controls.updateRangeFill.call(_this3, event.target); - }); + // Display captions container and button (for initialization) + show: function show() { + // If there's no caption toggle, bail + if (!utils.is.element(this.elements.buttons.captions)) { + return; } - // Seek tooltip - utils.on(this.elements.progress, 'mouseenter mouseleave mousemove', function (event) { - return controls.updateSeekTooltip.call(_this3, event); - }); - - // Toggle controls visibility based on mouse movement - if (this.config.hideControls) { - // Watch for cursor over controls so they don't hide when trying to interact - utils.on(this.elements.controls, 'mouseenter mouseleave', function (event) { - _this3.elements.controls.hover = event.type === 'mouseenter'; - }); - - // Watch for cursor over controls so they don't hide when trying to interact - utils.on(this.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) { - _this3.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type); - }); + // Try to load the value from storage + var active = this.storage.get('captions'); - // Focus in/out on controls - utils.on(this.elements.controls, 'focusin focusout', function (event) { - _this3.toggleControls(event); - }); + // Otherwise fall back to the default config + if (!utils.is.boolean(active)) { + active = this.config.captions.active; + } else { + this.captions.active = active; } - // Mouse wheel for volume - utils.on(this.elements.inputs.volume, 'wheel', function (event) { - return proxy(event, 'volume', function () { - // Detect "natural" scroll - suppored on OS X Safari only - // Other browsers on OS X will be inverted until support improves - var inverted = event.webkitDirectionInvertedFromDevice; - var step = 1 / 50; - var direction = 0; - - // Scroll down (or up on natural) to decrease - if (event.deltaY < 0 || event.deltaX > 0) { - if (inverted) { - _this3.decreaseVolume(step); - direction = -1; - } else { - _this3.increaseVolume(step); - direction = 1; - } - } - - // Scroll up (or down on natural) to increase - if (event.deltaY > 0 || event.deltaX < 0) { - if (inverted) { - _this3.increaseVolume(step); - direction = 1; - } else { - _this3.decreaseVolume(step); - direction = -1; - } - } - - // Don't break page scrolling at max and min - if (direction === 1 && _this3.media.volume < 1 || direction === -1 && _this3.media.volume > 0) { - event.preventDefault(); - } - }); - }, false); + if (active) { + utils.toggleClass(this.elements.container, this.config.classNames.captions.active, true); + utils.toggleState(this.elements.buttons.captions, true); + } } }; @@ -3242,7 +2179,7 @@ var ui = { // Re-attach media element listeners // TODO: Use event bubbling - listeners.media.call(this); + this.listeners.media(); // Don't setup interface if no support if (!this.supported.ui) { @@ -3261,7 +2198,7 @@ var ui = { controls.inject.call(this); // Re-attach control listeners - listeners.controls.call(this); + this.listeners.controls(); } // If there's no controls, bail @@ -3576,14 +2513,13 @@ var ui = { // Plyr controls // ========================================================================== -// Sniff out the browser -var browser$1 = utils.getBrowser(); +var browser$2 = utils.getBrowser(); var controls = { // Webkit polyfill for lower fill range updateRangeFill: function updateRangeFill(target) { // WebKit only - if (!browser$1.isWebkit) { + if (!browser$2.isWebkit) { return; } @@ -3604,7 +2540,7 @@ var controls = { getIconUrl: function getIconUrl() { return { url: this.config.iconUrl, - absolute: this.config.iconUrl.indexOf('http') === 0 || browser$1.isIE && !window.svg4everybody + absolute: this.config.iconUrl.indexOf('http') === 0 || browser$2.isIE && !window.svg4everybody }; }, @@ -4289,6 +3225,12 @@ var controls = { var form = this.elements.settings.form; var button = this.elements.buttons.settings; + + // Menu and button are required + if (!utils.is.element(form) || !utils.is.element(button)) { + return; + } + var show = utils.is.boolean(event) ? event : utils.is.element(form) && form.getAttribute('aria-hidden') === 'true'; if (utils.is.event(event)) { @@ -4765,238 +3707,1341 @@ var controls = { }; // ========================================================================== -// Plyr Captions -// TODO: Create as class +// Plyr Event Listeners // ========================================================================== -var captions = { - // Setup captions - setup: function setup() { - // Requires UI support - if (!this.supported.ui) { - return; - } +// Sniff out the browser +var browser$1 = utils.getBrowser(); - // Set default language if not set - var stored = this.storage.get('language'); +var Listeners = function () { + function Listeners(player) { + classCallCheck(this, Listeners); - if (!utils.is.empty(stored)) { - this.captions.language = stored; - } + this.player = player; + this.lastKey = null; - if (utils.is.empty(this.captions.language)) { - this.captions.language = this.config.captions.language.toLowerCase(); - } + this.handleKey = this.handleKey.bind(this); + this.toggleMenu = this.toggleMenu.bind(this); + } - // Set captions enabled state if not set - if (!utils.is.boolean(this.captions.active)) { - var active = this.storage.get('captions'); + // Handle key presses - if (utils.is.boolean(active)) { - this.captions.active = active; + + createClass(Listeners, [{ + key: 'handleKey', + value: function handleKey(event) { + var _this = this; + + var code = event.keyCode ? event.keyCode : event.which; + var pressed = event.type === 'keydown'; + var repeat = pressed && code === this.lastKey; + + // Bail if a modifier key is set + if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) { + return; + } + + // If the event is bubbled from the media element + // Firefox doesn't get the keycode for whatever reason + if (!utils.is.number(code)) { + return; + } + + // Seek by the number keys + var seekByKey = function seekByKey() { + // Divide the max duration into 10th's and times by the number value + _this.player.currentTime = _this.player.duration / 10 * (code - 48); + }; + + // Handle the key on keydown + // Reset on keyup + if (pressed) { + // Which keycodes should we prevent default + var preventDefault = [48, 49, 50, 51, 52, 53, 54, 56, 57, 32, 75, 38, 40, 77, 39, 37, 70, 67, 73, 76, 79]; + + // Check focused element + // and if the focused element is not editable (e.g. text input) + // and any that accept key input http://webaim.org/techniques/keyboard/ + var focused = utils.getFocusElement(); + if (utils.is.element(focused) && utils.matches(focused, this.player.config.selectors.editable)) { + return; + } + + // If the code is found prevent default (e.g. prevent scrolling for arrows) + if (preventDefault.includes(code)) { + event.preventDefault(); + event.stopPropagation(); + } + + switch (code) { + case 48: + case 49: + case 50: + case 51: + case 52: + case 53: + case 54: + case 55: + case 56: + case 57: + // 0-9 + if (!repeat) { + seekByKey(); + } + break; + + case 32: + case 75: + // Space and K key + if (!repeat) { + this.player.togglePlay(); + } + break; + + case 38: + // Arrow up + this.player.increaseVolume(0.1); + break; + + case 40: + // Arrow down + this.player.decreaseVolume(0.1); + break; + + case 77: + // M key + if (!repeat) { + this.player.muted = !this.player.muted; + } + break; + + case 39: + // Arrow forward + this.player.forward(); + break; + + case 37: + // Arrow back + this.player.rewind(); + break; + + case 70: + // F key + this.player.fullscreen.toggle(); + break; + + case 67: + // C key + if (!repeat) { + this.player.toggleCaptions(); + } + break; + + case 76: + // L key + this.player.loop = !this.player.loop; + break; + + /* case 73: + this.setLoop('start'); + break; + case 76: + this.setLoop(); + break; + case 79: + this.setLoop('end'); + break; */ + + default: + break; + } + + // Escape is handle natively when in full screen + // So we only need to worry about non native + if (!this.player.fullscreen.enabled && this.player.fullscreen.active && code === 27) { + this.player.fullscreen.toggle(); + } + + // Store last code for next cycle + this.lastKey = code; } else { - this.captions.active = this.config.captions.active; + this.lastKey = null; } } - // Only Vimeo and HTML5 video supported at this point - if (!this.isVideo || this.isYouTube || this.isHTML5 && !support.textTracks) { - // Clear menu and hide - if (utils.is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) { - controls.setCaptionsMenu.call(this); + // Toggle menu + + }, { + key: 'toggleMenu', + value: function toggleMenu(event) { + controls.toggleMenu.call(this.player, event); + } + + // Global window & document listeners + + }, { + key: 'global', + value: function global(toggle) { + // Keyboard shortcuts + if (this.player.config.keyboard.global) { + utils.toggleListener(window, 'keydown keyup', this.handleKey, toggle, false); } - return; + // Click anywhere closes menu + utils.toggleListener(document.body, 'click', this.toggleMenu, toggle); } - // Inject the container - if (!utils.is.element(this.elements.captions)) { - this.elements.captions = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.captions)); - utils.insertAfter(this.elements.captions, this.elements.wrapper); + // Container listeners + + }, { + key: 'container', + value: function container() { + var _this2 = this; + + // Keyboard shortcuts + if (!this.player.config.keyboard.global && this.player.config.keyboard.focused) { + utils.on(this.player.elements.container, 'keydown keyup', this.handleKey, false); + } + + // Detect tab focus + // Remove class on blur/focusout + utils.on(this.player.elements.container, 'focusout', function (event) { + utils.toggleClass(event.target, _this2.player.config.classNames.tabFocus, false); + }); + + // Add classname to tabbed elements + utils.on(this.player.elements.container, 'keydown', function (event) { + if (event.keyCode !== 9) { + return; + } + + // Delay the adding of classname until the focus has changed + // This event fires before the focusin event + setTimeout(function () { + utils.toggleClass(utils.getFocusElement(), _this2.player.config.classNames.tabFocus, true); + }, 0); + }); + + // Toggle controls visibility based on mouse movement + if (this.player.config.hideControls) { + // Toggle controls on mouse events and entering fullscreen + utils.on(this.player.elements.container, 'mouseenter mouseleave mousemove touchstart touchend touchmove enterfullscreen exitfullscreen', function (event) { + _this2.player.toggleControls(event); + }); + } } - // Set the class hook - utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(captions.getTracks.call(this))); + // Listen for media events - // Get tracks - var tracks = captions.getTracks.call(this); + }, { + key: 'media', + value: function media() { + var _this3 = this; - // If no caption file exists, hide container for caption text - if (utils.is.empty(tracks)) { - return; + // Time change on media + utils.on(this.player.media, 'timeupdate seeking', function (event) { + return ui.timeUpdate.call(_this3.player, event); + }); + + // Display duration + utils.on(this.player.media, 'durationchange loadedmetadata', function (event) { + return ui.durationUpdate.call(_this3.player, event); + }); + + // Check for audio tracks on load + // We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point + utils.on(this.player.media, 'loadeddata', function () { + utils.toggleHidden(_this3.player.elements.volume, !_this3.player.hasAudio); + utils.toggleHidden(_this3.player.elements.buttons.mute, !_this3.player.hasAudio); + }); + + // Handle the media finishing + utils.on(this.player.media, 'ended', function () { + // Show poster on end + if (_this3.player.isHTML5 && _this3.player.isVideo && _this3.player.config.showPosterOnEnd) { + // Restart + _this3.player.restart(); + + // Re-load media + _this3.player.media.load(); + } + }); + + // Check for buffer progress + utils.on(this.player.media, 'progress playing', function (event) { + return ui.updateProgress.call(_this3.player, event); + }); + + // Handle native mute + utils.on(this.player.media, 'volumechange', function (event) { + return ui.updateVolume.call(_this3.player, event); + }); + + // Handle native play/pause + utils.on(this.player.media, 'playing play pause ended', function (event) { + return ui.checkPlaying.call(_this3.player, event); + }); + + // Loading + utils.on(this.player.media, 'waiting canplay seeked playing', function (event) { + return ui.checkLoading.call(_this3.player, event); + }); + + // Check if media failed to load + // utils.on(this.player.media, 'play', event => ui.checkFailed.call(this.player, event)); + + // Click video + if (this.player.supported.ui && this.player.config.clickToPlay && !this.player.isAudio) { + // Re-fetch the wrapper + var wrapper = utils.getElement.call(this.player, '.' + this.player.config.classNames.video); + + // Bail if there's no wrapper (this should never happen) + if (!utils.is.element(wrapper)) { + return; + } + + // On click play, pause ore restart + utils.on(wrapper, 'click', function () { + // Touch devices will just show controls (if we're hiding controls) + if (_this3.player.config.hideControls && support.touch && !_this3.player.paused) { + return; + } + + if (_this3.player.paused) { + _this3.player.play(); + } else if (_this3.player.ended) { + _this3.player.restart(); + _this3.player.play(); + } else { + _this3.player.pause(); + } + }); + } + + // Disable right click + if (this.player.supported.ui && this.player.config.disableContextMenu) { + utils.on(this.player.media, 'contextmenu', function (event) { + event.preventDefault(); + }, false); + } + + // Volume change + utils.on(this.player.media, 'volumechange', function () { + // Save to storage + _this3.player.storage.set({ volume: _this3.player.volume, muted: _this3.player.muted }); + }); + + // Speed change + utils.on(this.player.media, 'ratechange', function () { + // Update UI + controls.updateSetting.call(_this3.player, 'speed'); + + // Save to storage + _this3.player.storage.set({ speed: _this3.player.speed }); + }); + + // Quality change + utils.on(this.player.media, 'qualitychange', function () { + // Update UI + controls.updateSetting.call(_this3.player, 'quality'); + + // Save to storage + _this3.player.storage.set({ quality: _this3.player.quality }); + }); + + // Caption language change + utils.on(this.player.media, 'languagechange', function () { + // Update UI + controls.updateSetting.call(_this3.player, 'captions'); + + // Save to storage + _this3.player.storage.set({ language: _this3.player.language }); + }); + + // Captions toggle + utils.on(this.player.media, 'captionsenabled captionsdisabled', function () { + // Update UI + controls.updateSetting.call(_this3.player, 'captions'); + + // Save to storage + _this3.player.storage.set({ captions: _this3.player.captions.active }); + }); + + // Proxy events to container + // Bubble up key events for Edge + utils.on(this.player.media, this.player.config.events.concat(['keyup', 'keydown']).join(' '), function (event) { + var detail = {}; + + // Get error details from media + if (event.type === 'error') { + detail = _this3.player.media.error; + } + + utils.dispatchEvent.call(_this3.player, _this3.player.elements.container, event.type, true, detail); + }); } - // Get browser info - var browser = utils.getBrowser(); + // Listen for control events - // Fix IE captions if CORS is used - // Fetch captions and inject as blobs instead (data URIs not supported!) - if (browser.isIE && window.URL) { - var elements = this.media.querySelectorAll('track'); + }, { + key: 'controls', + value: function controls$$1() { + var _this4 = this; - Array.from(elements).forEach(function (track) { - var src = track.getAttribute('src'); - var href = utils.parseUrl(src); + // IE doesn't support input event, so we fallback to change + var inputEvent = browser$1.isIE ? 'change' : 'input'; - if (href.hostname !== window.location.href.hostname && ['http:', 'https:'].includes(href.protocol)) { - utils.fetch(src, 'blob').then(function (blob) { - track.setAttribute('src', window.URL.createObjectURL(blob)); - }).catch(function () { - utils.removeElement(track); + // Trigger custom and default handlers + var proxy = function proxy(event, handlerKey, defaultHandler) { + var customHandler = _this4.player.config.listeners[handlerKey]; + + // Execute custom handler + if (utils.is.function(customHandler)) { + customHandler.call(_this4.player, event); + } + + // Only call default handler if not prevented in custom handler + if (!event.defaultPrevented && utils.is.function(defaultHandler)) { + defaultHandler.call(_this4.player, event); + } + }; + + // Play/pause toggle + utils.on(this.player.elements.buttons.play, 'click', function (event) { + return proxy(event, 'play', function () { + _this4.player.togglePlay(); + }); + }); + + // Pause + utils.on(this.player.elements.buttons.restart, 'click', function (event) { + return proxy(event, 'restart', function () { + _this4.player.restart(); + }); + }); + + // Rewind + utils.on(this.player.elements.buttons.rewind, 'click', function (event) { + return proxy(event, 'rewind', function () { + _this4.player.rewind(); + }); + }); + + // Rewind + utils.on(this.player.elements.buttons.forward, 'click', function (event) { + return proxy(event, 'forward', function () { + _this4.player.forward(); + }); + }); + + // Mute toggle + utils.on(this.player.elements.buttons.mute, 'click', function (event) { + return proxy(event, 'mute', function () { + _this4.player.muted = !_this4.player.muted; + }); + }); + + // Captions toggle + utils.on(this.player.elements.buttons.captions, 'click', function (event) { + return proxy(event, 'captions', function () { + _this4.player.toggleCaptions(); + }); + }); + + // Fullscreen toggle + utils.on(this.player.elements.buttons.fullscreen, 'click', function (event) { + return proxy(event, 'fullscreen', function () { + _this4.player.fullscreen.toggle(); + }); + }); + + // Picture-in-Picture + utils.on(this.player.elements.buttons.pip, 'click', function (event) { + return proxy(event, 'pip', function () { + _this4.player.pip = 'toggle'; + }); + }); + + // Airplay + utils.on(this.player.elements.buttons.airplay, 'click', function (event) { + return proxy(event, 'airplay', function () { + _this4.player.airplay(); + }); + }); + + // Settings menu + utils.on(this.player.elements.buttons.settings, 'click', function (event) { + controls.toggleMenu.call(_this4.player, event); + }); + + // Settings menu + utils.on(this.player.elements.settings.form, 'click', function (event) { + event.stopPropagation(); + + // Settings menu items - use event delegation as items are added/removed + if (utils.matches(event.target, _this4.player.config.selectors.inputs.language)) { + proxy(event, 'language', function () { + _this4.player.language = event.target.value; + }); + } else if (utils.matches(event.target, _this4.player.config.selectors.inputs.quality)) { + proxy(event, 'quality', function () { + _this4.player.quality = event.target.value; }); + } else if (utils.matches(event.target, _this4.player.config.selectors.inputs.speed)) { + proxy(event, 'speed', function () { + _this4.player.speed = parseFloat(event.target.value); + }); + } else { + controls.showTab.call(_this4.player, event); } }); + + // Seek + utils.on(this.player.elements.inputs.seek, inputEvent, function (event) { + return proxy(event, 'seek', function () { + _this4.player.currentTime = event.target.value / event.target.max * _this4.player.duration; + }); + }); + + // Current time invert + // Only if one time element is used for both currentTime and duration + if (this.player.config.toggleInvert && !utils.is.element(this.player.elements.display.duration)) { + utils.on(this.player.elements.display.currentTime, 'click', function () { + // Do nothing if we're at the start + if (_this4.player.currentTime === 0) { + return; + } + + _this4.player.config.invertTime = !_this4.player.config.invertTime; + ui.timeUpdate.call(_this4.player); + }); + } + + // Volume + utils.on(this.player.elements.inputs.volume, inputEvent, function (event) { + return proxy(event, 'volume', function () { + _this4.player.volume = event.target.value; + }); + }); + + // Polyfill for lower fill in <input type="range"> for webkit + if (browser$1.isWebkit) { + utils.on(utils.getElements.call(this.player, 'input[type="range"]'), 'input', function (event) { + controls.updateRangeFill.call(_this4.player, event.target); + }); + } + + // Seek tooltip + utils.on(this.player.elements.progress, 'mouseenter mouseleave mousemove', function (event) { + return controls.updateSeekTooltip.call(_this4.player, event); + }); + + // Toggle controls visibility based on mouse movement + if (this.player.config.hideControls) { + // Watch for cursor over controls so they don't hide when trying to interact + utils.on(this.player.elements.controls, 'mouseenter mouseleave', function (event) { + _this4.player.elements.controls.hover = event.type === 'mouseenter'; + }); + + // Watch for cursor over controls so they don't hide when trying to interact + utils.on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) { + _this4.player.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type); + }); + + // Focus in/out on controls + utils.on(this.player.elements.controls, 'focusin focusout', function (event) { + _this4.player.toggleControls(event); + }); + } + + // Mouse wheel for volume + utils.on(this.player.elements.inputs.volume, 'wheel', function (event) { + return proxy(event, 'volume', function () { + // Detect "natural" scroll - suppored on OS X Safari only + // Other browsers on OS X will be inverted until support improves + var inverted = event.webkitDirectionInvertedFromDevice; + var step = 1 / 50; + var direction = 0; + + // Scroll down (or up on natural) to decrease + if (event.deltaY < 0 || event.deltaX > 0) { + if (inverted) { + _this4.player.decreaseVolume(step); + direction = -1; + } else { + _this4.player.increaseVolume(step); + direction = 1; + } + } + + // Scroll up (or down on natural) to increase + if (event.deltaY > 0 || event.deltaX < 0) { + if (inverted) { + _this4.player.increaseVolume(step); + direction = 1; + } else { + _this4.player.decreaseVolume(step); + direction = -1; + } + } + + // Don't break page scrolling at max and min + if (direction === 1 && _this4.player.media.volume < 1 || direction === -1 && _this4.player.media.volume > 0) { + event.preventDefault(); + } + }); + }, false); } + }]); + return Listeners; +}(); - // Set language - captions.setLanguage.call(this); +// ========================================================================== +// Plyr storage +// ========================================================================== - // Enable UI - captions.show.call(this); +var Storage = function () { + function Storage(player) { + classCallCheck(this, Storage); - // Set available languages in list - if (utils.is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) { - controls.setCaptionsMenu.call(this); + this.enabled = player.config.storage.enabled; + this.key = player.config.storage.key; + } + + // Check for actual support (see if we can use it) + + + createClass(Storage, [{ + key: 'get', + value: function get$$1(key) { + var store = window.localStorage.getItem(this.key); + + if (!Storage.supported || utils.is.empty(store)) { + return null; + } + + var json = JSON.parse(store); + + return utils.is.string(key) && key.length ? json[key] : json; } - }, + }, { + key: 'set', + value: function set$$1(object) { + // Bail if we don't have localStorage support or it's disabled + if (!Storage.supported || !this.enabled) { + return; + } + + // Can only store objectst + if (!utils.is.object(object)) { + return; + } + // Get current storage + var storage = this.get(); - // Set the captions language - setLanguage: function setLanguage() { + // Default to empty object + if (utils.is.empty(storage)) { + storage = {}; + } + + // Update the working copy of the values + utils.extend(storage, object); + + // Update storage + window.localStorage.setItem(this.key, JSON.stringify(storage)); + } + }], [{ + key: 'supported', + get: function get$$1() { + if (!('localStorage' in window)) { + return false; + } + + var test = '___test'; + + // Try to use it (it might be disabled, e.g. user is in private mode) + // see: https://github.com/sampotts/plyr/issues/131 + try { + window.localStorage.setItem(test, test); + window.localStorage.removeItem(test); + return true; + } catch (e) { + return false; + } + } + }]); + return Storage; +}(); + +// ========================================================================== +// Advertisement plugin using Google IMA HTML5 SDK +// Create an account with our ad partner, vi here: +// https://www.vi.ai/publisher-video-monetization/ +// ========================================================================== + +/* global google */ + +var Ads = function () { + /** + * Ads constructor. + * @param {object} player + * @return {Ads} + */ + function Ads(player) { var _this = this; - // Setup HTML5 track rendering - if (this.isHTML5 && this.isVideo) { - captions.getTracks.call(this).forEach(function (track) { - // Show track - utils.on(track, 'cuechange', function (event) { - return captions.setCue.call(_this, event); + classCallCheck(this, Ads); + + this.player = player; + this.publisherId = player.config.ads.publisherId; + this.enabled = player.isHTML5 && player.isVideo && player.config.ads.enabled && utils.is.string(this.publisherId) && this.publisherId.length; + this.playing = false; + this.initialized = false; + this.elements = { + container: null, + displayContainer: null + }; + this.manager = null; + this.loader = null; + this.cuePoints = null; + this.events = {}; + this.safetyTimer = null; + this.countdownTimer = null; + + // Setup a promise to resolve when the IMA manager is ready + this.managerPromise = new Promise(function (resolve, reject) { + // The ad is pre-loaded and ready + _this.on('ADS_MANAGER_LOADED', resolve); + + // Ads failed + _this.on('ERROR', reject); + }); + + if (this.enabled) { + // Check if the Google IMA3 SDK is loaded or load it ourselves + if (!utils.is.object(window.google)) { + utils.loadScript(player.config.urls.googleIMA.api).then(function () { + _this.ready(); + }).catch(function () { + // Script failed to load or is blocked + _this.trigger('ERROR'); + _this.player.debug.error('Google IMA SDK failed to load'); }); + } else { + this.ready(); + } + } + } - // Turn off native caption rendering to avoid double captions - // eslint-disable-next-line - track.mode = 'hidden'; + /** + * Get the ads instance ready. + */ + + + createClass(Ads, [{ + key: 'ready', + value: function ready() { + var _this2 = this; + + // Start ticking our safety timer. If the whole advertisement + // thing doesn't resolve within our set time; we bail + this.startSafetyTimer(12000, 'ready()'); + + // Clear the safety timer + this.managerPromise.then(function () { + _this2.clearSafetyTimer('onAdsManagerLoaded()'); }); - // Get current track - var currentTrack = captions.getCurrentTrack.call(this); + // Set listeners on the Plyr instance + this.listeners(); - // Check if suported kind - if (utils.is.track(currentTrack)) { - // If we change the active track while a cue is already displayed we need to update it - if (Array.from(currentTrack.activeCues || []).length) { - captions.setCue.call(this, currentTrack); + // Setup the IMA SDK + this.setupIMA(); + } + + // Build the default tag URL + + }, { + key: 'setupIMA', + + + /** + * In order for the SDK to display ads for our video, we need to tell it where to put them, + * so here we define our ad container. This div is set up to render on top of the video player. + * Using the code below, we tell the SDK to render ads within that div. We also provide a + * handle to the content video player - the SDK will poll the current time of our player to + * properly place mid-rolls. After we create the ad display container, we initialize it. On + * mobile devices, this initialization is done as the result of a user action. + */ + value: function setupIMA() { + // Create the container for our advertisements + this.elements.container = utils.createElement('div', { + class: this.player.config.classNames.ads + }); + this.player.elements.container.appendChild(this.elements.container); + + // So we can run VPAID2 + google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED); + + // Set language + google.ima.settings.setLocale(this.player.config.ads.language); + + // We assume the adContainer is the video container of the plyr element + // that will house the ads + this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container); + + // Request video ads to be pre-loaded + this.requestAds(); + } + + /** + * Request advertisements + */ + + }, { + key: 'requestAds', + value: function requestAds() { + var _this3 = this; + + var container = this.player.elements.container; + + + try { + // Create ads loader + this.loader = new google.ima.AdsLoader(this.elements.displayContainer); + + // Listen and respond to ads loaded and error events + this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, function (event) { + return _this3.onAdsManagerLoaded(event); + }, false); + this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, function (error) { + return _this3.onAdError(error); + }, false); + + // Request video ads + var request = new google.ima.AdsRequest(); + request.adTagUrl = this.tagUrl; + + // Specify the linear and nonlinear slot sizes. This helps the SDK + // to select the correct creative if multiple are returned + request.linearAdSlotWidth = container.offsetWidth; + request.linearAdSlotHeight = container.offsetHeight; + request.nonLinearAdSlotWidth = container.offsetWidth; + request.nonLinearAdSlotHeight = container.offsetHeight; + + // We only overlay ads as we only support video. + request.forceNonLinearFullSlot = false; + + this.loader.requestAds(request); + + this.trigger('ADS_LOADER_LOADED'); + } catch (e) { + this.onAdError(e); + } + } + + /** + * Update the ad countdown + * @param {boolean} start + */ + + }, { + key: 'pollCountdown', + value: function pollCountdown() { + var _this4 = this; + + var start = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; + + if (!start) { + window.clearInterval(this.countdownTimer); + this.elements.container.removeAttribute('data-badge-text'); + return; + } + + var update = function update() { + var time = utils.formatTime(Math.max(_this4.manager.getRemainingTime(), 0)); + var label = _this4.player.config.i18n.advertisement + ' - ' + time; + _this4.elements.container.setAttribute('data-badge-text', label); + }; + + this.countdownTimer = window.setInterval(update, 100); + } + + /** + * This method is called whenever the ads are ready inside the AdDisplayContainer + * @param {Event} adsManagerLoadedEvent + */ + + }, { + key: 'onAdsManagerLoaded', + value: function onAdsManagerLoaded(adsManagerLoadedEvent) { + var _this5 = this; + + // Get the ads manager + var settings = new google.ima.AdsRenderingSettings(); + + // Tell the SDK to save and restore content video state on our behalf + settings.restoreCustomPlaybackStateOnAdBreakComplete = true; + settings.enablePreloading = true; + + // The SDK is polling currentTime on the contentPlayback. And needs a duration + // so it can determine when to start the mid- and post-roll + this.manager = adsManagerLoadedEvent.getAdsManager(this.player, settings); + + // Get the cue points for any mid-rolls by filtering out the pre- and post-roll + this.cuePoints = this.manager.getCuePoints(); + + // Add advertisement cue's within the time line if available + this.cuePoints.forEach(function (cuePoint) { + if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < _this5.player.duration) { + var seekElement = _this5.player.elements.progress; + + if (seekElement) { + var cuePercentage = 100 / _this5.player.duration * cuePoint; + var cue = utils.createElement('span', { + class: _this5.player.config.classNames.cues + }); + + cue.style.left = cuePercentage.toString() + '%'; + seekElement.appendChild(cue); + } } + }); + + // Get skippable state + // TODO: Skip button + // this.manager.getAdSkippableState(); + + // Set volume to match player + this.manager.setVolume(this.player.volume); + + // Add listeners to the required events + // Advertisement error events + this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, function (error) { + return _this5.onAdError(error); + }); + + // Advertisement regular events + Object.keys(google.ima.AdEvent.Type).forEach(function (type) { + _this5.manager.addEventListener(google.ima.AdEvent.Type[type], function (event) { + return _this5.onAdEvent(event); + }); + }); + + // Resolve our adsManager + this.trigger('ADS_MANAGER_LOADED'); + } + + /** + * This is where all the event handling takes place. Retrieve the ad from the event. Some + * events (e.g. ALL_ADS_COMPLETED) don't have the ad object associated + * https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type + * @param {Event} event + */ + + }, { + key: 'onAdEvent', + value: function onAdEvent(event) { + var _this6 = this; + + var container = this.player.elements.container; + + // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED) + // don't have ad object associated + + var ad = event.getAd(); + + // Proxy event + var dispatchEvent = function dispatchEvent(type) { + utils.dispatchEvent.call(_this6.player, _this6.player.media, 'ads' + type); + }; + + switch (event.type) { + case google.ima.AdEvent.Type.LOADED: + // This is the first event sent for an ad - it is possible to determine whether the + // ad is a video ad or an overlay + this.trigger('LOADED'); + + // Bubble event + dispatchEvent('loaded'); + + // Start countdown + this.pollCountdown(true); + + if (!ad.isLinear()) { + // Position AdDisplayContainer correctly for overlay + ad.width = container.offsetWidth; + ad.height = container.offsetHeight; + } + + // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex()); + // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset()); + break; + + case google.ima.AdEvent.Type.ALL_ADS_COMPLETED: + // All ads for the current videos are done. We can now request new advertisements + // in case the video is re-played + this.trigger('ALL_ADS_COMPLETED'); + + // Fire event + dispatchEvent('allcomplete'); + + // TODO: Example for what happens when a next video in a playlist would be loaded. + // So here we load a new video when all ads are done. + // Then we load new ads within a new adsManager. When the video + // Is started - after - the ads are loaded, then we get ads. + // You can also easily test cancelling and reloading by running + // player.ads.cancel() and player.ads.play from the console I guess. + // this.player.source = { + // type: 'video', + // title: 'View From A Blue Moon', + // sources: [{ + // src: + // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', type: + // 'video/mp4', }], poster: + // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', tracks: + // [ { kind: 'captions', label: 'English', srclang: 'en', src: + // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt', + // default: true, }, { kind: 'captions', label: 'French', srclang: 'fr', src: + // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', }, ], + // }; + + // TODO: So there is still this thing where a video should only be allowed to start + // playing when the IMA SDK is ready or has failed + + this.loadAds(); + break; + + case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED: + // This event indicates the ad has started - the video player can adjust the UI, + // for example display a pause button and remaining time. Fired when content should + // be paused. This usually happens right before an ad is about to cover the content + this.trigger('CONTENT_PAUSE_REQUESTED'); + + dispatchEvent('contentpause'); + + this.pauseContent(); + + break; + + case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED: + // This event indicates the ad has finished - the video player can perform + // appropriate UI actions, such as removing the timer for remaining time detection. + // Fired when content should be resumed. This usually happens when an ad finishes + // or collapses + this.trigger('CONTENT_RESUME_REQUESTED'); + + dispatchEvent('contentresume'); + + this.pollCountdown(); + + this.resumeContent(); + + break; + + case google.ima.AdEvent.Type.STARTED: + dispatchEvent('started'); + break; + + case google.ima.AdEvent.Type.MIDPOINT: + dispatchEvent('midpoint'); + break; + + case google.ima.AdEvent.Type.COMPLETE: + dispatchEvent('complete'); + break; + + case google.ima.AdEvent.Type.IMPRESSION: + dispatchEvent('impression'); + break; + + case google.ima.AdEvent.Type.CLICK: + dispatchEvent('click'); + break; + + default: + break; } - } else if (this.isVimeo && this.captions.active) { - this.embed.enableTextTrack(this.language); } - }, + /** + * Any ad error handling comes through here + * @param {Event} event + */ - // Get the tracks - getTracks: function getTracks() { - // Return empty array at least - if (utils.is.nullOrUndefined(this.media)) { - return []; + }, { + key: 'onAdError', + value: function onAdError(event) { + this.cancel(); + this.player.debug.log('Ads error', event); } - // Only get accepted kinds - return Array.from(this.media.textTracks || []).filter(function (track) { - return ['captions', 'subtitles'].includes(track.kind); - }); - }, + /** + * Setup hooks for Plyr and window events. This ensures + * the mid- and post-roll launch at the correct time. And + * resize the advertisement when the player resizes + */ + }, { + key: 'listeners', + value: function listeners() { + var _this7 = this; - // Get the current track for the current language - getCurrentTrack: function getCurrentTrack() { - var _this2 = this; + var container = this.player.elements.container; - return captions.getTracks.call(this).find(function (track) { - return track.language.toLowerCase() === _this2.language; - }); - }, + var time = void 0; + // Add listeners to the required events + this.player.on('ended', function () { + _this7.loader.contentComplete(); + }); - // Display active caption if it contains text - setCue: function setCue(input) { - // Get the track from the event if needed - var track = utils.is.event(input) ? input.target : input; - var activeCues = track.activeCues; + this.player.on('seeking', function () { + time = _this7.player.currentTime; + return time; + }); - var active = activeCues.length && activeCues[0]; - var currentTrack = captions.getCurrentTrack.call(this); + this.player.on('seeked', function () { + var seekedTime = _this7.player.currentTime; - // Only display current track - if (track !== currentTrack) { - return; + _this7.cuePoints.forEach(function (cuePoint, index) { + if (time < cuePoint && cuePoint < seekedTime) { + _this7.manager.discardAdBreak(); + _this7.cuePoints.splice(index, 1); + } + }); + }); + + // Listen to the resizing of the window. And resize ad accordingly + // TODO: eventually implement ResizeObserver + window.addEventListener('resize', function () { + _this7.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL); + }); } - // Display a cue, if there is one - if (utils.is.cue(active)) { - captions.setText.call(this, active.getCueAsHTML()); - } else { - captions.setText.call(this, null); + /** + * Initialize the adsManager and start playing advertisements + */ + + }, { + key: 'play', + value: function play() { + var _this8 = this; + + var container = this.player.elements.container; + + + if (!this.managerPromise) { + return; + } + + // Play the requested advertisement whenever the adsManager is ready + this.managerPromise.then(function () { + // Initialize the container. Must be done via a user action on mobile devices + _this8.elements.displayContainer.initialize(); + + try { + if (!_this8.initialized) { + // Initialize the ads manager. Ad rules playlist will start at this time + _this8.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL); + + // Call play to start showing the ad. Single video and overlay ads will + // start at this time; the call will be ignored for ad rules + _this8.manager.start(); + } + + _this8.initialized = true; + } catch (adError) { + // An error may be thrown if there was a problem with the + // VAST response + _this8.onAdError(adError); + } + }); } - utils.dispatchEvent.call(this, this.media, 'cuechange'); - }, + /** + * Resume our video. + */ + }, { + key: 'resumeContent', + value: function resumeContent() { + // Hide the advertisement container + this.elements.container.style.zIndex = ''; - // Set the current caption - setText: function setText(input) { - // Requires UI - if (!this.supported.ui) { - return; + // Ad is stopped + this.playing = false; + + // Play our video + if (this.player.currentTime < this.player.duration) { + this.player.play(); + } } - if (utils.is.element(this.elements.captions)) { - var content = utils.createElement('span'); + /** + * Pause our video + */ - // Empty the container - utils.emptyElement(this.elements.captions); + }, { + key: 'pauseContent', + value: function pauseContent() { + // Show the advertisement container + this.elements.container.style.zIndex = 3; - // Default to empty - var caption = !utils.is.nullOrUndefined(input) ? input : ''; + // Ad is playing. + this.playing = true; - // Set the span content - if (utils.is.string(caption)) { - content.textContent = caption.trim(); - } else { - content.appendChild(caption); + // Pause our video. + this.player.pause(); + } + + /** + * Destroy the adsManager so we can grab new ads after this. If we don't then we're not + * allowed to call new ads based on google policies, as they interpret this as an accidental + * video requests. https://developers.google.com/interactive- + * media-ads/docs/sdks/android/faq#8 + */ + + }, { + key: 'cancel', + value: function cancel() { + // Pause our video + if (this.initialized) { + this.resumeContent(); } - // Set new caption text - this.elements.captions.appendChild(content); - } else { - this.debug.warn('No captions element to render to'); + // Tell our instance that we're done for now + this.trigger('ERROR'); + + // Re-create our adsManager + this.loadAds(); } - }, + /** + * Re-create our adsManager + */ - // Display captions container and button (for initialization) - show: function show() { - // If there's no caption toggle, bail - if (!utils.is.element(this.elements.buttons.captions)) { - return; + }, { + key: 'loadAds', + value: function loadAds() { + var _this9 = this; + + // Tell our adsManager to go bye bye + this.managerPromise.then(function () { + // Destroy our adsManager + if (_this9.manager) { + _this9.manager.destroy(); + } + + // Re-set our adsManager promises + _this9.managerPromise = new Promise(function (resolve) { + _this9.on('ADS_MANAGER_LOADED', resolve); + _this9.player.debug.log(_this9.manager); + }); + + // Now request some new advertisements + _this9.requestAds(); + }); } - // Try to load the value from storage - var active = this.storage.get('captions'); + /** + * Handles callbacks after an ad event was invoked + * @param {string} event - Event type + */ - // Otherwise fall back to the default config - if (!utils.is.boolean(active)) { - active = this.config.captions.active; - } else { - this.captions.active = active; + }, { + key: 'trigger', + value: function trigger(event) { + var _this10 = this; + + var handlers = this.events[event]; + + if (utils.is.array(handlers)) { + handlers.forEach(function (handler) { + if (utils.is.function(handler)) { + handler.call(_this10); + } + }); + } } - if (active) { - utils.toggleClass(this.elements.container, this.config.classNames.captions.active, true); - utils.toggleState(this.elements.buttons.captions, true); + /** + * Add event listeners + * @param {string} event - Event type + * @param {function} callback - Callback for when event occurs + * @return {Ads} + */ + + }, { + key: 'on', + value: function on(event, callback) { + if (!utils.is.array(this.events[event])) { + this.events[event] = []; + } + + this.events[event].push(callback); + + return this; } - } -}; + + /** + * Setup a safety timer for when the ad network doesn't respond for whatever reason. + * The advertisement has 12 seconds to get its things together. We stop this timer when the + * advertisement is playing, or when a user action is required to start, then we clear the + * timer on ad ready + * @param {number} time + * @param {string} from + */ + + }, { + key: 'startSafetyTimer', + value: function startSafetyTimer(time, from) { + var _this11 = this; + + this.player.debug.log('Safety timer invoked from: ' + from); + + this.safetyTimer = setTimeout(function () { + _this11.cancel(); + _this11.clearSafetyTimer('startSafetyTimer()'); + }, time); + } + + /** + * Clear our safety timer(s) + * @param {string} from + */ + + }, { + key: 'clearSafetyTimer', + value: function clearSafetyTimer(from) { + if (!utils.is.nullOrUndefined(this.safetyTimer)) { + this.player.debug.log('Safety timer cleared from: ' + from); + + clearTimeout(this.safetyTimer); + this.safetyTimer = null; + } + } + }, { + key: 'tagUrl', + get: function get$$1() { + var params = { + AV_PUBLISHERID: '58c25bb0073ef448b1087ad6', + AV_CHANNELID: '5a0458dc28a06145e4519d21', + AV_URL: location.hostname, + cb: Date.now(), + AV_WIDTH: 640, + AV_HEIGHT: 480, + AV_CDIM2: this.publisherId + }; + + var base = 'https://go.aniview.com/api/adserver6/vast/'; + + return base + '?' + utils.buildUrlParams(params); + } + }]); + return Ads; +}(); // ========================================================================== // YouTube plugin @@ -5017,7 +5062,9 @@ var youtube = { youtube.ready.call(this); } else { // Load the API - utils.loadScript(this.config.urls.youtube.api); + utils.loadScript(this.config.urls.youtube.api).catch(function (error) { + _this.debug.warn('YouTube API failed to load', error); + }); // Setup callback for the API // YouTube has it's own system of course... @@ -5430,8 +5477,10 @@ var vimeo = { // Load the API if not already if (!utils.is.object(window.Vimeo)) { - utils.loadScript(this.config.urls.vimeo.api, function () { + utils.loadScript(this.config.urls.vimeo.api).then(function () { vimeo.ready.call(_this); + }).catch(function (error) { + _this.debug.warn('Vimeo API failed to load', error); }); } else { vimeo.ready.call(this); @@ -6189,6 +6238,9 @@ var Plyr = function () { return; } + // Create listeners + this.listeners = new Listeners(this); + // Setup local storage for user settings this.storage = new Storage(this); @@ -6204,9 +6256,6 @@ var Plyr = function () { // Allow focus to be captured this.elements.container.setAttribute('tabindex', 0); - // Global listeners - listeners.global.call(this); - // Add style hook ui.addStyleHook.call(this); @@ -6226,6 +6275,12 @@ var Plyr = function () { ui.build.call(this); } + // Container listeners + this.listeners.container(); + + // Global listeners + this.listeners.global(true); + // Setup fullscreen this.fullscreen = new Fullscreen(this); @@ -6250,10 +6305,15 @@ var Plyr = function () { * Play the media, or play the advertisement (if they are not blocked) */ value: function play() { - // TODO: Always return a promise? - if (this.ads.enabled && !this.ads.initialized && !this.ads.blocked) { - this.ads.play(); - return null; + var _this2 = this; + + // If ads are enabled, wait for them first + if (this.ads.enabled && !this.ads.initialized) { + return this.ads.managerPromise.then(function () { + return _this2.ads.play(); + }).catch(function () { + return _this2.media.play(); + }); } // Return the promise (for HTML5) @@ -6438,7 +6498,7 @@ var Plyr = function () { }, { key: 'toggleControls', value: function toggleControls(toggle) { - var _this2 = this; + var _this3 = this; // We need controls of course... if (!utils.is.element(this.elements.controls)) { @@ -6507,24 +6567,24 @@ var Plyr = function () { if (!show || this.playing) { this.timers.controls = setTimeout(function () { // If the mouse is over the controls (and not entering fullscreen), bail - if ((_this2.elements.controls.pressed || _this2.elements.controls.hover) && !isEnterFullscreen) { + if ((_this3.elements.controls.pressed || _this3.elements.controls.hover) && !isEnterFullscreen) { return; } // Restore transition behaviour - if (!utils.hasClass(_this2.elements.container, _this2.config.classNames.hideControls)) { - utils.toggleClass(_this2.elements.controls, _this2.config.classNames.noTransition, false); + if (!utils.hasClass(_this3.elements.container, _this3.config.classNames.hideControls)) { + utils.toggleClass(_this3.elements.controls, _this3.config.classNames.noTransition, false); } // Check if controls toggled - var toggled = utils.toggleClass(_this2.elements.container, _this2.config.classNames.hideControls, true); + var toggled = utils.toggleClass(_this3.elements.container, _this3.config.classNames.hideControls, true); // Trigger event and close menu if (toggled) { - utils.dispatchEvent.call(_this2, _this2.media, 'controlshidden'); + utils.dispatchEvent.call(_this3, _this3.media, 'controlshidden'); - if (_this2.config.controls.includes('settings') && !utils.is.empty(_this2.config.settings)) { - controls.toggleMenu.call(_this2, false); + if (_this3.config.controls.includes('settings') && !utils.is.empty(_this3.config.settings)) { + controls.toggleMenu.call(_this3, false); } } }, delay); @@ -6566,7 +6626,7 @@ var Plyr = function () { }, { key: 'destroy', value: function destroy(callback) { - var _this3 = this; + var _this4 = this; var soft = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; @@ -6575,22 +6635,22 @@ var Plyr = function () { document.body.style.overflow = ''; // GC for embed - _this3.embed = null; + _this4.embed = null; // If it's a soft destroy, make minimal changes if (soft) { - if (Object.keys(_this3.elements).length) { + if (Object.keys(_this4.elements).length) { // Remove elements - utils.removeElement(_this3.elements.buttons.play); - utils.removeElement(_this3.elements.captions); - utils.removeElement(_this3.elements.controls); - utils.removeElement(_this3.elements.wrapper); + utils.removeElement(_this4.elements.buttons.play); + utils.removeElement(_this4.elements.captions); + utils.removeElement(_this4.elements.controls); + utils.removeElement(_this4.elements.wrapper); // Clear for GC - _this3.elements.buttons.play = null; - _this3.elements.captions = null; - _this3.elements.controls = null; - _this3.elements.wrapper = null; + _this4.elements.buttons.play = null; + _this4.elements.captions = null; + _this4.elements.controls = null; + _this4.elements.wrapper = null; } // Callback @@ -6599,18 +6659,21 @@ var Plyr = function () { } } else { // Replace the container with the original element provided - utils.replaceElement(_this3.elements.original, _this3.elements.container); + utils.replaceElement(_this4.elements.original, _this4.elements.container); + + // Unbind global listeners + _this4.listeners.global(false); // Event - utils.dispatchEvent.call(_this3, _this3.elements.original, 'destroyed', true); + utils.dispatchEvent.call(_this4, _this4.elements.original, 'destroyed', true); // Callback if (utils.is.function(callback)) { - callback.call(_this3.elements.original); + callback.call(_this4.elements.original); } // Clear for GC - _this3.elements = null; + _this4.elements = null; } }; |