From 33a11fb53adff8716935755248e12a1597254233 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Wed, 9 May 2018 09:50:22 +1000 Subject: v3.3.7 --- src/js/defaults.js | 2 +- src/js/plyr.js | 2 +- src/js/plyr.polyfilled.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src/js') diff --git a/src/js/defaults.js b/src/js/defaults.js index a28f56ee..f160b1aa 100644 --- a/src/js/defaults.js +++ b/src/js/defaults.js @@ -56,7 +56,7 @@ const defaults = { // Sprite (for icons) loadSprite: true, iconPrefix: 'plyr', - iconUrl: 'https://cdn.plyr.io/3.3.6/plyr.svg', + iconUrl: 'https://cdn.plyr.io/3.3.7/plyr.svg', // Blank video (used to prevent errors on source change) blankVideo: 'https://cdn.plyr.io/static/blank.mp4', diff --git a/src/js/plyr.js b/src/js/plyr.js index c2a1d6e3..6a3deade 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr -// plyr.js v3.3.6 +// plyr.js v3.3.7 // https://github.com/sampotts/plyr // License: The MIT License (MIT) // ========================================================================== diff --git a/src/js/plyr.polyfilled.js b/src/js/plyr.polyfilled.js index 0cc3c526..635dee63 100644 --- a/src/js/plyr.polyfilled.js +++ b/src/js/plyr.polyfilled.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr Polyfilled Build -// plyr.js v3.3.6 +// plyr.js v3.3.7 // https://github.com/sampotts/plyr // License: The MIT License (MIT) // ========================================================================== -- cgit v1.2.3 From 765c01e83dc4173d778538061a82e1973a8574f0 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Thu, 10 May 2018 09:34:15 +1000 Subject: Remove references to window.Plyr --- src/js/fullscreen.js | 2 +- src/js/utils.js | 15 ++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) (limited to 'src/js') diff --git a/src/js/fullscreen.js b/src/js/fullscreen.js index cd2d8ac6..000ba706 100644 --- a/src/js/fullscreen.js +++ b/src/js/fullscreen.js @@ -19,7 +19,7 @@ function onChange() { } // Trigger an event - utils.dispatchEvent(this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true); + utils.dispatchEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true); // Trap focus in container if (!browser.isIos) { diff --git a/src/js/utils.js b/src/js/utils.js index d46a7601..ebfb9c96 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -9,9 +9,6 @@ import { providers } from './types'; const utils = { // Check variable types is: { - plyr(input) { - return this.instanceof(input, window.Plyr); - }, object(input) { return this.getConstructor(input) === Object; }, @@ -31,22 +28,22 @@ const utils = { return !this.nullOrUndefined(input) && Array.isArray(input); }, weakMap(input) { - return this.instanceof(input, window.WeakMap); + return this.instanceof(input, WeakMap); }, nodeList(input) { - return this.instanceof(input, window.NodeList); + return this.instanceof(input, NodeList); }, element(input) { - return this.instanceof(input, window.Element); + return this.instanceof(input, Element); }, textNode(input) { return this.getConstructor(input) === Text; }, event(input) { - return this.instanceof(input, window.Event); + return this.instanceof(input, Event); }, cue(input) { - return this.instanceof(input, window.TextTrackCue) || this.instanceof(input, window.VTTCue); + return this.instanceof(input, TextTrackCue) || this.instanceof(input, VTTCue); }, track(input) { return this.instanceof(input, TextTrack) || (!this.nullOrUndefined(input) && this.string(input.kind)); @@ -547,7 +544,7 @@ const utils = { const event = new CustomEvent(type, { bubbles, detail: Object.assign({}, detail, { - plyr: utils.is.plyr(this) ? this : null, + plyr: this, }), }); -- cgit v1.2.3 From f2fc3f5ea5c9df297f553b161fc0d6e5fa6f4ba5 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Sat, 12 May 2018 00:02:05 +0200 Subject: Fix the seek tooltip time difference from seek time --- src/js/controls.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/js') diff --git a/src/js/controls.js b/src/js/controls.js index ec64977e..5c7c592b 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -443,7 +443,7 @@ const controls = { // Calculate percentage let percent = 0; - const clientRect = this.elements.inputs.seek.getBoundingClientRect(); + const clientRect = this.elements.progress.getBoundingClientRect(); const visible = `${this.config.classNames.tooltip}--visible`; const toggle = toggle => { -- cgit v1.2.3 From 7ca21697907850790f875d01bf27870a0386527b Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 14 May 2018 06:49:04 +0200 Subject: Expose defaults (enable overriding) --- src/js/plyr.js | 3 +++ src/js/utils.js | 5 +++++ 2 files changed, 8 insertions(+) (limited to 'src/js') diff --git a/src/js/plyr.js b/src/js/plyr.js index 6a3deade..bed09827 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -55,6 +55,7 @@ class Plyr { this.config = utils.extend( {}, defaults, + Plyr.defaults, options || {}, (() => { try { @@ -1269,4 +1270,6 @@ class Plyr { } } +Plyr.defaults = utils.cloneDeep(defaults); + export default Plyr; diff --git a/src/js/utils.js b/src/js/utils.js index ebfb9c96..5c4934a1 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -703,6 +703,11 @@ const utils = { return array.filter((item, index) => array.indexOf(item) === index); }, + // Clone nested objects + cloneDeep(object) { + return JSON.parse(JSON.stringify(object)); + }, + // Get the closest value in an array closest(array, value) { if (!utils.is.array(array) || !array.length) { -- cgit v1.2.3 From d1acc4abb3e5c7d0b767b3977a6bc804e5b85b45 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 14 May 2018 19:47:50 +0200 Subject: Add event before seeking via mouse interaction to set alternative 'value' for the input matching the tooltip time --- src/js/listeners.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) (limited to 'src/js') diff --git a/src/js/listeners.js b/src/js/listeners.js index f4e9ade3..38cf530a 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -529,12 +529,30 @@ class Listeners { } }); + // Set range input alternative "value", which matches the tooltip time (#954) + on( + this.player.elements.inputs.seek, + 'mousedown mousemove', + event => { + const clientRect = this.player.elements.progress.getBoundingClientRect(); + const percent = 100 / clientRect.width * (event.pageX - clientRect.left); + event.currentTarget.setAttribute('seekNext', percent); + } + ); + // Seek on( this.player.elements.inputs.seek, inputEvent, event => { - this.player.currentTime = event.target.value / event.target.max * this.player.duration; + const seek = event.currentTarget; + // If it exists, use seekNext instead of "value" for consistency with tooltip time (#954) + let seekTo = seek.getAttribute('seekNext'); + if (utils.is.empty(seekTo)) { + seekTo = seek.value; + } + seek.removeAttribute('seekNext'); + this.player.currentTime = seekTo / seek.max * this.player.duration; }, 'seek', ); -- cgit v1.2.3 From 90d5b48845661ce99a204354f93fbbbc7a19f100 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Tue, 15 May 2018 04:23:27 +0200 Subject: Add async method to utils for loading/checking images --- src/js/utils.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'src/js') diff --git a/src/js/utils.js b/src/js/utils.js index ebfb9c96..0cd332dd 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -119,6 +119,21 @@ const utils = { }); }, + // Load image avoiding xhr/fetch CORS issues + // Server status can't be obtained this way unfortunately, so this uses "naturalWidth" to determine if the image has loaded. + // By default it checks if it is at least 1px, but you can add a second argument to change this. + loadImage(src, minWidth = 1) { + return new Promise((resolve, reject) => { + const image = new Image(); + const handler = () => { + delete image.onload; + delete image.onerror; + (image.naturalWidth >= minWidth ? resolve : reject)(image); + }; + Object.assign(image, {onload: handler, onerror: handler, src}); + }); + }, + // Load an external script loadScript(url) { return new Promise((resolve, reject) => { -- cgit v1.2.3 From 16c3a7d9e5be8ed2ffbbcee1c786b88d1cecc4cd Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Tue, 15 May 2018 05:16:06 +0200 Subject: Rewrite ui.setPoster to check that images arent broken or youtube fallback images. Only show poster element when valid --- src/js/defaults.js | 2 +- src/js/plugins/vimeo.js | 7 ++----- src/js/plugins/youtube.js | 8 +++++++- src/js/plyr.js | 5 +---- src/js/ui.js | 39 ++++++++++++++++++++++++++++++--------- 5 files changed, 41 insertions(+), 20 deletions(-) (limited to 'src/js') diff --git a/src/js/defaults.js b/src/js/defaults.js index f160b1aa..7cc5c082 100644 --- a/src/js/defaults.js +++ b/src/js/defaults.js @@ -199,7 +199,6 @@ const defaults = { youtube: { sdk: 'https://www.youtube.com/iframe_api', api: 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet', - poster: 'https://img.youtube.com/vi/{0}/maxresdefault.jpg,https://img.youtube.com/vi/{0}/hqdefault.jpg', }, googleIMA: { sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js', @@ -332,6 +331,7 @@ const defaults = { embed: 'plyr__video-embed', embedContainer: 'plyr__video-embed__container', poster: 'plyr__poster', + posterEnabled: 'plyr__poster-enabled', ads: 'plyr__ads', control: 'plyr__control', playing: 'plyr--playing', diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js index 0ceb89e5..96b36781 100644 --- a/src/js/plugins/vimeo.js +++ b/src/js/plugins/vimeo.js @@ -99,11 +99,8 @@ const vimeo = { // Get original image url.pathname = `${url.pathname.split('_')[0]}.jpg`; - // Set attribute - player.media.setAttribute('poster', url.href); - - // Update - ui.setPoster.call(player); + // Set and show poster + ui.setPoster.call(player, url.href); }); // Setup instance diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index 4fde9319..4ba8089b 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -162,7 +162,13 @@ const youtube = { player.media = utils.replaceElement(container, player.media); // Set poster image - player.media.setAttribute('poster', utils.format(player.config.urls.youtube.poster, videoId)); + const posterSrc = format => `https://img.youtube.com/vi/${videoId}/${format}default.jpg`; + + // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide) + utils.loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded + .catch(() => utils.loadImage(posterSrc('sd'), 121)) // 480p padded 4:3 + .catch(() => utils.loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists + .then(image => ui.setPoster.call(player, image.src)); // Setup instance // https://developers.google.com/youtube/iframe_api_reference diff --git a/src/js/plyr.js b/src/js/plyr.js index 6a3deade..dee90dd2 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -801,10 +801,7 @@ class Plyr { return; } - if (utils.is.string(input)) { - this.media.setAttribute('poster', input); - ui.setPoster.call(this); - } + ui.setPoster.call(this, input); } /** diff --git a/src/js/ui.js b/src/js/ui.js index 2347b5c8..50764a86 100644 --- a/src/js/ui.js +++ b/src/js/ui.js @@ -105,8 +105,10 @@ const ui = { // Set the title ui.setTitle.call(this); - // Set the poster image - ui.setPoster.call(this); + // Assure the poster image is set, if the property was added before the UI was created + if (this.poster) { + ui.setPoster.call(this, this.poster); + } }, // Setup aria attribute for play and iframe title @@ -146,15 +148,34 @@ const ui = { } }, - // Set the poster image - setPoster() { - if (!utils.is.element(this.elements.poster) || utils.is.empty(this.poster)) { - return; + // Toggle poster + togglePoster(enable) { + utils.toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable); + }, + + // Set the poster image (async) + setPoster(poster) { + // Set property regardless of validity + this.media.setAttribute('poster', poster); + + // Bail if element is missing + if (!utils.is.element(this.elements.poster)) { + return Promise.reject(); } - // Set the inline style - const posters = this.poster.split(','); - this.elements.poster.style.backgroundImage = posters.map(p => `url('${p}')`).join(','); + // Load the image, and set poster if successful + const loadPromise = utils.loadImage(poster) + .then(() => { + this.elements.poster.style.backgroundImage = `url('${poster}')`; + ui.togglePoster.call(this, true); + return poster; + }); + + // Hide the element if the poster can't be loaded (otherwise it will just be a black element covering the video) + loadPromise.catch(() => ui.togglePoster.call(this, false)); + + // Return the promise so the caller can use it as well + return loadPromise; }, // Check playing state -- cgit v1.2.3 From c845558d960412ad5e942334fd9f60ed173e0a5a Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Tue, 15 May 2018 16:22:51 +0200 Subject: Youtube poster: Set css backgroundSize to 'cover' for padded youtube thumbnails --- src/js/plugins/youtube.js | 8 +++++++- src/js/ui.js | 9 +++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) (limited to 'src/js') diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index 4ba8089b..10283998 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -168,7 +168,13 @@ const youtube = { utils.loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded .catch(() => utils.loadImage(posterSrc('sd'), 121)) // 480p padded 4:3 .catch(() => utils.loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists - .then(image => ui.setPoster.call(player, image.src)); + .then(image => ui.setPoster.call(player, image.src)) + .then(posterSrc => { + // If the image is padded, use background-size "cover" instead (like youtube does too with their posters) + if (!posterSrc.includes('maxres')) { + player.elements.poster.style.backgroundSize = 'cover'; + } + }); // Setup instance // https://developers.google.com/youtube/iframe_api_reference diff --git a/src/js/ui.js b/src/js/ui.js index 50764a86..f844f93c 100644 --- a/src/js/ui.js +++ b/src/js/ui.js @@ -105,8 +105,8 @@ const ui = { // Set the title ui.setTitle.call(this); - // Assure the poster image is set, if the property was added before the UI was created - if (this.poster) { + // Assure the poster image is set, if the property was added before the element was created + if (this.poster && this.elements.poster && !this.elements.poster.style.backgroundImage) { ui.setPoster.call(this, this.poster); } }, @@ -167,6 +167,11 @@ const ui = { const loadPromise = utils.loadImage(poster) .then(() => { this.elements.poster.style.backgroundImage = `url('${poster}')`; + Object.assign(this.elements.poster.style, { + backgroundImage: `url('${poster}')`, + // Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube) + backgroundSize: '', + }); ui.togglePoster.call(this, true); return poster; }); -- cgit v1.2.3 From 4db6bf7a2e2256c20c885a536fadd4a5f4433469 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Wed, 9 May 2018 22:08:09 +0200 Subject: Make utils.toggleClass() compatible with Element.classList.toggle (rename toggle argument to 'force' and make it optional) --- src/js/utils.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) (limited to 'src/js') diff --git a/src/js/utils.js b/src/js/utils.js index 5c4934a1..0a1c8fcc 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -390,14 +390,16 @@ const utils = { } }, - // Toggle class on an element - toggleClass(element, className, toggle) { + // Mirror Element.classList.toggle, with IE compatibility for "force" argument + toggleClass(element, className, force) { if (utils.is.element(element)) { - const contains = element.classList.contains(className); - - element.classList[toggle ? 'add' : 'remove'](className); + let method = 'toggle'; + if (typeof force !== 'undefined') { + method = force ? 'add' : 'remove'; + } - return (toggle && !contains) || (!toggle && contains); + element.classList[method](className); + return element.classList.contains(className); } return null; -- cgit v1.2.3 From d7356726a16d64d242cd20421bff431d66d146b8 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Thu, 10 May 2018 00:36:02 +0200 Subject: Remove ui.checkFailed() and error class --- src/js/defaults.js | 1 - src/js/listeners.js | 3 --- src/js/ui.js | 23 ----------------------- 3 files changed, 27 deletions(-) (limited to 'src/js') diff --git a/src/js/defaults.js b/src/js/defaults.js index f160b1aa..da089efc 100644 --- a/src/js/defaults.js +++ b/src/js/defaults.js @@ -338,7 +338,6 @@ const defaults = { paused: 'plyr--paused', stopped: 'plyr--stopped', loading: 'plyr--loading', - error: 'plyr--has-error', hover: 'plyr--hover', tooltip: 'plyr__tooltip', cues: 'plyr__cues', diff --git a/src/js/listeners.js b/src/js/listeners.js index f4e9ade3..ebcc5f06 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -283,9 +283,6 @@ class Listeners { // Loading state utils.on(this.player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(this.player, event)); - // Check if media failed to load - // utils.on(this.player.media, 'play', event => ui.checkFailed.call(this.player, event)); - // If autoplay, then load advertisement if required // TODO: Show some sort of loading state while the ad manager loads else there's a delay before ad shows utils.on(this.player.media, 'playing', () => { diff --git a/src/js/ui.js b/src/js/ui.js index 2347b5c8..ea592d82 100644 --- a/src/js/ui.js +++ b/src/js/ui.js @@ -196,29 +196,6 @@ const ui = { }, this.loading ? 250 : 0); }, - // Check if media failed to load - checkFailed() { - // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/networkState - this.failed = this.media.networkState === 3; - - if (this.failed) { - utils.toggleClass(this.elements.container, this.config.classNames.loading, false); - utils.toggleClass(this.elements.container, this.config.classNames.error, true); - } - - // Clear timer - clearTimeout(this.timers.failed); - - // Timer to prevent flicker when seeking - this.timers.loading = setTimeout(() => { - // Toggle container class hook - utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading); - - // Show controls if loading, hide if done - this.toggleControls(this.loading); - }, this.loading ? 250 : 0); - }, - // Update volume UI and storage updateVolume() { if (!this.supported.ui) { -- cgit v1.2.3 From 37c5fbfe16ba0969b727b8359fdd04eb0bf7a021 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Wed, 16 May 2018 04:52:04 +0200 Subject: toggleControls() rewrite --- src/js/listeners.js | 92 ++++++++++++++++++++++++++++----------- src/js/plyr.js | 123 ++++++++-------------------------------------------- src/js/ui.js | 18 ++++++-- 3 files changed, 99 insertions(+), 134 deletions(-) (limited to 'src/js') diff --git a/src/js/listeners.js b/src/js/listeners.js index ebcc5f06..d095bc03 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -238,13 +238,36 @@ class Listeners { }, 0); }); - // Toggle controls visibility based on mouse movement - if (this.player.config.hideControls) { - // Toggle controls on mouse events and entering fullscreen - utils.on(this.player.elements.container, 'mouseenter mouseleave mousemove touchstart touchend touchmove enterfullscreen exitfullscreen', event => { - this.player.toggleControls(event); - }); - } + // Toggle controls on mouse events and entering fullscreen + utils.on(this.player.elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => { + const { controls } = this.player.elements; + + // Remove button states for fullscreen + if (event.type === 'enterfullscreen') { + controls.pressed = false; + controls.hover = false; + } + + // Show, then hide after a timeout unless another control event occurs + const show = [ + 'touchstart', + 'touchmove', + 'mousemove', + ].includes(event.type); + + let delay = 0; + + if (show) { + ui.toggleControls.call(this.player, true); + // Use longer timeout for touch devices + delay = this.player.touch ? 3000 : 2000; + } + + // Clear timer + clearTimeout(this.player.timers.controls); + // Timer to prevent flicker when seeking + this.player.timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay); + }); } // Listen for media events @@ -570,26 +593,45 @@ class Listeners { // Seek tooltip on(this.player.elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(this.player, event)); - // Toggle controls visibility based on mouse movement - if (this.player.config.hideControls) { - // Watch for cursor over controls so they don't hide when trying to interact - on(this.player.elements.controls, 'mouseenter mouseleave', event => { - this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter'; - }); + // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting) + on(this.player.elements.controls, 'mouseenter mouseleave', event => { + this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter'; + }); - // Watch for cursor over controls so they don't hide when trying to interact - on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => { - this.player.elements.controls.pressed = [ - 'mousedown', - 'touchstart', - ].includes(event.type); - }); + // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting) + on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => { + this.player.elements.controls.pressed = [ + 'mousedown', + 'touchstart', + ].includes(event.type); + }); - // Focus in/out on controls - on(this.player.elements.controls, 'focusin focusout', event => { - this.player.toggleControls(event); - }); - } + // Focus in/out on controls + on(this.player.elements.controls, 'focusin focusout', event => { + const { config, elements, timers } = this.player; + + // Skip transition to prevent focus from scrolling the parent element + utils.toggleClass(elements.controls, config.classNames.noTransition, event.type === 'focusin'); + + // Toggle + ui.toggleControls.call(this.player, event.type === 'focusin'); + + // If focusin, hide again after delay + if (event.type === 'focusin') { + // Restore transition + setTimeout(() => { + utils.toggleClass(elements.controls, config.classNames.noTransition, false); + }, 0); + + // Delay a little more for keyboard users + const delay = this.touch ? 3000 : 4000; + + // Clear timer + clearTimeout(timers.controls); + // Hide + timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay); + } + }); // Mouse wheel for volume on( diff --git a/src/js/plyr.js b/src/js/plyr.js index bed09827..ffd2a1e3 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -971,119 +971,32 @@ class Plyr { /** * Toggle the player controls - * @param {boolean} toggle - Whether to show the controls + * @param {boolean} [toggle] - Whether to show the controls */ toggleControls(toggle) { - // We need controls of course... - if (!utils.is.element(this.elements.controls)) { - return; - } - - // Don't hide if no UI support or it's audio - if (!this.supported.ui || this.isAudio) { - return; - } - - let delay = 0; - let show = toggle; - let isEnterFullscreen = false; - - // Get toggle state if not set - if (!utils.is.boolean(toggle)) { - if (utils.is.event(toggle)) { - // Is the enter fullscreen event - isEnterFullscreen = toggle.type === 'enterfullscreen'; - - // Events that show the controls - const showEvents = [ - 'touchstart', - 'touchmove', - 'mouseenter', - 'mousemove', - 'focusin', - ]; - - // Events that delay hiding - const delayEvents = [ - 'touchmove', - 'touchend', - 'mousemove', - ]; - - // Whether to show controls - show = showEvents.includes(toggle.type); - - // Delay hiding on move events - if (delayEvents.includes(toggle.type)) { - delay = 2000; - } - - // Delay a little more for keyboard users - if (!this.touch && toggle.type === 'focusin') { - delay = 3000; - utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, true); - } - } else { - show = utils.hasClass(this.elements.container, this.config.classNames.hideControls); - } - } + // Don't toggle if missing UI support or if it's audio + if (this.supported.ui && !this.isAudio) { + // Get state before change + const isHidden = utils.hasClass(this.elements.container, this.config.classNames.hideControls); - // Clear timer on every call - clearTimeout(this.timers.controls); + // Negate the argument if not undefined since adding the class to hides the controls + const force = typeof toggle === 'undefined' ? undefined : !toggle; - // If the mouse is not over the controls, set a timeout to hide them - if (show || this.paused || this.loading) { - // Check if controls toggled - const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, false); - - // Trigger event - if (toggled) { - utils.dispatchEvent.call(this, this.media, 'controlsshown'); - } + // Apply and get updated state + const hiding = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, force); - // Always show controls when paused or if touch - if (this.paused || this.loading) { - return; + // Close menu + if (hiding && this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) { + controls.toggleMenu.call(this, false); } - - // Delay for hiding on touch - if (this.touch) { - delay = 3000; + // Trigger event on change + if (hiding !== isHidden) { + const eventName = hiding ? 'controlshidden' : 'controlsshown'; + utils.dispatchEvent.call(this, this.media, eventName); } + return !hiding; } - - // If toggle is false or if we're playing (regardless of toggle), - // then set the timer to hide the controls - if (!show || this.playing) { - this.timers.controls = setTimeout(() => { - // We need controls of course... - if (!utils.is.element(this.elements.controls)) { - return; - } - - // If the mouse is over the controls (and not entering fullscreen), bail - if ((this.elements.controls.pressed || this.elements.controls.hover) && !isEnterFullscreen) { - return; - } - - // Restore transition behaviour - if (!utils.hasClass(this.elements.container, this.config.classNames.hideControls)) { - utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, false); - } - - // Set hideControls class - const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, this.config.hideControls); - - // Trigger event and close menu - if (toggled) { - utils.dispatchEvent.call(this, this.media, 'controlshidden'); - - if (this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) { - controls.toggleMenu.call(this, false); - } - } - }, delay); - } + return false; } /** diff --git a/src/js/ui.js b/src/js/ui.js index ea592d82..557599da 100644 --- a/src/js/ui.js +++ b/src/js/ui.js @@ -173,7 +173,7 @@ const ui = { } // Toggle controls - this.toggleControls(!this.playing); + ui.toggleControls.call(this); }, // Check if media is loading @@ -188,14 +188,24 @@ const ui = { // Timer to prevent flicker when seeking this.timers.loading = setTimeout(() => { - // Toggle container class hook + // Update progress bar loading class state utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading); - // Show controls if loading, hide if done - this.toggleControls(this.loading); + // Update controls visibility + ui.toggleControls.call(this); }, this.loading ? 250 : 0); }, + // Toggle controls based on state and `force` argument + toggleControls(force) { + const { controls } = this.elements; + + if (controls && this.config.hideControls) { + // Show controls if force, loading, paused, or button interaction, otherwise hide + this.toggleControls(Boolean(force || this.loading || this.paused || controls.pressed || controls.hover)); + } + }, + // Update volume UI and storage updateVolume() { if (!this.supported.ui) { -- cgit v1.2.3 From 51814249afd4337c1a7d1426ea913988228a7574 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sat, 19 May 2018 11:24:56 +1000 Subject: Reduce circular dependencies --- src/js/captions.js | 2 +- src/js/controls.js | 182 +++++++++++++++++++++++++++++++++++++++++++++------- src/js/listeners.js | 11 ++-- src/js/support.js | 1 + src/js/ui.js | 140 +--------------------------------------- src/js/utils.js | 11 ++-- 6 files changed, 176 insertions(+), 171 deletions(-) (limited to 'src/js') diff --git a/src/js/captions.js b/src/js/captions.js index e0692dcf..df717351 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -236,7 +236,7 @@ const captions = { // Set the span content if (utils.is.string(caption)) { - content.textContent = caption.trim(); + content.innerText = caption.trim(); } else { content.appendChild(caption); } diff --git a/src/js/controls.js b/src/js/controls.js index ec64977e..8a160864 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -6,34 +6,13 @@ import captions from './captions'; import html5 from './html5'; import i18n from './i18n'; import support from './support'; -import ui from './ui'; import utils from './utils'; // Sniff out the browser const browser = utils.getBrowser(); const controls = { - // Webkit polyfill for lower fill range - updateRangeFill(target) { - // Get range from event if event passed - const range = utils.is.event(target) ? target.target : target; - - // Needs to be a valid - if (!utils.is.element(range) || range.getAttribute('type') !== 'range') { - return; - } - - // Set aria value for https://github.com/sampotts/plyr/issues/905 - range.setAttribute('aria-valuenow', range.value); - - // WebKit only - if (!browser.isWebkit) { - return; - } - // Set CSS custom property - range.style.setProperty('--value', `${range.value / range.max * 100}%`); - }, // Get icon URL getIconUrl() { @@ -373,7 +352,7 @@ const controls = { break; } - progress.textContent = `% ${suffix.toLowerCase()}`; + progress.innerText = `% ${suffix.toLowerCase()}`; } this.elements.display[type] = progress; @@ -429,6 +408,123 @@ const controls = { list.appendChild(item); }, + // Update the displayed time + updateTimeDisplay(target = null, time = 0, inverted = false) { + // Bail if there's no element to display or the value isn't a number + if (!utils.is.element(target) || !utils.is.number(time)) { + return; + } + + // Always display hours if duration is over an hour + const forceHours = utils.getHours(this.duration) > 0; + + // eslint-disable-next-line no-param-reassign + target.innerText = utils.formatTime(time, forceHours, inverted); + }, + + // Update volume UI and storage + updateVolume() { + if (!this.supported.ui) { + return; + } + + // Update range + if (utils.is.element(this.elements.inputs.volume)) { + controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume); + } + + // Update mute state + if (utils.is.element(this.elements.buttons.mute)) { + utils.toggleState(this.elements.buttons.mute, this.muted || this.volume === 0); + } + }, + + // Update seek value and lower fill + setRange(target, value = 0) { + if (!utils.is.element(target)) { + return; + } + + // eslint-disable-next-line + target.value = value; + + // Webkit range fill + controls.updateRangeFill.call(this, target); + }, + + // Update elements + updateProgress(event) { + if (!this.supported.ui || !utils.is.event(event)) { + return; + } + + let value = 0; + + const setProgress = (target, input) => { + const value = utils.is.number(input) ? input : 0; + const progress = utils.is.element(target) ? target : this.elements.display.buffer; + + // Update value and label + if (utils.is.element(progress)) { + progress.value = value; + + // Update text label inside + const label = progress.getElementsByTagName('span')[0]; + if (utils.is.element(label)) { + label.childNodes[0].nodeValue = value; + } + } + }; + + if (event) { + switch (event.type) { + // Video playing + case 'timeupdate': + case 'seeking': + value = utils.getPercentage(this.currentTime, this.duration); + + // Set seek range value only if it's a 'natural' time event + if (event.type === 'timeupdate') { + controls.setRange.call(this, this.elements.inputs.seek, value); + } + + break; + + // Check buffer status + case 'playing': + case 'progress': + setProgress(this.elements.display.buffer, this.buffered * 100); + + break; + + default: + break; + } + } + }, + + // Webkit polyfill for lower fill range + updateRangeFill(target) { + // Get range from event if event passed + const range = utils.is.event(target) ? target.target : target; + + // Needs to be a valid + if (!utils.is.element(range) || range.getAttribute('type') !== 'range') { + return; + } + + // Set aria value for https://github.com/sampotts/plyr/issues/905 + range.setAttribute('aria-valuenow', range.value); + + // WebKit only + if (!browser.isWebkit) { + return; + } + + // Set CSS custom property + range.style.setProperty('--value', `${range.value / range.max * 100}%`); + }, + // Update hover tooltip for seeking updateSeekTooltip(event) { // Bail if setting not true @@ -473,7 +569,7 @@ const controls = { } // Display the time a click would seek to - ui.updateTimeDisplay.call(this, this.elements.display.seekTooltip, this.duration / 100 * percent); + controls.updateTimeDisplay.call(this, this.elements.display.seekTooltip, this.duration / 100 * percent); // Set position this.elements.display.seekTooltip.style.left = `${percent}%`; @@ -488,6 +584,46 @@ const controls = { } }, + // Handle time change event + timeUpdate(event) { + // Only invert if only one time element is displayed and used for both duration and currentTime + const invert = !utils.is.element(this.elements.display.duration) && this.config.invertTime; + + // Duration + controls.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert); + + // Ignore updates while seeking + if (event && event.type === 'timeupdate' && this.media.seeking) { + return; + } + + // Playing progress + controls.updateProgress.call(this, event); + }, + + // Show the duration on metadataloaded + durationUpdate() { + if (!this.supported.ui) { + return; + } + + // If there's a spot to display duration + const hasDuration = utils.is.element(this.elements.display.duration); + + // If there's only one time display, display duration there + if (!hasDuration && this.config.displayDuration && this.paused) { + controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration); + } + + // If there's a duration element, update content + if (hasDuration) { + controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration); + } + + // Update the tooltip (if visible) + controls.updateSeekTooltip.call(this); + }, + // Hide/show a tab toggleTab(setting, toggle) { utils.toggleHidden(this.elements.settings.tabs[setting], !toggle); diff --git a/src/js/listeners.js b/src/js/listeners.js index f4e9ade3..167bee3c 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -250,10 +250,10 @@ class Listeners { // Listen for media events media() { // Time change on media - utils.on(this.player.media, 'timeupdate seeking', event => ui.timeUpdate.call(this.player, event)); + utils.on(this.player.media, 'timeupdate seeking', event => controls.timeUpdate.call(this.player, event)); // Display duration - utils.on(this.player.media, 'durationchange loadeddata loadedmetadata', event => ui.durationUpdate.call(this.player, event)); + utils.on(this.player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(this.player, event)); // Check for audio tracks on load // We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point @@ -272,10 +272,10 @@ class Listeners { }); // Check for buffer progress - utils.on(this.player.media, 'progress playing', event => ui.updateProgress.call(this.player, event)); + utils.on(this.player.media, 'progress playing', event => controls.updateProgress.call(this.player, event)); // Handle volume changes - utils.on(this.player.media, 'volumechange', event => ui.updateVolume.call(this.player, event)); + utils.on(this.player.media, 'volumechange', event => controls.updateVolume.call(this.player, event)); // Handle play/pause utils.on(this.player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(this.player, event)); @@ -549,7 +549,8 @@ class Listeners { } this.player.config.invertTime = !this.player.config.invertTime; - ui.timeUpdate.call(this.player); + + controls.timeUpdate.call(this.player); }); } diff --git a/src/js/support.js b/src/js/support.js index 5528e898..38212d9f 100644 --- a/src/js/support.js +++ b/src/js/support.js @@ -133,6 +133,7 @@ const support = { }, }); window.addEventListener('test', null, options); + window.removeEventListener('test', null, options); } catch (e) { // Do nothing } diff --git a/src/js/ui.js b/src/js/ui.js index 2347b5c8..8f3f6a77 100644 --- a/src/js/ui.js +++ b/src/js/ui.js @@ -74,10 +74,10 @@ const ui = { this.quality = null; // Reset volume display - ui.updateVolume.call(this); + controls.updateVolume.call(this); // Reset time display - ui.timeUpdate.call(this); + controls.timeUpdate.call(this); // Update the UI ui.checkPlaying.call(this); @@ -218,142 +218,6 @@ const ui = { this.toggleControls(this.loading); }, this.loading ? 250 : 0); }, - - // Update volume UI and storage - updateVolume() { - if (!this.supported.ui) { - return; - } - - // Update range - if (utils.is.element(this.elements.inputs.volume)) { - ui.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume); - } - - // Update mute state - if (utils.is.element(this.elements.buttons.mute)) { - utils.toggleState(this.elements.buttons.mute, this.muted || this.volume === 0); - } - }, - - // Update seek value and lower fill - setRange(target, value = 0) { - if (!utils.is.element(target)) { - return; - } - - // eslint-disable-next-line - target.value = value; - - // Webkit range fill - controls.updateRangeFill.call(this, target); - }, - - // Set value - setProgress(target, input) { - const value = utils.is.number(input) ? input : 0; - const progress = utils.is.element(target) ? target : this.elements.display.buffer; - - // Update value and label - if (utils.is.element(progress)) { - progress.value = value; - - // Update text label inside - const label = progress.getElementsByTagName('span')[0]; - if (utils.is.element(label)) { - label.childNodes[0].nodeValue = value; - } - } - }, - - // Update elements - updateProgress(event) { - if (!this.supported.ui || !utils.is.event(event)) { - return; - } - - let value = 0; - - if (event) { - switch (event.type) { - // Video playing - case 'timeupdate': - case 'seeking': - value = utils.getPercentage(this.currentTime, this.duration); - - // Set seek range value only if it's a 'natural' time event - if (event.type === 'timeupdate') { - ui.setRange.call(this, this.elements.inputs.seek, value); - } - - break; - - // Check buffer status - case 'playing': - case 'progress': - ui.setProgress.call(this, this.elements.display.buffer, this.buffered * 100); - - break; - - default: - break; - } - } - }, - - // Update the displayed time - updateTimeDisplay(target = null, time = 0, inverted = false) { - // Bail if there's no element to display or the value isn't a number - if (!utils.is.element(target) || !utils.is.number(time)) { - return; - } - - // Always display hours if duration is over an hour - const forceHours = utils.getHours(this.duration) > 0; - - // eslint-disable-next-line no-param-reassign - target.textContent = utils.formatTime(time, forceHours, inverted); - }, - - // Handle time change event - timeUpdate(event) { - // Only invert if only one time element is displayed and used for both duration and currentTime - const invert = !utils.is.element(this.elements.display.duration) && this.config.invertTime; - - // Duration - ui.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert); - - // Ignore updates while seeking - if (event && event.type === 'timeupdate' && this.media.seeking) { - return; - } - - // Playing progress - ui.updateProgress.call(this, event); - }, - - // Show the duration on metadataloaded - durationUpdate() { - if (!this.supported.ui) { - return; - } - - // If there's a spot to display duration - const hasDuration = utils.is.element(this.elements.display.duration); - - // If there's only one time display, display duration there - if (!hasDuration && this.config.displayDuration && this.paused) { - ui.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration); - } - - // If there's a duration element, update content - if (hasDuration) { - ui.updateTimeDisplay.call(this, this.elements.display.duration, this.duration); - } - - // Update the tooltip (if visible) - controls.updateSeekTooltip.call(this); - }, }; export default ui; diff --git a/src/js/utils.js b/src/js/utils.js index ebfb9c96..1de5ed6c 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -3,6 +3,7 @@ // ========================================================================== import loadjs from 'loadjs'; +import Storage from './storage'; import support from './support'; import { providers } from './types'; @@ -156,6 +157,8 @@ const utils = { // Only load once if ID set if (!hasId || !exists()) { + const useStorage = Storage.supported; + // Create container const container = document.createElement('div'); utils.toggleHidden(container, true); @@ -165,7 +168,7 @@ const utils = { } // Check in cache - if (support.storage) { + if (useStorage) { const cached = window.localStorage.getItem(prefix + id); isCached = cached !== null; @@ -184,7 +187,7 @@ const utils = { return; } - if (support.storage) { + if (useStorage) { window.localStorage.setItem( prefix + id, JSON.stringify({ @@ -247,7 +250,7 @@ const utils = { // Add text node if (utils.is.string(text)) { - element.textContent = text; + element.innerText = text; } // Return built element @@ -580,7 +583,7 @@ const utils = { return input; } - return input.toString().replace(/{(\d+)}/g, (match, i) => utils.is.string(args[i]) ? args[i] : ''); + return input.toString().replace(/{(\d+)}/g, (match, i) => (utils.is.string(args[i]) ? args[i] : '')); }, // Get percentage -- cgit v1.2.3 From 333435a9c2ce263686052318497e604c424d38f3 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Fri, 18 May 2018 00:34:59 +0200 Subject: Fix playback state (paused) and events (play/pause) --- src/js/plugins/vimeo.js | 27 ++++++++++++++------------- src/js/plugins/youtube.js | 26 ++++++++++++++++---------- 2 files changed, 30 insertions(+), 23 deletions(-) (limited to 'src/js') diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js index 96b36781..ec047c9b 100644 --- a/src/js/plugins/vimeo.js +++ b/src/js/plugins/vimeo.js @@ -7,6 +7,14 @@ import controls from './../controls'; import ui from './../ui'; import utils from './../utils'; +// Set playback state and trigger change (only on actual change) +function assurePlaybackState(play) { + if (this.media.paused === play) { + this.media.paused = !play; + utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause'); + } +} + const vimeo = { setup() { // Add embed class for responsive @@ -120,15 +128,13 @@ const vimeo = { // Create a faux HTML5 API using the Vimeo API player.media.play = () => { - player.embed.play().then(() => { - player.media.paused = false; - }); + assurePlaybackState.call(player, true); + return player.embed.play(); }; player.media.pause = () => { - player.embed.pause().then(() => { - player.media.paused = true; - }); + assurePlaybackState.call(player, false); + return player.embed.pause(); }; player.media.stop = () => { @@ -315,17 +321,12 @@ const vimeo = { }); player.embed.on('play', () => { - // Only fire play if paused before - if (player.media.paused) { - utils.dispatchEvent.call(player, player.media, 'play'); - } - player.media.paused = false; + assurePlaybackState.call(player, true); utils.dispatchEvent.call(player, player.media, 'playing'); }); player.embed.on('pause', () => { - player.media.paused = true; - utils.dispatchEvent.call(player, player.media, 'pause'); + assurePlaybackState.call(player, false); }); player.embed.on('timeupdate', data => { diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index 10283998..794d099b 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -64,6 +64,14 @@ function mapQualityUnits(levels) { return utils.dedupe(levels.map(level => mapQualityUnit(level))); } +// Set playback state and trigger change (only on actual change) +function assurePlaybackState(play) { + if (this.media.paused === play) { + this.media.paused = !play; + utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause'); + } +} + const youtube = { setup() { // Add embed class for responsive @@ -264,10 +272,12 @@ const youtube = { // Create a faux HTML5 API using the YouTube API player.media.play = () => { + assurePlaybackState.call(player, true); instance.playVideo(); }; player.media.pause = () => { + assurePlaybackState.call(player, false); instance.pauseVideo(); }; @@ -438,7 +448,8 @@ const youtube = { break; case 0: - player.media.paused = true; + // Assure state and event + assurePlaybackState.call(player, false); // YouTube doesn't support loop for a single video, so mimick it. if (player.media.loop) { @@ -455,14 +466,11 @@ const youtube = { // If we were seeking, fire seeked event if (player.media.seeking) { utils.dispatchEvent.call(player, player.media, 'seeked'); + player.media.seeking = false; } - player.media.seeking = false; - // Only fire play if paused before - if (player.media.paused) { - utils.dispatchEvent.call(player, player.media, 'play'); - } - player.media.paused = false; + // Assure state and event (must be done after seeked event) + assurePlaybackState.call(player, true); utils.dispatchEvent.call(player, player.media, 'playing'); @@ -485,9 +493,7 @@ const youtube = { break; case 2: - player.media.paused = true; - - utils.dispatchEvent.call(player, player.media, 'pause'); + assurePlaybackState.call(player, false); break; -- cgit v1.2.3 From f8c89e3e95cb01a621f59d66c60f0fa2d76c4d58 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Fri, 18 May 2018 04:42:58 +0200 Subject: Fix #876: YouTube and Vimeo autoplays on seek --- src/js/plugins/vimeo.js | 40 ++++++++++++++++---------------- src/js/plugins/youtube.js | 58 ++++++++++++++++++++++++----------------------- 2 files changed, 50 insertions(+), 48 deletions(-) (limited to 'src/js') diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js index ec047c9b..46d4f3f9 100644 --- a/src/js/plugins/vimeo.js +++ b/src/js/plugins/vimeo.js @@ -149,25 +149,26 @@ const vimeo = { return currentTime; }, set(time) { - // Get current paused state - // Vimeo will automatically play on seek - const { paused } = player.media; - - // Set seeking flag - player.media.seeking = true; - - // Trigger seeking - utils.dispatchEvent.call(player, player.media, 'seeking'); - - // Seek after events - player.embed.setCurrentTime(time).catch(() => { - // Do nothing - }); - - // Restore pause state - if (paused) { - player.pause(); - } + // 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; + + // Set seeking state and trigger event + media.seeking = true; + utils.dispatchEvent.call(player, media, 'seeking'); + + // If paused, mute until seek is complete + Promise.resolve(paused && embed.setVolume(0)) + // Seek + .then(() => embed.setCurrentTime(time)) + // Restore paused + .then(() => paused && embed.pause()) + // Restore volume + .then(() => paused && embed.setVolume(volume)) + .catch(() => { + // Do nothing + }); }, }); @@ -357,7 +358,6 @@ const vimeo = { player.embed.on('seeked', () => { player.media.seeking = false; utils.dispatchEvent.call(player, player.media, 'seeked'); - utils.dispatchEvent.call(player, player.media, 'play'); }); player.embed.on('ended', () => { diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index 794d099b..391da6ca 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -295,22 +295,17 @@ const youtube = { return Number(instance.getCurrentTime()); }, set(time) { - // Vimeo will automatically play on seek - const { paused } = player.media; + // If paused, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet). + if (player.paused) { + player.embed.mute(); + } - // Set seeking flag + // Set seeking state and trigger event player.media.seeking = true; - - // Trigger seeking utils.dispatchEvent.call(player, player.media, 'seeking'); // Seek after events sent instance.seekTo(time); - - // Restore pause state - if (paused) { - player.pause(); - } }, }); @@ -448,7 +443,6 @@ const youtube = { break; case 0: - // Assure state and event assurePlaybackState.call(player, false); // YouTube doesn't support loop for a single video, so mimick it. @@ -465,34 +459,42 @@ const youtube = { case 1: // If we were seeking, fire seeked event if (player.media.seeking) { - utils.dispatchEvent.call(player, player.media, 'seeked'); player.media.seeking = false; + utils.dispatchEvent.call(player, player.media, 'seeked'); } - // Assure state and event (must be done after seeked event) - assurePlaybackState.call(player, true); + // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet) + if (player.media.paused) { + player.media.pause(); + } else { + assurePlaybackState.call(player, true); - utils.dispatchEvent.call(player, player.media, 'playing'); + utils.dispatchEvent.call(player, player.media, 'playing'); - // Poll to get playback progress - player.timers.playing = setInterval(() => { - utils.dispatchEvent.call(player, player.media, 'timeupdate'); - }, 50); + // Poll to get playback progress + player.timers.playing = setInterval(() => { + utils.dispatchEvent.call(player, player.media, 'timeupdate'); + }, 50); - // Check duration again due to YouTube bug - // https://github.com/sampotts/plyr/issues/374 - // https://code.google.com/p/gdata-issues/issues/detail?id=8690 - if (player.media.duration !== instance.getDuration()) { - player.media.duration = instance.getDuration(); - utils.dispatchEvent.call(player, player.media, 'durationchange'); - } + // Check duration again due to YouTube bug + // https://github.com/sampotts/plyr/issues/374 + // https://code.google.com/p/gdata-issues/issues/detail?id=8690 + if (player.media.duration !== instance.getDuration()) { + player.media.duration = instance.getDuration(); + utils.dispatchEvent.call(player, player.media, 'durationchange'); + } - // Get quality - controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels())); + // Get quality + controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels())); + } break; case 2: + // Restore audio (YouTube starts playing on seek if the video hasn't been played yet) + if (!player.muted) { + player.embed.unMute(); + } assurePlaybackState.call(player, false); break; -- cgit v1.2.3 From 723298a07b4099486f6c071167979a8f8e2abed2 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Sat, 12 May 2018 10:09:53 +0200 Subject: Fix #921: Trigger seeked event in youtube plugin if either playing or paused --- src/js/plugins/youtube.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) (limited to 'src/js') diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index 391da6ca..67b8093e 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -424,6 +424,17 @@ const youtube = { // Reset timer clearInterval(player.timers.playing); + const seeked = player.media.seeking && [ + 1, + 2, + ].includes(event.data); + + if (seeked) { + // Unset seeking and fire seeked event + player.media.seeking = false; + utils.dispatchEvent.call(player, player.media, 'seeked'); + } + // Handle events // -1 Unstarted // 0 Ended @@ -457,12 +468,6 @@ const youtube = { break; case 1: - // If we were seeking, fire seeked event - if (player.media.seeking) { - player.media.seeking = false; - utils.dispatchEvent.call(player, player.media, 'seeked'); - } - // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet) if (player.media.paused) { player.media.pause(); -- cgit v1.2.3 From aa8fc313a91ec38b6c430c9b88eec7944948c521 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Sat, 12 May 2018 11:24:50 +0200 Subject: Fix #966: Add 'seeked' event listener to update progress (seeking doesn't have the correct time) --- src/js/controls.js | 1 + src/js/listeners.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'src/js') diff --git a/src/js/controls.js b/src/js/controls.js index 3f720925..61fb2039 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -481,6 +481,7 @@ const controls = { // Video playing case 'timeupdate': case 'seeking': + case 'seeked': value = utils.getPercentage(this.currentTime, this.duration); // Set seek range value only if it's a 'natural' time event diff --git a/src/js/listeners.js b/src/js/listeners.js index d5748806..99eeade4 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -273,7 +273,7 @@ class Listeners { // Listen for media events media() { // Time change on media - utils.on(this.player.media, 'timeupdate seeking', event => controls.timeUpdate.call(this.player, event)); + utils.on(this.player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(this.player, event)); // Display duration utils.on(this.player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(this.player, event)); @@ -295,7 +295,7 @@ class Listeners { }); // Check for buffer progress - utils.on(this.player.media, 'progress playing', event => controls.updateProgress.call(this.player, event)); + utils.on(this.player.media, 'progress playing seeking seeked', event => controls.updateProgress.call(this.player, event)); // Handle volume changes utils.on(this.player.media, 'volumechange', event => controls.updateVolume.call(this.player, event)); -- cgit v1.2.3 From 121093ae7113cbd74d4202332a88d184c0049320 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Sat, 12 May 2018 11:54:28 +0200 Subject: Prevent durationchange events from showing time when invertTime is false --- src/js/controls.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'src/js') diff --git a/src/js/controls.js b/src/js/controls.js index 61fb2039..d266ed6b 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -602,9 +602,10 @@ const controls = { controls.updateProgress.call(this, event); }, - // Show the duration on metadataloaded + // Show the duration on metadataloaded or durationchange events durationUpdate() { - if (!this.supported.ui) { + // Bail if no ui or durationchange event triggered after playing/seek when invertTime is false + if (!this.supported.ui || (!this.config.invertTime && this.currentTime)) { return; } -- cgit v1.2.3 From 41f9a87e0ec01146fe870fa2412ef6d4004221c1 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sun, 20 May 2018 23:40:00 +1000 Subject: Add URL polyfill --- src/js/plyr.polyfilled.js | 1 + 1 file changed, 1 insertion(+) (limited to 'src/js') diff --git a/src/js/plyr.polyfilled.js b/src/js/plyr.polyfilled.js index 635dee63..27b13dfd 100644 --- a/src/js/plyr.polyfilled.js +++ b/src/js/plyr.polyfilled.js @@ -7,6 +7,7 @@ import 'babel-polyfill'; import 'custom-event-polyfill'; +import 'url-polyfill'; import Plyr from './plyr'; export default Plyr; -- cgit v1.2.3 From d733454d7f1d8441c11c625ef56d1991984be70a Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sun, 20 May 2018 23:40:28 +1000 Subject: Pause while seeking --- src/js/listeners.js | 43 +++++++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 10 deletions(-) (limited to 'src/js') diff --git a/src/js/listeners.js b/src/js/listeners.js index 99eeade4..b23238d2 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -550,15 +550,34 @@ class Listeners { }); // Set range input alternative "value", which matches the tooltip time (#954) - on( - this.player.elements.inputs.seek, - 'mousedown mousemove', - event => { - const clientRect = this.player.elements.progress.getBoundingClientRect(); - const percent = 100 / clientRect.width * (event.pageX - clientRect.left); - event.currentTarget.setAttribute('seekNext', percent); + on(this.player.elements.inputs.seek, 'mousedown mousemove', event => { + const clientRect = this.player.elements.progress.getBoundingClientRect(); + const percent = 100 / clientRect.width * (event.pageX - clientRect.left); + event.currentTarget.setAttribute('seek', percent); + }); + + // Pause while seeking + on(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => { + const seek = event.currentTarget; + + // Was playing before? + const play = seek.hasAttribute('play-on-seeked'); + + // Done seeking + const done = [ + 'mouseup', + 'touchend', + 'keyup', + ].includes(event.type); + + if (play && done) { + seek.removeAttribute('play-on-seeked'); + this.player.play(); + } else if (!done && this.player.playing) { + seek.setAttribute('play-on-seeked', ''); + this.player.pause(); } - ); + }); // Seek on( @@ -566,12 +585,16 @@ class Listeners { inputEvent, event => { const seek = event.currentTarget; + // If it exists, use seekNext instead of "value" for consistency with tooltip time (#954) - let seekTo = seek.getAttribute('seekNext'); + let seekTo = seek.getAttribute('seek'); + if (utils.is.empty(seekTo)) { seekTo = seek.value; } - seek.removeAttribute('seekNext'); + + seek.removeAttribute('seek'); + this.player.currentTime = seekTo / seek.max * this.player.duration; }, 'seek', -- cgit v1.2.3 From edd67b0da36ac57e98d70223d6f4eacd4548e096 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sun, 20 May 2018 23:44:40 +1000 Subject: Typo --- src/js/listeners.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'src/js') diff --git a/src/js/listeners.js b/src/js/listeners.js index b23238d2..86236fe3 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -553,7 +553,7 @@ class Listeners { on(this.player.elements.inputs.seek, 'mousedown mousemove', event => { const clientRect = this.player.elements.progress.getBoundingClientRect(); const percent = 100 / clientRect.width * (event.pageX - clientRect.left); - event.currentTarget.setAttribute('seek', percent); + event.currentTarget.setAttribute('seek-value', percent); }); // Pause while seeking @@ -570,6 +570,7 @@ class Listeners { 'keyup', ].includes(event.type); + // If we're done seeking and it was playing, resume playback if (play && done) { seek.removeAttribute('play-on-seeked'); this.player.play(); @@ -586,14 +587,14 @@ class Listeners { event => { const seek = event.currentTarget; - // If it exists, use seekNext instead of "value" for consistency with tooltip time (#954) - let seekTo = seek.getAttribute('seek'); + // If it exists, use seek-value instead of "value" for consistency with tooltip time (#954) + let seekTo = seek.getAttribute('seek-value'); if (utils.is.empty(seekTo)) { seekTo = seek.value; } - seek.removeAttribute('seek'); + seek.removeAttribute('seek-value'); this.player.currentTime = seekTo / seek.max * this.player.duration; }, -- cgit v1.2.3 From cd51788b980a7bc7b5caaf2d595d2077be4138f5 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sat, 26 May 2018 13:53:15 +1000 Subject: v3.3.8 --- src/js/controls.js | 1 - src/js/defaults.js | 2 +- src/js/plyr.js | 2 +- src/js/plyr.polyfilled.js | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) (limited to 'src/js') diff --git a/src/js/controls.js b/src/js/controls.js index d266ed6b..c76bd66b 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -1165,7 +1165,6 @@ const controls = { const tooltip = utils.createElement( 'span', { - role: 'tooltip', class: this.config.classNames.tooltip, }, '00:00', diff --git a/src/js/defaults.js b/src/js/defaults.js index f66a7c2f..5b1a4dd3 100644 --- a/src/js/defaults.js +++ b/src/js/defaults.js @@ -56,7 +56,7 @@ const defaults = { // Sprite (for icons) loadSprite: true, iconPrefix: 'plyr', - iconUrl: 'https://cdn.plyr.io/3.3.7/plyr.svg', + iconUrl: 'https://cdn.plyr.io/3.3.8/plyr.svg', // Blank video (used to prevent errors on source change) blankVideo: 'https://cdn.plyr.io/static/blank.mp4', diff --git a/src/js/plyr.js b/src/js/plyr.js index 4c569fec..4c984fd7 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr -// plyr.js v3.3.7 +// plyr.js v3.3.8 // https://github.com/sampotts/plyr // License: The MIT License (MIT) // ========================================================================== diff --git a/src/js/plyr.polyfilled.js b/src/js/plyr.polyfilled.js index 27b13dfd..9570d753 100644 --- a/src/js/plyr.polyfilled.js +++ b/src/js/plyr.polyfilled.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr Polyfilled Build -// plyr.js v3.3.7 +// plyr.js v3.3.8 // https://github.com/sampotts/plyr // License: The MIT License (MIT) // ========================================================================== -- cgit v1.2.3 From f34bf221253be40e4a620107a3f27196d577608d Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Sun, 27 May 2018 20:28:48 +0200 Subject: Restore utils.is.cue() --- src/js/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/js') diff --git a/src/js/utils.js b/src/js/utils.js index 0c5a28d7..0334879d 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -44,7 +44,7 @@ const utils = { return this.instanceof(input, Event); }, cue(input) { - return this.instanceof(input, TextTrackCue) || this.instanceof(input, VTTCue); + return this.instanceof(input, window.TextTrackCue) || this.instanceof(input, window.VTTCue); }, track(input) { return this.instanceof(input, TextTrack) || (!this.nullOrUndefined(input) && this.string(input.kind)); -- cgit v1.2.3 From c69aa8a42b7f55276d35bf64a08e869654c3b0ce Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 28 May 2018 00:02:08 +0200 Subject: Avoid duration getter returning NaN before element has loaded --- src/js/plyr.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src/js') diff --git a/src/js/plyr.js b/src/js/plyr.js index 4c984fd7..21c00fd3 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -494,11 +494,11 @@ class Plyr { // Faux duration set via config const fauxDuration = parseFloat(this.config.duration); - // True duration - const realDuration = this.media ? Number(this.media.duration) : 0; + // Media duration can be NaN before the media has loaded + const duration = (this.media || {}).duration || 0; - // If custom duration is funky, use regular duration - return !Number.isNaN(fauxDuration) ? fauxDuration : realDuration; + // If config duration is funky, use regular duration + return fauxDuration || duration; } /** -- cgit v1.2.3 From fac8a185ba4a945ba33a45cdfd0dd55c53edf532 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 28 May 2018 00:33:17 +0200 Subject: Simplify currentTime setter and bail when media hasn't loaded --- src/js/plyr.js | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) (limited to 'src/js') diff --git a/src/js/plyr.js b/src/js/plyr.js index 21c00fd3..34b618bd 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -432,21 +432,16 @@ class Plyr { * @param {number} input - where to seek to in seconds. Defaults to 0 (the start) */ set currentTime(input) { - let targetTime = 0; - - if (utils.is.number(input)) { - targetTime = input; + // Bail if media duration isn't available yet + if (!this.duration) { + return; } - // Normalise targetTime - if (targetTime < 0) { - targetTime = 0; - } else if (targetTime > this.duration) { - targetTime = this.duration; - } + // Validate input + const inputIsValid = utils.is.number(input) && input > 0; // Set - this.media.currentTime = targetTime; + this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0; // Logging this.debug.log(`Seeking to ${this.currentTime} seconds`); -- cgit v1.2.3 From 6391ced99f27f2b14f8c77c6657926a0012a2a69 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Sun, 27 May 2018 05:48:38 +0200 Subject: If storage is disabled, disable get as well, not just set --- src/js/storage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/js') diff --git a/src/js/storage.js b/src/js/storage.js index 5b914331..e4dc9e1b 100644 --- a/src/js/storage.js +++ b/src/js/storage.js @@ -31,7 +31,7 @@ class Storage { } get(key) { - if (!Storage.supported) { + if (!Storage.supported || !this.enabled) { return null; } -- cgit v1.2.3 From 90c5735904354f5fde0dcdae9f8894fe9088739c Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 28 May 2018 10:19:07 +1000 Subject: WIP --- src/js/captions.js | 2 +- src/js/controls.js | 47 +++++++++++++++++++---------------------------- src/js/defaults.js | 5 +++++ src/js/fullscreen.js | 2 +- src/js/plyr.js | 2 +- src/js/ui.js | 25 +++++++++++++------------ src/js/utils.js | 22 ---------------------- 7 files changed, 40 insertions(+), 65 deletions(-) (limited to 'src/js') diff --git a/src/js/captions.js b/src/js/captions.js index df717351..fadab43f 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -262,7 +262,7 @@ const captions = { if (active) { utils.toggleClass(this.elements.container, this.config.classNames.captions.active, true); - utils.toggleState(this.elements.buttons.captions, true); + this.elements.buttons.captions.pressed = true; } }, }; diff --git a/src/js/controls.js b/src/js/controls.js index c76bd66b..fc000b52 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -243,9 +243,6 @@ const controls = { // Label/Tooltip button.appendChild(controls.createLabel.call(this, labelPressed, { class: 'label--pressed' })); button.appendChild(controls.createLabel.call(this, label, { class: 'label--not-pressed' })); - - // Add aria attributes - attributes['aria-pressed'] = false; } else { button.appendChild(controls.createIcon.call(this, icon)); button.appendChild(controls.createLabel.call(this, label)); @@ -267,22 +264,23 @@ const controls = { this.elements.buttons[type] = button; } + // Toggle classname when pressed property is set + const className = this.config.classNames.controlPressed; + Object.defineProperty(button, 'pressed', { + enumerable: true, + get() { + return utils.hasClass(button, className); + }, + set(pressed = false) { + utils.toggleClass(button, className, pressed); + }, + }); + return button; }, // Create an createRange(type, attributes) { - // Seek label - const label = utils.createElement( - 'label', - { - for: attributes.id, - id: `${attributes.id}-label`, - class: this.config.classNames.hidden, - }, - i18n.get(type, this.config), - ); - // Seek input const input = utils.createElement( 'input', @@ -297,7 +295,7 @@ const controls = { autocomplete: 'off', // A11y fixes for https://github.com/sampotts/plyr/issues/905 role: 'slider', - 'aria-labelledby': `${attributes.id}-label`, + 'aria-label': i18n.get(type, this.config), 'aria-valuemin': 0, 'aria-valuemax': 100, 'aria-valuenow': 0, @@ -311,10 +309,7 @@ const controls = { // Set the fill for webkit now controls.updateRangeFill.call(this, input); - return { - label, - input, - }; + return input; }, // Create a @@ -435,7 +430,7 @@ const controls = { // Update mute state if (utils.is.element(this.elements.buttons.mute)) { - utils.toggleState(this.elements.buttons.mute, this.muted || this.volume === 0); + this.elements.buttons.mute.pressed = this.muted || this.volume === 0; } }, @@ -1149,11 +1144,9 @@ const controls = { const progress = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.progress)); // Seek range slider - const seek = controls.createRange.call(this, 'seek', { + progress.appendChild(controls.createRange.call(this, 'seek', { id: `plyr-seek-${data.id}`, - }); - progress.appendChild(seek.label); - progress.appendChild(seek.input); + })); // Buffer progress progress.appendChild(controls.createProgress.call(this, 'buffer')); @@ -1207,15 +1200,13 @@ const controls = { }; // Create the volume range slider - const range = controls.createRange.call( + volume.appendChild(controls.createRange.call( this, 'volume', utils.extend(attributes, { id: `plyr-volume-${data.id}`, }), - ); - volume.appendChild(range.label); - volume.appendChild(range.input); + )); this.elements.volume = volume; diff --git a/src/js/defaults.js b/src/js/defaults.js index 5b1a4dd3..54c19f94 100644 --- a/src/js/defaults.js +++ b/src/js/defaults.js @@ -18,6 +18,10 @@ const defaults = { // 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, @@ -334,6 +338,7 @@ const defaults = { posterEnabled: 'plyr__poster-enabled', ads: 'plyr__ads', control: 'plyr__control', + controlPressed: 'plyr__control--pressed', playing: 'plyr--playing', paused: 'plyr--paused', stopped: 'plyr--stopped', diff --git a/src/js/fullscreen.js b/src/js/fullscreen.js index 000ba706..cc91d1a4 100644 --- a/src/js/fullscreen.js +++ b/src/js/fullscreen.js @@ -15,7 +15,7 @@ function onChange() { // Update toggle button const button = this.player.elements.buttons.fullscreen; if (utils.is.element(button)) { - utils.toggleState(button, this.active); + button.pressed = this.active; } // Trigger an event diff --git a/src/js/plyr.js b/src/js/plyr.js index 4c984fd7..2cf5d58d 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -854,7 +854,7 @@ class Plyr { this.captions.active = show; // Toggle state - utils.toggleState(this.elements.buttons.captions, this.captions.active); + this.elements.buttons.captions.pressed = this.captions.active; // Add class hook utils.toggleClass(this.elements.container, this.config.classNames.captions.active, this.captions.active); diff --git a/src/js/ui.js b/src/js/ui.js index 3a8f2d05..5b14e2fe 100644 --- a/src/js/ui.js +++ b/src/js/ui.js @@ -164,17 +164,16 @@ const ui = { } // Load the image, and set poster if successful - const loadPromise = utils.loadImage(poster) - .then(() => { - this.elements.poster.style.backgroundImage = `url('${poster}')`; - Object.assign(this.elements.poster.style, { - backgroundImage: `url('${poster}')`, - // Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube) - backgroundSize: '', - }); - ui.togglePoster.call(this, true); - return poster; + const loadPromise = utils.loadImage(poster).then(() => { + this.elements.poster.style.backgroundImage = `url('${poster}')`; + Object.assign(this.elements.poster.style, { + backgroundImage: `url('${poster}')`, + // Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube) + backgroundSize: '', }); + ui.togglePoster.call(this, true); + return poster; + }); // Hide the element if the poster can't be loaded (otherwise it will just be a black element covering the video) loadPromise.catch(() => ui.togglePoster.call(this, false)); @@ -190,8 +189,10 @@ const ui = { utils.toggleClass(this.elements.container, this.config.classNames.paused, this.paused); utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped); - // Set ARIA state - utils.toggleState(this.elements.buttons.play, this.playing); + // Set state + Array.from(this.elements.buttons.play).forEach(target => { + target.pressed = this.playing; + }); // Only update controls on non timeupdate events if (utils.is.event(event) && event.type === 'timeupdate') { diff --git a/src/js/utils.js b/src/js/utils.js index 0c5a28d7..201c06c8 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -572,28 +572,6 @@ const utils = { element.dispatchEvent(event); }, - // Toggle aria-pressed state on a toggle button - // http://www.ssbbartgroup.com/blog/how-not-to-misuse-aria-states-properties-and-roles - toggleState(element, input) { - // If multiple elements passed - if (utils.is.array(element) || utils.is.nodeList(element)) { - Array.from(element).forEach(target => utils.toggleState(target, input)); - return; - } - - // Bail if no target - if (!utils.is.element(element)) { - return; - } - - // Get state - const pressed = element.getAttribute('aria-pressed') === 'true'; - const state = utils.is.boolean(input) ? input : !pressed; - - // Set the attribute on target - element.setAttribute('aria-pressed', state); - }, - // Format string format(input, ...args) { if (utils.is.empty(input)) { -- cgit v1.2.3 From 69bb0917ad0e2a1ff2c033a0c4ddd2582de8124b Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 28 May 2018 10:41:51 +1000 Subject: v3.3.9 --- src/js/defaults.js | 2 +- src/js/plyr.js | 2 +- src/js/plyr.polyfilled.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src/js') diff --git a/src/js/defaults.js b/src/js/defaults.js index 5b1a4dd3..505520a5 100644 --- a/src/js/defaults.js +++ b/src/js/defaults.js @@ -56,7 +56,7 @@ const defaults = { // Sprite (for icons) loadSprite: true, iconPrefix: 'plyr', - iconUrl: 'https://cdn.plyr.io/3.3.8/plyr.svg', + iconUrl: 'https://cdn.plyr.io/3.3.9/plyr.svg', // Blank video (used to prevent errors on source change) blankVideo: 'https://cdn.plyr.io/static/blank.mp4', diff --git a/src/js/plyr.js b/src/js/plyr.js index 34b618bd..4a064e09 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr -// plyr.js v3.3.8 +// plyr.js v3.3.9 // https://github.com/sampotts/plyr // License: The MIT License (MIT) // ========================================================================== diff --git a/src/js/plyr.polyfilled.js b/src/js/plyr.polyfilled.js index 9570d753..3f45ec40 100644 --- a/src/js/plyr.polyfilled.js +++ b/src/js/plyr.polyfilled.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr Polyfilled Build -// plyr.js v3.3.8 +// plyr.js v3.3.9 // https://github.com/sampotts/plyr // License: The MIT License (MIT) // ========================================================================== -- cgit v1.2.3 From 7aad747c25b07fedf4a4fc75095c560ea3c9899c Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Sun, 27 May 2018 05:08:18 +0200 Subject: Optimize captions code reused and ensure captionsenabled/captionsdisabled will be sent on initial setup --- src/js/captions.js | 64 +++++++++++++++++------------------------------------- src/js/plyr.js | 21 +++++++----------- src/js/ui.js | 6 +++-- 3 files changed, 32 insertions(+), 59 deletions(-) (limited to 'src/js') diff --git a/src/js/captions.js b/src/js/captions.js index df717351..296888b2 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -27,17 +27,6 @@ const captions = { this.captions.language = this.config.captions.language.toLowerCase(); } - // Set captions enabled state if not set - if (!utils.is.boolean(this.captions.active)) { - const active = this.storage.get('captions'); - - if (utils.is.boolean(active)) { - this.captions.active = active; - } else { - this.captions.active = this.config.captions.active; - } - } - // Only Vimeo and HTML5 video supported at this point if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) { // Clear menu and hide @@ -55,17 +44,6 @@ const captions = { utils.insertAfter(this.elements.captions, this.elements.wrapper); } - // Set the class hook - utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(captions.getTracks.call(this))); - - // Get tracks - const tracks = captions.getTracks.call(this); - - // If no caption file exists, hide container for caption text - if (utils.is.empty(tracks)) { - return; - } - // Get browser info const browser = utils.getBrowser(); @@ -94,14 +72,30 @@ const captions = { }); } + // Try to load the value from storage + let active = this.storage.get('captions'); + + // Otherwise fall back to the default config + if (!utils.is.boolean(active)) { + ({ active } = this.config.captions); + } + + // Set toggled state + this.toggleCaptions(active); + + // Update available languages in list + captions.update.call(this); + }, + + update() { // Set language captions.setLanguage.call(this); - // Enable UI - captions.show.call(this); + // Toggle the class hooks + utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(captions.getTracks.call(this))); - // Set available languages in list - if (utils.is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) { + // Update available languages in list + if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) { controls.setCaptionsMenu.call(this); } }, @@ -247,24 +241,6 @@ const captions = { this.debug.warn('No captions element to render to'); } }, - - // Display captions container and button (for initialization) - show() { - // Try to load the value from storage - let active = this.storage.get('captions'); - - // Otherwise fall back to the default config - if (!utils.is.boolean(active)) { - ({ active } = this.config.captions); - } else { - this.captions.active = active; - } - - if (active) { - utils.toggleClass(this.elements.container, this.config.classNames.captions.active, true); - utils.toggleState(this.elements.buttons.captions, true); - } - }, }; export default captions; diff --git a/src/js/plyr.js b/src/js/plyr.js index 34b618bd..061225f8 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -838,24 +838,19 @@ class Plyr { } // If the method is called without parameter, toggle based on current value - const show = utils.is.boolean(input) ? input : !this.elements.container.classList.contains(this.config.classNames.captions.active); - - // Nothing to change... - if (this.captions.active === show) { - return; - } - - // Set global - this.captions.active = show; + const active = utils.is.boolean(input) ? input : !this.elements.container.classList.contains(this.config.classNames.captions.active); // Toggle state - utils.toggleState(this.elements.buttons.captions, this.captions.active); + utils.toggleState(this.elements.buttons.captions, active); // Add class hook - utils.toggleClass(this.elements.container, this.config.classNames.captions.active, this.captions.active); + utils.toggleClass(this.elements.container, this.config.classNames.captions.active, active); - // Trigger an event - utils.dispatchEvent.call(this, this.media, this.captions.active ? 'captionsenabled' : 'captionsdisabled'); + // Update state and trigger event + if (active !== this.captions.active) { + this.captions.active = active; + utils.dispatchEvent.call(this, this.media, this.captions.active ? 'captionsenabled' : 'captionsdisabled'); + } } /** diff --git a/src/js/ui.js b/src/js/ui.js index 3a8f2d05..0d8e532f 100644 --- a/src/js/ui.js +++ b/src/js/ui.js @@ -55,8 +55,10 @@ const ui = { // Remove native controls ui.toggleNativeControls.call(this); - // Captions - captions.setup.call(this); + // Setup captions for html5 + if (this.isHTML5) { + captions.setup.call(this); + } // Reset volume this.volume = null; -- cgit v1.2.3 From 813f703211230024b99f4de95e433e4d33119f9a Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Sun, 27 May 2018 08:10:33 +0200 Subject: Add option to watch caption track changes and update language options --- src/js/captions.js | 5 +++++ src/js/defaults.js | 3 +++ 2 files changed, 8 insertions(+) (limited to 'src/js') diff --git a/src/js/captions.js b/src/js/captions.js index 296888b2..5941ebda 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -83,6 +83,11 @@ const captions = { // Set toggled state this.toggleCaptions(active); + // Watch changes to textTracks and update captions menu + if (this.config.captions.update) { + utils.on(this.media.textTracks, 'change', captions.update.bind(this)); + } + // Update available languages in list captions.update.call(this); }, diff --git a/src/js/defaults.js b/src/js/defaults.js index 5b1a4dd3..dc8785d5 100644 --- a/src/js/defaults.js +++ b/src/js/defaults.js @@ -116,6 +116,9 @@ const defaults = { captions: { active: false, language: (navigator.language || navigator.userLanguage).split('-')[0], + // 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 -- cgit v1.2.3 From 0109454a34d58d5fc0b2828c5106486de6a334db Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 28 May 2018 04:38:08 +0200 Subject: Ensure language is set in case the track is added after initialization, and trigger languagechange event when language is initially set --- src/js/captions.js | 21 ++++++++------------- src/js/controls.js | 5 +---- 2 files changed, 9 insertions(+), 17 deletions(-) (limited to 'src/js') diff --git a/src/js/captions.js b/src/js/captions.js index 5941ebda..f62b2d4f 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -16,17 +16,6 @@ const captions = { return; } - // Set default language if not set - const stored = this.storage.get('language'); - - if (!utils.is.empty(stored)) { - this.captions.language = stored; - } - - if (utils.is.empty(this.captions.language)) { - this.captions.language = this.config.captions.language.toLowerCase(); - } - // Only Vimeo and HTML5 video supported at this point if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) { // Clear menu and hide @@ -93,8 +82,14 @@ const captions = { }, update() { - // Set language - captions.setLanguage.call(this); + // Update tracks + const tracks = captions.getTracks.call(this); + this.options.captions = tracks.map(({language}) => language); + + // Set language if it hasn't been set already + if (!this.language) { + this.language = this.storage.get('language') || (this.config.captions.language || '').toLowerCase(); + } // Toggle the class hooks utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(captions.getTracks.call(this))); diff --git a/src/js/controls.js b/src/js/controls.js index c76bd66b..32e82f78 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -883,13 +883,10 @@ const controls = { 'language', track.label, track.language !== 'enabled' ? controls.createBadge.call(this, track.language.toUpperCase()) : null, - track.language.toLowerCase() === this.captions.language.toLowerCase(), + track.language.toLowerCase() === this.language, ); }); - // Store reference - this.options.captions = tracks.map(track => track.language); - controls.updateSetting.call(this, type, list); }, -- cgit v1.2.3 From c9298fde768d14975a41ce8018fd0f10116943aa Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Sun, 13 May 2018 19:58:23 +0200 Subject: Fix typo --- src/js/ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/js') diff --git a/src/js/ui.js b/src/js/ui.js index 0d8e532f..9a6692dc 100644 --- a/src/js/ui.js +++ b/src/js/ui.js @@ -55,7 +55,7 @@ const ui = { // Remove native controls ui.toggleNativeControls.call(this); - // Setup captions for html5 + // Setup captions for HTML5 if (this.isHTML5) { captions.setup.call(this); } -- cgit v1.2.3 From 812e07b7347bc9d9a212b0204af1d90c92ee0c13 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 28 May 2018 07:43:37 +0200 Subject: Replace browser language detection in defaults.js with explicit 'auto' option --- src/js/captions.js | 6 +++++- src/js/defaults.js | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) (limited to 'src/js') diff --git a/src/js/captions.js b/src/js/captions.js index f62b2d4f..f8083b65 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -88,7 +88,11 @@ const captions = { // Set language if it hasn't been set already if (!this.language) { - this.language = this.storage.get('language') || (this.config.captions.language || '').toLowerCase(); + let { language } = this.config.captions; + if (language === 'auto') { + [ language ] = (navigator.language || navigator.userLanguage).split('-'); + } + this.language = this.storage.get('language') || (language || '').toLowerCase(); } // Toggle the class hooks diff --git a/src/js/defaults.js b/src/js/defaults.js index dc8785d5..977a77e7 100644 --- a/src/js/defaults.js +++ b/src/js/defaults.js @@ -115,7 +115,7 @@ const defaults = { // Captions settings captions: { active: false, - language: (navigator.language || navigator.userLanguage).split('-')[0], + 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, -- cgit v1.2.3 From f58e23b325a3b1d6ac30771d2167f22ea95fe54f Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 28 May 2018 16:19:44 +0200 Subject: Change to using addtrack and removetrack listeners since 'change' didn't trigger in firefox for embedded captions (may also be a hls.js issue) --- src/js/captions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/js') diff --git a/src/js/captions.js b/src/js/captions.js index f8083b65..52fdd8b3 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -74,7 +74,7 @@ const captions = { // Watch changes to textTracks and update captions menu if (this.config.captions.update) { - utils.on(this.media.textTracks, 'change', captions.update.bind(this)); + utils.on(this.media.textTracks, 'addtrack removetrack', captions.update.bind(this)); } // Update available languages in list -- cgit v1.2.3 From 64399e0717cf39f547594dad06097bb429cb9010 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 28 May 2018 17:54:25 +0200 Subject: Defer initial captions update to next tick, to avoid event being triggered to early --- src/js/captions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/js') diff --git a/src/js/captions.js b/src/js/captions.js index 52fdd8b3..30c4bc74 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -77,8 +77,8 @@ const captions = { utils.on(this.media.textTracks, 'addtrack removetrack', captions.update.bind(this)); } - // Update available languages in list - captions.update.call(this); + // Update available languages in list next tick (the event must not be triggered before the listeners) + setTimeout(captions.update.bind(this), 0); }, update() { -- cgit v1.2.3 From 9d798893b56e2ebe9da2d6b87f639f2d2ecee365 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Tue, 29 May 2018 16:06:07 +0200 Subject: Call duration update method manually if user config has duration --- src/js/ui.js | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'src/js') diff --git a/src/js/ui.js b/src/js/ui.js index 3a8f2d05..aa398e3a 100644 --- a/src/js/ui.js +++ b/src/js/ui.js @@ -109,6 +109,12 @@ const ui = { if (this.poster && this.elements.poster && !this.elements.poster.style.backgroundImage) { ui.setPoster.call(this, this.poster); } + + // Manually set the duration if user has overridden it. + // The event listeners for it doesn't get called if preload is disabled (#701) + if (this.config.duration) { + controls.durationUpdate.call(this); + } }, // Setup aria attribute for play and iframe title -- cgit v1.2.3 From 8de06fb862fa89533216c1f9a14737f98651bf36 Mon Sep 17 00:00:00 2001 From: Philip Giuliani Date: Wed, 30 May 2018 13:55:22 +0200 Subject: Accept quality 0 --- src/js/plyr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/js') diff --git a/src/js/plyr.js b/src/js/plyr.js index 557291d9..cfce37c9 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -675,7 +675,7 @@ class Plyr { quality = Number(input); } - if (!utils.is.number(quality) || quality === 0) { + if (!utils.is.number(quality)) { quality = this.storage.get('quality'); } -- cgit v1.2.3 From e0c09c51f292062ca8dae679990465763434adb1 Mon Sep 17 00:00:00 2001 From: Philip Giuliani Date: Wed, 30 May 2018 14:55:29 +0200 Subject: Allow nested translations --- src/js/i18n.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'src/js') diff --git a/src/js/i18n.js b/src/js/i18n.js index 58c3e7cf..d35daacc 100644 --- a/src/js/i18n.js +++ b/src/js/i18n.js @@ -6,11 +6,15 @@ import utils from './utils'; const i18n = { get(key = '', config = {}) { - if (utils.is.empty(key) || utils.is.empty(config) || !Object.keys(config.i18n).includes(key)) { + if (utils.is.empty(key) || utils.is.empty(config)) { return ''; } - let string = config.i18n[key]; + let string = key.split('.').reduce((o,i) => o[i] || {}, config.i18n); + + if (utils.is.empty(string)) { + return ''; + } const replace = { '{seektime}': config.seekTime, -- cgit v1.2.3 From 1c1668bfc344a21e2039527f70034ba4ecb68dfd Mon Sep 17 00:00:00 2001 From: Philip Giuliani Date: Wed, 30 May 2018 14:55:48 +0200 Subject: Implement translation support for qualityName and qualityBadge --- src/js/controls.js | 31 ++++++++----------------------- src/js/defaults.js | 8 ++++++++ 2 files changed, 16 insertions(+), 23 deletions(-) (limited to 'src/js') diff --git a/src/js/controls.js b/src/js/controls.js index 32e82f78..72b752c1 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -664,27 +664,7 @@ const controls = { // Get the badge HTML for HD, 4K etc const getBadge = quality => { - let label = ''; - - switch (quality) { - case 2160: - label = '4K'; - break; - - case 1440: - case 1080: - case 720: - label = 'HD'; - break; - - case 576: - case 480: - label = 'SD'; - break; - - default: - break; - } + const label = i18n.get(`qualityBadge.${quality}`, this.config); if (!label.length) { return null; @@ -708,7 +688,6 @@ const controls = { }, // Translate a value into a nice label - // TODO: Localisation getLabel(setting, value) { switch (setting) { case 'speed': @@ -716,7 +695,13 @@ const controls = { case 'quality': if (utils.is.number(value)) { - return `${value}p`; + const qualityName = i18n.get(`qualityName.${value}`, this.config); + + if (!qualityName.length) { + return `${value}p`; + } + + return qualityName; } return utils.toTitleCase(value); diff --git a/src/js/defaults.js b/src/js/defaults.js index 6a88e73f..a6b83f1f 100644 --- a/src/js/defaults.js +++ b/src/js/defaults.js @@ -190,6 +190,14 @@ const defaults = { disabled: 'Disabled', enabled: 'Enabled', advertisement: 'Ad', + qualityBadge: { + 2160: '4K', + 1440: 'HD', + 1080: 'HD', + 720: 'HD', + 576: 'SD', + 480: 'SD', + }, }, // URLs -- cgit v1.2.3 From 41c7dff0e81ccdad8a6ab80322e0f323eb9ac6c1 Mon Sep 17 00:00:00 2001 From: Philip Giuliani Date: Wed, 30 May 2018 16:59:11 +0200 Subject: Add getDeep method to utils --- src/js/i18n.js | 2 +- src/js/utils.js | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) (limited to 'src/js') diff --git a/src/js/i18n.js b/src/js/i18n.js index d35daacc..62e5bdb0 100644 --- a/src/js/i18n.js +++ b/src/js/i18n.js @@ -10,7 +10,7 @@ const i18n = { return ''; } - let string = key.split('.').reduce((o,i) => o[i] || {}, config.i18n); + let string = utils.getDeep(config.i18n, key); if (utils.is.empty(string)) { return ''; diff --git a/src/js/utils.js b/src/js/utils.js index 0334879d..e04d56e4 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -728,6 +728,11 @@ const utils = { return JSON.parse(JSON.stringify(object)); }, + // Get a nested value in an object + getDeep(object, value) { + return value.split('.').reduce((obj, key) => obj[key] || {}, object); + }, + // Get the closest value in an array closest(array, value) { if (!utils.is.array(array) || !array.length) { -- cgit v1.2.3 From 6435ced70710fd04f3e4a14d9150c529b3ebedc5 Mon Sep 17 00:00:00 2001 From: Philip Giuliani Date: Wed, 30 May 2018 17:10:38 +0200 Subject: Return undefined when the key is not present. --- src/js/utils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/js') diff --git a/src/js/utils.js b/src/js/utils.js index e04d56e4..ea2dc72d 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -729,8 +729,8 @@ const utils = { }, // Get a nested value in an object - getDeep(object, value) { - return value.split('.').reduce((obj, key) => obj[key] || {}, object); + getDeep(object, path) { + return path.split('.').reduce((obj, key) => (obj && obj[key]) || undefined, object); }, // Get the closest value in an array -- cgit v1.2.3 From a8fa125a96def9f7b41796375c11e2e1e753d28f Mon Sep 17 00:00:00 2001 From: Philip Giuliani Date: Wed, 30 May 2018 17:30:10 +0200 Subject: Refactor getDeep method --- src/js/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/js') diff --git a/src/js/utils.js b/src/js/utils.js index ea2dc72d..b6ba0941 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -730,7 +730,7 @@ const utils = { // Get a nested value in an object getDeep(object, path) { - return path.split('.').reduce((obj, key) => (obj && obj[key]) || undefined, object); + return path.split('.').reduce((obj, key) => obj && obj[key], object); }, // Get the closest value in an array -- cgit v1.2.3 From 61f4b998e1b9909b30a676463d7afa696299662e Mon Sep 17 00:00:00 2001 From: Philip Giuliani Date: Thu, 31 May 2018 10:17:21 +0200 Subject: Wait for the metadata to be loaded before setting the currentTime --- src/js/html5.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'src/js') diff --git a/src/js/html5.js b/src/js/html5.js index 3818a441..63596cfc 100644 --- a/src/js/html5.js +++ b/src/js/html5.js @@ -99,6 +99,13 @@ const html5 = { // Set new source player.media.src = supported[0].getAttribute('src'); + // Restore time + const onLoadedMetaData = () => { + player.currentTime = currentTime; + player.off('loadedmetadata', onLoadedMetaData); + }; + player.on('loadedmetadata', onLoadedMetaData); + // Load new source player.media.load(); @@ -107,9 +114,6 @@ const html5 = { player.play(); } - // Restore time - player.currentTime = currentTime; - // Trigger change event utils.dispatchEvent.call(player, player.media, 'qualitychange', false, { quality: input, -- cgit v1.2.3 From 56668f58b6284e63f2874120cf7e1747b868d2b0 Mon Sep 17 00:00:00 2001 From: Philip Giuliani Date: Thu, 31 May 2018 14:40:56 +0200 Subject: Rename qualityName to label --- src/js/controls.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/js') diff --git a/src/js/controls.js b/src/js/controls.js index 72b752c1..20518f9c 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -695,13 +695,13 @@ const controls = { case 'quality': if (utils.is.number(value)) { - const qualityName = i18n.get(`qualityName.${value}`, this.config); + const label = i18n.get(`qualityLabel.${value}`, this.config); - if (!qualityName.length) { + if (!label.length) { return `${value}p`; } - return qualityName; + return label; } return utils.toTitleCase(value); -- cgit v1.2.3 From 108bd3dfa078cf22c0446373830e86cba586919c Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Thu, 31 May 2018 23:33:59 +1000 Subject: Fixed incorrect BEM formatting, fixed buffer alignment --- src/js/defaults.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'src/js') diff --git a/src/js/defaults.js b/src/js/defaults.js index a6b83f1f..8d92219a 100644 --- a/src/js/defaults.js +++ b/src/js/defaults.js @@ -322,9 +322,8 @@ const defaults = { display: { currentTime: '.plyr__time--current', duration: '.plyr__time--duration', - buffer: '.plyr__progress--buffer', - played: '.plyr__progress--played', - loop: '.plyr__progress--loop', + buffer: '.plyr__progress__buffer', + loop: '.plyr__progress__loop', // Used later volume: '.plyr__volume--display', }, progress: '.plyr__progress', -- cgit v1.2.3 From 969a877a34e0cad1bca4bf17e9661ba6e73bcb99 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Thu, 31 May 2018 23:41:48 +1000 Subject: v3.3.10 --- src/js/defaults.js | 2 +- src/js/plyr.js | 2 +- src/js/plyr.polyfilled.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src/js') diff --git a/src/js/defaults.js b/src/js/defaults.js index 505520a5..34e077ee 100644 --- a/src/js/defaults.js +++ b/src/js/defaults.js @@ -56,7 +56,7 @@ const defaults = { // Sprite (for icons) loadSprite: true, iconPrefix: 'plyr', - iconUrl: 'https://cdn.plyr.io/3.3.9/plyr.svg', + iconUrl: 'https://cdn.plyr.io/3.3.10/plyr.svg', // Blank video (used to prevent errors on source change) blankVideo: 'https://cdn.plyr.io/static/blank.mp4', diff --git a/src/js/plyr.js b/src/js/plyr.js index 4a064e09..dcc9fee6 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr -// plyr.js v3.3.9 +// plyr.js v3.3.10 // https://github.com/sampotts/plyr // License: The MIT License (MIT) // ========================================================================== diff --git a/src/js/plyr.polyfilled.js b/src/js/plyr.polyfilled.js index 3f45ec40..f66a82de 100644 --- a/src/js/plyr.polyfilled.js +++ b/src/js/plyr.polyfilled.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr Polyfilled Build -// plyr.js v3.3.9 +// plyr.js v3.3.10 // https://github.com/sampotts/plyr // License: The MIT License (MIT) // ========================================================================== -- cgit v1.2.3 From c95d9923f7996ca7331e6f1f7f232066ec1d002e Mon Sep 17 00:00:00 2001 From: cky <576779975@qq.com> Date: Wed, 6 Jun 2018 16:57:24 +0800 Subject: fix: https://github.com/sampotts/plyr/issues/1006 --- src/js/listeners.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src/js') diff --git a/src/js/listeners.js b/src/js/listeners.js index 86236fe3..45d6097c 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -74,7 +74,10 @@ class Listeners { // and if the focused element is not editable (e.g. text input) // and any that accept key input http://webaim.org/techniques/keyboard/ const focused = utils.getFocusElement(); - if (utils.is.element(focused) && utils.matches(focused, this.player.config.selectors.editable)) { + if (utils.is.element(focused) && ( + focused !== this.player.elements.inputs.seek && + utils.matches(focused, this.player.config.selectors.editable)) + ) { return; } -- cgit v1.2.3 From 84424f7f67461a3da7f3a1ba1ffea6505dddc4dc Mon Sep 17 00:00:00 2001 From: cky <576779975@qq.com> Date: Wed, 6 Jun 2018 19:27:07 +0800 Subject: fix: when the seek input is focused and the video is playing, the space key can't make the video pause, because after 'keyup', it always make the video play --- src/js/listeners.js | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'src/js') diff --git a/src/js/listeners.js b/src/js/listeners.js index 45d6097c..81f5271c 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -563,6 +563,12 @@ class Listeners { on(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => { const seek = event.currentTarget; + const code = event.keyCode ? event.keyCode : event.which; + const eventType = event.type; + + if ((eventType === 'keydown' || eventType === 'keyup') && (code !== 39 && code !== 37)) { + return; + } // Was playing before? const play = seek.hasAttribute('play-on-seeked'); -- cgit v1.2.3 From b12eeb0eb7b59671bb887770fc787940e4659a21 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 4 Jun 2018 14:22:09 +0200 Subject: Merge captions setText and setCue into updateCues (fixes #998 and vimeo cuechange event) --- src/js/captions.js | 81 ++++++++++++++++++++----------------------------- src/js/plugins/vimeo.js | 11 ++----- src/js/plyr.js | 2 +- src/js/utils.js | 7 +++++ 4 files changed, 44 insertions(+), 57 deletions(-) (limited to 'src/js') diff --git a/src/js/captions.js b/src/js/captions.js index 30c4bc74..0baa0820 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -110,23 +110,16 @@ const captions = { if (this.isHTML5 && this.isVideo) { captions.getTracks.call(this).forEach(track => { // Show track - utils.on(track, 'cuechange', event => captions.setCue.call(this, event)); + utils.on(track, 'cuechange', () => captions.updateCues.call(this)); // Turn off native caption rendering to avoid double captions // eslint-disable-next-line track.mode = 'hidden'; }); - // Get current track - const currentTrack = captions.getCurrentTrack.call(this); + // If we change the active track while a cue is already displayed we need to update it + captions.updateCues.call(this); - // Check if suported kind - if (utils.is.track(currentTrack)) { - // If we change the active track while a cue is already displayed we need to update it - if (Array.from(currentTrack.activeCues || []).length) { - captions.setCue.call(this, currentTrack); - } - } } else if (this.isVimeo && this.captions.active) { this.embed.enableTextTrack(this.language); } @@ -193,56 +186,48 @@ const captions = { return i18n.get('disabled', this.config); }, - // Display active caption if it contains text - setCue(input) { - // Get the track from the event if needed - const track = utils.is.event(input) ? input.target : input; - const { activeCues } = track; - const active = activeCues.length && activeCues[0]; - const currentTrack = captions.getCurrentTrack.call(this); - - // Only display current track - if (track !== currentTrack) { + // 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; } - // Display a cue, if there is one - if (utils.is.cue(active)) { - captions.setText.call(this, active.getCueAsHTML()); - } else { - captions.setText.call(this, null); + if (!utils.is.element(this.elements.captions)) { + this.debug.warn('No captions element to render to'); + return; } - utils.dispatchEvent.call(this, this.media, 'cuechange'); - }, - - // Set the current caption - setText(input) { - // Requires UI - if (!this.supported.ui) { + // Only accept array or empty input + if (!utils.is.nullOrUndefined(input) && !Array.isArray(input)) { + this.debug.warn('updateCues: Invalid input', input); return; } - if (utils.is.element(this.elements.captions)) { - const content = utils.createElement('span'); + let cues = input; - // Empty the container - utils.emptyElement(this.elements.captions); + // Get cues from track + if (!cues) { + const track = captions.getCurrentTrack.call(this); + cues = Array.from((track || {}).activeCues || []) + .map(cue => cue.getCueAsHTML()) + .map(utils.getHTML); + } - // Default to empty - const caption = !utils.is.nullOrUndefined(input) ? input : ''; + // Set new caption text + const content = cues.map(cueText => cueText.trim()).join('\n'); + const changed = content !== this.elements.captions.innerHTML; - // Set the span content - if (utils.is.string(caption)) { - content.innerText = caption.trim(); - } else { - content.appendChild(caption); - } + if (changed) { + // Empty the container and create a new child element + utils.emptyElement(this.elements.captions); + const caption = utils.createElement('span'); + caption.innerHTML = content; + this.elements.captions.appendChild(caption); - // Set new caption text - this.elements.captions.appendChild(content); - } else { - this.debug.warn('No captions element to render to'); + // Trigger event + utils.dispatchEvent.call(this, this.media, 'cuechange'); } }, }; diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js index 46d4f3f9..190dd88c 100644 --- a/src/js/plugins/vimeo.js +++ b/src/js/plugins/vimeo.js @@ -301,14 +301,9 @@ const vimeo = { captions.setup.call(player); }); - player.embed.on('cuechange', data => { - let cue = null; - - if (data.cues.length) { - cue = utils.stripHTML(data.cues[0].text); - } - - captions.setText.call(player, cue); + player.embed.on('cuechange', ({ cues = [] }) => { + const strippedCues = cues.map(cue => utils.stripHTML(cue.text)); + captions.updateCues.call(player, strippedCues); }); player.embed.on('loaded', () => { diff --git a/src/js/plyr.js b/src/js/plyr.js index 5c51d617..caa9b55b 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -895,7 +895,7 @@ class Plyr { this.captions.language = language; // Clear caption - captions.setText.call(this, null); + captions.updateCues.call(this, []); // Update captions captions.setLanguage.call(this); diff --git a/src/js/utils.js b/src/js/utils.js index b6ba0941..a152e121 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -833,6 +833,13 @@ const utils = { return fragment.firstChild.innerText; }, + // Like outerHTML, but also works for DocumentFragment + getHTML(element) { + const wrapper = document.createElement('div'); + wrapper.appendChild(element); + return wrapper.innerHTML; + }, + // Get aspect ratio for dimensions getAspectRatio(width, height) { const getRatio = (w, h) => (h === 0 ? w : getRatio(h, w % h)); -- cgit v1.2.3 From 0c03accd417e31ad34f81b12f1e9febec6e6fc48 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sat, 9 Jun 2018 12:04:53 +1000 Subject: Fix Sprite issue --- src/js/utils.js | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) (limited to 'src/js') diff --git a/src/js/utils.js b/src/js/utils.js index b6ba0941..a7d2ada4 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -151,24 +151,23 @@ const utils = { return; } - const prefix = 'cache-'; + const prefix = 'cache'; const hasId = utils.is.string(id); let isCached = false; - const exists = () => document.querySelectorAll(`#${id}`).length; + const exists = () => document.getElementById(id) !== null; + + const update = (container, data) => { + container.innerHTML = data; - function injectSprite(data) { // Check again incase of race condition if (hasId && exists()) { return; } - // Inject content - this.innerHTML = data; - // Inject the SVG to the body - document.body.insertBefore(this, document.body.childNodes[0]); - } + document.body.insertAdjacentElement('afterbegin', container); + }; // Only load once if ID set if (!hasId || !exists()) { @@ -184,13 +183,12 @@ const utils = { // Check in cache if (useStorage) { - const cached = window.localStorage.getItem(prefix + id); + const cached = window.localStorage.getItem(`${prefix}-${id}`); isCached = cached !== null; if (isCached) { const data = JSON.parse(cached); - injectSprite.call(container, data.content); - return; + update(container, data.content); } } @@ -204,14 +202,14 @@ const utils = { if (useStorage) { window.localStorage.setItem( - prefix + id, + `${prefix}-${id}`, JSON.stringify({ content: result, }), ); } - injectSprite.call(container, result); + update(container, result); }) .catch(() => {}); } -- cgit v1.2.3 From a80b31bf98ca14279128ecbf32b13ba748e438fc Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Fri, 8 Jun 2018 13:31:29 +0200 Subject: Fix #1003: Formatted captions issue --- src/js/captions.js | 2 +- src/js/defaults.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'src/js') diff --git a/src/js/captions.js b/src/js/captions.js index 0baa0820..b3723885 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -222,7 +222,7 @@ const captions = { if (changed) { // Empty the container and create a new child element utils.emptyElement(this.elements.captions); - const caption = utils.createElement('span'); + const caption = utils.createElement('span', utils.getAttributesFromSelector(this.config.selectors.caption)); caption.innerHTML = content; this.elements.captions.appendChild(caption); diff --git a/src/js/defaults.js b/src/js/defaults.js index 16df8624..b1bdaa65 100644 --- a/src/js/defaults.js +++ b/src/js/defaults.js @@ -328,6 +328,7 @@ const defaults = { }, progress: '.plyr__progress', captions: '.plyr__captions', + caption: '.plyr__caption', menu: { quality: '.js-plyr__menu__list--quality', }, -- cgit v1.2.3 From b57784d1a546e133779fa7918e928dbf4e30ce68 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Fri, 8 Jun 2018 13:45:20 +0200 Subject: Change debug warn 'Unsupported language option' to log 'Language option doesn't yet exist' since it doesn't have to be an error --- src/js/plyr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/js') diff --git a/src/js/plyr.js b/src/js/plyr.js index caa9b55b..6387fd6c 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -874,7 +874,7 @@ class Plyr { // Check for support if (!this.options.captions.includes(language)) { - this.debug.warn(`Unsupported language option: ${language}`); + this.debug.log(`Language option: ${language} doesn't yet exist`); return; } -- cgit v1.2.3 From 9dc0f28800fd17eef442f868bd12c3017400a992 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Fri, 8 Jun 2018 21:16:17 +0200 Subject: Avoid condition in getTracks --- src/js/captions.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) (limited to 'src/js') diff --git a/src/js/captions.js b/src/js/captions.js index b3723885..38167d7a 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -127,13 +127,10 @@ const captions = { // Get the tracks getTracks() { - // Return empty array at least - if (utils.is.nullOrUndefined(this.media)) { - return []; - } - - // Only get accepted kinds - return Array.from(this.media.textTracks || []).filter(track => [ + // Handle media or textTracks missing or null + const { textTracks } = this.media || {}; + // Filter out invalid tracks kinds (like metadata) + return Array.from(textTracks || []).filter(track => [ 'captions', 'subtitles', ].includes(track.kind)); -- cgit v1.2.3 From 1fab4919c01347a29e11cbd78fedcddfabd1b814 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Sat, 9 Jun 2018 14:14:34 +0200 Subject: controls.createMenuItem: Change input to object (too many params made it hard to read) --- src/js/controls.js | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) (limited to 'src/js') diff --git a/src/js/controls.js b/src/js/controls.js index 20518f9c..e9529e4e 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -376,7 +376,7 @@ const controls = { }, // Create a settings menu item - createMenuItem(value, list, type, title, badge = null, checked = false) { + createMenuItem({value, list, type, title, badge = null, checked = false}) { const item = utils.createElement('li'); const label = utils.createElement('label', { @@ -680,8 +680,13 @@ const controls = { return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1; }) .forEach(quality => { - const label = controls.getLabel.call(this, 'quality', quality); - controls.createMenuItem.call(this, quality, list, type, label, getBadge(quality)); + controls.createMenuItem.call(this, { + value: quality, + list, + type, + title: controls.getLabel.call(this, 'quality', quality), + badge: getBadge(quality), + }); }); controls.updateSetting.call(this, type, list); @@ -861,15 +866,14 @@ const controls = { // Generate options tracks.forEach(track => { - controls.createMenuItem.call( - this, - track.language, + controls.createMenuItem.call(this, { + value: track.language, list, - 'language', - track.label, - track.language !== 'enabled' ? controls.createBadge.call(this, track.language.toUpperCase()) : null, - track.language.toLowerCase() === this.language, - ); + type: 'language', + title: track.label, + badge: track.language !== 'enabled' ? controls.createBadge.call(this, track.language.toUpperCase()) : null, + checked: track.language.toLowerCase() === this.language, + }); }); controls.updateSetting.call(this, type, list); @@ -927,8 +931,12 @@ const controls = { // Create items this.options.speed.forEach(speed => { - const label = controls.getLabel.call(this, 'speed', speed); - controls.createMenuItem.call(this, speed, list, type, label); + controls.createMenuItem.call(this, { + value: speed, + list, + type, + title: controls.getLabel.call(this, 'speed', speed), + }); }); controls.updateSetting.call(this, type, list); -- cgit v1.2.3 From c83487a293d61d3a1add31018918e9c831bbcac2 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Fri, 8 Jun 2018 14:49:35 +0200 Subject: Fix #1017, fix #980, fix #1014: Captions rewrite (use index internally) --- src/js/captions.js | 170 +++++++++++++++++++++++++++++++++++----------------- src/js/controls.js | 49 ++++++--------- src/js/listeners.js | 2 +- src/js/plyr.js | 72 +++++++--------------- 4 files changed, 157 insertions(+), 136 deletions(-) (limited to 'src/js') diff --git a/src/js/captions.js b/src/js/captions.js index 38167d7a..bafcf87e 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -69,12 +69,18 @@ const captions = { ({ active } = this.config.captions); } - // Set toggled state - this.toggleCaptions(active); + // Get language from storage, fallback to config + let language = this.storage.get('language') || this.config.captions.language; + if (language === 'auto') { + [ language ] = (navigator.language || navigator.userLanguage).split('-'); + } + // Set language and show if active + captions.setLanguage.call(this, language, active); // Watch changes to textTracks and update captions menu - if (this.config.captions.update) { - utils.on(this.media.textTracks, 'addtrack removetrack', captions.update.bind(this)); + if (this.isHTML5) { + const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack'; + utils.on(this.media.textTracks, trackEvents, captions.update.bind(this)); } // Update available languages in list next tick (the event must not be triggered before the listeners) @@ -82,21 +88,39 @@ const captions = { }, update() { - // Update tracks - const tracks = captions.getTracks.call(this); - this.options.captions = tracks.map(({language}) => language); + const tracks = captions.getTracks.call(this, true); + // Get the wanted language + const { language, meta } = this.captions; - // Set language if it hasn't been set already - if (!this.language) { - let { language } = this.config.captions; - if (language === 'auto') { - [ language ] = (navigator.language || navigator.userLanguage).split('-'); - } - this.language = this.storage.get('language') || (language || '').toLowerCase(); + // 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 + track.mode = 'hidden'; + + // Add event listener for cue changes + utils.on(track, 'cuechange', () => captions.updateCues.call(this)); + }); } - // Toggle the class hooks - utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(captions.getTracks.call(this))); + const trackRemoved = !tracks.find(track => track === this.captions.currentTrackNode); + const firstMatch = this.language !== language && tracks.find(track => track.language === language); + + // Update language if removed or first matching track added + if (trackRemoved || firstMatch) { + captions.setLanguage.call(this, language, this.config.captions.active); + } + + // Enable or disable captions based on track length + utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(tracks)); // Update available languages in list if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) { @@ -104,60 +128,94 @@ const captions = { } }, - // Set the captions language - setLanguage() { - // Setup HTML5 track rendering - if (this.isHTML5 && this.isVideo) { - captions.getTracks.call(this).forEach(track => { - // Show track - utils.on(track, 'cuechange', () => captions.updateCues.call(this)); + set(index, setLanguage = true, show = true) { + const tracks = captions.getTracks.call(this); - // Turn off native caption rendering to avoid double captions - // eslint-disable-next-line - track.mode = 'hidden'; - }); + // Disable captions if setting to -1 + if (index === -1) { + this.toggleCaptions(false); + return; + } - // If we change the active track while a cue is already displayed we need to update it - captions.updateCues.call(this); + if (!utils.is.number(index)) { + this.debug.warn('Invalid caption argument', index); + return; + } - } else if (this.isVimeo && this.captions.active) { - this.embed.enableTextTrack(this.language); + if (!(index in tracks)) { + this.debug.warn('Track not found', index); + return; } - }, - // Get the tracks - getTracks() { - // Handle media or textTracks missing or null - const { textTracks } = this.media || {}; - // Filter out invalid tracks kinds (like metadata) - return Array.from(textTracks || []).filter(track => [ - 'captions', - 'subtitles', - ].includes(track.kind)); - }, + if (this.captions.currentTrack !== index) { + this.captions.currentTrack = index; + const track = captions.getCurrentTrack.call(this); + const { language } = track || {}; - // Get the current track for the current language - getCurrentTrack() { - const tracks = captions.getTracks.call(this); + // Store reference to node for invalidation on remove + this.captions.currentTrackNode = track; + + // Prevent setting language in some cases, since it can violate user's intentions + if (setLanguage) { + this.captions.language = language; + } + + // Handle Vimeo captions + if (this.isVimeo) { + this.embed.enableTextTrack(language); + } - if (!tracks.length) { - return null; + // Trigger event + utils.dispatchEvent.call(this, this.media, 'languagechange'); } - // Get track based on current language - let track = tracks.find(track => track.language.toLowerCase() === this.language); + 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); + } - // Get the with default attribute - if (!track) { - track = utils.getElement.call(this, 'track[default]'); + // Show captions + if (show) { + this.toggleCaptions(true); } + }, - // Get the first track - if (!track) { - [track] = tracks; + setLanguage(language, show = true) { + if (!utils.is.string(language)) { + this.debug.warn('Invalid language argument', language); + return; } + // Normalize + this.captions.language = language.toLowerCase(); + + // Set currentTrack + const tracks = captions.getTracks.call(this); + const track = captions.getCurrentTrack.call(this, true); + captions.set.call(this, tracks.indexOf(track), false, show); + }, - return track; + // 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)); + }, + + // Get the current track for the current language + getCurrentTrack(fromLanguage = 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)); + return (!fromLanguage && tracks[this.currentTrack]) || sorted.find(track => track.language === this.captions.language) || sorted[0]; }, // Get UI label for track diff --git a/src/js/controls.js b/src/js/controls.js index e9529e4e..058e636f 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -727,16 +727,7 @@ const controls = { switch (setting) { case 'captions': - if (this.captions.active) { - if (this.options.captions.length > 2 || !this.options.captions.some(lang => lang === 'enabled')) { - value = this.captions.language; - } else { - value = 'enabled'; - } - } else { - value = ''; - } - + value = this.currentTrack; break; default: @@ -836,10 +827,10 @@ const controls = { // TODO: Captions or language? Currently it's mixed const type = 'captions'; const list = this.elements.settings.panes.captions.querySelector('ul'); + const tracks = captions.getTracks.call(this); // Toggle the pane and tab - const toggle = captions.getTracks.call(this).length; - controls.toggleTab.call(this, type, toggle); + controls.toggleTab.call(this, type, tracks.length); // Empty the menu utils.emptyElement(list); @@ -848,33 +839,31 @@ const controls = { controls.checkMenu.call(this); // If there's no captions, bail - if (!toggle) { + if (!tracks.length) { return; } - // Re-map the tracks into just the data we need - const tracks = captions.getTracks.call(this).map(track => ({ - language: !utils.is.empty(track.language) ? track.language : 'enabled', - label: captions.getLabel.call(this, track), + // Generate options data + const options = tracks.map((track, value) => ({ + value, + checked: this.captions.active && this.currentTrack === value, + title: captions.getLabel.call(this, track), + badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()), + list, + type: 'language', })); // Add the "Disabled" option to turn off captions - tracks.unshift({ - language: '', - label: i18n.get('disabled', this.config), + options.unshift({ + value: -1, + checked: !this.captions.active, + title: i18n.get('disabled', this.config), + list, + type: 'language', }); // Generate options - tracks.forEach(track => { - controls.createMenuItem.call(this, { - value: track.language, - list, - type: 'language', - title: track.label, - badge: track.language !== 'enabled' ? controls.createBadge.call(this, track.language.toUpperCase()) : null, - checked: track.language.toLowerCase() === this.language, - }); - }); + options.forEach(controls.createMenuItem.bind(this)); controls.updateSetting.call(this, type, list); }, diff --git a/src/js/listeners.js b/src/js/listeners.js index 81f5271c..72b60cc0 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -523,7 +523,7 @@ class Listeners { proxy( event, () => { - this.player.language = event.target.value; + this.player.currentTrack = Number(event.target.value); showHomeTab(); }, 'language', diff --git a/src/js/plyr.js b/src/js/plyr.js index 6387fd6c..b6f355ac 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -84,7 +84,8 @@ class Plyr { // Captions this.captions = { active: null, - currentTrack: null, + currentTrack: -1, + meta: new WeakMap(), }; // Fullscreen @@ -96,7 +97,6 @@ class Plyr { this.options = { speed: [], quality: [], - captions: [], }; // Debugging @@ -854,61 +854,35 @@ class Plyr { } /** - * Set the captions language - * @param {string} - Two character ISO language code (e.g. EN, FR, PT, etc) + * Set the caption track by index + * @param {number} - Caption index */ - set language(input) { - // Nothing specified - if (!utils.is.string(input)) { - return; - } - - // If empty string is passed, assume disable captions - if (utils.is.empty(input)) { - this.toggleCaptions(false); - return; - } - - // Normalize - const language = input.toLowerCase(); - - // Check for support - if (!this.options.captions.includes(language)) { - this.debug.log(`Language option: ${language} doesn't yet exist`); - return; - } - - // Ensure captions are enabled - this.toggleCaptions(true); - - // Enabled only - if (language === 'enabled') { - return; - } - - // If nothing to change, bail - if (this.language === language) { - return; - } - - // Update config - this.captions.language = language; - - // Clear caption - captions.updateCues.call(this, []); + set currentTrack(input) { + captions.set.call(this, input); + } - // Update captions - captions.setLanguage.call(this); + /** + * Get the current caption track index (-1 if disabled) + */ + get currentTrack() { + const { active, currentTrack } = this.captions; + return active ? currentTrack : -1; + } - // Trigger an event - utils.dispatchEvent.call(this, this.media, 'languagechange'); + /** + * Set the wanted language for captions + * Since tracks can be added later it won't update the actual caption track until there is a matching track + * @param {string} - Two character ISO language code (e.g. EN, FR, PT, etc) + */ + set language(input) { + captions.setLanguage.call(this, input); } /** - * Get the current captions language + * Get the current track's language */ get language() { - return this.captions.language; + return (captions.getCurrentTrack.call(this) || {}).language; } /** -- cgit v1.2.3 From d3e98eb27ef58b4a9e44c9aaecf4b420868a280c Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Sun, 10 Jun 2018 23:59:59 +0200 Subject: Vimeo: Assure state is updated with autoplay (fixes #1016) --- src/js/plugins/vimeo.js | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'src/js') diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js index 46d4f3f9..66ba97b7 100644 --- a/src/js/plugins/vimeo.js +++ b/src/js/plugins/vimeo.js @@ -312,6 +312,14 @@ const vimeo = { }); player.embed.on('loaded', () => { + // Assure state and events are updated on autoplay + player.embed.getPaused().then(paused => { + assurePlaybackState.call(player, !paused); + if (!paused) { + utils.dispatchEvent.call(player, player.media, 'playing'); + } + }); + if (utils.is.element(player.embed.element) && player.supported.ui) { const frame = player.embed.element; -- cgit v1.2.3 From 94699f325558e7e77d63010416e6c554ae87fef6 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 11 Jun 2018 02:05:26 +0200 Subject: Fix problem with YouTube and Vimeo seeking while playing --- src/js/plugins/vimeo.js | 10 +++++++--- src/js/plugins/youtube.js | 5 ++++- 2 files changed, 11 insertions(+), 4 deletions(-) (limited to 'src/js') diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js index 46d4f3f9..c7548e3d 100644 --- a/src/js/plugins/vimeo.js +++ b/src/js/plugins/vimeo.js @@ -9,6 +9,9 @@ import utils from './../utils'; // 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; utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause'); @@ -153,19 +156,20 @@ const vimeo = { // 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; utils.dispatchEvent.call(player, media, 'seeking'); // If paused, mute until seek is complete - Promise.resolve(paused && embed.setVolume(0)) + Promise.resolve(restorePause && embed.setVolume(0)) // Seek .then(() => embed.setCurrentTime(time)) // Restore paused - .then(() => paused && embed.pause()) + .then(() => restorePause && embed.pause()) // Restore volume - .then(() => paused && embed.setVolume(volume)) + .then(() => restorePause && embed.setVolume(volume)) .catch(() => { // Do nothing }); diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index 67b8093e..9b067c8a 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -66,6 +66,9 @@ function mapQualityUnits(levels) { // 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; utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause'); @@ -469,7 +472,7 @@ const youtube = { case 1: // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet) - if (player.media.paused) { + if (player.media.paused && !player.embed.hasPlayed) { player.media.pause(); } else { assurePlaybackState.call(player, true); -- cgit v1.2.3 From 16828e975a9a627fb60b0b7f266fe870521a3e08 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 11 Jun 2018 05:12:34 +0200 Subject: Move utils.is.getConstructor() to utils.getConstructor() --- src/js/utils.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) (limited to 'src/js') diff --git a/src/js/utils.js b/src/js/utils.js index 109de031..54635739 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -11,19 +11,19 @@ const utils = { // Check variable types is: { object(input) { - return this.getConstructor(input) === Object; + return utils.getConstructor(input) === Object; }, number(input) { - return this.getConstructor(input) === Number && !Number.isNaN(input); + return utils.getConstructor(input) === Number && !Number.isNaN(input); }, string(input) { - return this.getConstructor(input) === String; + return utils.getConstructor(input) === String; }, boolean(input) { - return this.getConstructor(input) === Boolean; + return utils.getConstructor(input) === Boolean; }, function(input) { - return this.getConstructor(input) === Function; + return utils.getConstructor(input) === Function; }, array(input) { return !this.nullOrUndefined(input) && Array.isArray(input); @@ -38,7 +38,7 @@ const utils = { return this.instanceof(input, Element); }, textNode(input) { - return this.getConstructor(input) === Text; + return utils.getConstructor(input) === Text; }, event(input) { return this.instanceof(input, Event); @@ -65,9 +65,10 @@ const utils = { instanceof(input, constructor) { return Boolean(input && constructor && input instanceof constructor); }, - getConstructor(input) { - return !this.nullOrUndefined(input) ? input.constructor : null; - }, + }, + + getConstructor(input) { + return !utils.is.nullOrUndefined(input) ? input.constructor : null; }, // Unfortunately, due to mixed support, UA sniffing is required -- cgit v1.2.3 From b148adc0aff87e12e50c85fd25c40b80de570505 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 11 Jun 2018 05:19:39 +0200 Subject: Avoid using this to refer to utils or utils.is, since that means methods can't be used statically --- src/js/utils.js | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) (limited to 'src/js') diff --git a/src/js/utils.js b/src/js/utils.js index 54635739..c36763dd 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -26,40 +26,40 @@ const utils = { return utils.getConstructor(input) === Function; }, array(input) { - return !this.nullOrUndefined(input) && Array.isArray(input); + return !utils.is.nullOrUndefined(input) && Array.isArray(input); }, weakMap(input) { - return this.instanceof(input, WeakMap); + return utils.is.instanceof(input, WeakMap); }, nodeList(input) { - return this.instanceof(input, NodeList); + return utils.is.instanceof(input, NodeList); }, element(input) { - return this.instanceof(input, Element); + return utils.is.instanceof(input, Element); }, textNode(input) { return utils.getConstructor(input) === Text; }, event(input) { - return this.instanceof(input, Event); + return utils.is.instanceof(input, Event); }, cue(input) { - return this.instanceof(input, window.TextTrackCue) || this.instanceof(input, window.VTTCue); + return utils.is.instanceof(input, window.TextTrackCue) || utils.is.instanceof(input, window.VTTCue); }, track(input) { - return this.instanceof(input, TextTrack) || (!this.nullOrUndefined(input) && this.string(input.kind)); + return utils.is.instanceof(input, TextTrack) || (!utils.is.nullOrUndefined(input) && utils.is.string(input.kind)); }, url(input) { - return !this.nullOrUndefined(input) && /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test(input); + return !utils.is.nullOrUndefined(input) && /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test(input); }, nullOrUndefined(input) { return input === null || typeof input === 'undefined'; }, empty(input) { return ( - this.nullOrUndefined(input) || - ((this.string(input) || this.array(input) || this.nodeList(input)) && !input.length) || - (this.object(input) && !Object.keys(input).length) + utils.is.nullOrUndefined(input) || + ((utils.is.string(input) || utils.is.array(input) || utils.is.nodeList(input)) && !input.length) || + (utils.is.object(input) && !Object.keys(input).length) ); }, instanceof(input, constructor) { @@ -626,16 +626,16 @@ const utils = { formatTime(time = 0, displayHours = false, inverted = false) { // Bail if the value isn't a number if (!utils.is.number(time)) { - return this.formatTime(null, displayHours, inverted); + return utils.formatTime(null, displayHours, inverted); } // Format time component to add leading zero const format = value => `0${value}`.slice(-2); // Breakdown to hours, mins, secs - let hours = this.getHours(time); - const mins = this.getMinutes(time); - const secs = this.getSeconds(time); + let hours = utils.getHours(time); + const mins = utils.getMinutes(time); + const secs = utils.getSeconds(time); // Do we need to display hours? if (displayHours || hours > 0) { @@ -793,10 +793,10 @@ const utils = { // Parse URL if needed if (input.startsWith('http://') || input.startsWith('https://')) { - ({ search } = this.parseUrl(input)); + ({ search } = utils.parseUrl(input)); } - if (this.is.empty(search)) { + if (utils.is.empty(search)) { return null; } -- cgit v1.2.3 From 37a3ab202ac3a3f980a7e5aed7a97a22a1aac5cb Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 11 Jun 2018 05:42:11 +0200 Subject: Remove wrapper function around utils.is.element in Plyr.setup() (no lnger needed) --- src/js/plyr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/js') diff --git a/src/js/plyr.js b/src/js/plyr.js index b6f355ac..181eff9e 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -1133,7 +1133,7 @@ class Plyr { } else if (utils.is.nodeList(selector)) { targets = Array.from(selector); } else if (utils.is.array(selector)) { - targets = selector.filter(i => utils.is.element(i)); + targets = selector.filter(utils.is.element); } if (utils.is.empty(targets)) { -- cgit v1.2.3 From cc97d7be6a127076f4870235e4f8174021656d41 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 11 Jun 2018 08:00:46 +0200 Subject: Fix synthetic event bubble/proxy loses detail --- src/js/listeners.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/js') diff --git a/src/js/listeners.js b/src/js/listeners.js index 72b60cc0..c391ea4c 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -414,7 +414,7 @@ class Listeners { 'keyup', 'keydown', ]).join(' '), event => { - let detail = {}; + let {detail = {}} = event; // Get error details from media if (event.type === 'error') { -- cgit v1.2.3 From 38f10d4cc67b3109189699f7e65189a852064236 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 11 Jun 2018 16:19:11 +1000 Subject: WIP --- src/js/controls.js | 80 ++++++++++++++++++++++++++++++++++++++---------------- src/js/defaults.js | 2 ++ src/js/plyr.js | 5 +--- src/js/ui.js | 3 -- 4 files changed, 59 insertions(+), 31 deletions(-) (limited to 'src/js') diff --git a/src/js/controls.js b/src/js/controls.js index e2b4ed1a..f39101af 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -12,8 +12,6 @@ import utils from './utils'; const browser = utils.getBrowser(); const controls = { - - // Get icon URL getIconUrl() { const url = new URL(this.config.iconUrl, window.location); @@ -359,10 +357,14 @@ const controls = { createTime(type) { const attributes = utils.getAttributesFromSelector(this.config.selectors.display[type]); - const container = utils.createElement('div', utils.extend(attributes, { - class: `plyr__time ${attributes.class}`, - 'aria-label': i18n.get(type, this.config), - }), '00:00'); + const container = utils.createElement( + 'div', + utils.extend(attributes, { + class: `plyr__time ${attributes.class}`, + 'aria-label': i18n.get(type, this.config), + }), + '00:00', + ); // Reference for updates this.elements.display[type] = container; @@ -403,6 +405,19 @@ const controls = { list.appendChild(item); }, + // Format a time for display + formatTime(time = 0, inverted = false) { + // Bail if the value isn't a number + if (!utils.is.number(time)) { + return time; + } + + // Always display hours if duration is over an hour + const forceHours = utils.getHours(this.duration) > 0; + + return utils.formatTime(time, forceHours, inverted); + }, + // Update the displayed time updateTimeDisplay(target = null, time = 0, inverted = false) { // Bail if there's no element to display or the value isn't a number @@ -410,11 +425,8 @@ const controls = { return; } - // Always display hours if duration is over an hour - const forceHours = utils.getHours(this.duration) > 0; - // eslint-disable-next-line no-param-reassign - target.innerText = utils.formatTime(time, forceHours, inverted); + target.innerText = controls.formatTime(time, inverted); }, // Update volume UI and storage @@ -509,8 +521,20 @@ const controls = { return; } - // Set aria value for https://github.com/sampotts/plyr/issues/905 - range.setAttribute('aria-valuenow', range.value); + // Set aria values for https://github.com/sampotts/plyr/issues/905 + if (utils.matches(range, this.config.selectors.inputs.seek)) { + range.setAttribute('aria-valuenow', this.currentTime); + const currentTime = controls.formatTime(this.currentTime); + const duration = controls.formatTime(this.duration); + const format = i18n.get('seekLabel', this.config); + range.setAttribute('aria-valuetext', format.replace('{currentTime}', currentTime).replace('{duration}', duration)); + } else if (utils.matches(range, this.config.selectors.inputs.volume)) { + const percent = range.value * 100; + range.setAttribute('aria-valuenow', percent); + range.setAttribute('aria-valuetext', `${percent}%`); + } else { + range.setAttribute('aria-valuenow', range.value); + } // WebKit only if (!browser.isWebkit) { @@ -599,11 +623,16 @@ const controls = { // Show the duration on metadataloaded or durationchange events durationUpdate() { - // Bail if no ui or durationchange event triggered after playing/seek when invertTime is false + // Bail if no UI or durationchange event triggered after playing/seek when invertTime is false if (!this.supported.ui || (!this.config.invertTime && this.currentTime)) { return; } + // Update ARIA values + if (utils.is.element(this.elements.inputs.seek)) { + this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration); + } + // If there's a spot to display duration const hasDuration = utils.is.element(this.elements.display.duration); @@ -1126,9 +1155,11 @@ const controls = { const progress = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.progress)); // Seek range slider - progress.appendChild(controls.createRange.call(this, 'seek', { - id: `plyr-seek-${data.id}`, - })); + progress.appendChild( + controls.createRange.call(this, 'seek', { + id: `plyr-seek-${data.id}`, + }), + ); // Buffer progress progress.appendChild(controls.createProgress.call(this, 'buffer')); @@ -1182,13 +1213,15 @@ const controls = { }; // Create the volume range slider - volume.appendChild(controls.createRange.call( - this, - 'volume', - utils.extend(attributes, { - id: `plyr-volume-${data.id}`, - }), - )); + volume.appendChild( + controls.createRange.call( + this, + 'volume', + utils.extend(attributes, { + id: `plyr-volume-${data.id}`, + }), + ), + ); this.elements.volume = volume; @@ -1463,7 +1496,6 @@ const controls = { Array.from(labels).forEach(label => { utils.toggleClass(label, this.config.classNames.hidden, false); utils.toggleClass(label, this.config.classNames.tooltip, true); - label.setAttribute('role', 'tooltip'); }); } }, diff --git a/src/js/defaults.js b/src/js/defaults.js index 78371d68..9026ab18 100644 --- a/src/js/defaults.js +++ b/src/js/defaults.js @@ -169,6 +169,7 @@ const defaults = { pause: 'Pause', fastForward: 'Forward {seektime}s', seek: 'Seek', + seekLabel: '{currentTime} of {duration}', played: 'Played', buffered: 'Buffered', currentTime: 'Current time', @@ -183,6 +184,7 @@ const defaults = { frameTitle: 'Player for {title}', captions: 'Captions', settings: 'Settings', + menuBack: 'Go back to previous menu', speed: 'Speed', normal: 'Normal', quality: 'Quality', diff --git a/src/js/plyr.js b/src/js/plyr.js index ce3d3be5..65f24239 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -260,9 +260,6 @@ class Plyr { utils.wrap(this.media, this.elements.container); } - // Allow focus to be captured - this.elements.container.setAttribute('tabindex', 0); - // Add style hook ui.addStyleHook.call(this); @@ -849,7 +846,7 @@ class Plyr { // Update state and trigger event if (active !== this.captions.active) { this.captions.active = active; - utils.dispatchEvent.call(this, this.media, this.captions.active ? 'captionsenabled' : 'captionsdisabled'); + utils.dispatchEvent.call(this, this.media, this.captions.active ? 'captionsenabled' : 'captionsdisabled'); } } diff --git a/src/js/ui.js b/src/js/ui.js index e90a1492..979d8341 100644 --- a/src/js/ui.js +++ b/src/js/ui.js @@ -127,9 +127,6 @@ const ui = { // If there's a media title set, use that for the label if (utils.is.string(this.config.title) && !utils.is.empty(this.config.title)) { label += `, ${this.config.title}`; - - // Set container label - this.elements.container.setAttribute('aria-label', this.config.title); } // If there's a play button, set label -- cgit v1.2.3 From 4c1337b4c5e86e22c47dac1d74e3b3298bbc01cb Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 11 Jun 2018 03:21:18 +0200 Subject: Assure type safety in getSources() and getQualityOptions() (always return arrays), and remove external conditions and type conversion no longer needed --- src/js/html5.js | 53 +++++++++++------------------------------------------ 1 file changed, 11 insertions(+), 42 deletions(-) (limited to 'src/js') diff --git a/src/js/html5.js b/src/js/html5.js index 63596cfc..a7ff0bd9 100644 --- a/src/js/html5.js +++ b/src/js/html5.js @@ -8,35 +8,21 @@ import utils from './utils'; const html5 = { getSources() { if (!this.isHTML5) { - return null; + return []; } - return this.media.querySelectorAll('source'); + return Array.from(this.media.querySelectorAll('source')); }, // Get quality levels getQualityOptions() { - if (!this.isHTML5) { - return null; - } - - // Get sources - const sources = html5.getSources.call(this); - - if (utils.is.empty(sources)) { - return null; - } - - // Get with size attribute - const sizes = Array.from(sources).filter(source => !utils.is.empty(source.getAttribute('size'))); - - // If none, bail - if (utils.is.empty(sizes)) { - return null; - } + // Get sizes from elements + const sizes = html5.getSources.call(this) + .map(source => Number(source.getAttribute('size'))) + .filter(Boolean); // Reduce to unique list - return utils.dedupe(sizes.map(source => Number(source.getAttribute('size')))); + return utils.dedupe(sizes); }, extend() { @@ -51,34 +37,17 @@ const html5 = { get() { // Get sources const sources = html5.getSources.call(player); + const [source] = sources.filter(source => source.getAttribute('src') === player.source); - if (utils.is.empty(sources)) { - return null; - } - - const matches = Array.from(sources).filter(source => source.getAttribute('src') === player.source); - - if (utils.is.empty(matches)) { - return null; - } - - return Number(matches[0].getAttribute('size')); + // Return size, if match is found + return source && Number(source.getAttribute('size')); }, set(input) { // Get sources const sources = html5.getSources.call(player); - if (utils.is.empty(sources)) { - return; - } - // Get matches for requested size - const matches = Array.from(sources).filter(source => Number(source.getAttribute('size')) === input); - - // No matches for requested size - if (utils.is.empty(matches)) { - return; - } + const matches = sources.filter(source => Number(source.getAttribute('size')) === input); // Get supported sources const supported = matches.filter(source => support.mime.call(player, source.getAttribute('type'))); -- cgit v1.2.3 From 62c263bda32434df26b5e63fc646cfe294c98449 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 11 Jun 2018 04:22:40 +0200 Subject: Replace quality setter conditions with Array.find() --- src/js/plyr.js | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) (limited to 'src/js') diff --git a/src/js/plyr.js b/src/js/plyr.js index 181eff9e..e2fce1fe 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -669,36 +669,28 @@ class Plyr { * @param {number} input - Quality level */ set quality(input) { - let quality = null; + const config = this.config.quality; + const options = this.options.quality; - if (!utils.is.empty(input)) { - quality = Number(input); - } - - if (!utils.is.number(quality)) { - quality = this.storage.get('quality'); - } - - if (!utils.is.number(quality)) { - quality = this.config.quality.selected; - } - - if (!utils.is.number(quality)) { - quality = this.config.quality.default; - } - - if (!this.options.quality.length) { + if (!options.length) { return; } - if (!this.options.quality.includes(quality)) { - const closest = utils.closest(this.options.quality, quality); + let quality = ([ + !utils.is.empty(input) && Number(input), + this.storage.get('quality'), + config.selected, + config.default, + ]).find(utils.is.number); + + if (!options.includes(quality)) { + const closest = utils.closest(options, quality); this.debug.warn(`Unsupported quality option: ${quality}, using ${closest} instead`); quality = closest; } // Update config - this.config.quality.selected = quality; + config.selected = quality; // Set quality this.media.quality = quality; -- cgit v1.2.3 From efe70ab48efe5eb183c92673a0b321c3004b7f58 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 11 Jun 2018 16:39:35 +1000 Subject: v3.3.11 --- src/js/defaults.js | 2 +- src/js/plyr.js | 2 +- src/js/plyr.polyfilled.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src/js') diff --git a/src/js/defaults.js b/src/js/defaults.js index 16df8624..37fcdeec 100644 --- a/src/js/defaults.js +++ b/src/js/defaults.js @@ -56,7 +56,7 @@ const defaults = { // Sprite (for icons) loadSprite: true, iconPrefix: 'plyr', - iconUrl: 'https://cdn.plyr.io/3.3.10/plyr.svg', + iconUrl: 'https://cdn.plyr.io/3.3.11/plyr.svg', // Blank video (used to prevent errors on source change) blankVideo: 'https://cdn.plyr.io/static/blank.mp4', diff --git a/src/js/plyr.js b/src/js/plyr.js index 5c51d617..90ddb8fa 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr -// plyr.js v3.3.10 +// plyr.js v3.3.11 // https://github.com/sampotts/plyr // License: The MIT License (MIT) // ========================================================================== diff --git a/src/js/plyr.polyfilled.js b/src/js/plyr.polyfilled.js index f66a82de..852630ac 100644 --- a/src/js/plyr.polyfilled.js +++ b/src/js/plyr.polyfilled.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr Polyfilled Build -// plyr.js v3.3.10 +// plyr.js v3.3.11 // https://github.com/sampotts/plyr // License: The MIT License (MIT) // ========================================================================== -- cgit v1.2.3 From 840e31a693462e7ed9f7644a13a0187d9e9d93a9 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 11 Jun 2018 17:10:37 +1000 Subject: v3.3.12 --- src/js/defaults.js | 2 +- src/js/plyr.js | 2 +- src/js/plyr.polyfilled.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src/js') diff --git a/src/js/defaults.js b/src/js/defaults.js index ff207017..1789b026 100644 --- a/src/js/defaults.js +++ b/src/js/defaults.js @@ -56,7 +56,7 @@ const defaults = { // Sprite (for icons) loadSprite: true, iconPrefix: 'plyr', - iconUrl: 'https://cdn.plyr.io/3.3.11/plyr.svg', + iconUrl: 'https://cdn.plyr.io/3.3.12/plyr.svg', // Blank video (used to prevent errors on source change) blankVideo: 'https://cdn.plyr.io/static/blank.mp4', diff --git a/src/js/plyr.js b/src/js/plyr.js index 752b3d3c..0786334d 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr -// plyr.js v3.3.11 +// plyr.js v3.3.12 // https://github.com/sampotts/plyr // License: The MIT License (MIT) // ========================================================================== diff --git a/src/js/plyr.polyfilled.js b/src/js/plyr.polyfilled.js index 852630ac..2c19ae1d 100644 --- a/src/js/plyr.polyfilled.js +++ b/src/js/plyr.polyfilled.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr Polyfilled Build -// plyr.js v3.3.11 +// plyr.js v3.3.12 // https://github.com/sampotts/plyr // License: The MIT License (MIT) // ========================================================================== -- cgit v1.2.3 From f15e07f7f54975caf41c975d06138d3846d22c03 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 11 Jun 2018 20:21:37 +0200 Subject: Simplify logic in youtube.mapQualityUnit (not that it matters much now) --- src/js/plugins/youtube.js | 64 ++++++++++++++--------------------------------- 1 file changed, 19 insertions(+), 45 deletions(-) (limited to 'src/js') diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index 9b067c8a..c759d8d2 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -8,52 +8,26 @@ import utils from './../utils'; // Standardise YouTube quality unit function mapQualityUnit(input) { - switch (input) { - case 'hd2160': - return 2160; - - case 2160: - return 'hd2160'; - - case 'hd1440': - return 1440; - - case 1440: - return 'hd1440'; - - case 'hd1080': - return 1080; - - case 1080: - return 'hd1080'; - - case 'hd720': - return 720; - - case 720: - return 'hd720'; - - case 'large': - return 480; - - case 480: - return 'large'; - - case 'medium': - return 360; - - case 360: - return 'medium'; - - case 'small': - return 240; - - case 240: - return 'small'; - - default: - return 'default'; + const qualities = { + hd2160: 2160, + hd1440: 1440, + hd1080: 1080, + hd720: 720, + large: 480, + medium: 360, + small: 240, + tiny: 144, + }; + + const entry = Object.entries(qualities) + .find(entry => entry.includes(input)); + + if (entry) { + // Get the match corresponding to the input + return entry.find(value => value !== input); } + + return 'default'; } function mapQualityUnits(levels) { -- cgit v1.2.3 From ed606c28abec076ba164ec600a743a2bdd3307f2 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 11 Jun 2018 06:34:02 +0200 Subject: Filter out unsupported mimetypes in getSources() instead of the quality setter --- src/js/html5.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) (limited to 'src/js') diff --git a/src/js/html5.js b/src/js/html5.js index a7ff0bd9..8f23b3c1 100644 --- a/src/js/html5.js +++ b/src/js/html5.js @@ -11,7 +11,10 @@ const html5 = { return []; } - return Array.from(this.media.querySelectorAll('source')); + const sources = Array.from(this.media.querySelectorAll('source')); + + // Filter out unsupported sources + return sources.filter(source => support.mime.call(this, source.getAttribute('type'))); }, // Get quality levels @@ -46,14 +49,11 @@ const html5 = { // Get sources const sources = html5.getSources.call(player); - // Get matches for requested size - const matches = sources.filter(source => Number(source.getAttribute('size')) === input); - - // Get supported sources - const supported = matches.filter(source => support.mime.call(player, source.getAttribute('type'))); + // Get first match for requested size + const source = sources.find(source => Number(source.getAttribute('size')) === input); - // No supported sources - if (utils.is.empty(supported)) { + // No matching source found + if (!source) { return; } @@ -66,7 +66,7 @@ const html5 = { const { currentTime, playing } = player; // Set new source - player.media.src = supported[0].getAttribute('src'); + player.media.src = source.getAttribute('src'); // Restore time const onLoadedMetaData = () => { -- cgit v1.2.3 From 81ee3f759c86ded1967555ab22cfef8cd16607b5 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 11 Jun 2018 08:56:13 +0200 Subject: Remove todo comment about Vimeo support for setting quality (they don't support it) --- src/js/controls.js | 1 - 1 file changed, 1 deletion(-) (limited to 'src/js') diff --git a/src/js/controls.js b/src/js/controls.js index 058e636f..dbc88a3a 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -632,7 +632,6 @@ const controls = { }, // Set the quality menu - // TODO: Vimeo support setQualityMenu(options) { // Menu required if (!utils.is.element(this.elements.settings.panes.quality)) { -- cgit v1.2.3 From 6d2dad58108d4c57e573a70872136c8dbb635d74 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 11 Jun 2018 20:41:53 +0200 Subject: Trigger qualityrequested event unconditionally when trying to set it (needed for streaming libraries to be able to listen) --- src/js/html5.js | 5 ----- src/js/plugins/youtube.js | 10 +--------- src/js/plyr.js | 3 +++ 3 files changed, 4 insertions(+), 14 deletions(-) (limited to 'src/js') diff --git a/src/js/html5.js b/src/js/html5.js index 8f23b3c1..9931ae93 100644 --- a/src/js/html5.js +++ b/src/js/html5.js @@ -57,11 +57,6 @@ const html5 = { return; } - // Trigger change event - utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, { - quality: input, - }); - // Get current state const { currentTime, playing } = player; diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index c759d8d2..f7458bcb 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -302,15 +302,7 @@ const youtube = { return mapQualityUnit(instance.getPlaybackQuality()); }, set(input) { - const quality = input; - - // Set via API - instance.setPlaybackQuality(mapQualityUnit(quality)); - - // Trigger request event - utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, { - quality, - }); + instance.setPlaybackQuality(mapQualityUnit(input)); }, }); diff --git a/src/js/plyr.js b/src/js/plyr.js index e2fce1fe..46fed3b2 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -689,6 +689,9 @@ class Plyr { quality = closest; } + // Trigger request event + utils.dispatchEvent.call(this, this.media, 'qualityrequested', false, { quality }); + // Update config config.selected = quality; -- cgit v1.2.3 From db95b3234fd38e5dd71d00876c925514960e63fc Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 11 Jun 2018 17:00:34 +0200 Subject: Move uniqueness filter from getQualityOptions to setQualityMenu --- src/js/controls.js | 5 +++-- src/js/html5.js | 5 +---- 2 files changed, 4 insertions(+), 6 deletions(-) (limited to 'src/js') diff --git a/src/js/controls.js b/src/js/controls.js index dbc88a3a..8fb2b7b7 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -641,9 +641,10 @@ const controls = { const type = 'quality'; const list = this.elements.settings.panes.quality.querySelector('ul'); - // Set options if passed and filter based on config + // Set options if passed and filter based on uniqueness and config if (utils.is.array(options)) { - this.options.quality = options.filter(quality => this.config.quality.options.includes(quality)); + this.options.quality = utils.dedupe(options) + .filter(quality => this.config.quality.options.includes(quality)); } // Toggle the pane and tab diff --git a/src/js/html5.js b/src/js/html5.js index 9931ae93..fb2bc359 100644 --- a/src/js/html5.js +++ b/src/js/html5.js @@ -20,12 +20,9 @@ const html5 = { // Get quality levels getQualityOptions() { // Get sizes from elements - const sizes = html5.getSources.call(this) + return html5.getSources.call(this) .map(source => Number(source.getAttribute('size'))) .filter(Boolean); - - // Reduce to unique list - return utils.dedupe(sizes); }, extend() { -- cgit v1.2.3 From ee4c044d2746ffc3cb5bd5de5fe6eab6b336a11c Mon Sep 17 00:00:00 2001 From: BoHong Li Date: Tue, 12 Jun 2018 11:35:31 +0800 Subject: fix: html5.cancelRequest not remove source tag correctly --- src/js/html5.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/js') diff --git a/src/js/html5.js b/src/js/html5.js index fb2bc359..c828dfaf 100644 --- a/src/js/html5.js +++ b/src/js/html5.js @@ -91,7 +91,7 @@ const html5 = { } // Remove child sources - utils.removeElement(html5.getSources()); + utils.removeElement(html5.getSources.call(this)); // Set blank video src attribute // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error -- cgit v1.2.3 From 87170ab46080ae0d82afd1b39ab3fdf2e3ff1e62 Mon Sep 17 00:00:00 2001 From: cky <576779975@qq.com> Date: Tue, 12 Jun 2018 20:55:31 +0800 Subject: remove event listeners in destroy, add once method --- src/js/captions.js | 4 ++-- src/js/controls.js | 4 +--- src/js/fullscreen.js | 4 ++-- src/js/html5.js | 3 +-- src/js/listeners.js | 60 +++++++++++++++++++++++----------------------------- src/js/plyr.js | 18 +++++++++++----- src/js/utils.js | 32 +++++++++++++++++++++++----- 7 files changed, 72 insertions(+), 53 deletions(-) (limited to 'src/js') diff --git a/src/js/captions.js b/src/js/captions.js index bafcf87e..18f4cbd3 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -80,7 +80,7 @@ const captions = { // Watch changes to textTracks and update captions menu if (this.isHTML5) { const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack'; - utils.on(this.media.textTracks, trackEvents, captions.update.bind(this)); + utils.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) @@ -107,7 +107,7 @@ const captions = { track.mode = 'hidden'; // Add event listener for cue changes - utils.on(track, 'cuechange', () => captions.updateCues.call(this)); + utils.on.call(this, track, 'cuechange', () => captions.updateCues.call(this)); }); } diff --git a/src/js/controls.js b/src/js/controls.js index 8fb2b7b7..1301084a 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -1063,12 +1063,10 @@ const controls = { container.style.width = ''; container.style.height = ''; - // Only listen once - utils.off(container, utils.transitionEndEvent, restore); }; // Listen for the transition finishing and restore auto height/width - utils.on(container, utils.transitionEndEvent, restore); + utils.once(container, utils.transitionEndEvent, restore); // Set dimensions to target container.style.width = `${size.width}px`; diff --git a/src/js/fullscreen.js b/src/js/fullscreen.js index 000ba706..50681c73 100644 --- a/src/js/fullscreen.js +++ b/src/js/fullscreen.js @@ -62,13 +62,13 @@ class Fullscreen { // Register event listeners // Handle event (incase user presses escape etc) - utils.on(document, this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, () => { + utils.on.call(this.player, document, this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, () => { // TODO: Filter for target?? onChange.call(this); }); // Fullscreen toggle on double click - utils.on(this.player.elements.container, 'dblclick', event => { + utils.on.call(this.player, this.player.elements.container, 'dblclick', event => { // Ignore double click in controls if (utils.is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) { return; diff --git a/src/js/html5.js b/src/js/html5.js index fb2bc359..3b0b8c71 100644 --- a/src/js/html5.js +++ b/src/js/html5.js @@ -63,9 +63,8 @@ const html5 = { // Restore time const onLoadedMetaData = () => { player.currentTime = currentTime; - player.off('loadedmetadata', onLoadedMetaData); }; - player.on('loadedmetadata', onLoadedMetaData); + player.once('loadedmetadata', onLoadedMetaData); // Load new source player.media.load(); diff --git a/src/js/listeners.js b/src/js/listeners.js index c391ea4c..56d0d177 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -197,39 +197,36 @@ class Listeners { // Add touch class utils.toggleClass(this.player.elements.container, this.player.config.classNames.isTouch, true); - // Clean up - utils.off(document.body, 'touchstart', this.firstTouch); } // Global window & document listeners global(toggle = true) { // Keyboard shortcuts if (this.player.config.keyboard.global) { - utils.toggleListener(window, 'keydown keyup', this.handleKey, toggle, false); + utils.toggleListener.call(this.player, window, 'keydown keyup', this.handleKey, toggle, false); } // Click anywhere closes menu - utils.toggleListener(document.body, 'click', this.toggleMenu, toggle); + utils.toggleListener.call(this.player, document.body, 'click', this.toggleMenu, toggle); // Detect touch by events - utils.on(document.body, 'touchstart', this.firstTouch); + utils.once(document.body, 'touchstart', this.firstTouch); } // Container listeners container() { // Keyboard shortcuts if (!this.player.config.keyboard.global && this.player.config.keyboard.focused) { - utils.on(this.player.elements.container, 'keydown keyup', this.handleKey, false); + utils.on.call(this.player, this.player.elements.container, 'keydown keyup', this.handleKey, false); } // Detect tab focus // Remove class on blur/focusout - utils.on(this.player.elements.container, 'focusout', event => { + utils.on.call(this.player, this.player.elements.container, 'focusout', event => { utils.toggleClass(event.target, this.player.config.classNames.tabFocus, false); }); - // Add classname to tabbed elements - utils.on(this.player.elements.container, 'keydown', event => { + utils.on.call(this.player, this.player.elements.container, 'keydown', event => { if (event.keyCode !== 9) { return; } @@ -242,7 +239,7 @@ class Listeners { }); // Toggle controls on mouse events and entering fullscreen - utils.on(this.player.elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => { + utils.on.call(this.player, this.player.elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => { const { controls } = this.player.elements; // Remove button states for fullscreen @@ -276,20 +273,20 @@ class Listeners { // Listen for media events media() { // Time change on media - utils.on(this.player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(this.player, event)); + utils.on.call(this.player, this.player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(this.player, event)); // Display duration - utils.on(this.player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(this.player, event)); + utils.on.call(this.player, this.player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(this.player, event)); // Check for audio tracks on load // We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point - utils.on(this.player.media, 'loadeddata', () => { + utils.on.call(this.player, this.player.media, 'loadeddata', () => { utils.toggleHidden(this.player.elements.volume, !this.player.hasAudio); utils.toggleHidden(this.player.elements.buttons.mute, !this.player.hasAudio); }); // Handle the media finishing - utils.on(this.player.media, 'ended', () => { + utils.on.call(this.player, this.player.media, 'ended', () => { // Show poster on end if (this.player.isHTML5 && this.player.isVideo && this.player.config.resetOnEnd) { // Restart @@ -298,20 +295,20 @@ class Listeners { }); // Check for buffer progress - utils.on(this.player.media, 'progress playing seeking seeked', event => controls.updateProgress.call(this.player, event)); + utils.on.call(this.player, this.player.media, 'progress playing seeking seeked', event => controls.updateProgress.call(this.player, event)); // Handle volume changes - utils.on(this.player.media, 'volumechange', event => controls.updateVolume.call(this.player, event)); + utils.on.call(this.player, this.player.media, 'volumechange', event => controls.updateVolume.call(this.player, event)); // Handle play/pause - utils.on(this.player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(this.player, event)); + utils.on.call(this.player, this.player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(this.player, event)); // Loading state - utils.on(this.player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(this.player, event)); + utils.on.call(this.player, this.player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(this.player, event)); // If autoplay, then load advertisement if required // TODO: Show some sort of loading state while the ad manager loads else there's a delay before ad shows - utils.on(this.player.media, 'playing', () => { + utils.on.call(this.player, this.player.media, 'playing', () => { if (!this.player.ads) { return; } @@ -334,7 +331,7 @@ class Listeners { } // On click play, pause ore restart - utils.on(wrapper, 'click', () => { + utils.on.call(this.player, wrapper, 'click', () => { // Touch devices will just show controls (if we're hiding controls) if (this.player.config.hideControls && this.player.touch && !this.player.paused) { return; @@ -353,7 +350,7 @@ class Listeners { // Disable right click if (this.player.supported.ui && this.player.config.disableContextMenu) { - utils.on( + utils.on.call(this.player, this.player.elements.wrapper, 'contextmenu', event => { @@ -364,13 +361,13 @@ class Listeners { } // Volume change - utils.on(this.player.media, 'volumechange', () => { + utils.on.call(this.player, this.player.media, 'volumechange', () => { // Save to storage this.player.storage.set({ volume: this.player.volume, muted: this.player.muted }); }); // Speed change - utils.on(this.player.media, 'ratechange', () => { + utils.on.call(this.player, this.player.media, 'ratechange', () => { // Update UI controls.updateSetting.call(this.player, 'speed'); @@ -379,19 +376,19 @@ class Listeners { }); // Quality request - utils.on(this.player.media, 'qualityrequested', event => { + utils.on.call(this.player, this.player.media, 'qualityrequested', event => { // Save to storage this.player.storage.set({ quality: event.detail.quality }); }); // Quality change - utils.on(this.player.media, 'qualitychange', event => { + utils.on.call(this.player, this.player.media, 'qualitychange', event => { // Update UI controls.updateSetting.call(this.player, 'quality', null, event.detail.quality); }); // Caption language change - utils.on(this.player.media, 'languagechange', () => { + utils.on.call(this.player, this.player.media, 'languagechange', () => { // Update UI controls.updateSetting.call(this.player, 'captions'); @@ -400,7 +397,7 @@ class Listeners { }); // Captions toggle - utils.on(this.player.media, 'captionsenabled captionsdisabled', () => { + utils.on.call(this.player, this.player.media, 'captionsenabled captionsdisabled', () => { // Update UI controls.updateSetting.call(this.player, 'captions'); @@ -410,7 +407,7 @@ class Listeners { // Proxy events to container // Bubble up key events for Edge - utils.on(this.player.media, this.player.config.events.concat([ + utils.on.call(this.player, this.player.media, this.player.config.events.concat([ 'keyup', 'keydown', ]).join(' '), event => { @@ -452,7 +449,7 @@ class Listeners { const customHandler = this.player.config.listeners[customHandlerKey]; const hasCustomHandler = utils.is.function(customHandler); - utils.on(element, type, event => proxy(event, defaultHandler, customHandlerKey), passive && !hasCustomHandler); + utils.on.call(this.player, element, type, event => proxy(event, defaultHandler, customHandlerKey), passive && !hasCustomHandler); }; // Play/pause toggle @@ -727,11 +724,6 @@ class Listeners { false, ); } - - // Reset on destroy - clear() { - this.global(false); - } } export default Listeners; diff --git a/src/js/plyr.js b/src/js/plyr.js index 71ca363e..2d2267da 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -245,6 +245,8 @@ class Plyr { return; } + this.eventListeners = []; + // Create listeners this.listeners = new Listeners(this); @@ -271,7 +273,7 @@ class Plyr { // Listen for events if debugging if (this.config.debug) { - utils.on(this.elements.container, this.config.events.join(' '), event => { + utils.on.call(this, this.elements.container, this.config.events.join(' '), event => { this.debug.log(`event: ${event.type}`); }); } @@ -961,9 +963,16 @@ class Plyr { * @param {function} callback - Callback for when event occurs */ on(event, callback) { - utils.on(this.elements.container, event, callback); + utils.on.call(this, this.elements.container, event, callback); + } + /** + * Add event listeners once + * @param {string} event - Event type + * @param {function} callback - Callback for when event occurs + */ + once(event, callback) { + utils.once(this.elements.container, event, callback); } - /** * Remove event listeners * @param {string} event - Event type @@ -1014,8 +1023,7 @@ class Plyr { } } else { // Unbind listeners - this.listeners.clear(); - + utils.cleanupEventListeners.call(this); // Replace the container with the original element provided utils.replaceElement(this.elements.original, this.elements.container); diff --git a/src/js/utils.js b/src/js/utils.js index c36763dd..b2a06204 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -494,14 +494,14 @@ const utils = { }; if (toggle) { - utils.on(this.elements.container, 'keydown', trap, false); + utils.on.call(this, this.elements.container, 'keydown', trap, false); } else { utils.off(this.elements.container, 'keydown', trap, false); } }, // Toggle event listener - toggleListener(elements, event, callback, toggle = false, passive = true, capture = false) { + toggleListener(elements, event, callback, toggle = false, passive = true, capture = false, once = false) { // Bail if no elemetns, event, or callback if (utils.is.empty(elements) || utils.is.empty(event) || !utils.is.function(callback)) { return; @@ -512,7 +512,7 @@ const utils = { // Create listener for each node Array.from(elements).forEach(element => { if (element instanceof Node) { - utils.toggleListener.call(null, element, event, callback, toggle, passive, capture); + utils.toggleListener.call(this, element, event, callback, toggle, passive, capture, once); } }); @@ -538,13 +538,35 @@ const utils = { // If a single node is passed, bind the event listener events.forEach(type => { + if (this && this.eventListeners && toggle && !once) { + // cache event listener + this.eventListeners.push({ elements, type, callback, options }); + } elements[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options); }); }, - + // remove all cached event listeners + cleanupEventListeners() { + if (this && this.eventListeners) { + this.eventListeners.forEach(item => { + const { elements, type, callback, options } = item; + elements.removeEventListener(type, callback, options); + }); + this.eventListeners = []; + } + }, // Bind event handler on(element, events = '', callback, passive = true, capture = false) { - utils.toggleListener(element, events, callback, true, passive, capture); + utils.toggleListener.call(this, element, events, callback, true, passive, capture); + }, + + // Bind event handler once + once(element, events = '', callback, passive = true, capture = false) { + function onceCallback(...args) { + utils.off(element, events, onceCallback, passive, capture); + callback.apply(this, args); + } + utils.toggleListener(element, events, onceCallback, true, passive, capture, true); }, // Unbind event handler -- cgit v1.2.3 From 392dfd024c505f5ae1bbb2f0d3e0793c251a1f35 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Wed, 13 Jun 2018 00:02:55 +1000 Subject: Utils broken down into seperate files and exports --- src/js/captions.js | 100 +++--- src/js/config/defaults.js | 406 +++++++++++++++++++++ src/js/config/types.js | 34 ++ src/js/controls.js | 320 +++++++++-------- src/js/defaults.js | 406 --------------------- src/js/fullscreen.js | 35 +- src/js/html5.js | 29 +- src/js/i18n.js | 12 +- src/js/listeners.js | 152 ++++---- src/js/media.js | 14 +- src/js/plugins/ads.js | 38 +- src/js/plugins/vimeo.js | 98 +++-- src/js/plugins/youtube.js | 104 +++--- src/js/plyr.js | 156 ++++---- src/js/source.js | 31 +- src/js/storage.js | 13 +- src/js/support.js | 39 +- src/js/types.js | 16 - src/js/ui.js | 70 ++-- src/js/utils.js | 875 --------------------------------------------- src/js/utils/animation.js | 30 ++ src/js/utils/arrays.js | 23 ++ src/js/utils/browser.js | 13 + src/js/utils/elements.js | 307 ++++++++++++++++ src/js/utils/events.js | 98 +++++ src/js/utils/fetch.js | 42 +++ src/js/utils/is.js | 64 ++++ src/js/utils/loadImage.js | 19 + src/js/utils/loadScript.js | 14 + src/js/utils/loadSprite.js | 75 ++++ src/js/utils/objects.js | 42 +++ src/js/utils/strings.js | 82 +++++ src/js/utils/time.js | 36 ++ src/js/utils/urls.js | 44 +++ 34 files changed, 1952 insertions(+), 1885 deletions(-) create mode 100644 src/js/config/defaults.js create mode 100644 src/js/config/types.js delete mode 100644 src/js/defaults.js delete mode 100644 src/js/types.js delete mode 100644 src/js/utils.js create mode 100644 src/js/utils/animation.js create mode 100644 src/js/utils/arrays.js create mode 100644 src/js/utils/browser.js create mode 100644 src/js/utils/elements.js create mode 100644 src/js/utils/events.js create mode 100644 src/js/utils/fetch.js create mode 100644 src/js/utils/is.js create mode 100644 src/js/utils/loadImage.js create mode 100644 src/js/utils/loadScript.js create mode 100644 src/js/utils/loadSprite.js create mode 100644 src/js/utils/objects.js create mode 100644 src/js/utils/strings.js create mode 100644 src/js/utils/time.js create mode 100644 src/js/utils/urls.js (limited to 'src/js') diff --git a/src/js/captions.js b/src/js/captions.js index bafcf87e..0506d1e6 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -6,7 +6,13 @@ import controls from './controls'; import i18n from './i18n'; import support from './support'; -import utils from './utils'; +import browser from './utils/browser'; +import { createElement, emptyElement, getAttributesFromSelector, insertAfter, removeElement, toggleClass } from './utils/elements'; +import { on, trigger } from './utils/events'; +import fetch from './utils/fetch'; +import is from './utils/is'; +import { getHTML } from './utils/strings'; +import { parseUrl } from './utils/urls'; const captions = { // Setup captions @@ -19,7 +25,7 @@ const captions = { // Only Vimeo and HTML5 video supported at this point if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) { // Clear menu and hide - if (utils.is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) { + if (is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) { controls.setCaptionsMenu.call(this); } @@ -27,15 +33,12 @@ const captions = { } // Inject the container - if (!utils.is.element(this.elements.captions)) { - this.elements.captions = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.captions)); + if (!is.element(this.elements.captions)) { + this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions)); - utils.insertAfter(this.elements.captions, this.elements.wrapper); + insertAfter(this.elements.captions, this.elements.wrapper); } - // Get browser info - const browser = utils.getBrowser(); - // Fix IE captions if CORS is used // Fetch captions and inject as blobs instead (data URIs not supported!) if (browser.isIE && window.URL) { @@ -43,19 +46,18 @@ const captions = { Array.from(elements).forEach(track => { const src = track.getAttribute('src'); - const href = utils.parseUrl(src); + const url = parseUrl(src); - if (href.hostname !== window.location.href.hostname && [ + if (url !== null && url.hostname !== window.location.href.hostname && [ 'http:', 'https:', - ].includes(href.protocol)) { - utils - .fetch(src, 'blob') + ].includes(url.protocol)) { + fetch(src, 'blob') .then(blob => { track.setAttribute('src', window.URL.createObjectURL(blob)); }) .catch(() => { - utils.removeElement(track); + removeElement(track); }); } }); @@ -65,14 +67,14 @@ const captions = { let active = this.storage.get('captions'); // Otherwise fall back to the default config - if (!utils.is.boolean(active)) { + if (!is.boolean(active)) { ({ active } = this.config.captions); } // Get language from storage, fallback to config let language = this.storage.get('language') || this.config.captions.language; if (language === 'auto') { - [ language ] = (navigator.language || navigator.userLanguage).split('-'); + [language] = (navigator.language || navigator.userLanguage).split('-'); } // Set language and show if active captions.setLanguage.call(this, language, active); @@ -80,7 +82,7 @@ const captions = { // Watch changes to textTracks and update captions menu if (this.isHTML5) { const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack'; - utils.on(this.media.textTracks, trackEvents, captions.update.bind(this)); + on(this.media.textTracks, trackEvents, captions.update.bind(this)); } // Update available languages in list next tick (the event must not be triggered before the listeners) @@ -94,21 +96,19 @@ const captions = { // 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 - track.mode = 'hidden'; - - // Add event listener for cue changes - utils.on(track, 'cuechange', () => captions.updateCues.call(this)); + 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 + track.mode = 'hidden'; + + // Add event listener for cue changes + on(track, 'cuechange', () => captions.updateCues.call(this)); + }); } const trackRemoved = !tracks.find(track => track === this.captions.currentTrackNode); @@ -120,7 +120,7 @@ const captions = { } // Enable or disable captions based on track length - utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(tracks)); + toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks)); // Update available languages in list if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) { @@ -137,7 +137,7 @@ const captions = { return; } - if (!utils.is.number(index)) { + if (!is.number(index)) { this.debug.warn('Invalid caption argument', index); return; } @@ -166,7 +166,7 @@ const captions = { } // Trigger event - utils.dispatchEvent.call(this, this.media, 'languagechange'); + trigger.call(this, this.media, 'languagechange'); } if (this.isHTML5 && this.isVideo) { @@ -181,7 +181,7 @@ const captions = { }, setLanguage(language, show = true) { - if (!utils.is.string(language)) { + if (!is.string(language)) { this.debug.warn('Invalid language argument', language); return; } @@ -202,12 +202,10 @@ const captions = { 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)); + return tracks.filter(track => !this.isHTML5 || update || this.captions.meta.has(track)).filter(track => [ + 'captions', + 'subtitles', + ].includes(track.kind)); }, // Get the current track for the current language @@ -222,16 +220,16 @@ const captions = { getLabel(track) { let currentTrack = track; - if (!utils.is.track(currentTrack) && support.textTracks && this.captions.active) { + if (!is.track(currentTrack) && support.textTracks && this.captions.active) { currentTrack = captions.getCurrentTrack.call(this); } - if (utils.is.track(currentTrack)) { - if (!utils.is.empty(currentTrack.label)) { + if (is.track(currentTrack)) { + if (!is.empty(currentTrack.label)) { return currentTrack.label; } - if (!utils.is.empty(currentTrack.language)) { + if (!is.empty(currentTrack.language)) { return track.language.toUpperCase(); } @@ -249,13 +247,13 @@ const captions = { return; } - if (!utils.is.element(this.elements.captions)) { + if (!is.element(this.elements.captions)) { this.debug.warn('No captions element to render to'); return; } // Only accept array or empty input - if (!utils.is.nullOrUndefined(input) && !Array.isArray(input)) { + if (!is.nullOrUndefined(input) && !Array.isArray(input)) { this.debug.warn('updateCues: Invalid input', input); return; } @@ -267,7 +265,7 @@ const captions = { const track = captions.getCurrentTrack.call(this); cues = Array.from((track || {}).activeCues || []) .map(cue => cue.getCueAsHTML()) - .map(utils.getHTML); + .map(getHTML); } // Set new caption text @@ -276,13 +274,13 @@ const captions = { if (changed) { // Empty the container and create a new child element - utils.emptyElement(this.elements.captions); - const caption = utils.createElement('span', utils.getAttributesFromSelector(this.config.selectors.caption)); + emptyElement(this.elements.captions); + const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption)); caption.innerHTML = content; this.elements.captions.appendChild(caption); // Trigger event - utils.dispatchEvent.call(this, this.media, 'cuechange'); + trigger.call(this, this.media, 'cuechange'); } }, }; diff --git a/src/js/config/defaults.js b/src/js/config/defaults.js new file mode 100644 index 00000000..1789b026 --- /dev/null +++ b/src/js/config/defaults.js @@ -0,0 +1,406 @@ +// ========================================================================== +// Plyr default config +// ========================================================================== + +const defaults = { + // 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, + + // 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, + + // Aspect ratio (for embeds) + ratio: '16:9', + + // 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.3.12/plyr.svg', + + // Blank video (used to prevent errors on source change) + blankVideo: 'https://cdn.plyr.io/static/blank.mp4', + + // Quality default + quality: { + default: 576, + options: [ + 4320, + 2880, + 2160, + 1440, + 1080, + 720, + 576, + 480, + 360, + 240, + 'default', // YouTube's "auto" + ], + }, + + // Set loops + loop: { + active: false, + // start: null, + // end: null, + }, + + // Speed default and options to display + speed: { + selected: 1, + options: [ + 0.5, + 0.75, + 1, + 1.25, + 1.5, + 1.75, + 2, + ], + }, + + // 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 for vintage browsers + iosNative: false, // Use the native fullscreen in iOS (disables custom controls) + }, + + // Local storage + storage: { + enabled: true, + key: 'plyr', + }, + + // Default controls + controls: [ + 'play-large', + // 'restart', + // 'rewind', + 'play', + // 'fast-forward', + 'progress', + 'current-time', + 'mute', + 'volume', + 'captions', + 'settings', + 'pip', + 'airplay', + 'fullscreen', + ], + settings: [ + 'captions', + 'quality', + 'speed', + ], + + // Localisation + i18n: { + restart: 'Restart', + rewind: 'Rewind {seektime}s', + play: 'Play', + pause: 'Pause', + fastForward: 'Forward {seektime}s', + seek: 'Seek', + played: 'Played', + buffered: 'Buffered', + currentTime: 'Current time', + duration: 'Duration', + volume: 'Volume', + mute: 'Mute', + unmute: 'Unmute', + enableCaptions: 'Enable captions', + disableCaptions: 'Disable captions', + enterFullscreen: 'Enter fullscreen', + exitFullscreen: 'Exit fullscreen', + frameTitle: 'Player for {title}', + captions: 'Captions', + settings: 'Settings', + 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', + }, + }, + + // URLs + urls: { + 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://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet', + }, + googleIMA: { + sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js', + }, + }, + + // Custom control listeners + listeners: { + seek: null, + play: null, + pause: null, + restart: null, + rewind: null, + fastForward: null, + mute: null, + volume: null, + captions: 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 + 'enterfullscreen', + 'exitfullscreen', + 'captionsenabled', + 'captionsdisabled', + 'languagechange', + 'controlshidden', + 'controlsshown', + 'ready', + + // YouTube + 'statechange', + 'qualitychange', + 'qualityrequested', + + // 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"]', + 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', + menu: { + quality: '.js-plyr__menu__list--quality', + }, + }, + + // Class hooks added to the player in different states + classNames: { + type: 'plyr--{0}', + provider: 'plyr--{0}', + video: 'plyr__video-wrapper', + embed: 'plyr__video-embed', + embedContainer: 'plyr__video-embed__container', + poster: 'plyr__poster', + posterEnabled: 'plyr__poster-enabled', + ads: 'plyr__ads', + control: 'plyr__control', + 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', + 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', + }, + + // Embed attributes + attributes: { + embed: { + provider: 'data-plyr-provider', + id: 'data-plyr-embed-id', + }, + }, + + // API keys + keys: { + google: null, + }, + + // Advertisements plugin + // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio + ads: { + enabled: false, + publisherId: '', + }, +}; + +export default defaults; diff --git a/src/js/config/types.js b/src/js/config/types.js new file mode 100644 index 00000000..13303573 --- /dev/null +++ b/src/js/config/types.js @@ -0,0 +1,34 @@ +// ========================================================================== +// Plyr supported types and providers +// ========================================================================== + +export const providers = { + html5: 'html5', + youtube: 'youtube', + vimeo: 'vimeo', +}; + +export const types = { + audio: 'audio', + video: 'video', +}; + +/** + * Get provider by URL + * @param {string} url + */ +export function getProviderByUrl(url) { + // YouTube + if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/.test(url)) { + return providers.youtube; + } + + // Vimeo + if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) { + return providers.vimeo; + } + + return null; +} + +export default { providers, types }; diff --git a/src/js/controls.js b/src/js/controls.js index 058e636f..cfab26bc 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -6,14 +6,17 @@ import captions from './captions'; import html5 from './html5'; import i18n from './i18n'; import support from './support'; -import utils from './utils'; - -// Sniff out the browser -const browser = utils.getBrowser(); +import { repaint, transitionEndEvent } from './utils/animation'; +import browser from './utils/browser'; +import { createElement, emptyElement, getAttributesFromSelector, getElement, getElements, hasClass, removeElement, setAttributes, toggleClass, toggleHidden, toggleState } from './utils/elements'; +import { off, on } from './utils/events'; +import is from './utils/is'; +import loadSprite from './utils/loadSprite'; +import { extend } from './utils/objects'; +import { getPercentage, replaceAll, toCamelCase, toTitleCase } from './utils/strings'; +import { formatTime, getHours } from './utils/time'; const controls = { - - // Get icon URL getIconUrl() { const url = new URL(this.config.iconUrl, window.location); @@ -29,41 +32,41 @@ const controls = { // TODO: Allow settings menus with custom controls findElements() { try { - this.elements.controls = utils.getElement.call(this, this.config.selectors.controls.wrapper); + this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper); // Buttons this.elements.buttons = { - play: utils.getElements.call(this, this.config.selectors.buttons.play), - pause: utils.getElement.call(this, this.config.selectors.buttons.pause), - restart: utils.getElement.call(this, this.config.selectors.buttons.restart), - rewind: utils.getElement.call(this, this.config.selectors.buttons.rewind), - fastForward: utils.getElement.call(this, this.config.selectors.buttons.fastForward), - mute: utils.getElement.call(this, this.config.selectors.buttons.mute), - pip: utils.getElement.call(this, this.config.selectors.buttons.pip), - airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay), - settings: utils.getElement.call(this, this.config.selectors.buttons.settings), - captions: utils.getElement.call(this, this.config.selectors.buttons.captions), - fullscreen: utils.getElement.call(this, this.config.selectors.buttons.fullscreen), + 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 = utils.getElement.call(this, this.config.selectors.progress); + this.elements.progress = getElement.call(this, this.config.selectors.progress); // Inputs this.elements.inputs = { - seek: utils.getElement.call(this, this.config.selectors.inputs.seek), - volume: utils.getElement.call(this, this.config.selectors.inputs.volume), + seek: getElement.call(this, this.config.selectors.inputs.seek), + volume: getElement.call(this, this.config.selectors.inputs.volume), }; // Display this.elements.display = { - buffer: utils.getElement.call(this, this.config.selectors.display.buffer), - currentTime: utils.getElement.call(this, this.config.selectors.display.currentTime), - duration: utils.getElement.call(this, this.config.selectors.display.duration), + 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 (utils.is.element(this.elements.progress)) { + if (is.element(this.elements.progress)) { this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`); } @@ -87,9 +90,9 @@ const controls = { // Create const icon = document.createElementNS(namespace, 'svg'); - utils.setAttributes( + setAttributes( icon, - utils.extend(attributes, { + extend(attributes, { role: 'presentation', focusable: 'false', }), @@ -138,21 +141,21 @@ const controls = { attributes.class = this.config.classNames.hidden; } - return utils.createElement('span', attributes, text); + return createElement('span', attributes, text); }, // Create a badge createBadge(text) { - if (utils.is.empty(text)) { + if (is.empty(text)) { return null; } - const badge = utils.createElement('span', { + const badge = createElement('span', { class: this.config.classNames.menu.value, }); badge.appendChild( - utils.createElement( + createElement( 'span', { class: this.config.classNames.menu.badge, @@ -166,9 +169,9 @@ const controls = { // Create a
to hide the standard controls and UI setAspectRatio(input) { - const ratio = utils.is.string(input) ? input.split(':') : this.config.ratio.split(':'); + const ratio = is.string(input) ? input.split(':') : this.config.ratio.split(':'); const padding = 100 / ratio[0] * ratio[1]; this.elements.wrapper.style.paddingBottom = `${padding}%`; @@ -73,34 +99,34 @@ const vimeo = { gesture: 'media', playsinline: !this.config.fullscreen.iosNative, }; - const params = utils.buildUrlParams(options); + const params = buildUrlParams(options); // Get the source URL or ID let source = player.media.getAttribute('src'); // Get from
if needed - if (utils.is.empty(source)) { + if (is.empty(source)) { source = player.media.getAttribute(player.config.attributes.embed.id); } - const id = utils.parseVimeoId(source); + const id = parseId(source); // Build an iframe - const iframe = utils.createElement('iframe'); - const src = utils.format(player.config.urls.vimeo.iframe, id, params); + 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'); // Inject the package - const wrapper = utils.createElement('div', { class: player.config.classNames.embedContainer }); + const wrapper = createElement('div', { class: player.config.classNames.embedContainer }); wrapper.appendChild(iframe); - player.media = utils.replaceElement(wrapper, player.media); + player.media = replaceElement(wrapper, player.media); // Get poster image - utils.fetch(utils.format(player.config.urls.vimeo.api, id), 'json').then(response => { - if (utils.is.empty(response)) { + fetch(format(player.config.urls.vimeo.api, id), 'json').then(response => { + if (is.empty(response)) { return; } @@ -160,7 +186,7 @@ const vimeo = { // Set seeking state and trigger event media.seeking = true; - utils.dispatchEvent.call(player, media, 'seeking'); + trigger.call(player, media, 'seeking'); // If paused, mute until seek is complete Promise.resolve(restorePause && embed.setVolume(0)) @@ -187,7 +213,7 @@ const vimeo = { .setPlaybackRate(input) .then(() => { speed = input; - utils.dispatchEvent.call(player, player.media, 'ratechange'); + trigger.call(player, player.media, 'ratechange'); }) .catch(error => { // Hide menu item (and menu if empty) @@ -207,7 +233,7 @@ const vimeo = { set(input) { player.embed.setVolume(input).then(() => { volume = input; - utils.dispatchEvent.call(player, player.media, 'volumechange'); + trigger.call(player, player.media, 'volumechange'); }); }, }); @@ -219,11 +245,11 @@ const vimeo = { return muted; }, set(input) { - const toggle = utils.is.boolean(input) ? input : false; + const toggle = is.boolean(input) ? input : false; player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => { muted = toggle; - utils.dispatchEvent.call(player, player.media, 'volumechange'); + trigger.call(player, player.media, 'volumechange'); }); }, }); @@ -235,7 +261,7 @@ const vimeo = { return loop; }, set(input) { - const toggle = utils.is.boolean(input) ? input : player.config.loop.active; + const toggle = is.boolean(input) ? input : player.config.loop.active; player.embed.setLoop(toggle).then(() => { loop = toggle; @@ -272,7 +298,7 @@ const vimeo = { player.embed.getVideoWidth(), player.embed.getVideoHeight(), ]).then(dimensions => { - const ratio = utils.getAspectRatio(dimensions[0], dimensions[1]); + const ratio = getAspectRatio(dimensions[0], dimensions[1]); vimeo.setAspectRatio.call(this, ratio); }); @@ -290,13 +316,13 @@ const vimeo = { // Get current time player.embed.getCurrentTime().then(value => { currentTime = value; - utils.dispatchEvent.call(player, player.media, 'timeupdate'); + trigger.call(player, player.media, 'timeupdate'); }); // Get duration player.embed.getDuration().then(value => { player.media.duration = value; - utils.dispatchEvent.call(player, player.media, 'durationchange'); + trigger.call(player, player.media, 'durationchange'); }); // Get captions @@ -306,7 +332,7 @@ const vimeo = { }); player.embed.on('cuechange', ({ cues = [] }) => { - const strippedCues = cues.map(cue => utils.stripHTML(cue.text)); + const strippedCues = cues.map(cue => stripHTML(cue.text)); captions.updateCues.call(player, strippedCues); }); @@ -315,11 +341,11 @@ const vimeo = { player.embed.getPaused().then(paused => { assurePlaybackState.call(player, !paused); if (!paused) { - utils.dispatchEvent.call(player, player.media, 'playing'); + trigger.call(player, player.media, 'playing'); } }); - if (utils.is.element(player.embed.element) && player.supported.ui) { + if (is.element(player.embed.element) && player.supported.ui) { const frame = player.embed.element; // Fix keyboard focus issues @@ -330,7 +356,7 @@ const vimeo = { player.embed.on('play', () => { assurePlaybackState.call(player, true); - utils.dispatchEvent.call(player, player.media, 'playing'); + trigger.call(player, player.media, 'playing'); }); player.embed.on('pause', () => { @@ -340,16 +366,16 @@ const vimeo = { player.embed.on('timeupdate', data => { player.media.seeking = false; currentTime = data.seconds; - utils.dispatchEvent.call(player, player.media, 'timeupdate'); + trigger.call(player, player.media, 'timeupdate'); }); player.embed.on('progress', data => { player.media.buffered = data.percent; - utils.dispatchEvent.call(player, player.media, 'progress'); + trigger.call(player, player.media, 'progress'); // Check all loaded if (parseInt(data.percent, 10) === 1) { - utils.dispatchEvent.call(player, player.media, 'canplaythrough'); + trigger.call(player, player.media, 'canplaythrough'); } // Get duration as if we do it before load, it gives an incorrect value @@ -357,24 +383,24 @@ const vimeo = { player.embed.getDuration().then(value => { if (value !== player.media.duration) { player.media.duration = value; - utils.dispatchEvent.call(player, player.media, 'durationchange'); + trigger.call(player, player.media, 'durationchange'); } }); }); player.embed.on('seeked', () => { player.media.seeking = false; - utils.dispatchEvent.call(player, player.media, 'seeked'); + trigger.call(player, player.media, 'seeked'); }); player.embed.on('ended', () => { player.media.paused = true; - utils.dispatchEvent.call(player, player.media, 'ended'); + trigger.call(player, player.media, 'ended'); }); player.embed.on('error', detail => { player.media.error = detail; - utils.dispatchEvent.call(player, player.media, 'error'); + trigger.call(player, player.media, 'error'); }); // Rebuild UI diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index 9b067c8a..e486aa43 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -4,7 +4,24 @@ import controls from './../controls'; import ui from './../ui'; -import utils from './../utils'; +import { dedupe } from './../utils/arrays'; +import { createElement, replaceElement, toggleClass } from './../utils/elements'; +import { trigger } from './../utils/events'; +import fetch from './../utils/fetch'; +import is from './../utils/is'; +import loadImage from './../utils/loadImage'; +import loadScript from './../utils/loadScript'; +import { format, generateId } from './../utils/strings'; + +// Parse YouTube ID from URL +function parseId(url) { + if (is.empty(url)) { + return null; + } + + const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; + return url.match(regex) ? RegExp.$2 : url; +} // Standardise YouTube quality unit function mapQualityUnit(input) { @@ -57,11 +74,11 @@ function mapQualityUnit(input) { } function mapQualityUnits(levels) { - if (utils.is.empty(levels)) { + if (is.empty(levels)) { return levels; } - return utils.dedupe(levels.map(level => mapQualityUnit(level))); + return dedupe(levels.map(level => mapQualityUnit(level))); } // Set playback state and trigger change (only on actual change) @@ -71,24 +88,24 @@ function assurePlaybackState(play) { } if (this.media.paused === play) { this.media.paused = !play; - utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause'); + trigger.call(this, this.media, play ? 'play' : 'pause'); } } const youtube = { setup() { // Add embed class for responsive - utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true); + toggleClass(this.elements.wrapper, this.config.classNames.embed, true); // Set aspect ratio youtube.setAspectRatio.call(this); // Setup API - if (utils.is.object(window.YT) && utils.is.function(window.YT.Player)) { + if (is.object(window.YT) && is.function(window.YT.Player)) { youtube.ready.call(this); } else { // Load the API - utils.loadScript(this.config.urls.youtube.sdk).catch(error => { + loadScript(this.config.urls.youtube.sdk).catch(error => { this.debug.warn('YouTube API failed to load', error); }); @@ -115,10 +132,10 @@ const youtube = { // Try via undocumented API method first // This method disappears now and then though... // https://github.com/sampotts/plyr/issues/709 - if (utils.is.function(this.embed.getVideoData)) { + if (is.function(this.embed.getVideoData)) { const { title } = this.embed.getVideoData(); - if (utils.is.empty(title)) { + if (is.empty(title)) { this.config.title = title; ui.setTitle.call(this); return; @@ -127,13 +144,12 @@ const youtube = { // Or via Google API const key = this.config.keys.google; - if (utils.is.string(key) && !utils.is.empty(key)) { - const url = utils.format(this.config.urls.youtube.api, videoId, key); + if (is.string(key) && !is.empty(key)) { + const url = format(this.config.urls.youtube.api, videoId, key); - utils - .fetch(url) + fetch(url) .then(result => { - if (utils.is.object(result)) { + if (is.object(result)) { this.config.title = result.items[0].snippet.title; ui.setTitle.call(this); } @@ -154,7 +170,7 @@ const youtube = { // Ignore already setup (race condition) const currentId = player.media.getAttribute('id'); - if (!utils.is.empty(currentId) && currentId.startsWith('youtube-')) { + if (!is.empty(currentId) && currentId.startsWith('youtube-')) { return; } @@ -162,23 +178,23 @@ const youtube = { let source = player.media.getAttribute('src'); // Get from
if needed - if (utils.is.empty(source)) { + if (is.empty(source)) { source = player.media.getAttribute(this.config.attributes.embed.id); } // Replace the