Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaScript get permission for Audio.setSinkId

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?)
like image 665
Gerrit Avatar asked Feb 18 '18 16:02

Gerrit


1 Answers

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.

like image 68
Kaiido Avatar answered Nov 14 '22 21:11

Kaiido