Search code examples
javascriptgoogle-chromeaudiowebrtcpeerjs

Why does audio automatically play after a peer accepts the call?


When I have two peers that call each other using peerjs, once a peer accepts the call, audio from the peer that initiated the call, starts playing even though I do not explicitly set autoplay or call play(). This problem occurs in the chrome browser.

<!DOCTYPE html>
<html>
    <head>
        <script src="https://unpkg.com/[email protected]/dist/peerjs.min.js"></script>
    </head>
    <body>
        <h1 id='peerid'></h1>
        <input type='text' id='in'/>
        <button onclick="call()">Call</button>
        <button onclick="debug()">debug</button>
        <button onclick="printtime()">time</button>
        <button onclick="printauto()">auto</button>
    </body>
        <script type='text/javascript'>
            let a = new Audio();
            a.onplay = () => {
                console.log('play');
            };
            a.onplaying = () => {
                console.log('playing');
            };
            a.onseeking = () => {
                console.log('seeking');
            };
            a.onseeked = () => {
                console.log('seeked');
            };
            let m = new MediaStream();
            a.srcObject = m;
            let header = document.getElementById('peerid');
            let i = document.getElementById('in');
            let peer = new Peer();
            peer.on('open', (id) => {
                header.innerHTML = id;
            });
            peer.on('call', (call) => {
                call.answer();
                call.on('stream', (stream) => {
                    stream.getAudioTracks().forEach(track => m.addTrack(track));
                });
            });
            let call = () => {
                navigator.mediaDevices.getUserMedia({ audio: true }).then(mediaStream => {
                    let call = peer.call(i.value, mediaStream);
                    call.on('stream', (stream) => {
                        stream.getAudioTracks().forEach(track => m.addTrack(track));
                    });
                }, rejected => {
                    console.log('rejected');
                }).catch(e => console.error(e));
            };
            let debug = () => {
                console.log(a.paused);
            };
            let printtime = () => {
                console.log(a.currentTime);
            };
            let printauto = () => {
                console.log(a.autoplay);
            };
        </script>
</html>

To replicate this,

  1. serve the html code above with a webserver (ex: python3 -m http.server)
  2. open two tabs that serve this page
  3. Copy the identifier in bold from one tab into the other and click "call"
  4. It should ask to have access to your microphone (it might take awhile so give it a sec, or just spam the call button)
  5. Once it has access to your microphone, the other tab should automatically play audio (the audio would be the sound of your own voice) without the audio element having autoplay as true or calling play() anywhere as you can see

I do not know why this happens. If you print out if the audio is paused or if autoplay is on, they both print that paused is true (even though audio is still playing) and that autoplay is false (even though audio is still playing). None of the events fire either. Is this a chrome bug? or am I just dumb? (I am assuming the latter)


Solution

  • This is a Chrome bug, I opened https://crbug.com/1279299.
    The gist of the bug is that adding an audio track on a MediaSource set to the srcObject of an HTMLAudioElement will force autoplay that MediaStream, even if the element never actually told it to...

    So a smaller repro can be done like so:

    const a = new Audio();
    const m = new MediaStream();
    a.srcObject = m;
    a.volume = 0.1; // volume still works...
    
    onclick = e => {
      const ctx = new AudioContext();
      const osc = ctx.createOscillator();
      const out = ctx.createMediaStreamDestination();
      osc.connect(out);
      osc.start(0);
      out.stream.getAudioTracks().forEach(t => m.addTrack(t));
    };
    Click to start

    Knowing this, the workaround is quite obvious too: Don't set your element's srcObject to this empty MediaStream, set it directly to the one stream you receive from your peer:

    call.on('stream', (stream) => {
      a.srcObject = stream;
    });