An embedded Eagle 3D Streaming page captures whichever microphone the operating system calls the default. Two commands let your own page offer the viewer a proper picker and switch device mid-session, without dropping the stream.
Without this, the only way a viewer can change input device is the browser's padlock menu or the operating system's sound settings. For anyone who is not comfortable in browser settings that may as well not exist — so the picker belongs on your page, where your users already are.
This is the part that catches people. You cannot get a useful device list before the browser has granted the microphone, and the browser only grants it when something asks — which the Eagle 3D page does when the session connects, not when your page loads.
YOUR PAGE THE PLAYER THE BROWSER
| | |
| (1) page loads | |
| iframe.src set | |
|------------------->| |
| | |
| X asking now is too early: no permission |
| yet, and no audio track to swap |
| | |
| | (2) session connects |
| | getUserMedia(audio)|
| |----------------------->|
| | |
| | +-------------+------+
| | | "Allow microphone?" |
| | +-------------+------+
| | |
| |<-----------------------|
| | granted, default mic |
| | |
| (3) stage5_play... | |
|<-------------------| |
| | |
| (4) getMicDevices | |
|------------------->| enumerateDevices() |
| |----------------------->|
| |<-----------------------|
| micDevices | ids AND labels |
|<-------------------| |
| | |
| (5) viewer picks one |
| setMicDevice { deviceId } |
|------------------->| getUserMedia(deviceId) |
| |----------------------->|
| |<-----------------------|
| | sender.replaceTrack() |
| | oldTrack.stop() |
| micDeviceChanged | |
|<-------------------| no renegotiation: |
| | the session stays up |
Neither command throws. Both answer — and the answer tells you which wall you hit, which is why it is worth reading rather than ignoring.
+------------------------------+
getMicDevices | Has the browser granted the |
| | microphone yet? |
v +--------------+---------------+
no | yes
+-------------------+-------------------+
v v
permissionGranted: false permissionGranted: true
deviceCount: 1 devices: [
devices: [] { deviceId: "z9y8...",
label: "Headset (Realtek)",
the browser answered with ONE isActive: true }
placeholder - deviceId "", ]
label "" - which is not a
device, so it is not listed what you want
The two empty lists mean different things and need different words on your
page. permissionGranted: false with a non-zero
deviceCount means "allow the microphone, then list again" —
there is something to do, and it is not on your page. No devices at all means
there is no microphone, and no amount of allowing will produce one.
+------------------------------+
setMicDevice | Is there an audio sender on |
| | the live connection? |
v +--------------+---------------+
no | yes
+-------------------+-------------------+
v v
ok: false getUserMedia({ deviceId })
error: "no audio sender - |
is the microphone enabled +----------+----------+
for this app?" v v
ok: true ok: false
You asked before the deviceId, label error: "..."
session was up, or
"Enable Browser Microphone" swapped live, see the table
is off in the dashboard session intact below
error carries the browser's own error name,
because the cases call for four different messages on your side.
error | What happened | What to tell the viewer |
|---|---|---|
NotAllowedError |
The viewer has refused the microphone for this site | "Allow the microphone in the padlock menu beside the address bar." Nothing on the page can override this. |
OverconstrainedError |
The chosen device is no longer there | "That microphone was disconnected." Re-list and let them pick again. |
NotFoundError |
There is no matching input device | Re-list. |
NotReadableError |
The device exists but another application has it | "Close whatever else is using the microphone and try again." |
no audio sender… |
There is no audio track on the connection to swap | You asked before the session was up, or the app is not configured with Enable Browser Microphone. |
On success you get back the deviceId as the browser resolved
it, plus the device's label — so your picker can
confirm what it actually got rather than what it asked for.
before setMicDevice after
+------------+ +------------+
| RTCPeerConn| | RTCPeerConn|
| +------+ | sender.replaceTrack(new) | +------+ |
| |sender|--+--> default mic ==========> | |sender|--+--> chosen mic
| +------+ | | +------+ |
+------------+ no renegotiation +------------+
no re-queue
same session ------ no reconnect ------------> same session
replaceTrack() swaps the outgoing track on an established sender
without renegotiating, so the call continues uninterrupted. The old track is
stopped after the new one is acquired and swapped in —
any other order means a device that has been unplugged since the list was
built costs the viewer the microphone they still had.
micDeviceChanged: ok says the command succeeded. It does not, by
itself, prove the audio being sent to your app now comes from the device the
viewer picked — a dropdown that changed its own label would look
identical.
The Eagle 3D page has an information panel that settles it. Open it with the i button on the stream's control bar and find the Microphone block. Its last row is Device, and it is read live from the track the connection is actually sending:
track.getSettings().deviceId // the sender's CURRENT track
Switch device and that row changes. That is the verification — it is the media layer reporting on itself, not the page repeating what it asked for.
| Row | After a successful switch |
|---|---|
| Device | changes to a different id — this is the one that matters |
| Track | stays live — replaceTrack does not renegotiate |
| Packets sent | keeps climbing, no reset — the session never dropped |
| Permission | stays granted |
| Sample rate, Channels, Echo cancellation | may change, if the new device reports different settings |
The player watches the browser's devicechange event and re-sends
micDevices on its own when the machine's inputs change. Plug in a
headset mid-session and your picker refreshes without anyone pressing
anything.
Reset the permissions for both sites, then hard reload. Both steps, in that order — a reset does nothing to the page already open, because the permission was read when it loaded. Resetting without reloading looks exactly like the reset not working.
Click the padlock beside the address bar → Site settings → Reset permissions.
The one people miss, because it never appears in the address bar. In Chrome, go straight to it:
chrome://settings/content/siteDetails?site=https://<your-connector>.eagle3dstreaming.com
The connector hostname is the first part of your stream URL. Set Microphone to Allow, or press Reset permissions to start clean.
Ctrl+Shift+R, or Cmd+Shift+R on a Mac. Then allow the microphone when asked.
| Browser | Where the reset is |
|---|---|
| Chrome, Edge | Padlock → Site settings → Reset permissions. For the Eagle 3D website, use the chrome://settings/content/siteDetails?site= address above. |
| Firefox | Padlock → Clear cookies and site data, or Settings → Privacy & Security → Permissions → Microphone → Settings. |
| Safari | Safari → Settings for This Website, or Settings → Websites → Microphone. |
| What you see | What it means |
|---|---|
| No Microphone row in the padlock menu at all, and "No usage data" in site settings | The browser never recorded a request. It refused before prompting — so there is nothing to reset on that website, and the cause is elsewhere. |
| Padlock says allowed, the stream says not granted | Two different sites. Reset and allow the stream's origin as well. |
NotAllowedError with no prompt ever shown |
Refused by policy or by a previous decision, not by the viewer just now. |
| Devices listed but all unnamed | Permission has not been granted yet in that document. The devices are real; the names are withheld. |
micDeviceChanged: no audio sender |
The session connected before the microphone was allowed, so it has no track to switch. Restart the stream. |
const frame = document.getElementById("streamFrame");
function send(message) {
frame.contentWindow.postMessage(JSON.stringify(message), "*");
}
window.addEventListener("message", (event) => {
const data = event.data;
if (!data || !data.type) return;
// Build the controls only once the video is playing - before that the
// labels are empty and there is no track to swap.
if (data.type === "stage5_playBtnPressed") {
send({ cmd: "getMicDevices" });
}
// Can also arrive unprompted, when a device is plugged in or removed.
if (data.type === "micDevices") {
const select = document.getElementById("micPicker");
select.innerHTML = "";
data.devices.forEach((device, i) => {
const option = document.createElement("option");
option.value = device.deviceId;
option.textContent = device.label || `Microphone ${i + 1}`;
option.selected = device.isActive;
select.appendChild(option);
});
}
if (data.type === "micDeviceChanged") {
if (data.ok) showStatus(`Now using ${data.label}`);
else showStatus(describe(data.error));
}
});
document.getElementById("micPicker").addEventListener("change", (e) => {
send({ cmd: "setMicDevice", deviceId: e.target.value });
});
| Requirement | Where |
|---|---|
The <iframe> carries allow="microphone" |
Your page's HTML. Read only when the frame's document loads — changing it later means reassigning src. |
| Enable Browser Microphone is on | The app's configuration in the Control Panel. Without it there is no audio sender to swap a track on. |
| The viewer allows the microphone when asked | The browser. If they refuse, only they can undo it. |
A complete working example, including the picker and a log of every message in both directions, is in the iframe demo — see its README under Microphone.
Last updated