Merge branch 'develop' into a11y-improvements
# Conflicts: # demo/dist/demo.css # dist/plyr.css # dist/plyr.js.map # dist/plyr.min.js # dist/plyr.min.js.map # dist/plyr.polyfilled.js.map # dist/plyr.polyfilled.min.js # dist/plyr.polyfilled.min.js.map # src/js/captions.js
This commit is contained in:
		@ -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,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));
 | 
			
		||||
    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;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
            // Get current track
 | 
			
		||||
            const currentTrack = captions.getCurrentTrack.call(this);
 | 
			
		||||
        if (!utils.is.number(index)) {
 | 
			
		||||
            this.debug.warn('Invalid caption argument', index);
 | 
			
		||||
            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);
 | 
			
		||||
                }
 | 
			
		||||
        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;
 | 
			
		||||
            }
 | 
			
		||||
        } else if (this.isVimeo && this.captions.active) {
 | 
			
		||||
            this.embed.enableTextTrack(this.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 we change the active track while a cue is already displayed we need to update it
 | 
			
		||||
            captions.updateCues.call(this);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Show captions
 | 
			
		||||
        if (show) {
 | 
			
		||||
            this.toggleCaptions(true);
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    // Get the tracks
 | 
			
		||||
    getTracks() {
 | 
			
		||||
        // Return empty array at least
 | 
			
		||||
        if (utils.is.nullOrUndefined(this.media)) {
 | 
			
		||||
            return [];
 | 
			
		||||
    setLanguage(language, show = true) {
 | 
			
		||||
        if (!utils.is.string(language)) {
 | 
			
		||||
            this.debug.warn('Invalid language argument', language);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        // Normalize
 | 
			
		||||
        this.captions.language = language.toLowerCase();
 | 
			
		||||
 | 
			
		||||
        // Only get accepted kinds
 | 
			
		||||
        return Array.from(this.media.textTracks || []).filter(track => [
 | 
			
		||||
            'captions',
 | 
			
		||||
            'subtitles',
 | 
			
		||||
        ].includes(track.kind));
 | 
			
		||||
        // 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() {
 | 
			
		||||
    getCurrentTrack(fromLanguage = false) {
 | 
			
		||||
        const tracks = captions.getTracks.call(this);
 | 
			
		||||
 | 
			
		||||
        if (!tracks.length) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 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;
 | 
			
		||||
        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) {
 | 
			
		||||
            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);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        utils.dispatchEvent.call(this, this.media, 'cuechange');
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    // Set the current caption
 | 
			
		||||
    setText(input) {
 | 
			
		||||
    // 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 (utils.is.element(this.elements.captions)) {
 | 
			
		||||
            const content = utils.createElement('span');
 | 
			
		||||
 | 
			
		||||
            // Empty the container
 | 
			
		||||
            utils.emptyElement(this.elements.captions);
 | 
			
		||||
 | 
			
		||||
            // Default to empty
 | 
			
		||||
            const caption = !utils.is.nullOrUndefined(input) ? input : '';
 | 
			
		||||
 | 
			
		||||
            // Set the span content
 | 
			
		||||
            if (utils.is.string(caption)) {
 | 
			
		||||
                content.innerText = caption.trim();
 | 
			
		||||
            } else {
 | 
			
		||||
                content.appendChild(caption);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Set new caption text
 | 
			
		||||
            this.elements.captions.appendChild(content);
 | 
			
		||||
        } else {
 | 
			
		||||
        if (!utils.is.element(this.elements.captions)) {
 | 
			
		||||
            this.debug.warn('No captions element to render to');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Only accept array or empty input
 | 
			
		||||
        if (!utils.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(utils.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
 | 
			
		||||
            utils.emptyElement(this.elements.captions);
 | 
			
		||||
            const caption = utils.createElement('span', utils.getAttributesFromSelector(this.config.selectors.caption));
 | 
			
		||||
            caption.innerHTML = content;
 | 
			
		||||
            this.elements.captions.appendChild(caption);
 | 
			
		||||
 | 
			
		||||
            // Trigger event
 | 
			
		||||
            utils.dispatchEvent.call(this, this.media, 'cuechange');
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										69
									
								
								src/js/controls.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										69
									
								
								src/js/controls.js
									
									
									
									
										vendored
									
									
								
							@ -373,7 +373,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', {
 | 
			
		||||
@ -704,8 +704,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);
 | 
			
		||||
@ -746,16 +751,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:
 | 
			
		||||
@ -855,10 +851,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);
 | 
			
		||||
@ -867,34 +863,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);
 | 
			
		||||
    },
 | 
			
		||||
@ -951,8 +944,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);
 | 
			
		||||
 | 
			
		||||
@ -60,7 +60,7 @@ const defaults = {
 | 
			
		||||
    // Sprite (for icons)
 | 
			
		||||
    loadSprite: true,
 | 
			
		||||
    iconPrefix: 'plyr',
 | 
			
		||||
    iconUrl: 'https://cdn.plyr.io/3.3.10/plyr.svg',
 | 
			
		||||
    iconUrl: 'https://cdn.plyr.io/3.3.11/plyr.svg',
 | 
			
		||||
 | 
			
		||||
    // Blank video (used to prevent errors on source change)
 | 
			
		||||
    blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
 | 
			
		||||
@ -334,6 +334,7 @@ const defaults = {
 | 
			
		||||
        },
 | 
			
		||||
        progress: '.plyr__progress',
 | 
			
		||||
        captions: '.plyr__captions',
 | 
			
		||||
        caption: '.plyr__caption',
 | 
			
		||||
        menu: {
 | 
			
		||||
            quality: '.js-plyr__menu__list--quality',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@ -414,7 +414,7 @@ class Listeners {
 | 
			
		||||
            'keyup',
 | 
			
		||||
            'keydown',
 | 
			
		||||
        ]).join(' '), event => {
 | 
			
		||||
            let detail = {};
 | 
			
		||||
            let {detail = {}} = event;
 | 
			
		||||
 | 
			
		||||
            // Get error details from media
 | 
			
		||||
            if (event.type === 'error') {
 | 
			
		||||
@ -523,7 +523,7 @@ class Listeners {
 | 
			
		||||
                proxy(
 | 
			
		||||
                    event,
 | 
			
		||||
                    () => {
 | 
			
		||||
                        this.player.language = event.target.value;
 | 
			
		||||
                        this.player.currentTrack = Number(event.target.value);
 | 
			
		||||
                        showHomeTab();
 | 
			
		||||
                    },
 | 
			
		||||
                    'language',
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,9 @@ import utils from './../utils';
 | 
			
		||||
 | 
			
		||||
// Set playback state and trigger change (only on actual change)
 | 
			
		||||
function assurePlaybackState(play) {
 | 
			
		||||
    if (play && !this.embed.hasPlayed) {
 | 
			
		||||
        this.embed.hasPlayed = true;
 | 
			
		||||
    }
 | 
			
		||||
    if (this.media.paused === play) {
 | 
			
		||||
        this.media.paused = !play;
 | 
			
		||||
        utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause');
 | 
			
		||||
@ -153,19 +156,20 @@ const vimeo = {
 | 
			
		||||
 | 
			
		||||
                // Get current paused state and volume etc
 | 
			
		||||
                const { embed, media, paused, volume } = player;
 | 
			
		||||
                const restorePause = paused && !embed.hasPlayed;
 | 
			
		||||
 | 
			
		||||
                // Set seeking state and trigger event
 | 
			
		||||
                media.seeking = true;
 | 
			
		||||
                utils.dispatchEvent.call(player, media, 'seeking');
 | 
			
		||||
 | 
			
		||||
                // If paused, mute until seek is complete
 | 
			
		||||
                Promise.resolve(paused && embed.setVolume(0))
 | 
			
		||||
                Promise.resolve(restorePause && embed.setVolume(0))
 | 
			
		||||
                    // Seek
 | 
			
		||||
                    .then(() => embed.setCurrentTime(time))
 | 
			
		||||
                    // Restore paused
 | 
			
		||||
                    .then(() => paused && embed.pause())
 | 
			
		||||
                    .then(() => restorePause && embed.pause())
 | 
			
		||||
                    // Restore volume
 | 
			
		||||
                    .then(() => paused && embed.setVolume(volume))
 | 
			
		||||
                    .then(() => restorePause && embed.setVolume(volume))
 | 
			
		||||
                    .catch(() => {
 | 
			
		||||
                        // Do nothing
 | 
			
		||||
                    });
 | 
			
		||||
@ -301,17 +305,20 @@ 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', () => {
 | 
			
		||||
            // Assure state and events are updated on autoplay
 | 
			
		||||
            player.embed.getPaused().then(paused => {
 | 
			
		||||
                assurePlaybackState.call(player, !paused);
 | 
			
		||||
                if (!paused) {
 | 
			
		||||
                    utils.dispatchEvent.call(player, player.media, 'playing');
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (utils.is.element(player.embed.element) && player.supported.ui) {
 | 
			
		||||
                const frame = player.embed.element;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -66,6 +66,9 @@ function mapQualityUnits(levels) {
 | 
			
		||||
 | 
			
		||||
// Set playback state and trigger change (only on actual change)
 | 
			
		||||
function assurePlaybackState(play) {
 | 
			
		||||
    if (play && !this.embed.hasPlayed) {
 | 
			
		||||
        this.embed.hasPlayed = true;
 | 
			
		||||
    }
 | 
			
		||||
    if (this.media.paused === play) {
 | 
			
		||||
        this.media.paused = !play;
 | 
			
		||||
        utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause');
 | 
			
		||||
@ -469,7 +472,7 @@ const youtube = {
 | 
			
		||||
 | 
			
		||||
                        case 1:
 | 
			
		||||
                            // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
 | 
			
		||||
                            if (player.media.paused) {
 | 
			
		||||
                            if (player.media.paused && !player.embed.hasPlayed) {
 | 
			
		||||
                                player.media.pause();
 | 
			
		||||
                            } else {
 | 
			
		||||
                                assurePlaybackState.call(player, true);
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
// ==========================================================================
 | 
			
		||||
// Plyr
 | 
			
		||||
// plyr.js v3.3.10
 | 
			
		||||
// plyr.js v3.3.11
 | 
			
		||||
// https://github.com/sampotts/plyr
 | 
			
		||||
// License: The MIT License (MIT)
 | 
			
		||||
// ==========================================================================
 | 
			
		||||
@ -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
 | 
			
		||||
@ -851,61 +851,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);
 | 
			
		||||
 | 
			
		||||
        // Update captions
 | 
			
		||||
        captions.setLanguage.call(this);
 | 
			
		||||
 | 
			
		||||
        // Trigger an event
 | 
			
		||||
        utils.dispatchEvent.call(this, this.media, 'languagechange');
 | 
			
		||||
    set currentTrack(input) {
 | 
			
		||||
        captions.set.call(this, input);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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() {
 | 
			
		||||
        return this.captions.language;
 | 
			
		||||
        return (captions.getCurrentTrack.call(this) || {}).language;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -1156,7 +1130,7 @@ class Plyr {
 | 
			
		||||
        } else if (utils.is.nodeList(selector)) {
 | 
			
		||||
            targets = Array.from(selector);
 | 
			
		||||
        } else if (utils.is.array(selector)) {
 | 
			
		||||
            targets = selector.filter(i => utils.is.element(i));
 | 
			
		||||
            targets = selector.filter(utils.is.element);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (utils.is.empty(targets)) {
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
// ==========================================================================
 | 
			
		||||
// Plyr Polyfilled Build
 | 
			
		||||
// plyr.js v3.3.10
 | 
			
		||||
// plyr.js v3.3.11
 | 
			
		||||
// https://github.com/sampotts/plyr
 | 
			
		||||
// License: The MIT License (MIT)
 | 
			
		||||
// ==========================================================================
 | 
			
		||||
 | 
			
		||||
@ -11,63 +11,64 @@ const utils = {
 | 
			
		||||
    // Check variable types
 | 
			
		||||
    is: {
 | 
			
		||||
        object(input) {
 | 
			
		||||
            return this.getConstructor(input) === Object;
 | 
			
		||||
            return utils.getConstructor(input) === Object;
 | 
			
		||||
        },
 | 
			
		||||
        number(input) {
 | 
			
		||||
            return this.getConstructor(input) === Number && !Number.isNaN(input);
 | 
			
		||||
            return utils.getConstructor(input) === Number && !Number.isNaN(input);
 | 
			
		||||
        },
 | 
			
		||||
        string(input) {
 | 
			
		||||
            return this.getConstructor(input) === String;
 | 
			
		||||
            return utils.getConstructor(input) === String;
 | 
			
		||||
        },
 | 
			
		||||
        boolean(input) {
 | 
			
		||||
            return this.getConstructor(input) === Boolean;
 | 
			
		||||
            return utils.getConstructor(input) === Boolean;
 | 
			
		||||
        },
 | 
			
		||||
        function(input) {
 | 
			
		||||
            return this.getConstructor(input) === Function;
 | 
			
		||||
            return utils.getConstructor(input) === Function;
 | 
			
		||||
        },
 | 
			
		||||
        array(input) {
 | 
			
		||||
            return !this.nullOrUndefined(input) && Array.isArray(input);
 | 
			
		||||
            return !utils.is.nullOrUndefined(input) && Array.isArray(input);
 | 
			
		||||
        },
 | 
			
		||||
        weakMap(input) {
 | 
			
		||||
            return this.instanceof(input, WeakMap);
 | 
			
		||||
            return utils.is.instanceof(input, WeakMap);
 | 
			
		||||
        },
 | 
			
		||||
        nodeList(input) {
 | 
			
		||||
            return this.instanceof(input, NodeList);
 | 
			
		||||
            return utils.is.instanceof(input, NodeList);
 | 
			
		||||
        },
 | 
			
		||||
        element(input) {
 | 
			
		||||
            return this.instanceof(input, Element);
 | 
			
		||||
            return utils.is.instanceof(input, Element);
 | 
			
		||||
        },
 | 
			
		||||
        textNode(input) {
 | 
			
		||||
            return this.getConstructor(input) === Text;
 | 
			
		||||
            return utils.getConstructor(input) === Text;
 | 
			
		||||
        },
 | 
			
		||||
        event(input) {
 | 
			
		||||
            return this.instanceof(input, Event);
 | 
			
		||||
            return utils.is.instanceof(input, Event);
 | 
			
		||||
        },
 | 
			
		||||
        cue(input) {
 | 
			
		||||
            return this.instanceof(input, window.TextTrackCue) || this.instanceof(input, window.VTTCue);
 | 
			
		||||
            return utils.is.instanceof(input, window.TextTrackCue) || utils.is.instanceof(input, window.VTTCue);
 | 
			
		||||
        },
 | 
			
		||||
        track(input) {
 | 
			
		||||
            return this.instanceof(input, TextTrack) || (!this.nullOrUndefined(input) && this.string(input.kind));
 | 
			
		||||
            return utils.is.instanceof(input, TextTrack) || (!utils.is.nullOrUndefined(input) && utils.is.string(input.kind));
 | 
			
		||||
        },
 | 
			
		||||
        url(input) {
 | 
			
		||||
            return !this.nullOrUndefined(input) && /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test(input);
 | 
			
		||||
            return !utils.is.nullOrUndefined(input) && /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test(input);
 | 
			
		||||
        },
 | 
			
		||||
        nullOrUndefined(input) {
 | 
			
		||||
            return input === null || typeof input === 'undefined';
 | 
			
		||||
        },
 | 
			
		||||
        empty(input) {
 | 
			
		||||
            return (
 | 
			
		||||
                this.nullOrUndefined(input) ||
 | 
			
		||||
                ((this.string(input) || this.array(input) || this.nodeList(input)) && !input.length) ||
 | 
			
		||||
                (this.object(input) && !Object.keys(input).length)
 | 
			
		||||
                utils.is.nullOrUndefined(input) ||
 | 
			
		||||
                ((utils.is.string(input) || utils.is.array(input) || utils.is.nodeList(input)) && !input.length) ||
 | 
			
		||||
                (utils.is.object(input) && !Object.keys(input).length)
 | 
			
		||||
            );
 | 
			
		||||
        },
 | 
			
		||||
        instanceof(input, constructor) {
 | 
			
		||||
            return Boolean(input && constructor && input instanceof constructor);
 | 
			
		||||
        },
 | 
			
		||||
        getConstructor(input) {
 | 
			
		||||
            return !this.nullOrUndefined(input) ? input.constructor : null;
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    getConstructor(input) {
 | 
			
		||||
        return !utils.is.nullOrUndefined(input) ? input.constructor : null;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    // Unfortunately, due to mixed support, UA sniffing is required
 | 
			
		||||
@ -603,16 +604,16 @@ const utils = {
 | 
			
		||||
    formatTime(time = 0, displayHours = false, inverted = false) {
 | 
			
		||||
        // Bail if the value isn't a number
 | 
			
		||||
        if (!utils.is.number(time)) {
 | 
			
		||||
            return this.formatTime(null, displayHours, inverted);
 | 
			
		||||
            return utils.formatTime(null, displayHours, inverted);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Format time component to add leading zero
 | 
			
		||||
        const format = value => `0${value}`.slice(-2);
 | 
			
		||||
 | 
			
		||||
        // Breakdown to hours, mins, secs
 | 
			
		||||
        let hours = this.getHours(time);
 | 
			
		||||
        const mins = this.getMinutes(time);
 | 
			
		||||
        const secs = this.getSeconds(time);
 | 
			
		||||
        let hours = utils.getHours(time);
 | 
			
		||||
        const mins = utils.getMinutes(time);
 | 
			
		||||
        const secs = utils.getSeconds(time);
 | 
			
		||||
 | 
			
		||||
        // Do we need to display hours?
 | 
			
		||||
        if (displayHours || hours > 0) {
 | 
			
		||||
@ -770,10 +771,10 @@ const utils = {
 | 
			
		||||
 | 
			
		||||
        // Parse URL if needed
 | 
			
		||||
        if (input.startsWith('http://') || input.startsWith('https://')) {
 | 
			
		||||
            ({ search } = this.parseUrl(input));
 | 
			
		||||
            ({ search } = utils.parseUrl(input));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.is.empty(search)) {
 | 
			
		||||
        if (utils.is.empty(search)) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -809,6 +810,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));
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user