5

I'm trying to change the sinkId at an Audio-Element in a chrome app.

Code:

var audio = new Audio();
audio.setSinkId("communications");

I'll get this error:

DOMException: No permission to use requested device

So to get the permission I tried to do that:

navigator.webkitGetUserMedia({
    audio: true
}, (stream) => {
    console.log("stream: ", stream);
}, (error) => {
    console.log("error: ", error);
});

But now I get this:

error:
NavigatorUserMediaError {name: "InvalidStateError", message: "", constraintName: ""}

I don't know, whether

  • 'audio' is not available (but actually it should)
  • I need a secure connection (but how do I get this at a chrome app?)
Kaiido
  • 123,334
  • 13
  • 219
  • 285
Gerrit
  • 423
  • 5
  • 13
  • I have the same problem and looked at the docs: https://developers.google.com/web/updates/2015/10/media-devices the example (which is also not working on Windows 10): https://webrtc.github.io/samples/src/content/devices/input-output/ And an open issue at github: https://github.com/WebAudio/web-audio-api/issues/445 It seems to be broken currently.. PS: There is also an Chrome extension which should be able to switch audio outputs here: https://chrome.google.com/webstore/detail/audiopick/gfhcppdamigjkficnjnhmnljljhagaha It might contains some interesting code? (haven't looked at it) – Wezelkrozum Feb 19 '18 at 00:42
  • 1
    Thanks for your reply. Unfortunately, the example and the chrome extension aren't working on my computer (Windows 8, chrome version 63.0.3239.132), too. Hope it will be fixed soon! – Gerrit Feb 19 '18 at 13:56
  • 1
    No "invalid state" error here on Chrome v78, but it's still seemingly broken to have to request an audio input stream to get permissions for audio output. I added a bounty to your question to hopefully get a better answer. – Brad Dec 10 '19 at 04:19

1 Answers1

7

There is now a mediacapture-output WG and they did define an extension to the MediaDevices interface which adds a selectAudioOutput() method which takes care of the internal permission requests.
This method would return a deviceInfo object from where you can extract the deviceId you would pass to HTMLMediaElement.setSinkId().

const deviceInfo = await navigator.mediaDevices.selectAudioOuput();
audio_elem.setSinkId(deviceInfo.deviceId);

Note also that there is active discussions about renaming this setSinkId method to setAudioOutput, so it might change again when browsers will start implementing all this.

Firefox 93 starts exposing this method, under the media.setsinkid.enabled flag.



Alternatively there should be a Permissions.request() method, which should accept a PermissionDescriptor object which itself should support a PermissionName of value "speaker-selection".

So that would give us

 await navigator.permissions.request( { name: "speaker-selection" } );
 const all_devices = await navigator.mediaDevices.enumerateDevices();
 const audio_outs = all_devices.filter( ({ kind }) => kind === "audiooutput" );
 // ...
 audioElem.setSinkId( audio_outs[ n ].deviceId );


But as of today August of 2021, it seems that still no browser supports this.

Chrome (which is the only one currently supporting MediaElement.setSinkId()) does expose the Permission.request() method under chrome://flags/#enable-experimental-web-platform-features, but they still don't support the PermissionName "speaker-selection" yet.

So we still need the less than optimal workaround of requesting for the "microphone" one instead.

(async () => {
  const query = await navigator.permissions.query( { name: "microphone" } );
  switch( query.state ) {
    case "denied": throw new Error('denied');
    case "prompt":
      await queryUserMedia();
      await getListOfDevices();
      break;
    case "granted": await getListOfDevices();
  }
  function queryUserMedia() {
    return navigator.mediaDevices.getUserMedia( { audio: true } );
  }
  async function getListOfDevices() {
    const all_devs = await navigator.mediaDevices.enumerateDevices();
    const audioouts = all_devs.filter( ({ kind }) => kind === "audiooutput" );
    const options = audioouts.map( ( dev, index ) => {
      const name = dev.label || ('audio out ' + index );
      return new Option( name , dev.deviceId );
    } );
    sel.append.apply( sel, options );
    sel.onchange = e => aud.setSinkId( sel.value );
  }

})().catch( console.error );

As a fiddle since Stack-Snippets iframes won't allow for the use of the microphone.


Ps: Note that if/when browsers were supporting the specs defined API, then it would even be possible to run this code on non-secure contexts.

Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • 1
    Bummer! I was really hoping I was just reading this spec wrong. In any case, thanks for confirming that it isn't supported today. I'll stick with `getUserMedia()` for now. – Brad Dec 10 '19 at 05:29
  • Do you by chance have a link to a Chromium bug, so we can follow it? Also, I tossed a bounty on a similar question here: https://stackoverflow.com/q/59131286/362536 – Brad Dec 10 '19 at 05:33
  • Already saw that other question.. Unfortunately I couldn't think of anything better than my poor suggestions in the comments... For crbugs, [here](https://bugs.chromium.org/p/chromium/issues/detail?id=510405) is the one about implementing `request` and `revoke` but I don't think `"speaker"` is tracked anywhere yet (the only mention when [searching for it](https://bugs.chromium.org/p/chromium/issues/list?q=component%3ABlink%3EPermissionsAPI%20speaker) being this [unrelated crbug](https://bugs.chromium.org/p/chromium/issues/detail?id=938810)) – Kaiido Dec 10 '19 at 05:39
  • 1
    The spec now calls this permission `speaker-selection` and it is starting to be implemented (e.g. appears in Firefox 92) – Hamish Willee Aug 13 '21 at 03:20
  • Ah now I see it, `navigator.mediaDevices.selectAudioOutput` now appears with `media.setsinkid.enabled` flag on. Do you know if there is an umbrella bug we could follow @HamishWillee – Kaiido Aug 13 '21 at 03:52
  • Not as far as I know. There is no evidence of it in Chrome or Webkit right now. – Hamish Willee Aug 14 '21 at 04:24