e8d883edba
* force fullscreen events to trigger on plyr element (media element in iOS) and not fullscreen container * Fixing "missing code in detail" for PlyrEvent type When using typescript and listening for youtube statechange event, it is missing the code property definition inside the event (even though it is provided in the code). By making events a map of key-value, we can add easily custom event type for specific event name. Since YouTube "statechange" event differs from the basic PlyrEvent, I added a new Event Type "PlyrStateChangeEvent" having a code property corresponding to a YoutubeState enum defined by the YouTube API documentation. This pattern follows how addEventListener in the lib.dom.d.ts is defined. * Update link to working dash.js demo (was broken) * Fix PreviewThumbnailsOptions type According to the docs, the `src` should also accept an array of strings. * fix issue #1872 * Check if key is a string before attempt --plyr checking * Fix for Slow loading videos not autoplaying * Fix for Slow loading videos not autoplaying * Network requests are not cancelled after the player is destroyed * Fix for apect ratio problem when using Vimeo player on mobile devices (issue #1940) * chore: update packages and linting * Invoke custom listener on triggering fullscreen via double-click * Fix volume when unmuting from volume 0 * adding a nice Svelte plugin that I found * Add missing unit to calc in media query * Assigning player's lastSeekTime on rewind/fast forward to prevent immediate controls hide on mobile * Fix youtube not working when player is inside shadow dom * v3.6.2 * ESLint to use common config * add BitChute to users list * Fix aspect ratio issue * Revert noCookie change * feat: demo radius tweaks * fix: poster image shouldn’t receive click events * chore: package updates * chore: linting * feat: custom controls option for embedded players * Package upgrades * ESLint to use common config * Linting changes * Update README.md * chore: formatting * fix: revert pointer events change for poster * fix: hack for Safari 14 not repainting Vimeo embed on entering fullscreen * fix: demo using custom controls for YouTube * doc: Add STROLLÿN among the list of Plyr users * Fixes #2005 * fix: overflowing volume slider * chore: clean up CSS * fix: hide poster when not using custom controls * Package upgrades * ESLint to use common config * Linting changes * chore: revert customControls default option (to prevent breaking change) * docs: changelog for v3.6.3 Co-authored-by: Som Meaden <som@theprojectsomething.com> Co-authored-by: akuma06 <demon.akuma06@gmail.com> Co-authored-by: Jonathan Arbely <dev@jonathanarbely.de> Co-authored-by: Takeshi <iwatakeshi@users.noreply.github.com> Co-authored-by: Hex <hex@codeigniter.org.cn> Co-authored-by: Syed Husain <syed.husain@appspace.com> Co-authored-by: Danielh112 <Daniel@sbgsportssoftware.com> Co-authored-by: Danil Stoyanov <d.stoyanov@corp.mail.ru> Co-authored-by: Guru Prasad Srinivasa <gurupras@buffalo.edu> Co-authored-by: Stephane Fortin Bouchard <stephane.f.bouchard@gmail.com> Co-authored-by: Zev Averbach <zev@averba.ch> Co-authored-by: Vincent Orback <hello@vincentorback.se> Co-authored-by: trafium <trafium@gmail.com> Co-authored-by: xansen <27698939+xansen@users.noreply.github.com> Co-authored-by: zoomerdev <59863739+zoomerdev@users.noreply.github.com> Co-authored-by: Mikaël Castellani <mikael.castellani@gmail.com> Co-authored-by: dirkjf <d.j.faber@outlook.com>
293 lines
8.6 KiB
JavaScript
293 lines
8.6 KiB
JavaScript
// ==========================================================================
|
|
// Plyr UI
|
|
// ==========================================================================
|
|
|
|
import captions from './captions';
|
|
import controls from './controls';
|
|
import support from './support';
|
|
import browser from './utils/browser';
|
|
import { getElement, toggleClass } from './utils/elements';
|
|
import { ready, triggerEvent } from './utils/events';
|
|
import i18n from './utils/i18n';
|
|
import is from './utils/is';
|
|
import loadImage from './utils/load-image';
|
|
|
|
const ui = {
|
|
addStyleHook() {
|
|
toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);
|
|
toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);
|
|
},
|
|
|
|
// Toggle native HTML5 media controls
|
|
toggleNativeControls(toggle = false) {
|
|
if (toggle && this.isHTML5) {
|
|
this.media.setAttribute('controls', '');
|
|
} else {
|
|
this.media.removeAttribute('controls');
|
|
}
|
|
},
|
|
|
|
// Setup the UI
|
|
build() {
|
|
// Re-attach media element listeners
|
|
// TODO: Use event bubbling?
|
|
this.listeners.media();
|
|
|
|
// Don't setup interface if no support
|
|
if (!this.supported.ui) {
|
|
this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);
|
|
|
|
// Restore native controls
|
|
ui.toggleNativeControls.call(this, true);
|
|
|
|
// Bail
|
|
return;
|
|
}
|
|
|
|
// Inject custom controls if not present
|
|
if (!is.element(this.elements.controls)) {
|
|
// Inject custom controls
|
|
controls.inject.call(this);
|
|
|
|
// Re-attach control listeners
|
|
this.listeners.controls();
|
|
}
|
|
|
|
// Remove native controls
|
|
ui.toggleNativeControls.call(this);
|
|
|
|
// Setup captions for HTML5
|
|
if (this.isHTML5) {
|
|
captions.setup.call(this);
|
|
}
|
|
|
|
// Reset volume
|
|
this.volume = null;
|
|
|
|
// Reset mute state
|
|
this.muted = null;
|
|
|
|
// Reset loop state
|
|
this.loop = null;
|
|
|
|
// Reset quality setting
|
|
this.quality = null;
|
|
|
|
// Reset speed
|
|
this.speed = null;
|
|
|
|
// Reset volume display
|
|
controls.updateVolume.call(this);
|
|
|
|
// Reset time display
|
|
controls.timeUpdate.call(this);
|
|
|
|
// Update the UI
|
|
ui.checkPlaying.call(this);
|
|
|
|
// Check for picture-in-picture support
|
|
toggleClass(
|
|
this.elements.container,
|
|
this.config.classNames.pip.supported,
|
|
support.pip && this.isHTML5 && this.isVideo,
|
|
);
|
|
|
|
// Check for airplay support
|
|
toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
|
|
|
|
// Add iOS class
|
|
toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
|
|
|
|
// Add touch class
|
|
toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
|
|
|
|
// Ready for API calls
|
|
this.ready = true;
|
|
|
|
// Ready event at end of execution stack
|
|
setTimeout(() => {
|
|
triggerEvent.call(this, this.media, 'ready');
|
|
}, 0);
|
|
|
|
// Set the title
|
|
ui.setTitle.call(this);
|
|
|
|
// Assure the poster image is set, if the property was added before the element was created
|
|
if (this.poster) {
|
|
ui.setPoster.call(this, this.poster, false).catch(() => {});
|
|
}
|
|
|
|
// Manually set the duration if user has overridden it.
|
|
// The event listeners for it doesn't get called if preload is disabled (#701)
|
|
if (this.config.duration) {
|
|
controls.durationUpdate.call(this);
|
|
}
|
|
},
|
|
|
|
// Setup aria attribute for play and iframe title
|
|
setTitle() {
|
|
// Find the current text
|
|
let label = i18n.get('play', this.config);
|
|
|
|
// If there's a media title set, use that for the label
|
|
if (is.string(this.config.title) && !is.empty(this.config.title)) {
|
|
label += `, ${this.config.title}`;
|
|
}
|
|
|
|
// If there's a play button, set label
|
|
Array.from(this.elements.buttons.play || []).forEach((button) => {
|
|
button.setAttribute('aria-label', label);
|
|
});
|
|
|
|
// Set iframe title
|
|
// https://github.com/sampotts/plyr/issues/124
|
|
if (this.isEmbed) {
|
|
const iframe = getElement.call(this, 'iframe');
|
|
|
|
if (!is.element(iframe)) {
|
|
return;
|
|
}
|
|
|
|
// Default to media type
|
|
const title = !is.empty(this.config.title) ? this.config.title : 'video';
|
|
const format = i18n.get('frameTitle', this.config);
|
|
|
|
iframe.setAttribute('title', format.replace('{title}', title));
|
|
}
|
|
},
|
|
|
|
// Toggle poster
|
|
togglePoster(enable) {
|
|
toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
|
|
},
|
|
|
|
// Set the poster image (async)
|
|
// Used internally for the poster setter, with the passive option forced to false
|
|
setPoster(poster, passive = true) {
|
|
// Don't override if call is passive
|
|
if (passive && this.poster) {
|
|
return Promise.reject(new Error('Poster already set'));
|
|
}
|
|
|
|
// Set property synchronously to respect the call order
|
|
this.media.setAttribute('data-poster', poster);
|
|
|
|
// Show the poster
|
|
this.elements.poster.removeAttribute('hidden');
|
|
|
|
// Wait until ui is ready
|
|
return (
|
|
ready
|
|
.call(this)
|
|
// Load image
|
|
.then(() => loadImage(poster))
|
|
.catch((err) => {
|
|
// Hide poster on error unless it's been set by another call
|
|
if (poster === this.poster) {
|
|
ui.togglePoster.call(this, false);
|
|
}
|
|
// Rethrow
|
|
throw err;
|
|
})
|
|
.then(() => {
|
|
// Prevent race conditions
|
|
if (poster !== this.poster) {
|
|
throw new Error('setPoster cancelled by later call to setPoster');
|
|
}
|
|
})
|
|
.then(() => {
|
|
Object.assign(this.elements.poster.style, {
|
|
backgroundImage: `url('${poster}')`,
|
|
// Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
|
|
backgroundSize: '',
|
|
});
|
|
|
|
ui.togglePoster.call(this, true);
|
|
|
|
return poster;
|
|
})
|
|
);
|
|
},
|
|
|
|
// Check playing state
|
|
checkPlaying(event) {
|
|
// Class hooks
|
|
toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
|
|
toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
|
|
toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
|
|
|
|
// Set state
|
|
Array.from(this.elements.buttons.play || []).forEach((target) => {
|
|
Object.assign(target, { pressed: this.playing });
|
|
target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));
|
|
});
|
|
|
|
// Only update controls on non timeupdate events
|
|
if (is.event(event) && event.type === 'timeupdate') {
|
|
return;
|
|
}
|
|
|
|
// Toggle controls
|
|
ui.toggleControls.call(this);
|
|
},
|
|
|
|
// Check if media is loading
|
|
checkLoading(event) {
|
|
this.loading = ['stalled', 'waiting'].includes(event.type);
|
|
|
|
// Clear timer
|
|
clearTimeout(this.timers.loading);
|
|
|
|
// Timer to prevent flicker when seeking
|
|
this.timers.loading = setTimeout(
|
|
() => {
|
|
// Update progress bar loading class state
|
|
toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
|
|
|
|
// Update controls visibility
|
|
ui.toggleControls.call(this);
|
|
},
|
|
this.loading ? 250 : 0,
|
|
);
|
|
},
|
|
|
|
// Toggle controls based on state and `force` argument
|
|
toggleControls(force) {
|
|
const { controls: controlsElement } = this.elements;
|
|
|
|
if (controlsElement && this.config.hideControls) {
|
|
// Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)
|
|
const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();
|
|
|
|
// Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide
|
|
this.toggleControls(
|
|
Boolean(
|
|
force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek,
|
|
),
|
|
);
|
|
}
|
|
},
|
|
|
|
// Migrate any custom properties from the media to the parent
|
|
migrateStyles() {
|
|
// Loop through values (as they are the keys when the object is spread 🤔)
|
|
Object.values({ ...this.media.style })
|
|
// We're only fussed about Plyr specific properties
|
|
.filter((key) => !is.empty(key) && is.string(key) && key.startsWith('--plyr'))
|
|
.forEach((key) => {
|
|
// Set on the container
|
|
this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));
|
|
|
|
// Clean up from media element
|
|
this.media.style.removeProperty(key);
|
|
});
|
|
|
|
// Remove attribute if empty
|
|
if (is.empty(this.media.style)) {
|
|
this.media.removeAttribute('style');
|
|
}
|
|
},
|
|
};
|
|
|
|
export default ui;
|