diff options
| author | Sam Potts <sam@potts.es> | 2018-06-11 13:21:05 +1000 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-06-11 13:21:05 +1000 | 
| commit | 7d26f41d646d37e52fa774c99c603aa0663c51d4 (patch) | |
| tree | 40a352afc6088130aad907f2f5e3bdb54deb46b4 /src | |
| parent | f37f465ce452f33a0b1b06f2fd07ed014906b715 (diff) | |
| parent | 41012a9843558f67bac7969ffe5bf7161a10893c (diff) | |
| download | plyr-7d26f41d646d37e52fa774c99c603aa0663c51d4.tar.lz plyr-7d26f41d646d37e52fa774c99c603aa0663c51d4.tar.xz plyr-7d26f41d646d37e52fa774c99c603aa0663c51d4.zip | |
Merge pull request #1015 from friday/captions-fixes-again
Captions rewrite (use index internally to support missing or duplicate languages)
Diffstat (limited to 'src')
| -rw-r--r-- | src/js/captions.js | 246 | ||||
| -rw-r--r-- | src/js/controls.js | 69 | ||||
| -rw-r--r-- | src/js/defaults.js | 1 | ||||
| -rw-r--r-- | src/js/listeners.js | 2 | ||||
| -rw-r--r-- | src/js/plugins/vimeo.js | 11 | ||||
| -rw-r--r-- | src/js/plyr.js | 72 | ||||
| -rw-r--r-- | src/js/utils.js | 7 | ||||
| -rw-r--r-- | src/sass/components/captions.scss | 2 | 
8 files changed, 212 insertions, 198 deletions
| diff --git a/src/js/captions.js b/src/js/captions.js index 30c4bc74..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)); +                }); +        } + +        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);          } -        // Toggle the class hooks -        utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(captions.getTracks.call(this))); +        // 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,70 +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', event => captions.setCue.call(this, event)); - -                // Turn off native caption rendering to avoid double captions -                // eslint-disable-next-line -                track.mode = 'hidden'; -            }); +    set(index, setLanguage = true, show = true) { +        const tracks = captions.getTracks.call(this); -            // Get current track -            const currentTrack = captions.getCurrentTrack.call(this); +        // Disable captions if setting to -1 +        if (index === -1) { +            this.toggleCaptions(false); +            return; +        } -            // Check if suported kind -            if (utils.is.track(currentTrack)) { -                // If we change the active track while a cue is already displayed we need to update it -                if (Array.from(currentTrack.activeCues || []).length) { -                    captions.setCue.call(this, currentTrack); -                } -            } -        } else if (this.isVimeo && this.captions.active) { -            this.embed.enableTextTrack(this.language); +        if (!utils.is.number(index)) { +            this.debug.warn('Invalid caption argument', index); +            return;          } -    }, -    // Get the tracks -    getTracks() { -        // Return empty array at least -        if (utils.is.nullOrUndefined(this.media)) { -            return []; +        if (!(index in tracks)) { +            this.debug.warn('Track not found', index); +            return;          } -        // Only get accepted kinds -        return Array.from(this.media.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; +            } -        if (!tracks.length) { -            return null; +            // Handle Vimeo captions +            if (this.isVimeo) { +                this.embed.enableTextTrack(language); +            } + +            // 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(); -        return track; +        // 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 +        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 @@ -193,56 +241,48 @@ const captions = {          return i18n.get('disabled', this.config);      }, -    // Display active caption if it contains text -    setCue(input) { -        // Get the track from the event if needed -        const track = utils.is.event(input) ? input.target : input; -        const { activeCues } = track; -        const active = activeCues.length && activeCues[0]; -        const currentTrack = captions.getCurrentTrack.call(this); - -        // Only display current track -        if (track !== currentTrack) { +    // 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;          } -        // Display a cue, if there is one -        if (utils.is.cue(active)) { -            captions.setText.call(this, active.getCueAsHTML()); -        } else { -            captions.setText.call(this, null); +        if (!utils.is.element(this.elements.captions)) { +            this.debug.warn('No captions element to render to'); +            return;          } -        utils.dispatchEvent.call(this, this.media, 'cuechange'); -    }, - -    // Set the current caption -    setText(input) { -        // Requires UI -        if (!this.supported.ui) { +        // Only accept array or empty input +        if (!utils.is.nullOrUndefined(input) && !Array.isArray(input)) { +            this.debug.warn('updateCues: Invalid input', input);              return;          } -        if (utils.is.element(this.elements.captions)) { -            const content = utils.createElement('span'); +        let cues = input; -            // Empty the container -            utils.emptyElement(this.elements.captions); +        // Get cues from track +        if (!cues) { +            const track = captions.getCurrentTrack.call(this); +            cues = Array.from((track || {}).activeCues || []) +                .map(cue => cue.getCueAsHTML()) +                .map(utils.getHTML); +        } -            // Default to empty -            const caption = !utils.is.nullOrUndefined(input) ? input : ''; +        // Set new caption text +        const content = cues.map(cueText => cueText.trim()).join('\n'); +        const changed = content !== this.elements.captions.innerHTML; -            // Set the span content -            if (utils.is.string(caption)) { -                content.innerText = caption.trim(); -            } else { -                content.appendChild(caption); -            } +        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)); +            caption.innerHTML = content; +            this.elements.captions.appendChild(caption); -            // Set new caption text -            this.elements.captions.appendChild(content); -        } else { -            this.debug.warn('No captions element to render to'); +            // Trigger event +            utils.dispatchEvent.call(this, this.media, 'cuechange');          }      },  }; diff --git a/src/js/controls.js b/src/js/controls.js index 20518f9c..058e636f 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -376,7 +376,7 @@ const controls = {      },      // Create a settings menu item -    createMenuItem(value, list, type, title, badge = null, checked = false) { +    createMenuItem({value, list, type, title, badge = null, checked = false}) {          const item = utils.createElement('li');          const label = utils.createElement('label', { @@ -680,8 +680,13 @@ const controls = {                  return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;              })              .forEach(quality => { -                const label = controls.getLabel.call(this, 'quality', quality); -                controls.createMenuItem.call(this, quality, list, type, label, getBadge(quality)); +                controls.createMenuItem.call(this, { +                    value: quality, +                    list, +                    type, +                    title: controls.getLabel.call(this, 'quality', quality), +                    badge: getBadge(quality), +                });              });          controls.updateSetting.call(this, type, list); @@ -722,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: @@ -831,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); @@ -843,34 +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, -                track.language, -                list, -                'language', -                track.label, -                track.language !== 'enabled' ? controls.createBadge.call(this, track.language.toUpperCase()) : null, -                track.language.toLowerCase() === this.language, -            ); -        }); +        options.forEach(controls.createMenuItem.bind(this));          controls.updateSetting.call(this, type, list);      }, @@ -927,8 +920,12 @@ const controls = {          // Create items          this.options.speed.forEach(speed => { -            const label = controls.getLabel.call(this, 'speed', speed); -            controls.createMenuItem.call(this, speed, list, type, label); +            controls.createMenuItem.call(this, { +                value: speed, +                list, +                type, +                title: controls.getLabel.call(this, 'speed', speed), +            });          });          controls.updateSetting.call(this, type, list); diff --git a/src/js/defaults.js b/src/js/defaults.js index 16df8624..b1bdaa65 100644 --- a/src/js/defaults.js +++ b/src/js/defaults.js @@ -328,6 +328,7 @@ const defaults = {          },          progress: '.plyr__progress',          captions: '.plyr__captions', +        caption: '.plyr__caption',          menu: {              quality: '.js-plyr__menu__list--quality',          }, 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/plugins/vimeo.js b/src/js/plugins/vimeo.js index 83cb80bf..652c920c 100644 --- a/src/js/plugins/vimeo.js +++ b/src/js/plugins/vimeo.js @@ -305,14 +305,9 @@ const vimeo = {              captions.setup.call(player);          }); -        player.embed.on('cuechange', data => { -            let cue = null; - -            if (data.cues.length) { -                cue = utils.stripHTML(data.cues[0].text); -            } - -            captions.setText.call(player, cue); +        player.embed.on('cuechange', ({ cues = [] }) => { +            const strippedCues = cues.map(cue => utils.stripHTML(cue.text)); +            captions.updateCues.call(player, strippedCues);          });          player.embed.on('loaded', () => { diff --git a/src/js/plyr.js b/src/js/plyr.js index 5c51d617..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.warn(`Unsupported language option: ${language}`); -            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.setText.call(this, null); +    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;      }      /** diff --git a/src/js/utils.js b/src/js/utils.js index a7d2ada4..109de031 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -831,6 +831,13 @@ const utils = {          return fragment.firstChild.innerText;      }, +    // Like outerHTML, but also works for DocumentFragment +    getHTML(element) { +        const wrapper = document.createElement('div'); +        wrapper.appendChild(element); +        return wrapper.innerHTML; +    }, +      // Get aspect ratio for dimensions      getAspectRatio(width, height) {          const getRatio = (w, h) => (h === 0 ? w : getRatio(h, w % h)); diff --git a/src/sass/components/captions.scss b/src/sass/components/captions.scss index 9dfc2be8..8fce581a 100644 --- a/src/sass/components/captions.scss +++ b/src/sass/components/captions.scss @@ -21,7 +21,7 @@      transition: transform 0.4s ease-in-out;      width: 100%; -    span { +    .plyr__caption {          background: $plyr-captions-bg;          border-radius: 2px;          box-decoration-break: clone; | 
