From 550bd543e3928f7aba1a368a8be5a1e3ae86bd05 Mon Sep 17 00:00:00 2001 From: John Law Date: Wed, 12 Feb 2020 00:06:28 -0800 Subject: Added missing full screen options for type definition --- src/js/plyr.d.ts | 1 + 1 file changed, 1 insertion(+) (limited to 'src/js') diff --git a/src/js/plyr.d.ts b/src/js/plyr.d.ts index cd204a6f..7c7f28b1 100644 --- a/src/js/plyr.d.ts +++ b/src/js/plyr.d.ts @@ -507,6 +507,7 @@ declare namespace Plyr { enabled?: boolean; fallback?: boolean; allowAudio?: boolean; + iosNative?: boolean; } interface CaptionOptions { -- cgit v1.2.3 From 0cf5d25a7f28203553b1fa2db5c600995c284b65 Mon Sep 17 00:00:00 2001 From: Hugues Date: Thu, 20 Feb 2020 12:57:47 +0000 Subject: catch error in setPlaybackRate on Vimeo --- src/js/plugins/vimeo.js | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src/js') diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js index fa965d8e..010cf5f7 100644 --- a/src/js/plugins/vimeo.js +++ b/src/js/plugins/vimeo.js @@ -204,6 +204,9 @@ const vimeo = { player.embed.setPlaybackRate(input).then(() => { speed = input; triggerEvent.call(player, player.media, 'ratechange'); + }).catch(() => { + // Cannot set Playback Rate, Video is probably not on Pro account + player.options.speed = [1]; }); }, }); -- cgit v1.2.3 From 81b41be750c9eddbabafdbd304614d827cd0ca82 Mon Sep 17 00:00:00 2001 From: max Date: Tue, 25 Feb 2020 17:53:44 +0100 Subject: preview-thumbnails via src:callback() --- src/js/plugins/preview-thumbnails.js | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) (limited to 'src/js') diff --git a/src/js/plugins/preview-thumbnails.js b/src/js/plugins/preview-thumbnails.js index 86eeebc8..e313a01f 100644 --- a/src/js/plugins/preview-thumbnails.js +++ b/src/js/plugins/preview-thumbnails.js @@ -137,19 +137,31 @@ class PreviewThumbnails { throw new Error('Missing previewThumbnails.src config attribute'); } - // If string, convert into single-element list - const urls = is.string(src) ? [src] : src; - // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails - const promises = urls.map(u => this.getThumbnail(u)); - - Promise.all(promises).then(() => { + // Resolve promise + const exec_resolve = () => { // Sort smallest to biggest (e.g., [120p, 480p, 1080p]) this.thumbnails.sort((x, y) => x.height - y.height); this.player.debug.log('Preview thumbnails', this.thumbnails); resolve(); - }); + }; + // Via callback() + if( typeof(src) == 'function' ) { + // Ask + this.thumbnails = src(); + // Resolve + exec_resolve(); + } + // VTT urls + else { + // If string, convert into single-element list + const urls = is.string(src) ? [src] : src; + // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails + const promises = urls.map(u => this.getThumbnail(u)); + // Resolve + Promise.all(promises).then(exec_resolve); + } }); } -- cgit v1.2.3 From b212b25a9eca8a440a05d53b744aa56f646b2929 Mon Sep 17 00:00:00 2001 From: max Date: Wed, 26 Feb 2020 10:35:08 +0100 Subject: Fixes --- src/js/plugins/preview-thumbnails.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) (limited to 'src/js') diff --git a/src/js/plugins/preview-thumbnails.js b/src/js/plugins/preview-thumbnails.js index e313a01f..7e9f0dc9 100644 --- a/src/js/plugins/preview-thumbnails.js +++ b/src/js/plugins/preview-thumbnails.js @@ -138,7 +138,7 @@ class PreviewThumbnails { } // Resolve promise - const exec_resolve = () => { + const resolvePromise = () => { // Sort smallest to biggest (e.g., [120p, 480p, 1080p]) this.thumbnails.sort((x, y) => x.height - y.height); @@ -147,11 +147,14 @@ class PreviewThumbnails { resolve(); }; // Via callback() - if( typeof(src) == 'function' ) { + if (typeof(src) == 'function') { // Ask - this.thumbnails = src(); - // Resolve - exec_resolve(); + let that = this; + src(function(thumbnails) { + that.thumbnails = thumbnails; + // Resolve + resolvePromise(); + }); } // VTT urls else { @@ -160,7 +163,7 @@ class PreviewThumbnails { // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails const promises = urls.map(u => this.getThumbnail(u)); // Resolve - Promise.all(promises).then(exec_resolve); + Promise.all(promises).then(resolvePromise); } }); } -- cgit v1.2.3 From ace682abbdeba13ea3664e9dde38a903a4a5da5e Mon Sep 17 00:00:00 2001 From: max Date: Wed, 26 Feb 2020 10:41:26 +0100 Subject: Fixes2 --- src/js/plugins/preview-thumbnails.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src/js') diff --git a/src/js/plugins/preview-thumbnails.js b/src/js/plugins/preview-thumbnails.js index 7e9f0dc9..4c13ab33 100644 --- a/src/js/plugins/preview-thumbnails.js +++ b/src/js/plugins/preview-thumbnails.js @@ -138,7 +138,7 @@ class PreviewThumbnails { } // Resolve promise - const resolvePromise = () => { + const sortAndResolve = () => { // Sort smallest to biggest (e.g., [120p, 480p, 1080p]) this.thumbnails.sort((x, y) => x.height - y.height); @@ -147,13 +147,13 @@ class PreviewThumbnails { resolve(); }; // Via callback() - if (typeof(src) == 'function') { + if (is.function(src)) { // Ask let that = this; src(function(thumbnails) { that.thumbnails = thumbnails; // Resolve - resolvePromise(); + sortAndResolve(); }); } // VTT urls @@ -163,7 +163,7 @@ class PreviewThumbnails { // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails const promises = urls.map(u => this.getThumbnail(u)); // Resolve - Promise.all(promises).then(resolvePromise); + Promise.all(promises).then(sortAndResolve); } }); } -- cgit v1.2.3 From fd353225c27fa210a036b87a847336bf10957ed1 Mon Sep 17 00:00:00 2001 From: Steejo Date: Mon, 9 Mar 2020 23:18:19 +0000 Subject: Ads plugin fixes to allow multiple VAST requests --- src/js/plugins/ads.js | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) (limited to 'src/js') diff --git a/src/js/plugins/ads.js b/src/js/plugins/ads.js index 6b4fca10..79e6e5d9 100644 --- a/src/js/plugins/ads.js +++ b/src/js/plugins/ads.js @@ -172,6 +172,17 @@ class Ads { // 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, this.player.media); + // 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, + event => this.onAdsManagerLoaded(event), + false, + ); + this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false); + // Request video ads to be pre-loaded this.requestAds(); } @@ -183,17 +194,6 @@ class Ads { const { container } = this.player.elements; 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, - event => this.onAdsManagerLoaded(event), - false, - ); - this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false); - // Request video ads const request = new google.ima.AdsRequest(); request.adTagUrl = this.tagUrl; @@ -369,7 +369,14 @@ class Ads { // 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(); + if (player.ended){ + this.loadAds(); + } + else + { + // The SDK won't allow new ads to be called without receiving a contentComplete() + this.loader.contentComplete(); + } break; @@ -563,6 +570,8 @@ class Ads { this.on('loaded', resolve); this.player.debug.log(this.manager); }); + // Now that the manager has been destroyed set it to also be un-initialized + this.initialized = false; // Now request some new advertisements this.requestAds(); -- cgit v1.2.3 From c7bf0c5c03a5c7716a39a0f2f5a11681eedbac7f Mon Sep 17 00:00:00 2001 From: Jesper Date: Tue, 10 Mar 2020 09:19:34 +0100 Subject: Fix prototype used for selector matcher function --- src/js/utils/elements.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/js') diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index b88aad0c..43f46416 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -221,7 +221,7 @@ export function hasClass(element, className) { // Element matches selector export function matches(element, selector) { - const prototype = { Element }; + const prototype = Element.prototype; function match() { return Array.from(document.querySelectorAll(selector)).includes(this); -- cgit v1.2.3 From 99ae4eb3c5aa335926ea76e868d236112404dc22 Mon Sep 17 00:00:00 2001 From: Jesper Date: Tue, 10 Mar 2020 09:30:42 +0100 Subject: Compare fullscreenElement with shadowroot host if player is in shadow DOM --- src/js/fullscreen.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/js') diff --git a/src/js/fullscreen.js b/src/js/fullscreen.js index c74b3406..6dc069b2 100644 --- a/src/js/fullscreen.js +++ b/src/js/fullscreen.js @@ -118,7 +118,7 @@ class Fullscreen { const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`]; - return element === this.target; + return (element && element.shadowRoot) ? element === this.target.getRootNode().host : element === this.target; } // Get target element -- cgit v1.2.3 From 71928443f317e624ab94ff18e207447f06f745ad Mon Sep 17 00:00:00 2001 From: ydylla Date: Mon, 23 Mar 2020 22:50:19 +0100 Subject: silence all internal play promises --- src/js/fullscreen.js | 3 ++- src/js/html5.js | 3 ++- src/js/listeners.js | 11 ++++++----- src/js/plugins/ads.js | 3 ++- src/js/plyr.js | 5 +++-- src/js/utils/promise.js | 27 +++++++++++++++++++++++++++ 6 files changed, 42 insertions(+), 10 deletions(-) create mode 100644 src/js/utils/promise.js (limited to 'src/js') diff --git a/src/js/fullscreen.js b/src/js/fullscreen.js index c74b3406..5da89e9c 100644 --- a/src/js/fullscreen.js +++ b/src/js/fullscreen.js @@ -8,6 +8,7 @@ import browser from './utils/browser'; import { getElements, hasClass, toggleClass } from './utils/elements'; import { on, triggerEvent } from './utils/events'; import is from './utils/is'; +import { silencePromise } from './utils/promise'; class Fullscreen { constructor(player) { @@ -268,7 +269,7 @@ class Fullscreen { // iOS native fullscreen if (browser.isIos && this.player.config.fullscreen.iosNative) { this.target.webkitExitFullscreen(); - this.player.play(); + silencePromise(this.player.play()); } else if (!Fullscreen.native || this.forceFallback) { this.toggleFallback(false); } else if (!this.prefix) { diff --git a/src/js/html5.js b/src/js/html5.js index 0591a709..6e8c6483 100644 --- a/src/js/html5.js +++ b/src/js/html5.js @@ -6,6 +6,7 @@ import support from './support'; import { removeElement } from './utils/elements'; import { triggerEvent } from './utils/events'; import is from './utils/is'; +import { silencePromise } from './utils/promise'; import { setAspectRatio } from './utils/style'; const html5 = { @@ -101,7 +102,7 @@ const html5 = { // Resume playing if (!paused) { - player.play(); + silencePromise(player.play()); } }); diff --git a/src/js/listeners.js b/src/js/listeners.js index 6a0046ee..d134a350 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -9,6 +9,7 @@ import browser from './utils/browser'; import { getElement, getElements, matches, toggleClass } from './utils/elements'; import { off, on, once, toggleListener, triggerEvent } from './utils/events'; import is from './utils/is'; +import { silencePromise } from './utils/promise'; import { getAspectRatio, setAspectRatio } from './utils/style'; class Listeners { @@ -99,7 +100,7 @@ class Listeners { case 75: // Space and K key if (!repeat) { - player.togglePlay(); + silencePromise(player.togglePlay()); } break; @@ -431,9 +432,9 @@ class Listeners { if (player.ended) { this.proxy(event, player.restart, 'restart'); - this.proxy(event, player.play, 'play'); + this.proxy(event, () => { silencePromise(player.play()) }, 'play'); } else { - this.proxy(event, player.togglePlay, 'play'); + this.proxy(event, () => { silencePromise(player.togglePlay()) }, 'play'); } }); } @@ -539,7 +540,7 @@ class Listeners { // Play/pause toggle if (elements.buttons.play) { Array.from(elements.buttons.play).forEach(button => { - this.bind(button, 'click', player.togglePlay, 'play'); + this.bind(button, 'click', () => { silencePromise(player.togglePlay()) }, 'play'); }); } @@ -681,7 +682,7 @@ class Listeners { // If we're done seeking and it was playing, resume playback if (play && done) { seek.removeAttribute(attribute); - player.play(); + silencePromise(player.play()); } else if (!done && player.playing) { seek.setAttribute(attribute, ''); player.pause(); diff --git a/src/js/plugins/ads.js b/src/js/plugins/ads.js index 6b4fca10..62def372 100644 --- a/src/js/plugins/ads.js +++ b/src/js/plugins/ads.js @@ -11,6 +11,7 @@ import { triggerEvent } from '../utils/events'; import i18n from '../utils/i18n'; import is from '../utils/is'; import loadScript from '../utils/load-script'; +import { silencePromise } from '../utils/promise'; import { formatTime } from '../utils/time'; import { buildUrlParams } from '../utils/urls'; @@ -510,7 +511,7 @@ class Ads { this.playing = false; // Play video - this.player.media.play(); + silencePromise(this.player.media.play()); } /** diff --git a/src/js/plyr.js b/src/js/plyr.js index 00d33463..00b95a5f 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -27,6 +27,7 @@ import is from './utils/is'; import loadSprite from './utils/load-sprite'; import { clamp } from './utils/numbers'; import { cloneDeep, extend } from './utils/objects'; +import { silencePromise } from './utils/promise'; import { getAspectRatio, reduceAspectRatio, setAspectRatio, validateRatio } from './utils/style'; import { parseUrl } from './utils/urls'; @@ -303,7 +304,7 @@ class Plyr { // Autoplay if required if (this.isHTML5 && this.config.autoplay) { - setTimeout(() => this.play(), 10); + setTimeout(() => silencePromise(this.play()), 10); } // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek @@ -356,7 +357,7 @@ class Plyr { // Intecept play with ads if (this.ads && this.ads.enabled) { - this.ads.managerPromise.then(() => this.ads.play()).catch(() => this.media.play()); + this.ads.managerPromise.then(() => this.ads.play()).catch(() => silencePromise(this.media.play())); } // Return the promise (for HTML5) diff --git a/src/js/utils/promise.js b/src/js/utils/promise.js new file mode 100644 index 00000000..42fcc2c3 --- /dev/null +++ b/src/js/utils/promise.js @@ -0,0 +1,27 @@ +/** + * Returns whether an object is `Promise`-like (i.e. has a `then` method). + * + * @param {Object} value + * An object that may or may not be `Promise`-like. + * + * @return {boolean} + * Whether or not the object is `Promise`-like. + */ +export function isPromise(value) { + return value !== undefined && value !== null && typeof value.then === 'function'; +} + +/** + * Silence a Promise-like object. + * + * This is useful for avoiding non-harmful, but potentially confusing "uncaught + * play promise" rejection error messages. + * + * @param {Object} value + * An object that may or may not be `Promise`-like. + */ +export function silencePromise(value) { + if (isPromise(value)) { + value.then(null, () => {}); + } +} -- cgit v1.2.3 From ad63af5096e014785bd22eac24bc8030c0dc70d6 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sun, 29 Mar 2020 12:13:24 +1100 Subject: Added prettier script --- src/js/listeners.js | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) (limited to 'src/js') diff --git a/src/js/listeners.js b/src/js/listeners.js index d134a350..cd468083 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -432,9 +432,21 @@ class Listeners { if (player.ended) { this.proxy(event, player.restart, 'restart'); - this.proxy(event, () => { silencePromise(player.play()) }, 'play'); + this.proxy( + event, + () => { + silencePromise(player.play()); + }, + 'play', + ); } else { - this.proxy(event, () => { silencePromise(player.togglePlay()) }, 'play'); + this.proxy( + event, + () => { + silencePromise(player.togglePlay()); + }, + 'play', + ); } }); } @@ -540,7 +552,14 @@ class Listeners { // Play/pause toggle if (elements.buttons.play) { Array.from(elements.buttons.play).forEach(button => { - this.bind(button, 'click', () => { silencePromise(player.togglePlay()) }, 'play'); + this.bind( + button, + 'click', + () => { + silencePromise(player.togglePlay()); + }, + 'play', + ); }); } @@ -611,7 +630,7 @@ class Listeners { controls.toggleMenu.call(player, event); }, null, - false + false, ); // Can't be passive as we're preventing default // Settings menu - keyboard toggle -- cgit v1.2.3 From d06881783d7c8a9faa5d902da5ec33bc74f3aa38 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 30 Mar 2020 17:04:43 +1100 Subject: Formatting fixes --- src/js/plugins/preview-thumbnails.js | 15 ++++++++++++--- src/js/plugins/vimeo.js | 17 ++++++++++------- src/js/utils/elements.js | 2 +- src/js/utils/events.js | 2 +- 4 files changed, 24 insertions(+), 12 deletions(-) (limited to 'src/js') diff --git a/src/js/plugins/preview-thumbnails.js b/src/js/plugins/preview-thumbnails.js index 290ce949..60033331 100644 --- a/src/js/plugins/preview-thumbnails.js +++ b/src/js/plugins/preview-thumbnails.js @@ -628,7 +628,10 @@ class PreviewThumbnails { } determineContainerAutoSizing() { - if (this.elements.thumb.imageContainer.clientHeight > 20 || this.elements.thumb.imageContainer.clientWidth > 20) { + if ( + this.elements.thumb.imageContainer.clientHeight > 20 || + this.elements.thumb.imageContainer.clientWidth > 20 + ) { // This will prevent auto sizing in this.setThumbContainerSizeAndPos() this.sizeSpecifiedInCSS = true; } @@ -640,10 +643,16 @@ class PreviewThumbnails { const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio); this.elements.thumb.imageContainer.style.height = `${this.thumbContainerHeight}px`; this.elements.thumb.imageContainer.style.width = `${thumbWidth}px`; - } else if (this.elements.thumb.imageContainer.clientHeight > 20 && this.elements.thumb.imageContainer.clientWidth < 20) { + } else if ( + this.elements.thumb.imageContainer.clientHeight > 20 && + this.elements.thumb.imageContainer.clientWidth < 20 + ) { const thumbWidth = Math.floor(this.elements.thumb.imageContainer.clientHeight * this.thumbAspectRatio); this.elements.thumb.imageContainer.style.width = `${thumbWidth}px`; - } else if (this.elements.thumb.imageContainer.clientHeight < 20 && this.elements.thumb.imageContainer.clientWidth > 20) { + } else if ( + this.elements.thumb.imageContainer.clientHeight < 20 && + this.elements.thumb.imageContainer.clientWidth > 20 + ) { const thumbHeight = Math.floor(this.elements.thumb.imageContainer.clientWidth / this.thumbAspectRatio); this.elements.thumb.imageContainer.style.height = `${thumbHeight}px`; } diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js index 010cf5f7..8f999153 100644 --- a/src/js/plugins/vimeo.js +++ b/src/js/plugins/vimeo.js @@ -201,13 +201,16 @@ const vimeo = { return speed; }, set(input) { - player.embed.setPlaybackRate(input).then(() => { - speed = input; - triggerEvent.call(player, player.media, 'ratechange'); - }).catch(() => { - // Cannot set Playback Rate, Video is probably not on Pro account - player.options.speed = [1]; - }); + player.embed + .setPlaybackRate(input) + .then(() => { + speed = input; + triggerEvent.call(player, player.media, 'ratechange'); + }) + .catch(() => { + // Cannot set Playback Rate, Video is probably not on Pro account + player.options.speed = [1]; + }); }, }); diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index bdf18bfd..acff0dd9 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -221,7 +221,7 @@ export function hasClass(element, className) { // Element matches selector export function matches(element, selector) { - const {prototype} = Element; + const { prototype } = Element; function match() { return Array.from(document.querySelectorAll(selector)).includes(this); diff --git a/src/js/utils/events.js b/src/js/utils/events.js index 31571b2d..48300b6b 100644 --- a/src/js/utils/events.js +++ b/src/js/utils/events.js @@ -90,7 +90,7 @@ export function triggerEvent(element, type = '', bubbles = false, detail = {}) { // Create and dispatch the event const event = new CustomEvent(type, { bubbles, - detail: { ...detail, plyr: this,}, + detail: { ...detail, plyr: this }, }); // Dispatch the event -- cgit v1.2.3 From 49ed2cac4eff3ff3eae7a2c72e5280a302906f7d Mon Sep 17 00:00:00 2001 From: Som Meaden Date: Sat, 4 Apr 2020 13:43:51 +1000 Subject: This is a PR to allow for contextual content to be included in fullscreen (or fallback) mode. This means arbitrary elements (extensions to the basic player UI) can be overlaid and remain visible when the player switches to fullscreen. Example use-cases include: - display of video title or other metadata (see the included demo) - alternative access to menu items, such as a searchable captions list (in cases where many hundreds of languages are available) - custom share dialogs - integrated playlists with 'playing next' overlays This approach / PR is just an example of how this feature could work and aims to keep Plyr complexity to a minimum (while enabling some fairly interesting integrations). It utilises a single config option, and does away with the need for injecting bespoke APIs or elements into the player context on a per-project basis. Or trying to mess with what is a pretty slick, but tightly coupled system. For the user: A new `fullscreen.container` attribute is used to provide a container selector. The container must be an ancestor of the player, otherwise it's ignored. When toggling fullscreen mode, this container is now used in place of the player. Hovering over any children of the container is the same as hovering over the controls. The exception is where the player and the child share a common ancestor (that's not the fullscreen container) ... sounds complex but it's not. You can also gain pretty fine control this way with pointer events. Under the hood: it adds a `utils/elements/closest` helper method to find the right ancestor. If found this is returned as the fullscreen target in place of the player container. Fullscreen is instantiated slightly earlier in the setup so this container is available for the `listeners.controls` call. In here we add some more 'mouseenter/mouseleave' listeners to any direct descendants of the container, that aren't also ancestors of the player. And that's it. No extra classes, nothing else. There are some style changes to the demo (top margin on the player) but these would be project specific. Thanks for reading. --- src/js/config/defaults.js | 3 +++ src/js/fullscreen.js | 9 +++++++-- src/js/listeners.js | 11 +++++++++++ src/js/plyr.js | 7 ++++--- src/js/utils/elements.js | 22 ++++++++++++++++++++++ 5 files changed, 47 insertions(+), 5 deletions(-) (limited to 'src/js') diff --git a/src/js/config/defaults.js b/src/js/config/defaults.js index c299a3c9..31ae6983 100644 --- a/src/js/config/defaults.js +++ b/src/js/config/defaults.js @@ -115,6 +115,9 @@ const defaults = { enabled: true, // Allow fullscreen? fallback: true, // Fallback using full viewport/window iosNative: false, // Use the native fullscreen in iOS (disables custom controls) + // Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode + // Non-ancestors of the player element will be ignored + // container: null, // defaults to the player element }, // Local storage diff --git a/src/js/fullscreen.js b/src/js/fullscreen.js index 4d3c89ac..0db4aa3f 100644 --- a/src/js/fullscreen.js +++ b/src/js/fullscreen.js @@ -5,7 +5,7 @@ // ========================================================================== import browser from './utils/browser'; -import { getElements, hasClass, toggleClass } from './utils/elements'; +import { getElements, hasClass, toggleClass, closest } from './utils/elements'; import { on, triggerEvent } from './utils/events'; import is from './utils/is'; import { silencePromise } from './utils/promise'; @@ -25,6 +25,11 @@ class Fullscreen { // Force the use of 'full window/browser' rather than fullscreen this.forceFallback = player.config.fullscreen.fallback === 'force'; + // Get the fullscreen element + // Checks container is an ancestor, defaults to null + this.player.elements.fullscreen = player.config.fullscreen.container + && closest(this.player.elements.container, player.config.fullscreen.container); + // Register event listeners // Handle event (incase user presses escape etc) on.call( @@ -126,7 +131,7 @@ class Fullscreen { get target() { return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media - : this.player.elements.container; + : this.player.elements.fullscreen || this.player.elements.container; } onChange() { diff --git a/src/js/listeners.js b/src/js/listeners.js index cd468083..ede8d88c 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -814,6 +814,17 @@ class Listeners { elements.controls.hover = !player.touch && event.type === 'mouseenter'; }); + // Also update controls.hover state for any non-player children of fullscreen element (as above) + if (elements.fullscreen) { + for (let i = 0; i < elements.fullscreen.children.length; i++) { + if (!elements.fullscreen.children[i].contains(elements.container)) { + this.bind(elements.fullscreen.children[i], 'mouseenter mouseleave', event => { + elements.controls.hover = !player.touch && event.type === 'mouseenter'; + }); + } + } + } + // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting) this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => { elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type); diff --git a/src/js/plyr.js b/src/js/plyr.js index 00b95a5f..a69e793d 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -80,6 +80,7 @@ class Plyr { // Elements cache this.elements = { container: null, + fullscreen: null, captions: null, buttons: {}, display: {}, @@ -282,6 +283,9 @@ class Plyr { }); } + // Setup fullscreen + this.fullscreen = new Fullscreen(this); + // Setup interface // If embed but not fully supported, build interface now to avoid flash of controls if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) { @@ -294,9 +298,6 @@ class Plyr { // Global listeners this.listeners.global(); - // Setup fullscreen - this.fullscreen = new Fullscreen(this); - // Setup ads if provided if (this.config.ads.enabled) { this.ads = new Ads(this); diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index bdf18bfd..f782fc3e 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -237,6 +237,28 @@ export function matches(element, selector) { return method.call(element, selector); } +// Closest ancestor element matching selector (also tests element itself) +export function closest(element, selector) { + const {prototype} = Element; + + // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill + function closestElement() { + let el = this; + + do { + if (matches.matches(el, selector)) return el; + el = el.parentElement || el.parentNode; + } while (el !== null && el.nodeType === 1); + return null; + } + + const method = + prototype.closest || + closestElement; + + return method.call(element, selector); +} + // Find all elements export function getElements(selector) { return this.elements.container.querySelectorAll(selector); -- cgit v1.2.3 From 12ab1ed1441db90d74b9c2226299de3050f30949 Mon Sep 17 00:00:00 2001 From: Som Meaden Date: Sat, 4 Apr 2020 20:00:43 +1000 Subject: convert fullscreen children listeners logic to ~ES6 remove gulp serve shortcut --- src/js/listeners.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'src/js') diff --git a/src/js/listeners.js b/src/js/listeners.js index ede8d88c..4bacbb2c 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -816,13 +816,13 @@ class Listeners { // Also update controls.hover state for any non-player children of fullscreen element (as above) if (elements.fullscreen) { - for (let i = 0; i < elements.fullscreen.children.length; i++) { - if (!elements.fullscreen.children[i].contains(elements.container)) { - this.bind(elements.fullscreen.children[i], 'mouseenter mouseleave', event => { + Array.from(elements.fullscreen.children) + .filter(c => !c.contains(elements.container)) + .forEach(child => { + this.bind(child, 'mouseenter mouseleave', event => { elements.controls.hover = !player.touch && event.type === 'mouseenter'; }); - } - } + }); } // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting) -- cgit v1.2.3 From 502d5977d79148957828cbf313b7ef4c9f31973f Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sat, 11 Apr 2020 16:23:14 +1000 Subject: Converted to 2 space indentation --- src/js/captions.js | 702 ++++---- src/js/config/defaults.js | 822 ++++----- src/js/config/states.js | 4 +- src/js/config/types.js | 28 +- src/js/console.js | 34 +- src/js/controls.js | 3244 +++++++++++++++++----------------- src/js/fullscreen.js | 477 +++-- src/js/html5.js | 248 +-- src/js/listeners.js | 1566 ++++++++-------- src/js/media.js | 96 +- src/js/plugins/ads.js | 1120 ++++++------ src/js/plugins/preview-thumbnails.js | 1181 ++++++------- src/js/plugins/vimeo.js | 738 ++++---- src/js/plugins/youtube.js | 790 ++++----- src/js/plyr.d.ts | 952 +++++----- src/js/plyr.js | 2116 +++++++++++----------- src/js/source.js | 274 +-- src/js/storage.js | 104 +- src/js/support.js | 200 +-- src/js/ui.js | 513 +++--- src/js/utils/animation.js | 46 +- src/js/utils/arrays.js | 16 +- src/js/utils/browser.js | 10 +- src/js/utils/elements.js | 336 ++-- src/js/utils/events.js | 144 +- src/js/utils/fetch.js | 58 +- src/js/utils/i18n.js | 50 +- src/js/utils/is.js | 78 +- src/js/utils/load-image.js | 18 +- src/js/utils/load-script.js | 10 +- src/js/utils/load-sprite.js | 104 +- src/js/utils/numbers.js | 2 +- src/js/utils/objects.js | 40 +- src/js/utils/promise.js | 6 +- src/js/utils/strings.js | 75 +- src/js/utils/style.js | 108 +- src/js/utils/time.js | 36 +- src/js/utils/urls.js | 36 +- 38 files changed, 8181 insertions(+), 8201 deletions(-) (limited to 'src/js') diff --git a/src/js/captions.js b/src/js/captions.js index 04da4651..0e6173ca 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -8,12 +8,12 @@ import support from './support'; import { dedupe } from './utils/arrays'; import browser from './utils/browser'; import { - createElement, - emptyElement, - getAttributesFromSelector, - insertAfter, - removeElement, - toggleClass, + createElement, + emptyElement, + getAttributesFromSelector, + insertAfter, + removeElement, + toggleClass, } from './utils/elements'; import { on, triggerEvent } from './utils/events'; import fetch from './utils/fetch'; @@ -23,371 +23,371 @@ import { getHTML } from './utils/strings'; import { parseUrl } from './utils/urls'; const captions = { - // Setup captions - setup() { - // Requires UI support - if (!this.supported.ui) { - return; - } - - // Only Vimeo and HTML5 video supported at this point - if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) { - // Clear menu and hide - if ( - is.array(this.config.controls) && - this.config.controls.includes('settings') && - this.config.settings.includes('captions') - ) { - controls.setCaptionsMenu.call(this); - } - - return; - } - - // Inject the container - if (!is.element(this.elements.captions)) { - this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions)); - - insertAfter(this.elements.captions, this.elements.wrapper); - } - - // Fix IE captions if CORS is used - // Fetch captions and inject as blobs instead (data URIs not supported!) - if (browser.isIE && window.URL) { - const elements = this.media.querySelectorAll('track'); - - Array.from(elements).forEach(track => { - const src = track.getAttribute('src'); - const url = parseUrl(src); - - if ( - url !== null && - url.hostname !== window.location.href.hostname && - ['http:', 'https:'].includes(url.protocol) - ) { - fetch(src, 'blob') - .then(blob => { - track.setAttribute('src', window.URL.createObjectURL(blob)); - }) - .catch(() => { - removeElement(track); - }); - } - }); - } - - // Get and set initial data - // The "preferred" options are not realized unless / until the wanted language has a match - // * languages: Array of user's browser languages. - // * language: The language preferred by user settings or config - // * active: The state preferred by user settings or config - // * toggled: The real captions state - - const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en']; - const languages = dedupe(browserLanguages.map(language => language.split('-')[0])); - let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase(); - - // Use first browser language when language is 'auto' - if (language === 'auto') { - [language] = languages; - } - - let active = this.storage.get('captions'); - if (!is.boolean(active)) { - ({ active } = this.config.captions); - } - - Object.assign(this.captions, { - toggled: false, - active, - language, - languages, - }); - - // Watch changes to textTracks and update captions menu - if (this.isHTML5) { - const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack'; - on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this)); - } - - // Update available languages in list next tick (the event must not be triggered before the listeners) - setTimeout(captions.update.bind(this), 0); - }, - - // Update available language options in settings based on tracks - update() { - const tracks = captions.getTracks.call(this, true); - // Get the wanted language - const { active, language, meta, currentTrackNode } = this.captions; - const languageExists = Boolean(tracks.find(track => track.language === language)); - - // Handle tracks (add event listener and "pseudo"-default) - if (this.isHTML5 && this.isVideo) { - tracks - .filter(track => !meta.get(track)) - .forEach(track => { - this.debug.log('Track added', track); - // Attempt to store if the original dom element was "default" - meta.set(track, { - default: track.mode === 'showing', - }); - - // Turn off native caption rendering to avoid double captions - // eslint-disable-next-line no-param-reassign - track.mode = 'hidden'; - - // Add event listener for cue changes - on.call(this, track, 'cuechange', () => captions.updateCues.call(this)); - }); - } - - // Update language first time it matches, or if the previous matching track was removed - if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) { - captions.setLanguage.call(this, language); - captions.toggle.call(this, active && languageExists); - } + // Setup captions + setup() { + // Requires UI support + if (!this.supported.ui) { + return; + } + + // Only Vimeo and HTML5 video supported at this point + if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) { + // Clear menu and hide + if ( + is.array(this.config.controls) && + this.config.controls.includes('settings') && + this.config.settings.includes('captions') + ) { + controls.setCaptionsMenu.call(this); + } + + return; + } + + // Inject the container + if (!is.element(this.elements.captions)) { + this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions)); + + insertAfter(this.elements.captions, this.elements.wrapper); + } + + // Fix IE captions if CORS is used + // Fetch captions and inject as blobs instead (data URIs not supported!) + if (browser.isIE && window.URL) { + const elements = this.media.querySelectorAll('track'); + + Array.from(elements).forEach(track => { + const src = track.getAttribute('src'); + const url = parseUrl(src); - // Enable or disable captions based on track length - toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks)); - - // Update available languages in list if ( - is.array(this.config.controls) && - this.config.controls.includes('settings') && - this.config.settings.includes('captions') + url !== null && + url.hostname !== window.location.href.hostname && + ['http:', 'https:'].includes(url.protocol) ) { - controls.setCaptionsMenu.call(this); - } - }, - - // Toggle captions display - // Used internally for the toggleCaptions method, with the passive option forced to false - toggle(input, passive = true) { - // If there's no full support - if (!this.supported.ui) { - return; - } - - const { toggled } = this.captions; // Current state - const activeClass = this.config.classNames.captions.active; - // Get the next state - // If the method is called without parameter, toggle based on current value - const active = is.nullOrUndefined(input) ? !toggled : input; - - // Update state and trigger event - if (active !== toggled) { - // When passive, don't override user preferences - if (!passive) { - this.captions.active = active; - this.storage.set({ captions: active }); - } - - // Force language if the call isn't passive and there is no matching language to toggle to - if (!this.language && active && !passive) { - const tracks = captions.getTracks.call(this); - const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true); - - // Override user preferences to avoid switching languages if a matching track is added - this.captions.language = track.language; - - // Set caption, but don't store in localStorage as user preference - captions.set.call(this, tracks.indexOf(track)); - return; - } - - // Toggle button if it's enabled - if (this.elements.buttons.captions) { - this.elements.buttons.captions.pressed = active; - } - - // Add class hook - toggleClass(this.elements.container, activeClass, active); - - this.captions.toggled = active; - - // Update settings menu - controls.updateSetting.call(this, 'captions'); - - // Trigger event (not used internally) - triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled'); - } - }, - - // Set captions by track index - // Used internally for the currentTrack setter with the passive option forced to false - set(index, passive = true) { - const tracks = captions.getTracks.call(this); - - // Disable captions if setting to -1 - if (index === -1) { - captions.toggle.call(this, false, passive); - return; - } - - if (!is.number(index)) { - this.debug.warn('Invalid caption argument', index); - return; - } - - if (!(index in tracks)) { - this.debug.warn('Track not found', index); - return; - } - - if (this.captions.currentTrack !== index) { - this.captions.currentTrack = index; - const track = tracks[index]; - const { language } = track || {}; - - // Store reference to node for invalidation on remove - this.captions.currentTrackNode = track; - - // Update settings menu - controls.updateSetting.call(this, 'captions'); - - // When passive, don't override user preferences - if (!passive) { - this.captions.language = language; - this.storage.set({ language }); - } - - // Handle Vimeo captions - if (this.isVimeo) { - this.embed.enableTextTrack(language); - } - - // Trigger event - triggerEvent.call(this, this.media, 'languagechange'); - } - - // Show captions - captions.toggle.call(this, true, passive); - - if (this.isHTML5 && this.isVideo) { - // If we change the active track while a cue is already displayed we need to update it - captions.updateCues.call(this); - } - }, - - // Set captions by language - // Used internally for the language setter with the passive option forced to false - setLanguage(input, passive = true) { - if (!is.string(input)) { - this.debug.warn('Invalid language argument', input); - return; + fetch(src, 'blob') + .then(blob => { + track.setAttribute('src', window.URL.createObjectURL(blob)); + }) + .catch(() => { + removeElement(track); + }); } - // Normalize - const language = input.toLowerCase(); - this.captions.language = language; - - // Set currentTrack - const tracks = captions.getTracks.call(this); - const track = captions.findTrack.call(this, [language]); - captions.set.call(this, tracks.indexOf(track), passive); - }, - - // Get current valid caption tracks - // If update is false it will also ignore tracks without metadata - // This is used to "freeze" the language options when captions.update is false - getTracks(update = false) { - // Handle media or textTracks missing or null - const tracks = Array.from((this.media || {}).textTracks || []); - // For HTML5, use cache instead of current tracks when it exists (if captions.update is false) - // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata) - return tracks - .filter(track => !this.isHTML5 || update || this.captions.meta.has(track)) - .filter(track => ['captions', 'subtitles'].includes(track.kind)); - }, - - // Match tracks based on languages and get the first - findTrack(languages, force = false) { - const tracks = captions.getTracks.call(this); - const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default); - const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a)); - let track; - - languages.every(language => { - track = sorted.find(t => t.language === language); - return !track; // Break iteration if there is a match + }); + } + + // Get and set initial data + // The "preferred" options are not realized unless / until the wanted language has a match + // * languages: Array of user's browser languages. + // * language: The language preferred by user settings or config + // * active: The state preferred by user settings or config + // * toggled: The real captions state + + const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en']; + const languages = dedupe(browserLanguages.map(language => language.split('-')[0])); + let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase(); + + // Use first browser language when language is 'auto' + if (language === 'auto') { + [language] = languages; + } + + let active = this.storage.get('captions'); + if (!is.boolean(active)) { + ({ active } = this.config.captions); + } + + Object.assign(this.captions, { + toggled: false, + active, + language, + languages, + }); + + // Watch changes to textTracks and update captions menu + if (this.isHTML5) { + const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack'; + on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this)); + } + + // Update available languages in list next tick (the event must not be triggered before the listeners) + setTimeout(captions.update.bind(this), 0); + }, + + // Update available language options in settings based on tracks + update() { + const tracks = captions.getTracks.call(this, true); + // Get the wanted language + const { active, language, meta, currentTrackNode } = this.captions; + const languageExists = Boolean(tracks.find(track => track.language === language)); + + // Handle tracks (add event listener and "pseudo"-default) + if (this.isHTML5 && this.isVideo) { + tracks + .filter(track => !meta.get(track)) + .forEach(track => { + this.debug.log('Track added', track); + // Attempt to store if the original dom element was "default" + meta.set(track, { + default: track.mode === 'showing', + }); + + // Turn off native caption rendering to avoid double captions + // eslint-disable-next-line no-param-reassign + track.mode = 'hidden'; + + // Add event listener for cue changes + on.call(this, track, 'cuechange', () => captions.updateCues.call(this)); }); + } + + // Update language first time it matches, or if the previous matching track was removed + if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) { + captions.setLanguage.call(this, language); + captions.toggle.call(this, active && languageExists); + } + + // Enable or disable captions based on track length + toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks)); + + // Update available languages in list + if ( + is.array(this.config.controls) && + this.config.controls.includes('settings') && + this.config.settings.includes('captions') + ) { + controls.setCaptionsMenu.call(this); + } + }, + + // Toggle captions display + // Used internally for the toggleCaptions method, with the passive option forced to false + toggle(input, passive = true) { + // If there's no full support + if (!this.supported.ui) { + return; + } + + const { toggled } = this.captions; // Current state + const activeClass = this.config.classNames.captions.active; + // Get the next state + // If the method is called without parameter, toggle based on current value + const active = is.nullOrUndefined(input) ? !toggled : input; + + // Update state and trigger event + if (active !== toggled) { + // When passive, don't override user preferences + if (!passive) { + this.captions.active = active; + this.storage.set({ captions: active }); + } + + // Force language if the call isn't passive and there is no matching language to toggle to + if (!this.language && active && !passive) { + const tracks = captions.getTracks.call(this); + const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true); - // If no match is found but is required, get first - return track || (force ? sorted[0] : undefined); - }, - - // Get the current track - getCurrentTrack() { - return captions.getTracks.call(this)[this.currentTrack]; - }, - - // Get UI label for track - getLabel(track) { - let currentTrack = track; + // Override user preferences to avoid switching languages if a matching track is added + this.captions.language = track.language; - if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) { - currentTrack = captions.getCurrentTrack.call(this); - } + // Set caption, but don't store in localStorage as user preference + captions.set.call(this, tracks.indexOf(track)); + return; + } - if (is.track(currentTrack)) { - if (!is.empty(currentTrack.label)) { - return currentTrack.label; - } + // Toggle button if it's enabled + if (this.elements.buttons.captions) { + this.elements.buttons.captions.pressed = active; + } - if (!is.empty(currentTrack.language)) { - return track.language.toUpperCase(); - } + // Add class hook + toggleClass(this.elements.container, activeClass, active); - return i18n.get('enabled', this.config); - } + this.captions.toggled = active; - return i18n.get('disabled', this.config); - }, + // Update settings menu + controls.updateSetting.call(this, 'captions'); - // Update captions using current track's active cues - // Also optional array argument in case there isn't any track (ex: vimeo) - updateCues(input) { - // Requires UI - if (!this.supported.ui) { - return; - } + // Trigger event (not used internally) + triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled'); + } + }, - if (!is.element(this.elements.captions)) { - this.debug.warn('No captions element to render to'); - return; - } + // Set captions by track index + // Used internally for the currentTrack setter with the passive option forced to false + set(index, passive = true) { + const tracks = captions.getTracks.call(this); - // Only accept array or empty input - if (!is.nullOrUndefined(input) && !Array.isArray(input)) { - this.debug.warn('updateCues: Invalid input', input); - return; - } + // Disable captions if setting to -1 + if (index === -1) { + captions.toggle.call(this, false, passive); + return; + } - let cues = input; + if (!is.number(index)) { + this.debug.warn('Invalid caption argument', index); + return; + } - // Get cues from track - if (!cues) { - const track = captions.getCurrentTrack.call(this); + if (!(index in tracks)) { + this.debug.warn('Track not found', index); + return; + } - cues = Array.from((track || {}).activeCues || []) - .map(cue => cue.getCueAsHTML()) - .map(getHTML); - } + if (this.captions.currentTrack !== index) { + this.captions.currentTrack = index; + const track = tracks[index]; + const { language } = track || {}; - // Set new caption text - const content = cues.map(cueText => cueText.trim()).join('\n'); - const changed = content !== this.elements.captions.innerHTML; + // Store reference to node for invalidation on remove + this.captions.currentTrackNode = track; - if (changed) { - // Empty the container and create a new child element - emptyElement(this.elements.captions); - const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption)); - caption.innerHTML = content; - this.elements.captions.appendChild(caption); + // Update settings menu + controls.updateSetting.call(this, 'captions'); - // Trigger event - triggerEvent.call(this, this.media, 'cuechange'); - } - }, + // When passive, don't override user preferences + if (!passive) { + this.captions.language = language; + this.storage.set({ language }); + } + + // Handle Vimeo captions + if (this.isVimeo) { + this.embed.enableTextTrack(language); + } + + // Trigger event + triggerEvent.call(this, this.media, 'languagechange'); + } + + // Show captions + captions.toggle.call(this, true, passive); + + if (this.isHTML5 && this.isVideo) { + // If we change the active track while a cue is already displayed we need to update it + captions.updateCues.call(this); + } + }, + + // Set captions by language + // Used internally for the language setter with the passive option forced to false + setLanguage(input, passive = true) { + if (!is.string(input)) { + this.debug.warn('Invalid language argument', input); + return; + } + // Normalize + const language = input.toLowerCase(); + this.captions.language = language; + + // Set currentTrack + const tracks = captions.getTracks.call(this); + const track = captions.findTrack.call(this, [language]); + captions.set.call(this, tracks.indexOf(track), passive); + }, + + // Get current valid caption tracks + // If update is false it will also ignore tracks without metadata + // This is used to "freeze" the language options when captions.update is false + getTracks(update = false) { + // Handle media or textTracks missing or null + const tracks = Array.from((this.media || {}).textTracks || []); + // For HTML5, use cache instead of current tracks when it exists (if captions.update is false) + // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata) + return tracks + .filter(track => !this.isHTML5 || update || this.captions.meta.has(track)) + .filter(track => ['captions', 'subtitles'].includes(track.kind)); + }, + + // Match tracks based on languages and get the first + findTrack(languages, force = false) { + const tracks = captions.getTracks.call(this); + const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default); + const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a)); + let track; + + languages.every(language => { + track = sorted.find(t => t.language === language); + return !track; // Break iteration if there is a match + }); + + // If no match is found but is required, get first + return track || (force ? sorted[0] : undefined); + }, + + // Get the current track + getCurrentTrack() { + return captions.getTracks.call(this)[this.currentTrack]; + }, + + // Get UI label for track + getLabel(track) { + let currentTrack = track; + + if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) { + currentTrack = captions.getCurrentTrack.call(this); + } + + if (is.track(currentTrack)) { + if (!is.empty(currentTrack.label)) { + return currentTrack.label; + } + + if (!is.empty(currentTrack.language)) { + return track.language.toUpperCase(); + } + + return i18n.get('enabled', this.config); + } + + return i18n.get('disabled', this.config); + }, + + // Update captions using current track's active cues + // Also optional array argument in case there isn't any track (ex: vimeo) + updateCues(input) { + // Requires UI + if (!this.supported.ui) { + return; + } + + if (!is.element(this.elements.captions)) { + this.debug.warn('No captions element to render to'); + return; + } + + // Only accept array or empty input + if (!is.nullOrUndefined(input) && !Array.isArray(input)) { + this.debug.warn('updateCues: Invalid input', input); + return; + } + + let cues = input; + + // Get cues from track + if (!cues) { + const track = captions.getCurrentTrack.call(this); + + cues = Array.from((track || {}).activeCues || []) + .map(cue => cue.getCueAsHTML()) + .map(getHTML); + } + + // Set new caption text + const content = cues.map(cueText => cueText.trim()).join('\n'); + const changed = content !== this.elements.captions.innerHTML; + + if (changed) { + // Empty the container and create a new child element + emptyElement(this.elements.captions); + const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption)); + caption.innerHTML = content; + this.elements.captions.appendChild(caption); + + // Trigger event + triggerEvent.call(this, this.media, 'cuechange'); + } + }, }; export default captions; diff --git a/src/js/config/defaults.js b/src/js/config/defaults.js index c299a3c9..bd4052be 100644 --- a/src/js/config/defaults.js +++ b/src/js/config/defaults.js @@ -3,437 +3,437 @@ // ========================================================================== const defaults = { - // Disable + // Disable + enabled: true, + + // Custom media title + title: '', + + // Logging to console + debug: false, + + // Auto play (if supported) + autoplay: false, + + // Only allow one media playing at once (vimeo only) + autopause: true, + + // Allow inline playback on iOS (this effects YouTube/Vimeo - HTML5 requires the attribute present) + // TODO: Remove iosNative fullscreen option in favour of this (logic needs work) + playsinline: true, + + // Default time to skip when rewind/fast forward + seekTime: 10, + + // Default volume + volume: 1, + muted: false, + + // Pass a custom duration + duration: null, + + // Display the media duration on load in the current time position + // If you have opted to display both duration and currentTime, this is ignored + displayDuration: true, + + // Invert the current time to be a countdown + invertTime: true, + + // Clicking the currentTime inverts it's value to show time left rather than elapsed + toggleInvert: true, + + // Force an aspect ratio + // The format must be `'w:h'` (e.g. `'16:9'`) + ratio: null, + + // Click video container to play/pause + clickToPlay: true, + + // Auto hide the controls + hideControls: true, + + // Reset to start when playback ended + resetOnEnd: false, + + // Disable the standard context menu + disableContextMenu: true, + + // Sprite (for icons) + loadSprite: true, + iconPrefix: 'plyr', + iconUrl: 'https://cdn.plyr.io/3.5.10/plyr.svg', + + // Blank video (used to prevent errors on source change) + blankVideo: 'https://cdn.plyr.io/static/blank.mp4', + + // Quality default + quality: { + default: 576, + // The options to display in the UI, if available for the source media + options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240], + forced: false, + onChange: null, + }, + + // Set loops + loop: { + active: false, + // start: null, + // end: null, + }, + + // Speed default and options to display + speed: { + selected: 1, + // The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x) + options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4], + }, + + // Keyboard shortcut settings + keyboard: { + focused: true, + global: false, + }, + + // Display tooltips + tooltips: { + controls: false, + seek: true, + }, + + // Captions settings + captions: { + active: false, + language: 'auto', + // Listen to new tracks added after Plyr is initialized. + // This is needed for streaming captions, but may result in unselectable options + update: false, + }, + + // Fullscreen settings + fullscreen: { + enabled: true, // Allow fullscreen? + fallback: true, // Fallback using full viewport/window + iosNative: false, // Use the native fullscreen in iOS (disables custom controls) + }, + + // Local storage + storage: { enabled: true, - - // Custom media title - title: '', - - // Logging to console - debug: false, - - // Auto play (if supported) - autoplay: false, - - // Only allow one media playing at once (vimeo only) - autopause: true, - - // Allow inline playback on iOS (this effects YouTube/Vimeo - HTML5 requires the attribute present) - // TODO: Remove iosNative fullscreen option in favour of this (logic needs work) - playsinline: true, - - // Default time to skip when rewind/fast forward - seekTime: 10, - - // Default volume - volume: 1, - muted: false, - - // Pass a custom duration - duration: null, - - // Display the media duration on load in the current time position - // If you have opted to display both duration and currentTime, this is ignored - displayDuration: true, - - // Invert the current time to be a countdown - invertTime: true, - - // Clicking the currentTime inverts it's value to show time left rather than elapsed - toggleInvert: true, - - // Force an aspect ratio - // The format must be `'w:h'` (e.g. `'16:9'`) - ratio: null, - - // Click video container to play/pause - clickToPlay: true, - - // Auto hide the controls - hideControls: true, - - // Reset to start when playback ended - resetOnEnd: false, - - // Disable the standard context menu - disableContextMenu: true, - - // Sprite (for icons) - loadSprite: true, - iconPrefix: 'plyr', - iconUrl: 'https://cdn.plyr.io/3.5.10/plyr.svg', - - // Blank video (used to prevent errors on source change) - blankVideo: 'https://cdn.plyr.io/static/blank.mp4', - - // Quality default - quality: { - default: 576, - // The options to display in the UI, if available for the source media - options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240], - forced: false, - onChange: null, + key: 'plyr', + }, + + // Default controls + controls: [ + 'play-large', + // 'restart', + // 'rewind', + 'play', + // 'fast-forward', + 'progress', + 'current-time', + // 'duration', + 'mute', + 'volume', + 'captions', + 'settings', + 'pip', + 'airplay', + // 'download', + 'fullscreen', + ], + settings: ['captions', 'quality', 'speed'], + + // Localisation + i18n: { + restart: 'Restart', + rewind: 'Rewind {seektime}s', + play: 'Play', + pause: 'Pause', + fastForward: 'Forward {seektime}s', + seek: 'Seek', + seekLabel: '{currentTime} of {duration}', + played: 'Played', + buffered: 'Buffered', + currentTime: 'Current time', + duration: 'Duration', + volume: 'Volume', + mute: 'Mute', + unmute: 'Unmute', + enableCaptions: 'Enable captions', + disableCaptions: 'Disable captions', + download: 'Download', + enterFullscreen: 'Enter fullscreen', + exitFullscreen: 'Exit fullscreen', + frameTitle: 'Player for {title}', + captions: 'Captions', + settings: 'Settings', + pip: 'PIP', + menuBack: 'Go back to previous menu', + speed: 'Speed', + normal: 'Normal', + quality: 'Quality', + loop: 'Loop', + start: 'Start', + end: 'End', + all: 'All', + reset: 'Reset', + disabled: 'Disabled', + enabled: 'Enabled', + advertisement: 'Ad', + qualityBadge: { + 2160: '4K', + 1440: 'HD', + 1080: 'HD', + 720: 'HD', + 576: 'SD', + 480: 'SD', }, + }, - // Set loops - loop: { - active: false, - // start: null, - // end: null, - }, - - // Speed default and options to display - speed: { - selected: 1, - // The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x) - options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4], + // URLs + urls: { + download: null, + vimeo: { + sdk: 'https://player.vimeo.com/api/player.js', + iframe: 'https://player.vimeo.com/video/{0}?{1}', + api: 'https://vimeo.com/api/v2/video/{0}.json', }, - - // Keyboard shortcut settings - keyboard: { - focused: true, - global: false, + youtube: { + sdk: 'https://www.youtube.com/iframe_api', + api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}', }, - - // Display tooltips - tooltips: { - controls: false, - seek: true, + googleIMA: { + sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js', }, - - // Captions settings - captions: { - active: false, - language: 'auto', - // Listen to new tracks added after Plyr is initialized. - // This is needed for streaming captions, but may result in unselectable options - update: false, + }, + + // Custom control listeners + listeners: { + seek: null, + play: null, + pause: null, + restart: null, + rewind: null, + fastForward: null, + mute: null, + volume: null, + captions: null, + download: null, + fullscreen: null, + pip: null, + airplay: null, + speed: null, + quality: null, + loop: null, + language: null, + }, + + // Events to watch and bubble + events: [ + // Events to watch on HTML5 media elements and bubble + // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events + 'ended', + 'progress', + 'stalled', + 'playing', + 'waiting', + 'canplay', + 'canplaythrough', + 'loadstart', + 'loadeddata', + 'loadedmetadata', + 'timeupdate', + 'volumechange', + 'play', + 'pause', + 'error', + 'seeking', + 'seeked', + 'emptied', + 'ratechange', + 'cuechange', + + // Custom events + 'download', + 'enterfullscreen', + 'exitfullscreen', + 'captionsenabled', + 'captionsdisabled', + 'languagechange', + 'controlshidden', + 'controlsshown', + 'ready', + + // YouTube + 'statechange', + + // Quality + 'qualitychange', + + // Ads + 'adsloaded', + 'adscontentpause', + 'adscontentresume', + 'adstarted', + 'adsmidpoint', + 'adscomplete', + 'adsallcomplete', + 'adsimpression', + 'adsclick', + ], + + // Selectors + // Change these to match your template if using custom HTML + selectors: { + editable: 'input, textarea, select, [contenteditable]', + container: '.plyr', + controls: { + container: null, + wrapper: '.plyr__controls', }, - - // Fullscreen settings - fullscreen: { - enabled: true, // Allow fullscreen? - fallback: true, // Fallback using full viewport/window - iosNative: false, // Use the native fullscreen in iOS (disables custom controls) + labels: '[data-plyr]', + buttons: { + play: '[data-plyr="play"]', + pause: '[data-plyr="pause"]', + restart: '[data-plyr="restart"]', + rewind: '[data-plyr="rewind"]', + fastForward: '[data-plyr="fast-forward"]', + mute: '[data-plyr="mute"]', + captions: '[data-plyr="captions"]', + download: '[data-plyr="download"]', + fullscreen: '[data-plyr="fullscreen"]', + pip: '[data-plyr="pip"]', + airplay: '[data-plyr="airplay"]', + settings: '[data-plyr="settings"]', + loop: '[data-plyr="loop"]', }, - - // Local storage - storage: { - enabled: true, - key: 'plyr', + inputs: { + seek: '[data-plyr="seek"]', + volume: '[data-plyr="volume"]', + speed: '[data-plyr="speed"]', + language: '[data-plyr="language"]', + quality: '[data-plyr="quality"]', }, - - // Default controls - controls: [ - 'play-large', - // 'restart', - // 'rewind', - 'play', - // 'fast-forward', - 'progress', - 'current-time', - // 'duration', - 'mute', - 'volume', - 'captions', - 'settings', - 'pip', - 'airplay', - // 'download', - 'fullscreen', - ], - settings: ['captions', 'quality', 'speed'], - - // Localisation - i18n: { - restart: 'Restart', - rewind: 'Rewind {seektime}s', - play: 'Play', - pause: 'Pause', - fastForward: 'Forward {seektime}s', - seek: 'Seek', - seekLabel: '{currentTime} of {duration}', - played: 'Played', - buffered: 'Buffered', - currentTime: 'Current time', - duration: 'Duration', - volume: 'Volume', - mute: 'Mute', - unmute: 'Unmute', - enableCaptions: 'Enable captions', - disableCaptions: 'Disable captions', - download: 'Download', - enterFullscreen: 'Enter fullscreen', - exitFullscreen: 'Exit fullscreen', - frameTitle: 'Player for {title}', - captions: 'Captions', - settings: 'Settings', - pip: 'PIP', - menuBack: 'Go back to previous menu', - speed: 'Speed', - normal: 'Normal', - quality: 'Quality', - loop: 'Loop', - start: 'Start', - end: 'End', - all: 'All', - reset: 'Reset', - disabled: 'Disabled', - enabled: 'Enabled', - advertisement: 'Ad', - qualityBadge: { - 2160: '4K', - 1440: 'HD', - 1080: 'HD', - 720: 'HD', - 576: 'SD', - 480: 'SD', - }, + display: { + currentTime: '.plyr__time--current', + duration: '.plyr__time--duration', + buffer: '.plyr__progress__buffer', + loop: '.plyr__progress__loop', // Used later + volume: '.plyr__volume--display', }, - - // URLs - urls: { - download: null, - vimeo: { - sdk: 'https://player.vimeo.com/api/player.js', - iframe: 'https://player.vimeo.com/video/{0}?{1}', - api: 'https://vimeo.com/api/v2/video/{0}.json', - }, - youtube: { - sdk: 'https://www.youtube.com/iframe_api', - api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}', - }, - googleIMA: { - sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js', - }, + progress: '.plyr__progress', + captions: '.plyr__captions', + caption: '.plyr__caption', + }, + + // Class hooks added to the player in different states + classNames: { + type: 'plyr--{0}', + provider: 'plyr--{0}', + video: 'plyr__video-wrapper', + embed: 'plyr__video-embed', + videoFixedRatio: 'plyr__video-wrapper--fixed-ratio', + embedContainer: 'plyr__video-embed__container', + poster: 'plyr__poster', + posterEnabled: 'plyr__poster-enabled', + ads: 'plyr__ads', + control: 'plyr__control', + controlPressed: 'plyr__control--pressed', + playing: 'plyr--playing', + paused: 'plyr--paused', + stopped: 'plyr--stopped', + loading: 'plyr--loading', + hover: 'plyr--hover', + tooltip: 'plyr__tooltip', + cues: 'plyr__cues', + hidden: 'plyr__sr-only', + hideControls: 'plyr--hide-controls', + isIos: 'plyr--is-ios', + isTouch: 'plyr--is-touch', + uiSupported: 'plyr--full-ui', + noTransition: 'plyr--no-transition', + display: { + time: 'plyr__time', }, - - // Custom control listeners - listeners: { - seek: null, - play: null, - pause: null, - restart: null, - rewind: null, - fastForward: null, - mute: null, - volume: null, - captions: null, - download: null, - fullscreen: null, - pip: null, - airplay: null, - speed: null, - quality: null, - loop: null, - language: null, + menu: { + value: 'plyr__menu__value', + badge: 'plyr__badge', + open: 'plyr--menu-open', }, - - // Events to watch and bubble - events: [ - // Events to watch on HTML5 media elements and bubble - // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events - 'ended', - 'progress', - 'stalled', - 'playing', - 'waiting', - 'canplay', - 'canplaythrough', - 'loadstart', - 'loadeddata', - 'loadedmetadata', - 'timeupdate', - 'volumechange', - 'play', - 'pause', - 'error', - 'seeking', - 'seeked', - 'emptied', - 'ratechange', - 'cuechange', - - // Custom events - 'download', - 'enterfullscreen', - 'exitfullscreen', - 'captionsenabled', - 'captionsdisabled', - 'languagechange', - 'controlshidden', - 'controlsshown', - 'ready', - - // YouTube - 'statechange', - - // Quality - 'qualitychange', - - // Ads - 'adsloaded', - 'adscontentpause', - 'adscontentresume', - 'adstarted', - 'adsmidpoint', - 'adscomplete', - 'adsallcomplete', - 'adsimpression', - 'adsclick', - ], - - // Selectors - // Change these to match your template if using custom HTML - selectors: { - editable: 'input, textarea, select, [contenteditable]', - container: '.plyr', - controls: { - container: null, - wrapper: '.plyr__controls', - }, - labels: '[data-plyr]', - buttons: { - play: '[data-plyr="play"]', - pause: '[data-plyr="pause"]', - restart: '[data-plyr="restart"]', - rewind: '[data-plyr="rewind"]', - fastForward: '[data-plyr="fast-forward"]', - mute: '[data-plyr="mute"]', - captions: '[data-plyr="captions"]', - download: '[data-plyr="download"]', - fullscreen: '[data-plyr="fullscreen"]', - pip: '[data-plyr="pip"]', - airplay: '[data-plyr="airplay"]', - settings: '[data-plyr="settings"]', - loop: '[data-plyr="loop"]', - }, - inputs: { - seek: '[data-plyr="seek"]', - volume: '[data-plyr="volume"]', - speed: '[data-plyr="speed"]', - language: '[data-plyr="language"]', - quality: '[data-plyr="quality"]', - }, - display: { - currentTime: '.plyr__time--current', - duration: '.plyr__time--duration', - buffer: '.plyr__progress__buffer', - loop: '.plyr__progress__loop', // Used later - volume: '.plyr__volume--display', - }, - progress: '.plyr__progress', - captions: '.plyr__captions', - caption: '.plyr__caption', + captions: { + enabled: 'plyr--captions-enabled', + active: 'plyr--captions-active', }, - - // Class hooks added to the player in different states - classNames: { - type: 'plyr--{0}', - provider: 'plyr--{0}', - video: 'plyr__video-wrapper', - embed: 'plyr__video-embed', - videoFixedRatio: 'plyr__video-wrapper--fixed-ratio', - embedContainer: 'plyr__video-embed__container', - poster: 'plyr__poster', - posterEnabled: 'plyr__poster-enabled', - ads: 'plyr__ads', - control: 'plyr__control', - controlPressed: 'plyr__control--pressed', - playing: 'plyr--playing', - paused: 'plyr--paused', - stopped: 'plyr--stopped', - loading: 'plyr--loading', - hover: 'plyr--hover', - tooltip: 'plyr__tooltip', - cues: 'plyr__cues', - hidden: 'plyr__sr-only', - hideControls: 'plyr--hide-controls', - isIos: 'plyr--is-ios', - isTouch: 'plyr--is-touch', - uiSupported: 'plyr--full-ui', - noTransition: 'plyr--no-transition', - display: { - time: 'plyr__time', - }, - menu: { - value: 'plyr__menu__value', - badge: 'plyr__badge', - open: 'plyr--menu-open', - }, - captions: { - enabled: 'plyr--captions-enabled', - active: 'plyr--captions-active', - }, - fullscreen: { - enabled: 'plyr--fullscreen-enabled', - fallback: 'plyr--fullscreen-fallback', - }, - pip: { - supported: 'plyr--pip-supported', - active: 'plyr--pip-active', - }, - airplay: { - supported: 'plyr--airplay-supported', - active: 'plyr--airplay-active', - }, - tabFocus: 'plyr__tab-focus', - previewThumbnails: { - // Tooltip thumbs - thumbContainer: 'plyr__preview-thumb', - thumbContainerShown: 'plyr__preview-thumb--is-shown', - imageContainer: 'plyr__preview-thumb__image-container', - timeContainer: 'plyr__preview-thumb__time-container', - // Scrubbing - scrubbingContainer: 'plyr__preview-scrubbing', - scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown', - }, + fullscreen: { + enabled: 'plyr--fullscreen-enabled', + fallback: 'plyr--fullscreen-fallback', }, - - // Embed attributes - attributes: { - embed: { - provider: 'data-plyr-provider', - id: 'data-plyr-embed-id', - }, + pip: { + supported: 'plyr--pip-supported', + active: 'plyr--pip-active', }, - - // Advertisements plugin - // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio - ads: { - enabled: false, - publisherId: '', - tagUrl: '', + airplay: { + supported: 'plyr--airplay-supported', + active: 'plyr--airplay-active', }, - - // Preview Thumbnails plugin + tabFocus: 'plyr__tab-focus', previewThumbnails: { - enabled: false, - src: '', + // Tooltip thumbs + thumbContainer: 'plyr__preview-thumb', + thumbContainerShown: 'plyr__preview-thumb--is-shown', + imageContainer: 'plyr__preview-thumb__image-container', + timeContainer: 'plyr__preview-thumb__time-container', + // Scrubbing + scrubbingContainer: 'plyr__preview-scrubbing', + scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown', }, + }, - // Vimeo plugin - vimeo: { - byline: false, - portrait: false, - title: false, - speed: true, - transparent: false, - // These settings require a pro or premium account to work - sidedock: false, - controls: false, - // Custom settings from Plyr - referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy - }, - - // YouTube plugin - youtube: { - noCookie: false, // Whether to use an alternative version of YouTube without cookies - rel: 0, // No related vids - showinfo: 0, // Hide info - iv_load_policy: 3, // Hide annotations - modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused) + // Embed attributes + attributes: { + embed: { + provider: 'data-plyr-provider', + id: 'data-plyr-embed-id', }, + }, + + // Advertisements plugin + // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio + ads: { + enabled: false, + publisherId: '', + tagUrl: '', + }, + + // Preview Thumbnails plugin + previewThumbnails: { + enabled: false, + src: '', + }, + + // Vimeo plugin + vimeo: { + byline: false, + portrait: false, + title: false, + speed: true, + transparent: false, + // These settings require a pro or premium account to work + sidedock: false, + controls: false, + // Custom settings from Plyr + referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy + }, + + // YouTube plugin + youtube: { + noCookie: false, // Whether to use an alternative version of YouTube without cookies + rel: 0, // No related vids + showinfo: 0, // Hide info + iv_load_policy: 3, // Hide annotations + modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused) + }, }; export default defaults; diff --git a/src/js/config/states.js b/src/js/config/states.js index 7dd1476b..1c1618e4 100644 --- a/src/js/config/states.js +++ b/src/js/config/states.js @@ -3,8 +3,8 @@ // ========================================================================== export const pip = { - active: 'picture-in-picture', - inactive: 'inline', + active: 'picture-in-picture', + inactive: 'inline', }; export default { pip }; diff --git a/src/js/config/types.js b/src/js/config/types.js index e0ccdaff..31e488eb 100644 --- a/src/js/config/types.js +++ b/src/js/config/types.js @@ -3,14 +3,14 @@ // ========================================================================== export const providers = { - html5: 'html5', - youtube: 'youtube', - vimeo: 'vimeo', + html5: 'html5', + youtube: 'youtube', + vimeo: 'vimeo', }; export const types = { - audio: 'audio', - video: 'video', + audio: 'audio', + video: 'video', }; /** @@ -18,17 +18,17 @@ export const types = { * @param {String} url */ export function getProviderByUrl(url) { - // YouTube - if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtube-nocookie\.com|youtu\.?be)\/.+$/.test(url)) { - return providers.youtube; - } + // YouTube + if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtube-nocookie\.com|youtu\.?be)\/.+$/.test(url)) { + return providers.youtube; + } - // Vimeo - if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) { - return providers.vimeo; - } + // Vimeo + if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) { + return providers.vimeo; + } - return null; + return null; } export default { providers, types }; diff --git a/src/js/console.js b/src/js/console.js index e8099569..f9d734aa 100644 --- a/src/js/console.js +++ b/src/js/console.js @@ -5,26 +5,26 @@ const noop = () => {}; export default class Console { - constructor(enabled = false) { - this.enabled = window.console && enabled; + constructor(enabled = false) { + this.enabled = window.console && enabled; - if (this.enabled) { - this.log('Debugging enabled'); - } + if (this.enabled) { + this.log('Debugging enabled'); } + } - get log() { - // eslint-disable-next-line no-console - return this.enabled ? Function.prototype.bind.call(console.log, console) : noop; - } + get log() { + // eslint-disable-next-line no-console + return this.enabled ? Function.prototype.bind.call(console.log, console) : noop; + } - get warn() { - // eslint-disable-next-line no-console - return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop; - } + get warn() { + // eslint-disable-next-line no-console + return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop; + } - get error() { - // eslint-disable-next-line no-console - return this.enabled ? Function.prototype.bind.call(console.error, console) : noop; - } + get error() { + // eslint-disable-next-line no-console + return this.enabled ? Function.prototype.bind.call(console.error, console) : noop; + } } diff --git a/src/js/controls.js b/src/js/controls.js index 37df497f..5bdb5b0a 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -12,18 +12,18 @@ import { repaint, transitionEndEvent } from './utils/animation'; import { dedupe } from './utils/arrays'; import browser from './utils/browser'; import { - createElement, - emptyElement, - getAttributesFromSelector, - getElement, - getElements, - hasClass, - matches, - removeElement, - setAttributes, - setFocus, - toggleClass, - toggleHidden, + createElement, + emptyElement, + getAttributesFromSelector, + getElement, + getElements, + hasClass, + matches, + removeElement, + setAttributes, + setFocus, + toggleClass, + toggleHidden, } from './utils/elements'; import { off, on } from './utils/events'; import i18n from './utils/i18n'; @@ -35,917 +35,915 @@ import { formatTime, getHours } from './utils/time'; // TODO: Don't export a massive object - break down and create class const controls = { - // Get icon URL - getIconUrl() { - const url = new URL(this.config.iconUrl, window.location); - const cors = url.host !== window.location.host || (browser.isIE && !window.svg4everybody); - - return { - url: this.config.iconUrl, - cors, - }; - }, - - // Find the UI controls - findElements() { - try { - this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper); - - // Buttons - this.elements.buttons = { - play: getElements.call(this, this.config.selectors.buttons.play), - pause: getElement.call(this, this.config.selectors.buttons.pause), - restart: getElement.call(this, this.config.selectors.buttons.restart), - rewind: getElement.call(this, this.config.selectors.buttons.rewind), - fastForward: getElement.call(this, this.config.selectors.buttons.fastForward), - mute: getElement.call(this, this.config.selectors.buttons.mute), - pip: getElement.call(this, this.config.selectors.buttons.pip), - airplay: getElement.call(this, this.config.selectors.buttons.airplay), - settings: getElement.call(this, this.config.selectors.buttons.settings), - captions: getElement.call(this, this.config.selectors.buttons.captions), - fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen), - }; - - // Progress - this.elements.progress = getElement.call(this, this.config.selectors.progress); - - // Inputs - this.elements.inputs = { - seek: getElement.call(this, this.config.selectors.inputs.seek), - volume: getElement.call(this, this.config.selectors.inputs.volume), - }; - - // Display - this.elements.display = { - buffer: getElement.call(this, this.config.selectors.display.buffer), - currentTime: getElement.call(this, this.config.selectors.display.currentTime), - duration: getElement.call(this, this.config.selectors.display.duration), - }; - - // Seek tooltip - if (is.element(this.elements.progress)) { - this.elements.display.seekTooltip = this.elements.progress.querySelector( - `.${this.config.classNames.tooltip}`, - ); - } - - return true; - } catch (error) { - // Log it - this.debug.warn('It looks like there is a problem with your custom controls HTML', error); - - // Restore native video controls - this.toggleNativeControls(true); - - return false; - } - }, - - // Create icon - createIcon(type, attributes) { - const namespace = 'http://www.w3.org/2000/svg'; - const iconUrl = controls.getIconUrl.call(this); - const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`; - // Create - const icon = document.createElementNS(namespace, 'svg'); - setAttributes( - icon, - extend(attributes, { - 'aria-hidden': 'true', - focusable: 'false', - }), - ); - - // Create the to reference sprite - const use = document.createElementNS(namespace, 'use'); - const path = `${iconPath}-${type}`; - - // Set `href` attributes - // https://github.com/sampotts/plyr/issues/460 - // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href - if ('href' in use) { - use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path); - } - - // Always set the older attribute even though it's "deprecated" (it'll be around for ages) - use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path); - - // Add to - icon.appendChild(use); - - return icon; - }, - - // Create hidden text label - createLabel(key, attr = {}) { - const text = i18n.get(key, this.config); - const attributes = { ...attr, class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ') }; - - return createElement('span', attributes, text); - }, - - // Create a badge - createBadge(text) { - if (is.empty(text)) { - return null; - } - - const badge = createElement('span', { - class: this.config.classNames.menu.value, + // Get icon URL + getIconUrl() { + const url = new URL(this.config.iconUrl, window.location); + const cors = url.host !== window.location.host || (browser.isIE && !window.svg4everybody); + + return { + url: this.config.iconUrl, + cors, + }; + }, + + // Find the UI controls + findElements() { + try { + this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper); + + // Buttons + this.elements.buttons = { + play: getElements.call(this, this.config.selectors.buttons.play), + pause: getElement.call(this, this.config.selectors.buttons.pause), + restart: getElement.call(this, this.config.selectors.buttons.restart), + rewind: getElement.call(this, this.config.selectors.buttons.rewind), + fastForward: getElement.call(this, this.config.selectors.buttons.fastForward), + mute: getElement.call(this, this.config.selectors.buttons.mute), + pip: getElement.call(this, this.config.selectors.buttons.pip), + airplay: getElement.call(this, this.config.selectors.buttons.airplay), + settings: getElement.call(this, this.config.selectors.buttons.settings), + captions: getElement.call(this, this.config.selectors.buttons.captions), + fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen), + }; + + // Progress + this.elements.progress = getElement.call(this, this.config.selectors.progress); + + // Inputs + this.elements.inputs = { + seek: getElement.call(this, this.config.selectors.inputs.seek), + volume: getElement.call(this, this.config.selectors.inputs.volume), + }; + + // Display + this.elements.display = { + buffer: getElement.call(this, this.config.selectors.display.buffer), + currentTime: getElement.call(this, this.config.selectors.display.currentTime), + duration: getElement.call(this, this.config.selectors.display.duration), + }; + + // Seek tooltip + if (is.element(this.elements.progress)) { + this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`); + } + + return true; + } catch (error) { + // Log it + this.debug.warn('It looks like there is a problem with your custom controls HTML', error); + + // Restore native video controls + this.toggleNativeControls(true); + + return false; + } + }, + + // Create icon + createIcon(type, attributes) { + const namespace = 'http://www.w3.org/2000/svg'; + const iconUrl = controls.getIconUrl.call(this); + const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`; + // Create + const icon = document.createElementNS(namespace, 'svg'); + setAttributes( + icon, + extend(attributes, { + 'aria-hidden': 'true', + focusable: 'false', + }), + ); + + // Create the to reference sprite + const use = document.createElementNS(namespace, 'use'); + const path = `${iconPath}-${type}`; + + // Set `href` attributes + // https://github.com/sampotts/plyr/issues/460 + // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href + if ('href' in use) { + use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path); + } + + // Always set the older attribute even though it's "deprecated" (it'll be around for ages) + use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path); + + // Add to + icon.appendChild(use); + + return icon; + }, + + // Create hidden text label + createLabel(key, attr = {}) { + const text = i18n.get(key, this.config); + const attributes = { ...attr, class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ') }; + + return createElement('span', attributes, text); + }, + + // Create a badge + createBadge(text) { + if (is.empty(text)) { + return null; + } + + const badge = createElement('span', { + class: this.config.classNames.menu.value, + }); + + badge.appendChild( + createElement( + 'span', + { + class: this.config.classNames.menu.badge, + }, + text, + ), + ); + + return badge; + }, + + // Create a
if needed - if (is.empty(source)) { - source = player.media.getAttribute(player.config.attributes.embed.id); - } - - const id = parseId(source); - // Build an iframe - const iframe = createElement('iframe'); - const src = format(player.config.urls.vimeo.iframe, id, params); - iframe.setAttribute('src', src); - iframe.setAttribute('allowfullscreen', ''); - iframe.setAttribute('allowtransparency', ''); - iframe.setAttribute('allow', 'autoplay'); - - // Set the referrer policy if required - if (!is.empty(config.referrerPolicy)) { - iframe.setAttribute('referrerPolicy', config.referrerPolicy); - } - - // Get poster, if already set - const { poster } = player; - // Inject the package - const wrapper = createElement('div', { poster, class: player.config.classNames.embedContainer }); - wrapper.appendChild(iframe); - player.media = replaceElement(wrapper, player.media); - - // Get poster image - fetch(format(player.config.urls.vimeo.api, id), 'json').then(response => { - if (is.empty(response)) { - return; - } - - // Get the URL for thumbnail - const url = new URL(response[0].thumbnail_large); - - // Get original image - url.pathname = `${url.pathname.split('_')[0]}.jpg`; - - // Set and show poster - ui.setPoster.call(player, url.href).catch(() => {}); - }); - - // Setup instance - // https://github.com/vimeo/player.js - player.embed = new window.Vimeo.Player(iframe, { - autopause: player.config.autopause, - muted: player.muted, - }); - - player.media.paused = true; - player.media.currentTime = 0; - - // Disable native text track rendering - if (player.supported.ui) { - player.embed.disableTextTrack(); - } - - // Create a faux HTML5 API using the Vimeo API - player.media.play = () => { - assurePlaybackState.call(player, true); - return player.embed.play(); - }; - - player.media.pause = () => { - assurePlaybackState.call(player, false); - return player.embed.pause(); - }; - - player.media.stop = () => { - player.pause(); - player.currentTime = 0; - }; - - // Seeking - let { currentTime } = player.media; - Object.defineProperty(player.media, 'currentTime', { - get() { - return currentTime; - }, - set(time) { - // Vimeo will automatically play on seek if the video hasn't been played before - - // Get current paused state and volume etc - const { embed, media, paused, volume } = player; - const restorePause = paused && !embed.hasPlayed; - - // Set seeking state and trigger event - media.seeking = true; - triggerEvent.call(player, media, 'seeking'); - - // If paused, mute until seek is complete - Promise.resolve(restorePause && embed.setVolume(0)) - // Seek - .then(() => embed.setCurrentTime(time)) - // Restore paused - .then(() => restorePause && embed.pause()) - // Restore volume - .then(() => restorePause && embed.setVolume(volume)) - .catch(() => { - // Do nothing - }); - }, - }); - - // Playback speed - let speed = player.config.speed.selected; - Object.defineProperty(player.media, 'playbackRate', { - get() { - return speed; - }, - set(input) { - player.embed - .setPlaybackRate(input) - .then(() => { - speed = input; - triggerEvent.call(player, player.media, 'ratechange'); - }) - .catch(() => { - // Cannot set Playback Rate, Video is probably not on Pro account - player.options.speed = [1]; - }); - }, - }); - - // Volume - let { volume } = player.config; - Object.defineProperty(player.media, 'volume', { - get() { - return volume; - }, - set(input) { - player.embed.setVolume(input).then(() => { - volume = input; - triggerEvent.call(player, player.media, 'volumechange'); - }); - }, + setup() { + const player = this; + + // Add embed class for responsive + toggleClass(player.elements.wrapper, player.config.classNames.embed, true); + + // Set speed options from config + player.options.speed = player.config.speed.options; + + // Set intial ratio + setAspectRatio.call(player); + + // Load the SDK if not already + if (!is.object(window.Vimeo)) { + loadScript(player.config.urls.vimeo.sdk) + .then(() => { + vimeo.ready.call(player); + }) + .catch(error => { + player.debug.warn('Vimeo SDK (player.js) failed to load', error); }); + } else { + vimeo.ready.call(player); + } + }, + + // API Ready + ready() { + const player = this; + const config = player.config.vimeo; + + // Get Vimeo params for the iframe + const params = buildUrlParams( + extend( + {}, + { + loop: player.config.loop.active, + autoplay: player.autoplay, + muted: player.muted, + gesture: 'media', + playsinline: !this.config.fullscreen.iosNative, + }, + config, + ), + ); + + // Get the source URL or ID + let source = player.media.getAttribute('src'); + + // Get from
if needed + if (is.empty(source)) { + source = player.media.getAttribute(player.config.attributes.embed.id); + } - // Muted - let { muted } = player.config; - Object.defineProperty(player.media, 'muted', { - get() { - return muted; - }, - set(input) { - const toggle = is.boolean(input) ? input : false; - - player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => { - muted = toggle; - triggerEvent.call(player, player.media, 'volumechange'); - }); - }, - }); + const id = parseId(source); + // Build an iframe + const iframe = createElement('iframe'); + const src = format(player.config.urls.vimeo.iframe, id, params); + iframe.setAttribute('src', src); + iframe.setAttribute('allowfullscreen', ''); + iframe.setAttribute('allowtransparency', ''); + iframe.setAttribute('allow', 'autoplay'); + + // Set the referrer policy if required + if (!is.empty(config.referrerPolicy)) { + iframe.setAttribute('referrerPolicy', config.referrerPolicy); + } - // Loop - let { loop } = player.config; - Object.defineProperty(player.media, 'loop', { - get() { - return loop; - }, - set(input) { - const toggle = is.boolean(input) ? input : player.config.loop.active; - - player.embed.setLoop(toggle).then(() => { - loop = toggle; - }); - }, - }); + // Get poster, if already set + const { poster } = player; + // Inject the package + const wrapper = createElement('div', { poster, class: player.config.classNames.embedContainer }); + wrapper.appendChild(iframe); + player.media = replaceElement(wrapper, player.media); + + // Get poster image + fetch(format(player.config.urls.vimeo.api, id), 'json').then(response => { + if (is.empty(response)) { + return; + } + + // Get the URL for thumbnail + const url = new URL(response[0].thumbnail_large); + + // Get original image + url.pathname = `${url.pathname.split('_')[0]}.jpg`; + + // Set and show poster + ui.setPoster.call(player, url.href).catch(() => {}); + }); + + // Setup instance + // https://github.com/vimeo/player.js + player.embed = new window.Vimeo.Player(iframe, { + autopause: player.config.autopause, + muted: player.muted, + }); + + player.media.paused = true; + player.media.currentTime = 0; + + // Disable native text track rendering + if (player.supported.ui) { + player.embed.disableTextTrack(); + } - // Source - let currentSrc; + // Create a faux HTML5 API using the Vimeo API + player.media.play = () => { + assurePlaybackState.call(player, true); + return player.embed.play(); + }; + + player.media.pause = () => { + assurePlaybackState.call(player, false); + return player.embed.pause(); + }; + + player.media.stop = () => { + player.pause(); + player.currentTime = 0; + }; + + // Seeking + let { currentTime } = player.media; + Object.defineProperty(player.media, 'currentTime', { + get() { + return currentTime; + }, + set(time) { + // Vimeo will automatically play on seek if the video hasn't been played before + + // Get current paused state and volume etc + const { embed, media, paused, volume } = player; + const restorePause = paused && !embed.hasPlayed; + + // Set seeking state and trigger event + media.seeking = true; + triggerEvent.call(player, media, 'seeking'); + + // If paused, mute until seek is complete + Promise.resolve(restorePause && embed.setVolume(0)) + // Seek + .then(() => embed.setCurrentTime(time)) + // Restore paused + .then(() => restorePause && embed.pause()) + // Restore volume + .then(() => restorePause && embed.setVolume(volume)) + .catch(() => { + // Do nothing + }); + }, + }); + + // Playback speed + let speed = player.config.speed.selected; + Object.defineProperty(player.media, 'playbackRate', { + get() { + return speed; + }, + set(input) { player.embed - .getVideoUrl() - .then(value => { - currentSrc = value; - controls.setDownloadUrl.call(player); - }) - .catch(error => { - this.debug.warn(error); - }); - - Object.defineProperty(player.media, 'currentSrc', { - get() { - return currentSrc; - }, + .setPlaybackRate(input) + .then(() => { + speed = input; + triggerEvent.call(player, player.media, 'ratechange'); + }) + .catch(() => { + // Cannot set Playback Rate, Video is probably not on Pro account + player.options.speed = [1]; + }); + }, + }); + + // Volume + let { volume } = player.config; + Object.defineProperty(player.media, 'volume', { + get() { + return volume; + }, + set(input) { + player.embed.setVolume(input).then(() => { + volume = input; + triggerEvent.call(player, player.media, 'volumechange'); }); - - // Ended - Object.defineProperty(player.media, 'ended', { - get() { - return player.currentTime === player.duration; - }, - }); - - // Set aspect ratio based on video size - Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => { - const [width, height] = dimensions; - player.embed.ratio = [width, height]; - setAspectRatio.call(this); + }, + }); + + // Muted + let { muted } = player.config; + Object.defineProperty(player.media, 'muted', { + get() { + return muted; + }, + set(input) { + const toggle = is.boolean(input) ? input : false; + + player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => { + muted = toggle; + triggerEvent.call(player, player.media, 'volumechange'); }); - - // Set autopause - player.embed.setAutopause(player.config.autopause).then(state => { - player.config.autopause = state; + }, + }); + + // Loop + let { loop } = player.config; + Object.defineProperty(player.media, 'loop', { + get() { + return loop; + }, + set(input) { + const toggle = is.boolean(input) ? input : player.config.loop.active; + + player.embed.setLoop(toggle).then(() => { + loop = toggle; }); - - // Get title - player.embed.getVideoTitle().then(title => { - player.config.title = title; - ui.setTitle.call(this); - }); - - // Get current time - player.embed.getCurrentTime().then(value => { - currentTime = value; - triggerEvent.call(player, player.media, 'timeupdate'); - }); - - // Get duration - player.embed.getDuration().then(value => { - player.media.duration = value; - triggerEvent.call(player, player.media, 'durationchange'); - }); - - // Get captions - player.embed.getTextTracks().then(tracks => { - player.media.textTracks = tracks; - captions.setup.call(player); - }); - - player.embed.on('cuechange', ({ cues = [] }) => { - const strippedCues = cues.map(cue => stripHTML(cue.text)); - captions.updateCues.call(player, strippedCues); - }); - - player.embed.on('loaded', () => { - // Assure state and events are updated on autoplay - player.embed.getPaused().then(paused => { - assurePlaybackState.call(player, !paused); - if (!paused) { - triggerEvent.call(player, player.media, 'playing'); - } - }); - - if (is.element(player.embed.element) && player.supported.ui) { - const frame = player.embed.element; - - // Fix keyboard focus issues - // https://github.com/sampotts/plyr/issues/317 - frame.setAttribute('tabindex', -1); - } - }); - - player.embed.on('bufferstart', () => { - triggerEvent.call(player, player.media, 'waiting'); - }); - - player.embed.on('bufferend', () => { - triggerEvent.call(player, player.media, 'playing'); - }); - - player.embed.on('play', () => { - assurePlaybackState.call(player, true); - triggerEvent.call(player, player.media, 'playing'); - }); - - player.embed.on('pause', () => { - assurePlaybackState.call(player, false); - }); - - player.embed.on('timeupdate', data => { - player.media.seeking = false; - currentTime = data.seconds; - triggerEvent.call(player, player.media, 'timeupdate'); - }); - - player.embed.on('progress', data => { - player.media.buffered = data.percent; - triggerEvent.call(player, player.media, 'progress'); - - // Check all loaded - if (parseInt(data.percent, 10) === 1) { - triggerEvent.call(player, player.media, 'canplaythrough'); - } - - // Get duration as if we do it before load, it gives an incorrect value - // https://github.com/sampotts/plyr/issues/891 - player.embed.getDuration().then(value => { - if (value !== player.media.duration) { - player.media.duration = value; - triggerEvent.call(player, player.media, 'durationchange'); - } - }); - }); - - player.embed.on('seeked', () => { - player.media.seeking = false; - triggerEvent.call(player, player.media, 'seeked'); - }); - - player.embed.on('ended', () => { - player.media.paused = true; - triggerEvent.call(player, player.media, 'ended'); - }); - - player.embed.on('error', detail => { - player.media.error = detail; - triggerEvent.call(player, player.media, 'error'); - }); - - // Rebuild UI - setTimeout(() => ui.build.call(player), 0); - }, + }, + }); + + // Source + let currentSrc; + player.embed + .getVideoUrl() + .then(value => { + currentSrc = value; + controls.setDownloadUrl.call(player); + }) + .catch(error => { + this.debug.warn(error); + }); + + Object.defineProperty(player.media, 'currentSrc', { + get() { + return currentSrc; + }, + }); + + // Ended + Object.defineProperty(player.media, 'ended', { + get() { + return player.currentTime === player.duration; + }, + }); + + // Set aspect ratio based on video size + Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => { + const [width, height] = dimensions; + player.embed.ratio = [width, height]; + setAspectRatio.call(this); + }); + + // Set autopause + player.embed.setAutopause(player.config.autopause).then(state => { + player.config.autopause = state; + }); + + // Get title + player.embed.getVideoTitle().then(title => { + player.config.title = title; + ui.setTitle.call(this); + }); + + // Get current time + player.embed.getCurrentTime().then(value => { + currentTime = value; + triggerEvent.call(player, player.media, 'timeupdate'); + }); + + // Get duration + player.embed.getDuration().then(value => { + player.media.duration = value; + triggerEvent.call(player, player.media, 'durationchange'); + }); + + // Get captions + player.embed.getTextTracks().then(tracks => { + player.media.textTracks = tracks; + captions.setup.call(player); + }); + + player.embed.on('cuechange', ({ cues = [] }) => { + const strippedCues = cues.map(cue => stripHTML(cue.text)); + captions.updateCues.call(player, strippedCues); + }); + + player.embed.on('loaded', () => { + // Assure state and events are updated on autoplay + player.embed.getPaused().then(paused => { + assurePlaybackState.call(player, !paused); + if (!paused) { + triggerEvent.call(player, player.media, 'playing'); + } + }); + + if (is.element(player.embed.element) && player.supported.ui) { + const frame = player.embed.element; + + // Fix keyboard focus issues + // https://github.com/sampotts/plyr/issues/317 + frame.setAttribute('tabindex', -1); + } + }); + + player.embed.on('bufferstart', () => { + triggerEvent.call(player, player.media, 'waiting'); + }); + + player.embed.on('bufferend', () => { + triggerEvent.call(player, player.media, 'playing'); + }); + + player.embed.on('play', () => { + assurePlaybackState.call(player, true); + triggerEvent.call(player, player.media, 'playing'); + }); + + player.embed.on('pause', () => { + assurePlaybackState.call(player, false); + }); + + player.embed.on('timeupdate', data => { + player.media.seeking = false; + currentTime = data.seconds; + triggerEvent.call(player, player.media, 'timeupdate'); + }); + + player.embed.on('progress', data => { + player.media.buffered = data.percent; + triggerEvent.call(player, player.media, 'progress'); + + // Check all loaded + if (parseInt(data.percent, 10) === 1) { + triggerEvent.call(player, player.media, 'canplaythrough'); + } + + // Get duration as if we do it before load, it gives an incorrect value + // https://github.com/sampotts/plyr/issues/891 + player.embed.getDuration().then(value => { + if (value !== player.media.duration) { + player.media.duration = value; + triggerEvent.call(player, player.media, 'durationchange'); + } + }); + }); + + player.embed.on('seeked', () => { + player.media.seeking = false; + triggerEvent.call(player, player.media, 'seeked'); + }); + + player.embed.on('ended', () => { + player.media.paused = true; + triggerEvent.call(player, player.media, 'ended'); + }); + + player.embed.on('error', detail => { + player.media.error = detail; + triggerEvent.call(player, player.media, 'error'); + }); + + // Rebuild UI + setTimeout(() => ui.build.call(player), 0); + }, }; export default vimeo; diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index 8c65b1dc..4de46395 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -15,426 +15,426 @@ import { setAspectRatio } from '../utils/style'; // Parse YouTube ID from URL function parseId(url) { - if (is.empty(url)) { - return null; - } + if (is.empty(url)) { + return null; + } - const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; - return url.match(regex) ? RegExp.$2 : url; + const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; + return url.match(regex) ? RegExp.$2 : url; } // Set playback state and trigger change (only on actual change) function assurePlaybackState(play) { - if (play && !this.embed.hasPlayed) { - this.embed.hasPlayed = true; - } - if (this.media.paused === play) { - this.media.paused = !play; - triggerEvent.call(this, this.media, play ? 'play' : 'pause'); - } + if (play && !this.embed.hasPlayed) { + this.embed.hasPlayed = true; + } + if (this.media.paused === play) { + this.media.paused = !play; + triggerEvent.call(this, this.media, play ? 'play' : 'pause'); + } } function getHost(config) { - if (config.noCookie) { - return 'https://www.youtube-nocookie.com'; - } + if (config.noCookie) { + return 'https://www.youtube-nocookie.com'; + } - if (window.location.protocol === 'http:') { - return 'http://www.youtube.com'; - } + if (window.location.protocol === 'http:') { + return 'http://www.youtube.com'; + } - // Use YouTube's default - return undefined; + // Use YouTube's default + return undefined; } const youtube = { - setup() { - // Add embed class for responsive - toggleClass(this.elements.wrapper, this.config.classNames.embed, true); - - // Setup API - if (is.object(window.YT) && is.function(window.YT.Player)) { - youtube.ready.call(this); - } else { - // Reference current global callback - const callback = window.onYouTubeIframeAPIReady; - - // Set callback to process queue - window.onYouTubeIframeAPIReady = () => { - // Call global callback if set - if (is.function(callback)) { - callback(); - } - - youtube.ready.call(this); - }; - - // Load the SDK - loadScript(this.config.urls.youtube.sdk).catch(error => { - this.debug.warn('YouTube API failed to load', error); - }); + setup() { + // Add embed class for responsive + toggleClass(this.elements.wrapper, this.config.classNames.embed, true); + + // Setup API + if (is.object(window.YT) && is.function(window.YT.Player)) { + youtube.ready.call(this); + } else { + // Reference current global callback + const callback = window.onYouTubeIframeAPIReady; + + // Set callback to process queue + window.onYouTubeIframeAPIReady = () => { + // Call global callback if set + if (is.function(callback)) { + callback(); } - }, - // Get the media title - getTitle(videoId) { - const url = format(this.config.urls.youtube.api, videoId); + youtube.ready.call(this); + }; - fetch(url) - .then(data => { - if (is.object(data)) { - const { title, height, width } = data; + // Load the SDK + loadScript(this.config.urls.youtube.sdk).catch(error => { + this.debug.warn('YouTube API failed to load', error); + }); + } + }, - // Set title - this.config.title = title; - ui.setTitle.call(this); + // Get the media title + getTitle(videoId) { + const url = format(this.config.urls.youtube.api, videoId); - // Set aspect ratio - this.embed.ratio = [width, height]; - } + fetch(url) + .then(data => { + if (is.object(data)) { + const { title, height, width } = data; - setAspectRatio.call(this); - }) - .catch(() => { - // Set aspect ratio - setAspectRatio.call(this); - }); - }, - - // API ready - ready() { - const player = this; - // Ignore already setup (race condition) - const currentId = player.media && player.media.getAttribute('id'); - if (!is.empty(currentId) && currentId.startsWith('youtube-')) { - return; + // Set title + this.config.title = title; + ui.setTitle.call(this); + + // Set aspect ratio + this.embed.ratio = [width, height]; } - // Get the source URL or ID - let source = player.media.getAttribute('src'); + setAspectRatio.call(this); + }) + .catch(() => { + // Set aspect ratio + setAspectRatio.call(this); + }); + }, + + // API ready + ready() { + const player = this; + // Ignore already setup (race condition) + const currentId = player.media && player.media.getAttribute('id'); + if (!is.empty(currentId) && currentId.startsWith('youtube-')) { + return; + } + + // Get the source URL or ID + let source = player.media.getAttribute('src'); + + // Get from
if needed + if (is.empty(source)) { + source = player.media.getAttribute(this.config.attributes.embed.id); + } - // Get from
if needed - if (is.empty(source)) { - source = player.media.getAttribute(this.config.attributes.embed.id); + // Replace the