On macOS, when routing a mono MediaStream through a MediaStreamAudioSourceNode to a PannerNode in Firefox, the OS output becomes mono.

On macOS, when routing a mono MediaStream through a MediaStreamAudioSourceNode to a PannerNode in Firefox, the OS output becomes mono.

When spatializing browser audio with PannerNode, I ran into a symptom where only Firefox on macOS becomes mono at the OS output. After narrowing it down step by step with minimal reproduction code, I found that Firefox loses stereo at the OS output only when a mono MediaStream is passed through a MediaStreamAudioSourceNode.
2026.05.15

This page has been translated by machine translation. View original

Introduction

While building a prototype for exchanging audio between browsers, I encountered a symptom where spatial audio left/right panning was not reflected in the OS output when using Firefox on macOS as the receiver. The L/R monitor built into the app showed the audio moving left and right.

app monitor

In OBS's captured waveform and by ear, it becomes mono.

obs monitor

Upon investigation, I found that in Firefox on macOS, when a mono MediaStream is passed through a MediaStreamAudioSourceNode into a PannerNode, the OS audio output becomes mono even though panning is being calculated within the in-app Web Audio chain. This can be avoided by creating the input source MediaStream as stereo from the start, which allows the same MediaStreamAudioSourceNode → PannerNode path to work correctly.

Test Environment

  • macOS
  • Firefox 150.0.3 (64-bit)
  • Chrome 148.0.7778.168 (arm64)
  • Desktop audio captured with OBS Studio
  • Listening confirmed on built-in speakers
  • Verified on 2026-05-15

Target Audience

  • People who tried spatial audio with the Web Audio API and encountered a symptom where left/right panning only doesn't work in Firefox
  • People passing audio received via WebRTC through a PannerNode
  • People who want to confirm a procedure for isolating the issue with a minimal reproduction before concluding it's a browser problem

References

Isolating the Issue with a Minimal Reproduction

By building an L/R monitor into the app and simultaneously capturing desktop audio with OBS, I was able to independently observe the behavior inside the Web Audio chain and at the OS output.

I compare three paths.

In addition to these, I also prepared a path where a ChannelMerger(2) is inserted after the microphone mono path to convert it to stereo (microphone mono + ChannelMerger expansion), and a path with audio processing disabled in getUserMedia (microphone mono + audio processing off). I ran each path in Firefox and Chrome, and the following table shows the results comparing the L/R monitor and OBS behavior when the panner X coordinate was set to -3 and +3.

Path In-app L/R monitor OBS waveform and listening
OscillatorNode direct connection Moves left and right Moves left and right
Microphone mono path Moves left and right Mono
Microphone mono + ChannelMerger expansion Moves left and right Mono
Microphone mono + audio processing off ( echoCancellation: false, noiseSuppression: false, autoGainControl: false, channelCount: 2 ) Moves left and right Mono
Synthesized stereo path Moves left and right Moves left and right

What we can learn from this is as follows.

  • Since OscillatorNode direct connection worked in both browsers, the panning calculation of PannerNode itself works in Firefox as well.
  • The symptom was reproduced in the microphone mono path. The only difference from the OscillatorNode direct connection is whether it goes "direct from OscillatorNode" or "via MediaStreamAudioSourceNode".
  • For microphone mono + ChannelMerger expansion and microphone mono + audio processing off, neither the downstream stereo expansion nor turning off getUserMedia audio processing (AEC / NS / AGC) had any effect. Since the macOS built-in microphone is mono hardware, even requesting channelCount: 2 leaves sourceChannelCount at 1. These values were confirmed using getSettings().

In other words, it can be said that no matter what is done downstream, the OS output mono conversion cannot be undone.

Key part of the microphone mono path
experiments/firefox-panning-min/04-mediastream-mic.html
const micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
const audioCtx = new AudioContext();
const { masterGain, analyserL, analyserR } = buildMasterChain(audioCtx);

const source = audioCtx.createMediaStreamSource(micStream);
const panner = audioCtx.createPanner();
panner.panningModel = 'equalpower';
panner.distanceModel = 'inverse';
source.connect(panner);
panner.connect(masterGain);
// Update panner.positionX etc. each time the panner X coordinate is changed

It Works with a Stereo MediaStream

I changed the path with the synthesized stereo route. Without using a microphone, two OscillatorNodes (440Hz and 880Hz) are merged into stereo using ChannelMerger(2) and fed into a MediaStreamAudioDestinationNode, creating a stereo MediaStream from the input source stage. This is then received again with MediaStreamAudioSourceNode and passed through a PannerNode. The MediaStreamAudioSourceNode → PannerNode portion of the path is identical to the microphone mono path; only the channel count of the input MediaStream differs.

Key part of the synthesized stereo path
experiments/firefox-panning-min/08-stereo-mediastream.html
const oscL = audioCtx.createOscillator();
oscL.frequency.value = 440;
const oscR = audioCtx.createOscillator();
oscR.frequency.value = 880;

const merger = audioCtx.createChannelMerger(2);
oscL.connect(merger, 0, 0);
oscR.connect(merger, 0, 1);
oscL.start();
oscR.start();

const dest = audioCtx.createMediaStreamDestination();
merger.connect(dest);

// Re-receive the stereo MediaStream assembled with ChannelMerger via MediaStreamAudioSourceNode
const source = audioCtx.createMediaStreamSource(dest.stream);
const panner = audioCtx.createPanner();
panner.panningModel = 'equalpower';
panner.distanceModel = 'inverse';
source.connect(panner);
panner.connect(masterGain);

When I ran the synthesized stereo path in Firefox 150.0.3 on macOS, both the in-app L/R monitor and OBS waveform showed left/right separation. In other words, even with the same MediaStreamAudioSourceNode → PannerNode path, panning is reflected in the OS output when the input MediaStream is stereo.

From this, the cause becomes clear. Firefox on macOS appears to have some internal processing that "fixes the output path to mono" when MediaStreamAudioSourceNode receives a mono MediaStream, ignoring any channel count changes made in the downstream Web Audio chain. Since the W3C specification for PannerNode defines the output as always stereo, Firefox's behavior on macOS can be considered a specification compliance issue.

When spatializing audio received via WebRTC, the standard Opus codec is mono by default. If the receiver is Firefox on macOS, this issue hits directly. If you are only looking at the in-app L/R monitor, the panning appears to be working, but the OS output will be mono.

Summary

I confirmed that in Firefox on macOS, passing a mono MediaStream through a MediaStreamAudioSourceNode into a PannerNode results in the OS output becoming mono. This can be avoided by creating the input source MediaStream as stereo from the start, which allows the same path to work correctly. Even if panning appears to be working in the in-app monitor, the OS output may behave differently, so when dealing with audio between browsers, it is safer to observe both independently.

I hope this is helpful for those who have encountered a similar issue. Note that the verification in this article was conducted only on Firefox 150.0.3 on macOS, so behavior may differ on other operating systems or Firefox versions.

Share this article