The aria-label attribute set on all play buttons does not change according the player state. When the video is playing, the aria-label should change to pause otherwise screen reader will not detect that this button now can be used to pause the video.
273 lines
8.9 KiB
JavaScript
273 lines
8.9 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('poster', poster);
|
|
|
|
// 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,
|
|
),
|
|
);
|
|
}
|
|
},
|
|
};
|
|
|
|
export default ui;
|