aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSam Potts <sam@potts.es>2018-06-11 13:21:05 +1000
committerGitHub <noreply@github.com>2018-06-11 13:21:05 +1000
commit7d26f41d646d37e52fa774c99c603aa0663c51d4 (patch)
tree40a352afc6088130aad907f2f5e3bdb54deb46b4
parentf37f465ce452f33a0b1b06f2fd07ed014906b715 (diff)
parent41012a9843558f67bac7969ffe5bf7161a10893c (diff)
downloadplyr-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)
-rw-r--r--readme.md3
-rw-r--r--src/js/captions.js246
-rw-r--r--src/js/controls.js69
-rw-r--r--src/js/defaults.js1
-rw-r--r--src/js/listeners.js2
-rw-r--r--src/js/plugins/vimeo.js11
-rw-r--r--src/js/plyr.js72
-rw-r--r--src/js/utils.js7
-rw-r--r--src/sass/components/captions.scss2
9 files changed, 214 insertions, 199 deletions
diff --git a/readme.md b/readme.md
index 9f4819e3..248d324f 100644
--- a/readme.md
+++ b/readme.md
@@ -407,7 +407,8 @@ player.fullscreen.active; // false;
| `source` | ✓ | ✓ | Gets or sets the current source for the player. The setter accepts an object. See [source setter](#source-setter) below for examples. |
| `poster` | ✓ | ✓ | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. |
| `autoplay` | ✓ | ✓ | Gets or sets the autoplay state of the player. The setter accepts a boolean. |
-| `language` | ✓ | ✓ | Gets or sets the preferred captions language for the player. The setter accepts an ISO two-letter language code. Support for the languages is dependent on the captions you include. |
+| `currentTrack` | ✓ | ✓ | Gets or sets the caption track by index. `-1` means the track is missing or captions is not active |
+| `language` | ✓ | ✓ | Gets or sets the preferred captions language for the player. The setter accepts an ISO two-letter language code. Support for the languages is dependent on the captions you include. If your captions don't have any language data, or if you have multiple tracks with the same language, you may want to use `currentTrack` instead. |
| `fullscreen.active` | ✓ | - | Returns a boolean indicating if the current player is in fullscreen mode. |
| `fullscreen.enabled` | ✓ | - | Returns a boolean indicating if the current player has fullscreen enabled. |
| `pip` | ✓ | ✓ | Gets or sets the picture-in-picture state of the player. The setter accepts a boolean. This currently only supported on Safari 10+ on MacOS Sierra+ and iOS 10+. |
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;