import $ from 'jquery'; function addCaptionFunctions(AblePlayer) { AblePlayer.prototype.updateCaption = function (time) { if ( !this.usingYouTubeCaptions && !this.usingVimeoCaptions && typeof this.$captionsWrapper !== "undefined" ) { if (this.captionsOn) { this.$captionsWrapper.show(); if (typeof time !== "undefined") { this.showCaptions(time); } } else if (this.$captionsWrapper) { this.$captionsWrapper.hide(); this.prefCaptions = 0; } } }; AblePlayer.prototype.updateCaptionsMenu = function (lang) { // uncheck all previous menu items this.captionsPopup.find("li").attr("aria-checked", "false"); if (typeof lang === "undefined") { // check the last menu item (captions off) this.captionsPopup.find("li").last().attr("aria-checked", "true"); } else { // check the newly selected lang this.captionsPopup .find("li[lang=" + lang + "]") .attr("aria-checked", "true"); } }; AblePlayer.prototype.getCaptionClickFunction = function (track) { // Returns the function used when a caption is clicked in the captions menu. // Not called if user clicks "Captions off". Instead, that triggers getCaptionOffFunction() var thisObj = this; return function () { thisObj.selectedCaptions = track; thisObj.captionLang = track.language; thisObj.currentCaption = -1; if (thisObj.usingYouTubeCaptions) { if (thisObj.captionsOn) { // Two things must be true in order for setOption() to work: // The YouTube caption module must be loaded // and the video must have started playing if ( thisObj.youTubePlayer.getOptions("captions") && thisObj.startedPlaying ) { thisObj.youTubePlayer.setOption("captions", "track", { languageCode: thisObj.captionLang, }); } else { // the two conditions were not met // try again to set the language after onApiChange event is triggered // meanwhile, the following variable will hold the value thisObj.captionLangPending = thisObj.captionLang; } } else { if (thisObj.youTubePlayer.getOptions("captions")) { thisObj.youTubePlayer.setOption("captions", "track", { languageCode: thisObj.captionLang, }); } else { thisObj.youTubePlayer.loadModule("captions"); thisObj.captionLangPending = thisObj.captionLang; } } } else if (thisObj.usingVimeoCaptions) { thisObj.vimeoPlayer .enableTextTrack(thisObj.captionLang) .catch(function (error) { switch (error.name) { case 'InvalidTrackLanguageError': // There is no text track for the specified language console.log(`No Vimeo text track is available in the specified language (${thisObj.captionLang})`); break; case 'InvalidTrackError': // There is no such text track console.log('No Vimeo text track is available'); break; default: // some other error occurred console.log('Error enabling Vimeo text track'); break; } }); } else { // using local track elements for captions/subtitles thisObj.syncTrackLanguages("captions", thisObj.captionLang); if (!thisObj.swappingSrc) { thisObj.updateCaption(thisObj.elapsed); thisObj.showDescription(thisObj.elapsed); } } thisObj.captionsOn = true; // stopgap to prevent spacebar in Firefox from reopening popup // immediately after closing it (used in handleCaptionToggle()) thisObj.hidingPopup = true; thisObj.captionsPopup.hide(); thisObj.$ccButton.attr("aria-expanded", "false"); if (thisObj.mediaType === "audio") { thisObj.$captionsContainer.removeClass("captions-off"); } // Ensure stopgap gets cancelled if handleCaptionToggle() isn't called // e.g., if user triggered button with Enter or mouse click, not spacebar setTimeout(function () { thisObj.hidingPopup = false; }, 100); thisObj.updateCaptionsMenu(thisObj.captionLang); thisObj.waitThenFocus(thisObj.$ccButton); // save preference to cookie thisObj.prefCaptions = 1; thisObj.updatePreferences("prefCaptions"); thisObj.refreshControls("captions"); }; }; // Returns the function used when the "Captions Off" button is clicked in the captions tooltip. AblePlayer.prototype.getCaptionOffFunction = function () { var thisObj = this; return function () { if (thisObj.player == "youtube") { thisObj.youTubePlayer.unloadModule("captions"); } else if (thisObj.usingVimeoCaptions) { thisObj.vimeoPlayer.disableTextTrack(); } thisObj.captionsOn = false; thisObj.currentCaption = -1; if (thisObj.mediaType === "audio") { thisObj.$captionsContainer.addClass("captions-off"); } // stopgap to prevent spacebar in Firefox from reopening popup // immediately after closing it (used in handleCaptionToggle()) thisObj.hidingPopup = true; thisObj.captionsPopup.hide(); thisObj.$ccButton.attr("aria-expanded", "false"); // Ensure stopgap gets cancelled if handleCaptionToggle() isn't called // e.g., if user triggered button with Enter or mouse click, not spacebar setTimeout(function () { thisObj.hidingPopup = false; }, 100); thisObj.updateCaptionsMenu(); thisObj.waitThenFocus(thisObj.$ccButton); // save preference to cookie thisObj.prefCaptions = 0; thisObj.updatePreferences("prefCaptions"); if (!this.swappingSrc) { thisObj.refreshControls("captions"); thisObj.updateCaption(); } }; }; AblePlayer.prototype.showCaptions = function (now) { var c, thisCaption, nextCaption, captionText, announceText, announcement, availableTime, rate, cueLength, estimatedTime; var cues; if (null !== this.selectedCaptions.cues && this.selectedCaptions.cues.length) { cues = this.selectedCaptions.cues; } else if (this.captions.length >= 1) { cues = this.captions[0].cues; } else { cues = []; } for (c = 0; c < cues.length; c++) { if (cues[c].start <= now && cues[c].end > now) { thisCaption = c; nextCaption = cues[ c + 1 ]; break; } } if (typeof thisCaption !== "undefined") { if (this.currentCaption !== thisCaption) { // it's time to load the new caption into the container div captionText = this.flattenCueForCaption(cues[thisCaption]).replace( /\n/g, "
" ); // If preference enabled to voice captions, send to synthesizer. if ( this.speechEnabled && this.prefCaptionsSpeak == 1 ) { announceText = new DOMParser().parseFromString( captionText, 'text/html' ); announcement = announceText.body.textContent || ''; availableTime = ( thisCaption ) ? nextCaption.start - cues[thisCaption].start : 0; rate = false, cueLength, estimatedTime; if ( availableTime ) { cueLength = announcement.trim().split(/\W+/).length; estimatedTime = Math.round( ( ( cueLength ) / 135 ) * 60 ); rate = ( estimatedTime / availableTime ); } // use browser's built-in speech synthesis this.announceText( 'caption', announcement, rate ); } this.$captionsDiv.html(captionText); this.currentCaption = thisCaption; if (captionText.length === 0) { // hide captionsDiv; otherwise background-color is visible due to padding this.$captionsDiv.css("display", "none"); } else { this.$captionsDiv.css("display", "inline-block"); } } } else { this.$captionsDiv.html("").css("display", "none"); this.currentCaption = -1; } }; AblePlayer.prototype.flattenCueForCaption = function (cue) { // Takes a cue and returns the caption text to display // Also used for chapters // Support for 'i' and 'b' tags added in 2.3.66 // TODO: Add support for 'c' (class) and 'ruby' // c (class): Some text // Classes can be used to modify other tags too (e.g., ) // If tag, should be rendered as a // ruby: http://www.w3schools.com/tags/tag_ruby.asp // WebVTT also supports 'u' (underline) // I see no reason to support that in Able Player. // If it's available authors are likely to use it incorrectly // where or should be used instead // Here are the rare use cases where an underline is appropriate on the web: // http://html5doctor.com/u-element/ var result = []; var flattenComponent = function (component) { var result = [], ii; if (component.type === "string") { result.push(component.value); } else if (component.type === "v") { result.push("(" + component.value + ")"); for (ii = 0; ii < component.children.length; ii++) { result.push(flattenComponent(component.children[ii])); } } else if (component.type === "i") { result.push(""); for (ii = 0; ii < component.children.length; ii++) { result.push(flattenComponent(component.children[ii])); } result.push(""); } else if (component.type === "b") { result.push(""); for (ii = 0; ii < component.children.length; ii++) { result.push(flattenComponent(component.children[ii])); } result.push(""); } else { for (ii = 0; ii < component.children.length; ii++) { result.push(flattenComponent(component.children[ii])); } } return result.join(""); }; if (typeof cue.components !== "undefined") { for (var ii = 0; ii < cue.components.children.length; ii++) { result.push(flattenComponent(cue.components.children[ii])); } } return result.join(""); }; AblePlayer.prototype.getCaptionsOptions = function (pref) { var options = []; switch (pref) { case "prefCaptionsFont": options[0] = ["serif", this.translate( 'serif', 'serif' )]; options[1] = ["sans-serif", this.translate( 'sans', 'sans-serif' )]; options[2] = ["cursive", this.translate( 'cursive', 'cursive' )]; options[3] = ["fantasy", this.translate( 'fantasy', 'fantasy' )]; options[4] = ["monospace", this.translate( 'monospace', 'monospace' )]; break; case "prefCaptionsColor": case "prefCaptionsBGColor": // HTML color values must be in English options[0] = ["white", this.translate( 'white', 'white' )]; options[1] = ["yellow", this.translate( 'yellow', 'yellow' )]; options[2] = ["green", this.translate( 'green', 'green' )]; options[3] = ["cyan", this.translate( 'cyan', 'cyan' )]; options[4] = ["blue", this.translate( 'blue', 'blue' )]; options[5] = ["magenta", this.translate( 'magenta', 'magenta' )]; options[6] = ["red", this.translate( 'red', 'red' )]; options[7] = ["black", this.translate( 'black', 'black' )]; break; case "prefCaptionsSize": options[0] = "75%"; options[1] = "100%"; options[2] = "125%"; options[3] = "150%"; options[4] = "200%"; break; case "prefCaptionsOpacity": options[0] = "0%"; options[1] = "25%"; options[2] = "50%"; options[3] = "75%"; options[4] = "100%"; break; case "prefCaptionsStyle": options[0] = this.translate( 'captionsStylePopOn', 'Pop-on' ); options[1] = this.translate( 'captionsStyleRollUp', 'Roll-up' ); break; case "prefCaptionsPosition": options[0] = "overlay"; options[1] = "below"; break; case "prefCaptionsSpeak": options[0] = ["0", this.translate( 'off', 'Off' ) ]; options[1] = ["1", this.translate( 'on', 'On' ) ]; break; case "prefCaptionsVoice": options[0] = null; // set later. break; case "prefCaptionsPitch": options[0] = null; // set later. break; case "prefCaptionsRate": options[0] = null; // set later. break; case "prefCaptionsVolume": options[0] = null; // set later. break; } return options; }; AblePlayer.prototype.translatePrefs = function (pref, value, outputFormat) { // translate current value of pref to a value supported by outputformat if (outputFormat == "youtube") { if (pref === "size") { // YouTube font sizes are a range from -1 to 3 (0 = default) switch (value) { case "75%": return -1; case "100%": return 0; case "125%": return 1; case "150%": return 2; case "200%": return 3; } } } return false; }; AblePlayer.prototype.stylizeCaptions = function ($element, pref) { // $element is the jQuery element containing the captions // this function handles stylizing of the sample caption text in the Prefs dialog // plus the actual production captions // TODO: consider applying the same user prefs to visible text-based description var property, newValue, opacity; if (typeof $element !== "undefined") { if (pref == "prefCaptionsPosition") { this.positionCaptions(); } else if (typeof pref !== "undefined") { // just change the one property that user just changed if (pref === "prefCaptionsFont") { property = "font-family"; } else if (pref === "prefCaptionsSize") { property = "font-size"; } else if (pref === "prefCaptionsColor") { property = "color"; } else if (pref === "prefCaptionsBGColor") { property = "background-color"; } else if (pref === "prefCaptionsOpacity") { property = "opacity"; } if (pref === "prefCaptionsOpacity") { newValue = parseFloat($("#" + this.mediaId + "_" + pref).val()) / 100.0; } else { newValue = $("#" + this.mediaId + "_" + pref).val(); } $element.css(property, newValue); } else { // no property was specified, update all styles with current saved prefs opacity = parseFloat(this.prefCaptionsOpacity) / 100.0; $element.css({ "font-family": this.prefCaptionsFont, color: this.prefCaptionsColor, "background-color": this.prefCaptionsBGColor, opacity: opacity, }); if ($element === this.$captionsDiv) { if (typeof this.$captionsDiv !== "undefined") { this.$captionsDiv.css({ "font-size": this.prefCaptionsSize, }); } } if (this.prefCaptionsPosition === "below") { // also need to add the background color to the wrapper div if (typeof this.$captionsWrapper !== "undefined") { this.$captionsWrapper.css({ "background-color": this.prefCaptionsBGColor, opacity: "1", }); } } else if (this.prefCaptionsPosition === "overlay") { // no background color for overlay wrapper, captions are displayed in-line if (typeof this.$captionsWrapper !== "undefined") { this.$captionsWrapper.css({ "background-color": "transparent", opacity: "", }); } } this.positionCaptions(); } } }; AblePlayer.prototype.positionCaptions = function (position) { // set caption position to either 'overlay' or 'below' // if position parameter was passed to this function, use that // otherwise use user preference if (typeof position === "undefined") { position = this.prefCaptionsPosition; } if (typeof this.$captionsWrapper !== "undefined") { if (position == "below") { this.$captionsWrapper .removeClass("able-captions-overlay") .addClass("able-captions-below"); // also need to update in-line styles this.$captionsWrapper.css({ "background-color": this.prefCaptionsBGColor, opacity: "1", }); } else { this.$captionsWrapper .removeClass("able-captions-below") .addClass("able-captions-overlay"); this.$captionsWrapper.css({ "background-color": "transparent", opacity: "", }); } } }; } export default addCaptionFunctions;