Merge pull request #1036 from friday/captions-passive-toggle

Captions fixes (again)
This commit is contained in:
Sam Potts 2018-06-15 15:33:39 +10:00 committed by GitHub
commit 3cd2b9a6c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 133 additions and 81 deletions

View File

@ -7,10 +7,11 @@ import controls from './controls';
import i18n from './i18n'; 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, toggleState } from './utils/elements';
import { on, triggerEvent } 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 { dedupe } from './utils/arrays';
import { getHTML } from './utils/strings'; import { getHTML } from './utils/strings';
import { parseUrl } from './utils/urls'; import { parseUrl } from './utils/urls';
@ -63,21 +64,34 @@ const captions = {
}); });
} }
// Try to load the value from storage // Get and set initial data
let active = this.storage.get('captions'); // 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 const languages = dedupe(Array.from(navigator.languages || navigator.userLanguage)
.map(language => language.split('-')[0]));
let language = this.storage.get('language') || this.config.captions.language;
// Use first browser language when language is 'auto'
if (language === 'auto') {
[language] = languages;
}
let active = this.storage.get('captions');
if (!is.boolean(active)) { if (!is.boolean(active)) {
({ active } = this.config.captions); ({ active } = this.config.captions);
} }
// Get language from storage, fallback to config Object.assign(this.captions, {
let language = this.storage.get('language') || this.config.captions.language; toggled: false,
if (language === 'auto') { active,
[language] = (navigator.language || navigator.userLanguage).split('-'); language,
} languages,
// Set language and show if active });
captions.setLanguage.call(this, language, active);
// Watch changes to textTracks and update captions menu // Watch changes to textTracks and update captions menu
if (this.isHTML5) { if (this.isHTML5) {
@ -89,10 +103,12 @@ const captions = {
setTimeout(captions.update.bind(this), 0); setTimeout(captions.update.bind(this), 0);
}, },
// Update available language options in settings based on tracks
update() { update() {
const tracks = captions.getTracks.call(this, true); const tracks = captions.getTracks.call(this, true);
// Get the wanted language // 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) // Handle tracks (add event listener and "pseudo"-default)
if (this.isHTML5 && this.isVideo) { if (this.isHTML5 && this.isVideo) {
@ -111,12 +127,10 @@ const captions = {
}); });
} }
const trackRemoved = !tracks.find(track => track === this.captions.currentTrackNode); // Update language first time it matches, or if the previous matching track was removed
const firstMatch = this.language !== language && tracks.find(track => track.language === language); if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) {
captions.setLanguage.call(this, language);
// Update language if removed or first matching track added captions.toggle.call(this, active && languageExists);
if (trackRemoved || firstMatch) {
captions.setLanguage.call(this, language, this.config.captions.active);
} }
// Enable or disable captions based on track length // Enable or disable captions based on track length
@ -128,12 +142,69 @@ 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) {
// 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
toggleState(this.elements.buttons.captions, active);
// Add class hook
toggleClass(this.elements.container, activeClass, active);
this.captions.toggled = active;
// Update settings menu
controls.updateSetting.call(this, 'captions');
// When passive, don't override user preferences
if (!passive) {
this.captions.active = active;
this.storage.set({ captions: active });
}
// 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); const tracks = captions.getTracks.call(this);
// Disable captions if setting to -1 // Disable captions if setting to -1
if (index === -1) { if (index === -1) {
this.toggleCaptions(false); captions.toggle.call(this, false, passive);
return; return;
} }
@ -149,15 +220,19 @@ const captions = {
if (this.captions.currentTrack !== index) { if (this.captions.currentTrack !== index) {
this.captions.currentTrack = index; this.captions.currentTrack = index;
const track = captions.getCurrentTrack.call(this); const track = tracks[index];
const { language } = track || {}; const { language } = track || {};
// Store reference to node for invalidation on remove // Store reference to node for invalidation on remove
this.captions.currentTrackNode = track; this.captions.currentTrackNode = track;
// Prevent setting language in some cases, since it can violate user's intentions // Update settings menu
if (setLanguage) { controls.updateSetting.call(this, 'captions');
// When passive, don't override user preferences
if (!passive) {
this.captions.language = language; this.captions.language = language;
this.storage.set({ language });
} }
// Handle Vimeo captions // Handle Vimeo captions
@ -175,12 +250,12 @@ const captions = {
} }
// Show captions // Show captions
if (show) { captions.toggle.call(this, true, passive);
this.toggleCaptions(true);
}
}, },
setLanguage(language, show = true) { // Set captions by language
// Used internally for the language setter with the passive option forced to false
setLanguage(language, passive = true) {
if (!is.string(language)) { if (!is.string(language)) {
this.debug.warn('Invalid language argument', language); this.debug.warn('Invalid language argument', language);
return; return;
@ -190,8 +265,8 @@ const captions = {
// Set currentTrack // Set currentTrack
const tracks = captions.getTracks.call(this); const tracks = captions.getTracks.call(this);
const track = captions.getCurrentTrack.call(this, true); const track = captions.findTrack.call(this, [language]);
captions.set.call(this, tracks.indexOf(track), false, show); captions.set.call(this, tracks.indexOf(track), passive);
}, },
// Get current valid caption tracks // Get current valid caption tracks
@ -208,19 +283,30 @@ const captions = {
].includes(track.kind)); ].includes(track.kind));
}, },
// Get the current track for the current language // Match tracks based on languages and get the first
getCurrentTrack(fromLanguage = false) { findTrack(languages, force = false) {
const tracks = captions.getTracks.call(this); const tracks = captions.getTracks.call(this);
const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default); const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);
const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a)); 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 // Get UI label for track
getLabel(track) { getLabel(track) {
let currentTrack = track; let currentTrack = track;
if (!is.track(currentTrack) && support.textTracks && this.captions.active) { if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {
currentTrack = captions.getCurrentTrack.call(this); currentTrack = captions.getCurrentTrack.call(this);
} }

12
src/js/controls.js vendored
View File

@ -10,7 +10,7 @@ import { repaint, transitionEndEvent } from './utils/animation';
import { dedupe } from './utils/arrays'; 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 { once } from './utils/events'; import { on, off } 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';
@ -848,7 +848,7 @@ const controls = {
// Generate options data // Generate options data
const options = tracks.map((track, value) => ({ const options = tracks.map((track, value) => ({
value, value,
checked: this.captions.active && this.currentTrack === value, checked: this.captions.toggled && this.currentTrack === value,
title: captions.getLabel.call(this, track), title: captions.getLabel.call(this, track),
badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()), badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()),
list, list,
@ -858,7 +858,7 @@ const controls = {
// Add the "Disabled" option to turn off captions // Add the "Disabled" option to turn off captions
options.unshift({ options.unshift({
value: -1, value: -1,
checked: !this.captions.active, checked: !this.captions.toggled,
title: i18n.get('disabled', this.config), title: i18n.get('disabled', this.config),
list, list,
type: 'language', type: 'language',
@ -1026,7 +1026,7 @@ const controls = {
return; return;
} }
// Are we targetting a tab? If not, bail // Are we targeting a tab? If not, bail
const isTab = pane.getAttribute('role') === 'tabpanel'; const isTab = pane.getAttribute('role') === 'tabpanel';
if (!isTab) { if (!isTab) {
return; return;
@ -1065,10 +1065,12 @@ const controls = {
container.style.width = ''; container.style.width = '';
container.style.height = ''; container.style.height = '';
// Only listen once
off.call(this, container, transitionEndEvent, restore);
}; };
// Listen for the transition finishing and restore auto height/width // Listen for the transition finishing and restore auto height/width
once.call(this, container, transitionEndEvent, restore); on.call(this, 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

@ -387,24 +387,6 @@ class Listeners {
controls.updateSetting.call(this.player, 'quality', null, event.detail.quality); controls.updateSetting.call(this.player, 'quality', null, event.detail.quality);
}); });
// Caption language change
on.call(this.player, this.player.media, 'languagechange', () => {
// Update UI
controls.updateSetting.call(this.player, 'captions');
// Save to storage
this.player.storage.set({ language: this.player.language });
});
// Captions toggle
on.call(this.player, this.player.media, 'captionsenabled captionsdisabled', () => {
// Update UI
controls.updateSetting.call(this.player, 'captions');
// Save to storage
this.player.storage.set({ captions: this.player.captions.active });
});
// Proxy events to container // Proxy events to container
// Bubble up key events for Edge // Bubble up key events for Edge
on.call(this.player, this.player.media, this.player.config.events.concat([ on.call(this.player, this.player.media, this.player.config.events.concat([
@ -477,7 +459,7 @@ class Listeners {
); );
// Captions toggle // Captions toggle
bind(this.player.elements.buttons.captions, 'click', this.player.toggleCaptions); bind(this.player.elements.buttons.captions, 'click', () => this.player.toggleCaptions());
// Fullscreen toggle // Fullscreen toggle
bind( bind(

View File

@ -19,7 +19,7 @@ import Storage from './storage';
import support from './support'; 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, wrap } from './utils/elements';
import { off, on, once, triggerEvent, unbindListeners } 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/loadSprite'; import loadSprite from './utils/loadSprite';
@ -833,25 +833,7 @@ class Plyr {
* @param {boolean} input - Whether to enable captions * @param {boolean} input - Whether to enable captions
*/ */
toggleCaptions(input) { toggleCaptions(input) {
// If there's no full support captions.toggle.call(this, input, false);
if (!this.supported.ui) {
return;
}
// If the method is called without parameter, toggle based on current value
const active = is.boolean(input) ? input : !this.elements.container.classList.contains(this.config.classNames.captions.active);
// Toggle state
toggleState(this.elements.buttons.captions, active);
// Add class hook
toggleClass(this.elements.container, this.config.classNames.captions.active, active);
// Update state and trigger event
if (active !== this.captions.active) {
this.captions.active = active;
triggerEvent.call(this, this.media, this.captions.active ? 'captionsenabled' : 'captionsdisabled');
}
} }
/** /**
@ -859,15 +841,15 @@ class Plyr {
* @param {number} - Caption index * @param {number} - Caption index
*/ */
set currentTrack(input) { set currentTrack(input) {
captions.set.call(this, input); captions.set.call(this, input, false);
} }
/** /**
* Get the current caption track index (-1 if disabled) * Get the current caption track index (-1 if disabled)
*/ */
get currentTrack() { get currentTrack() {
const { active, currentTrack } = this.captions; const { toggled, currentTrack } = this.captions;
return active ? currentTrack : -1; return toggled ? currentTrack : -1;
} }
/** /**
@ -876,7 +858,7 @@ class Plyr {
* @param {string} - Two character ISO language code (e.g. EN, FR, PT, etc) * @param {string} - Two character ISO language code (e.g. EN, FR, PT, etc)
*/ */
set language(input) { set language(input) {
captions.setLanguage.call(this, input); captions.setLanguage.call(this, input, false);
} }
/** /**