Merge branch 'develop' of github.com:sampotts/plyr into develop

# Conflicts:
#	src/js/captions.js
#	src/js/controls.js
#	src/js/fullscreen.js
#	src/js/html5.js
#	src/js/listeners.js
#	src/js/plugins/youtube.js
#	src/js/plyr.js
#	src/js/utils.js
This commit is contained in:
Sam Potts
2018-06-13 00:41:30 +10:00
20 changed files with 667 additions and 797 deletions
+229 -266
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+241 -280
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -215,7 +215,7 @@ You can specify a range of arguments for the constructor to use:
* A CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector) * A CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
* A [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement) * A [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement)
* A [`NodeList]`(https://developer.mozilla.org/en-US/docs/Web/API/NodeList) * A [`NodeList`](https://developer.mozilla.org/en-US/docs/Web/API/NodeList)
* A [jQuery](https://jquery.com) object * A [jQuery](https://jquery.com) object
_Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element will be used for setup. To setup multiple players, see [setting up multiple players](#setting-up-multiple-players) below. _Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element will be used for setup. To setup multiple players, see [setting up multiple players](#setting-up-multiple-players) below.
@@ -367,6 +367,7 @@ player.fullscreen.enter(); // Enter fullscreen
| `airplay()` | - | Trigger the airplay dialog on supported devices. | | `airplay()` | - | Trigger the airplay dialog on supported devices. |
| `toggleControls(toggle)` | Boolean | Toggle the controls (video only). Takes optional truthy value to force it on/off. | | `toggleControls(toggle)` | Boolean | Toggle the controls (video only). Takes optional truthy value to force it on/off. |
| `on(event, function)` | String, Function | Add an event listener for the specified event. | | `on(event, function)` | String, Function | Add an event listener for the specified event. |
| `once(event, function)` | String, Function | Add an event listener for the specified event once. |
| `off(event, function)` | String, Function | Remove an event listener for the specified event. | | `off(event, function)` | String, Function | Remove an event listener for the specified event. |
| `supports(type)` | String | Check support for a mime type. | | `supports(type)` | String | Check support for a mime type. |
| `destroy()` | - | Destroy the instance and garbage collect any elements. | | `destroy()` | - | Destroy the instance and garbage collect any elements. |
+5 -5
View File
@@ -8,7 +8,7 @@ import i18n from './i18n';
import support from './support'; import support from './support';
import browser from './utils/browser'; import browser from './utils/browser';
import { createElement, emptyElement, getAttributesFromSelector, insertAfter, removeElement, toggleClass } from './utils/elements'; import { createElement, emptyElement, getAttributesFromSelector, insertAfter, removeElement, toggleClass } from './utils/elements';
import { on, trigger } from './utils/events'; import { on, triggerEvent } from './utils/events';
import fetch from './utils/fetch'; import fetch from './utils/fetch';
import is from './utils/is'; import is from './utils/is';
import { getHTML } from './utils/strings'; import { getHTML } from './utils/strings';
@@ -82,7 +82,7 @@ const captions = {
// Watch changes to textTracks and update captions menu // Watch changes to textTracks and update captions menu
if (this.isHTML5) { if (this.isHTML5) {
const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack'; const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';
on(this.media.textTracks, trackEvents, captions.update.bind(this)); 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) // 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'; track.mode = 'hidden';
// Add event listener for cue changes // Add event listener for cue changes
on(track, 'cuechange', () => captions.updateCues.call(this)); on.call(this, track, 'cuechange', () => captions.updateCues.call(this));
}); });
} }
@@ -166,7 +166,7 @@ const captions = {
} }
// Trigger event // Trigger event
trigger.call(this, this.media, 'languagechange'); triggerEvent.call(this, this.media, 'languagechange');
} }
if (this.isHTML5 && this.isVideo) { if (this.isHTML5 && this.isVideo) {
@@ -280,7 +280,7 @@ const captions = {
this.elements.captions.appendChild(caption); this.elements.captions.appendChild(caption);
// Trigger event // Trigger event
trigger.call(this, this.media, 'cuechange'); triggerEvent.call(this, this.media, 'cuechange');
} }
}, },
}; };
+5 -7
View File
@@ -7,9 +7,10 @@ import html5 from './html5';
import i18n from './i18n'; import i18n from './i18n';
import support from './support'; import support from './support';
import { repaint, transitionEndEvent } from './utils/animation'; import { repaint, transitionEndEvent } from './utils/animation';
import { dedupe } from './utils/arrays';
import browser from './utils/browser'; import browser from './utils/browser';
import { createElement, emptyElement, getAttributesFromSelector, getElement, getElements, hasClass, removeElement, setAttributes, toggleClass, toggleHidden, toggleState } from './utils/elements'; import { createElement, emptyElement, getAttributesFromSelector, getElement, getElements, hasClass, removeElement, setAttributes, toggleClass, toggleHidden, toggleState } from './utils/elements';
import { off, on } from './utils/events'; import { once } from './utils/events';
import is from './utils/is'; import is from './utils/is';
import loadSprite from './utils/loadSprite'; import loadSprite from './utils/loadSprite';
import { extend } from './utils/objects'; import { extend } from './utils/objects';
@@ -634,7 +635,6 @@ const controls = {
}, },
// Set the quality menu // Set the quality menu
// TODO: Vimeo support
setQualityMenu(options) { setQualityMenu(options) {
// Menu required // Menu required
if (!is.element(this.elements.settings.panes.quality)) { if (!is.element(this.elements.settings.panes.quality)) {
@@ -644,9 +644,9 @@ const controls = {
const type = 'quality'; const type = 'quality';
const list = this.elements.settings.panes.quality.querySelector('ul'); 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 (is.array(options)) { if (is.array(options)) {
this.options.quality = options.filter(quality => this.config.quality.options.includes(quality)); this.options.quality = dedupe(options).filter(quality => this.config.quality.options.includes(quality));
} }
// Toggle the pane and tab // Toggle the pane and tab
@@ -1065,12 +1065,10 @@ const controls = {
container.style.width = ''; container.style.width = '';
container.style.height = ''; container.style.height = '';
// Only listen once
off(container, transitionEndEvent, restore);
}; };
// Listen for the transition finishing and restore auto height/width // Listen for the transition finishing and restore auto height/width
on(container, transitionEndEvent, restore); once(container, transitionEndEvent, restore);
// Set dimensions to target // Set dimensions to target
container.style.width = `${size.width}px`; container.style.width = `${size.width}px`;
+4 -4
View File
@@ -5,7 +5,7 @@
import browser from './utils/browser'; import browser from './utils/browser';
import { hasClass, toggleClass, toggleState, trapFocus } from './utils/elements'; import { hasClass, toggleClass, toggleState, trapFocus } from './utils/elements';
import { on, trigger } from './utils/events'; import { on, triggerEvent } from './utils/events';
import is from './utils/is'; import is from './utils/is';
function onChange() { function onChange() {
@@ -20,7 +20,7 @@ function onChange() {
} }
// Trigger an event // Trigger an event
trigger.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true); triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
// Trap focus in container // Trap focus in container
if (!browser.isIos) { if (!browser.isIos) {
@@ -63,13 +63,13 @@ class Fullscreen {
// Register event listeners // Register event listeners
// Handle event (incase user presses escape etc) // Handle event (incase user presses escape etc)
on(document, this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, () => { on.call(this.player, document, this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, () => {
// TODO: Filter for target?? // TODO: Filter for target??
onChange.call(this); onChange.call(this);
}); });
// Fullscreen toggle on double click // Fullscreen toggle on double click
on(this.player.elements.container, 'dblclick', event => { on.call(this.player, this.player.elements.container, 'dblclick', event => {
// Ignore double click in controls // Ignore double click in controls
if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) { if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {
return; return;
+23 -64
View File
@@ -3,43 +3,28 @@
// ========================================================================== // ==========================================================================
import support from './support'; import support from './support';
import { dedupe } from './utils/arrays';
import { removeElement } from './utils/elements'; import { removeElement } from './utils/elements';
import { trigger } from './utils/events'; import { triggerEvent } from './utils/events';
import is from './utils/is';
const html5 = { const html5 = {
getSources() { getSources() {
if (!this.isHTML5) { if (!this.isHTML5) {
return null; return [];
} }
return 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 // Get quality levels
getQualityOptions() { getQualityOptions() {
if (!this.isHTML5) { // Get sizes from <source> elements
return null; return html5.getSources
} .call(this)
.map(source => Number(source.getAttribute('size')))
// Get sources .filter(Boolean);
const sources = html5.getSources.call(this);
if (is.empty(sources)) {
return null;
}
// Get <source> with size attribute
const sizes = Array.from(sources).filter(source => !is.empty(source.getAttribute('size')));
// If none, bail
if (is.empty(sizes)) {
return null;
}
// Reduce to unique list
return dedupe(sizes.map(source => Number(source.getAttribute('size'))));
}, },
extend() { extend() {
@@ -54,60 +39,34 @@ const html5 = {
get() { get() {
// Get sources // Get sources
const sources = html5.getSources.call(player); const sources = html5.getSources.call(player);
const [source] = sources.filter(source => source.getAttribute('src') === player.source);
if (is.empty(sources)) { // Return size, if match is found
return null; return source && Number(source.getAttribute('size'));
}
const matches = Array.from(sources).filter(source => source.getAttribute('src') === player.source);
if (is.empty(matches)) {
return null;
}
return Number(matches[0].getAttribute('size'));
}, },
set(input) { set(input) {
// Get sources // Get sources
const sources = html5.getSources.call(player); const sources = html5.getSources.call(player);
if (is.empty(sources)) { // Get first match for requested size
const source = sources.find(source => Number(source.getAttribute('size')) === input);
// No matching source found
if (!source) {
return; return;
} }
// Get matches for requested size
const matches = Array.from(sources).filter(source => Number(source.getAttribute('size')) === input);
// No matches for requested size
if (is.empty(matches)) {
return;
}
// Get supported sources
const supported = matches.filter(source => support.mime.call(player, source.getAttribute('type')));
// No supported sources
if (is.empty(supported)) {
return;
}
// Trigger change event
trigger.call(player, player.media, 'qualityrequested', false, {
quality: input,
});
// Get current state // Get current state
const { currentTime, playing } = player; const { currentTime, playing } = player;
// Set new source // Set new source
player.media.src = supported[0].getAttribute('src'); player.media.src = source.getAttribute('src');
// Restore time // Restore time
const onLoadedMetaData = () => { const onLoadedMetaData = () => {
player.currentTime = currentTime; player.currentTime = currentTime;
player.off('loadedmetadata', onLoadedMetaData);
}; };
player.on('loadedmetadata', onLoadedMetaData); player.once('loadedmetadata', onLoadedMetaData);
// Load new source // Load new source
player.media.load(); player.media.load();
@@ -118,7 +77,7 @@ const html5 = {
} }
// Trigger change event // Trigger change event
trigger.call(player, player.media, 'qualitychange', false, { triggerEvent.call(player, player.media, 'qualitychange', false, {
quality: input, quality: input,
}); });
}, },
@@ -133,7 +92,7 @@ const html5 = {
} }
// Remove child sources // Remove child sources
removeElement(html5.getSources()); removeElement(html5.getSources.call(this));
// Set blank video src attribute // Set blank video src attribute
// This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
+28 -36
View File
@@ -6,7 +6,7 @@ import controls from './controls';
import ui from './ui'; import ui from './ui';
import browser from './utils/browser'; import browser from './utils/browser';
import { getElement, getElements, getFocusElement, matches, toggleClass, toggleHidden } from './utils/elements'; import { getElement, getElements, getFocusElement, matches, toggleClass, toggleHidden } from './utils/elements';
import { off, on, toggleListener, trigger } from './utils/events'; import { on, once, toggleListener, triggerEvent } from './utils/events';
import is from './utils/is'; import is from './utils/is';
class Listeners { class Listeners {
@@ -197,39 +197,36 @@ class Listeners {
// Add touch class // Add touch class
toggleClass(this.player.elements.container, this.player.config.classNames.isTouch, true); toggleClass(this.player.elements.container, this.player.config.classNames.isTouch, true);
// Clean up
off(document.body, 'touchstart', this.firstTouch);
} }
// Global window & document listeners // Global window & document listeners
global(toggle = true) { global(toggle = true) {
// Keyboard shortcuts // Keyboard shortcuts
if (this.player.config.keyboard.global) { if (this.player.config.keyboard.global) {
toggleListener(window, 'keydown keyup', this.handleKey, toggle, false); toggleListener.call(this.player, window, 'keydown keyup', this.handleKey, toggle, false);
} }
// Click anywhere closes menu // Click anywhere closes menu
toggleListener(document.body, 'click', this.toggleMenu, toggle); toggleListener.call(this.player, document.body, 'click', this.toggleMenu, toggle);
// Detect touch by events // Detect touch by events
on(document.body, 'touchstart', this.firstTouch); once(document.body, 'touchstart', this.firstTouch);
} }
// Container listeners // Container listeners
container() { container() {
// Keyboard shortcuts // Keyboard shortcuts
if (!this.player.config.keyboard.global && this.player.config.keyboard.focused) { if (!this.player.config.keyboard.global && this.player.config.keyboard.focused) {
on(this.player.elements.container, 'keydown keyup', this.handleKey, false); on.call(this.player, this.player.elements.container, 'keydown keyup', this.handleKey, false);
} }
// Detect tab focus // Detect tab focus
// Remove class on blur/focusout // Remove class on blur/focusout
on(this.player.elements.container, 'focusout', event => { on.call(this.player, this.player.elements.container, 'focusout', event => {
toggleClass(event.target, this.player.config.classNames.tabFocus, false); toggleClass(event.target, this.player.config.classNames.tabFocus, false);
}); });
// Add classname to tabbed elements // Add classname to tabbed elements
on(this.player.elements.container, 'keydown', event => { on.call(this.player, this.player.elements.container, 'keydown', event => {
if (event.keyCode !== 9) { if (event.keyCode !== 9) {
return; return;
} }
@@ -242,7 +239,7 @@ class Listeners {
}); });
// Toggle controls on mouse events and entering fullscreen // Toggle controls on mouse events and entering fullscreen
on(this.player.elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => { on.call(this.player, this.player.elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => {
const { controls } = this.player.elements; const { controls } = this.player.elements;
// Remove button states for fullscreen // Remove button states for fullscreen
@@ -276,20 +273,20 @@ class Listeners {
// Listen for media events // Listen for media events
media() { media() {
// Time change on media // Time change on media
on(this.player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(this.player, event)); on.call(this.player, this.player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(this.player, event));
// Display duration // Display duration
on(this.player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(this.player, event)); on.call(this.player, this.player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(this.player, event));
// Check for audio tracks on load // Check for audio tracks on load
// We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point // We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point
on(this.player.media, 'loadeddata canplay', () => { on.call(this.player, this.player.media, 'canplay', () => {
toggleHidden(this.player.elements.volume, !this.player.hasAudio); toggleHidden(this.player.elements.volume, !this.player.hasAudio);
toggleHidden(this.player.elements.buttons.mute, !this.player.hasAudio); toggleHidden(this.player.elements.buttons.mute, !this.player.hasAudio);
}); });
// Handle the media finishing // Handle the media finishing
on(this.player.media, 'ended', () => { on.call(this.player, this.player.media, 'ended', () => {
// Show poster on end // Show poster on end
if (this.player.isHTML5 && this.player.isVideo && this.player.config.resetOnEnd) { if (this.player.isHTML5 && this.player.isVideo && this.player.config.resetOnEnd) {
// Restart // Restart
@@ -298,20 +295,20 @@ class Listeners {
}); });
// Check for buffer progress // Check for buffer progress
on(this.player.media, 'progress playing seeking seeked', event => controls.updateProgress.call(this.player, event)); on.call(this.player, this.player.media, 'progress playing seeking seeked', event => controls.updateProgress.call(this.player, event));
// Handle volume changes // Handle volume changes
on(this.player.media, 'volumechange', event => controls.updateVolume.call(this.player, event)); on.call(this.player, this.player.media, 'volumechange', event => controls.updateVolume.call(this.player, event));
// Handle play/pause // Handle play/pause
on(this.player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(this.player, event)); on.call(this.player, this.player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(this.player, event));
// Loading state // Loading state
on(this.player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(this.player, event)); 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 // 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 // TODO: Show some sort of loading state while the ad manager loads else there's a delay before ad shows
on(this.player.media, 'playing', () => { on.call(this.player, this.player.media, 'playing', () => {
if (!this.player.ads) { if (!this.player.ads) {
return; return;
} }
@@ -334,7 +331,7 @@ class Listeners {
} }
// On click play, pause ore restart // On click play, pause ore restart
on(wrapper, 'click', () => { on.call(this.player, wrapper, 'click', () => {
// Touch devices will just show controls (if we're hiding controls) // Touch devices will just show controls (if we're hiding controls)
if (this.player.config.hideControls && this.player.touch && !this.player.paused) { if (this.player.config.hideControls && this.player.touch && !this.player.paused) {
return; return;
@@ -353,7 +350,7 @@ class Listeners {
// Disable right click // Disable right click
if (this.player.supported.ui && this.player.config.disableContextMenu) { if (this.player.supported.ui && this.player.config.disableContextMenu) {
on( on.call(this.player,
this.player.elements.wrapper, this.player.elements.wrapper,
'contextmenu', 'contextmenu',
event => { event => {
@@ -364,13 +361,13 @@ class Listeners {
} }
// Volume change // Volume change
on(this.player.media, 'volumechange', () => { on.call(this.player, this.player.media, 'volumechange', () => {
// Save to storage // Save to storage
this.player.storage.set({ volume: this.player.volume, muted: this.player.muted }); this.player.storage.set({ volume: this.player.volume, muted: this.player.muted });
}); });
// Speed change // Speed change
on(this.player.media, 'ratechange', () => { on.call(this.player, this.player.media, 'ratechange', () => {
// Update UI // Update UI
controls.updateSetting.call(this.player, 'speed'); controls.updateSetting.call(this.player, 'speed');
@@ -379,19 +376,19 @@ class Listeners {
}); });
// Quality request // Quality request
on(this.player.media, 'qualityrequested', event => { on.call(this.player, this.player.media, 'qualityrequested', event => {
// Save to storage // Save to storage
this.player.storage.set({ quality: event.detail.quality }); this.player.storage.set({ quality: event.detail.quality });
}); });
// Quality change // Quality change
on(this.player.media, 'qualitychange', event => { on.call(this.player, this.player.media, 'qualitychange', event => {
// Update UI // Update UI
controls.updateSetting.call(this.player, 'quality', null, event.detail.quality); controls.updateSetting.call(this.player, 'quality', null, event.detail.quality);
}); });
// Caption language change // Caption language change
on(this.player.media, 'languagechange', () => { on.call(this.player, this.player.media, 'languagechange', () => {
// Update UI // Update UI
controls.updateSetting.call(this.player, 'captions'); controls.updateSetting.call(this.player, 'captions');
@@ -400,7 +397,7 @@ class Listeners {
}); });
// Captions toggle // Captions toggle
on(this.player.media, 'captionsenabled captionsdisabled', () => { on.call(this.player, this.player.media, 'captionsenabled captionsdisabled', () => {
// Update UI // Update UI
controls.updateSetting.call(this.player, 'captions'); controls.updateSetting.call(this.player, 'captions');
@@ -410,7 +407,7 @@ class Listeners {
// Proxy events to container // Proxy events to container
// Bubble up key events for Edge // Bubble up key events for Edge
on(this.player.media, this.player.config.events.concat([ on.call(this.player, this.player.media, this.player.config.events.concat([
'keyup', 'keyup',
'keydown', 'keydown',
]).join(' '), event => { ]).join(' '), event => {
@@ -421,7 +418,7 @@ class Listeners {
detail = this.player.media.error; detail = this.player.media.error;
} }
trigger.call(this.player, this.player.elements.container, event.type, true, detail); triggerEvent.call(this.player, this.player.elements.container, event.type, true, detail);
}); });
} }
@@ -452,7 +449,7 @@ class Listeners {
const customHandler = this.player.config.listeners[customHandlerKey]; const customHandler = this.player.config.listeners[customHandlerKey];
const hasCustomHandler = is.function(customHandler); const hasCustomHandler = is.function(customHandler);
on(element, type, event => proxy(event, defaultHandler, customHandlerKey), passive && !hasCustomHandler); on.call(this.player, element, type, event => proxy(event, defaultHandler, customHandlerKey), passive && !hasCustomHandler);
}; };
// Play/pause toggle // Play/pause toggle
@@ -727,11 +724,6 @@ class Listeners {
false, false,
); );
} }
// Reset on destroy
clear() {
this.global(false);
}
} }
export default Listeners; export default Listeners;
+2 -2
View File
@@ -8,7 +8,7 @@
import i18n from '../i18n'; import i18n from '../i18n';
import { createElement } from './../utils/elements'; import { createElement } from './../utils/elements';
import { trigger } from './../utils/events'; import { triggerEvent } from './../utils/events';
import is from './../utils/is'; import is from './../utils/is';
import loadScript from './../utils/loadScript'; import loadScript from './../utils/loadScript';
import { formatTime } from './../utils/time'; import { formatTime } from './../utils/time';
@@ -270,7 +270,7 @@ class Ads {
// Proxy event // Proxy event
const dispatchEvent = type => { const dispatchEvent = type => {
const event = `ads${type.replace(/_/g, '').toLowerCase()}`; const event = `ads${type.replace(/_/g, '').toLowerCase()}`;
trigger.call(this.player, this.player.media, event); triggerEvent.call(this.player, this.player.media, event);
}; };
switch (event.type) { switch (event.type) {
+17 -17
View File
@@ -6,7 +6,7 @@ import captions from './../captions';
import controls from './../controls'; import controls from './../controls';
import ui from './../ui'; import ui from './../ui';
import { createElement, replaceElement, toggleClass } from './../utils/elements'; import { createElement, replaceElement, toggleClass } from './../utils/elements';
import { trigger } from './../utils/events'; import { triggerEvent } from './../utils/events';
import fetch from './../utils/fetch'; import fetch from './../utils/fetch';
import is from './../utils/is'; import is from './../utils/is';
import loadScript from './../utils/loadScript'; import loadScript from './../utils/loadScript';
@@ -41,7 +41,7 @@ function assurePlaybackState(play) {
} }
if (this.media.paused === play) { if (this.media.paused === play) {
this.media.paused = !play; this.media.paused = !play;
trigger.call(this, this.media, play ? 'play' : 'pause'); triggerEvent.call(this, this.media, play ? 'play' : 'pause');
} }
} }
@@ -186,7 +186,7 @@ const vimeo = {
// Set seeking state and trigger event // Set seeking state and trigger event
media.seeking = true; media.seeking = true;
trigger.call(player, media, 'seeking'); triggerEvent.call(player, media, 'seeking');
// If paused, mute until seek is complete // If paused, mute until seek is complete
Promise.resolve(restorePause && embed.setVolume(0)) Promise.resolve(restorePause && embed.setVolume(0))
@@ -213,7 +213,7 @@ const vimeo = {
.setPlaybackRate(input) .setPlaybackRate(input)
.then(() => { .then(() => {
speed = input; speed = input;
trigger.call(player, player.media, 'ratechange'); triggerEvent.call(player, player.media, 'ratechange');
}) })
.catch(error => { .catch(error => {
// Hide menu item (and menu if empty) // Hide menu item (and menu if empty)
@@ -233,7 +233,7 @@ const vimeo = {
set(input) { set(input) {
player.embed.setVolume(input).then(() => { player.embed.setVolume(input).then(() => {
volume = input; volume = input;
trigger.call(player, player.media, 'volumechange'); triggerEvent.call(player, player.media, 'volumechange');
}); });
}, },
}); });
@@ -249,7 +249,7 @@ const vimeo = {
player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => { player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => {
muted = toggle; muted = toggle;
trigger.call(player, player.media, 'volumechange'); triggerEvent.call(player, player.media, 'volumechange');
}); });
}, },
}); });
@@ -316,13 +316,13 @@ const vimeo = {
// Get current time // Get current time
player.embed.getCurrentTime().then(value => { player.embed.getCurrentTime().then(value => {
currentTime = value; currentTime = value;
trigger.call(player, player.media, 'timeupdate'); triggerEvent.call(player, player.media, 'timeupdate');
}); });
// Get duration // Get duration
player.embed.getDuration().then(value => { player.embed.getDuration().then(value => {
player.media.duration = value; player.media.duration = value;
trigger.call(player, player.media, 'durationchange'); triggerEvent.call(player, player.media, 'durationchange');
}); });
// Get captions // Get captions
@@ -341,7 +341,7 @@ const vimeo = {
player.embed.getPaused().then(paused => { player.embed.getPaused().then(paused => {
assurePlaybackState.call(player, !paused); assurePlaybackState.call(player, !paused);
if (!paused) { if (!paused) {
trigger.call(player, player.media, 'playing'); triggerEvent.call(player, player.media, 'playing');
} }
}); });
@@ -356,7 +356,7 @@ const vimeo = {
player.embed.on('play', () => { player.embed.on('play', () => {
assurePlaybackState.call(player, true); assurePlaybackState.call(player, true);
trigger.call(player, player.media, 'playing'); triggerEvent.call(player, player.media, 'playing');
}); });
player.embed.on('pause', () => { player.embed.on('pause', () => {
@@ -366,16 +366,16 @@ const vimeo = {
player.embed.on('timeupdate', data => { player.embed.on('timeupdate', data => {
player.media.seeking = false; player.media.seeking = false;
currentTime = data.seconds; currentTime = data.seconds;
trigger.call(player, player.media, 'timeupdate'); triggerEvent.call(player, player.media, 'timeupdate');
}); });
player.embed.on('progress', data => { player.embed.on('progress', data => {
player.media.buffered = data.percent; player.media.buffered = data.percent;
trigger.call(player, player.media, 'progress'); triggerEvent.call(player, player.media, 'progress');
// Check all loaded // Check all loaded
if (parseInt(data.percent, 10) === 1) { if (parseInt(data.percent, 10) === 1) {
trigger.call(player, player.media, 'canplaythrough'); triggerEvent.call(player, player.media, 'canplaythrough');
} }
// Get duration as if we do it before load, it gives an incorrect value // Get duration as if we do it before load, it gives an incorrect value
@@ -383,24 +383,24 @@ const vimeo = {
player.embed.getDuration().then(value => { player.embed.getDuration().then(value => {
if (value !== player.media.duration) { if (value !== player.media.duration) {
player.media.duration = value; player.media.duration = value;
trigger.call(player, player.media, 'durationchange'); triggerEvent.call(player, player.media, 'durationchange');
} }
}); });
}); });
player.embed.on('seeked', () => { player.embed.on('seeked', () => {
player.media.seeking = false; player.media.seeking = false;
trigger.call(player, player.media, 'seeked'); triggerEvent.call(player, player.media, 'seeked');
}); });
player.embed.on('ended', () => { player.embed.on('ended', () => {
player.media.paused = true; player.media.paused = true;
trigger.call(player, player.media, 'ended'); triggerEvent.call(player, player.media, 'ended');
}); });
player.embed.on('error', detail => { player.embed.on('error', detail => {
player.media.error = detail; player.media.error = detail;
trigger.call(player, player.media, 'error'); triggerEvent.call(player, player.media, 'error');
}); });
// Rebuild UI // Rebuild UI
+37 -72
View File
@@ -6,7 +6,7 @@ import controls from './../controls';
import ui from './../ui'; import ui from './../ui';
import { dedupe } from './../utils/arrays'; import { dedupe } from './../utils/arrays';
import { createElement, replaceElement, toggleClass } from './../utils/elements'; import { createElement, replaceElement, toggleClass } from './../utils/elements';
import { trigger } from './../utils/events'; import { triggerEvent } from './../utils/events';
import fetch from './../utils/fetch'; import fetch from './../utils/fetch';
import is from './../utils/is'; import is from './../utils/is';
import loadImage from './../utils/loadImage'; import loadImage from './../utils/loadImage';
@@ -25,52 +25,25 @@ function parseId(url) {
// Standardise YouTube quality unit // Standardise YouTube quality unit
function mapQualityUnit(input) { function mapQualityUnit(input) {
switch (input) { const qualities = {
case 'hd2160': hd2160: 2160,
return 2160; hd1440: 1440,
hd1080: 1080,
hd720: 720,
large: 480,
medium: 360,
small: 240,
tiny: 144,
};
case 2160: const entry = Object.entries(qualities).find(entry => entry.includes(input));
return 'hd2160';
case 'hd1440': if (entry) {
return 1440; // Get the match corresponding to the input
return entry.find(value => value !== input);
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';
} }
return 'default';
} }
function mapQualityUnits(levels) { function mapQualityUnits(levels) {
@@ -88,7 +61,7 @@ function assurePlaybackState(play) {
} }
if (this.media.paused === play) { if (this.media.paused === play) {
this.media.paused = !play; this.media.paused = !play;
trigger.call(this, this.media, play ? 'play' : 'pause'); triggerEvent.call(this, this.media, play ? 'play' : 'pause');
} }
} }
@@ -266,10 +239,10 @@ const youtube = {
player.media.error = detail; player.media.error = detail;
trigger.call(player, player.media, 'error'); triggerEvent.call(player, player.media, 'error');
}, },
onPlaybackQualityChange() { onPlaybackQualityChange() {
trigger.call(player, player.media, 'qualitychange', false, { triggerEvent.call(player, player.media, 'qualitychange', false, {
quality: player.media.quality, quality: player.media.quality,
}); });
}, },
@@ -280,7 +253,7 @@ const youtube = {
// Get current speed // Get current speed
player.media.playbackRate = instance.getPlaybackRate(); player.media.playbackRate = instance.getPlaybackRate();
trigger.call(player, player.media, 'ratechange'); triggerEvent.call(player, player.media, 'ratechange');
}, },
onReady(event) { onReady(event) {
// Get the instance // Get the instance
@@ -321,7 +294,7 @@ const youtube = {
// Set seeking state and trigger event // Set seeking state and trigger event
player.media.seeking = true; player.media.seeking = true;
trigger.call(player, player.media, 'seeking'); triggerEvent.call(player, player.media, 'seeking');
// Seek after events sent // Seek after events sent
instance.seekTo(time); instance.seekTo(time);
@@ -344,15 +317,7 @@ const youtube = {
return mapQualityUnit(instance.getPlaybackQuality()); return mapQualityUnit(instance.getPlaybackQuality());
}, },
set(input) { set(input) {
const quality = input; instance.setPlaybackQuality(mapQualityUnit(input));
// Set via API
instance.setPlaybackQuality(mapQualityUnit(quality));
// Trigger request event
trigger.call(player, player.media, 'qualityrequested', false, {
quality,
});
}, },
}); });
@@ -365,7 +330,7 @@ const youtube = {
set(input) { set(input) {
volume = input; volume = input;
instance.setVolume(volume * 100); instance.setVolume(volume * 100);
trigger.call(player, player.media, 'volumechange'); triggerEvent.call(player, player.media, 'volumechange');
}, },
}); });
@@ -379,7 +344,7 @@ const youtube = {
const toggle = is.boolean(input) ? input : muted; const toggle = is.boolean(input) ? input : muted;
muted = toggle; muted = toggle;
instance[toggle ? 'mute' : 'unMute'](); instance[toggle ? 'mute' : 'unMute']();
trigger.call(player, player.media, 'volumechange'); triggerEvent.call(player, player.media, 'volumechange');
}, },
}); });
@@ -405,8 +370,8 @@ const youtube = {
player.media.setAttribute('tabindex', -1); player.media.setAttribute('tabindex', -1);
} }
trigger.call(player, player.media, 'timeupdate'); triggerEvent.call(player, player.media, 'timeupdate');
trigger.call(player, player.media, 'durationchange'); triggerEvent.call(player, player.media, 'durationchange');
// Reset timer // Reset timer
clearInterval(player.timers.buffering); clearInterval(player.timers.buffering);
@@ -418,7 +383,7 @@ const youtube = {
// Trigger progress only when we actually buffer something // Trigger progress only when we actually buffer something
if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) { if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {
trigger.call(player, player.media, 'progress'); triggerEvent.call(player, player.media, 'progress');
} }
// Set last buffer point // Set last buffer point
@@ -429,7 +394,7 @@ const youtube = {
clearInterval(player.timers.buffering); clearInterval(player.timers.buffering);
// Trigger event // Trigger event
trigger.call(player, player.media, 'canplaythrough'); triggerEvent.call(player, player.media, 'canplaythrough');
} }
}, 200); }, 200);
@@ -451,7 +416,7 @@ const youtube = {
if (seeked) { if (seeked) {
// Unset seeking and fire seeked event // Unset seeking and fire seeked event
player.media.seeking = false; player.media.seeking = false;
trigger.call(player, player.media, 'seeked'); triggerEvent.call(player, player.media, 'seeked');
} }
// Handle events // Handle events
@@ -464,11 +429,11 @@ const youtube = {
switch (event.data) { switch (event.data) {
case -1: case -1:
// Update scrubber // Update scrubber
trigger.call(player, player.media, 'timeupdate'); triggerEvent.call(player, player.media, 'timeupdate');
// Get loaded % from YouTube // Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction(); player.media.buffered = instance.getVideoLoadedFraction();
trigger.call(player, player.media, 'progress'); triggerEvent.call(player, player.media, 'progress');
break; break;
@@ -481,7 +446,7 @@ const youtube = {
instance.stopVideo(); instance.stopVideo();
instance.playVideo(); instance.playVideo();
} else { } else {
trigger.call(player, player.media, 'ended'); triggerEvent.call(player, player.media, 'ended');
} }
break; break;
@@ -493,11 +458,11 @@ const youtube = {
} else { } else {
assurePlaybackState.call(player, true); assurePlaybackState.call(player, true);
trigger.call(player, player.media, 'playing'); triggerEvent.call(player, player.media, 'playing');
// Poll to get playback progress // Poll to get playback progress
player.timers.playing = setInterval(() => { player.timers.playing = setInterval(() => {
trigger.call(player, player.media, 'timeupdate'); triggerEvent.call(player, player.media, 'timeupdate');
}, 50); }, 50);
// Check duration again due to YouTube bug // Check duration again due to YouTube bug
@@ -505,7 +470,7 @@ const youtube = {
// https://code.google.com/p/gdata-issues/issues/detail?id=8690 // https://code.google.com/p/gdata-issues/issues/detail?id=8690
if (player.media.duration !== instance.getDuration()) { if (player.media.duration !== instance.getDuration()) {
player.media.duration = instance.getDuration(); player.media.duration = instance.getDuration();
trigger.call(player, player.media, 'durationchange'); triggerEvent.call(player, player.media, 'durationchange');
} }
// Get quality // Get quality
@@ -527,7 +492,7 @@ const youtube = {
break; break;
} }
trigger.call(player, player.elements.container, 'statechange', false, { triggerEvent.call(player, player.elements.container, 'statechange', false, {
code: event.data, code: event.data,
}); });
}, },
+35 -31
View File
@@ -20,9 +20,9 @@ import support from './support';
import ui from './ui'; import ui from './ui';
import { closest } from './utils/arrays'; import { closest } from './utils/arrays';
import { createElement, hasClass, removeElement, replaceElement, toggleClass, toggleState, wrap } from './utils/elements'; import { createElement, hasClass, removeElement, replaceElement, toggleClass, toggleState, wrap } from './utils/elements';
import { off, on, trigger } from './utils/events'; import { off, on, once, triggerEvent, unbindListeners } from './utils/events';
import is from './utils/is'; import is from './utils/is';
import loadSprite from './utils/loadScript'; import loadSprite from './utils/loadSprite';
import { cloneDeep, extend } from './utils/objects'; import { cloneDeep, extend } from './utils/objects';
import { parseUrl } from './utils/urls'; import { parseUrl } from './utils/urls';
@@ -171,7 +171,7 @@ class Plyr {
this.elements.container.className = ''; this.elements.container.className = '';
// Get attributes from URL and set config // Get attributes from URL and set config
if (!url.searchParams) { if (url.searchParams.length) {
const truthy = [ const truthy = [
'1', '1',
'true', 'true',
@@ -249,6 +249,8 @@ class Plyr {
return; return;
} }
this.eventListeners = [];
// Create listeners // Create listeners
this.listeners = new Listeners(this); this.listeners = new Listeners(this);
@@ -275,7 +277,7 @@ class Plyr {
// Listen for events if debugging // Listen for events if debugging
if (this.config.debug) { if (this.config.debug) {
on(this.elements.container, this.config.events.join(' '), event => { on.call(this, this.elements.container, this.config.events.join(' '), event => {
this.debug.log(`event: ${event.type}`); this.debug.log(`event: ${event.type}`);
}); });
} }
@@ -673,36 +675,31 @@ class Plyr {
* @param {number} input - Quality level * @param {number} input - Quality level
*/ */
set quality(input) { set quality(input) {
let quality = null; const config = this.config.quality;
const options = this.options.quality;
if (!is.empty(input)) { if (!options.length) {
quality = Number(input);
}
if (!is.number(quality)) {
quality = this.storage.get('quality');
}
if (!is.number(quality)) {
quality = this.config.quality.selected;
}
if (!is.number(quality)) {
quality = this.config.quality.default;
}
if (!this.options.quality.length) {
return; return;
} }
if (!this.options.quality.includes(quality)) { let quality = ([
const value = closest(this.options.quality, quality); !is.empty(input) && Number(input),
this.storage.get('quality'),
config.selected,
config.default,
]).find(is.number);
if (!options.includes(quality)) {
const value = closest(options, quality);
this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`); this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`);
quality = value; quality = value;
} }
// Trigger request event
triggerEvent.call(this, this.media, 'qualityrequested', false, { quality });
// Update config // Update config
this.config.quality.selected = quality; config.selected = quality;
// Set quality // Set quality
this.media.quality = quality; this.media.quality = quality;
@@ -853,7 +850,7 @@ class Plyr {
// Update state and trigger event // Update state and trigger event
if (active !== this.captions.active) { if (active !== this.captions.active) {
this.captions.active = active; this.captions.active = active;
trigger.call(this, this.media, this.captions.active ? 'captionsenabled' : 'captionsdisabled'); triggerEvent.call(this, this.media, this.captions.active ? 'captionsenabled' : 'captionsdisabled');
} }
} }
@@ -957,7 +954,7 @@ class Plyr {
// Trigger event on change // Trigger event on change
if (hiding !== isHidden) { if (hiding !== isHidden) {
const eventName = hiding ? 'controlshidden' : 'controlsshown'; const eventName = hiding ? 'controlshidden' : 'controlsshown';
trigger.call(this, this.media, eventName); triggerEvent.call(this, this.media, eventName);
} }
return !hiding; return !hiding;
} }
@@ -970,9 +967,16 @@ class Plyr {
* @param {function} callback - Callback for when event occurs * @param {function} callback - Callback for when event occurs
*/ */
on(event, callback) { on(event, callback) {
on(this.elements.container, event, callback); 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) {
once(this.elements.container, event, callback);
} }
/** /**
* Remove event listeners * Remove event listeners
* @param {string} event - Event type * @param {string} event - Event type
@@ -1023,13 +1027,13 @@ class Plyr {
} }
} else { } else {
// Unbind listeners // Unbind listeners
this.listeners.clear(); unbindListeners.call(this);
// Replace the container with the original element provided // Replace the container with the original element provided
replaceElement(this.elements.original, this.elements.container); replaceElement(this.elements.original, this.elements.container);
// Event // Event
trigger.call(this, this.elements.original, 'destroyed', true); triggerEvent.call(this, this.elements.original, 'destroyed', true);
// Callback // Callback
if (is.function(callback)) { if (is.function(callback)) {
+2 -2
View File
@@ -8,7 +8,7 @@ import i18n from './i18n';
import support from './support'; import support from './support';
import browser from './utils/browser'; import browser from './utils/browser';
import { getElement, toggleClass, toggleState } from './utils/elements'; import { getElement, toggleClass, toggleState } from './utils/elements';
import { trigger } from './utils/events'; import { triggerEvent } from './utils/events';
import is from './utils/is'; import is from './utils/is';
import loadImage from './utils/loadImage'; import loadImage from './utils/loadImage';
@@ -102,7 +102,7 @@ const ui = {
// Ready event at end of execution stack // Ready event at end of execution stack
setTimeout(() => { setTimeout(() => {
trigger.call(this, this.media, 'ready'); triggerEvent.call(this, this.media, 'ready');
}, 0); }, 0);
// Set the title // Set the title
+31 -4
View File
@@ -27,7 +27,7 @@ const supportsPassiveListeners = (() => {
})(); })();
// Toggle event listener // Toggle event listener
export function toggleListener(elements, event, callback, toggle = false, passive = true, capture = false) { export function toggleListener(elements, event, callback, toggle = false, passive = true, capture = false, once = false) {
// Bail if no elemetns, event, or callback // Bail if no elemetns, event, or callback
if (is.empty(elements) || is.empty(event) || !is.function(callback)) { if (is.empty(elements) || is.empty(event) || !is.function(callback)) {
return; return;
@@ -64,22 +64,37 @@ export function toggleListener(elements, event, callback, toggle = false, passiv
// If a single node is passed, bind the event listener // If a single node is passed, bind the event listener
events.forEach(type => { 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); elements[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);
}); });
} }
// Bind event handler // Bind event handler
export function on(element, events = '', callback, passive = true, capture = false) { export function on(element, events = '', callback, passive = true, capture = false) {
toggleListener(element, events, callback, true, passive, capture); toggleListener.call(this, element, events, callback, true, passive, capture);
} }
// Unbind event handler // Unbind event handler
export function off(element, events = '', callback, passive = true, capture = false) { export function off(element, events = '', callback, passive = true, capture = false) {
toggleListener(element, events, callback, false, passive, capture); toggleListener.call(this, element, events, callback, false, passive, capture);
}
// Bind once-only event handler
export function once(element, events = '', callback, passive = true, capture = false) {
function onceCallback(...args) {
off(element, events, onceCallback, passive, capture);
callback.apply(this, args);
}
toggleListener(element, events, onceCallback, true, passive, capture, true);
} }
// Trigger event // Trigger event
export function trigger(element, type = '', bubbles = false, detail = {}) { export function triggerEvent(element, type = '', bubbles = false, detail = {}) {
// Bail if no element // Bail if no element
if (!is.element(element) || is.empty(type)) { if (!is.element(element) || is.empty(type)) {
return; return;
@@ -96,3 +111,15 @@ export function trigger(element, type = '', bubbles = false, detail = {}) {
// Dispatch the event // Dispatch the event
element.dispatchEvent(event); element.dispatchEvent(event);
} }
// Unbind all cached event listeners
export function unbindListeners() {
if (this && this.eventListeners) {
this.eventListeners.forEach(item => {
const { elements, type, callback, options } = item;
elements.removeEventListener(type, callback, options);
});
this.eventListeners = [];
}
}