assets/ableplayer/scripts/description.js in wai-website-theme-1.3.1 vs assets/ableplayer/scripts/description.js in wai-website-theme-1.4

- old
+ new

@@ -1,283 +1,360 @@ (function ($) { - AblePlayer.prototype.initDescription = function() { + AblePlayer.prototype.initDescription = function() { - // set default mode for delivering description (open vs closed) - // based on availability and user preference + // 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 - // In the latter two scendarios, this.refreshingDesc == true via control.js > handleDescriptionToggle() + // called when player is being built, or when a user + // toggles the Description button or changes a description-related preference + // In the latter two scendarios, this.refreshingDesc == true via control.js > handleDescriptionToggle() - // The following variables are applicable to delivery of description: - // prefDesc == 1 if user wants description (i.e., Description button is on); else 0 - // prefDescFormat == either 'video' or 'text' - // prefDescPause == 1 to pause video when description starts; else 0 - // prefVisibleDesc == 1 to visibly show text-based description area; else 0 - // hasOpenDesc == true if a described version of video is available via data-desc-src attribute - // hasClosedDesc == true if a description text track is available - // this.useDescFormat == either 'video' or 'text'; the format ultimately delivered - // descOn == true if description of either type is on - if (!this.refreshingDesc) { - // this is the initial build - // first, 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? - if (this.youTubeDescId) { - this.hasOpenDesc = true; - } - else { // there are no open-described versions from any source - this.hasOpenDesc = false; - } - } - } - // update this.useDescFormat based on media availability & user preferences - if (this.prefDesc) { - if (this.hasOpenDesc && this.hasClosedDesc) { - // both formats are available. Use whichever one user prefers - this.useDescFormat = this.prefDescFormat; - this.descOn = true; - } - else if (this.hasOpenDesc) { - this.useDescFormat = 'video'; - this.descOn = true; - } - else if (this.hasClosedDesc) { - this.useDescFormat = 'text'; - this.descOn = true; - } - } - else { // description button is off - if (this.refreshingDesc) { // user just now toggled it off - this.prevDescFormat = this.useDescFormat; - this.useDescFormat = false; - this.descOn = false; - } - else { // desc has always been off - this.useDescFormat = false; - } - } + // The following variables are applicable to delivery of description: + // prefDesc == 1 if user wants description (i.e., Description button is on); else 0 + // prefDescFormat == either 'video' or 'text' + // prefDescPause == 1 to pause video when description starts; else 0 + // prefVisibleDesc == 1 to visibly show text-based description area; else 0 + // hasOpenDesc == true if a described version of video is available via data-desc-src attribute + // hasClosedDesc == true if a description text track is available + // this.useDescFormat == either 'video' or 'text'; the format ultimately delivered + // descOn == true if description of either type is on - if (this.descOn) { + var thisObj = this; - if (this.useDescFormat === 'video') { + if (!this.refreshingDesc) { + // this is the initial build + // first, 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 (this.youTubeDescId || this.vimeoDescId) { + this.hasOpenDesc = true; + } + else { // there are no open-described versions from any source + this.hasOpenDesc = false; + } + } + } + // update this.useDescFormat based on media availability & user preferences + if (this.prefDesc) { + if (this.hasOpenDesc && this.hasClosedDesc) { + // both formats are available. Use whichever one user prefers + this.useDescFormat = this.prefDescFormat; + this.descOn = true; + } + else if (this.hasOpenDesc) { + this.useDescFormat = 'video'; + this.descOn = true; + } + else if (this.hasClosedDesc) { + this.useDescFormat = 'text'; + this.descOn = true; + } + } + else { // description button is off + if (this.refreshingDesc) { // user just now toggled it off + this.prevDescFormat = this.useDescFormat; + this.useDescFormat = false; + this.descOn = false; + } + else { // desc has always been off + this.useDescFormat = false; + } + } - if (!this.usingAudioDescription()) { - // switched from non-described to described version - this.swapDescription(); - } - // hide description div - this.$descDiv.hide(); - this.$descDiv.removeClass('able-clipped'); - } - else if (this.useDescFormat === 'text') { - this.$descDiv.show(); - if (this.prefVisibleDesc) { // make it visible to everyone - this.$descDiv.removeClass('able-clipped'); - } - else { // keep it visible to screen readers, but hide from everyone else - this.$descDiv.addClass('able-clipped'); - } - if (!this.swappingSrc) { - this.showDescription(this.getElapsed()); - } - } - } - else { // description is off. + if (this.useDescFormat === 'text') { + // check whether browser supports the Web Speech API + if (window.speechSynthesis) { + // It does! + this.synth = window.speechSynthesis; + this.descVoices = this.synth.getVoices(); + // select the first voice that matches the track language + // available languages are identified with local suffixes (e.g., en-US) + // in case no matching voices are found, use the first voice in the voices array + this.descVoiceIndex = 0; + for (var i=0; i<this.descVoices.length; i++) { + if (this.captionLang.length === 2) { + // match only the first 2 characters of the lang code + if (this.descVoices[i].lang.substr(0,2).toLowerCase() === this.captionLang.toLowerCase()) { + this.descVoiceIndex = i; + break; + } + } + else { + // match the entire lang code + if (this.descVoices[i].lang.toLowerCase() === this.captionLang.toLowerCase()) { + this.descVoiceIndex = i; + break; + } + } + } + } + } + if (this.descOn) { - if (this.prevDescFormat === 'video') { // user was previously using description via video - if (this.usingAudioDescription()) { - this.swapDescription(); - } - } - else if (this.prevDescFormat === 'text') { // user was previously using text description - // hide description div from everyone, including screen reader users - this.$descDiv.hide(); - this.$descDiv.removeClass('able-clipped'); - } - } - this.refreshingDesc = false; - }; + if (this.useDescFormat === 'video') { + if (!this.usingAudioDescription()) { + // switched from non-described to described version + this.swapDescription(); + } + // hide description div + this.$descDiv.hide(); + this.$descDiv.removeClass('able-clipped'); + } + else if (this.useDescFormat === 'text') { + this.$descDiv.show(); + if (this.prefVisibleDesc) { // make it visible to everyone + this.$descDiv.removeClass('able-clipped'); + } + else { // keep it visible to screen readers, but hide from everyone else + this.$descDiv.addClass('able-clipped'); + } + if (!this.swappingSrc) { + this.showDescription(this.elapsed); + } + } + } + else { // description is off. - // Returns true if currently using audio description, false otherwise. - AblePlayer.prototype.usingAudioDescription = function () { + if (this.prevDescFormat === 'video') { // user was previously using description via video + if (this.usingAudioDescription()) { + this.swapDescription(); + } + } + else if (this.prevDescFormat === 'text') { // user was previously using text description + // hide description div from everyone, including screen reader users + this.$descDiv.hide(); + this.$descDiv.removeClass('able-clipped'); + } + } + this.refreshingDesc = false; + }; - if (this.player === 'youtube') { - return (this.activeYouTubeId === this.youTubeDescId); - } - else { - return (this.$sources.first().attr('data-desc-src') === this.$sources.first().attr('src')); - } - }; + // Returns true if currently using audio description, false otherwise. + AblePlayer.prototype.usingAudioDescription = function () { - 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) - // 2. User is toggling description - var thisObj, i, origSrc, descSrc, srcType, jwSourceIndex, newSource; + 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')); + } + }; - thisObj = this; + AblePlayer.prototype.swapDescription = function() { - // get current time, and start new video at the same time - // NOTE: There is some risk in resuming playback at the same start time - // since the described version might include extended audio description (with pauses) - // and might therefore be longer than the non-described version - // The benefits though would seem to outweigh this risk - this.swapTime = this.getElapsed(); // video will scrub to this time after loaded (see event.js) + // 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) + // 2. User is toggling description + var thisObj, i, origSrc, descSrc, srcType, newSource; - 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); - } + thisObj = this; - if (this.player === 'html5') { + // get current time, and start new video at the same time + // NOTE: There is some risk in resuming playback at the same start time + // since the described version might include extended audio description (with pauses) + // and might therefore be longer than the non-described version + // The benefits though would seem to outweigh this risk - if (this.usingAudioDescription()) { - // the described version is currently playing. Swap to non-described - for (i=0; i < this.$sources.length; i++) { - // for all <source> 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); - } - if (srcType === 'video/mp4') { - jwSourceIndex = i; - } - } - // No need to check for this.initializing - // This function is only called during initialization - // if swapping from non-described to described - this.swappingSrc = true; - } - else { - // the non-described version is currently playing. Swap to described. - for (i=0; i < this.$sources.length; i++) { - // for all <source> 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 (srcType === 'video/mp4') { - jwSourceIndex = i; - } - } - this.swappingSrc = true; - } + this.swapTime = this.elapsed; // video will scrub to this time after loaded (see event.js) + 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') { - // now reload the source file. - if (this.player === 'html5') { - this.media.load(); - } - else if (this.player === 'youtube') { - // TODO: Load new youTubeId - } - else if (this.player === 'jw' && this.jwPlayer) { - newSource = this.$sources[jwSourceIndex].getAttribute('src'); - this.jwPlayer.load({file: newSource}); - } - } - else if (this.player === 'youtube') { + if (this.usingAudioDescription()) { + // the described version is currently playing. Swap to non-described + for (i=0; i < this.$sources.length; i++) { + // for all <source> 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); + } + } + // No need to check for this.initializing + // This function is only called during initialization + // if swapping from non-described to described + this.swappingSrc = true; + } + else { + // the non-described version is currently playing. Swap to described. + for (i=0; i < this.$sources.length; i++) { + // for all <source> 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); + } + } + this.swappingSrc = true; + } - if (this.usingAudioDescription()) { - // the described version is currently playing. Swap to non-described - this.activeYouTubeId = this.youTubeId; - this.showAlert(this.tt.alertNonDescribedVersion); - } - else { - // the non-described version is currently playing. Swap to described. - this.activeYouTubeId = this.youTubeDescId; - this.showAlert(this.tt.alertDescribedVersion); - } - if (typeof this.youTubePlayer !== 'undefined') { + // now reload the source file. + if (this.player === 'html5') { + this.media.load(); + } + } + else if (this.player === 'youtube') { - // retrieve/setup captions for the new video from YouTube - this.setupAltCaptions().then(function() { + if (this.usingAudioDescription()) { + // the described version is currently playing. Swap to non-described + this.activeYouTubeId = this.youTubeId; + this.showAlert(this.tt.alertNonDescribedVersion); + } + else { + // the non-described version is currently playing. Swap to described. + this.activeYouTubeId = this.youTubeDescId; + this.showAlert(this.tt.alertDescribedVersion); + } + if (typeof this.youTubePlayer !== 'undefined') { - 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); - } - }); - } - } - }; + // retrieve/setup captions for the new video from YouTube + this.setupAltCaptions().then(function() { - AblePlayer.prototype.showDescription = function(now) { + 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); + } + }); + } + } + else if (this.player === 'vimeo') { + if (this.usingAudioDescription()) { + // 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); + } + // load the new video source + this.vimeoPlayer.loadVideo(this.activeVimeoId).then(function() { - // there's a lot of redundancy between this function and showCaptions - // Trying to combine them ended up in a mess though. Keeping as is for now. + 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(); + } + }); + } + }; - if (this.swappingSrc) { - return; - } + AblePlayer.prototype.showDescription = function(now) { - var d, thisDescription; - var flattenComponentForDescription = function (component) { - var result = []; - if (component.type === 'string') { - result.push(component.value); - } - else { - for (var ii = 0; ii < component.children.length; ii++) { - result.push(flattenComponentForDescription(component.children[ii])); - } - } - return result.join(''); - }; + // there's a lot of redundancy between this function and showCaptions + // Trying to combine them ended up in a mess though. Keeping as is for now. - var cues; - 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'); - // load the new description into the container div - this.$descDiv.html(flattenComponentForDescription(cues[thisDescription].components)); - if (this.prefDescPause) { - this.pauseMedia(); - } - this.currentDescription = thisDescription; - } - } - else { - this.$descDiv.html(''); - this.currentDescription = -1; - // restore aria-live to $status - this.$status.attr('aria-live','polite'); - } - }; + if (this.swappingSrc) { + 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 (typeof this.synth !== 'undefined' && typeof this.descVoiceIndex !== 'undefined') { + // browser supports speech synthesis and a voice has been selected in initDescription() + // use the web speech API + msg = new SpeechSynthesisUtterance(); + msg.voice = this.descVoices[this.descVoiceIndex]; // Note: some voices don't support altering params + msg.voiceURI = 'native'; + msg.volume = 1; // 0 to 1 + msg.rate = 1.5; // 0.1 to 10 (1 is normal human speech; 2 is fast but easily decipherable; anything above 2 is blazing fast) + msg.pitch = 1; //0 to 2 + msg.text = descText; + msg.lang = this.captionLang; + msg.onend = function(e) { + // NOTE: e.elapsedTime might be useful + if (thisObj.pausedForDescription) { + thisObj.playMedia(); + } + }; + this.synth.speak(msg); + if (this.prefVisibleDesc) { + // 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.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'); + } + }; })(jQuery);