undergroundmonorail
undergroundmonorail

Reputation: 129

Userscript that requires microphone access prompts for permissions in Chrome multiple times

I wrote a userscript that runs on the page for a game called TagPro that allows for voice recognition. It listens for key words followed by phrases, and depending on the word it puts the phrase into a different chat channel.

I'm running the script in Chrome's Tampermonkey extension. The speech recognition library I'm using is Annyang.

At the beginning of every game, Chrome confirms that you want to allow the site to use your microphone with a prompt like this:

http://tagpro-radius.koalabeast.com:8005/ wants to use your microphone. Allow/Deny

The problem I'm having is that sometimes, in the middle of a game, the prompt will come up again. It doesn't happen in every game, but when it does happen it usually happens more than once. I haven't noticed any patterns that would make it reproducible. That makes it very hard to diagnose, let alone fix. Is there an error with my script that could be causing this?

// ==UserScript==
// @name          TagPro Speech To Text
// @namespace     http://www.reddit.com/u/undergroundmonorail
// @description   Say a message out loud to say it into chat.
// @include       http://tagpro-*.koalabeast.com:*
// @include       http://tangent.jukejuice.com:*
// @include       http://maptest.newcompte.fr:*
// @include       http://justletme.be*
// @license       MIT
// @author        monorail
// @version       0.2
// ==/UserScript==

(function() {
    // https://github.com/TalAter/annyang
    // I couldn't figure out how to load it dynamically, so I just copypasted
    // the minified version.
    // @require works in Firefox, but not Chrome, and this is way easier than
    // any alternative I found.
    (function(a){"use strict";var b=this,c=b.SpeechRecognition||b.webkitSpeechRecognition||b.mozSpeechRecognition||b.msSpeechRecognition||b.oSpeechRecognition;if(!c)return b.annyang=null,a;var d,e,f=[],g={start:[],error:[],end:[],result:[],resultMatch:[],resultNoMatch:[],errorNetwork:[],errorPermissionBlocked:[],errorPermissionDenied:[]},h=0,i=!1,j="font-weight: bold; color: #00f;",k=/\s*\((.*?)\)\s*/g,l=/(\(\?:[^)]+\))\?/g,m=/(\(\?)?:\w+/g,n=/\*\w+/g,o=/[\-{}\[\]+?.,\\\^$|#]/g,p=function(a){return a=a.replace(o,"\\$&").replace(k,"(?:$1)?").replace(m,function(a,b){return b?a:"([^\\s]+)"}).replace(n,"(.*?)").replace(l,"\\s*$1?\\s*"),new RegExp("^"+a+"$","i")},q=function(a){a.forEach(function(a){a.callback.apply(a.context)})},r=function(){d===a&&b.annyang.init({},!1)};b.annyang={init:function(k,l){l=l===a?!0:!!l,d&&d.abort&&d.abort(),d=new c,d.maxAlternatives=5,d.continuous=!0,d.lang="en-US",d.onstart=function(){q(g.start)},d.onerror=function(a){switch(q(g.error),a.error){case"network":q(g.errorNetwork);break;case"not-allowed":case"service-not-allowed":e=!1,(new Date).getTime()-h<200?q(g.errorPermissionBlocked):q(g.errorPermissionDenied)}},d.onend=function(){if(q(g.end),e){var a=(new Date).getTime()-h;1e3>a?setTimeout(b.annyang.start,1e3-a):b.annyang.start()}},d.onresult=function(a){q(g.result);for(var c,d=a.results[a.resultIndex],e=0;e<d.length;e++){c=d[e].transcript.trim(),i&&b.console.log("Speech recognized: %c"+c,j);for(var h=0,k=f.length;k>h;h++){var l=f[h].command.exec(c);if(l){var m=l.slice(1);return i&&(b.console.log("command matched: %c"+f[h].originalPhrase,j),m.length&&b.console.log("with parameters",m)),f[h].callback.apply(this,m),q(g.resultMatch),!0}}}return q(g.resultNoMatch),!1},l&&(f=[]),k.length&&this.addCommands(k)},start:function(b){r(),b=b||{},e=b.autoRestart!==a?!!b.autoRestart:!0,h=(new Date).getTime(),d.start()},abort:function(){r(),e=!1,d.abort()},debug:function(a){i=arguments.length>0?!!a:!0},setLanguage:function(a){r(),d.lang=a},addCommands:function(a){var c,d;r();for(var e in a)if(a.hasOwnProperty(e)){if(c=b[a[e]]||a[e],"function"!=typeof c)continue;d=p(e),f.push({command:d,callback:c,originalPhrase:e})}i&&b.console.log("Commands successfully loaded: %c"+f.length,j)},removeCommands:function(a){a=Array.isArray(a)?a:[a],f=f.filter(function(b){for(var c=0;c<a.length;c++)if(a[c]===b.originalPhrase)return!1;return!0})},addCallback:function(c,d,e){if(g[c]!==a){var f=b[d]||d;"function"==typeof f&&g[c].push({callback:f,context:e||this})}}}}).call(this);

    // The following code is the function for sending a chat message. This is
    // how every userscript that touches chat does it. It's almost definitely
    // not related to the problem.

    var lastMessage = 0;

    var chat = function(message, all) {
        var limit = 500 + 10;
        var now = new Date();
        var timeDiff = now - lastMessage;
        if (timeDiff > limit) {
            tagpro.socket.emit("chat", {
                message: message,
                toAll: all
            });
            lastMessage = new Date();
        } else if (timeDiff >= 0) {
            setTimeout(chat, limit - timeDiff, chatMessage)
        }
    }

    // Code that I wrote begins here.

    var team = function(message) { chat(message, 0); };
    var all = function(message) { chat(message, 1); };
    var group = function(message) {
        if (tagpro.group.socket) {tagpro.group.socket.emit('chat', message);}
    };

    commands = { 'say *message': all,
                 'team *message': team,
                 'group *message': group };

    annyang.addCommands(commands);

    annyang.start();

})();

Upvotes: 1

Views: 1431

Answers (3)

Tal Ater
Tal Ater

Reputation: 1189

Chrome's speech recognition implementation simply stops from time-to-time. To handle this, annyang restarts it (unless it is stopped manually by you or the user). Since your page doesn't use HTTPS, the permission you gave Chrome to use speech recognition on that page doesn't persist, and it asks the user for permission again and again. This is why it is recommended to use HTTPS whenever using speech recognition on the page.

Upvotes: 3

Brock Adams
Brock Adams

Reputation: 93613

You are probably getting the extra prompts due to AJAX loading in iframes for various purposes. Wrap your code in a frame check:

if (window.top == window.self) {
    //-- Only run on the master (top) page...
    (function() {
        // https://github.com/TalAter/annyang
        // I couldn't figure out how to load it dynamically, so I just copypasted
        // the minified version.
        // @require works in Firefox, but not Chrome, and this is way easier than
        // any alternative I found.
        (function(a){"use strict";var b=this,c=b.SpeechRecognition||b.webkitSpeechRecognition|| ...

    // etc...
}


To eliminate the prompt completely, you have to use a full-blown extension. Then you can use techniques like in this other answer. (But then you also have to hassle with the Chrome store if you want to share the extension easily.)

Upvotes: 1

elzi
elzi

Reputation: 5692

This was a funny one.

So I unminified annyang.js to find the onend and onerror functions. In the onerror call I console logged the error to get:

Console result

Note error: "no-speech".

You see where this is going...

Looking up the W3 spec: https://dvcs.w3.org/hg/speech-api/raw-file/tip/speechapi.html#dfn-sre.nospeech

It reads:

"no-speech" No speech was detected.

Long story short, you're being too quiet.

As for PREVENTING it - dig into annyang.js, particularly the onerror event. Or sing the ABCs repeatedly while you need it on.

Upvotes: 2

Related Questions