Code examples: Existing API and JSEP

Hi

I've done some JS coding to compare the API changes introduces by JSEP with the
existing API (with e.g. ROAP under the hood. Basically all the work is done in
the configurePeer() method. There are two versions of that method in the code
below; on  for the old API and then a version with JSEP. (The version with
the old API runs in the Ericsson prototype if the first argument getUserMedia
is changed to a string.)

The basic flow of the application is:
1. configurePeer() is called either with or without an initial offer; if there's
   an initial offer we're being called, otherwise we're initiating a call.
2. configurePeer() creates a PeerConnection object and sets up the necessary
   event listeners.
3. The signaling channel is re-configured (from accepting a call) to handle
   incoming signaling messages.
4. If we're the caller the initiate a new call, otherwise we handle an incoming
   call.

<script>
// shared code
var pc;
var addButton;
var selfView;
var remoteView;

var signalingChannel;

window.onload = function () {
    document.getElementById("call_button").onclick = initiateCall;
    addButton = document.getElementById("add_button");

    selfView = document.getElementById("self_view");
    remoteView = document.getElementById("remote_view");

    signalingChannel = new SignalingChannel();

    // setup message handler to handle an incoming call
    signalingChannel.onmessage = function (evt) {
        configurePeer(evt.data);
    };
};

function initiateCall() {
    configurePeer();
}

// old API (with ROAP)
function configurePeer(initialOffer) {
    navigator.webkitGetUserMedia({"audio": true, "video": true}, function (localStream) {
        // create a new PeerConnection object and direct all generated signaling messages
        // to the other peer via the signaling channel
        pc = new webkitPeerConnection("", signalingChannel.send);

        // reset message handler to feed incoming signaling messages to PeerConnection
        signalingChannel.onmessage = function (evt) {
            pc.processSignalingMessage(evt.data);
        };

        // once remote stream arrives, show it in the remote view video element
        pc.onaddstream = function (evt) {
            remoteView.src = webkitURL.createObjectURL(evt.stream);
        };

        // once the PeerConnection is open, prepare for adding a new stream to call 
        pc.onopen = function () {
            addButton.onclick = function () {
                navigator.webkitGetUserMedia({"audio": true, "video": true}, function (localStream) {
                    // show the new stream in the self-view
                    selfView.src = webkitURL.createObjectURL(localStream);

                    // add the new local stream  to be sent
                    pc.addStream(localStream);
                });
            };
        };

        // if we have a initial offer (we're being called), process it
        if (initialOffer)
            pc.processSignalingMessage(initialOffer);

        // show local stream as self-view
        selfView.src = webkitURL.createObjectURL(localStream);

        // add local stream to be sent
        pc.addStream(localStream);
    });
}

// JSEP
function configurePeer(initialOffer) {
    navigator.webkitGetUserMedia({"audio": true, "video": true}, function (localStream) {
        var updateOffer;
        var hasOutstandingOffer = false;
        var hasPendingStreamsToOffer = false;
        var calleeIceStarted = false;

        // create a new PeerConnection object and direct all generated ice candidate messages
        // to the other peer via the signaling channel
        pc = new webkitPeerConnection("", function (candidate) {
            signalingChannel.send(JSON.stringify({ "type": "candidate", "candidate": candidate }));
        });

        function createAndSendUpdateOffer() {
            // create a new updated sdp offer and send it to the other peer via the signaling channel
            // (we can't call setLocalDescription() rigth away so we store the sdp offer in local variable)
            updateOffer = pc.createOffer();
            signalingChannel.send(JSON.stringify({ "type": "offer", "sdp": updateOffer }));

            // mark that we have an outstanding offer
            hasOutstandingOffer = true;
        }

        // reset message handler to handle incoming signaling messages
        signalingChannel.onmessage = function (evt) {
            var msg = JSON.parse(evt.data);

            if (msg.type == "candidate") {
                // feed any incoming candidates to our PeerConnection
                pc.processIceMessage(msg.candidate);

                // time for callee to start ice processing
                if (initialOffer && !calleeIceStarted) {
                    pc.connect();
                    calleeIceStarted = true;
                }

            } else if (msg.type == "offer") {
                // feed the sdp offer into PeerConnection
                pc.setRemoteDescription(PeerConnection.SDP_OFFER, msg.sdp);

                // create an sdp answer based on the offer and set it as our local description
                var answer = pc.createAnswer(msg.sdp);
                pc.setLocalDescription(PeerConnection.SDP_ANSWER, answer);

                // send the answer via the signaling channel
                signalingChannel.send(JSON.stringify({ "type": "answer", "sdp": answer }));

            } else if (msg.type == "answer") {
                // if the answer corresponds to an updated offer (i.e. not the initial offer),
                // we need to process both the updated offer as well as the received answer
                if (!msg.initialAnswer)
                    pc.setLocalDescription(PeerConnection.SDP_OFFER, updateOffer);
                pc.setRemoteDescription(PeerConnection.SDP_ANSWER, msg.sdp);

                // mark that we have received an answer and no longer has an outstanding offer
                hasOutstandingOffer = false;

                // offer any pending streams added while we had an outstanding offer
                if (hasPendingStreamsToOffer) {
                    createAndSendUpdateOffer();
                    hasPendingStreamsToOffer = false;
                }
            }
        };

        // once remote stream arrives, show it in the remote view video element
        pc.onaddstream = function (evt) {
            remoteView.src = webkitURL.createObjectURL(evt.stream);
        };

        // once the PeerConnection is open, prepare for adding a new stream to call
        pc.onopen = function () {
            addButton.onclick = function () {
                navigator.webkitGetUserMedia({"audio": true, "video": true}, function (localStream) {
                    // show the new stream in the self-view
                    selfView.src = webkitURL.createObjectURL(localStream);

                    // add the new local stream  to be sent
                    pc.addStream(localStream);

                    // check if we have an outstanding offer (i.e. we can't send a new one until
                    // we have received an answer)
                    if (hasOutstandingOffer) {
                        hasPendingStreamsToOffer = true;
                        return;
                    }

                    createAndSendUpdateOffer();
                });
            };
        };

        // show local stream as self-view
        selfView.src = webkitURL.createObjectURL(localStream);

        // if we have an initial offer, we're being called; otherwise we're initiating a call
        if (initialOffer) {
            // parse the incoming offer
            var offer = JSON.parse(initialOffer);

            // feed the sdp offer into PeerConnection
            pc.setRemoteDescription(PeerConnection.SDP_OFFER, offer.sdp);

            // add the reply stream to be sent
            pc.addStream(localStream);

            // create an sdp answer based on the offer and set it as our local description
            var answer = pc.createAnswer(offer.sdp);
            pc.setLocalDescription(PeerConnection.SDP_ANSWER, answer);

            // send the answer via the signaling channel (mark it as an anser to the initial offer)
            signalingChannel.send(JSON.stringify({ "type": "answer", "initialAnswer": true, "sdp": answer }));
        } else { // we're initiating a call
            // add the stream to be sent
            pc.addStream(localStream);

            // create the initial sdp offer and set it as our local description
            var offer = pc.createOffer();
            pc.setLocalDescription(PeerConnection.SDP_OFFER, offer);

            // send the offer to the other peer via the signaling channel
            signalingChannel.send(JSON.stringify({ "type": "offer", "sdp": offer }));

            // start gathering candidates
            pc.connect();
        }
    });
}

</script>

Some comments:

The API changes that JSEP introduces makes it quite a bit more complicated to work
with compared to the existing API (with e.g. ROAP under the hood). For example:

- More code for a simple example (e.g. to be presented as an example in the spec).

- JSEP has five ways to feed signaling information to PeerConnection compared to
one in the ROAP case. You really need to know what you're getting on the signaling
channel.

- Special cases such as glare and multiple offers have to be handled in JavaScript.
A simple example with the existing API (and ROAP) is quite powerful sice special
cases are handled under the hood.

It's also a question if PeerConnection.createOffer() and PeerConnection.createAnswer()
actually can have a return value? If the browser needs to reach down into the platform
to gather information these methods may need to be async and use a callback.

----
BR
Adam

Received on Thursday, 26 January 2012 12:42:18 UTC