0

Trying to setSinkId on an audio node. I have noticed setSinkId only works in very specific circumstances and I need clarification. Examples behave similar in latest firefox and chrome.

This works:

index.html
<audio id="audio"></audio>

app.js
this.audio = document.getElementById('audio');
this.audio.src =... and .play()
this.audio.setSinkId(this.deviceId);

This is not OK beyond testing as now every player will be sharing a node. They each need a unique one.

This does not:

app.js
this.audio = new Audio();
this.audio.src =... and .play()
this.audio.setSinkId(this.deviceId)

This also doesn't work

app.js
this.audio = document.createElement('audio');
document.body.appendChild(this.audio);
this.audio.src =... and .play()
this.audio.setSinkId(this.deviceId)

Are there differences between new Audio, createElement, and audio present in HTML? Why doesn't setSinkId work on a new Audio()?

Kyle Ross
  • 1
  • 1
  • 1
  • Hi Kyle and welcome SO. Can you expand on "doesn't work" for your non-working cases? What is your evidence? Does, for instance `this.audio.sinkId` update after calling `setSinkId`? – spender Apr 17 '20 at 16:16
  • I have a headset and laptop speakers. Headset is the primary audio destination in windows. Using the output from enumerateDevices to switch between laptop and headset. When I say "doesn't work" I mean the audio stays routed to the default destination (headset). Yes, the sink id does change. The promise returned (in chrome) is resolved OK and there are no errors. – Kyle Ross Apr 17 '20 at 17:05
  • Maybe `chrome://media-internals/` might reveal what's happening. This looks relevant: https://bugs.chromium.org/p/chromium/issues/detail?id=930876 ... some-way down the page "A workaround for this issue is to not call setSinkId() until after the canplay event." – spender Apr 18 '20 at 00:33

3 Answers3

2

For me it works both in Chrome and in Firefox (after I changed the prefs), for as long as I do request the permissions before-hand, and intialize the audio through an user-gesture.

For requesting access to the audio-output will become as easy as calling navigator.mediaDevices.selectAudioOutput() and let the user choose what they want, but until this is implemented, we still have to hack-around by requesting a media input instead, as explained in this answer.

So this script should do it:

btn.onclick = async (evt) => {
  // request device access the bad way,
  // until we get a proper mediaDevices.selectAudioOutput
  (await navigator.mediaDevices.getUserMedia({audio:true}))
      .getTracks().forEach( (track) => track.stop());
  
  const devices = await navigator.mediaDevices.enumerateDevices();
  const audio_outputs = devices.filter( (device) => device.kind === "audiooutput" );
  
  const sel = document.createElement("select");
  audio_outputs.forEach( (device, i) => sel.append(new Option( device.label || `device ${i}`, device.deviceId )) );
  document.body.append(sel);
  
  btn.textContent = "play audio in selected output";
  btn.onclick = (evt) => {
    const aud = new Audio(AUDIO_SRC);
    aud.setSinkId(sel.value);
    aud.play();
  };
}

But since StackSnippets don't allow gUM calls, we have to outsource it, so here is a live fiddle.

Kaiido
  • 123,334
  • 13
  • 219
  • 285
0

The setSinkId function returns Promise, so it must be handled with then... catch or asynchronous function (async ... await)

Here is a code sample :

this.audio.setSinkId(this.deviceId).then(() => {
    // here is the code to run after connecting to the device
}).cacth(error => {
   // code if error  
})
-1

It looks like when you create a new audio it takes some time to play media and be ready to use setSinkId. My solution (I don't like it too much, but is what I have right now) is to use a setTimeout function with one second or less. Something like this:

Replace

this.audio.setSinkId(this.deviceId)

By

setTimeout(() => { 
    this.audio.setSinkId(this.deviceId)
  }, 1000);