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 [`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
_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. |
| `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. |
| `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. |
| `supports(type)` | String | Check support for a mime type. |
| `destroy()` | - | Destroy the instance and garbage collect any elements. |

View File

@ -8,7 +8,7 @@ import i18n from './i18n';
import support from './support';
import browser from './utils/browser';
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 is from './utils/is';
import { getHTML } from './utils/strings';
@ -82,7 +82,7 @@ const captions = {
// Watch changes to textTracks and update captions menu
if (this.isHTML5) {
const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';
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)
@ -107,7 +107,7 @@ const captions = {
track.mode = 'hidden';
// 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.call(this, this.media, 'languagechange');
triggerEvent.call(this, this.media, 'languagechange');
}
if (this.isHTML5 && this.isVideo) {
@ -280,7 +280,7 @@ const captions = {
this.elements.captions.appendChild(caption);
// 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 support from './support';
import { repaint, transitionEndEvent } from './utils/animation';
import { dedupe } from './utils/arrays';
import browser from './utils/browser';
import { createElement, emptyElement, getAttributesFromSelector, getElement, getElements, hasClass, removeElement, setAttributes, toggleClass, toggleHidden, toggleState } from './utils/elements';
import { off, on } from './utils/events';
import { once } from './utils/events';
import is from './utils/is';
import loadSprite from './utils/loadSprite';
import { extend } from './utils/objects';
@ -634,7 +635,6 @@ const controls = {
},
// Set the quality menu
// TODO: Vimeo support
setQualityMenu(options) {
// Menu required
if (!is.element(this.elements.settings.panes.quality)) {
@ -644,9 +644,9 @@ const controls = {
const type = 'quality';
const list = this.elements.settings.panes.quality.querySelector('ul');
// Set options if passed and filter based on config
// Set options if passed and filter based on uniqueness and config
if (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
@ -1065,12 +1065,10 @@ const controls = {
container.style.width = '';
container.style.height = '';
// Only listen once
off(container, transitionEndEvent, restore);
};
// Listen for the transition finishing and restore auto height/width
on(container, transitionEndEvent, restore);
once(container, transitionEndEvent, restore);
// Set dimensions to target
container.style.width = `${size.width}px`;

View File

@ -5,7 +5,7 @@
import browser from './utils/browser';
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';
function onChange() {
@ -20,7 +20,7 @@ function onChange() {
}
// 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
if (!browser.isIos) {
@ -63,13 +63,13 @@ class Fullscreen {
// Register event listeners
// 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??
onChange.call(this);
});
// 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
if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {
return;

View File

@ -3,43 +3,28 @@
// ==========================================================================
import support from './support';
import { dedupe } from './utils/arrays';
import { removeElement } from './utils/elements';
import { trigger } from './utils/events';
import is from './utils/is';
import { triggerEvent } from './utils/events';
const html5 = {
getSources() {
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
getQualityOptions() {
if (!this.isHTML5) {
return null;
}
// Get sources
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'))));
// Get sizes from <source> elements
return html5.getSources
.call(this)
.map(source => Number(source.getAttribute('size')))
.filter(Boolean);
},
extend() {
@ -54,60 +39,34 @@ const html5 = {
get() {
// Get sources
const sources = html5.getSources.call(player);
const [source] = sources.filter(source => source.getAttribute('src') === player.source);
if (is.empty(sources)) {
return null;
}
const matches = Array.from(sources).filter(source => source.getAttribute('src') === player.source);
if (is.empty(matches)) {
return null;
}
return Number(matches[0].getAttribute('size'));
// Return size, if match is found
return source && Number(source.getAttribute('size'));
},
set(input) {
// Get sources
const sources = html5.getSources.call(player);
if (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;
}
// 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
const { currentTime, playing } = player;
// Set new source
player.media.src = supported[0].getAttribute('src');
player.media.src = source.getAttribute('src');
// Restore time
const onLoadedMetaData = () => {
player.currentTime = currentTime;
player.off('loadedmetadata', onLoadedMetaData);
};
player.on('loadedmetadata', onLoadedMetaData);
player.once('loadedmetadata', onLoadedMetaData);
// Load new source
player.media.load();
@ -118,7 +77,7 @@ const html5 = {
}
// Trigger change event
trigger.call(player, player.media, 'qualitychange', false, {
triggerEvent.call(player, player.media, 'qualitychange', false, {
quality: input,
});
},
@ -133,7 +92,7 @@ const html5 = {
}
// Remove child sources
removeElement(html5.getSources());
removeElement(html5.getSources.call(this));
// Set blank video src attribute
// 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 browser from './utils/browser';
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';
class Listeners {
@ -197,39 +197,36 @@ class Listeners {
// Add touch class
toggleClass(this.player.elements.container, this.player.config.classNames.isTouch, true);
// Clean up
off(document.body, 'touchstart', this.firstTouch);
}
// Global window & document listeners
global(toggle = true) {
// Keyboard shortcuts
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
toggleListener(document.body, 'click', this.toggleMenu, toggle);
toggleListener.call(this.player, document.body, 'click', this.toggleMenu, toggle);
// Detect touch by events
on(document.body, 'touchstart', this.firstTouch);
once(document.body, 'touchstart', this.firstTouch);
}
// Container listeners
container() {
// Keyboard shortcuts
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
// 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);
});
// 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) {
return;
}
@ -242,7 +239,7 @@ class Listeners {
});
// 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;
// Remove button states for fullscreen
@ -276,20 +273,20 @@ class Listeners {
// Listen for media events
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
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
// 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.buttons.mute, !this.player.hasAudio);
});
// Handle the media finishing
on(this.player.media, 'ended', () => {
on.call(this.player, this.player.media, 'ended', () => {
// Show poster on end
if (this.player.isHTML5 && this.player.isVideo && this.player.config.resetOnEnd) {
// Restart
@ -298,20 +295,20 @@ class Listeners {
});
// Check for buffer progress
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
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
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
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
// 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) {
return;
}
@ -334,7 +331,7 @@ class Listeners {
}
// 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)
if (this.player.config.hideControls && this.player.touch && !this.player.paused) {
return;
@ -353,7 +350,7 @@ class Listeners {
// Disable right click
if (this.player.supported.ui && this.player.config.disableContextMenu) {
on(
on.call(this.player,
this.player.elements.wrapper,
'contextmenu',
event => {
@ -364,13 +361,13 @@ class Listeners {
}
// Volume change
on(this.player.media, 'volumechange', () => {
on.call(this.player, this.player.media, 'volumechange', () => {
// Save to storage
this.player.storage.set({ volume: this.player.volume, muted: this.player.muted });
});
// Speed change
on(this.player.media, 'ratechange', () => {
on.call(this.player, this.player.media, 'ratechange', () => {
// Update UI
controls.updateSetting.call(this.player, 'speed');
@ -379,19 +376,19 @@ class Listeners {
});
// Quality request
on(this.player.media, 'qualityrequested', event => {
on.call(this.player, this.player.media, 'qualityrequested', event => {
// Save to storage
this.player.storage.set({ quality: event.detail.quality });
});
// Quality change
on(this.player.media, 'qualitychange', event => {
on.call(this.player, this.player.media, 'qualitychange', event => {
// Update UI
controls.updateSetting.call(this.player, 'quality', null, event.detail.quality);
});
// Caption language change
on(this.player.media, 'languagechange', () => {
on.call(this.player, this.player.media, 'languagechange', () => {
// Update UI
controls.updateSetting.call(this.player, 'captions');
@ -400,7 +397,7 @@ class Listeners {
});
// Captions toggle
on(this.player.media, 'captionsenabled captionsdisabled', () => {
on.call(this.player, this.player.media, 'captionsenabled captionsdisabled', () => {
// Update UI
controls.updateSetting.call(this.player, 'captions');
@ -410,7 +407,7 @@ class Listeners {
// Proxy events to container
// Bubble up key events for Edge
on(this.player.media, this.player.config.events.concat([
on.call(this.player, this.player.media, this.player.config.events.concat([
'keyup',
'keydown',
]).join(' '), event => {
@ -421,7 +418,7 @@ class Listeners {
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 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
@ -727,11 +724,6 @@ class Listeners {
false,
);
}
// Reset on destroy
clear() {
this.global(false);
}
}
export default Listeners;

View File

@ -8,7 +8,7 @@
import i18n from '../i18n';
import { createElement } from './../utils/elements';
import { trigger } from './../utils/events';
import { triggerEvent } from './../utils/events';
import is from './../utils/is';
import loadScript from './../utils/loadScript';
import { formatTime } from './../utils/time';
@ -270,7 +270,7 @@ class Ads {
// Proxy event
const dispatchEvent = type => {
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) {

View File

@ -6,7 +6,7 @@ import captions from './../captions';
import controls from './../controls';
import ui from './../ui';
import { createElement, replaceElement, toggleClass } from './../utils/elements';
import { trigger } from './../utils/events';
import { triggerEvent } from './../utils/events';
import fetch from './../utils/fetch';
import is from './../utils/is';
import loadScript from './../utils/loadScript';
@ -41,7 +41,7 @@ function assurePlaybackState(play) {
}
if (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
media.seeking = true;
trigger.call(player, media, 'seeking');
triggerEvent.call(player, media, 'seeking');
// If paused, mute until seek is complete
Promise.resolve(restorePause && embed.setVolume(0))
@ -213,7 +213,7 @@ const vimeo = {
.setPlaybackRate(input)
.then(() => {
speed = input;
trigger.call(player, player.media, 'ratechange');
triggerEvent.call(player, player.media, 'ratechange');
})
.catch(error => {
// Hide menu item (and menu if empty)
@ -233,7 +233,7 @@ const vimeo = {
set(input) {
player.embed.setVolume(input).then(() => {
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(() => {
muted = toggle;
trigger.call(player, player.media, 'volumechange');
triggerEvent.call(player, player.media, 'volumechange');
});
},
});
@ -316,13 +316,13 @@ const vimeo = {
// Get current time
player.embed.getCurrentTime().then(value => {
currentTime = value;
trigger.call(player, player.media, 'timeupdate');
triggerEvent.call(player, player.media, 'timeupdate');
});
// Get duration
player.embed.getDuration().then(value => {
player.media.duration = value;
trigger.call(player, player.media, 'durationchange');
triggerEvent.call(player, player.media, 'durationchange');
});
// Get captions
@ -341,7 +341,7 @@ const vimeo = {
player.embed.getPaused().then(paused => {
assurePlaybackState.call(player, !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', () => {
assurePlaybackState.call(player, true);
trigger.call(player, player.media, 'playing');
triggerEvent.call(player, player.media, 'playing');
});
player.embed.on('pause', () => {
@ -366,16 +366,16 @@ const vimeo = {
player.embed.on('timeupdate', data => {
player.media.seeking = false;
currentTime = data.seconds;
trigger.call(player, player.media, 'timeupdate');
triggerEvent.call(player, player.media, 'timeupdate');
});
player.embed.on('progress', data => {
player.media.buffered = data.percent;
trigger.call(player, player.media, 'progress');
triggerEvent.call(player, player.media, 'progress');
// Check all loaded
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
@ -383,24 +383,24 @@ const vimeo = {
player.embed.getDuration().then(value => {
if (value !== player.media.duration) {
player.media.duration = value;
trigger.call(player, player.media, 'durationchange');
triggerEvent.call(player, player.media, 'durationchange');
}
});
});
player.embed.on('seeked', () => {
player.media.seeking = false;
trigger.call(player, player.media, 'seeked');
triggerEvent.call(player, player.media, 'seeked');
});
player.embed.on('ended', () => {
player.media.paused = true;
trigger.call(player, player.media, 'ended');
triggerEvent.call(player, player.media, 'ended');
});
player.embed.on('error', detail => {
player.media.error = detail;
trigger.call(player, player.media, 'error');
triggerEvent.call(player, player.media, 'error');
});
// Rebuild UI

View File

@ -6,7 +6,7 @@ import controls from './../controls';
import ui from './../ui';
import { dedupe } from './../utils/arrays';
import { createElement, replaceElement, toggleClass } from './../utils/elements';
import { trigger } from './../utils/events';
import { triggerEvent } from './../utils/events';
import fetch from './../utils/fetch';
import is from './../utils/is';
import loadImage from './../utils/loadImage';
@ -25,52 +25,25 @@ function parseId(url) {
// Standardise YouTube quality unit
function mapQualityUnit(input) {
switch (input) {
case 'hd2160':
return 2160;
const qualities = {
hd2160: 2160,
hd1440: 1440,
hd1080: 1080,
hd720: 720,
large: 480,
medium: 360,
small: 240,
tiny: 144,
};
case 2160:
return 'hd2160';
const entry = Object.entries(qualities).find(entry => entry.includes(input));
case 'hd1440':
return 1440;
case 1440:
return 'hd1440';
case 'hd1080':
return 1080;
case 1080:
return 'hd1080';
case 'hd720':
return 720;
case 720:
return 'hd720';
case 'large':
return 480;
case 480:
return 'large';
case 'medium':
return 360;
case 360:
return 'medium';
case 'small':
return 240;
case 240:
return 'small';
default:
return 'default';
if (entry) {
// Get the match corresponding to the input
return entry.find(value => value !== input);
}
return 'default';
}
function mapQualityUnits(levels) {
@ -88,7 +61,7 @@ function assurePlaybackState(play) {
}
if (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;
trigger.call(player, player.media, 'error');
triggerEvent.call(player, player.media, 'error');
},
onPlaybackQualityChange() {
trigger.call(player, player.media, 'qualitychange', false, {
triggerEvent.call(player, player.media, 'qualitychange', false, {
quality: player.media.quality,
});
},
@ -280,7 +253,7 @@ const youtube = {
// Get current speed
player.media.playbackRate = instance.getPlaybackRate();
trigger.call(player, player.media, 'ratechange');
triggerEvent.call(player, player.media, 'ratechange');
},
onReady(event) {
// Get the instance
@ -321,7 +294,7 @@ const youtube = {
// Set seeking state and trigger event
player.media.seeking = true;
trigger.call(player, player.media, 'seeking');
triggerEvent.call(player, player.media, 'seeking');
// Seek after events sent
instance.seekTo(time);
@ -344,15 +317,7 @@ const youtube = {
return mapQualityUnit(instance.getPlaybackQuality());
},
set(input) {
const quality = input;
// Set via API
instance.setPlaybackQuality(mapQualityUnit(quality));
// Trigger request event
trigger.call(player, player.media, 'qualityrequested', false, {
quality,
});
instance.setPlaybackQuality(mapQualityUnit(input));
},
});
@ -365,7 +330,7 @@ const youtube = {
set(input) {
volume = input;
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;
muted = toggle;
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);
}
trigger.call(player, player.media, 'timeupdate');
trigger.call(player, player.media, 'durationchange');
triggerEvent.call(player, player.media, 'timeupdate');
triggerEvent.call(player, player.media, 'durationchange');
// Reset timer
clearInterval(player.timers.buffering);
@ -418,7 +383,7 @@ const youtube = {
// Trigger progress only when we actually buffer something
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
@ -429,7 +394,7 @@ const youtube = {
clearInterval(player.timers.buffering);
// Trigger event
trigger.call(player, player.media, 'canplaythrough');
triggerEvent.call(player, player.media, 'canplaythrough');
}
}, 200);
@ -451,7 +416,7 @@ const youtube = {
if (seeked) {
// Unset seeking and fire seeked event
player.media.seeking = false;
trigger.call(player, player.media, 'seeked');
triggerEvent.call(player, player.media, 'seeked');
}
// Handle events
@ -464,11 +429,11 @@ const youtube = {
switch (event.data) {
case -1:
// Update scrubber
trigger.call(player, player.media, 'timeupdate');
triggerEvent.call(player, player.media, 'timeupdate');
// Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction();
trigger.call(player, player.media, 'progress');
triggerEvent.call(player, player.media, 'progress');
break;
@ -481,7 +446,7 @@ const youtube = {
instance.stopVideo();
instance.playVideo();
} else {
trigger.call(player, player.media, 'ended');
triggerEvent.call(player, player.media, 'ended');
}
break;
@ -493,11 +458,11 @@ const youtube = {
} else {
assurePlaybackState.call(player, true);
trigger.call(player, player.media, 'playing');
triggerEvent.call(player, player.media, 'playing');
// Poll to get playback progress
player.timers.playing = setInterval(() => {
trigger.call(player, player.media, 'timeupdate');
triggerEvent.call(player, player.media, 'timeupdate');
}, 50);
// Check duration again due to YouTube bug
@ -505,7 +470,7 @@ const youtube = {
// https://code.google.com/p/gdata-issues/issues/detail?id=8690
if (player.media.duration !== instance.getDuration()) {
player.media.duration = instance.getDuration();
trigger.call(player, player.media, 'durationchange');
triggerEvent.call(player, player.media, 'durationchange');
}
// Get quality
@ -527,7 +492,7 @@ const youtube = {
break;
}
trigger.call(player, player.elements.container, 'statechange', false, {
triggerEvent.call(player, player.elements.container, 'statechange', false, {
code: event.data,
});
},

View File

@ -20,9 +20,9 @@ import support from './support';
import ui from './ui';
import { closest } from './utils/arrays';
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 loadSprite from './utils/loadScript';
import loadSprite from './utils/loadSprite';
import { cloneDeep, extend } from './utils/objects';
import { parseUrl } from './utils/urls';
@ -171,7 +171,7 @@ class Plyr {
this.elements.container.className = '';
// Get attributes from URL and set config
if (!url.searchParams) {
if (url.searchParams.length) {
const truthy = [
'1',
'true',
@ -249,6 +249,8 @@ class Plyr {
return;
}
this.eventListeners = [];
// Create listeners
this.listeners = new Listeners(this);
@ -275,7 +277,7 @@ class Plyr {
// Listen for events if debugging
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}`);
});
}
@ -673,36 +675,31 @@ class Plyr {
* @param {number} input - Quality level
*/
set quality(input) {
let quality = null;
const config = this.config.quality;
const options = this.options.quality;
if (!is.empty(input)) {
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) {
if (!options.length) {
return;
}
if (!this.options.quality.includes(quality)) {
const value = closest(this.options.quality, quality);
let 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`);
quality = value;
}
// Trigger request event
triggerEvent.call(this, this.media, 'qualityrequested', false, { quality });
// Update config
this.config.quality.selected = quality;
config.selected = quality;
// Set quality
this.media.quality = quality;
@ -853,7 +850,7 @@ class Plyr {
// Update state and trigger event
if (active !== this.captions.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
if (hiding !== isHidden) {
const eventName = hiding ? 'controlshidden' : 'controlsshown';
trigger.call(this, this.media, eventName);
triggerEvent.call(this, this.media, eventName);
}
return !hiding;
}
@ -970,9 +967,16 @@ class Plyr {
* @param {function} callback - Callback for when event occurs
*/
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
* @param {string} event - Event type
@ -1023,13 +1027,13 @@ class Plyr {
}
} else {
// Unbind listeners
this.listeners.clear();
unbindListeners.call(this);
// Replace the container with the original element provided
replaceElement(this.elements.original, this.elements.container);
// Event
trigger.call(this, this.elements.original, 'destroyed', true);
triggerEvent.call(this, this.elements.original, 'destroyed', true);
// Callback
if (is.function(callback)) {

View File

@ -8,7 +8,7 @@ import i18n from './i18n';
import support from './support';
import browser from './utils/browser';
import { getElement, toggleClass, toggleState } from './utils/elements';
import { trigger } from './utils/events';
import { triggerEvent } from './utils/events';
import is from './utils/is';
import loadImage from './utils/loadImage';
@ -102,7 +102,7 @@ const ui = {
// Ready event at end of execution stack
setTimeout(() => {
trigger.call(this, this.media, 'ready');
triggerEvent.call(this, this.media, 'ready');
}, 0);
// Set the title

View File

@ -27,7 +27,7 @@ const supportsPassiveListeners = (() => {
})();
// 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
if (is.empty(elements) || is.empty(event) || !is.function(callback)) {
return;
@ -64,22 +64,37 @@ export function toggleListener(elements, event, callback, toggle = false, passiv
// If a single node is passed, bind the event listener
events.forEach(type => {
if (this && this.eventListeners && toggle && !once) {
// Cache event listener
this.eventListeners.push({ elements, type, callback, options });
}
elements[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);
});
}
// Bind event handler
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
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
export function trigger(element, type = '', bubbles = false, detail = {}) {
export function triggerEvent(element, type = '', bubbles = false, detail = {}) {
// Bail if no element
if (!is.element(element) || is.empty(type)) {
return;
@ -96,3 +111,15 @@ export function trigger(element, type = '', bubbles = false, detail = {}) {
// Dispatch the 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 = [];
}
}