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 ------------------- src/sass/components/control.scss | 8 +++---- 8 files changed, 44 insertions(+), 69 deletions(-) (limited to 'src') 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)) { diff --git a/src/sass/components/control.scss b/src/sass/components/control.scss index 52716805..cfef1b3a 100644 --- a/src/sass/components/control.scss +++ b/src/sass/components/control.scss @@ -34,10 +34,10 @@ } // Change icons on state change -.plyr__control[aria-pressed='false'] .icon--pressed, -.plyr__control[aria-pressed='true'] .icon--not-pressed, -.plyr__control[aria-pressed='false'] .label--pressed, -.plyr__control[aria-pressed='true'] .label--not-pressed { +.plyr__control:not(.plyr__control--pressed) .icon--pressed, +.plyr__control.plyr__control--pressed .icon--not-pressed, +.plyr__control:not(.plyr__control--pressed) .label--pressed, +.plyr__control.plyr__control--pressed .label--not-pressed { display: none; } -- 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') 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') 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') 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 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') 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') 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') 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') 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') 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') 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') 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') 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