Bootlin logo

Elixir Cross Referencer

<!doctype html>
<html>
<head>
<title>One tab p2p</title>

<style type="text/css">
    video { width: 240px; height: 160px; border: black 1px dashed; }
    input { margin: 2px }
</style>

<script>
// Make use of prefixed APIs to run this test in Chrome and Firefox
self.RTCPeerConnection = self.RTCPeerConnection || self.webkitRTCPeerConnection || self.mozRTCPeerConnection;
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;

let legacyCheckBox;
let closeButton;
let pcA;
let pcB;
let localStream;

const pcNames = {
    first: "A",
    second: "B"
};

// FIXME: We should be able to use an empty configuration (bug: http://webkit.org/b/158936)
const configuration = { "iceServers": [{ "urls": "stun:mmt-stun.verkstad.net" }] };

document.addEventListener("DOMContentLoaded", function () {
    legacyCheckBox = document.querySelector("#legacy_check");
    const audioCheckBox = document.querySelector("#audio_check");
    const videoCheckBox = document.querySelector("#video_check");

    const startButton = document.querySelector("#start_but");
    closeButton = document.querySelector("#close_but");

    const testButtons = {
        "single": document.querySelector("#single_but"),
        "mediaAtoB": document.querySelector("#media_A_to_B_but"),
        "mediaBtoA": document.querySelector("#media_B_to_A_but")
    };

    function setTestButtonsDisabled(isDisabled) {
        for (let p in testButtons)
            testButtons[p].disabled = isDisabled;
    }

    startButton.onclick = function () {
        navigator.getUserMedia({
            "audio": audioCheckBox.checked,
            "video": videoCheckBox.checked
        }, function (stream) {
            audioCheckBox.disabled = videoCheckBox.disabled = true;
            localStream = stream;
            startButton.disabled = true;
            setTestButtonsDisabled(false);
        }, logError);
    };

    closeButton.onclick = function (evt) {
        evt.target.disabled = true;
        console.log("Closing");
        pcA.close();
        pcB.close();
        pcA = null;
        pcB = null;

        setTestButtonsDisabled(false);
    }

    testButtons.single.onclick = function (evt) {
        setTestButtonsDisabled(true);
        getTestFunction("singleDialog")();
    }

    testButtons.mediaAtoB.onclick = function (evt) {
        setTestButtonsDisabled(true);
        if (!pcA)
            commonSetup();
        getTestFunction("addOneWayMedia")(pcA, pcB, testButtons.mediaBtoA);
    }

    testButtons.mediaBtoA.onclick = function (evt) {
        setTestButtonsDisabled(true);
        if (!pcA)
            commonSetup();
        getTestFunction("addOneWayMedia")(pcB, pcA, testButtons.mediaAtoB);
    }
});

function getTestFunction(name) {
    const functionName = legacyCheckBox.checked ? name : `${name}Promise`;
    return self[functionName];
}

function singleDialog() {
    commonSetup();

    renderStream(localStream, document.querySelector("#self_viewA"));
    pcA.addStream(localStream);

    pcA.createOffer(function (offer) {
        pcA.setLocalDescription(offer, function () {
            offerToB(pcA.localDescription);
        }, logError);
    }, logError);

    function offerToB(offer) {
        logSignalling(offer, pcA, pcB);
        pcB.setRemoteDescription(offer, function () {
            addStoredCandidates(pcB);
            renderStream(localStream, document.querySelector("#self_viewB"));
            pcB.addStream(localStream);

            pcB.createAnswer(function (answer) {
                pcB.setLocalDescription(answer, function () {
                    answerToA(pcB.localDescription);
                }, logError);
            }, logError);
        }, logError);
    }

    function answerToA(answer) {
        logSignalling(answer, pcB, pcA);
        pcA.setRemoteDescription(answer, function () {
            console.log("Initiator got answer, O/A dialog completed");
            addStoredCandidates(pcA);
            closeButton.disabled = false;
        }, logError);
    }
}

function singleDialogPromise() {
    commonSetup();

    renderStream(localStream, document.querySelector("#self_viewA"));
    localStream.getTracks().forEach(track => {
        pcA.addTrack(track, localStream);
    });

    pcA.createOffer().then(function (offer) {
        return pcA.setLocalDescription(offer);
    })
    .then(function () {
        logSignalling(pcA.localDescription, pcA, pcB);
        return pcB.setRemoteDescription(pcA.localDescription);
    })
    .then(function () {
        addStoredCandidates(pcB);
        renderStream(localStream, document.querySelector("#self_viewB"));
        localStream.getTracks().forEach(track => {
            pcB.addTrack(track, localStream);
        });
        return pcB.createAnswer();
    })
    .then(function (answer) {
        return pcB.setLocalDescription(answer);
    })
    .then(function () {
        logSignalling(pcB.localDescription, pcB, pcA);
        return pcA.setRemoteDescription(pcB.localDescription);
    })
    .then(function () {
        addStoredCandidates(pcA);
        console.log("Initiator got answer, O/A dialog completed");
        closeButton.disabled = false;
    })
    .catch(logError);
}

function addOneWayMedia(offeringPc, answeringPc, continueButton) {
    renderStream(localStream, document.querySelector(`#self_view${offeringPc.name}`));
    offeringPc.addStream(localStream);

    offeringPc.createOffer(function (offer) {
        offeringPc.setLocalDescription(offer, function () {
            offerToAnsweringPc(offeringPc.localDescription);
        }, logError);
    }, logError);

    function offerToAnsweringPc(offer) {
        logSignalling(offer, offeringPc, answeringPc);
        answeringPc.setRemoteDescription(offer, function () {
            addStoredCandidates(answeringPc);
            answeringPc.createAnswer(function (answer) {
                answeringPc.setLocalDescription(answer, function () {
                    answerToOfferingPc(answeringPc.localDescription);
                }, logError);
            }, logError);
        }, logError);
    }

    function answerToOfferingPc(answer) {
        logSignalling(answer, answeringPc, offeringPc);
        offeringPc.setRemoteDescription(answer, function () {
            console.log("Initiator side got answer, single way O/A dialog completed");
            addStoredCandidates(offeringPc);
            continueButton.disabled = false;
            closeButton.disabled = false;
        }, logError);
    }
}

function addOneWayMediaPromise(offeringPc, answeringPc, continueButton) {
    renderStream(localStream, document.querySelector(`#self_view${offeringPc.name}`));
    localStream.getTracks().forEach(track => {
        offeringPc.addTrack(track, localStream);
    });

    offeringPc.createOffer().then(function (offer) {
        return offeringPc.setLocalDescription(offer);
    })
    .then(function () {
        logSignalling(offeringPc.localDescription, offeringPc, answeringPc);
        return answeringPc.setRemoteDescription(offeringPc.localDescription);
    })
    .then(function () {
        addStoredCandidates(answeringPc);
        return answeringPc.createAnswer();
    })
    .then(function (answer) {
        return answeringPc.setLocalDescription(answer)
    })
    .then(function () {
        logSignalling(answeringPc.localDescription, answeringPc, offeringPc);
        return offeringPc.setRemoteDescription(answeringPc.localDescription)
    })
    .then(function () {
        console.log("Initiator side got answer, single way O/A dialog completed");
        addStoredCandidates(offeringPc);
        continueButton.disabled = false;
        closeButton.disabled = false;
    })
    .catch(logError);
}

function commonSetup() {
    pcA = new RTCPeerConnection(configuration);
    pcB = new RTCPeerConnection(configuration);

    pcA.name = pcNames.first;
    pcB.name = pcNames.second;

    symetricSetup(pcA, pcB);
    symetricSetup(pcB, pcA);
}

function addStoredCandidates(pc) {
    if (!pc.storedCandidates)
        return;

    pc.storedCandidates.forEach(candidate => {
        pc.addIceCandidate(candidate).catch(logError);
    });

    console.log(`Added ${pc.storedCandidates.length} stored candidates (arrived before remote description was set)`);
    pc.storedCandidates = null;
}

function symetricSetup(pc, otherPc) {
    pc.onicecandidate = function (evt) {
        if (evt.candidate) {
            logSignalling(evt.candidate, pc, otherPc);
            // If the remote description isn't set yet, store the candidate
            if (!otherPc.remoteDescription) {
                if (!otherPc.storedCandidates)
                    otherPc.storedCandidates = [];
                otherPc.storedCandidates.push(evt.candidate);
            } else
                otherPc.addIceCandidate(evt.candidate).catch(logError);
        }
    };

    pc.onaddstream = function (evt) {
        renderStream(evt.stream, document.querySelector(`#remote_view${pc.name}`));
    };
}

function renderStream(stream, video) {
    if (typeof video.srcObject !== "undefined")
        video.srcObject = stream;
    else
        video.src = URL.createObjectURL(stream);
}

function logSignalling(msg, fromPc, toPc) {
    const type = msg.candidate ? "Candidate" : msg.type.replace(/^[a-z]/, s => s.toUpperCase());
    let header = `${type} `;
    header += fromPc.name == pcNames.first ? `${fromPc.name} -> ${toPc.name}` : `${toPc.name} <- ${fromPc.name}`;
    console.groupCollapsed(header);
    console.log(msg.candidate || msg.sdp);
    console.groupEnd();
}

function logError(error) {
    if (error) {
        if (error.name || error.message)
            console.error(`logError: ${error.name || "-"}: ${error.message || "-"}`);
        else
            console.error(`logError: ${error}`);
    } else
        console.error("logError: (no error message)");
}
</script>

</head>
<body>
<h3>One Tab P2P - Test Different Signaling Schemas</h3>
<p>Click start to request user media. The same stream is sent in both directions so a successful
bidirectional media setup shows the same output in all four video elements. Open console to view
signaling details. Some browsers only allow access to user media via a secure origin (e.g.
localhost).</p>
<input type="checkbox" id="legacy_check">Use Legacy APIs (Chrome compatible)<br>
<input type="checkbox" id="audio_check">Audio<br>
<input type="checkbox" id="video_check" checked>Video<br>

<input type="button" id="start_but" value="Start">
<input type="button" id="close_but" value="Close Connections" disabled>
<br>
Setup bidirectional media: <input type="button" id="single_but" value="Single SDP dialog" disabled>
<br>
Setup media in one direction at a time: <input type="button" id="media_A_to_B_but" value="Media A to B" disabled>
<input type="button" id="media_B_to_A_but" value="Media B to A" disabled>
<br>

<table>
    <tr>
        <td>Local (A)</td><td>Remote (A)</td>
    </tr>
    <tr>
        <td><video id="self_viewA" autoplay muted></video></td>
        <td><video id="remote_viewA" autoplay></video></td>
    </tr>
    <tr>
        <td>Local (B)</td><td>Remote (B)</td>
    </tr>
    <tr>
        <td><video id="self_viewB" autoplay muted></video></td>
        <td><video id="remote_viewB" autoplay></video></td>
    </tr>
</table>
</body>
</html>