Fix #1017, fix #980, fix #1014: Captions rewrite (use index internally)

This commit is contained in:
Albin Larsson 2018-06-08 14:49:35 +02:00
parent 1fab4919c0
commit c83487a293
5 changed files with 163 additions and 141 deletions

View File

@ -407,7 +407,8 @@ player.fullscreen.active; // false;
| `source` | ✓ | ✓ | Gets or sets the current source for the player. The setter accepts an object. See [source setter](#source-setter) below for examples. | | `source` | ✓ | ✓ | Gets or sets the current source for the player. The setter accepts an object. See [source setter](#source-setter) below for examples. |
| `poster` | ✓ | ✓ | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. | | `poster` | ✓ | ✓ | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. |
| `autoplay` | ✓ | ✓ | Gets or sets the autoplay state of the player. The setter accepts a boolean. | | `autoplay` | ✓ | ✓ | Gets or sets the autoplay state of the player. The setter accepts a boolean. |
| `language` | ✓ | ✓ | Gets or sets the preferred captions language for the player. The setter accepts an ISO two-letter language code. Support for the languages is dependent on the captions you include. | | `currentTrack` | ✓ | ✓ | Gets or sets the caption track by index. `-1` means there track is missing or captions is not active |
| `language` | ✓ | ✓ | Gets or sets the preferred captions language for the player. The setter accepts an ISO two-letter language code. Support for the languages is dependent on the captions you include. If your captions don't have any language, or if you have multiple tracks with the same language, you may want to use `currentTrack` instead. |
| `fullscreen.active` | ✓ | - | Returns a boolean indicating if the current player is in fullscreen mode. | | `fullscreen.active` | ✓ | - | Returns a boolean indicating if the current player is in fullscreen mode. |
| `fullscreen.enabled` | ✓ | - | Returns a boolean indicating if the current player has fullscreen enabled. | | `fullscreen.enabled` | ✓ | - | Returns a boolean indicating if the current player has fullscreen enabled. |
| `pip` | ✓ | ✓ | Gets or sets the picture-in-picture state of the player. The setter accepts a boolean. This currently only supported on Safari 10+ on MacOS Sierra+ and iOS 10+. | | `pip` | ✓ | ✓ | Gets or sets the picture-in-picture state of the player. The setter accepts a boolean. This currently only supported on Safari 10+ on MacOS Sierra+ and iOS 10+. |

View File

@ -69,12 +69,18 @@ const captions = {
({ active } = this.config.captions); ({ active } = this.config.captions);
} }
// Set toggled state // Get language from storage, fallback to config
this.toggleCaptions(active); 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);
// Watch changes to textTracks and update captions menu // Watch changes to textTracks and update captions menu
if (this.config.captions.update) { if (this.isHTML5) {
utils.on(this.media.textTracks, 'addtrack removetrack', captions.update.bind(this)); const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';
utils.on(this.media.textTracks, trackEvents, captions.update.bind(this));
} }
// Update available languages in list next tick (the event must not be triggered before the listeners) // Update available languages in list next tick (the event must not be triggered before the listeners)
@ -82,21 +88,39 @@ const captions = {
}, },
update() { update() {
// Update tracks const tracks = captions.getTracks.call(this, true);
const tracks = captions.getTracks.call(this); // Get the wanted language
this.options.captions = tracks.map(({language}) => language); const { language, meta } = this.captions;
// Set language if it hasn't been set already // Handle tracks (add event listener and "pseudo"-default)
if (!this.language) { if (this.isHTML5 && this.isVideo) {
let { language } = this.config.captions; tracks
if (language === 'auto') { .filter(track => !meta.get(track))
[ language ] = (navigator.language || navigator.userLanguage).split('-'); .forEach(track => {
} this.debug.log('Track added', track);
this.language = this.storage.get('language') || (language || '').toLowerCase(); // 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));
});
} }
// Toggle the class hooks const trackRemoved = !tracks.find(track => track === this.captions.currentTrackNode);
utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(captions.getTracks.call(this))); 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);
}
// Enable or disable captions based on track length
utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(tracks));
// Update available languages in list // Update available languages in list
if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) { if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) {
@ -104,60 +128,94 @@ const captions = {
} }
}, },
// Set the captions language set(index, setLanguage = true, show = true) {
setLanguage() { const tracks = captions.getTracks.call(this);
// Setup HTML5 track rendering
// Disable captions if setting to -1
if (index === -1) {
this.toggleCaptions(false);
return;
}
if (!utils.is.number(index)) {
this.debug.warn('Invalid caption argument', index);
return;
}
if (!(index in tracks)) {
this.debug.warn('Track not found', index);
return;
}
if (this.captions.currentTrack !== index) {
this.captions.currentTrack = index;
const track = captions.getCurrentTrack.call(this);
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) {
this.captions.language = language;
}
// Handle Vimeo captions
if (this.isVimeo) {
this.embed.enableTextTrack(language);
}
// Trigger event
utils.dispatchEvent.call(this, this.media, 'languagechange');
}
if (this.isHTML5 && this.isVideo) { if (this.isHTML5 && this.isVideo) {
captions.getTracks.call(this).forEach(track => {
// Show track
utils.on(track, 'cuechange', () => captions.updateCues.call(this));
// Turn off native caption rendering to avoid double captions
// eslint-disable-next-line
track.mode = 'hidden';
});
// If we change the active track while a cue is already displayed we need to update it // If we change the active track while a cue is already displayed we need to update it
captions.updateCues.call(this); captions.updateCues.call(this);
}
} else if (this.isVimeo && this.captions.active) { // Show captions
this.embed.enableTextTrack(this.language); if (show) {
this.toggleCaptions(true);
} }
}, },
// Get the tracks setLanguage(language, show = true) {
getTracks() { if (!utils.is.string(language)) {
this.debug.warn('Invalid language argument', language);
return;
}
// Normalize
this.captions.language = language.toLowerCase();
// Set currentTrack
const tracks = captions.getTracks.call(this);
const track = captions.getCurrentTrack.call(this, true);
captions.set.call(this, tracks.indexOf(track), false, show);
},
// Get current valid caption tracks
// If update is false it will also ignore tracks without metadata
// This is used to "freeze" the language options when captions.update is false
getTracks(update = false) {
// Handle media or textTracks missing or null // Handle media or textTracks missing or null
const { textTracks } = this.media || {}; const tracks = Array.from((this.media || {}).textTracks || []);
// Filter out invalid tracks kinds (like metadata) // For HTML5, use cache instead of current tracks when it exists (if captions.update is false)
return Array.from(textTracks || []).filter(track => [ // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)
'captions', return tracks
'subtitles', .filter(track => !this.isHTML5 || update || this.captions.meta.has(track))
].includes(track.kind)); .filter(track => [
'captions',
'subtitles',
].includes(track.kind));
}, },
// Get the current track for the current language // Get the current track for the current language
getCurrentTrack() { getCurrentTrack(fromLanguage = false) {
const tracks = captions.getTracks.call(this); const tracks = captions.getTracks.call(this);
const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);
if (!tracks.length) { const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));
return null; return (!fromLanguage && tracks[this.currentTrack]) || sorted.find(track => track.language === this.captions.language) || sorted[0];
}
// Get track based on current language
let track = tracks.find(track => track.language.toLowerCase() === this.language);
// Get the <track> with default attribute
if (!track) {
track = utils.getElement.call(this, 'track[default]');
}
// Get the first track
if (!track) {
[track] = tracks;
}
return track;
}, },
// Get UI label for track // Get UI label for track

49
src/js/controls.js vendored
View File

@ -727,16 +727,7 @@ const controls = {
switch (setting) { switch (setting) {
case 'captions': case 'captions':
if (this.captions.active) { value = this.currentTrack;
if (this.options.captions.length > 2 || !this.options.captions.some(lang => lang === 'enabled')) {
value = this.captions.language;
} else {
value = 'enabled';
}
} else {
value = '';
}
break; break;
default: default:
@ -836,10 +827,10 @@ const controls = {
// TODO: Captions or language? Currently it's mixed // TODO: Captions or language? Currently it's mixed
const type = 'captions'; const type = 'captions';
const list = this.elements.settings.panes.captions.querySelector('ul'); const list = this.elements.settings.panes.captions.querySelector('ul');
const tracks = captions.getTracks.call(this);
// Toggle the pane and tab // Toggle the pane and tab
const toggle = captions.getTracks.call(this).length; controls.toggleTab.call(this, type, tracks.length);
controls.toggleTab.call(this, type, toggle);
// Empty the menu // Empty the menu
utils.emptyElement(list); utils.emptyElement(list);
@ -848,33 +839,31 @@ const controls = {
controls.checkMenu.call(this); controls.checkMenu.call(this);
// If there's no captions, bail // If there's no captions, bail
if (!toggle) { if (!tracks.length) {
return; return;
} }
// Re-map the tracks into just the data we need // Generate options data
const tracks = captions.getTracks.call(this).map(track => ({ const options = tracks.map((track, value) => ({
language: !utils.is.empty(track.language) ? track.language : 'enabled', value,
label: captions.getLabel.call(this, track), checked: this.captions.active && this.currentTrack === value,
title: captions.getLabel.call(this, track),
badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()),
list,
type: 'language',
})); }));
// Add the "Disabled" option to turn off captions // Add the "Disabled" option to turn off captions
tracks.unshift({ options.unshift({
language: '', value: -1,
label: i18n.get('disabled', this.config), checked: !this.captions.active,
title: i18n.get('disabled', this.config),
list,
type: 'language',
}); });
// Generate options // Generate options
tracks.forEach(track => { options.forEach(controls.createMenuItem.bind(this));
controls.createMenuItem.call(this, {
value: track.language,
list,
type: 'language',
title: track.label,
badge: track.language !== 'enabled' ? controls.createBadge.call(this, track.language.toUpperCase()) : null,
checked: track.language.toLowerCase() === this.language,
});
});
controls.updateSetting.call(this, type, list); controls.updateSetting.call(this, type, list);
}, },

View File

@ -523,7 +523,7 @@ class Listeners {
proxy( proxy(
event, event,
() => { () => {
this.player.language = event.target.value; this.player.currentTrack = Number(event.target.value);
showHomeTab(); showHomeTab();
}, },
'language', 'language',

View File

@ -84,7 +84,8 @@ class Plyr {
// Captions // Captions
this.captions = { this.captions = {
active: null, active: null,
currentTrack: null, currentTrack: -1,
meta: new WeakMap(),
}; };
// Fullscreen // Fullscreen
@ -96,7 +97,6 @@ class Plyr {
this.options = { this.options = {
speed: [], speed: [],
quality: [], quality: [],
captions: [],
}; };
// Debugging // Debugging
@ -854,61 +854,35 @@ class Plyr {
} }
/** /**
* Set the captions language * Set the caption track by index
* @param {string} - Two character ISO language code (e.g. EN, FR, PT, etc) * @param {number} - Caption index
*/ */
set language(input) { set currentTrack(input) {
// Nothing specified captions.set.call(this, input);
if (!utils.is.string(input)) {
return;
}
// If empty string is passed, assume disable captions
if (utils.is.empty(input)) {
this.toggleCaptions(false);
return;
}
// Normalize
const language = input.toLowerCase();
// Check for support
if (!this.options.captions.includes(language)) {
this.debug.log(`Language option: ${language} doesn't yet exist`);
return;
}
// Ensure captions are enabled
this.toggleCaptions(true);
// Enabled only
if (language === 'enabled') {
return;
}
// If nothing to change, bail
if (this.language === language) {
return;
}
// Update config
this.captions.language = language;
// Clear caption
captions.updateCues.call(this, []);
// Update captions
captions.setLanguage.call(this);
// Trigger an event
utils.dispatchEvent.call(this, this.media, 'languagechange');
} }
/** /**
* Get the current captions language * Get the current caption track index (-1 if disabled)
*/
get currentTrack() {
const { active, currentTrack } = this.captions;
return active ? currentTrack : -1;
}
/**
* Set the wanted language for captions
* Since tracks can be added later it won't update the actual caption track until there is a matching track
* @param {string} - Two character ISO language code (e.g. EN, FR, PT, etc)
*/
set language(input) {
captions.setLanguage.call(this, input);
}
/**
* Get the current track's language
*/ */
get language() { get language() {
return this.captions.language; return (captions.getCurrentTrack.call(this) || {}).language;
} }
/** /**