@@ -1142,7 +1142,6 @@ var AblePlayerInstances = [];
11421142 // only call these functions once
11431143 this.loadCurrentPreferences();
11441144 this.injectPlayerCode();
1145- this.getBrowserVoices();
11461145 }
11471146
11481147 // call all remaining functions each time a new media instance is loaded
@@ -1153,6 +1152,8 @@ var AblePlayerInstances = [];
11531152
11541153 thisObj.setupTracks().then(function() {
11551154
1155+ thisObj.getBrowserVoices();
1156+
11561157 thisObj.setupAltCaptions().then(function() {
11571158
11581159 thisObj.setupTranscript().then(function() {
@@ -1790,7 +1791,8 @@ var AblePlayerInstances = [];
17901791 // Creates a preferences form and injects it.
17911792 // form is one of the supported forms (groups) defined in getPreferencesGroups()
17921793
1793- var available, thisObj, $prefsDiv, formTitle, introText,
1794+ var thisObj, available, descLangs,
1795+ $prefsDiv, formTitle, introText,
17941796 $prefsIntro,$prefsIntroP2,p3Text,$prefsIntroP3,i, j,
17951797 $fieldset, fieldsetClass, fieldsetId,
17961798 $descFieldset, $descLegend, $legend,
@@ -2003,6 +2005,7 @@ var AblePlayerInstances = [];
20032005 $thisOption.prop('selected',true);
20042006 }
20052007 $thisField.append($thisOption);
2008+ this.$voiceSelectField = $thisField;
20062009 }
20072010 }
20082011 else {
@@ -2283,9 +2286,9 @@ var AblePlayerInstances = [];
22832286
22842287 // Called if getBrowserVoices() succeeded after an earlier failure
22852288
2286- var $voiceSelect, i, optionValue, optionText, $thisOption;
2289+ var i, optionValue, optionText, $thisOption;
22872290
2288- $voiceSelect = $('#' + this.mediaId + '_prefDescVoice');
2291+ this.$voiceSelectField = $('#' + this.mediaId + '_prefDescVoice');
22892292 for (i=0; i < this.descVoices.length; i++) {
22902293 optionValue = this.descVoices[i].name;
22912294 optionText = optionValue + ' (' + this.descVoices[i].lang + ')';
@@ -2296,7 +2299,7 @@ var AblePlayerInstances = [];
22962299 if (this.prefDescVoice == optionValue) {
22972300 $thisOption.prop('selected',true);
22982301 }
2299- $voiceSelect .append($thisOption);
2302+ this.$voiceSelectField .append($thisOption);
23002303 }
23012304 };
23022305
@@ -7716,23 +7719,103 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith('<?xml
77167719 return false;
77177720 };
77187721
7722+ AblePlayer.prototype.getBrowserVoices = function () {
77197723
7720- AblePlayer.prototype.usingAudioDescription = function () {
7724+ // define this.descVoices
7725+ // NOTE: Some browsers require a user-initiated click before
7726+ // this.synth.getVoices() will work
77217727
7722- // Returns true if currently using audio description, false otherwise.
7728+ var voices, descLangs, voiceLang, playerLang;
77237729
7724- if (this.player === 'youtube') {
7725- return (this.activeYouTubeId === this.youTubeDescId);
7730+ // if browser supports Web Speech API
7731+ // define this.descvoices (an array of available voices in this browser)
7732+ if (window.speechSynthesis) {
7733+ this.synth = window.speechSynthesis;
7734+ voices = this.synth.getVoices();
7735+ descLangs = this.getDescriptionLangs();
7736+
7737+ if (voices.length > 0) {
7738+ this.descVoices = [];
7739+ // available languages are identified with local suffixes (e.g., en-US)
7740+ for (var i=0; i<voices.length; i++) {
7741+ // match only the first 2 characters of the lang code
7742+ // include any language for which there is a matching description track
7743+ // as well as the overall player lang
7744+ voiceLang = voices[i].lang.substr(0,2).toLowerCase();
7745+ playerLang = this.lang.substr(0,2).toLowerCase();
7746+ if (voiceLang === playerLang || (descLangs.indexOf(voiceLang) !== -1)) {
7747+ // this is a match. Add to the final array
7748+ this.descVoices.push(voices[i]);
7749+ }
7750+ }
7751+ if (!this.descVoices.length) {
7752+ // no voices available in the default language(s)
7753+ // just use all voices, regardless of language
7754+ this.descVoices = voices;
7755+ }
7756+ }
77267757 }
7727- else if (this.player === 'vimeo') {
7728- return (this.activeVimeoId === this.vimeoDescId);
7758+ return false;
7759+ };
7760+
7761+ AblePlayer.prototype.getDescriptionLangs = function () {
7762+
7763+ // returns an array of languages (from srclang atttributes)
7764+ // in which there are description tracks
7765+ // use only first two characters of the lang code
7766+ var descLangs = [];
7767+ if (this.tracks) {
7768+ for (var i=0; i < this.tracks.length; i++) {
7769+ if (this.tracks[i].kind === 'descriptions') {
7770+ descLangs.push(this.tracks[i].language.substr(0,2).toLowerCase());
7771+ }
7772+ }
77297773 }
7730- else {
7731- return (this.$sources.first().attr('data-desc-src') === this.$sources.first().attr('src'));
7774+ return descLangs;
7775+ };
7776+
7777+ AblePlayer.prototype.updateDescriptionVoice = function () {
7778+
7779+ // Called if user chooses a subtitle language for which there is a matching
7780+ // description track, and the subtitle language is different than the player language
7781+ // This ensures the description is read in a proper voice for the selected language
7782+
7783+ var voices, descVoice;
7784+
7785+ if (!this.descVoices) {
7786+ this.getBrowserVoices();
7787+ if (this.descVoices) {
7788+ this.rebuildDescPrefsForm();
7789+ }
7790+ }
7791+ else if (!this.$voiceSelectField) {
7792+ this.rebuildDescPrefsForm();
7793+ }
7794+
7795+ descVoice = this.selectedDescriptions.language;
7796+
7797+ if (this.synth) {
7798+ voices = this.synth.getVoices();
7799+ if (voices.length > 0) {
7800+ // available languages are identified with local suffixes (e.g., en-US)
7801+ for (var i=0; i<voices.length; i++) {
7802+ // select the first language that matches the first 2 characters of the lang code
7803+ if (voices[i].lang.substr(0,2).toLowerCase() === descVoice.substr(0,2).toLowerCase()) {
7804+ // make this the user's current preferred voice
7805+ this.prefDescVoice = voices[i].name;
7806+ // select this voice in the Description Prefs dialog
7807+ if (this.$voiceSelectField) {
7808+ this.$voiceSelectField.val(this.prefDescVoice);
7809+ }
7810+ break;
7811+ }
7812+ }
7813+ }
77327814 }
77337815 };
77347816
77357817 AblePlayer.prototype.swapDescription = function() {
7818+
77367819 // swap described and non-described source media, depending on which is playing
77377820 // this function is only called in two circumstances:
77387821 // 1. Swapping to described version when initializing player (based on user prefs & availability)
@@ -7944,12 +8027,15 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith('<?xml
79448027 // Therefore, this is a good time to check that, and try again if needed
79458028 // 2. In some browsers, the window.speechSynthesis.speaking property fails to reset,
79468029 // and onend event is never fired. This prevents new speech from being spoken.
8030+ // window.speechSynthesis.cancel() also fails, so it's impossible to recover.
79478031 // This only seems to happen with some voices.
79488032 // Typically the first voice in the getVoices() array (index 0) is realiable
7949- // Test results: The following voices seem to be reliable:
7950- // In Firefox on Mac: Alex, Fred, Victoria
7951- // In Chrome on Mac: same as above (and when Chrome stops speaking, it requires a reboot to start again!)
8033+ // When speech synthesis gets wonky, this is a deep problem that impacts all browsers
8034+ // and typically requires a computer reboot to make right again.
8035+ // This has been observed frequently in macOS Big Sur, but also in Windows 10
79528036 // To ignore user's voice preferences and always use the first voice, set the following var to true
8037+ // This is for testing only; not recommended for production
8038+ // unless the voice select field is also removed from the Prefs dialog
79538039 var useFirstVoice = false;
79548040
79558041 if (!this.descVoices) {
@@ -10112,7 +10198,6 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith('<?xml
1011210198 meta = this.meta[i];
1011310199 }
1011410200 }
10115-
1011610201 // regardless of source...
1011710202 this.transcriptLang = language;
1011810203
@@ -10136,6 +10221,12 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith('<?xml
1013610221 this.transcriptChapters = chapters;
1013710222 this.transcriptDescriptions = descriptions;
1013810223 }
10224+ if (this.selectedDescriptions) {
10225+ if (this.selectedDescriptions.language !== this.lang) {
10226+ // updating description voice to match new description language
10227+ this.updateDescriptionVoice();
10228+ }
10229+ }
1013910230 this.updateTranscript();
1014010231 };
1014110232
0 commit comments