aboutsummaryrefslogtreecommitdiffstats
path: root/src/js/captions.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/js/captions.js')
-rw-r--r--src/js/captions.js199
1 files changed, 199 insertions, 0 deletions
diff --git a/src/js/captions.js b/src/js/captions.js
new file mode 100644
index 00000000..847ef7ff
--- /dev/null
+++ b/src/js/captions.js
@@ -0,0 +1,199 @@
+// ==========================================================================
+// Plyr Captions
+// ==========================================================================
+
+import support from './support';
+import utils from './utils';
+import controls from './controls';
+
+const captions = {
+ // Setup captions
+ setup() {
+ // Requires UI support
+ if (!this.supported.ui) {
+ return;
+ }
+
+ // Set default language if not set
+ const stored = this.storage.get('language');
+
+ if (!utils.is.empty(stored)) {
+ this.captions.language = stored;
+ }
+
+ if (utils.is.empty(this.captions.language)) {
+ this.captions.language = this.config.captions.language.toLowerCase();
+ }
+
+ // Set captions enabled state if not set
+ if (!utils.is.boolean(this.captions.active)) {
+ const active = this.storage.get('captions');
+
+ if (utils.is.boolean(active)) {
+ this.captions.active = active;
+ } else {
+ this.captions.active = this.config.captions.active;
+ }
+ }
+
+ // Only Vimeo and HTML5 video supported at this point
+ if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {
+ // Clear menu and hide
+ if (this.config.controls.includes('settings') && this.config.settings.includes('captions')) {
+ controls.setCaptionsMenu.call(this);
+ }
+
+ return;
+ }
+
+ // Inject the container
+ if (!utils.is.element(this.elements.captions)) {
+ this.elements.captions = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.captions));
+
+ utils.insertAfter(this.elements.captions, this.elements.wrapper);
+ }
+
+ // Set the class hook
+ utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(captions.getTracks.call(this)));
+
+ // If no caption file exists, hide container for caption text
+ if (utils.is.empty(captions.getTracks.call(this))) {
+ return;
+ }
+
+ // Set language
+ captions.setLanguage.call(this);
+
+ // Enable UI
+ captions.show.call(this);
+
+ // Set available languages in list
+ if (this.config.controls.includes('settings') && this.config.settings.includes('captions')) {
+ controls.setCaptionsMenu.call(this);
+ }
+ },
+
+ // Set the captions language
+ setLanguage() {
+ // Setup HTML5 track rendering
+ if (this.isHTML5 && this.isVideo) {
+ captions.getTracks.call(this).forEach(track => {
+ // Remove previous bindings
+ 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';
+ });
+
+ // Get current track
+ const currentTrack = captions.getCurrentTrack.call(this);
+
+ // 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);
+ }
+ },
+
+ // Get the tracks
+ getTracks() {
+ // Return empty array at least
+ if (utils.is.nullOrUndefined(this.media)) {
+ return [];
+ }
+
+ // Only get accepted kinds
+ return Array.from(this.media.textTracks || []).filter(track => [
+ 'captions',
+ 'subtitles',
+ ].includes(track.kind));
+ },
+
+ // Get the current track for the current language
+ getCurrentTrack() {
+ return captions.getTracks.call(this).find(track => track.language.toLowerCase() === this.language);
+ },
+
+ // 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 active = track.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) {
+ // 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.textContent = caption.trim();
+ } else {
+ content.appendChild(caption);
+ }
+
+ // Set new caption text
+ this.elements.captions.appendChild(content);
+ } else {
+ this.debug.warn('No captions element to render to');
+ }
+ },
+
+ // Display captions container and button (for initialization)
+ show() {
+ // If there's no caption toggle, bail
+ if (!utils.is.element(this.elements.buttons.captions)) {
+ return;
+ }
+
+ // Try to load the value from storage
+ let active = this.storage.get('captions');
+
+ // Otherwise fall back to the default config
+ if (!utils.is.boolean(active)) {
+ ({ active } = this.config.captions);
+ } else {
+ this.captions.active = active;
+ }
+
+ if (active) {
+ utils.toggleClass(this.elements.container, this.config.classNames.captions.active, true);
+ utils.toggleState(this.elements.buttons.captions, true);
+ }
+ },
+};
+
+export default captions;