Merge branch 'develop' into a11y-improvements
# Conflicts: # dist/plyr.js # dist/plyr.js.map # dist/plyr.min.js # dist/plyr.min.js.map # dist/plyr.polyfilled.js # dist/plyr.polyfilled.js.map # dist/plyr.polyfilled.min.js # dist/plyr.polyfilled.min.js.map # src/js/controls.js # src/js/fullscreen.js # src/js/plyr.js # src/js/ui.js # src/js/utils.js
This commit is contained in:
@ -6,7 +6,21 @@
|
||||
import controls from './controls';
|
||||
import i18n from './i18n';
|
||||
import support from './support';
|
||||
import utils from './utils';
|
||||
import { dedupe } from './utils/arrays';
|
||||
import browser from './utils/browser';
|
||||
import {
|
||||
createElement,
|
||||
emptyElement,
|
||||
getAttributesFromSelector,
|
||||
insertAfter,
|
||||
removeElement,
|
||||
toggleClass,
|
||||
} from './utils/elements';
|
||||
import { on, triggerEvent } from './utils/events';
|
||||
import fetch from './utils/fetch';
|
||||
import is from './utils/is';
|
||||
import { getHTML } from './utils/strings';
|
||||
import { parseUrl } from './utils/urls';
|
||||
|
||||
const captions = {
|
||||
// Setup captions
|
||||
@ -19,7 +33,11 @@ const captions = {
|
||||
// Only Vimeo and HTML5 video supported at this point
|
||||
if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {
|
||||
// Clear menu and hide
|
||||
if (utils.is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {
|
||||
if (
|
||||
is.array(this.config.controls) &&
|
||||
this.config.controls.includes('settings') &&
|
||||
this.config.settings.includes('captions')
|
||||
) {
|
||||
controls.setCaptionsMenu.call(this);
|
||||
}
|
||||
|
||||
@ -27,15 +45,12 @@ const captions = {
|
||||
}
|
||||
|
||||
// Inject the container
|
||||
if (!utils.is.element(this.elements.captions)) {
|
||||
this.elements.captions = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.captions));
|
||||
if (!is.element(this.elements.captions)) {
|
||||
this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));
|
||||
|
||||
utils.insertAfter(this.elements.captions, this.elements.wrapper);
|
||||
insertAfter(this.elements.captions, this.elements.wrapper);
|
||||
}
|
||||
|
||||
// Get browser info
|
||||
const browser = utils.getBrowser();
|
||||
|
||||
// Fix IE captions if CORS is used
|
||||
// Fetch captions and inject as blobs instead (data URIs not supported!)
|
||||
if (browser.isIE && window.URL) {
|
||||
@ -43,84 +58,96 @@ const captions = {
|
||||
|
||||
Array.from(elements).forEach(track => {
|
||||
const src = track.getAttribute('src');
|
||||
const href = utils.parseUrl(src);
|
||||
const url = parseUrl(src);
|
||||
|
||||
if (href.hostname !== window.location.href.hostname && [
|
||||
'http:',
|
||||
'https:',
|
||||
].includes(href.protocol)) {
|
||||
utils
|
||||
.fetch(src, 'blob')
|
||||
if (
|
||||
url !== null &&
|
||||
url.hostname !== window.location.href.hostname &&
|
||||
['http:', 'https:'].includes(url.protocol)
|
||||
) {
|
||||
fetch(src, 'blob')
|
||||
.then(blob => {
|
||||
track.setAttribute('src', window.URL.createObjectURL(blob));
|
||||
})
|
||||
.catch(() => {
|
||||
utils.removeElement(track);
|
||||
removeElement(track);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Try to load the value from storage
|
||||
let active = this.storage.get('captions');
|
||||
// Get and set initial data
|
||||
// The "preferred" options are not realized unless / until the wanted language has a match
|
||||
// * languages: Array of user's browser languages.
|
||||
// * language: The language preferred by user settings or config
|
||||
// * active: The state preferred by user settings or config
|
||||
// * toggled: The real captions state
|
||||
|
||||
// Otherwise fall back to the default config
|
||||
if (!utils.is.boolean(active)) {
|
||||
const languages = dedupe(
|
||||
Array.from(navigator.languages || navigator.userLanguage).map(language => language.split('-')[0]),
|
||||
);
|
||||
|
||||
let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();
|
||||
|
||||
// Use first browser language when language is 'auto'
|
||||
if (language === 'auto') {
|
||||
[language] = languages;
|
||||
}
|
||||
|
||||
let active = this.storage.get('captions');
|
||||
if (!is.boolean(active)) {
|
||||
({ active } = this.config.captions);
|
||||
}
|
||||
|
||||
// Get language from storage, fallback to config
|
||||
let language = this.storage.get('language') || this.config.captions.language;
|
||||
if (language === 'auto') {
|
||||
[ language ] = (navigator.language || navigator.userLanguage).split('-');
|
||||
}
|
||||
// Set language and show if active
|
||||
captions.setLanguage.call(this, language, active);
|
||||
Object.assign(this.captions, {
|
||||
toggled: false,
|
||||
active,
|
||||
language,
|
||||
languages,
|
||||
});
|
||||
|
||||
// Watch changes to textTracks and update captions menu
|
||||
if (this.isHTML5) {
|
||||
const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';
|
||||
utils.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)
|
||||
setTimeout(captions.update.bind(this), 0);
|
||||
},
|
||||
|
||||
// Update available language options in settings based on tracks
|
||||
update() {
|
||||
const tracks = captions.getTracks.call(this, true);
|
||||
// Get the wanted language
|
||||
const { language, meta } = this.captions;
|
||||
const { active, language, meta, currentTrackNode } = this.captions;
|
||||
const languageExists = Boolean(tracks.find(track => track.language === language));
|
||||
|
||||
// Handle tracks (add event listener and "pseudo"-default)
|
||||
if (this.isHTML5 && this.isVideo) {
|
||||
tracks
|
||||
.filter(track => !meta.get(track))
|
||||
.forEach(track => {
|
||||
this.debug.log('Track added', track);
|
||||
// Attempt to store if the original dom element was "default"
|
||||
meta.set(track, {
|
||||
default: track.mode === 'showing',
|
||||
});
|
||||
|
||||
// Turn off native caption rendering to avoid double captions
|
||||
track.mode = 'hidden';
|
||||
|
||||
// Add event listener for cue changes
|
||||
utils.on(track, 'cuechange', () => captions.updateCues.call(this));
|
||||
tracks.filter(track => !meta.get(track)).forEach(track => {
|
||||
this.debug.log('Track added', track);
|
||||
// Attempt to store if the original dom element was "default"
|
||||
meta.set(track, {
|
||||
default: track.mode === 'showing',
|
||||
});
|
||||
|
||||
// Turn off native caption rendering to avoid double captions
|
||||
track.mode = 'hidden';
|
||||
|
||||
// Add event listener for cue changes
|
||||
on.call(this, track, 'cuechange', () => captions.updateCues.call(this));
|
||||
});
|
||||
}
|
||||
|
||||
const trackRemoved = !tracks.find(track => track === this.captions.currentTrackNode);
|
||||
const firstMatch = this.language !== language && tracks.find(track => track.language === language);
|
||||
|
||||
// Update language if removed or first matching track added
|
||||
if (trackRemoved || firstMatch) {
|
||||
captions.setLanguage.call(this, language, this.config.captions.active);
|
||||
// Update language first time it matches, or if the previous matching track was removed
|
||||
if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) {
|
||||
captions.setLanguage.call(this, language);
|
||||
captions.toggle.call(this, active && languageExists);
|
||||
}
|
||||
|
||||
// Enable or disable captions based on track length
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(tracks));
|
||||
toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));
|
||||
|
||||
// Update available languages in list
|
||||
if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) {
|
||||
@ -128,16 +155,70 @@ const captions = {
|
||||
}
|
||||
},
|
||||
|
||||
set(index, setLanguage = true, show = true) {
|
||||
// Toggle captions display
|
||||
// Used internally for the toggleCaptions method, with the passive option forced to false
|
||||
toggle(input, passive = true) {
|
||||
// If there's no full support
|
||||
if (!this.supported.ui) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { toggled } = this.captions; // Current state
|
||||
const activeClass = this.config.classNames.captions.active;
|
||||
|
||||
// Get the next state
|
||||
// If the method is called without parameter, toggle based on current value
|
||||
const active = is.nullOrUndefined(input) ? !toggled : input;
|
||||
|
||||
// Update state and trigger event
|
||||
if (active !== toggled) {
|
||||
// When passive, don't override user preferences
|
||||
if (!passive) {
|
||||
this.captions.active = active;
|
||||
this.storage.set({ captions: active });
|
||||
}
|
||||
|
||||
// Force language if the call isn't passive and there is no matching language to toggle to
|
||||
if (!this.language && active && !passive) {
|
||||
const tracks = captions.getTracks.call(this);
|
||||
const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);
|
||||
|
||||
// Override user preferences to avoid switching languages if a matching track is added
|
||||
this.captions.language = track.language;
|
||||
|
||||
// Set caption, but don't store in localStorage as user preference
|
||||
captions.set.call(this, tracks.indexOf(track));
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle state
|
||||
this.elements.buttons.captions.pressed = active;
|
||||
|
||||
// Add class hook
|
||||
toggleClass(this.elements.container, activeClass, active);
|
||||
|
||||
this.captions.toggled = active;
|
||||
|
||||
// Update settings menu
|
||||
controls.updateSetting.call(this, 'captions');
|
||||
|
||||
// Trigger event (not used internally)
|
||||
triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');
|
||||
}
|
||||
},
|
||||
|
||||
// Set captions by track index
|
||||
// Used internally for the currentTrack setter with the passive option forced to false
|
||||
set(index, passive = true) {
|
||||
const tracks = captions.getTracks.call(this);
|
||||
|
||||
// Disable captions if setting to -1
|
||||
if (index === -1) {
|
||||
this.toggleCaptions(false);
|
||||
captions.toggle.call(this, false, passive);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!utils.is.number(index)) {
|
||||
if (!is.number(index)) {
|
||||
this.debug.warn('Invalid caption argument', index);
|
||||
return;
|
||||
}
|
||||
@ -149,15 +230,19 @@ const captions = {
|
||||
|
||||
if (this.captions.currentTrack !== index) {
|
||||
this.captions.currentTrack = index;
|
||||
const track = captions.getCurrentTrack.call(this);
|
||||
const track = tracks[index];
|
||||
const { language } = track || {};
|
||||
|
||||
// Store reference to node for invalidation on remove
|
||||
this.captions.currentTrackNode = track;
|
||||
|
||||
// Prevent setting language in some cases, since it can violate user's intentions
|
||||
if (setLanguage) {
|
||||
// Update settings menu
|
||||
controls.updateSetting.call(this, 'captions');
|
||||
|
||||
// When passive, don't override user preferences
|
||||
if (!passive) {
|
||||
this.captions.language = language;
|
||||
this.storage.set({ language });
|
||||
}
|
||||
|
||||
// Handle Vimeo captions
|
||||
@ -166,32 +251,33 @@ const captions = {
|
||||
}
|
||||
|
||||
// Trigger event
|
||||
utils.dispatchEvent.call(this, this.media, 'languagechange');
|
||||
triggerEvent.call(this, this.media, 'languagechange');
|
||||
}
|
||||
|
||||
// Show captions
|
||||
captions.toggle.call(this, true, passive);
|
||||
|
||||
if (this.isHTML5 && this.isVideo) {
|
||||
// If we change the active track while a cue is already displayed we need to update it
|
||||
captions.updateCues.call(this);
|
||||
}
|
||||
|
||||
// Show captions
|
||||
if (show) {
|
||||
this.toggleCaptions(true);
|
||||
}
|
||||
},
|
||||
|
||||
setLanguage(language, show = true) {
|
||||
if (!utils.is.string(language)) {
|
||||
this.debug.warn('Invalid language argument', language);
|
||||
// Set captions by language
|
||||
// Used internally for the language setter with the passive option forced to false
|
||||
setLanguage(input, passive = true) {
|
||||
if (!is.string(input)) {
|
||||
this.debug.warn('Invalid language argument', input);
|
||||
return;
|
||||
}
|
||||
// Normalize
|
||||
this.captions.language = language.toLowerCase();
|
||||
const language = input.toLowerCase();
|
||||
this.captions.language = language;
|
||||
|
||||
// Set currentTrack
|
||||
const tracks = captions.getTracks.call(this);
|
||||
const track = captions.getCurrentTrack.call(this, true);
|
||||
captions.set.call(this, tracks.indexOf(track), false, show);
|
||||
const track = captions.findTrack.call(this, [language]);
|
||||
captions.set.call(this, tracks.indexOf(track), passive);
|
||||
},
|
||||
|
||||
// Get current valid caption tracks
|
||||
@ -204,34 +290,42 @@ const captions = {
|
||||
// Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)
|
||||
return tracks
|
||||
.filter(track => !this.isHTML5 || update || this.captions.meta.has(track))
|
||||
.filter(track => [
|
||||
'captions',
|
||||
'subtitles',
|
||||
].includes(track.kind));
|
||||
.filter(track => ['captions', 'subtitles'].includes(track.kind));
|
||||
},
|
||||
|
||||
// Get the current track for the current language
|
||||
getCurrentTrack(fromLanguage = false) {
|
||||
// Match tracks based on languages and get the first
|
||||
findTrack(languages, force = false) {
|
||||
const tracks = captions.getTracks.call(this);
|
||||
const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);
|
||||
const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));
|
||||
return (!fromLanguage && tracks[this.currentTrack]) || sorted.find(track => track.language === this.captions.language) || sorted[0];
|
||||
let track;
|
||||
languages.every(language => {
|
||||
track = sorted.find(track => track.language === language);
|
||||
return !track; // Break iteration if there is a match
|
||||
});
|
||||
// If no match is found but is required, get first
|
||||
return track || (force ? sorted[0] : undefined);
|
||||
},
|
||||
|
||||
// Get the current track
|
||||
getCurrentTrack() {
|
||||
return captions.getTracks.call(this)[this.currentTrack];
|
||||
},
|
||||
|
||||
// Get UI label for track
|
||||
getLabel(track) {
|
||||
let currentTrack = track;
|
||||
|
||||
if (!utils.is.track(currentTrack) && support.textTracks && this.captions.active) {
|
||||
if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {
|
||||
currentTrack = captions.getCurrentTrack.call(this);
|
||||
}
|
||||
|
||||
if (utils.is.track(currentTrack)) {
|
||||
if (!utils.is.empty(currentTrack.label)) {
|
||||
if (is.track(currentTrack)) {
|
||||
if (!is.empty(currentTrack.label)) {
|
||||
return currentTrack.label;
|
||||
}
|
||||
|
||||
if (!utils.is.empty(currentTrack.language)) {
|
||||
if (!is.empty(currentTrack.language)) {
|
||||
return track.language.toUpperCase();
|
||||
}
|
||||
|
||||
@ -249,13 +343,13 @@ const captions = {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!utils.is.element(this.elements.captions)) {
|
||||
if (!is.element(this.elements.captions)) {
|
||||
this.debug.warn('No captions element to render to');
|
||||
return;
|
||||
}
|
||||
|
||||
// Only accept array or empty input
|
||||
if (!utils.is.nullOrUndefined(input) && !Array.isArray(input)) {
|
||||
if (!is.nullOrUndefined(input) && !Array.isArray(input)) {
|
||||
this.debug.warn('updateCues: Invalid input', input);
|
||||
return;
|
||||
}
|
||||
@ -267,7 +361,7 @@ const captions = {
|
||||
const track = captions.getCurrentTrack.call(this);
|
||||
cues = Array.from((track || {}).activeCues || [])
|
||||
.map(cue => cue.getCueAsHTML())
|
||||
.map(utils.getHTML);
|
||||
.map(getHTML);
|
||||
}
|
||||
|
||||
// Set new caption text
|
||||
@ -276,13 +370,13 @@ const captions = {
|
||||
|
||||
if (changed) {
|
||||
// Empty the container and create a new child element
|
||||
utils.emptyElement(this.elements.captions);
|
||||
const caption = utils.createElement('span', utils.getAttributesFromSelector(this.config.selectors.caption));
|
||||
emptyElement(this.elements.captions);
|
||||
const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));
|
||||
caption.innerHTML = content;
|
||||
this.elements.captions.appendChild(caption);
|
||||
|
||||
// Trigger event
|
||||
utils.dispatchEvent.call(this, this.media, 'cuechange');
|
||||
triggerEvent.call(this, this.media, 'cuechange');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
Reference in New Issue
Block a user