Following HTML shows empty array in console on first click:
<!DOCTYPE html>
<html>
<head>
<script>
function test(){
console.log(window.speechSynthesis.getVoices())
}
</script>
</head>
<body>
Test
</body>
</html>
In second click you will get the expected list.
If you add onload event to call this function (<body onload="test()">), then you can get correct result on first click. Note that the first call on onload still doesn't work properly. It returns empty on page load but works afterward.
Questions:
Since it might be a bug in beta version, I gave up on "Why" questions.
Now, the question is if you want to access window.speechSynthesis on page load:
What is the best hack for this issue?
How can you make sure it will load speechSynthesis, on page load?
Background and tests:
I was testing the new features in Web Speech API, then I got to this problem in my code:
<script type="text/javascript">
$(document).ready(function(){
// Browser support messages. (You might need Chrome 33.0 Beta)
if (!('speechSynthesis' in window)) {
alert("You don't have speechSynthesis");
}
var voices = window.speechSynthesis.getVoices();
console.log(voices) // []
$("#test").on('click', function(){
var voices = window.speechSynthesis.getVoices();
console.log(voices); // [SpeechSynthesisVoice, ...]
});
});
</script>
<a id="test" href="#">click here if 'ready()' didn't work</a>
My question was: why does window.speechSynthesis.getVoices() return empty array, after page is loaded and onready function is triggered? As you can see if you click on the link, same function returns an array of available voices of Chrome by onclick triger?
It seems Chrome loads window.speechSynthesis after the page load!
The problem is not in ready event. If I remove the line var voice=... from ready function, for first click it shows empty list in console. But the second click works fine.
It seems window.speechSynthesis needs more time to load after first call. You need to call it twice! But also, you need to wait and let it load before second call on window.speechSynthesis. For example, following code shows two empty arrays in console if you run it for first time:
// First speechSynthesis call
var voices = window.speechSynthesis.getVoices();
console.log(voices);
// Second speechSynthesis call
voices = window.speechSynthesis.getVoices();
console.log(voices);
According to Web Speech API Errata (E11 2013-10-17), the voice list is loaded async to the page. An onvoiceschanged event is fired when they are loaded.
voiceschanged: Fired when the contents of the SpeechSynthesisVoiceList, that the getVoices method will return, have changed. Examples include: server-side synthesis where the list is determined asynchronously, or when client-side voices are installed/uninstalled.
So, the trick is to set your voice from the callback for that event listener:
// wait on voices to be loaded before fetching list
window.speechSynthesis.onvoiceschanged = function() {
window.speechSynthesis.getVoices();
...
};
You can use a setInterval to wait until the voices are loaded before using them however you need and then clearing the setInterval:
var timer = setInterval(function() {
var voices = speechSynthesis.getVoices();
console.log(voices);
if (voices.length !== 0) {
var msg = new SpeechSynthesisUtterance(/*some string here*/);
msg.voice = voices[/*some number here to choose from array*/];
speechSynthesis.speak(msg);
clearInterval(timer);
}
}, 200);
$("#test").on('click', timer);
After studying the behavior on Google Chrome and Firefox, this is what can get all voices:
Since it involves something asynchronous, it might be best done with a promise:
const allVoicesObtained = new Promise(function(resolve, reject) {
let voices = window.speechSynthesis.getVoices();
if (voices.length !== 0) {
resolve(voices);
} else {
window.speechSynthesis.addEventListener("voiceschanged", function() {
voices = window.speechSynthesis.getVoices();
resolve(voices);
});
}
});
allVoicesObtained.then(voices => console.log("All voices:", voices));
Note:
When the event voiceschanged fires, we need to call .getVoices() again. The original array won't be populated with content.
On Google Chrome, we don't have to call getVoices() initially. We only need to listen on the event, and it will then happen. On Firefox, listening is not enough, you have to call getVoices() and then listen on the event voiceschanged, and set the array using getVoices() once you get notified.
Using a promise makes the code more clean. Everything related to getting voices are in this promise code. If you don't use a promise but instead put this code in your speech routine, it is quite messy.
You can write a voiceObtained promise to resolve to a voice you want, and then your function to say something can just do: voiceObtained.then(voice => { }) and inside that handler, call the window.speechSynthesis.speak() to speak something. Or you can even write a promise speechReady("hello world").then(speech => { window.speechSynthesis.speak(speech) }) to say something.
heres the answer
function synthVoice(text) {
const awaitVoices = new Promise(resolve=>
window.speechSynthesis.onvoiceschanged = resolve)
.then(()=> {
const synth = window.speechSynthesis;
var voices = synth.getVoices();
console.log(voices)
const utterance = new SpeechSynthesisUtterance();
utterance.voice = voices[3];
utterance.text = text;
synth.speak(utterance);
});
}
At first i used onvoiceschanged , but it kept firing even after the voices was loaded, so my goal was to avoid onvoiceschanged at all cost.
This is what i came up with. It seems to work so far, will update if it breaks.
loadVoicesWhenAvailable();
function loadVoicesWhenAvailable() {
voices = synth.getVoices();
if (voices.length !== 0) {
console.log("start loading voices");
LoadVoices();
}
else {
setTimeout(function () { loadVoicesWhenAvailable(); }, 10)
}
}
setInterval solution by Salman Oskooi was perfect
Please see https://jsfiddle.net/exrx8e1y/
function myFunction() {
dtlarea=document.getElementById("details");
//dtlarea.style.display="none";
dtltxt="";
var mytimer = setInterval(function() {
var voices = speechSynthesis.getVoices();
//console.log(voices);
if (voices.length !== 0) {
var msg = new SpeechSynthesisUtterance();
msg.rate = document.getElementById("rate").value; // 0.1 to 10
msg.pitch = document.getElementById("pitch").value; //0 to 2
msg.volume = document.getElementById("volume").value; // 0 to 1
msg.text = document.getElementById("sampletext").value;
msg.lang = document.getElementById("lang").value; //'hi-IN';
for(var i=0;i<voices.length;i++){
dtltxt+=voices[i].lang+' '+voices[i].name+'\n';
if(voices[i].lang==msg.lang) {
msg.voice = voices[i]; // Note: some voices don't support altering params
msg.voiceURI = voices[i].voiceURI;
// break;
}
}
msg.onend = function(e) {
console.log('Finished in ' + event.elapsedTime + ' seconds.');
dtlarea.value=dtltxt;
};
speechSynthesis.speak(msg);
clearInterval(mytimer);
}
}, 1000);
}
This works fine on Chrome for MAC, Linux(Ubuntu), Windows and Android
Android has non-standard en_GB wile others have en-GB as language code
Also you will see that same language(lang) has multiple names
On Mac Chrome you get en-GB Daniel besides en-GB Google UK English Female and n-GB Google UK English Male
en-GB Daniel (Mac and iOS)
en-GB Google UK English Female
en-GB Google UK English Male
en_GB English United Kingdom
hi-IN Google हिन्दी
hi-IN Lekha (Mac and iOS)
hi_IN Hindi India
Another way to ensure voices are loaded before you need them is to bind their loading state to a promise, and then dispatch your speech commands from a then:
const awaitVoices = new Promise(done => speechSynthesis.onvoiceschanged = done);
function listVoices() {
awaitVoices.then(()=> {
let voices = speechSynthesis.getVoices();
console.log(voices);
});
}
When you call listVoices, it will either wait for the voices to load first, or dispatch your operation on the next tick.
I used this code to load voices successfully:
<select id="voices"></select>
...
function loadVoices() {
populateVoiceList();
if (speechSynthesis.onvoiceschanged !== undefined) {
speechSynthesis.onvoiceschanged = populateVoiceList;
}
}
function populateVoiceList() {
var allVoices = speechSynthesis.getVoices();
allVoices.forEach(function(voice, index) {
var option = $('<option>').val(index).html(voice.name).prop("selected", voice.default);
$('#voices').append(option);
});
if (allVoices.length > 0 && speechSynthesis.onvoiceschanged !== undefined) {
// unregister event listener (it is fired multiple times)
speechSynthesis.onvoiceschanged = null;
}
}
I found the 'onvoiceschanged' code from this article: https://hacks.mozilla.org/2016/01/firefox-and-the-web-speech-api/
Note: requires JQuery.
Works in Firefox/Safari and Chrome (and in Google Apps Script too - but only in the HTML).
async function speak(txt) {
await initVoices();
const u = new SpeechSynthesisUtterance(txt);
u.voice = speechSynthesis.getVoices()[3];
speechSynthesis.speak(u);
}
function initVoices() {
return new Promise(function (res, rej){
speechSynthesis.getVoices();
if (window.speechSynthesis.onvoiceschanged) {
res();
} else {
window.speechSynthesis.onvoiceschanged = () => res();
}
});
}
While the accepted answer works great but if you're using SPA and not loading full-page, on navigating between links, the voices will not be available.
This will run on a full-page load
window.speechSynthesis.onvoiceschanged
For SPA, it wouldn't run.
You can check if it's undefined, run it, or else, get it from the window object.
An example that works:
let voices = [];
if(window.speechSynthesis.onvoiceschanged == undefined){
window.speechSynthesis.onvoiceschanged = () => {
voices = window.speechSynthesis.getVoices();
}
}else{
voices = window.speechSynthesis.getVoices();
}
// console.log("voices", voices);
I had to do my own research for this to make sure I understood it properly, so just sharing (feel free to edit).
My goal is to:
Get a list of voices available on my device
Populate a select element with those voices (after a particular page loads)
Use easy to understand code
The basic functionality is demonstrated in MDN's official live demo of:
https://github.com/mdn/web-speech-api/tree/master/speak-easy-synthesis
but I wanted to understand it better.
To break the topic down...
SpeechSynthesis
The SpeechSynthesis interface of the Web Speech API is the controller
interface for the speech service; this can be used to retrieve
information about the synthesis voices available on the device, start
and pause speech, and other commands besides.
Source
onvoiceschanged
The onvoiceschanged property of the SpeechSynthesis interface
represents an event handler that will run when the list of
SpeechSynthesisVoice objects that would be returned by the
SpeechSynthesis.getVoices() method has changed (when the voiceschanged
event fires.)
Source
Example A
If my application merely has:
var synth = window.speechSynthesis;
console.log(synth);
console.log(synth.onvoiceschanged);
Chrome developer tools console will show:
Example B
If I change the code to:
var synth = window.speechSynthesis;
console.log("BEFORE");
console.log(synth);
console.log(synth.onvoiceschanged);
console.log("AFTER");
var voices = synth.getVoices();
console.log(voices);
console.log(synth);
console.log(synth.onvoiceschanged);
The before and after states are the same, and voices is an empty array.
Solution
Although i'm not confident implementing Promises, the following worked for me:
Defining the function
var synth = window.speechSynthesis;
// declare so that values are accessible globally
var voices = [];
function set_up_speech() {
return new Promise(function(resolve, reject) {
// get the voices
var voices = synth.getVoices();
// get reference to select element
var $select_topic_speaking_voice = $("#select_topic_speaking_voice");
// for each voice, generate select option html and append to select
for (var i = 0; i < voices.length; i++) {
var option = $("<option></option>");
var suffix = "";
// if it is the default voice, add suffix text
if (voices[i].default) {
suffix = " -- DEFAULT";
}
// create the option text
var option_text = voices[i].name + " (" + voices[i].lang + suffix + ")";
// add the option text
option.text(option_text);
// add option attributes
option.attr("data-lang", voices[i].lang);
option.attr("data-name", voices[i].name);
// append option to select element
$select_topic_speaking_voice.append(option);
}
// resolve the voices value
resolve(voices)
});
}
Calling the function
// in your handler, populate the select element
if (page_title === "something") {
set_up_speech()
}
Android Chrome - turn off data saver. It was helpfull for me.(Chrome 71.0.3578.99)
// wait until the voices load
window.speechSynthesis.onvoiceschanged = function() {
window.speechSynthesis.getVoices();
};
let voices = speechSynthesis.getVoices();
let gotVoices = false;
if (voices.length) {
resolve(voices, message);
} else {
speechSynthesis.onvoiceschanged = () => {
if (!gotVoices) {
voices = speechSynthesis.getVoices();
gotVoices = true;
if (voices.length) resolve(voices, message);
}
};
}
function resolve(voices, message) {
var synth = window.speechSynthesis;
let utter = new SpeechSynthesisUtterance();
utter.lang = 'en-US';
utter.voice = voices[65];
utter.text = message;
utter.volume = 100.0;
synth.speak(utter);
}
Works for Edge, Chrome and Safari - doesn't repeat the sentences.
I'm trying to add a 1 second cooldown to my send-message system (as in, you can send 1 message per second max). So my initial thought was simply to create a timeout, and before attempting in sending to check if it exists still. That turned out to take more line of code than I anticipated initially.
Is there something I'm missing here? Isn't there something as simple as:
//inside some message sending function
if(!mySuperCooldown)
{
//send message
mySuperCooldown = cooldown(1000);
}
Everything else I construct successfully ends up taking loads of lines, and it appears to me as something someone thought of before. Thank you, and excuse my illiteracy.
Have a flag that allows messages, and set it to false when a message is sent. Then set a timeout for 1000 milliseconds that resets the flag to true.
var allowMessage = true;
function sendMessage(msg) {
if (allowMessage) {
//do something
allowMessage = false;
setTimeout(() => allowMessage = true, 1000);
}
}
Make a higher order function that turns a normal function into one that is rate limited:
function rate_limit(delay, func) {
var last_call = null;
return function() {
if (last_call && (Date.now() - last_call <= delay)) {
return;
}
last_call = Date.now();
return func();
};
}
You can then rate limit any function:
var my_function = rate_limit(1000, function() {
console.log('foo');
});
Running my_function() will only call your original function once per second.
I have written a program using CefSharp that scrapes a web page. When I began the project, I didn't realize that the web site made use of the now defunct showModalDialog function. I really like CefSharp and don't want to have to use the .Net WebBrowser if I can help it. I implemented the replacement for showModalDialog as an extension using a trimmed down version of the code found here :
https://github.com/niutech/showModalDialog
I created the following showModalDialog.js and added it as a Resource:
(function () {
showModalDialog = function (url, arg, opt) {
url = url || ''; //URL of a dialog
arg = arg || null; //arguments to a dialog
opt = opt || 'dialogWidth:300px;dialogHeight:200px'; //options: dialogTop;dialogLeft;dialogWidth;dialogHeight or CSS styles
var caller = showModalDialog.caller.toString();
var dialog = document.body.appendChild(document.createElement('dialog'));
dialog.setAttribute('style', opt.replace(/dialog/gi, ''));
dialog.innerHTML = '×<iframe id="dialog-body" src="' + url + '" style="border: 0; width: 100%; height: 100%;"></iframe>';
document.getElementById('dialog-body').contentWindow.dialogArguments = arg;
document.getElementById('dialog-close').addEventListener('click', function (e) {
e.preventDefault();
dialog.close();
});
dialog.showModal();
//if using yield
if (caller.indexOf('yield') >= 0) {
return new Promise(function (resolve, reject) {
dialog.addEventListener('close', function () {
var returnValue = document.getElementById('dialog-body').contentWindow.returnValue;
document.body.removeChild(dialog);
resolve(returnValue);
});
});
}
//if using eval
var isNext = false;
var nextStmts = caller.split('\n').filter(function (stmt) {
if (isNext || stmt.indexOf('showModalDialog(') >= 0)
return isNext = true;
return false;
});
dialog.addEventListener('close', function () {
var returnValue = document.getElementById('dialog-body').contentWindow.returnValue;
document.body.removeChild(dialog);
nextStmts[0] = nextStmts[0].replace(/(window\.)?showModalDialog\(.*\)/g, JSON.stringify(returnValue));
eval('{\n' + nextStmts.join('\n'));
});
throw 'Execution stopped until showModalDialog is closed';
};
})();
I got it registered in CefSharp in the startup as follows:
CefSettings settings = new CefSettings();
settings.RegisterExtension(new CefExtension("showModalDialog", Resources.showModalDialog));
//Perform dependency check to make sure all relevant resources are in our output directory.
Cef.Initialize(settings, shutdownOnProcessExit: true, performDependencyCheck: true);
So when showModalDialog is called from a web page, it pops up the dialog. The problem is that it doesn't seem to pass the parameter correctly. The page I am scraping sets up the showModalDialog as follows:
function popupPanel() {
var args = new Array(document.getElementById("IDofElementToReceiveTheValue"));
var url = '<url of the html for the popup>'
var windowParam = 'resizable=yes;dialogWidth=975px;dialogHeight=750px;scrollbars=yes;status=no';
showModalDialog(url,args, windowParam);
return true;
}
The popup displays a list of records with a select column. When you click the link in the select column, this function fires:
function selectItem(keyvalue) {
window.dialogArguments[0].value = keyvalue;
window.close();
}
So when the showModalDialog is called, it creates an array containing one element that should receive the value of the record that is selected. It passes this array as an argument to the showModalDialog. The showModalDialog passes this array into the dialogArguments for the window of the popup. When you select an item, it throws an error saying "there is no object at index 0 of null" and the window never closes (I assume this is because it never gets to that step due to the exception). For some reason, when the selectItem function is called, the dialogArguments of the window is null. The object is not making it from the initial setup for the call to showModalDialog to the selectItem function in the popup. Can anyone shed any light on why this might be? Any help with this would be greatly appreciated.
Thanks,
Jim
I'm using osx 10.9.2, protractor 0.21.0, selenium-server-standalone 2.40.0 and chromedriver 2.9.
I'm having some problems, which (I believe) was due to window focusing issue.
When I run my e2e test using protractor, the browser window would show but my terminal will still be the one in focus. This is apparent from "Terminal" was still shown in my menu bar rather than "Chrome" (osx behavior that indicates which app is in focus).
I tried to remedy the situation by doing this to no avail:
browser.driver.getAllWindowHandles().then(function(handles) {
console.log(handles[0]);
browser.driver.switchTo().window(handles[0]);
});
This situation causes some of my tests to fail. For example, tests that include clicking a field with bootstrap datepicker won't show the calendar and making my test cannot interact with the datepicker calendar.
The situation is even worse on firefox. Firefox won't even show any dropdown menu when clicked if the browser is not in focus.
Funnily, when I click the browser window manually after it shows up the first time, the tests will work normally.
When I tried a different approach: Doing the test on a freshly installed debian linux, still not working. Behavior is similar as described above.
These are my configuration files: https://gist.github.com/giosakti/ca24a13705d15f4374b0
Unfortunately IE & Firefox don't ensure the windows handlers order, so we need iterate them. And getting focus on the new browser window/tab can be tricky too.
I've run into these issues so i created:
A helper function to overcome those issues
// Needs an element to make sure we are on the correct popup
var waitForPopUpHandle = function(elm, errorMessage) {
if (errorMessage == null) {
errorMessage = 'Expected a new browser tab or window to pop up';
};
if (elm == null) {
throw 'waitForPopUpHandle needs an element to wait for!';
};
browser.ignoreSynchronization = true; // not a protractor page
// IE & Firefox don't ensure the windows handlers order, so we need iterate them.
// First wait to have more that 1 browser tab
browser.manage().timeouts().implicitlyWait(300); // a reasonable wait-retry time
var i = 0;
var popUpHandle = browser.driver.wait(function() {
return browser.getAllWindowHandles().then(function(handles) {
if (handles.length > 1) {
return browser.switchTo().window(handles[i]).then(function() {
return browser.driver.isElementPresent(elm).then(function(result) {
if (result) {
return handles[i];
} else {
browser.sleep(400); // give it a break
i = i + 1;
if (i >= handles.length) {
i = 0;
};
return false;
};
});
});
} else {
browser.sleep(400); // give it a break
return false;
};
});
}, browser.params.timeouts.pageLoadTimeout, errorMessage);
// restore implicit wait
browser.manage().timeouts().implicitlyWait(0); //restore
return popUpHandle;
};
Sample usage of that helper
var popUpHandle = waitForPopUpHandle(by.css('div.some-element-unique-to-that-popup'));
browser.switchTo().window(popUpHandle).then(function() {
browser.ignoreSynchronization = true; // not an angular page
browser.driver.findElement(by.css('div.some-element-unique-to-that-popup')); // wait for the elm
// your expect()'s go here ...
// ...
browser.close().then(function() {
// This close() promise is necessary on IE and probably on Firefox too
var mainTab = waitForMainWindow();
expect(browser.switchTo().window(mainTab).then(function() {
browser.ignoreSynchronization = false; // restore if main window is an angular page
// Ensure we are back on the main window
// ....
return true;
})).toBe(true);
});
});
And finally waitForMainWindow helper
var waitForMainWindow = function(errorMessage) {
if (errorMessage == null) {
errorMessage = 'Expected main browser window to be available';
};
browser.ignoreSynchronization = true; // not an angular page
return browser.driver.wait(function() {
return browser.getAllWindowHandles().then(function(handles) {
if (handles.length > 1) {
var hnd = handles[handles.length - 1];
return browser.switchTo().window(hnd).then(function() {
return browser.close().then(function() {
browser.sleep(400); // wait for close
return false;
});
});
} else {
return handles[0];
};
});
}, 5000, errorMessage);
};
I found a silver lining! I downgrade the chrome using installer from http://google-chrome.en.uptodown.com/mac/old and the focus issues are gone.. (the issues still persist on firefox though)..
If you search "chrome 34 focus issues" on google, you will find several reports that maybe correlate with this issue. for example: https://productforums.google.com/forum/#!topic/chrome/pN5pYf2kolc
but I still don't know whether this was a bug or expected behavior of chrome 34. So for now I block google updater and use Chrome 33.
I'm using Devise, and automatic logout works great.
However, the user is not informed they have been logged out until they make another request, at which point they are redirected to the sign in page. For AJAX functionality, this is not great, it either fails silently or raises an exception.
Devise wiki doesnt seem to have an example, is there a standard solution to this? A javascript popup with a countdown timer, that does a redirect if the user doesnt click "keep me logged in"?
I ended up implementing jscript timeout something similar to below.
Some unanswered questions:
what happens on switching to another tab?
works in IE?
application.js
//= require timer
// redirect user after 15 minutes of inactivity - should match Devise.timeout_in + 1 second grace period
$(function() {
var logout_timer = new Timer(901, 'users/sign_in', window);
logout_timer.start();
// restart timer if activity
$(document).on('keyup keypress blur change mousemove',function(){
logout_timer.start();
});
});
timer.js
Timer = function(time_in_secs, path, windowobj) { // window object must be injected, else location replace will fail specs
var self = this; // 'this' not avail in setInterval, must set to local var avail to all functions
this.state = 'init'
this.time_remaining = time_in_secs;
this.timer_id = undefined;
this.start = function() {
// if restarting, there will be a timer id. Clear it to prevent creating a new timer, reset time remaining
if (this.timer_id !== undefined) {
this.time_remaining = time_in_secs;
this.clear_timer(this.timer_id, self);
}
this.state = 'running';
this.timer_id = setInterval(function() { // IE any version does not allow args to setInterval. Therefore, local variables or refer to self obj
self.time_remaining -= 1;
// log status every 10 seconds
if ((self.time_remaining % 10) === 0) {
console.log("logging user out in " + self.time_remaining + " seconds");
}
// when timer runs out, clear timer and redirect
if ( self.time_remaining <= 0 ) {
self.clear_timer(self.timer_id, self);
self.do_redirect(path, windowobj);
};
}, 1000);
return this.timer_id;
};
this.clear_timer = function(timer_id, self) {
self.state = 'stopped';
clearInterval(self.timer_id);
}
this.remaining = function() {
return this.time_remaining;
};
this.do_redirect = function(path, windowobj) {
console.log("Redirecting to " + path);
self.state = 'redirecting';
windowobj.location = path;
}
}