From 6fa25f3a675edca21431f5b890edfc700341b5d4 Mon Sep 17 00:00:00 2001 From: Jeane Marty Date: Tue, 25 Jun 2024 11:22:11 -0700 Subject: [PATCH 1/6] initial fix for unbinding issue #575 --- build/ableplayer.dist.js | 1521 ++++++++++++++++++------------------- build/ableplayer.js | 1523 ++++++++++++++++++-------------------- build/ableplayer.min.js | 2 +- package-lock.json | 32 +- scripts/description.js | 1523 ++++++++++++++++++-------------------- 5 files changed, 2229 insertions(+), 2372 deletions(-) diff --git a/build/ableplayer.dist.js b/build/ableplayer.dist.js index 3de8a002..aeaaf6eb 100644 --- a/build/ableplayer.dist.js +++ b/build/ableplayer.dist.js @@ -7539,791 +7539,742 @@ var AblePlayerInstances = []; })(jQuery); (function ($) { - AblePlayer.prototype.initDescription = function() { - - // set default mode for delivering description (open vs closed) - // based on availability and user preference - - // called when player is being built, or when a user - // toggles the Description button or changes a description-related preference - - // The following variables are applicable to delivery of description: - // defaultStateDescriptions == 'on' or 'off', defined by website owner (overridden by prefDesc) - // prefDesc == 1 if user wants description (i.e., Description button is on); else 0 - // prefDescPause == 1 to pause video when description starts; else 0 - // prefDescVisible == 1 to visibly show text-based description area; else 0 - // prefDescMethod == either 'video' or 'text' (as of v4.0.10, prefDescMethod is always 'video') - // descMethod is the format actually used ('video' or 'text'), regardless of user preference - // hasOpenDesc == true if a described version of video is available via data-desc-src attribute - // hasClosedDesc == true if a description text track is available - // descOn == true if description of either type is on - // readDescriptionsAloud == true if text description is to be announced audibly; otherwise false - // descReader == either 'browser' or 'screenreader' - - var deferred, promise, thisObj; - - deferred = new $.Deferred(); - promise = deferred.promise(); - thisObj = this; - - if (this.mediaType === 'audio') { - deferred.resolve(); - } - - // check to see if there's an open-described version of this video - // checks only the first source since if a described version is provided, - // it must be provided for all sources - this.descFile = this.$sources.first().attr('data-desc-src'); - if (typeof this.descFile !== 'undefined') { - this.hasOpenDesc = true; - } - else { - // there's no open-described version via data-desc-src, - // but what about data-youtube-desc-src or data-vimeo-desc-src? - // if these exist, they would have been defined earlier - if (this.youTubeDescId || this.vimeoDescId) { - this.hasOpenDesc = true; - } - else { // there are no open-described versions from any source - this.hasOpenDesc = false; - } - } - - // Set this.descMethod based on media availability & user preferences - if (this.hasOpenDesc && this.hasClosedDesc) { - // both formats are available. User gets their preference. - if (this.prefDescMethod) { - this.descMethod = this.prefDescMethod; - } - else { - // user has no preference. Video is default. - this.descMethod = 'video'; - } - } - else if (this.hasOpenDesc) { - this.descMethod = 'video'; - } - else if (this.hasClosedDesc) { - this.descMethod = 'text'; - } - else { - // no description is available for this video - this.descMethod = null; - } - - // Set the default state of descriptions - if (this.descMethod) { - if (this.prefDesc === 1) { - this.descOn = true; - } - else if (this.prefDesc === 0) { - this.descOn = false; - } - else { - // user has no prefs. Use default state. - if (this.defaultStateDescriptions === 1) { - this.descOn = true; - } - else { - this.descOn = false; - } - } - } - else { - this.descOn = false; - } - if (typeof this.$descDiv === 'undefined' && this.hasClosedDesc && this.descMethod === 'text') { - this.injectTextDescriptionArea(); - } - - if (this.descOn) { - if (this.descMethod === 'video') { - if (!this.usingDescribedVersion()) { - // switched from non-described to described version - this.swapDescription(); - } - } - if (this.hasClosedDesc) { - if (this.prefDescVisible) { - // make description text visible - if (typeof this.$descDiv !== 'undefined') { - this.$descDiv.show(); - this.$descDiv.removeClass('able-clipped'); - } - } - else { - // keep it visible to screen readers, but hide it visibly - if (typeof this.$descDiv !== 'undefined') { - this.$descDiv.addClass('able-clipped'); - } - } - } - } - else { // description is off. - if (this.descMethod === 'video') { // user has turned off described version of video - if (this.usingDescribedVersion()) { - // user was using the described verion. Swap for non-described version - this.swapDescription(); - } - } - else if (this.descMethod === 'text') { // user has turned off text description - // hide description div from everyone, including screen reader users - if (typeof this.$descDiv !== 'undefined') { - this.$descDiv.hide(); - this.$descDiv.removeClass('able-clipped'); - } - } - } - deferred.resolve(); - return promise; - }; - - AblePlayer.prototype.usingDescribedVersion = function () { - - // Returns true if currently using audio description, false otherwise. - - if (this.player === 'youtube') { - return (this.activeYouTubeId === this.youTubeDescId); - } - else if (this.player === 'vimeo') { - return (this.activeVimeoId === this.vimeoDescId); - } - else { - return (this.$sources.first().attr('data-desc-src') === this.$sources.first().attr('src')); - } - }; - - AblePlayer.prototype.initSpeech = function (context) { - - // Some browsers &/or operating systems require a user-initiated click - // before this.synth.getVoices() will work. As of Nov 2022: - // Chrome requires a click before synth.getVoices() will work - // iOS requires a click before synth.speak() will work - // A hack to address this: Listen for ANY click, then play an inaudible utterance - // to intitiate speech synthesis - // https://stackoverflow.com/questions/32193704/js-speech-synthesis-issue-on-ios - // This function does that, and sets this.speechEnabled - // It's called with either of these contexts: - // 'init' - player is being initialized - // 'play' - user has clicked play - // 'prefs' - user has clicked prefs button - // 'desc' - it's time to announce a description! - - var thisObj = this; - - if (this.speechEnabled === null) { - - if (typeof this.synth !== 'undefined') { - // cancel any previous synth instance and reinitialize - this.synth.cancel(); - } - - if (window.speechSynthesis) { - - // browser supports speech synthesis - this.synth = window.speechSynthesis; - this.synth.cancel(); - if (context === 'init') { - // handle a click on anything, in case the user - // clicks something before they click 'play' or 'prefs' buttons - // that would allow us to init speech before it's needed - $(document).on('click',function() { - var greeting = new SpeechSynthesisUtterance('\x20'); - thisObj.synth.speak(greeting); - greeting.onstart = function(e) { - // utterance has started - $(document).off('click'); // unbind the click event listener - } - greeting.onend = function(e) { - // should now be able to get browser voices - // in browsers that require a click - thisObj.getBrowserVoices(); - if (thisObj.descVoices.length) { - thisObj.speechEnabled = true; - } - }; - }); - - // go ahead and call get browser voices in case it might work, - // for browsers that don't require a click - this.getBrowserVoices(); - if (this.descVoices.length) { - this.speechEnabled = true; - } - } - else { // context is either 'play' or 'prefs' or 'desc' - var greeting = new SpeechSynthesisUtterance('\x20'); - thisObj.synth.speak(greeting); - greeting.onstart = function(e) { - // utterance has started - $(document).off('click'); // unbind the click event listener - }; - greeting.onend = function(e) { - // should now be able to get browser voices - // in browsers that require a click - thisObj.getBrowserVoices(); - /* - // Safari 15.4 on MacOS has a bug: No voice array is returned - // The browser speaks, but we have no control over voices. - // Therefore, speechEnabled cannot be dependent on descVoices - // as long as Safari 15.4 is still supported - if (thisObj.descVoices.length) { - thisObj.speechEnabled = true; - } - */ - thisObj.speechEnabled = true; - }; - } - } - else { - // browser does not support speech synthesis - this.speechEnabled = false; - } - } - }; - - AblePlayer.prototype.getBrowserVoices = function () { - - // define this.descVoices array - // includes only languages that match the language of the captions or player - - var voices, descLangs, voiceLang, preferredLang; - - if (this.captionLang) { - preferredLang = this.captionLang.substring(0,2).toLowerCase(); - } - else { - preferredLang = this.lang.substring(0,2).toLowerCase(); - } - this.descVoices = []; - voices = this.synth.getVoices(); - descLangs = this.getDescriptionLangs(); - if (voices.length > 0) { - this.descVoices = []; - // available languages are identified with local suffixes (e.g., en-US) - for (var i=0; i 0) { - if (prefDescVoice) { - // select the language that matches prefDescVoice, if it's available - prefVoiceFound = false; - for (var i=0; i 0) { - this.swapTime = this.elapsed; - } - else { - this.swapTime = 0; - } - if (this.duration > 0) { - this.prevDuration = this.duration; - } - - // Capture current playback state, so media can resume after source is swapped - if (!this.okToPlay) { - this.okToPlay = this.playing; - } - - if (this.descOn) { - // user has requested the described version - this.showAlert(this.tt.alertDescribedVersion); - } - else { - // user has requested the non-described version - this.showAlert(this.tt.alertNonDescribedVersion); - } - - if (this.player === 'html5') { - - this.swappingSrc = true; - this.paused = true; - - if (this.usingDescribedVersion()) { - // the described version is currently playing. Swap to non-described - for (i=0; i < this.$sources.length; i++) { - // for all elements, replace src with data-orig-src - origSrc = this.$sources[i].getAttribute('data-orig-src'); - srcType = this.$sources[i].getAttribute('type'); - if (origSrc) { - this.$sources[i].setAttribute('src',origSrc); - } - } - } - else { - // the non-described version is currently playing. Swap to described. - for (i=0; i < this.$sources.length; i++) { - // for all elements, replace src with data-desc-src (if one exists) - // then store original source in a new data-orig-src attribute - origSrc = this.$sources[i].getAttribute('src'); - descSrc = this.$sources[i].getAttribute('data-desc-src'); - srcType = this.$sources[i].getAttribute('type'); - if (descSrc) { - this.$sources[i].setAttribute('src',descSrc); - this.$sources[i].setAttribute('data-orig-src',origSrc); - } - } - } - - if (this.recreatingPlayer) { - // stopgap to prevent multiple firings of recreatePlayer() - return; - } - if (this.playerCreated) { - // delete old player, then recreate it with new source & tracks - this.deletePlayer('swap-desc-html'); - this.recreatePlayer().then(function() { - if (!thisObj.loadingMedia) { - thisObj.media.load(); - thisObj.loadingMedia = true; - } - }); - } - else { - // player is in the process of being created - // no need to recreate it - } - } - else if (this.player === 'youtube') { - - if (this.usingDescribedVersion()) { - // the described version is currently playing. Swap to non-described - this.activeYouTubeId = this.youTubeId; - } - else { - // the non-described version is currently playing. Swap to described. - this.activeYouTubeId = this.youTubeDescId; - } - if (typeof this.youTubePlayer !== 'undefined') { - thisObj.swappingSrc = true; - if (thisObj.playing) { - // loadVideoById() loads and immediately plays the new video at swapTime - thisObj.youTubePlayer.loadVideoById(thisObj.activeYouTubeId,thisObj.swapTime); - } - else { - // cueVideoById() loads the new video and seeks to swapTime, but does not play - thisObj.youTubePlayer.cueVideoById(thisObj.activeYouTubeId,thisObj.swapTime); - } - } - if (this.playerCreated) { - this.deletePlayer('swap-desc-youtube'); - } - // player needs to be recreated with new source - if (this.recreatingPlayer) { - // stopgap to prevent multiple firings of recreatePlayer() - return; - } - this.recreatePlayer().then(function() { - // nothing to do here - // next steps occur when youtube onReady event fires - // see youtube.js > finalizeYoutubeInit() - }); - } - else if (this.player === 'vimeo') { - if (this.usingDescribedVersion()) { - // the described version is currently playing. Swap to non-described - this.activeVimeoId = this.vimeoId; - this.showAlert(this.tt.alertNonDescribedVersion); - } - else { - // the non-described version is currently playing. Swap to described. - this.activeVimeoId = this.vimeoDescId; - this.showAlert(this.tt.alertDescribedVersion); - } - if (this.playerCreated) { - this.deletePlayer('swap-desc-vimeo'); - } - // player needs to be recreated with new source - if (this.recreatingPlayer) { - // stopgap to prevent multiple firings of recreatePlayer() - return; - } - this.recreatePlayer().then(function() { - // load the new video source - thisObj.vimeoPlayer.loadVideo(thisObj.activeVimeoId).then(function() { - if (thisObj.playing) { - // video was playing when user requested an alternative version - // seek to swapTime and continue playback (playback happens automatically) - thisObj.vimeoPlayer.setCurrentTime(thisObj.swapTime); - } - else { - // Vimeo autostarts immediately after video loads - // The "Described" button should not trigger playback, so stop this before the user notices. - thisObj.vimeoPlayer.pause(); - } - }); - }); - } - }; - - AblePlayer.prototype.showDescription = function(now) { - - if (!this.hasClosedDesc || this.swappingSrc || !this.descOn || this.descMethod === 'video') { - return; - } - - var thisObj, i, cues, d, thisDescription, descText, msg; - thisObj = this; - - var flattenComponentForDescription = function (component) { - var result = []; - if (component.type === 'string') { - result.push(component.value); - } - else { - for (var i = 0; i < component.children.length; i++) { - result.push(flattenComponentForDescription(component.children[i])); - } - } - return result.join(''); - }; - - if (this.selectedDescriptions) { - cues = this.selectedDescriptions.cues; - } - else if (this.descriptions.length >= 1) { - cues = this.descriptions[0].cues; - } - else { - cues = []; - } - for (d = 0; d < cues.length; d++) { - if ((cues[d].start <= now) && (cues[d].end > now)) { - thisDescription = d; - break; - } - } - if (typeof thisDescription !== 'undefined') { - if (this.currentDescription !== thisDescription) { - // temporarily remove aria-live from $status in order to prevent description from being interrupted - this.$status.removeAttr('aria-live'); - descText = flattenComponentForDescription(cues[thisDescription].components); - if (this.descReader === 'screenreader') { - // load the new description into the container div for screen readers to read - this.$descDiv.html(descText); - } - else if (this.speechEnabled) { - // use browser's built-in speech synthesis - this.announceDescriptionText('description',descText); - if (this.prefDescVisible) { - // write description to the screen for sighted users - // but remove ARIA attributes since it isn't intended to be read by screen readers - this.$descDiv.html(descText).removeAttr('aria-live aria-atomic'); - } - } - else { - // browser does not support speech synthesis - // load the new description into the container div for screen readers to read - this.$descDiv.html(descText); - } - if (this.prefDescPause && this.descMethod === 'text') { - this.pauseMedia(); - this.pausedForDescription = true; - } - this.currentDescription = thisDescription; - } - } - else { - this.$descDiv.html(''); - this.currentDescription = -1; - // restore aria-live to $status - this.$status.attr('aria-live','polite'); - } - }; - - AblePlayer.prototype.syncSpeechToPlaybackRate = function(rate) { - - // called when user changed playback rate - // adjust rate of audio description to match - - var speechRate; - - if (rate === 0.5) { - speechRate = 0.7; // option 1 in prefs menu - } - else if (rate === 0.75) { - speechRate = 0.8; // option 2 in prefs menu - } - else if (rate === 1.0) { - speechRate = 1; // option 4 in prefs menu (normal speech, default) - } - else if (rate === 1.25) { - speechRate = 1.1; // option 5 in prefs menu - } - else if (rate === 1.5) { - speechRate = 1.2; // option 6 in prefs menu - } - else if (rate === 1.75) { - speechRate = 1.5; // option 7 in prefs menu - } - else if (rate === 2.0) { - speechRate = 2; // option 8 in prefs menu (fast) - } - else if (rate === 2.25) { - speechRate = 2.5; // option 9 in prefs menu (very fast) - } - else if (rate >= 2.5) { - speechRate = 3; // option 10 in prefs menu (super fast) - } - this.prefDescRate = speechRate; - }; - - AblePlayer.prototype.announceDescriptionText = function(context, text) { - - // this function announces description text using speech synthesis - // it's only called if already determined that browser supports speech synthesis - // context is either: - // 'description' - actual description text extracted from WebVTT file - // 'sample' - called when user changes a setting in Description Prefs dialog - - var thisObj, voiceName, i, voice, pitch, rate, volume, utterance, - timeElapsed, secondsElapsed; - - thisObj = this; - - // As of Feb 2021, - // 1. In some browsers (e.g., Chrome) window.speechSynthesis.getVoices() - // returns 0 voices unless the request is triggered with a user click - // Therefore, description may have failed to initialize when the page loaded - // This function cannot have been called without a mouse click. - // Therefore, this is a good time to check that, and try again if needed - // 2. In some browsers, the window.speechSynthesis.speaking property fails to reset, - // and onend event is never fired. This prevents new speech from being spoken. - // window.speechSynthesis.cancel() also fails, so it's impossible to recover. - // This only seems to happen with some voices. - // Typically the first voice in the getVoices() array (index 0) is realiable - // When speech synthesis gets wonky, this is a deep problem that impacts all browsers - // and typically requires a computer reboot to make right again. - // This has been observed frequently in macOS Big Sur, but also in Windows 10 - // To ignore user's voice preferences and always use the first voice, set the following var to true - // This is for testing only; not recommended for production - // unless the voice select field is also removed from the Prefs dialog - var useFirstVoice = false; - - if (!this.speechEnabled) { - // voices array failed to load the first time. Try again - this.initSpeech('desc'); - } - - if (context === 'sample') { - // get settings from form - voiceName = $('#' + this.mediaId + '_prefDescVoice').val(); - pitch = $('#' + this.mediaId + '_prefDescPitch').val(); - rate = $('#' + this.mediaId + '_prefDescRate').val(); - volume = $('#' + this.mediaId + '_prefDescVolume').val(); - } - else { - // get settings from global prefs - voiceName = this.prefDescVoice; - pitch = this.prefDescPitch; - rate = this.prefDescRate; - volume = this.prefDescVolume; - } - - // get the voice associated with the user's chosen voice name - if (this.descVoices) { - if (this.descVoices.length > 0) { - if (useFirstVoice) { - voice = this.descVoices[0]; - } - else if (voiceName) { - // get the voice that matches user's preferred voiceName - for (i = 0; i < this.descVoices.length; i++) { - if (this.descVoices[i].name == voiceName) { - voice = this.descVoices[i]; - break; - } - } - } - if (typeof voice === 'undefined') { - // no matching voice was found - // use the first voice in the array - voice = this.descVoices[0]; - } - } - } - else { - voice = null; - } - utterance = new SpeechSynthesisUtterance(); - if (voice) { - utterance.voice = voice; - } - utterance.voiceURI = 'native'; - utterance.volume = volume; - utterance.rate = rate; - utterance.pitch = pitch; - utterance.text = text; - // TODO: Consider the best language for the utterance: - // language of the web page? (this.lang) - // language of the WebVTT description track? - // language of the user's chosen voice? - // If there's a mismatch between any of these, the description will likely be unintelligible - utterance.lang = this.lang; - utterance.onstart = function(e) { - // utterance has started - }; - utterance.onpause = function(e) { - // utterance has paused - }; - utterance.onend = function(e) { - // utterance has ended - this.speakingDescription = false; - timeElapsed = e.elapsedTime; - // As of Firefox 95, e.elapsedTime is expressed in seconds - // Other browsers (tested in Chrome & Edge) express this in milliseconds - // Assume no utterance will require over 100 seconds to express... - if (timeElapsed > 100) { - // time is likely expressed in milliseconds - secondsElapsed = (e.elapsedTime/1000).toFixed(2); - } - else { - // time is likely already expressed in seconds; just need to round it - secondsElapsed = (e.elapsedTime).toFixed(2); - } - if (this.debug) { - - } - if (context === 'description') { - if (thisObj.prefDescPause) { - if (thisObj.pausedForDescription) { - thisObj.playMedia(); - this.pausedForDescription = false; - } - } - } - }; - utterance.onerror = function(e) { - // handle error - - }; - if (this.synth.paused) { - this.synth.resume(); - } - this.synth.speak(utterance); - this.speakingDescription = true; - }; - + AblePlayer.prototype.initDescription = function () { + // set default mode for delivering description (open vs closed) + // based on availability and user preference + + // called when player is being built, or when a user + // toggles the Description button or changes a description-related preference + + // The following variables are applicable to delivery of description: + // defaultStateDescriptions == 'on' or 'off', defined by website owner (overridden by prefDesc) + // prefDesc == 1 if user wants description (i.e., Description button is on); else 0 + // prefDescPause == 1 to pause video when description starts; else 0 + // prefDescVisible == 1 to visibly show text-based description area; else 0 + // prefDescMethod == either 'video' or 'text' (as of v4.0.10, prefDescMethod is always 'video') + // descMethod is the format actually used ('video' or 'text'), regardless of user preference + // hasOpenDesc == true if a described version of video is available via data-desc-src attribute + // hasClosedDesc == true if a description text track is available + // descOn == true if description of either type is on + // readDescriptionsAloud == true if text description is to be announced audibly; otherwise false + // descReader == either 'browser' or 'screenreader' + + var deferred, promise, thisObj; + + deferred = new $.Deferred(); + promise = deferred.promise(); + thisObj = this; + + if (this.mediaType === "audio") { + deferred.resolve(); + } + + // check to see if there's an open-described version of this video + // checks only the first source since if a described version is provided, + // it must be provided for all sources + this.descFile = this.$sources.first().attr("data-desc-src"); + if (typeof this.descFile !== "undefined") { + this.hasOpenDesc = true; + } else { + // there's no open-described version via data-desc-src, + // but what about data-youtube-desc-src or data-vimeo-desc-src? + // if these exist, they would have been defined earlier + if (this.youTubeDescId || this.vimeoDescId) { + this.hasOpenDesc = true; + } else { + // there are no open-described versions from any source + this.hasOpenDesc = false; + } + } + + // Set this.descMethod based on media availability & user preferences + if (this.hasOpenDesc && this.hasClosedDesc) { + // both formats are available. User gets their preference. + if (this.prefDescMethod) { + this.descMethod = this.prefDescMethod; + } else { + // user has no preference. Video is default. + this.descMethod = "video"; + } + } else if (this.hasOpenDesc) { + this.descMethod = "video"; + } else if (this.hasClosedDesc) { + this.descMethod = "text"; + } else { + // no description is available for this video + this.descMethod = null; + } + + // Set the default state of descriptions + if (this.descMethod) { + if (this.prefDesc === 1) { + this.descOn = true; + } else if (this.prefDesc === 0) { + this.descOn = false; + } else { + // user has no prefs. Use default state. + if (this.defaultStateDescriptions === 1) { + this.descOn = true; + } else { + this.descOn = false; + } + } + } else { + this.descOn = false; + } + if ( + typeof this.$descDiv === "undefined" && + this.hasClosedDesc && + this.descMethod === "text" + ) { + this.injectTextDescriptionArea(); + } + + if (this.descOn) { + if (this.descMethod === "video") { + if (!this.usingDescribedVersion()) { + // switched from non-described to described version + this.swapDescription(); + } + } + if (this.hasClosedDesc) { + if (this.prefDescVisible) { + // make description text visible + if (typeof this.$descDiv !== "undefined") { + this.$descDiv.show(); + this.$descDiv.removeClass("able-clipped"); + } + } else { + // keep it visible to screen readers, but hide it visibly + if (typeof this.$descDiv !== "undefined") { + this.$descDiv.addClass("able-clipped"); + } + } + } + } else { + // description is off. + if (this.descMethod === "video") { + // user has turned off described version of video + if (this.usingDescribedVersion()) { + // user was using the described verion. Swap for non-described version + this.swapDescription(); + } + } else if (this.descMethod === "text") { + // user has turned off text description + // hide description div from everyone, including screen reader users + if (typeof this.$descDiv !== "undefined") { + this.$descDiv.hide(); + this.$descDiv.removeClass("able-clipped"); + } + } + } + deferred.resolve(); + return promise; + }; + + AblePlayer.prototype.usingDescribedVersion = function () { + // Returns true if currently using audio description, false otherwise. + + if (this.player === "youtube") { + return this.activeYouTubeId === this.youTubeDescId; + } else if (this.player === "vimeo") { + return this.activeVimeoId === this.vimeoDescId; + } else { + return ( + this.$sources.first().attr("data-desc-src") === + this.$sources.first().attr("src") + ); + } + }; + + AblePlayer.prototype.initSpeech = function (context) { + var thisObj = this; + + if (this.speechEnabled === null) { + if (typeof this.synth !== "undefined") { + // cancel any previous synth instance and reinitialize + this.synth.cancel(); + } + + if (window.speechSynthesis) { + // browser supports speech synthesis + this.synth = window.speechSynthesis; + this.synth.cancel(); + if (context === "init") { + // Only add the click listener if context is 'init' + var initSpeechSynthesis = function () { + var greeting = new SpeechSynthesisUtterance("\x20"); + thisObj.synth.speak(greeting); + greeting.onend = function (e) { + // should now be able to get browser voices + // in browsers that require a click + thisObj.getBrowserVoices(); + if (thisObj.descVoices.length) { + thisObj.speechEnabled = true; + } + // Remove the click event listener after the first invocation + $(document).off("click", initSpeechSynthesis); + }; + }; + // Bind the click event listener + $(document).on("click", initSpeechSynthesis); + } else { + // context is either 'play', 'prefs', or 'desc' + // This part remains unchanged, as it's outside the 'init' context scope + var greeting = new SpeechSynthesisUtterance("\x20"); + thisObj.synth.speak(greeting); + greeting.onstart = function (e) { + // utterance has started + }; + greeting.onend = function (e) { + thisObj.getBrowserVoices(); + thisObj.speechEnabled = true; + }; + } + // Attempt to get browser voices immediately, for browsers that don't require a click + this.getBrowserVoices(); + if (this.descVoices.length) { + this.speechEnabled = true; + } + } else { + // browser does not support speech synthesis + this.speechEnabled = false; + } + } + }; + + AblePlayer.prototype.getBrowserVoices = function () { + // define this.descVoices array + // includes only languages that match the language of the captions or player + + var voices, descLangs, voiceLang, preferredLang; + + if (this.captionLang) { + preferredLang = this.captionLang.substring(0, 2).toLowerCase(); + } else { + preferredLang = this.lang.substring(0, 2).toLowerCase(); + } + this.descVoices = []; + voices = this.synth.getVoices(); + descLangs = this.getDescriptionLangs(); + if (voices.length > 0) { + this.descVoices = []; + // available languages are identified with local suffixes (e.g., en-US) + for (var i = 0; i < voices.length; i++) { + // match only the first 2 characters of the lang code + voiceLang = voices[i].lang.substring(0, 2).toLowerCase(); + if ( + voiceLang === preferredLang && + descLangs.indexOf(voiceLang) !== -1 + ) { + // this voice matches preferredLang + // AND there's a matching description track in this language + // Add this voice to final array + this.descVoices.push(voices[i]); + } + } + if (!this.descVoices.length) { + // no voices available in the default language(s) + // just use all voices, regardless of language + this.descVoices = voices; + } + } + return false; + }; + + AblePlayer.prototype.getDescriptionLangs = function () { + // returns an array of languages (from srclang atttributes) + // in which there are description tracks + // use only first two characters of the lang code + var descLangs = []; + if (this.tracks) { + for (var i = 0; i < this.tracks.length; i++) { + if (this.tracks[i].kind === "descriptions") { + descLangs.push(this.tracks[i].language.substring(0, 2).toLowerCase()); + } + } + } + return descLangs; + }; + + AblePlayer.prototype.setDescriptionVoice = function () { + // set description voice on player init, or when user changes caption language + // Voice is determined in the following order of precedence: + // 1. User's preferred voice for this language, saved in a cookie + // 2. The first available voice in the array of available voices for this browser in this language + + var cookie, voices, prefDescVoice, descVoice, descLang, prefVoiceFound; + cookie = this.getCookie(); + if (typeof cookie.voices !== "undefined") { + prefDescVoice = this.getPrefDescVoice(); + } else { + prefDescVoice = null; + } + + this.getBrowserVoices(); + this.rebuildDescPrefsForm(); + + if (this.selectedDescriptions) { + descLang = this.selectedDescriptions.language; + } else if (this.captionLang) { + descLang = this.captionLang; + } else { + descLang = this.lang; + } + + if (this.synth) { + voices = this.synth.getVoices(); + if (voices.length > 0) { + if (prefDescVoice) { + // select the language that matches prefDescVoice, if it's available + prefVoiceFound = false; + for (var i = 0; i < voices.length; i++) { + // first, be sure voice is the correct language + if ( + voices[i].lang.substring(0, 2).toLowerCase() === + descLang.substring(0, 2).toLowerCase() + ) { + if (voices[i].name === prefDescVoice) { + descVoice = voices[i].name; + prefVoiceFound = true; + break; + } + } + } + } + if (!prefVoiceFound) { + // select the first language that matches the first 2 characters of the lang code + for (var i = 0; i < voices.length; i++) { + if ( + voices[i].lang.substring(0, 2).toLowerCase() === + descLang.substring(0, 2).toLowerCase() + ) { + descVoice = voices[i].name; + break; + } + } + } + // make this the user's current preferred voice + this.prefDescVoice = descVoice; + this.prefDescVoiceLang = descLang; + // select this voice in the Description Prefs dialog + if (this.$voiceSelectField) { + var selectedOption = this.$voiceSelectField.find( + 'option[value="' + this.prefDescVoice + '"]' + ); + this.$voiceSelectField.val(this.prefDescVoice); + } + this.updateCookie("voice"); + } + } + }; + + AblePlayer.prototype.swapDescription = function () { + // swap described and non-described source media, depending on which is playing + // this function is only called in two circumstances: + // 1. Swapping to described version when initializing player (based on user prefs & availability) + // (playerCreated == false) + // 2. User is toggling description + // (playerCreated == true) + + var thisObj, i, origSrc, descSrc, srcType, newSource; + + thisObj = this; + + // We are no longer loading the previous media source + // Only now, as a new source is requested, is it safe to reset this var + // It will be reset to true when media.load() is called + this.loadingMedia = false; + + // get element that has focus at the time swap is initiated + // after player is rebuilt, focus will return to that same element + // (if it exists) + this.$focusedElement = $(":focus"); + + // get current time of current source, and attempt to start new video at the same time + // whether this is possible will be determined after the new media source has loaded + // see onMediaNewSourceLoad() + if (this.elapsed > 0) { + this.swapTime = this.elapsed; + } else { + this.swapTime = 0; + } + if (this.duration > 0) { + this.prevDuration = this.duration; + } + + // Capture current playback state, so media can resume after source is swapped + if (!this.okToPlay) { + this.okToPlay = this.playing; + } + + if (this.descOn) { + // user has requested the described version + this.showAlert(this.tt.alertDescribedVersion); + } else { + // user has requested the non-described version + this.showAlert(this.tt.alertNonDescribedVersion); + } + + if (this.player === "html5") { + this.swappingSrc = true; + this.paused = true; + + if (this.usingDescribedVersion()) { + // the described version is currently playing. Swap to non-described + for (i = 0; i < this.$sources.length; i++) { + // for all elements, replace src with data-orig-src + origSrc = this.$sources[i].getAttribute("data-orig-src"); + srcType = this.$sources[i].getAttribute("type"); + if (origSrc) { + this.$sources[i].setAttribute("src", origSrc); + } + } + } else { + // the non-described version is currently playing. Swap to described. + for (i = 0; i < this.$sources.length; i++) { + // for all elements, replace src with data-desc-src (if one exists) + // then store original source in a new data-orig-src attribute + origSrc = this.$sources[i].getAttribute("src"); + descSrc = this.$sources[i].getAttribute("data-desc-src"); + srcType = this.$sources[i].getAttribute("type"); + if (descSrc) { + this.$sources[i].setAttribute("src", descSrc); + this.$sources[i].setAttribute("data-orig-src", origSrc); + } + } + } + + if (this.recreatingPlayer) { + // stopgap to prevent multiple firings of recreatePlayer() + return; + } + if (this.playerCreated) { + // delete old player, then recreate it with new source & tracks + this.deletePlayer("swap-desc-html"); + this.recreatePlayer().then(function () { + if (!thisObj.loadingMedia) { + thisObj.media.load(); + thisObj.loadingMedia = true; + } + }); + } else { + // player is in the process of being created + // no need to recreate it + } + } else if (this.player === "youtube") { + if (this.usingDescribedVersion()) { + // the described version is currently playing. Swap to non-described + this.activeYouTubeId = this.youTubeId; + } else { + // the non-described version is currently playing. Swap to described. + this.activeYouTubeId = this.youTubeDescId; + } + if (typeof this.youTubePlayer !== "undefined") { + thisObj.swappingSrc = true; + if (thisObj.playing) { + // loadVideoById() loads and immediately plays the new video at swapTime + thisObj.youTubePlayer.loadVideoById( + thisObj.activeYouTubeId, + thisObj.swapTime + ); + } else { + // cueVideoById() loads the new video and seeks to swapTime, but does not play + thisObj.youTubePlayer.cueVideoById( + thisObj.activeYouTubeId, + thisObj.swapTime + ); + } + } + if (this.playerCreated) { + this.deletePlayer("swap-desc-youtube"); + } + // player needs to be recreated with new source + if (this.recreatingPlayer) { + // stopgap to prevent multiple firings of recreatePlayer() + return; + } + this.recreatePlayer().then(function () { + // nothing to do here + // next steps occur when youtube onReady event fires + // see youtube.js > finalizeYoutubeInit() + }); + } else if (this.player === "vimeo") { + if (this.usingDescribedVersion()) { + // the described version is currently playing. Swap to non-described + this.activeVimeoId = this.vimeoId; + this.showAlert(this.tt.alertNonDescribedVersion); + } else { + // the non-described version is currently playing. Swap to described. + this.activeVimeoId = this.vimeoDescId; + this.showAlert(this.tt.alertDescribedVersion); + } + if (this.playerCreated) { + this.deletePlayer("swap-desc-vimeo"); + } + // player needs to be recreated with new source + if (this.recreatingPlayer) { + // stopgap to prevent multiple firings of recreatePlayer() + return; + } + this.recreatePlayer().then(function () { + // load the new video source + thisObj.vimeoPlayer.loadVideo(thisObj.activeVimeoId).then(function () { + if (thisObj.playing) { + // video was playing when user requested an alternative version + // seek to swapTime and continue playback (playback happens automatically) + thisObj.vimeoPlayer.setCurrentTime(thisObj.swapTime); + } else { + // Vimeo autostarts immediately after video loads + // The "Described" button should not trigger playback, so stop this before the user notices. + thisObj.vimeoPlayer.pause(); + } + }); + }); + } + }; + + AblePlayer.prototype.showDescription = function (now) { + if ( + !this.hasClosedDesc || + this.swappingSrc || + !this.descOn || + this.descMethod === "video" + ) { + return; + } + + var thisObj, i, cues, d, thisDescription, descText, msg; + thisObj = this; + + var flattenComponentForDescription = function (component) { + var result = []; + if (component.type === "string") { + result.push(component.value); + } else { + for (var i = 0; i < component.children.length; i++) { + result.push(flattenComponentForDescription(component.children[i])); + } + } + return result.join(""); + }; + + if (this.selectedDescriptions) { + cues = this.selectedDescriptions.cues; + } else if (this.descriptions.length >= 1) { + cues = this.descriptions[0].cues; + } else { + cues = []; + } + for (d = 0; d < cues.length; d++) { + if (cues[d].start <= now && cues[d].end > now) { + thisDescription = d; + break; + } + } + if (typeof thisDescription !== "undefined") { + if (this.currentDescription !== thisDescription) { + // temporarily remove aria-live from $status in order to prevent description from being interrupted + this.$status.removeAttr("aria-live"); + descText = flattenComponentForDescription( + cues[thisDescription].components + ); + if (this.descReader === "screenreader") { + // load the new description into the container div for screen readers to read + this.$descDiv.html(descText); + } else if (this.speechEnabled) { + // use browser's built-in speech synthesis + this.announceDescriptionText("description", descText); + if (this.prefDescVisible) { + // write description to the screen for sighted users + // but remove ARIA attributes since it isn't intended to be read by screen readers + this.$descDiv.html(descText).removeAttr("aria-live aria-atomic"); + } + } else { + // browser does not support speech synthesis + // load the new description into the container div for screen readers to read + this.$descDiv.html(descText); + } + if (this.prefDescPause && this.descMethod === "text") { + this.pauseMedia(); + this.pausedForDescription = true; + } + this.currentDescription = thisDescription; + } + } else { + this.$descDiv.html(""); + this.currentDescription = -1; + // restore aria-live to $status + this.$status.attr("aria-live", "polite"); + } + }; + + AblePlayer.prototype.syncSpeechToPlaybackRate = function (rate) { + // called when user changed playback rate + // adjust rate of audio description to match + + var speechRate; + + if (rate === 0.5) { + speechRate = 0.7; // option 1 in prefs menu + } else if (rate === 0.75) { + speechRate = 0.8; // option 2 in prefs menu + } else if (rate === 1.0) { + speechRate = 1; // option 4 in prefs menu (normal speech, default) + } else if (rate === 1.25) { + speechRate = 1.1; // option 5 in prefs menu + } else if (rate === 1.5) { + speechRate = 1.2; // option 6 in prefs menu + } else if (rate === 1.75) { + speechRate = 1.5; // option 7 in prefs menu + } else if (rate === 2.0) { + speechRate = 2; // option 8 in prefs menu (fast) + } else if (rate === 2.25) { + speechRate = 2.5; // option 9 in prefs menu (very fast) + } else if (rate >= 2.5) { + speechRate = 3; // option 10 in prefs menu (super fast) + } + this.prefDescRate = speechRate; + }; + + AblePlayer.prototype.announceDescriptionText = function (context, text) { + // this function announces description text using speech synthesis + // it's only called if already determined that browser supports speech synthesis + // context is either: + // 'description' - actual description text extracted from WebVTT file + // 'sample' - called when user changes a setting in Description Prefs dialog + + var thisObj, + voiceName, + i, + voice, + pitch, + rate, + volume, + utterance, + timeElapsed, + secondsElapsed; + + thisObj = this; + + // As of Feb 2021, + // 1. In some browsers (e.g., Chrome) window.speechSynthesis.getVoices() + // returns 0 voices unless the request is triggered with a user click + // Therefore, description may have failed to initialize when the page loaded + // This function cannot have been called without a mouse click. + // Therefore, this is a good time to check that, and try again if needed + // 2. In some browsers, the window.speechSynthesis.speaking property fails to reset, + // and onend event is never fired. This prevents new speech from being spoken. + // window.speechSynthesis.cancel() also fails, so it's impossible to recover. + // This only seems to happen with some voices. + // Typically the first voice in the getVoices() array (index 0) is realiable + // When speech synthesis gets wonky, this is a deep problem that impacts all browsers + // and typically requires a computer reboot to make right again. + // This has been observed frequently in macOS Big Sur, but also in Windows 10 + // To ignore user's voice preferences and always use the first voice, set the following var to true + // This is for testing only; not recommended for production + // unless the voice select field is also removed from the Prefs dialog + var useFirstVoice = false; + + if (!this.speechEnabled) { + // voices array failed to load the first time. Try again + this.initSpeech("desc"); + } + + if (context === "sample") { + // get settings from form + voiceName = $("#" + this.mediaId + "_prefDescVoice").val(); + pitch = $("#" + this.mediaId + "_prefDescPitch").val(); + rate = $("#" + this.mediaId + "_prefDescRate").val(); + volume = $("#" + this.mediaId + "_prefDescVolume").val(); + } else { + // get settings from global prefs + voiceName = this.prefDescVoice; + pitch = this.prefDescPitch; + rate = this.prefDescRate; + volume = this.prefDescVolume; + } + + // get the voice associated with the user's chosen voice name + if (this.descVoices) { + if (this.descVoices.length > 0) { + if (useFirstVoice) { + voice = this.descVoices[0]; + } else if (voiceName) { + // get the voice that matches user's preferred voiceName + for (i = 0; i < this.descVoices.length; i++) { + if (this.descVoices[i].name == voiceName) { + voice = this.descVoices[i]; + break; + } + } + } + if (typeof voice === "undefined") { + // no matching voice was found + // use the first voice in the array + voice = this.descVoices[0]; + } + } + } else { + voice = null; + } + utterance = new SpeechSynthesisUtterance(); + if (voice) { + utterance.voice = voice; + } + utterance.voiceURI = "native"; + utterance.volume = volume; + utterance.rate = rate; + utterance.pitch = pitch; + utterance.text = text; + // TODO: Consider the best language for the utterance: + // language of the web page? (this.lang) + // language of the WebVTT description track? + // language of the user's chosen voice? + // If there's a mismatch between any of these, the description will likely be unintelligible + utterance.lang = this.lang; + utterance.onstart = function (e) { + // utterance has started + }; + utterance.onpause = function (e) { + // utterance has paused + }; + utterance.onend = function (e) { + // utterance has ended + this.speakingDescription = false; + timeElapsed = e.elapsedTime; + // As of Firefox 95, e.elapsedTime is expressed in seconds + // Other browsers (tested in Chrome & Edge) express this in milliseconds + // Assume no utterance will require over 100 seconds to express... + if (timeElapsed > 100) { + // time is likely expressed in milliseconds + secondsElapsed = (e.elapsedTime / 1000).toFixed(2); + } else { + // time is likely already expressed in seconds; just need to round it + secondsElapsed = e.elapsedTime.toFixed(2); + } + if (this.debug) { + + } + if (context === "description") { + if (thisObj.prefDescPause) { + if (thisObj.pausedForDescription) { + thisObj.playMedia(); + this.pausedForDescription = false; + } + } + } + }; + utterance.onerror = function (e) { + // handle error + + }; + if (this.synth.paused) { + this.synth.resume(); + } + this.synth.speak(utterance); + this.speakingDescription = true; + }; })(jQuery); (function ($) { diff --git a/build/ableplayer.js b/build/ableplayer.js index 61ce60aa..d7563963 100644 --- a/build/ableplayer.js +++ b/build/ableplayer.js @@ -7539,791 +7539,744 @@ var AblePlayerInstances = []; })(jQuery); (function ($) { - AblePlayer.prototype.initDescription = function() { - - // set default mode for delivering description (open vs closed) - // based on availability and user preference - - // called when player is being built, or when a user - // toggles the Description button or changes a description-related preference - - // The following variables are applicable to delivery of description: - // defaultStateDescriptions == 'on' or 'off', defined by website owner (overridden by prefDesc) - // prefDesc == 1 if user wants description (i.e., Description button is on); else 0 - // prefDescPause == 1 to pause video when description starts; else 0 - // prefDescVisible == 1 to visibly show text-based description area; else 0 - // prefDescMethod == either 'video' or 'text' (as of v4.0.10, prefDescMethod is always 'video') - // descMethod is the format actually used ('video' or 'text'), regardless of user preference - // hasOpenDesc == true if a described version of video is available via data-desc-src attribute - // hasClosedDesc == true if a description text track is available - // descOn == true if description of either type is on - // readDescriptionsAloud == true if text description is to be announced audibly; otherwise false - // descReader == either 'browser' or 'screenreader' - - var deferred, promise, thisObj; - - deferred = new $.Deferred(); - promise = deferred.promise(); - thisObj = this; - - if (this.mediaType === 'audio') { - deferred.resolve(); - } - - // check to see if there's an open-described version of this video - // checks only the first source since if a described version is provided, - // it must be provided for all sources - this.descFile = this.$sources.first().attr('data-desc-src'); - if (typeof this.descFile !== 'undefined') { - this.hasOpenDesc = true; - } - else { - // there's no open-described version via data-desc-src, - // but what about data-youtube-desc-src or data-vimeo-desc-src? - // if these exist, they would have been defined earlier - if (this.youTubeDescId || this.vimeoDescId) { - this.hasOpenDesc = true; - } - else { // there are no open-described versions from any source - this.hasOpenDesc = false; - } - } - - // Set this.descMethod based on media availability & user preferences - if (this.hasOpenDesc && this.hasClosedDesc) { - // both formats are available. User gets their preference. - if (this.prefDescMethod) { - this.descMethod = this.prefDescMethod; - } - else { - // user has no preference. Video is default. - this.descMethod = 'video'; - } - } - else if (this.hasOpenDesc) { - this.descMethod = 'video'; - } - else if (this.hasClosedDesc) { - this.descMethod = 'text'; - } - else { - // no description is available for this video - this.descMethod = null; - } - - // Set the default state of descriptions - if (this.descMethod) { - if (this.prefDesc === 1) { - this.descOn = true; - } - else if (this.prefDesc === 0) { - this.descOn = false; - } - else { - // user has no prefs. Use default state. - if (this.defaultStateDescriptions === 1) { - this.descOn = true; - } - else { - this.descOn = false; - } - } - } - else { - this.descOn = false; - } - if (typeof this.$descDiv === 'undefined' && this.hasClosedDesc && this.descMethod === 'text') { - this.injectTextDescriptionArea(); - } - - if (this.descOn) { - if (this.descMethod === 'video') { - if (!this.usingDescribedVersion()) { - // switched from non-described to described version - this.swapDescription(); - } - } - if (this.hasClosedDesc) { - if (this.prefDescVisible) { - // make description text visible - if (typeof this.$descDiv !== 'undefined') { - this.$descDiv.show(); - this.$descDiv.removeClass('able-clipped'); - } - } - else { - // keep it visible to screen readers, but hide it visibly - if (typeof this.$descDiv !== 'undefined') { - this.$descDiv.addClass('able-clipped'); - } - } - } - } - else { // description is off. - if (this.descMethod === 'video') { // user has turned off described version of video - if (this.usingDescribedVersion()) { - // user was using the described verion. Swap for non-described version - this.swapDescription(); - } - } - else if (this.descMethod === 'text') { // user has turned off text description - // hide description div from everyone, including screen reader users - if (typeof this.$descDiv !== 'undefined') { - this.$descDiv.hide(); - this.$descDiv.removeClass('able-clipped'); - } - } - } - deferred.resolve(); - return promise; - }; - - AblePlayer.prototype.usingDescribedVersion = function () { - - // Returns true if currently using audio description, false otherwise. - - if (this.player === 'youtube') { - return (this.activeYouTubeId === this.youTubeDescId); - } - else if (this.player === 'vimeo') { - return (this.activeVimeoId === this.vimeoDescId); - } - else { - return (this.$sources.first().attr('data-desc-src') === this.$sources.first().attr('src')); - } - }; - - AblePlayer.prototype.initSpeech = function (context) { - - // Some browsers &/or operating systems require a user-initiated click - // before this.synth.getVoices() will work. As of Nov 2022: - // Chrome requires a click before synth.getVoices() will work - // iOS requires a click before synth.speak() will work - // A hack to address this: Listen for ANY click, then play an inaudible utterance - // to intitiate speech synthesis - // https://stackoverflow.com/questions/32193704/js-speech-synthesis-issue-on-ios - // This function does that, and sets this.speechEnabled - // It's called with either of these contexts: - // 'init' - player is being initialized - // 'play' - user has clicked play - // 'prefs' - user has clicked prefs button - // 'desc' - it's time to announce a description! - - var thisObj = this; - - if (this.speechEnabled === null) { - - if (typeof this.synth !== 'undefined') { - // cancel any previous synth instance and reinitialize - this.synth.cancel(); - } - - if (window.speechSynthesis) { - - // browser supports speech synthesis - this.synth = window.speechSynthesis; - this.synth.cancel(); - if (context === 'init') { - // handle a click on anything, in case the user - // clicks something before they click 'play' or 'prefs' buttons - // that would allow us to init speech before it's needed - $(document).on('click',function() { - var greeting = new SpeechSynthesisUtterance('\x20'); - thisObj.synth.speak(greeting); - greeting.onstart = function(e) { - // utterance has started - $(document).off('click'); // unbind the click event listener - } - greeting.onend = function(e) { - // should now be able to get browser voices - // in browsers that require a click - thisObj.getBrowserVoices(); - if (thisObj.descVoices.length) { - thisObj.speechEnabled = true; - } - }; - }); - - // go ahead and call get browser voices in case it might work, - // for browsers that don't require a click - this.getBrowserVoices(); - if (this.descVoices.length) { - this.speechEnabled = true; - } - } - else { // context is either 'play' or 'prefs' or 'desc' - var greeting = new SpeechSynthesisUtterance('\x20'); - thisObj.synth.speak(greeting); - greeting.onstart = function(e) { - // utterance has started - $(document).off('click'); // unbind the click event listener - }; - greeting.onend = function(e) { - // should now be able to get browser voices - // in browsers that require a click - thisObj.getBrowserVoices(); - /* - // Safari 15.4 on MacOS has a bug: No voice array is returned - // The browser speaks, but we have no control over voices. - // Therefore, speechEnabled cannot be dependent on descVoices - // as long as Safari 15.4 is still supported - if (thisObj.descVoices.length) { - thisObj.speechEnabled = true; - } - */ - thisObj.speechEnabled = true; - }; - } - } - else { - // browser does not support speech synthesis - this.speechEnabled = false; - } - } - }; - - AblePlayer.prototype.getBrowserVoices = function () { - - // define this.descVoices array - // includes only languages that match the language of the captions or player - - var voices, descLangs, voiceLang, preferredLang; - - if (this.captionLang) { - preferredLang = this.captionLang.substring(0,2).toLowerCase(); - } - else { - preferredLang = this.lang.substring(0,2).toLowerCase(); - } - this.descVoices = []; - voices = this.synth.getVoices(); - descLangs = this.getDescriptionLangs(); - if (voices.length > 0) { - this.descVoices = []; - // available languages are identified with local suffixes (e.g., en-US) - for (var i=0; i 0) { - if (prefDescVoice) { - // select the language that matches prefDescVoice, if it's available - prefVoiceFound = false; - for (var i=0; i 0) { - this.swapTime = this.elapsed; - } - else { - this.swapTime = 0; - } - if (this.duration > 0) { - this.prevDuration = this.duration; - } - - // Capture current playback state, so media can resume after source is swapped - if (!this.okToPlay) { - this.okToPlay = this.playing; - } - - if (this.descOn) { - // user has requested the described version - this.showAlert(this.tt.alertDescribedVersion); - } - else { - // user has requested the non-described version - this.showAlert(this.tt.alertNonDescribedVersion); - } - - if (this.player === 'html5') { - - this.swappingSrc = true; - this.paused = true; - - if (this.usingDescribedVersion()) { - // the described version is currently playing. Swap to non-described - for (i=0; i < this.$sources.length; i++) { - // for all elements, replace src with data-orig-src - origSrc = this.$sources[i].getAttribute('data-orig-src'); - srcType = this.$sources[i].getAttribute('type'); - if (origSrc) { - this.$sources[i].setAttribute('src',origSrc); - } - } - } - else { - // the non-described version is currently playing. Swap to described. - for (i=0; i < this.$sources.length; i++) { - // for all elements, replace src with data-desc-src (if one exists) - // then store original source in a new data-orig-src attribute - origSrc = this.$sources[i].getAttribute('src'); - descSrc = this.$sources[i].getAttribute('data-desc-src'); - srcType = this.$sources[i].getAttribute('type'); - if (descSrc) { - this.$sources[i].setAttribute('src',descSrc); - this.$sources[i].setAttribute('data-orig-src',origSrc); - } - } - } - - if (this.recreatingPlayer) { - // stopgap to prevent multiple firings of recreatePlayer() - return; - } - if (this.playerCreated) { - // delete old player, then recreate it with new source & tracks - this.deletePlayer('swap-desc-html'); - this.recreatePlayer().then(function() { - if (!thisObj.loadingMedia) { - thisObj.media.load(); - thisObj.loadingMedia = true; - } - }); - } - else { - // player is in the process of being created - // no need to recreate it - } - } - else if (this.player === 'youtube') { - - if (this.usingDescribedVersion()) { - // the described version is currently playing. Swap to non-described - this.activeYouTubeId = this.youTubeId; - } - else { - // the non-described version is currently playing. Swap to described. - this.activeYouTubeId = this.youTubeDescId; - } - if (typeof this.youTubePlayer !== 'undefined') { - thisObj.swappingSrc = true; - if (thisObj.playing) { - // loadVideoById() loads and immediately plays the new video at swapTime - thisObj.youTubePlayer.loadVideoById(thisObj.activeYouTubeId,thisObj.swapTime); - } - else { - // cueVideoById() loads the new video and seeks to swapTime, but does not play - thisObj.youTubePlayer.cueVideoById(thisObj.activeYouTubeId,thisObj.swapTime); - } - } - if (this.playerCreated) { - this.deletePlayer('swap-desc-youtube'); - } - // player needs to be recreated with new source - if (this.recreatingPlayer) { - // stopgap to prevent multiple firings of recreatePlayer() - return; - } - this.recreatePlayer().then(function() { - // nothing to do here - // next steps occur when youtube onReady event fires - // see youtube.js > finalizeYoutubeInit() - }); - } - else if (this.player === 'vimeo') { - if (this.usingDescribedVersion()) { - // the described version is currently playing. Swap to non-described - this.activeVimeoId = this.vimeoId; - this.showAlert(this.tt.alertNonDescribedVersion); - } - else { - // the non-described version is currently playing. Swap to described. - this.activeVimeoId = this.vimeoDescId; - this.showAlert(this.tt.alertDescribedVersion); - } - if (this.playerCreated) { - this.deletePlayer('swap-desc-vimeo'); - } - // player needs to be recreated with new source - if (this.recreatingPlayer) { - // stopgap to prevent multiple firings of recreatePlayer() - return; - } - this.recreatePlayer().then(function() { - // load the new video source - thisObj.vimeoPlayer.loadVideo(thisObj.activeVimeoId).then(function() { - if (thisObj.playing) { - // video was playing when user requested an alternative version - // seek to swapTime and continue playback (playback happens automatically) - thisObj.vimeoPlayer.setCurrentTime(thisObj.swapTime); - } - else { - // Vimeo autostarts immediately after video loads - // The "Described" button should not trigger playback, so stop this before the user notices. - thisObj.vimeoPlayer.pause(); - } - }); - }); - } - }; - - AblePlayer.prototype.showDescription = function(now) { - - if (!this.hasClosedDesc || this.swappingSrc || !this.descOn || this.descMethod === 'video') { - return; - } - - var thisObj, i, cues, d, thisDescription, descText, msg; - thisObj = this; - - var flattenComponentForDescription = function (component) { - var result = []; - if (component.type === 'string') { - result.push(component.value); - } - else { - for (var i = 0; i < component.children.length; i++) { - result.push(flattenComponentForDescription(component.children[i])); - } - } - return result.join(''); - }; - - if (this.selectedDescriptions) { - cues = this.selectedDescriptions.cues; - } - else if (this.descriptions.length >= 1) { - cues = this.descriptions[0].cues; - } - else { - cues = []; - } - for (d = 0; d < cues.length; d++) { - if ((cues[d].start <= now) && (cues[d].end > now)) { - thisDescription = d; - break; - } - } - if (typeof thisDescription !== 'undefined') { - if (this.currentDescription !== thisDescription) { - // temporarily remove aria-live from $status in order to prevent description from being interrupted - this.$status.removeAttr('aria-live'); - descText = flattenComponentForDescription(cues[thisDescription].components); - if (this.descReader === 'screenreader') { - // load the new description into the container div for screen readers to read - this.$descDiv.html(descText); - } - else if (this.speechEnabled) { - // use browser's built-in speech synthesis - this.announceDescriptionText('description',descText); - if (this.prefDescVisible) { - // write description to the screen for sighted users - // but remove ARIA attributes since it isn't intended to be read by screen readers - this.$descDiv.html(descText).removeAttr('aria-live aria-atomic'); - } - } - else { - // browser does not support speech synthesis - // load the new description into the container div for screen readers to read - this.$descDiv.html(descText); - } - if (this.prefDescPause && this.descMethod === 'text') { - this.pauseMedia(); - this.pausedForDescription = true; - } - this.currentDescription = thisDescription; - } - } - else { - this.$descDiv.html(''); - this.currentDescription = -1; - // restore aria-live to $status - this.$status.attr('aria-live','polite'); - } - }; - - AblePlayer.prototype.syncSpeechToPlaybackRate = function(rate) { - - // called when user changed playback rate - // adjust rate of audio description to match - - var speechRate; - - if (rate === 0.5) { - speechRate = 0.7; // option 1 in prefs menu - } - else if (rate === 0.75) { - speechRate = 0.8; // option 2 in prefs menu - } - else if (rate === 1.0) { - speechRate = 1; // option 4 in prefs menu (normal speech, default) - } - else if (rate === 1.25) { - speechRate = 1.1; // option 5 in prefs menu - } - else if (rate === 1.5) { - speechRate = 1.2; // option 6 in prefs menu - } - else if (rate === 1.75) { - speechRate = 1.5; // option 7 in prefs menu - } - else if (rate === 2.0) { - speechRate = 2; // option 8 in prefs menu (fast) - } - else if (rate === 2.25) { - speechRate = 2.5; // option 9 in prefs menu (very fast) - } - else if (rate >= 2.5) { - speechRate = 3; // option 10 in prefs menu (super fast) - } - this.prefDescRate = speechRate; - }; - - AblePlayer.prototype.announceDescriptionText = function(context, text) { - - // this function announces description text using speech synthesis - // it's only called if already determined that browser supports speech synthesis - // context is either: - // 'description' - actual description text extracted from WebVTT file - // 'sample' - called when user changes a setting in Description Prefs dialog - - var thisObj, voiceName, i, voice, pitch, rate, volume, utterance, - timeElapsed, secondsElapsed; - - thisObj = this; - - // As of Feb 2021, - // 1. In some browsers (e.g., Chrome) window.speechSynthesis.getVoices() - // returns 0 voices unless the request is triggered with a user click - // Therefore, description may have failed to initialize when the page loaded - // This function cannot have been called without a mouse click. - // Therefore, this is a good time to check that, and try again if needed - // 2. In some browsers, the window.speechSynthesis.speaking property fails to reset, - // and onend event is never fired. This prevents new speech from being spoken. - // window.speechSynthesis.cancel() also fails, so it's impossible to recover. - // This only seems to happen with some voices. - // Typically the first voice in the getVoices() array (index 0) is realiable - // When speech synthesis gets wonky, this is a deep problem that impacts all browsers - // and typically requires a computer reboot to make right again. - // This has been observed frequently in macOS Big Sur, but also in Windows 10 - // To ignore user's voice preferences and always use the first voice, set the following var to true - // This is for testing only; not recommended for production - // unless the voice select field is also removed from the Prefs dialog - var useFirstVoice = false; - - if (!this.speechEnabled) { - // voices array failed to load the first time. Try again - this.initSpeech('desc'); - } - - if (context === 'sample') { - // get settings from form - voiceName = $('#' + this.mediaId + '_prefDescVoice').val(); - pitch = $('#' + this.mediaId + '_prefDescPitch').val(); - rate = $('#' + this.mediaId + '_prefDescRate').val(); - volume = $('#' + this.mediaId + '_prefDescVolume').val(); - } - else { - // get settings from global prefs - voiceName = this.prefDescVoice; - pitch = this.prefDescPitch; - rate = this.prefDescRate; - volume = this.prefDescVolume; - } - - // get the voice associated with the user's chosen voice name - if (this.descVoices) { - if (this.descVoices.length > 0) { - if (useFirstVoice) { - voice = this.descVoices[0]; - } - else if (voiceName) { - // get the voice that matches user's preferred voiceName - for (i = 0; i < this.descVoices.length; i++) { - if (this.descVoices[i].name == voiceName) { - voice = this.descVoices[i]; - break; - } - } - } - if (typeof voice === 'undefined') { - // no matching voice was found - // use the first voice in the array - voice = this.descVoices[0]; - } - } - } - else { - voice = null; - } - utterance = new SpeechSynthesisUtterance(); - if (voice) { - utterance.voice = voice; - } - utterance.voiceURI = 'native'; - utterance.volume = volume; - utterance.rate = rate; - utterance.pitch = pitch; - utterance.text = text; - // TODO: Consider the best language for the utterance: - // language of the web page? (this.lang) - // language of the WebVTT description track? - // language of the user's chosen voice? - // If there's a mismatch between any of these, the description will likely be unintelligible - utterance.lang = this.lang; - utterance.onstart = function(e) { - // utterance has started - }; - utterance.onpause = function(e) { - // utterance has paused - }; - utterance.onend = function(e) { - // utterance has ended - this.speakingDescription = false; - timeElapsed = e.elapsedTime; - // As of Firefox 95, e.elapsedTime is expressed in seconds - // Other browsers (tested in Chrome & Edge) express this in milliseconds - // Assume no utterance will require over 100 seconds to express... - if (timeElapsed > 100) { - // time is likely expressed in milliseconds - secondsElapsed = (e.elapsedTime/1000).toFixed(2); - } - else { - // time is likely already expressed in seconds; just need to round it - secondsElapsed = (e.elapsedTime).toFixed(2); - } - if (this.debug) { - console.log('Finished speaking. That took ' + secondsElapsed + ' seconds.'); - } - if (context === 'description') { - if (thisObj.prefDescPause) { - if (thisObj.pausedForDescription) { - thisObj.playMedia(); - this.pausedForDescription = false; - } - } - } - }; - utterance.onerror = function(e) { - // handle error - console.log('Web Speech API error',e); - }; - if (this.synth.paused) { - this.synth.resume(); - } - this.synth.speak(utterance); - this.speakingDescription = true; - }; - + AblePlayer.prototype.initDescription = function () { + // set default mode for delivering description (open vs closed) + // based on availability and user preference + + // called when player is being built, or when a user + // toggles the Description button or changes a description-related preference + + // The following variables are applicable to delivery of description: + // defaultStateDescriptions == 'on' or 'off', defined by website owner (overridden by prefDesc) + // prefDesc == 1 if user wants description (i.e., Description button is on); else 0 + // prefDescPause == 1 to pause video when description starts; else 0 + // prefDescVisible == 1 to visibly show text-based description area; else 0 + // prefDescMethod == either 'video' or 'text' (as of v4.0.10, prefDescMethod is always 'video') + // descMethod is the format actually used ('video' or 'text'), regardless of user preference + // hasOpenDesc == true if a described version of video is available via data-desc-src attribute + // hasClosedDesc == true if a description text track is available + // descOn == true if description of either type is on + // readDescriptionsAloud == true if text description is to be announced audibly; otherwise false + // descReader == either 'browser' or 'screenreader' + + var deferred, promise, thisObj; + + deferred = new $.Deferred(); + promise = deferred.promise(); + thisObj = this; + + if (this.mediaType === "audio") { + deferred.resolve(); + } + + // check to see if there's an open-described version of this video + // checks only the first source since if a described version is provided, + // it must be provided for all sources + this.descFile = this.$sources.first().attr("data-desc-src"); + if (typeof this.descFile !== "undefined") { + this.hasOpenDesc = true; + } else { + // there's no open-described version via data-desc-src, + // but what about data-youtube-desc-src or data-vimeo-desc-src? + // if these exist, they would have been defined earlier + if (this.youTubeDescId || this.vimeoDescId) { + this.hasOpenDesc = true; + } else { + // there are no open-described versions from any source + this.hasOpenDesc = false; + } + } + + // Set this.descMethod based on media availability & user preferences + if (this.hasOpenDesc && this.hasClosedDesc) { + // both formats are available. User gets their preference. + if (this.prefDescMethod) { + this.descMethod = this.prefDescMethod; + } else { + // user has no preference. Video is default. + this.descMethod = "video"; + } + } else if (this.hasOpenDesc) { + this.descMethod = "video"; + } else if (this.hasClosedDesc) { + this.descMethod = "text"; + } else { + // no description is available for this video + this.descMethod = null; + } + + // Set the default state of descriptions + if (this.descMethod) { + if (this.prefDesc === 1) { + this.descOn = true; + } else if (this.prefDesc === 0) { + this.descOn = false; + } else { + // user has no prefs. Use default state. + if (this.defaultStateDescriptions === 1) { + this.descOn = true; + } else { + this.descOn = false; + } + } + } else { + this.descOn = false; + } + if ( + typeof this.$descDiv === "undefined" && + this.hasClosedDesc && + this.descMethod === "text" + ) { + this.injectTextDescriptionArea(); + } + + if (this.descOn) { + if (this.descMethod === "video") { + if (!this.usingDescribedVersion()) { + // switched from non-described to described version + this.swapDescription(); + } + } + if (this.hasClosedDesc) { + if (this.prefDescVisible) { + // make description text visible + if (typeof this.$descDiv !== "undefined") { + this.$descDiv.show(); + this.$descDiv.removeClass("able-clipped"); + } + } else { + // keep it visible to screen readers, but hide it visibly + if (typeof this.$descDiv !== "undefined") { + this.$descDiv.addClass("able-clipped"); + } + } + } + } else { + // description is off. + if (this.descMethod === "video") { + // user has turned off described version of video + if (this.usingDescribedVersion()) { + // user was using the described verion. Swap for non-described version + this.swapDescription(); + } + } else if (this.descMethod === "text") { + // user has turned off text description + // hide description div from everyone, including screen reader users + if (typeof this.$descDiv !== "undefined") { + this.$descDiv.hide(); + this.$descDiv.removeClass("able-clipped"); + } + } + } + deferred.resolve(); + return promise; + }; + + AblePlayer.prototype.usingDescribedVersion = function () { + // Returns true if currently using audio description, false otherwise. + + if (this.player === "youtube") { + return this.activeYouTubeId === this.youTubeDescId; + } else if (this.player === "vimeo") { + return this.activeVimeoId === this.vimeoDescId; + } else { + return ( + this.$sources.first().attr("data-desc-src") === + this.$sources.first().attr("src") + ); + } + }; + + AblePlayer.prototype.initSpeech = function (context) { + var thisObj = this; + + if (this.speechEnabled === null) { + if (typeof this.synth !== "undefined") { + // cancel any previous synth instance and reinitialize + this.synth.cancel(); + } + + if (window.speechSynthesis) { + // browser supports speech synthesis + this.synth = window.speechSynthesis; + this.synth.cancel(); + if (context === "init") { + // Only add the click listener if context is 'init' + var initSpeechSynthesis = function () { + var greeting = new SpeechSynthesisUtterance("\x20"); + thisObj.synth.speak(greeting); + greeting.onend = function (e) { + // should now be able to get browser voices + // in browsers that require a click + thisObj.getBrowserVoices(); + if (thisObj.descVoices.length) { + thisObj.speechEnabled = true; + } + // Remove the click event listener after the first invocation + $(document).off("click", initSpeechSynthesis); + }; + }; + // Bind the click event listener + $(document).on("click", initSpeechSynthesis); + } else { + // context is either 'play', 'prefs', or 'desc' + // This part remains unchanged, as it's outside the 'init' context scope + var greeting = new SpeechSynthesisUtterance("\x20"); + thisObj.synth.speak(greeting); + greeting.onstart = function (e) { + // utterance has started + }; + greeting.onend = function (e) { + thisObj.getBrowserVoices(); + thisObj.speechEnabled = true; + }; + } + // Attempt to get browser voices immediately, for browsers that don't require a click + this.getBrowserVoices(); + if (this.descVoices.length) { + this.speechEnabled = true; + } + } else { + // browser does not support speech synthesis + this.speechEnabled = false; + } + } + }; + + AblePlayer.prototype.getBrowserVoices = function () { + // define this.descVoices array + // includes only languages that match the language of the captions or player + + var voices, descLangs, voiceLang, preferredLang; + + if (this.captionLang) { + preferredLang = this.captionLang.substring(0, 2).toLowerCase(); + } else { + preferredLang = this.lang.substring(0, 2).toLowerCase(); + } + this.descVoices = []; + voices = this.synth.getVoices(); + descLangs = this.getDescriptionLangs(); + if (voices.length > 0) { + this.descVoices = []; + // available languages are identified with local suffixes (e.g., en-US) + for (var i = 0; i < voices.length; i++) { + // match only the first 2 characters of the lang code + voiceLang = voices[i].lang.substring(0, 2).toLowerCase(); + if ( + voiceLang === preferredLang && + descLangs.indexOf(voiceLang) !== -1 + ) { + // this voice matches preferredLang + // AND there's a matching description track in this language + // Add this voice to final array + this.descVoices.push(voices[i]); + } + } + if (!this.descVoices.length) { + // no voices available in the default language(s) + // just use all voices, regardless of language + this.descVoices = voices; + } + } + return false; + }; + + AblePlayer.prototype.getDescriptionLangs = function () { + // returns an array of languages (from srclang atttributes) + // in which there are description tracks + // use only first two characters of the lang code + var descLangs = []; + if (this.tracks) { + for (var i = 0; i < this.tracks.length; i++) { + if (this.tracks[i].kind === "descriptions") { + descLangs.push(this.tracks[i].language.substring(0, 2).toLowerCase()); + } + } + } + return descLangs; + }; + + AblePlayer.prototype.setDescriptionVoice = function () { + // set description voice on player init, or when user changes caption language + // Voice is determined in the following order of precedence: + // 1. User's preferred voice for this language, saved in a cookie + // 2. The first available voice in the array of available voices for this browser in this language + + var cookie, voices, prefDescVoice, descVoice, descLang, prefVoiceFound; + cookie = this.getCookie(); + if (typeof cookie.voices !== "undefined") { + prefDescVoice = this.getPrefDescVoice(); + } else { + prefDescVoice = null; + } + + this.getBrowserVoices(); + this.rebuildDescPrefsForm(); + + if (this.selectedDescriptions) { + descLang = this.selectedDescriptions.language; + } else if (this.captionLang) { + descLang = this.captionLang; + } else { + descLang = this.lang; + } + + if (this.synth) { + voices = this.synth.getVoices(); + if (voices.length > 0) { + if (prefDescVoice) { + // select the language that matches prefDescVoice, if it's available + prefVoiceFound = false; + for (var i = 0; i < voices.length; i++) { + // first, be sure voice is the correct language + if ( + voices[i].lang.substring(0, 2).toLowerCase() === + descLang.substring(0, 2).toLowerCase() + ) { + if (voices[i].name === prefDescVoice) { + descVoice = voices[i].name; + prefVoiceFound = true; + break; + } + } + } + } + if (!prefVoiceFound) { + // select the first language that matches the first 2 characters of the lang code + for (var i = 0; i < voices.length; i++) { + if ( + voices[i].lang.substring(0, 2).toLowerCase() === + descLang.substring(0, 2).toLowerCase() + ) { + descVoice = voices[i].name; + break; + } + } + } + // make this the user's current preferred voice + this.prefDescVoice = descVoice; + this.prefDescVoiceLang = descLang; + // select this voice in the Description Prefs dialog + if (this.$voiceSelectField) { + var selectedOption = this.$voiceSelectField.find( + 'option[value="' + this.prefDescVoice + '"]' + ); + this.$voiceSelectField.val(this.prefDescVoice); + } + this.updateCookie("voice"); + } + } + }; + + AblePlayer.prototype.swapDescription = function () { + // swap described and non-described source media, depending on which is playing + // this function is only called in two circumstances: + // 1. Swapping to described version when initializing player (based on user prefs & availability) + // (playerCreated == false) + // 2. User is toggling description + // (playerCreated == true) + + var thisObj, i, origSrc, descSrc, srcType, newSource; + + thisObj = this; + + // We are no longer loading the previous media source + // Only now, as a new source is requested, is it safe to reset this var + // It will be reset to true when media.load() is called + this.loadingMedia = false; + + // get element that has focus at the time swap is initiated + // after player is rebuilt, focus will return to that same element + // (if it exists) + this.$focusedElement = $(":focus"); + + // get current time of current source, and attempt to start new video at the same time + // whether this is possible will be determined after the new media source has loaded + // see onMediaNewSourceLoad() + if (this.elapsed > 0) { + this.swapTime = this.elapsed; + } else { + this.swapTime = 0; + } + if (this.duration > 0) { + this.prevDuration = this.duration; + } + + // Capture current playback state, so media can resume after source is swapped + if (!this.okToPlay) { + this.okToPlay = this.playing; + } + + if (this.descOn) { + // user has requested the described version + this.showAlert(this.tt.alertDescribedVersion); + } else { + // user has requested the non-described version + this.showAlert(this.tt.alertNonDescribedVersion); + } + + if (this.player === "html5") { + this.swappingSrc = true; + this.paused = true; + + if (this.usingDescribedVersion()) { + // the described version is currently playing. Swap to non-described + for (i = 0; i < this.$sources.length; i++) { + // for all elements, replace src with data-orig-src + origSrc = this.$sources[i].getAttribute("data-orig-src"); + srcType = this.$sources[i].getAttribute("type"); + if (origSrc) { + this.$sources[i].setAttribute("src", origSrc); + } + } + } else { + // the non-described version is currently playing. Swap to described. + for (i = 0; i < this.$sources.length; i++) { + // for all elements, replace src with data-desc-src (if one exists) + // then store original source in a new data-orig-src attribute + origSrc = this.$sources[i].getAttribute("src"); + descSrc = this.$sources[i].getAttribute("data-desc-src"); + srcType = this.$sources[i].getAttribute("type"); + if (descSrc) { + this.$sources[i].setAttribute("src", descSrc); + this.$sources[i].setAttribute("data-orig-src", origSrc); + } + } + } + + if (this.recreatingPlayer) { + // stopgap to prevent multiple firings of recreatePlayer() + return; + } + if (this.playerCreated) { + // delete old player, then recreate it with new source & tracks + this.deletePlayer("swap-desc-html"); + this.recreatePlayer().then(function () { + if (!thisObj.loadingMedia) { + thisObj.media.load(); + thisObj.loadingMedia = true; + } + }); + } else { + // player is in the process of being created + // no need to recreate it + } + } else if (this.player === "youtube") { + if (this.usingDescribedVersion()) { + // the described version is currently playing. Swap to non-described + this.activeYouTubeId = this.youTubeId; + } else { + // the non-described version is currently playing. Swap to described. + this.activeYouTubeId = this.youTubeDescId; + } + if (typeof this.youTubePlayer !== "undefined") { + thisObj.swappingSrc = true; + if (thisObj.playing) { + // loadVideoById() loads and immediately plays the new video at swapTime + thisObj.youTubePlayer.loadVideoById( + thisObj.activeYouTubeId, + thisObj.swapTime + ); + } else { + // cueVideoById() loads the new video and seeks to swapTime, but does not play + thisObj.youTubePlayer.cueVideoById( + thisObj.activeYouTubeId, + thisObj.swapTime + ); + } + } + if (this.playerCreated) { + this.deletePlayer("swap-desc-youtube"); + } + // player needs to be recreated with new source + if (this.recreatingPlayer) { + // stopgap to prevent multiple firings of recreatePlayer() + return; + } + this.recreatePlayer().then(function () { + // nothing to do here + // next steps occur when youtube onReady event fires + // see youtube.js > finalizeYoutubeInit() + }); + } else if (this.player === "vimeo") { + if (this.usingDescribedVersion()) { + // the described version is currently playing. Swap to non-described + this.activeVimeoId = this.vimeoId; + this.showAlert(this.tt.alertNonDescribedVersion); + } else { + // the non-described version is currently playing. Swap to described. + this.activeVimeoId = this.vimeoDescId; + this.showAlert(this.tt.alertDescribedVersion); + } + if (this.playerCreated) { + this.deletePlayer("swap-desc-vimeo"); + } + // player needs to be recreated with new source + if (this.recreatingPlayer) { + // stopgap to prevent multiple firings of recreatePlayer() + return; + } + this.recreatePlayer().then(function () { + // load the new video source + thisObj.vimeoPlayer.loadVideo(thisObj.activeVimeoId).then(function () { + if (thisObj.playing) { + // video was playing when user requested an alternative version + // seek to swapTime and continue playback (playback happens automatically) + thisObj.vimeoPlayer.setCurrentTime(thisObj.swapTime); + } else { + // Vimeo autostarts immediately after video loads + // The "Described" button should not trigger playback, so stop this before the user notices. + thisObj.vimeoPlayer.pause(); + } + }); + }); + } + }; + + AblePlayer.prototype.showDescription = function (now) { + if ( + !this.hasClosedDesc || + this.swappingSrc || + !this.descOn || + this.descMethod === "video" + ) { + return; + } + + var thisObj, i, cues, d, thisDescription, descText, msg; + thisObj = this; + + var flattenComponentForDescription = function (component) { + var result = []; + if (component.type === "string") { + result.push(component.value); + } else { + for (var i = 0; i < component.children.length; i++) { + result.push(flattenComponentForDescription(component.children[i])); + } + } + return result.join(""); + }; + + if (this.selectedDescriptions) { + cues = this.selectedDescriptions.cues; + } else if (this.descriptions.length >= 1) { + cues = this.descriptions[0].cues; + } else { + cues = []; + } + for (d = 0; d < cues.length; d++) { + if (cues[d].start <= now && cues[d].end > now) { + thisDescription = d; + break; + } + } + if (typeof thisDescription !== "undefined") { + if (this.currentDescription !== thisDescription) { + // temporarily remove aria-live from $status in order to prevent description from being interrupted + this.$status.removeAttr("aria-live"); + descText = flattenComponentForDescription( + cues[thisDescription].components + ); + if (this.descReader === "screenreader") { + // load the new description into the container div for screen readers to read + this.$descDiv.html(descText); + } else if (this.speechEnabled) { + // use browser's built-in speech synthesis + this.announceDescriptionText("description", descText); + if (this.prefDescVisible) { + // write description to the screen for sighted users + // but remove ARIA attributes since it isn't intended to be read by screen readers + this.$descDiv.html(descText).removeAttr("aria-live aria-atomic"); + } + } else { + // browser does not support speech synthesis + // load the new description into the container div for screen readers to read + this.$descDiv.html(descText); + } + if (this.prefDescPause && this.descMethod === "text") { + this.pauseMedia(); + this.pausedForDescription = true; + } + this.currentDescription = thisDescription; + } + } else { + this.$descDiv.html(""); + this.currentDescription = -1; + // restore aria-live to $status + this.$status.attr("aria-live", "polite"); + } + }; + + AblePlayer.prototype.syncSpeechToPlaybackRate = function (rate) { + // called when user changed playback rate + // adjust rate of audio description to match + + var speechRate; + + if (rate === 0.5) { + speechRate = 0.7; // option 1 in prefs menu + } else if (rate === 0.75) { + speechRate = 0.8; // option 2 in prefs menu + } else if (rate === 1.0) { + speechRate = 1; // option 4 in prefs menu (normal speech, default) + } else if (rate === 1.25) { + speechRate = 1.1; // option 5 in prefs menu + } else if (rate === 1.5) { + speechRate = 1.2; // option 6 in prefs menu + } else if (rate === 1.75) { + speechRate = 1.5; // option 7 in prefs menu + } else if (rate === 2.0) { + speechRate = 2; // option 8 in prefs menu (fast) + } else if (rate === 2.25) { + speechRate = 2.5; // option 9 in prefs menu (very fast) + } else if (rate >= 2.5) { + speechRate = 3; // option 10 in prefs menu (super fast) + } + this.prefDescRate = speechRate; + }; + + AblePlayer.prototype.announceDescriptionText = function (context, text) { + // this function announces description text using speech synthesis + // it's only called if already determined that browser supports speech synthesis + // context is either: + // 'description' - actual description text extracted from WebVTT file + // 'sample' - called when user changes a setting in Description Prefs dialog + + var thisObj, + voiceName, + i, + voice, + pitch, + rate, + volume, + utterance, + timeElapsed, + secondsElapsed; + + thisObj = this; + + // As of Feb 2021, + // 1. In some browsers (e.g., Chrome) window.speechSynthesis.getVoices() + // returns 0 voices unless the request is triggered with a user click + // Therefore, description may have failed to initialize when the page loaded + // This function cannot have been called without a mouse click. + // Therefore, this is a good time to check that, and try again if needed + // 2. In some browsers, the window.speechSynthesis.speaking property fails to reset, + // and onend event is never fired. This prevents new speech from being spoken. + // window.speechSynthesis.cancel() also fails, so it's impossible to recover. + // This only seems to happen with some voices. + // Typically the first voice in the getVoices() array (index 0) is realiable + // When speech synthesis gets wonky, this is a deep problem that impacts all browsers + // and typically requires a computer reboot to make right again. + // This has been observed frequently in macOS Big Sur, but also in Windows 10 + // To ignore user's voice preferences and always use the first voice, set the following var to true + // This is for testing only; not recommended for production + // unless the voice select field is also removed from the Prefs dialog + var useFirstVoice = false; + + if (!this.speechEnabled) { + // voices array failed to load the first time. Try again + this.initSpeech("desc"); + } + + if (context === "sample") { + // get settings from form + voiceName = $("#" + this.mediaId + "_prefDescVoice").val(); + pitch = $("#" + this.mediaId + "_prefDescPitch").val(); + rate = $("#" + this.mediaId + "_prefDescRate").val(); + volume = $("#" + this.mediaId + "_prefDescVolume").val(); + } else { + // get settings from global prefs + voiceName = this.prefDescVoice; + pitch = this.prefDescPitch; + rate = this.prefDescRate; + volume = this.prefDescVolume; + } + + // get the voice associated with the user's chosen voice name + if (this.descVoices) { + if (this.descVoices.length > 0) { + if (useFirstVoice) { + voice = this.descVoices[0]; + } else if (voiceName) { + // get the voice that matches user's preferred voiceName + for (i = 0; i < this.descVoices.length; i++) { + if (this.descVoices[i].name == voiceName) { + voice = this.descVoices[i]; + break; + } + } + } + if (typeof voice === "undefined") { + // no matching voice was found + // use the first voice in the array + voice = this.descVoices[0]; + } + } + } else { + voice = null; + } + utterance = new SpeechSynthesisUtterance(); + if (voice) { + utterance.voice = voice; + } + utterance.voiceURI = "native"; + utterance.volume = volume; + utterance.rate = rate; + utterance.pitch = pitch; + utterance.text = text; + // TODO: Consider the best language for the utterance: + // language of the web page? (this.lang) + // language of the WebVTT description track? + // language of the user's chosen voice? + // If there's a mismatch between any of these, the description will likely be unintelligible + utterance.lang = this.lang; + utterance.onstart = function (e) { + // utterance has started + }; + utterance.onpause = function (e) { + // utterance has paused + }; + utterance.onend = function (e) { + // utterance has ended + this.speakingDescription = false; + timeElapsed = e.elapsedTime; + // As of Firefox 95, e.elapsedTime is expressed in seconds + // Other browsers (tested in Chrome & Edge) express this in milliseconds + // Assume no utterance will require over 100 seconds to express... + if (timeElapsed > 100) { + // time is likely expressed in milliseconds + secondsElapsed = (e.elapsedTime / 1000).toFixed(2); + } else { + // time is likely already expressed in seconds; just need to round it + secondsElapsed = e.elapsedTime.toFixed(2); + } + if (this.debug) { + console.log( + "Finished speaking. That took " + secondsElapsed + " seconds." + ); + } + if (context === "description") { + if (thisObj.prefDescPause) { + if (thisObj.pausedForDescription) { + thisObj.playMedia(); + this.pausedForDescription = false; + } + } + } + }; + utterance.onerror = function (e) { + // handle error + console.log("Web Speech API error", e); + }; + if (this.synth.paused) { + this.synth.resume(); + } + this.synth.speak(utterance); + this.speakingDescription = true; + }; })(jQuery); (function ($) { diff --git a/build/ableplayer.min.js b/build/ableplayer.min.js index 9bdfb075..f5b8d93a 100644 --- a/build/ableplayer.min.js +++ b/build/ableplayer.min.js @@ -1,3 +1,3 @@ /*! ableplayer V4.5.3 */ -"use strict";var AblePlayerInstances=[];!function(r){r(function(){r("video, audio").each(function(t,e){void 0!==r(e).data("able-player")&&AblePlayerInstances.push(new AblePlayer(r(this),r(e)))})}),window.onYouTubeIframeAPIReady=function(){AblePlayer.youTubeIframeAPIReady=!0,r("body").trigger("youTubeIframeAPIReady",[])},r(window).on("keydown",function(t){1===AblePlayer.nextIndex&&AblePlayer.lastCreated.onPlayerKeyPress(t)}),window.AblePlayer=function(t){if((AblePlayer.lastCreated=this).media=t,0!==r(t).length){if(void 0!==r(t).attr("autoplay")?(this.autoplay=!0,this.okToPlay=!0):(this.autoplay=!1,this.okToPlay=!1),void 0!==r(t).attr("loop")?this.loop=!0:this.loop=!1,void 0!==r(t).attr("playsinline")?this.playsInline="1":this.playsInline="0",r(t).attr("poster")?this.hasPoster=!0:this.hasPoster=!1,r(t).attr("width")&&(this.width=r(t).attr("width")),r(t).attr("height")&&(this.height=r(t).attr("height")),void 0!==r(t).data("start-time")&&r.isNumeric(r(t).data("start-time"))?this.startTime=r(t).data("start-time"):this.startTime=0,void 0!==r(t).data("debug")&&!1!==r(t).data("debug")?this.debug=!0:this.debug=!1,void 0!==r(t).data("root-path")?this.rootPath=r(t).data("root-path").replace(/\/?$/,"/"):this.rootPath=this.getRootPath(),this.defaultVolume=7,void 0!==r(t).data("volume")&&""!==r(t).data("volume")){var e=r(t).data("volume");0<=e&&e<=10&&(this.defaultVolume=e)}if(this.volume=this.defaultVolume,void 0!==r(t).data("use-chapters-button")&&!1===r(t).data("use-chapters-button")?this.useChaptersButton=!1:this.useChaptersButton=!0,void 0!==r(t).data("descriptions-audible")&&!1===r(t).data("descriptions-audible")?this.readDescriptionsAloud=!1:void 0!==r(t).data("description-audible")&&!1===r(t).data("description-audible")?this.readDescriptionsAloud=!1:this.readDescriptionsAloud=!0,"screenreader"==r(t).data("desc-reader")?this.descReader="screenreader":this.descReader="browser","off"==r(t).data("state-captions")?this.defaultStateCaptions=0:this.defaultStateCaptions=1,"on"==r(t).data("state-descriptions")?this.defaultStateDescriptions=1:this.defaultStateDescriptions=0,"off"==r(t).data("desc-pause-default")?this.defaultDescPause=0:this.defaultDescPause=1,void 0!==r(t).data("heading-level")&&""!==r(t).data("heading-level")){var i=r(t).data("heading-level");/^[0-6]*$/.test(i)&&(this.playerHeadingLevel=i)}if(void 0!==r(t).data("transcript-div")&&""!==r(t).data("transcript-div")?this.transcriptDivLocation=r(t).data("transcript-div"):this.transcriptDivLocation=null,void 0!==r(t).data("include-transcript")&&!1===r(t).data("include-transcript")?this.hideTranscriptButton=!0:this.hideTranscriptButton=null,this.transcriptType=null,void 0!==r(t).data("transcript-src")?(this.transcriptSrc=r(t).data("transcript-src"),this.transcriptSrcHasRequiredParts()&&(this.transcriptType="manual")):0",{class:"able-controller"}).hide():"toolbar"===t[e]&&(i=n("
",{class:"able-window-toolbar"}).hide()),n("body").append(i),a=.2126*(s=i.css("background-color").replace(/[^\d,]/g,"").split(","))[0]+.7152*s[1]+.0722*s[2]<125?"white":"black","controller"===t[e]?this.iconColor=a:"toolbar"===t[e]&&(this.toolbarIconColor=a),i.remove()},AblePlayer.prototype.setButtonImages=function(){this.imgPath=this.rootPath+"button-icons/"+this.iconColor+"/",this.playButtonImg=this.imgPath+"play.png",this.pauseButtonImg=this.imgPath+"pause.png",this.restartButtonImg=this.imgPath+"restart.png",this.rewindButtonImg=this.imgPath+"rewind.png",this.forwardButtonImg=this.imgPath+"forward.png",this.previousButtonImg=this.imgPath+"previous.png",this.nextButtonImg=this.imgPath+"next.png","arrows"===this.speedIcons?(this.fasterButtonImg=this.imgPath+"slower.png",this.slowerButtonImg=this.imgPath+"faster.png"):"animals"===this.speedIcons&&(this.fasterButtonImg=this.imgPath+"rabbit.png",this.slowerButtonImg=this.imgPath+"turtle.png"),this.captionsButtonImg=this.imgPath+"captions.png",this.chaptersButtonImg=this.imgPath+"chapters.png",this.signButtonImg=this.imgPath+"sign.png",this.transcriptButtonImg=this.imgPath+"transcript.png",this.descriptionsButtonImg=this.imgPath+"descriptions.png",this.fullscreenExpandButtonImg=this.imgPath+"fullscreen-expand.png",this.fullscreenCollapseButtonImg=this.imgPath+"fullscreen-collapse.png",this.prefsButtonImg=this.imgPath+"preferences.png",this.helpButtonImg=this.imgPath+"help.png"},AblePlayer.prototype.getSvgData=function(t){var e=Array();switch(t){case"play":e[0]="0 0 16 20",e[1]="M0 18.393v-16.429q0-0.29 0.184-0.402t0.441 0.033l14.821 8.237q0.257 0.145 0.257 0.346t-0.257 0.346l-14.821 8.237q-0.257 0.145-0.441 0.033t-0.184-0.402z";break;case"pause":e[0]="0 0 20 20",e[1]="M0 18.036v-15.714q0-0.29 0.212-0.502t0.502-0.212h5.714q0.29 0 0.502 0.212t0.212 0.502v15.714q0 0.29-0.212 0.502t-0.502 0.212h-5.714q-0.29 0-0.502-0.212t-0.212-0.502zM10 18.036v-15.714q0-0.29 0.212-0.502t0.502-0.212h5.714q0.29 0 0.502 0.212t0.212 0.502v15.714q0 0.29-0.212 0.502t-0.502 0.212h-5.714q-0.29 0-0.502-0.212t-0.212-0.502z";break;case"stop":e[0]="0 0 20 20",e[1]="M0 18.036v-15.714q0-0.29 0.212-0.502t0.502-0.212h15.714q0.29 0 0.502 0.212t0.212 0.502v15.714q0 0.29-0.212 0.502t-0.502 0.212h-15.714q-0.29 0-0.502-0.212t-0.212-0.502z";break;case"restart":e[0]="0 0 20 20",e[1]="M18 8h-6l2.243-2.243c-1.133-1.133-2.64-1.757-4.243-1.757s-3.109 0.624-4.243 1.757c-1.133 1.133-1.757 2.64-1.757 4.243s0.624 3.109 1.757 4.243c1.133 1.133 2.64 1.757 4.243 1.757s3.109-0.624 4.243-1.757c0.095-0.095 0.185-0.192 0.273-0.292l1.505 1.317c-1.466 1.674-3.62 2.732-6.020 2.732-4.418 0-8-3.582-8-8s3.582-8 8-8c2.209 0 4.209 0.896 5.656 2.344l2.344-2.344v6z";break;case"rewind":e[0]="0 0 20 20",e[1]="M11.25 3.125v6.25l6.25-6.25v13.75l-6.25-6.25v6.25l-6.875-6.875z";break;case"forward":e[0]="0 0 20 20",e[1]="M10 16.875v-6.25l-6.25 6.25v-13.75l6.25 6.25v-6.25l6.875 6.875z";break;case"previous":e[0]="0 0 20 20",e[1]="M5 17.5v-15h2.5v6.875l6.25-6.25v13.75l-6.25-6.25v6.875z";break;case"next":e[0]="0 0 20 20",e[1]="M15 2.5v15h-2.5v-6.875l-6.25 6.25v-13.75l6.25 6.25v-6.875z";break;case"slower":e[0]="0 0 20 20",e[1]="M0 7.321q0-0.29 0.212-0.502t0.502-0.212h10q0.29 0 0.502 0.212t0.212 0.502-0.212 0.502l-5 5q-0.212 0.212-0.502 0.212t-0.502-0.212l-5-5q-0.212-0.212-0.212-0.502z";break;case"faster":e[0]="0 0 11 20",e[1]="M0 12.411q0-0.29 0.212-0.502l5-5q0.212-0.212 0.502-0.212t0.502 0.212l5 5q0.212 0.212 0.212 0.502t-0.212 0.502-0.502 0.212h-10q-0.29 0-0.502-0.212t-0.212-0.502z";break;case"turtle":e[0]="0 0 20 20",e[1]="M17.212 3.846c-0.281-0.014-0.549 0.025-0.817 0.144-1.218 0.542-1.662 2.708-2.163 3.942-1.207 2.972-7.090 4.619-11.755 5.216-0.887 0.114-1.749 0.74-2.428 1.466 0.82-0.284 2.126-0.297 2.74 0.144 0.007 0.488-0.376 1.062-0.625 1.37-0.404 0.5-0.398 0.793 0.12 0.793 0.473 0 0.752 0.007 1.635 0 0.393-0.003 0.618-0.16 1.49-1.49 3.592 0.718 5.986-0.264 5.986-0.264s0.407 1.755 1.418 1.755h1.49c0.633 0 0.667-0.331 0.625-0.433-0.448-1.082-0.68-1.873-0.769-2.5-0.263-1.857 0.657-3.836 2.524-5.457 0.585 0.986 2.253 0.845 2.909-0.096s0.446-2.268-0.192-3.221c-0.49-0.732-1.345-1.327-2.188-1.37zM8.221 4.663c-0.722-0.016-1.536 0.111-2.5 0.409-4.211 1.302-4.177 4.951-3.51 5.745 0 0-0.955 0.479-0.409 1.274 0.448 0.652 3.139 0.191 5.409-0.529s4.226-1.793 5.312-2.692c0.948-0.785 0.551-2.106-0.505-1.947-0.494-0.98-1.632-2.212-3.798-2.26zM18.846 5.962c0.325 0 0.577 0.252 0.577 0.577s-0.252 0.577-0.577 0.577c-0.325 0-0.577-0.252-0.577-0.577s0.252-0.577 0.577-0.577z";break;case"rabbit":e[0]="0 0 20 20",e[1]="M10.817 0c-2.248 0-1.586 0.525-1.154 0.505 1.551-0.072 5.199 0.044 6.851 2.428 0 0-1.022-2.933-5.697-2.933zM10.529 0.769c-2.572 0-2.837 0.51-2.837 1.106 0 0.545 1.526 0.836 2.524 0.697 2.778-0.386 4.231-0.12 5.264 0.865-1.010 0.779-0.75 1.401-1.274 1.851-1.093 0.941-2.643-0.673-4.976-0.673-2.496 0-4.712 1.92-4.712 4.76-0.157-0.537-0.769-0.913-1.442-0.913-0.974 0-1.514 0.637-1.514 1.49 0 0.769 1.13 1.791 2.861 0.938 0.499 1.208 2.265 1.364 2.452 1.418 0.538 0.154 1.875 0.098 1.875 0.865 0 0.794-1.034 1.094-1.034 1.707 0 1.070 1.758 0.873 2.284 1.034 1.683 0.517 2.103 1.214 2.788 2.212 0.771 1.122 2.572 1.408 2.572 0.625 0-3.185-4.413-4.126-4.399-4.135 0.608-0.382 2.139-1.397 2.139-3.534 0-1.295-0.703-2.256-1.755-2.861 1.256 0.094 2.572 1.205 2.572 2.74 0 1.877-0.653 2.823-0.769 2.957 1.975-1.158 3.193-3.91 3.029-6.37 0.61 0.401 1.27 0.577 1.971 0.625 0.751 0.052 1.475-0.225 1.635-0.529 0.38-0.723 0.162-2.321-0.12-2.837-0.763-1.392-2.236-1.73-3.606-1.683-1.202-1.671-3.812-2.356-5.529-2.356zM1.37 3.077l-0.553 1.538h3.726c0.521-0.576 1.541-1.207 2.284-1.538h-5.457zM18.846 5.192c0.325 0 0.577 0.252 0.577 0.577s-0.252 0.577-0.577 0.577c-0.325 0-0.577-0.252-0.577-0.577s0.252-0.577 0.577-0.577zM0.553 5.385l-0.553 1.538h3.197c0.26-0.824 0.586-1.328 0.769-1.538h-3.413z";break;case"ellipsis":e[0]="0 0 20 20",e[1]="M10.001 7.8c-1.215 0-2.201 0.985-2.201 2.2s0.986 2.2 2.201 2.2c1.215 0 2.199-0.985 2.199-2.2s-0.984-2.2-2.199-2.2zM3.001 7.8c-1.215 0-2.201 0.985-2.201 2.2s0.986 2.2 2.201 2.2c1.215 0 2.199-0.986 2.199-2.2s-0.984-2.2-2.199-2.2zM17.001 7.8c-1.215 0-2.201 0.985-2.201 2.2s0.986 2.2 2.201 2.2c1.215 0 2.199-0.985 2.199-2.2s-0.984-2.2-2.199-2.2z";break;case"pipe":e[0]="0 0 20 20",e[1]="M10.15 0.179h0.623c0.069 0 0.127 0.114 0.127 0.253v19.494c0 0.139-0.057 0.253-0.127 0.253h-1.247c-0.069 0-0.126-0.114-0.126-0.253v-19.494c0-0.139 0.057-0.253 0.126-0.253h0.623z";break;case"captions":e[0]="0 0 20 20",e[1]="M0.033 3.624h19.933v12.956h-19.933v-12.956zM18.098 10.045c-0.025-2.264-0.124-3.251-0.743-3.948-0.112-0.151-0.322-0.236-0.496-0.344-0.606-0.386-3.465-0.526-6.782-0.526s-6.313 0.14-6.907 0.526c-0.185 0.108-0.396 0.193-0.519 0.344-0.607 0.697-0.693 1.684-0.731 3.948 0.037 2.265 0.124 3.252 0.731 3.949 0.124 0.161 0.335 0.236 0.519 0.344 0.594 0.396 3.59 0.526 6.907 0.547 3.317-0.022 6.176-0.151 6.782-0.547 0.174-0.108 0.384-0.183 0.496-0.344 0.619-0.697 0.717-1.684 0.743-3.949v0 0zM9.689 9.281c-0.168-1.77-1.253-2.813-3.196-2.813-1.773 0-3.168 1.387-3.168 3.617 0 2.239 1.271 3.636 3.372 3.636 1.676 0 2.851-1.071 3.035-2.852h-2.003c-0.079 0.661-0.397 1.168-1.068 1.168-1.059 0-1.253-0.91-1.253-1.876 0-1.33 0.442-2.010 1.174-2.010 0.653 0 1.068 0.412 1.13 1.129h1.977zM16.607 9.281c-0.167-1.77-1.252-2.813-3.194-2.813-1.773 0-3.168 1.387-3.168 3.617 0 2.239 1.271 3.636 3.372 3.636 1.676 0 2.851-1.071 3.035-2.852h-2.003c-0.079 0.661-0.397 1.168-1.068 1.168-1.059 0-1.253-0.91-1.253-1.876 0-1.33 0.441-2.010 1.174-2.010 0.653 0 1.068 0.412 1.13 1.129h1.976z";break;case"descriptions":e[0]="0 0 20 20",e[1]="M17.623 3.57h-1.555c1.754 1.736 2.763 4.106 2.763 6.572 0 2.191-0.788 4.286-2.189 5.943h1.484c1.247-1.704 1.945-3.792 1.945-5.943-0-2.418-0.886-4.754-2.447-6.572v0zM14.449 3.57h-1.55c1.749 1.736 2.757 4.106 2.757 6.572 0 2.191-0.788 4.286-2.187 5.943h1.476c1.258-1.704 1.951-3.792 1.951-5.943-0-2.418-0.884-4.754-2.447-6.572v0zM11.269 3.57h-1.542c1.752 1.736 2.752 4.106 2.752 6.572 0 2.191-0.791 4.286-2.181 5.943h1.473c1.258-1.704 1.945-3.792 1.945-5.943 0-2.418-0.876-4.754-2.447-6.572v0zM10.24 9.857c0 3.459-2.826 6.265-6.303 6.265v0.011h-3.867v-12.555h3.896c3.477 0 6.274 2.806 6.274 6.279v0zM6.944 9.857c0-1.842-1.492-3.338-3.349-3.338h-0.876v6.686h0.876c1.858 0 3.349-1.498 3.349-3.348v0z";break;case"sign":e[0]="0 0 20 20",e[1]="M10.954 10.307c0.378 0.302 0.569 1.202 0.564 1.193 0.697 0.221 1.136 0.682 1.136 0.682 1.070-0.596 1.094-0.326 1.558-0.682 0.383-0.263 0.366-0.344 0.567-1.048 0.187-0.572-0.476-0.518-1.021-1.558-0.95 0.358-1.463 0.196-1.784 0.167-0.145-0.020-0.12 0.562-1.021 1.247zM14.409 17.196c-0.133 0.182-0.196 0.218-0.363 0.454-0.28 0.361 0.076 0.906 0.253 0.82 0.206-0.076 0.341-0.488 0.567-0.623 0.115-0.061 0.422-0.513 0.709-0.82 0.211-0.238 0.363-0.344 0.564-0.594 0.341-0.422 0.412-0.744 0.709-1.193 0.184-0.236 0.312-0.307 0.481-0.594 0.886-1.679 0.628-2.432 1.475-3.629 0.26-0.353 0.552-0.442 0.964-0.653 0.383-2.793-0.888-4.356-0.879-4.361-1.067 0.623-1.644 0.879-2.751 0.82-0.417-0.005-0.636-0.182-1.048-0.145-0.385 0.015-0.582 0.159-0.964 0.29-0.589 0.182-0.91 0.344-1.529 0.535-0.393 0.11-0.643 0.115-1.050 0.255-0.348 0.147-0.182 0.029-0.427 0.312-0.317 0.348-0.238 0.623-0.535 1.222-0.371 0.785-0.326 0.891-0.115 0.987-0.14 0.402-0.174 0.672-0.14 1.107 0.039 0.331-0.101 0.562 0.255 0.825 0.483 0.361 1.499 1.205 1.757 1.217 0.39-0.012 1.521 0.029 2.096-0.368 0.13-0.081 0.167-0.162 0.056 0.145-0.022 0.037-1.433 1.136-1.585 1.131-1.794 0.056-1.193 0.157-1.303 0.115-0.091 0-0.955-1.055-1.477-0.682-0.196 0.12-0.287 0.236-0.363 0.452 0.066 0.137 0.383 0.358 0.675 0.54 0.422 0.27 0.461 0.552 0.881 0.653 0.513 0.115 1.060 0.039 1.387 0.081 0.125 0.034 1.256-0.297 1.961-0.675 0.65-0.336-0.898 0.648-1.276 1.131-1.141 0.358-0.82 0.373-1.362 0.483-0.503 0.115-0.479 0.086-0.822 0.196-0.356 0.086-0.648 0.572-0.312 0.825 0.201 0.167 0.827-0.066 1.445-0.086 0.275-0.005 1.391-0.518 1.644-0.653 0.633-0.339 1.099-0.81 1.472-1.077 0.518-0.361-0.584 0.991-1.050 1.558zM8.855 9.799c-0.378-0.312-0.569-1.212-0.564-1.217-0.697-0.206-1.136-0.667-1.136-0.653-1.070 0.582-1.099 0.312-1.558 0.653-0.388 0.277-0.366 0.363-0.567 1.045-0.187 0.594 0.471 0.535 1.021 1.561 0.95-0.344 1.463-0.182 1.784-0.142 0.145 0.010 0.12-0.572 1.021-1.247zM5.4 2.911c0.133-0.191 0.196-0.228 0.368-0.454 0.27-0.371-0.081-0.915-0.253-0.849-0.211 0.096-0.346 0.508-0.599 0.653-0.093 0.052-0.4 0.503-0.682 0.82-0.211 0.228-0.363 0.334-0.564 0.599-0.346 0.407-0.412 0.729-0.709 1.161-0.184 0.258-0.317 0.324-0.481 0.621-0.886 1.669-0.631 2.422-1.475 3.6-0.26 0.38-0.552 0.461-0.964 0.682-0.383 2.788 0.883 4.346 0.879 4.336 1.068-0.609 1.639-0.861 2.751-0.825 0.417 0.025 0.636 0.201 1.048 0.174 0.385-0.025 0.582-0.169 0.964-0.285 0.589-0.196 0.91-0.358 1.499-0.54 0.422-0.12 0.672-0.125 1.080-0.285 0.348-0.128 0.182-0.010 0.427-0.282 0.312-0.358 0.238-0.633 0.508-1.217 0.398-0.8 0.353-0.906 0.142-0.991 0.135-0.412 0.174-0.677 0.14-1.107-0.044-0.336 0.101-0.572-0.255-0.82-0.483-0.375-1.499-1.22-1.752-1.222-0.395 0.002-1.526-0.039-2.101 0.339-0.13 0.101-0.167 0.182-0.056-0.11 0.022-0.052 1.433-1.148 1.585-1.163 1.794-0.039 1.193-0.14 1.303-0.088 0.091-0.007 0.955 1.045 1.477 0.682 0.191-0.13 0.287-0.245 0.368-0.452-0.071-0.147-0.388-0.368-0.68-0.537-0.422-0.282-0.464-0.564-0.881-0.655-0.513-0.125-1.065-0.049-1.387-0.11-0.125-0.015-1.256 0.317-1.956 0.68-0.66 0.351 0.893-0.631 1.276-1.136 1.136-0.339 0.81-0.353 1.36-0.479 0.501-0.101 0.476-0.071 0.82-0.172 0.351-0.096 0.648-0.577 0.312-0.849-0.206-0.152-0.827 0.081-1.44 0.086-0.28 0.020-1.396 0.533-1.649 0.677-0.633 0.329-1.099 0.8-1.472 1.048-0.523 0.38 0.584-0.967 1.050-1.529z";break;case"mute":case"volume-mute":e[0]="0 0 20 20",e[1]="M7.839 1.536c0.501-0.501 0.911-0.331 0.911 0.378v16.172c0 0.709-0.41 0.879-0.911 0.378l-4.714-4.713h-3.125v-7.5h3.125l4.714-4.714zM18.75 12.093v1.657h-1.657l-2.093-2.093-2.093 2.093h-1.657v-1.657l2.093-2.093-2.093-2.093v-1.657h1.657l2.093 2.093 2.093-2.093h1.657v1.657l-2.093 2.093z";break;case"volume-soft":e[0]="0 0 20 20",e[1]="M10.723 14.473c-0.24 0-0.48-0.092-0.663-0.275-0.366-0.366-0.366-0.96 0-1.326 1.584-1.584 1.584-4.161 0-5.745-0.366-0.366-0.366-0.96 0-1.326s0.96-0.366 1.326 0c2.315 2.315 2.315 6.082 0 8.397-0.183 0.183-0.423 0.275-0.663 0.275zM7.839 1.536c0.501-0.501 0.911-0.331 0.911 0.378v16.172c0 0.709-0.41 0.879-0.911 0.378l-4.714-4.713h-3.125v-7.5h3.125l4.714-4.714z";break;case"volume-medium":e[0]="0 0 20 20",e[1]="M14.053 16.241c-0.24 0-0.48-0.092-0.663-0.275-0.366-0.366-0.366-0.96 0-1.326 2.559-2.559 2.559-6.722 0-9.281-0.366-0.366-0.366-0.96 0-1.326s0.96-0.366 1.326 0c1.594 1.594 2.471 3.712 2.471 5.966s-0.878 4.373-2.471 5.966c-0.183 0.183-0.423 0.275-0.663 0.275zM10.723 14.473c-0.24 0-0.48-0.092-0.663-0.275-0.366-0.366-0.366-0.96 0-1.326 1.584-1.584 1.584-4.161 0-5.745-0.366-0.366-0.366-0.96 0-1.326s0.96-0.366 1.326 0c2.315 2.315 2.315 6.082 0 8.397-0.183 0.183-0.423 0.275-0.663 0.275zM7.839 1.536c0.501-0.501 0.911-0.331 0.911 0.378v16.172c0 0.709-0.41 0.879-0.911 0.378l-4.714-4.713h-3.125v-7.5h3.125l4.714-4.714z";break;case"volume-loud":e[0]="0 0 21 20",e[1]="M17.384 18.009c-0.24 0-0.48-0.092-0.663-0.275-0.366-0.366-0.366-0.96 0-1.326 1.712-1.712 2.654-3.988 2.654-6.408s-0.943-4.696-2.654-6.408c-0.366-0.366-0.366-0.96 0-1.326s0.96-0.366 1.326 0c2.066 2.066 3.204 4.813 3.204 7.734s-1.138 5.668-3.204 7.734c-0.183 0.183-0.423 0.275-0.663 0.275zM14.053 16.241c-0.24 0-0.48-0.092-0.663-0.275-0.366-0.366-0.366-0.96 0-1.326 2.559-2.559 2.559-6.722 0-9.281-0.366-0.366-0.366-0.96 0-1.326s0.96-0.366 1.326 0c1.594 1.594 2.471 3.712 2.471 5.966s-0.878 4.373-2.471 5.966c-0.183 0.183-0.423 0.275-0.663 0.275zM10.723 14.473c-0.24 0-0.48-0.092-0.663-0.275-0.366-0.366-0.366-0.96 0-1.326 1.584-1.584 1.584-4.161 0-5.745-0.366-0.366-0.366-0.96 0-1.326s0.96-0.366 1.326 0c2.315 2.315 2.315 6.082 0 8.397-0.183 0.183-0.423 0.275-0.663 0.275zM7.839 1.536c0.501-0.501 0.911-0.331 0.911 0.378v16.172c0 0.709-0.41 0.879-0.911 0.378l-4.714-4.713h-3.125v-7.5h3.125l4.714-4.714z";break;case"chapters":e[0]="0 0 20 20",e[1]="M5 2.5v17.5l6.25-6.25 6.25 6.25v-17.5zM15 0h-12.5v17.5l1.25-1.25v-15h11.25z";break;case"transcript":e[0]="0 0 20 20",e[1]="M0 19.107v-17.857q0-0.446 0.313-0.759t0.759-0.313h8.929v6.071q0 0.446 0.313 0.759t0.759 0.313h6.071v11.786q0 0.446-0.313 0.759t-0.759 0.312h-15q-0.446 0-0.759-0.313t-0.313-0.759zM4.286 15.536q0 0.156 0.1 0.257t0.257 0.1h7.857q0.156 0 0.257-0.1t0.1-0.257v-0.714q0-0.156-0.1-0.257t-0.257-0.1h-7.857q-0.156 0-0.257 0.1t-0.1 0.257v0.714zM4.286 12.679q0 0.156 0.1 0.257t0.257 0.1h7.857q0.156 0 0.257-0.1t0.1-0.257v-0.714q0-0.156-0.1-0.257t-0.257-0.1h-7.857q-0.156 0-0.257 0.1t-0.1 0.257v0.714zM4.286 9.821q0 0.156 0.1 0.257t0.257 0.1h7.857q0.156 0 0.257-0.1t0.1-0.257v-0.714q0-0.156-0.1-0.257t-0.257-0.1h-7.857q-0.156 0-0.257 0.1t-0.1 0.257v0.714zM11.429 5.893v-5.268q0.246 0.156 0.402 0.313l4.554 4.554q0.156 0.156 0.313 0.402h-5.268z";break;case"preferences":e[0]="0 0 20 20",e[1]="M18.238 11.919c-1.049-1.817-0.418-4.147 1.409-5.205l-1.965-3.404c-0.562 0.329-1.214 0.518-1.911 0.518-2.1 0-3.803-1.714-3.803-3.828h-3.931c0.005 0.653-0.158 1.314-0.507 1.919-1.049 1.818-3.382 2.436-5.212 1.382l-1.965 3.404c0.566 0.322 1.056 0.793 1.404 1.396 1.048 1.815 0.42 4.139-1.401 5.2l1.965 3.404c0.56-0.326 1.209-0.513 1.902-0.513 2.094 0 3.792 1.703 3.803 3.808h3.931c-0.002-0.646 0.162-1.3 0.507-1.899 1.048-1.815 3.375-2.433 5.203-1.387l1.965-3.404c-0.562-0.322-1.049-0.791-1.395-1.391zM10 14.049c-2.236 0-4.050-1.813-4.050-4.049s1.813-4.049 4.050-4.049 4.049 1.813 4.049 4.049c-0 2.237-1.813 4.049-4.049 4.049z";break;case"close":e[0]="0 0 16 20",e[1]="M1.228 14.933q0-0.446 0.312-0.759l3.281-3.281-3.281-3.281q-0.313-0.313-0.313-0.759t0.313-0.759l1.518-1.518q0.313-0.313 0.759-0.313t0.759 0.313l3.281 3.281 3.281-3.281q0.313-0.313 0.759-0.313t0.759 0.313l1.518 1.518q0.313 0.313 0.313 0.759t-0.313 0.759l-3.281 3.281 3.281 3.281q0.313 0.313 0.313 0.759t-0.313 0.759l-1.518 1.518q-0.313 0.313-0.759 0.313t-0.759-0.313l-3.281-3.281-3.281 3.281q-0.313 0.313-0.759 0.313t-0.759-0.313l-1.518-1.518q-0.313-0.313-0.313-0.759z";break;case"fullscreen-expand":e[0]="0 0 20 20",e[1]="M0 18.036v-5q0-0.29 0.212-0.502t0.502-0.212 0.502 0.212l1.607 1.607 3.705-3.705q0.112-0.112 0.257-0.112t0.257 0.112l1.272 1.272q0.112 0.112 0.112 0.257t-0.112 0.257l-3.705 3.705 1.607 1.607q0.212 0.212 0.212 0.502t-0.212 0.502-0.502 0.212h-5q-0.29 0-0.502-0.212t-0.212-0.502zM8.717 8.393q0-0.145 0.112-0.257l3.705-3.705-1.607-1.607q-0.212-0.212-0.212-0.502t0.212-0.502 0.502-0.212h5q0.29 0 0.502 0.212t0.212 0.502v5q0 0.29-0.212 0.502t-0.502 0.212-0.502-0.212l-1.607-1.607-3.705 3.705q-0.112 0.112-0.257 0.112t-0.257-0.112l-1.272-1.272q-0.112-0.112-0.112-0.257z";break;case"fullscreen-collapse":e[0]="0 0 20 20",e[1]="M0.145 16.964q0-0.145 0.112-0.257l3.705-3.705-1.607-1.607q-0.212-0.212-0.212-0.502t0.212-0.502 0.502-0.212h5q0.29 0 0.502 0.212t0.212 0.502v5q0 0.29-0.212 0.502t-0.502 0.212-0.502-0.212l-1.607-1.607-3.705 3.705q-0.112 0.112-0.257 0.112t-0.257-0.112l-1.272-1.272q-0.112-0.112-0.112-0.257zM8.571 9.464v-5q0-0.29 0.212-0.502t0.502-0.212 0.502 0.212l1.607 1.607 3.705-3.705q0.112-0.112 0.257-0.112t0.257 0.112l1.272 1.272q0.112 0.112 0.112 0.257t-0.112 0.257l-3.705 3.705 1.607 1.607q0.212 0.212 0.212 0.502t-0.212 0.502-0.502 0.212h-5q-0.29 0-0.502-0.212t-0.212-0.502z";break;case"help":e[0]="0 0 11 20",e[1]="M0.577 6.317q-0.028-0.167 0.061-0.313 1.786-2.969 5.179-2.969 0.893 0 1.797 0.346t1.629 0.926 1.183 1.423 0.458 1.769q0 0.603-0.173 1.127t-0.391 0.854-0.614 0.664-0.642 0.485-0.681 0.396q-0.458 0.257-0.765 0.725t-0.307 0.748q0 0.19-0.134 0.363t-0.313 0.173h-2.679q-0.167 0-0.285-0.206t-0.117-0.419v-0.502q0-0.926 0.725-1.747t1.596-1.211q0.658-0.301 0.938-0.625t0.279-0.848q0-0.469-0.519-0.826t-1.2-0.357q-0.725 0-1.205 0.324-0.391 0.279-1.194 1.283-0.145 0.179-0.346 0.179-0.134 0-0.279-0.089l-1.83-1.395q-0.145-0.112-0.173-0.279zM3.786 16.875v-2.679q0-0.179 0.134-0.313t0.313-0.134h2.679q0.179 0 0.313 0.134t0.134 0.313v2.679q0 0.179-0.134 0.313t-0.313 0.134h-2.679q-0.179 0-0.313-0.134t-0.134-0.313z"}return e},AblePlayer.prototype.reinitialize=function(){var t,e;if(e=(t=new n.Deferred).promise(),this,window.console||(this.debug=!1),this.startedPlaying=!1,this.autoScrollTranscript=!0,this.$media=n(this.media).first(),this.media=this.$media[0],this.$media.is("audio"))this.mediaType="audio";else{if(!this.$media.is("video"))return this.provideFallback(),t.fail(),e;this.mediaType="video"}return this.$sources=this.$media.find("source"),this.player=this.getPlayer(),this.player||this.provideFallback(),this.setIconType(),t.resolve(),e},AblePlayer.prototype.setPlayerSize=function(t,e){this.$media.attr("id");"audio"===this.mediaType?this.playerWidth&&this.$ableWrapper.css("width",this.playerWidth+"px"):0",{class:"icon-play able-clipped"}),n("body").append(t),t),void 0!==(i=window.getComputedStyle(e.get(0),null).getPropertyValue("font-family"))&&-1!==i.indexOf("able")?this.iconType="font":this.iconType="image"):this.iconType="image",void 0!==t&&t.remove())},AblePlayer.prototype.setupInstance=function(){var t=new n.Deferred,e=t.promise();return this.$media.attr("id")?this.mediaId=this.$media.attr("id"):(this.mediaId="ableMediaId_"+this.ableIndex,this.$media.attr("id",this.mediaId)),t.resolve(),e},AblePlayer.prototype.setupInstancePlaylist=function(){var s=this;if(this.hasPlaylist=!1,n(".able-playlist").each(function(){if(n(this).data("player")===s.mediaId){s.hasPlaylist=!0,s.$playlist=n(this).find("li"),n(this).find("li[data-youtube-id]").each(function(){var t=n(this).attr("data-youtube-id"),e=s.getYouTubePosterUrl(t,"120"),i=n("",{src:e,alt:""});n(this).find("button").prepend(i)}),n(this).find("li[data-vimeo-id]").each(function(){var t=n(this).attr("data-vimeo-id"),e=s.getVimeoPosterUrl(t,"120"),i=n("",{src:e,alt:""});n(this).find("button").prepend(i)}),n(this).find("li span").attr("aria-hidden","true"),s.playlistIndex=0;var t=n(this).data("embedded");s.playlistEmbed=void 0!==t&&!1!==t}}),this.hasPlaylist&&this.loop&&this.media.removeAttribute("loop"),this.hasPlaylist&&this.playlistEmbed){var t=this.$playlist.parent();this.$playlistDom=t.clone(),t.remove()}this.hasPlaylist&&0===this.$sources.length&&(this.cuePlaylistItem(0),this.$sources=this.$media.find("source"))},AblePlayer.prototype.recreatePlayer=function(){var e,t,i,s,a;if(this.player)return t=(e=new n.Deferred).promise(),(i=this).playerDeleted=!1,this.recreatingPlayer=!0,this.playerCreated||(this.loadCurrentPreferences(),this.injectPlayerCode(),this.resizePlayer(this.media.videoWidth,this.media.videoHeight)),this.getSampleDescriptionText(),this.initSignLanguage(),this.initPlayer().then(function(){i.getTracks().then(function(){i.initDescription().then(function(){i.setupTracks().then(function(){i.hasClosedDesc&&(!i.$descDiv||i.$descDiv&&!n.contains(i.$ableDiv[0],i.$descDiv[0]))&&i.injectTextDescriptionArea(),i.initSpeech("init"),i.setupTranscript().then(function(){i.initStenoFrame().then(function(){i.stenoMode&&i.$stenoFrame&&(i.stenoFrameContents=i.$stenoFrame.contents()),i.getMediaTimes().then(function(t){for(i.duration=t.duration,i.elapsed=t.elapsed,i.setFullscreen(!1),void 0===i.volume&&(i.volume=i.defaultVolume),i.volume&&i.setVolume(i.volume),i.transcriptType&&(i.addTranscriptAreaEvents(),i.updateTranscript()),i.captions.length&&i.initDefaultCaption(),i.setMediaAttributes(),i.addControls(),i.addEventListeners(),s=i.getPreferencesGroups(),a=0;a",{class:"able-prefs-form "})).addClass(W),"captions"==t)a=this.tt.prefTitleCaptions;else if("descriptions"==t){a=this.tt.prefTitleDescriptions;var q=Y("

",{text:this.tt.prefIntroDescription1}),H=Y("

    "),j=Y("
  • ",{text:this.tt.prefDescFormatOption1}),K=Y("
  • ",{text:this.tt.prefDescFormatOption2});H.append(j,K),this.hasOpenDesc&&this.hasClosedDesc?(I=this.tt.prefIntroDescription2+" ",I+=""+this.tt.prefDescFormatOption1b+"",I+=" "+this.tt.and+" "+this.tt.prefDescFormatOption2b+"."):this.hasOpenDesc?(I=this.tt.prefIntroDescription2,I+=" "+this.tt.prefDescFormatOption1b+"."):this.hasClosedDesc?(I=this.tt.prefIntroDescription2,I+=" "+this.tt.prefDescFormatOption2b+"."):I=this.tt.prefIntroDescriptionNone,r=Y("

    ",{html:I}),o=this.tt.prefIntroDescription3,(this.hasOpenDesc||this.hasClosedDesc)&&(o+=" "+this.tt.prefIntroDescription4),l=Y("

    ",{text:o}),s.append(q,H,r,l)}else"keyboard"==t?(a=this.tt.prefTitleKeyboard,n=this.tt.prefIntroKeyboard1,n+=" "+this.tt.prefIntroKeyboard2,n+=" "+this.tt.prefIntroKeyboard3,q=Y("

    ",{text:n}),s.append(q)):"transcript"==t&&(a=this.tt.prefTitleTranscript);for(c=Y("

    ").attr("role","group"),d="able-prefs-"+t,g=(u=this.mediaId+"-prefs-"+t)+"-legend",c.addClass(d).attr("id",u),"keyboard"===t?((f=Y("

    "+this.tt.prefHeadingKeyboard1+"

    ")).attr("id",g),c.attr("aria-labelledby",g),c.append(f)):"descriptions"===t&&((f=Y("

    "+this.tt.prefHeadingTextDescription+"

    ")).attr("id",g),c.attr("aria-labelledby",g),c.append(f)),h=0;h").addClass(y),"captions"===t){for(P=Y('"),k=Y("",{type:"checkbox",name:m,id:b,value:"true"}),1===this[m]&&k.prop("checked",!0),v.append(k,P);else if(this.synth){if(v.addClass("able-prefs-select"),k=Y("",{type:"checkbox",name:m,id:b,value:"true"}),1===this[m]&&k.prop("checked",!0),"keyboard"===t&&k.on("change",function(){"prefAltKey"===(x=Y(this).attr("name"))?(A=".able-modkey-alt",S=e.tt.prefAltKey+" + "):"prefCtrlKey"===x?(A=".able-modkey-ctrl",S=e.tt.prefCtrlKey+" + "):"prefShiftKey"===x&&(A=".able-modkey-shift",S=e.tt.prefShiftKey+" + "),Y(this).is(":checked")?Y(A).text(S):Y(A).text("")}),v.append(k,P);("prefDescVoice"!==m||this.descVoices.length)&&c.append(v)}if(s.append(c),"captions"===t)this.usingYouTubeCaptions||(this.$sampleCapsDiv=Y("
    ",{class:"able-captions-sample"}).text(this.tt.sampleCaptionText),s.append(this.$sampleCapsDiv),this.stylizeCaptions(this.$sampleCapsDiv));else if("descriptions"===t)this.synth&&(this.$sampleDescDiv=Y("
    ",{class:"able-desc-sample"}).text(this.tt.sampleDescriptionText),s.append(this.$sampleDescDiv),this.currentSampleText=this.tt.sampleDescriptionText);else if("keyboard"===t){for(B=Y("

    ",{text:this.tt.prefHeadingKeyboard2}),M=Y("
      "),F=[],V=[],h=0;h "+this.tt.or+' '+this.tt.spacebar)):"restart"===this.controls[h]?(F.push(this.tt.restart),V.push("s")):"previous"===this.controls[h]?(F.push(this.tt.prevTrack),V.push("b")):"next"===this.controls[h]?(F.push(this.tt.nextTrack),V.push("n")):"rewind"===this.controls[h]?(F.push(this.tt.rewind),V.push("r")):"forward"===this.controls[h]?(F.push(this.tt.forward),V.push("f")):"volume"===this.controls[h]?(F.push(this.tt.volume),V.push("v "+this.tt.or+' 1-9'),F.push(this.tt.mute+"/"+this.tt.unmute),V.push("m")):"captions"===this.controls[h]?(1',1===this.prefAltKey&&(L+=this.tt.prefAltKey+" + "),L+="",L+='',1===this.prefCtrlKey&&(L+=this.tt.prefCtrlKey+" + "),L+="",L+='',1===this.prefShiftKey&&(L+=this.tt.prefShiftKey+" + "),L+="",L+=''+V[h]+"",L+=" = "+F[h],z=Y("
    • ",{html:L}),M.append(z);L=''+this.tt.escapeKey+"",L+=" = "+this.tt.escapeKeyFunction,z=Y("
    • ",{html:L}),M.append(z),s.append(B,M)}Y("body").append(s),O=new AccessibleDialog(s,this.$prefsButton,"dialog",!0,a,q,e.tt.closeButtonLabel,"32em"),s.append("
      "),R=Y('"),E=Y('"),R.click(function(){O.hide(),e.savePrefsFromForm()}),E.click(function(){O.hide(),e.resetPrefsForm()}),s.append(R),s.append(E),"captions"!==t&&"transcript"!==t||c.attr("aria-labelledby",O.titleH1.attr("id")),"captions"===t?this.captionPrefsDialog=O:"descriptions"===t?this.descPrefsDialog=O:"keyboard"===t?this.keyboardPrefsDialog=O:"transcript"===t&&(this.transcriptPrefsDialog=O),Y("div.able-prefs-form button.modalCloseButton").click(function(){e.resetPrefsForm()}),Y("div.able-prefs-form").keydown(function(t){27===t.which&&e.resetPrefsForm()})},AblePlayer.prototype.getPrefDescVoice=function(){var t,e,i;if(t=this.selectedDescriptions?this.selectedDescriptions.language:this.captionLang?this.captionLang:this.lang,(e=this.getCookie()).voices)for(i=0;i",{value:e,"data-lang":this.descVoices[t].lang.substring(0,2).toLowerCase(),text:i}),this.prefDescVoice==e&&s.prop("selected",!0),this.$voiceSelectField.append(s)},AblePlayer.prototype.makePrefsValueReadable=function(t,e){if("prefDescPitch"===t){if(0===e)return this.tt.prefDescPitch1;if(.5===e)return this.tt.prefDescPitch2;if(1===e)return this.tt.prefDescPitch3;if(1.5===e)return this.tt.prefDescPitch4;if(2===e)return this.tt.prefDescPitch5}else if("prefDescRate"===t){if(.7===e)return 1;if(.8===e)return 2;if(.9===e)return 3;if(1===e)return 4;if(1.1===e)return 5;if(1.2===e)return 6;if(1.5===e)return 7;if(2===e)return 8;if(2.5===e)return 9;if(3===e)return 10}else if("prefDescVolume"===t)return 10*e;return e},AblePlayer.prototype.resetPrefsForm=function(){var t,e,i,s;for(t=this.getCookie(),e=this.getAvailablePreferences(),i=0;i"):"&lrm"===s?i.push("‎"):"&rlm"===s?i.push("‏"):" "===s?i.push(" "):(i.push(s),i.push(";")),e="data";else{if("<"===n||""===n)return i.push(s),a.type="string",a.value=i.join(""),a;if("\t"===n||"\n"===n||"\f"===n||" "===n)return i.push(s),a.type="string",a.value=i.join(""),a;i.push(s),e="data"}else if("tag"===e)if("\t"===n||"\n"===n||"\f"===n||" "===n)e="startTagAnnotation";else if("."===n)e="startTagClass";else if("/"===n)e="endTag";else if(n.match("[0-9]"))e="timestampTag",i.push(n);else{if(">"===n){c(t,1);break}if(""===n)return a.tagName="",a.type="startTag",a;i.push(n),e="startTag"}else if("startTag"===e)if("\t"===n||"\f"===n||" "===n)e="startTagAnnotation";else if("\n"===n)s=n,e="startTagAnnotation";else if("."===n)e="startTagClass";else{if(">"===n)return c(t,1),a.tagName=i.join(""),a.type="startTag",a;if(""===n)return a.tagName=i.join(""),a.type="startTag",a;i.push(n)}else if("startTagClass"===e)if("\t"===n||"\f"===n||" "===n)a.classes.push(s),s="",e="startTagAnnotation";else if("\n"===n)a.classes.push(s),s=n,e="startTagAnnotation";else if("."===n)a.classes.push(s),s="";else{if(">"===n)return c(t,1),a.classes.push(s),a.type="startTag",a.tagName=i.join(""),a;if(""===n)return a.classes.push(s),a.type="startTag",a.tagName=i.join(""),a;s+="c"}else if("startTagAnnotation"===e){if(">"===n)return c(t,1),s=l.trim(s).replace(/ +/," "),a.type="startTag",a.tagName=i.join(""),a.annotation=s,a;if(""===n)return s=l.trim(s).replace(/ +/," "),a.type="startTag",a.tagName=i.join(""),a.annotation=s,a;s+=n}else if("endTag"===e){if(">"===n)return c(t,1),a.type="endTag",a.tagName=i.join(""),a;if(""===n)return a.type="endTag",a.tagName=i.join(""),a;i.push(n)}else{if("timestampTag"!==e)throw"Unknown tokenState "+e;if(">"===n)return c(t,1),a.type="timestampTag",a.name=i.join(""),a;if(""===n)return a.type="timestampTag",a.name=i.join(""),a;i.push(n)}c(t,1)}}function b(t){if(-1===d(t).indexOf("--\x3e"))for(;;){var e=u(t);if(0===l.trim(e).length)return;if(-1!==e.indexOf("--\x3e"))return void(t.error="Invalid syntax: --\x3e in comment.");d(t)}else t.error="Invalid syntax: --\x3e in NOTE line."}function P(t){"\ufeff"===t.text[0]&&c(t,1)}function k(t){"WEBVTT"===t.text.substring(0,6)?c(t,6):t.error="Invalid signature."}function T(t){t.text.length<3||"--\x3e"!==t.text.substring(0,3)?t.error="Missing --\x3e":c(t,3)}function C(t){for(;"\t"===t.text[0]||" "===t.text[0];)c(t,1)}function w(t){for(var e=0;"\t"===t.text[0]||" "===t.text[0];)c(t,1),e+=1;0===e&&(t.error="Missing space.")}function $(t){var e=t.text.indexOf("\n");-1===e?t.error="Missing EOL.":c(t,e+1)}function D(t){for(;0

    ').parent(),this.$ableDiv=this.$mediaContainer.wrap('
    ').parent(),this.$ableWrapper=this.$ableDiv.wrap('
    ').parent(),this.$ableWrapper.addClass("able-skin-"+this.skin),this.$ableWrapper.css({width:this.playerWidth+"px"}),"video"===this.mediaType&&("image"==this.iconType||"youtube"===this.player&&!this.hasPoster||this.injectBigPlayButton()),t=D("
    "),"video"===this.mediaType?t.addClass("able-vidcap-container"):"audio"===this.mediaType&&(t.addClass("able-audcap-container"),t.addClass("captions-off")),this.injectPlayerControlArea(),this.$captionsContainer=this.$mediaContainer.wrap(t).parent(),this.injectAlert(),this.injectPlaylist(),this.injectOffscreenHeading()},AblePlayer.prototype.injectOffscreenHeading=function(){var t;"0"==this.playerHeadingLevel||(void 0===this.playerHeadingLevel&&(this.playerHeadingLevel=this.getNextHeadingLevel(this.$ableDiv)),t="h"+this.playerHeadingLevel.toString(),this.$headingDiv=D("<"+t+">"),this.$ableDiv.prepend(this.$headingDiv),this.$headingDiv.addClass("able-offscreen"),this.$headingDiv.text(this.tt.playerHeading))},AblePlayer.prototype.injectBigPlayButton=function(){var e,t,i,s;(e=this).$bigPlayButton=D("
    '),y=0)}),a}}(jQuery),function(f){AblePlayer.prototype.showSearchResults=function(){var i=this;if(this.searchDiv&&this.searchString&&f("#"+this.SearchDiv)){var t="

    "+this.tt.resultsSummary1+" ";t+=''+this.searchString+"",t+="

    ";var e=this.searchFor(this.searchString,this.searchIgnoreCaps);if(0",{class:"able-search-results-summary"}),a=this.tt.resultsSummary2;a+=" "+e.length+" ",a+=this.tt.resultsSummary3+" ",a+=this.tt.resultsSummary4,s.html(a);for(var n=f("
      "),r=0;r",{}),h=this.secondsToTime(e[r].start),p=this.tt.searchButtonLabel+" "+h.title,c=f(""),k=$('"),P.on("click",function(){T=$("#"+n).val(),C=$("#"+r).val(),T===o&&C===l||(i.resizeObject(t,T,C),i.updateCookie(t)),w.hide(),s.hide(),a.focus()}),k.on("click",function(){w.hide(),s.hide(),a.focus()}),d.append(f,u),g.append(v,m),c.append(d,g),p.append(c,"
      ",P,k),$("body").append(p),w=new AccessibleDialog(p,a,"dialog",!0,this.tt.windowResizeHeading,c,this.tt.closeButtonLabel,"20em"),"transcript"===t?this.transcriptResizeDialog=w:"sign"===t&&(this.signResizeDialog=w)},AblePlayer.prototype.handleWindowButtonClick=function(t,e){var i,s,a,n;if((i=this).focusNotClick)return!1;if("transcript"===t?(s=this.$transcriptPopup,a=this.$transcriptPopupButton,this.$transcriptToolbar):"sign"===t&&(s=this.$signPopup,a=this.$signPopupButton,this.$signToolbar),"keydown"===e.type)if(32===e.which||13===e.which)this.windowMenuClickRegistered=!0;else{if(27!==e.which)return!1;s.is(":visible")?s.hide("fast",function(){i.windowMenuClickRegistered=!1,s.find("li").removeClass("able-focus").attr("tabindex","-1"),a.focus()}):"sign"===t?this.handleSignToggle():"transcript"===t&&this.handleTranscriptToggle()}else this.windowMenuClickRegistered=!0;s.is(":visible")?(s.hide(200,"",function(){i.windowMenuClickRegistered=!1}),s.find("li").removeClass("able-focus"),a.attr("aria-expanded","false").focus()):(this.updateZIndex(t),n=a.position().top+a.outerHeight(),s.css("top",n),s.show(200,"",function(){a.attr("aria-expanded","true"),$(this).find("li").first().focus().addClass("able-focus"),i.windowMenuClickRegistered=!1}))},AblePlayer.prototype.handleMenuChoice=function(t,e,i){var s,a,n,r,o;if(s=this,"transcript"===t?(a=this.$transcriptArea,n=this.$transcriptPopup,r=this.$transcriptPopupButton,o=this.transcriptResizeDialog):"sign"===t&&(a=this.$signWindow,n=this.$signPopup,r=this.$signPopupButton,o=this.signResizeDialog),this.$activeWindow=a,"keydown"===i.type)return 27===i.which?n.hide("fast",function(){s.windowMenuClickRegistered=!1,n.find("li").removeClass("able-focus").attr("tabindex","-1"),r.attr("aria-expanded","false"),r.focus()}):"close"!==e&&(this.$activeWindow=a),!1;if(n.hide("fast",function(){s.windowMenuClickRegistered=!1,n.find("li").removeClass("able-focus").attr("tabindex","-1"),r.attr("aria-expanded","false")}),"close"!==e&&r.focus(),"move"===e)this.$activeWindow.attr("role","application"),this.showedAlert(t)||(this.showAlert(this.tt.windowMoveAlert,t),"transcript"===t?this.showedTranscriptAlert=!0:"sign"===t&&(this.showedSignAlert=!0)),"keydown"===i.type?this.dragDevice="keyboard":this.dragDevice="mouse",this.startDrag(t,a),n.hide().parent().focus();else if("resize"==e){var l=o.getInputs();l&&(l[0].value=a.width(),l[1].value=a.height()),o.show()}else"close"==e&&("transcript"===t?(this.closingTranscript=!0,this.handleTranscriptToggle()):"sign"===t&&(this.closingSign=!0,this.handleSignToggle()))},AblePlayer.prototype.startDrag=function(e,t){var i,s,a,n,r;return(i=this).$activeWindow||(this.$activeWindow=t),this.dragging=!0,"transcript"===e?s=this.$transcriptPopup:"sign"===e&&(s=this.$signPopup),this.showedAlert(e)||(this.showAlert(this.tt.windowMoveAlert,e),"transcript"===e?this.showedTranscriptAlert=!0:"sign"===e&&(this.showedSignAlert=!0)),s.is(":visible")&&s.hide(),this.updateZIndex(e),a=this.$activeWindow.position(),this.dragStartX=a.left,this.dragStartY=a.top,void 0===this.startMouseX?(this.dragDevice="keyboard",this.dragKeyX=this.dragStartX,this.dragKeyY=this.dragStartY,this.startingDrag=!0):(this.dragDevice="mouse",this.dragOffsetX=this.startMouseX-this.dragStartX,this.dragOffsetY=this.startMouseY-this.dragStartY),this.$activeWindow.addClass("able-drag").css({position:"absolute",top:this.dragStartY+"px",left:this.dragStartX+"px"}).focus(),"mouse"===this.dragDevice?$(document).on("mousemove touchmove",function(t){i.dragging&&(n=t.pageX-i.dragOffsetX,r=t.pageY-i.dragOffsetY,i.resetDraggedObject(n,r))}):"keyboard"===this.dragDevice&&this.$activeWindow.on("keydown",function(t){i.dragging&&i.dragKeys(e,t)}),!1},AblePlayer.prototype.dragKeys=function(t,e){if(this.startingDrag)return this.startingDrag=!1;switch(10,e.which){case 37:case 63234:this.dragKeyX-=10;break;case 38:case 63232:this.dragKeyY-=10;break;case 39:case 63235:this.dragKeyX+=10;break;case 40:case 63233:this.dragKeyY+=10;break;case 13:case 27:return this.endDrag(t),!1;default:return!1}return this.resetDraggedObject(this.dragKeyX,this.dragKeyY),e.preventDefault&&e.preventDefault(),!1},AblePlayer.prototype.resetDraggedObject=function(t,e){this.$activeWindow.css({left:t+"px",top:e+"px"})},AblePlayer.prototype.resizeObject=function(t,e,i){var s;this.$activeWindow.css({width:e+"px",height:i+"px"}),"transcript"===t&&(s=i-50,this.$transcriptDiv.css("height",s+"px"))},AblePlayer.prototype.endDrag=function(t){var e,i;e=this,"transcript"===t?(this.$transcriptPopup,i=this.$transcriptPopupButton):"sign"===t&&(this.$signPopup,i=this.$signPopupButton),$(document).off("mousemove mouseup touchmove touchup"),this.$activeWindow.off("keydown").removeClass("able-drag"),this.$activeWindow.attr("role","dialog"),this.$activeWindow=null,"keyboard"===this.dragDevice&&i.focus(),this.dragging=!1,this.updateCookie(t),this.startMouseX=void 0,this.startMouseY=void 0,this.windowMenuClickRegistered=!1,this.finishingDrag=!0,setTimeout(function(){e.finishingDrag=!1},100)},AblePlayer.prototype.isCloseToCorner=function(t,e,i){var s,a,n,r,o,l;return 10,a=(s=t.offset()).top,n=s.left,r=t.width(),o=a+t.height(),l=n+r,Math.abs(o-i)<=10&&Math.abs(l-e)<=10},AblePlayer.prototype.startResize=function(e,t){var i,s,a,n;return(i=this).$activeWindow=t,this.resizing=!0,"transcript"===e?s=this.$transcriptPopup:"sign"===e&&(s=this.$signPopup),s.is(":visible")&&s.hide().parent().focus(),this.$activeWindow.position(),this.dragKeyX=this.dragStartX,this.dragKeyY=this.dragStartY,this.dragStartWidth=this.$activeWindow.width(),this.dragStartHeight=this.$activeWindow.height(),$(document).on("mousemove touchmove",function(t){i.resizing&&(a=i.dragStartWidth+(t.pageX-i.startMouseX),n=i.dragStartHeight+(t.pageY-i.startMouseY),i.resizeObject(e,a,n))}),!1},AblePlayer.prototype.endResize=function(t){var e;"transcript"===t?(this.$transcriptPopup,e=this.$transcriptPopupButton):"sign"===t&&(this.$signPopup,e=this.$signPopupButton),$(document).off("mousemove mouseup touchmove touchup"),this.$activeWindow.off("keydown"),e.show().focus(),this.resizing=!1,this.$activeWindow.removeClass("able-resize"),this.updateCookie(t),this.windowMenuClickRegistered=!1,this.finishingDrag=!0,setTimeout(function(){this.finishingDrag=!1},100)}}(jQuery),function(n){AblePlayer.prototype.initSignLanguage=function(){"html5"===this.player?(this.signFile=this.$sources.first().attr("data-sign-src"),this.signFile?this.isIOS()?(this.hasSignLanguage=!1,this.debug):(this.debug,this.hasSignLanguage=!0,this.injectSignPlayerCode()):this.hasSignLanguage=!1):this.hasSignLanguage=!1},AblePlayer.prototype.injectSignPlayerCode=function(){var t,e,i,s,a;for(this.getDefaultWidth("sign"),t=this.mediaId+"-sign",this.$signVideo=n("