From 502d5977d79148957828cbf313b7ef4c9f31973f Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sat, 11 Apr 2020 16:23:14 +1000 Subject: Converted to 2 space indentation --- src/js/captions.js | 702 ++++++++++++++++++++++++++--------------------------- 1 file changed, 351 insertions(+), 351 deletions(-) (limited to 'src/js/captions.js') diff --git a/src/js/captions.js b/src/js/captions.js index 04da4651..0e6173ca 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -8,12 +8,12 @@ import support from './support'; import { dedupe } from './utils/arrays'; import browser from './utils/browser'; import { - createElement, - emptyElement, - getAttributesFromSelector, - insertAfter, - removeElement, - toggleClass, + createElement, + emptyElement, + getAttributesFromSelector, + insertAfter, + removeElement, + toggleClass, } from './utils/elements'; import { on, triggerEvent } from './utils/events'; import fetch from './utils/fetch'; @@ -23,371 +23,371 @@ import { getHTML } from './utils/strings'; import { parseUrl } from './utils/urls'; const captions = { - // Setup captions - setup() { - // Requires UI support - if (!this.supported.ui) { - return; - } - - // Only Vimeo and HTML5 video supported at this point - if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) { - // Clear menu and hide - if ( - is.array(this.config.controls) && - this.config.controls.includes('settings') && - this.config.settings.includes('captions') - ) { - controls.setCaptionsMenu.call(this); - } - - return; - } - - // Inject the container - if (!is.element(this.elements.captions)) { - this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions)); - - insertAfter(this.elements.captions, this.elements.wrapper); - } - - // Fix IE captions if CORS is used - // Fetch captions and inject as blobs instead (data URIs not supported!) - if (browser.isIE && window.URL) { - const elements = this.media.querySelectorAll('track'); - - Array.from(elements).forEach(track => { - const src = track.getAttribute('src'); - const url = parseUrl(src); - - 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(() => { - removeElement(track); - }); - } - }); - } - - // 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 - - const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en']; - const languages = dedupe(browserLanguages.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); - } - - 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'; - 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 { 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 - // eslint-disable-next-line no-param-reassign - track.mode = 'hidden'; - - // Add event listener for cue changes - on.call(this, track, 'cuechange', () => captions.updateCues.call(this)); - }); - } - - // 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); - } + // Setup captions + setup() { + // Requires UI support + if (!this.supported.ui) { + return; + } + + // Only Vimeo and HTML5 video supported at this point + if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) { + // Clear menu and hide + if ( + is.array(this.config.controls) && + this.config.controls.includes('settings') && + this.config.settings.includes('captions') + ) { + controls.setCaptionsMenu.call(this); + } + + return; + } + + // Inject the container + if (!is.element(this.elements.captions)) { + this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions)); + + insertAfter(this.elements.captions, this.elements.wrapper); + } + + // Fix IE captions if CORS is used + // Fetch captions and inject as blobs instead (data URIs not supported!) + if (browser.isIE && window.URL) { + const elements = this.media.querySelectorAll('track'); + + Array.from(elements).forEach(track => { + const src = track.getAttribute('src'); + const url = parseUrl(src); - // Enable or disable captions based on track length - toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks)); - - // Update available languages in list if ( - is.array(this.config.controls) && - this.config.controls.includes('settings') && - this.config.settings.includes('captions') + url !== null && + url.hostname !== window.location.href.hostname && + ['http:', 'https:'].includes(url.protocol) ) { - controls.setCaptionsMenu.call(this); - } - }, - - // 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 button if it's enabled - if (this.elements.buttons.captions) { - 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) { - captions.toggle.call(this, false, passive); - return; - } - - if (!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 = tracks[index]; - const { language } = track || {}; - - // Store reference to node for invalidation on remove - this.captions.currentTrackNode = track; - - // 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 - if (this.isVimeo) { - this.embed.enableTextTrack(language); - } - - // Trigger event - 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); - } - }, - - // 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; + fetch(src, 'blob') + .then(blob => { + track.setAttribute('src', window.URL.createObjectURL(blob)); + }) + .catch(() => { + removeElement(track); + }); } - // Normalize - const language = input.toLowerCase(); - this.captions.language = language; - - // Set currentTrack - const tracks = captions.getTracks.call(this); - const track = captions.findTrack.call(this, [language]); - captions.set.call(this, tracks.indexOf(track), passive); - }, - - // 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)); - }, - - // 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)); - let track; - - languages.every(language => { - track = sorted.find(t => t.language === language); - return !track; // Break iteration if there is a match + }); + } + + // 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 + + const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en']; + const languages = dedupe(browserLanguages.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); + } + + 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'; + 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 { 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 + // eslint-disable-next-line no-param-reassign + track.mode = 'hidden'; + + // Add event listener for cue changes + on.call(this, track, 'cuechange', () => captions.updateCues.call(this)); }); + } + + // 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 + toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks)); + + // Update available languages in list + if ( + is.array(this.config.controls) && + this.config.controls.includes('settings') && + this.config.settings.includes('captions') + ) { + controls.setCaptionsMenu.call(this); + } + }, + + // 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); - // 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; + // Override user preferences to avoid switching languages if a matching track is added + this.captions.language = track.language; - if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) { - currentTrack = captions.getCurrentTrack.call(this); - } + // Set caption, but don't store in localStorage as user preference + captions.set.call(this, tracks.indexOf(track)); + return; + } - if (is.track(currentTrack)) { - if (!is.empty(currentTrack.label)) { - return currentTrack.label; - } + // Toggle button if it's enabled + if (this.elements.buttons.captions) { + this.elements.buttons.captions.pressed = active; + } - if (!is.empty(currentTrack.language)) { - return track.language.toUpperCase(); - } + // Add class hook + toggleClass(this.elements.container, activeClass, active); - return i18n.get('enabled', this.config); - } + this.captions.toggled = active; - return i18n.get('disabled', this.config); - }, + // Update settings menu + controls.updateSetting.call(this, 'captions'); - // Update captions using current track's active cues - // Also optional array argument in case there isn't any track (ex: vimeo) - updateCues(input) { - // Requires UI - if (!this.supported.ui) { - return; - } + // Trigger event (not used internally) + triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled'); + } + }, - if (!is.element(this.elements.captions)) { - this.debug.warn('No captions element to render to'); - return; - } + // 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); - // Only accept array or empty input - if (!is.nullOrUndefined(input) && !Array.isArray(input)) { - this.debug.warn('updateCues: Invalid input', input); - return; - } + // Disable captions if setting to -1 + if (index === -1) { + captions.toggle.call(this, false, passive); + return; + } - let cues = input; + if (!is.number(index)) { + this.debug.warn('Invalid caption argument', index); + return; + } - // Get cues from track - if (!cues) { - const track = captions.getCurrentTrack.call(this); + if (!(index in tracks)) { + this.debug.warn('Track not found', index); + return; + } - cues = Array.from((track || {}).activeCues || []) - .map(cue => cue.getCueAsHTML()) - .map(getHTML); - } + if (this.captions.currentTrack !== index) { + this.captions.currentTrack = index; + const track = tracks[index]; + const { language } = track || {}; - // Set new caption text - const content = cues.map(cueText => cueText.trim()).join('\n'); - const changed = content !== this.elements.captions.innerHTML; + // Store reference to node for invalidation on remove + this.captions.currentTrackNode = track; - if (changed) { - // Empty the container and create a new child element - emptyElement(this.elements.captions); - const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption)); - caption.innerHTML = content; - this.elements.captions.appendChild(caption); + // Update settings menu + controls.updateSetting.call(this, 'captions'); - // Trigger event - triggerEvent.call(this, this.media, 'cuechange'); - } - }, + // When passive, don't override user preferences + if (!passive) { + this.captions.language = language; + this.storage.set({ language }); + } + + // Handle Vimeo captions + if (this.isVimeo) { + this.embed.enableTextTrack(language); + } + + // Trigger event + 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); + } + }, + + // 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 + const language = input.toLowerCase(); + this.captions.language = language; + + // Set currentTrack + const tracks = captions.getTracks.call(this); + const track = captions.findTrack.call(this, [language]); + captions.set.call(this, tracks.indexOf(track), passive); + }, + + // 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)); + }, + + // 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)); + let track; + + languages.every(language => { + track = sorted.find(t => t.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 (!is.track(currentTrack) && support.textTracks && this.captions.toggled) { + currentTrack = captions.getCurrentTrack.call(this); + } + + if (is.track(currentTrack)) { + if (!is.empty(currentTrack.label)) { + return currentTrack.label; + } + + if (!is.empty(currentTrack.language)) { + return track.language.toUpperCase(); + } + + return i18n.get('enabled', this.config); + } + + return i18n.get('disabled', this.config); + }, + + // Update captions using current track's active cues + // Also optional array argument in case there isn't any track (ex: vimeo) + updateCues(input) { + // Requires UI + if (!this.supported.ui) { + return; + } + + if (!is.element(this.elements.captions)) { + this.debug.warn('No captions element to render to'); + return; + } + + // Only accept array or empty input + if (!is.nullOrUndefined(input) && !Array.isArray(input)) { + this.debug.warn('updateCues: Invalid input', input); + return; + } + + let cues = input; + + // Get cues from track + if (!cues) { + const track = captions.getCurrentTrack.call(this); + + cues = Array.from((track || {}).activeCues || []) + .map(cue => cue.getCueAsHTML()) + .map(getHTML); + } + + // Set new caption text + const content = cues.map(cueText => cueText.trim()).join('\n'); + const changed = content !== this.elements.captions.innerHTML; + + if (changed) { + // Empty the container and create a new child element + emptyElement(this.elements.captions); + const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption)); + caption.innerHTML = content; + this.elements.captions.appendChild(caption); + + // Trigger event + triggerEvent.call(this, this.media, 'cuechange'); + } + }, }; export default captions; -- cgit v1.2.3 From 40f06e0b4cd9df8daa11e1dec1f3f51fc63424b2 Mon Sep 17 00:00:00 2001 From: Som Meaden Date: Thu, 23 Apr 2020 17:01:49 +1000 Subject: This commit addresses preloading TextTracks as outlined in feature request #1791 These changes bring Plyr captions download behaviour in line with that of the default video element in major browsers. Specifically text tracks only download as they are required for display. Previously all text tracks would download when the Plyr instance was instantiated - which could become an issue when e.g. many translations are available. For a track to be downloaded it must either be the default track, the active track when captions are toggled on, or selected from the captions menu. --- src/js/captions.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) (limited to 'src/js/captions.js') diff --git a/src/js/captions.js b/src/js/captions.js index 04da4651..60381f88 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -133,8 +133,12 @@ const captions = { }); // Turn off native caption rendering to avoid double captions + // Note: mode='hidden' forces a track to download. To ensure every track + // isn't downloaded at once, only 'showing' tracks should be reassigned // eslint-disable-next-line no-param-reassign - track.mode = 'hidden'; + if (track.mode === 'showing') { + track.mode = 'hidden'; + } // Add event listener for cue changes on.call(this, track, 'cuechange', () => captions.updateCues.call(this)); @@ -211,6 +215,14 @@ const captions = { // Trigger event (not used internally) triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled'); } + + // Wait for the call stack to clear before setting mode='hidden' + // on the active track - forcing the browser to download it + setTimeout(() => { + if (active && this.captions.toggled) { + this.captions.currentTrackNode.mode = 'hidden'; + } + }); }, // Set captions by track index -- cgit v1.2.3 From ba91f23c50a6c7efa7d39dfb7105c16c20671434 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Fri, 24 Apr 2020 00:39:26 +1000 Subject: Fix linting issues --- src/js/captions.js | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src/js/captions.js') diff --git a/src/js/captions.js b/src/js/captions.js index cf437bde..ebb678f8 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -127,6 +127,7 @@ const captions = { .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', @@ -137,6 +138,7 @@ const captions = { // isn't downloaded at once, only 'showing' tracks should be reassigned // eslint-disable-next-line no-param-reassign if (track.mode === 'showing') { + // eslint-disable-next-line no-param-reassign track.mode = 'hidden'; } -- cgit v1.2.3