diff options
author | Albin Larsson <mail@albinlarsson.com> | 2018-06-08 14:49:35 +0200 |
---|---|---|
committer | Albin Larsson <mail@albinlarsson.com> | 2018-06-10 19:00:07 +0200 |
commit | c83487a293d61d3a1add31018918e9c831bbcac2 (patch) | |
tree | 00e8b3a177e7e79615ee5611aac560dffab53e65 /src | |
parent | 1fab4919c01347a29e11cbd78fedcddfabd1b814 (diff) | |
download | plyr-c83487a293d61d3a1add31018918e9c831bbcac2.tar.lz plyr-c83487a293d61d3a1add31018918e9c831bbcac2.tar.xz plyr-c83487a293d61d3a1add31018918e9c831bbcac2.zip |
Fix #1017, fix #980, fix #1014: Captions rewrite (use index internally)
Diffstat (limited to 'src')
-rw-r--r-- | src/js/captions.js | 170 | ||||
-rw-r--r-- | src/js/controls.js | 49 | ||||
-rw-r--r-- | src/js/listeners.js | 2 | ||||
-rw-r--r-- | src/js/plyr.js | 72 |
4 files changed, 157 insertions, 136 deletions
diff --git a/src/js/captions.js b/src/js/captions.js index 38167d7a..bafcf87e 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -69,12 +69,18 @@ const captions = { ({ active } = this.config.captions); } - // Set toggled state - this.toggleCaptions(active); + // 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); // Watch changes to textTracks and update captions menu - if (this.config.captions.update) { - utils.on(this.media.textTracks, 'addtrack removetrack', captions.update.bind(this)); + if (this.isHTML5) { + 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) @@ -82,21 +88,39 @@ const captions = { }, update() { - // Update tracks - const tracks = captions.getTracks.call(this); - this.options.captions = tracks.map(({language}) => language); + const tracks = captions.getTracks.call(this, true); + // Get the wanted language + const { language, meta } = this.captions; - // Set language if it hasn't been set already - if (!this.language) { - let { language } = this.config.captions; - if (language === 'auto') { - [ language ] = (navigator.language || navigator.userLanguage).split('-'); - } - this.language = this.storage.get('language') || (language || '').toLowerCase(); + // 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)); + }); } - // Toggle the class hooks - utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(captions.getTracks.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); + } + + // 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 if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) { @@ -104,60 +128,94 @@ const captions = { } }, - // Set the captions language - setLanguage() { - // Setup HTML5 track rendering - if (this.isHTML5 && this.isVideo) { - captions.getTracks.call(this).forEach(track => { - // Show track - utils.on(track, 'cuechange', () => captions.updateCues.call(this)); + set(index, setLanguage = true, show = true) { + const tracks = captions.getTracks.call(this); - // Turn off native caption rendering to avoid double captions - // eslint-disable-next-line - track.mode = 'hidden'; - }); + // Disable captions if setting to -1 + if (index === -1) { + this.toggleCaptions(false); + return; + } - // If we change the active track while a cue is already displayed we need to update it - captions.updateCues.call(this); + if (!utils.is.number(index)) { + this.debug.warn('Invalid caption argument', index); + return; + } - } else if (this.isVimeo && this.captions.active) { - this.embed.enableTextTrack(this.language); + if (!(index in tracks)) { + this.debug.warn('Track not found', index); + return; } - }, - // Get the tracks - getTracks() { - // Handle media or textTracks missing or null - const { textTracks } = this.media || {}; - // Filter out invalid tracks kinds (like metadata) - return Array.from(textTracks || []).filter(track => [ - 'captions', - 'subtitles', - ].includes(track.kind)); - }, + if (this.captions.currentTrack !== index) { + this.captions.currentTrack = index; + const track = captions.getCurrentTrack.call(this); + const { language } = track || {}; - // Get the current track for the current language - getCurrentTrack() { - const tracks = captions.getTracks.call(this); + // 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); + } - if (!tracks.length) { - return null; + // Trigger event + utils.dispatchEvent.call(this, this.media, 'languagechange'); } - // Get track based on current language - let track = tracks.find(track => track.language.toLowerCase() === this.language); + 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); + } - // Get the <track> with default attribute - if (!track) { - track = utils.getElement.call(this, 'track[default]'); + // Show captions + if (show) { + this.toggleCaptions(true); } + }, - // Get the first track - if (!track) { - [track] = tracks; + setLanguage(language, show = true) { + 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); + }, - return track; + // 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 + const tracks = Array.from((this.media || {}).textTracks || []); + // For HTML5, use cache instead of current tracks when it exists (if captions.update is false) + // 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)); + }, + + // Get the current track for the current language + getCurrentTrack(fromLanguage = 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]; }, // Get UI label for track diff --git a/src/js/controls.js b/src/js/controls.js index e9529e4e..058e636f 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -727,16 +727,7 @@ const controls = { switch (setting) { case 'captions': - if (this.captions.active) { - if (this.options.captions.length > 2 || !this.options.captions.some(lang => lang === 'enabled')) { - value = this.captions.language; - } else { - value = 'enabled'; - } - } else { - value = ''; - } - + value = this.currentTrack; break; default: @@ -836,10 +827,10 @@ const controls = { // TODO: Captions or language? Currently it's mixed const type = 'captions'; const list = this.elements.settings.panes.captions.querySelector('ul'); + const tracks = captions.getTracks.call(this); // Toggle the pane and tab - const toggle = captions.getTracks.call(this).length; - controls.toggleTab.call(this, type, toggle); + controls.toggleTab.call(this, type, tracks.length); // Empty the menu utils.emptyElement(list); @@ -848,33 +839,31 @@ const controls = { controls.checkMenu.call(this); // If there's no captions, bail - if (!toggle) { + if (!tracks.length) { return; } - // Re-map the tracks into just the data we need - const tracks = captions.getTracks.call(this).map(track => ({ - language: !utils.is.empty(track.language) ? track.language : 'enabled', - label: captions.getLabel.call(this, track), + // Generate options data + const options = tracks.map((track, value) => ({ + value, + 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 - tracks.unshift({ - language: '', - label: i18n.get('disabled', this.config), + options.unshift({ + value: -1, + checked: !this.captions.active, + title: i18n.get('disabled', this.config), + list, + type: 'language', }); // Generate options - tracks.forEach(track => { - 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, - }); - }); + options.forEach(controls.createMenuItem.bind(this)); controls.updateSetting.call(this, type, list); }, diff --git a/src/js/listeners.js b/src/js/listeners.js index 81f5271c..72b60cc0 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -523,7 +523,7 @@ class Listeners { proxy( event, () => { - this.player.language = event.target.value; + this.player.currentTrack = Number(event.target.value); showHomeTab(); }, 'language', diff --git a/src/js/plyr.js b/src/js/plyr.js index 6387fd6c..b6f355ac 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -84,7 +84,8 @@ class Plyr { // Captions this.captions = { active: null, - currentTrack: null, + currentTrack: -1, + meta: new WeakMap(), }; // Fullscreen @@ -96,7 +97,6 @@ class Plyr { this.options = { speed: [], quality: [], - captions: [], }; // Debugging @@ -854,61 +854,35 @@ class Plyr { } /** - * Set the captions language - * @param {string} - Two character ISO language code (e.g. EN, FR, PT, etc) + * Set the caption track by index + * @param {number} - Caption index */ - set language(input) { - // Nothing specified - 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, []); + set currentTrack(input) { + captions.set.call(this, input); + } - // Update captions - captions.setLanguage.call(this); + /** + * Get the current caption track index (-1 if disabled) + */ + get currentTrack() { + const { active, currentTrack } = this.captions; + return active ? currentTrack : -1; + } - // Trigger an event - utils.dispatchEvent.call(this, this.media, 'languagechange'); + /** + * 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 captions language + * Get the current track's language */ get language() { - return this.captions.language; + return (captions.getCurrentTrack.call(this) || {}).language; } /** |