JSFiddle - React, Tailwind, and code Playground

HTML

<video id="left-flix" controls autoplay style="width: 320px; border-bottom: 1px solid #ccc;"></video>
<video id="right-flix" controls autoplay style="width: 320px; border-bottom: 1px solid #ccc;"></video>
<div id="errors" style="color: red;"></div>
<div>
    <h1>Use Firefox 59 or later.</h1>
    <p>click buttons from left to right</p>
    <div>
        <h3>Working</h3>
        <button onclick="setup()">setup</button>
        <button onclick="addStream()">add stream</button>
        <button onclick="runSignaling()">signaling!</button>
        <button onclick="removeStream()">remove stream</button>
        <button onclick="tearDown()">tearDown</button>
    </div>

    <div>
        <h3>Broken</h3>
        <button onclick="setup()">setup</button>
        <button onclick="runSignaling()">signaling!</button>
        <button onclick="addStream()">add stream</button>
        <button onclick="runSignaling()">signaling!</button>
        <button onclick="removeStream()">remove stream</button>
        <button onclick="tearDown()">tearDown</button>
    </div>


    <h2>Problem</h2>
    <ol>
        <li>send an offer without any streams first</li>
        <li>add a stream to the local peer connection and send an offer</li>
        <li>remote does not display the stream, although it receives data</li>
        <li>removing the upstream throws an exception</li>
    </ol>

    <h2>Expected</h2>
    <ul>
        <li>you can send as many offers with/without stream as you like</li>
        <li>removing stream from a peer connection does not throw</li>
    </ul>
</div>

JavaScript

var local;
var remote;

var upstreamVideo = document.getElementById('left-flix');
var downstreamVideo = document.getElementById('right-flix');
var err = document.getElementById('errors');

function getVideo() {
    return navigator.mediaDevices.getUserMedia({video: true});
}

function setup() {
    local = new RTCPeerConnection(null);
    remote = new RTCPeerConnection(null);

    local.onicecandidate = function(candidate) {
        remote.addIceCandidate(candidate.candidate);
    };

    remote.onicecandidate = function(candidate) {
        local.addIceCandidate(candidate.candidate);
    };

    remote.ontrack = function(event) {
    		console.info('got downstream', event.streams[0]);
        downstreamVideo.srcObject = event.streams[0];
    };
}

function signaling() {
    return local.createOffer({offerToReceiveVideo: 1})
        .then(function(offer){
            local.setLocalDescription(offer);
            return remote.setRemoteDescription(offer);
        })
        .then(function() {
            return remote.createAnswer();
        })
        .then(function(answer){
            remote.setLocalDescription(answer);
            local.setRemoteDescription(answer);
        });
}

function addStream() {
    return getVideo().then(function(stream){
        upstreamVideo.srcObject = stream;
        stream.getTracks().forEach(function(track) {local.addTrack(track, stream)});
    });
}

function removeStream() {
    local.getSenders().forEach(function(sender) {local.removeTrack(sender);});
}

function tearDown() {
    local.close();
    remote.close();
    upstreamVideo.srcObject = null;
    downstreamVideo.srcObject = null;
}

window.setup = setup;
window.addStream = addStream;
window.removeStream = removeStream;
window.runSignaling = signaling;
window.tearDown = tearDown;
window.onerror = function(...error) {
    err.innerHTML += error + '<br>';
};