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
commit aae1092bac
20 changed files with 667 additions and 797 deletions

495
dist/plyr.js vendored

File diff suppressed because it is too large Load Diff

2
dist/plyr.js.map vendored

File diff suppressed because one or more lines are too long

2
dist/plyr.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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. |

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');
} }
}, },
}; };

12
src/js/controls.js vendored
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`;

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;

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

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;

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) {

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

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,
}); });
}, },

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)) {

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

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 = [];
}
}