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 having trouble adding an observer to watch for changes in firefox's search engines. I read the nsIBrowserSearchService page on the Mozilla Developer Site which suggests to use the init() method of the Services.search object.
void init([optional] in nsIBrowserSearchInitObserver observer);
I tried that and I managed to get it to execute the function once on start up but it never calls it again when I add or remove or reorder the search engines. So I'm doing something wrong.
I have experience with observers but only with using general preferences and I generally use add them and remove them using the code below. I'm not sure how to do it with the nsIBrowserSearchService . I would like to observe the nsIBrowserSearchService in the same or a similar way but I'm not sure what I would put for
branch = Services.prefs.getBranch("preferenceNameHere");
I'm not observing Services.prefs but Services.search I assume, and that has no getBranch method as far as I can tell.
This is how I normally add and remove the observer in Chrome.js
const {Ci, Cu} = require("chrome");
const {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
branch = Services.prefs.getBranch("preferenceNameHere");
branch.addObserver("", observe, false);
function observe(subject, topic, data) {
// instanceof actually also "casts" subject
if (!(subject instanceof Ci.nsIPrefBranch)) {
return;
}
//do stuff here
};
exports.onUnload = function(reason) {
// Need to remove our observer again! This isn't automatic and will leak
// otherwise.
branch.removeObserver("", observe);
if(reason == "disable" || reason == "uninstall"){
//restore changes made by addon
}
};
Can anyone advise me on how to do this so I can observe for changes and ensure that I remove the observer properly with the search object. Thanks
What you are trying above is trying to use pref observer on non-prefs, this is not possible. You have to use the regular observer service like this.
This notifies you when the user changes the engine.
Cu.import('resource://gre/modules/Services.jsm');
var observers = {
'browser-search-engine-modified': {
aTopic: 'browser-search-engine-modified',
observe: function (aSubject, aTopic, aData) {
if (aData == 'engine-current') {
console.log('current engine was changed!');
//console.log('aSubject on change:', aSubject.name, 'same as Services.search.currentEngine.name:', Services.search.currentEngine.name); //aSubject is the engine
//console.log('aTopic on change:', aTopic); //aTopic is obviously `browser-search-engine-modified`
}
},
reg: function () {
Services.obs.addObserver(observers[this.aTopic], this.aTopic, false);
},
unreg: function () {
Services.obs.removeObserver(observers[this.aTopic], this.aTopic);
}
}
};
To start listening do this:
for (var o in observers) {
observers[o].reg();
}
To stop listening do this:
for (var o in observers) {
observers[o].unreg();
}
I'm not sure what happens when user adds a new engine but doenst select it. Or if he removes a engine. Please let me know what those messages are when user does that.
I'm running a script on Facebook that requires me to get the IDs of people in my "friends" window (this might not be the most efficient way to accomplish this specific task, but since I'd like to know how to do this in general it's a good example).
This means that if I have more than a small number of friends I have to scroll down for Facebook to add them to the page.
I've added logic that scrolls the page down to the footer, but I don't know how to force my function that grabs the IDs to run after the content loads.
For now, I've resorted to using setTimeout for a few seconds - obviously, this isn't guaranteed to at the appropriate time, so I'd like to know how to do this properly:
var k;
function doit(){
k = document.getElementsByClassName("_698");
var g= Array.prototype.slice.call(k);
confirm(g.length);
// the confirm is just to make sure it's working
// (if i don't use setTimeout it'll return a smaller number
// since not all the friends were included)
}
window.addEventListener("load", function(){
document.getElementById( "pageFooter" )
.scrollIntoView();setTimeout(doit,3000);
});
Crayon Violent details how to accomplish this in his answer to JavaScript detect an AJAX event. The trick is to hook the underlying XMLHttpRequest object in order to detect when a request is sent.
I've re-written the logic there a bit to make it more suitable for your needs:
//
// Hooks XMLHttpRequest to log all AJAX requests.
// Override ajaxHook.requestCompleted() to do something specific
// in response to a given request.
//
var ajaxHook = (function()
{
// we're using a self-executing function here to avoid polluting the global
// namespace. The hook object is returned to expose just the properties
// needed by client code.
var hook = {
// by default, just logs all requests to the console.
// Can be overridden to do something more interesting.
requestCompleted: function(xmlHttp, url, method) { console.log(url); }
};
// hook open() to store URL and method
var oldOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url)
{
this.hook_method = method;
this.hook_url = url;
oldOpen.apply(this, arguments);
}
// hook send() to allow hooking onreadystatechange
var oldSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function()
{
var xmlhttp = this;
//hook onreadystatechange event to allow processing results
var oldReadyStateChange = xmlhttp.onreadystatechange;
xmlhttp.onreadystatechange = function()
{
oldReadyStateChange.apply(xmlhttp, arguments);
if ( this.readyState === 4 ) // completed
{
hook.requestCompleted(xmlhttp,
xmlhttp.hook_url, xmlhttp.hook_method);
}
};
oldSend.apply(this, arguments);
};
return hook;
})();
With this bit of code loaded in your userscript, you can then implement your logic as follows:
var k;
function doit()
{
k = document.getElementsByClassName("_698");
var g= Array.prototype.slice.call(k);
confirm(g.length);
}
window.addEventListener("load", function()
{
ajaxHook.requestCompleted = function(xmlhttp, url, method)
{
// is this the request we're interested in?
// (Facebook appears to load friends from a URL that contains this string)
if ( /AllFriendsAppCollectionPagelet/.test(url) )
{
// Facebook defers rendering the results here,
// so we just queue up scraping them until afterwards
setTimeout(doit, 0);
}
};
// trigger loading of more friends by scrolling the bottom into view
document.getElementById( "pageFooter" )
.scrollIntoView();
});
Is there any chance to pass some data to my server through install.rdf when my Firefox add-on check server for update?
Example:
...
<em:updateURL>http://www.site.com/update.php?var=myData</em:updateURL>
...
where "myData" is saved in options.xul or in another place like simple-storage.
Yes, but it is quite nasty. The AddonManager will replace a bunch of predefined and dynamic properties in the URL:
Register a new component implementing nsIPropertyBag2 (or use an existing implementation, such as ["#mozilla.org/hash-property-bag;1"]).
Register your component in the nsICategoryManager under the "extension-update-params" category.
Since you mentioned simple-storage: restartless add-ons must also unregister their stuff when being unloaded.
There is a unit test demonstrating how this stuff works. You of course need to adapt it a bit (if alone for require("chrome").
I found one "simple solution" but I dont know if that is also good practice ...
var origLink = "http://www.site.net/update.php?var=myData";
var newsLink = "http://www.site.net/update.php?var=" + simplePref.prefs.myData;
const {Cc,Ci,Cu} = require("chrome");
var observer = {
QueryInterface: function(iid) {
if (iid.equals(Ci.nsIObserver) || iid.equals(Ci.nsISupports)) return this;
},
observe: function(subject, topic, data){
if (topic == "http-on-modify-request"){
var channel = subject.QueryInterface(Ci.nsIChannel);
if (channel.originalURI.spec == origLink) {
channel.originalURI.spec = newsLink;
}
}
}
};
var ObsService = Cc["#mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
ObsService.addObserver(observer, "http-on-modify-request", false);
I've been reading and hacking around with https://developer.mozilla.org/en/XUL_School/Intercepting_Page_Loads but can seem to do what I need.
I'm working on Chromeless, trying to prevent the main xulbrowser element from ever being navigated away from, e.g., links should not work, neither should window.location.href="http://www.example.com/".
I'm assuming I can do this via browser.webProgress.addProgressListener and then listen to onProgressChange but I can't figure out how to differentiate between a resource request and the browser changing locations (it seems that onLocationChange is too late as the document is already being unloaded).
browser.webProgress.addProgressListener({
onLocationChange: function(){},
onStatusChange: function(){},
onStateChange: function(){},
onSecurityChange: function(){},
onProgressChange: function(){
aRequest.QueryInterface(Components.interfaces.nsIHttpChannel)
if( /* need to check if the object triggering the event is the xulbrowser */ ){
aRequest.cancel(Components.results.NS_BINDING_ABORTED);
}
},
QueryInterface: xpcom.utils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference])
}, wo._browser.webProgress.NOTIFY_ALL);
Another option that sounds promising is the nsIContentPolicy.shouldLoad() method but I really have no clue how to "create an XPCOM component that extends nsIContentPolicy and register it to the "content-policy" category using the nsICategoryManager."
Any Ideas?
I got help on this from the mozilla's #xulrunner irc channel.
Resulting solution follows.
Note: this is a module for use in Mozilla Chromeless, the require("chrome") and require("xpcom") bits will NOT be available under normal circumstances.
const {Cc, Ci, Cu, Cm, Cr} = require("chrome");
const xpcom = require("xpcom");
/***********************************************************
class definition
***********************************************************/
var description = "Chromeless Policy XPCOM Component";
/* UID generated by http://www.famkruithof.net/uuid/uuidgen */
var classID = Components.ID("{2e946f14-72d5-42f3-95b7-4907c676cf2b}");
// I just made this up. Don't know if I'm supposed to do that.
var contractID = "#mozilla.org/chromeless-policy;1";
//class constructor
function ChromelessPolicy() {
//this.wrappedJSObject = this;
}
// class definition
var ChromelessPolicy = {
// properties required for XPCOM registration:
classDescription: description,
classID: classID,
contractID: contractID,
xpcom_categories: ["content-policy"],
// QueryInterface implementation
QueryInterface: xpcom.utils.generateQI([Ci.nsIContentPolicy,
Ci.nsIFactory, Ci.nsISupportsWeakReference]),
// ...component implementation...
shouldLoad : function(aContentType, aContentLocation, aRequestOrigin, aContext, aMimeTypeGuess, aExtra) {
let result = Ci.nsIContentPolicy.ACCEPT;
// only filter DOCUMENTs (not SUB_DOCUMENTs, like iframes)
if( aContentType === Ci.nsIContentPolicy["TYPE_DOCUMENT"]
// block http(s) protocols...
&& /^http(s):/.test(aContentLocation.spec) ){
// make sure we deny the request now
result = Ci.nsIContentPolicy.REJECT_REQUEST;
}
// continue loading...
return result;
},
createInstance: function(outer, iid) {
if (outer)
throw Cr.NS_ERROR_NO_AGGREGATION;
return this.QueryInterface(iid);
}
};
let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
try
{
Cm.nsIComponentRegistrar.registerFactory(classID, description, contractID, ChromelessPolicy);
}
catch (e) {
// Don't stop on errors - the factory might already be registered
Cu.reportError(e);
}
const categoryManager = Cc["#mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
for each (let category in ChromelessPolicy.xpcom_categories) {
categoryManager.addCategoryEntry(category, ChromelessPolicy.classDescription, ChromelessPolicy.contractID, false, true);
}
Pull Request on github for those that are interested: https://github.com/mozilla/chromeless/pull/114