Remote VideoStream no funciona con WebRTC

EDITAR: escribí un tutorial detallado que explica cómo construir una aplicación de Videochat simple que incluya un servidor de señalización:

Tutorial: Crea tu propia aplicación de Videochat con HTML y JavaScript

Por favor, dime si lo encuentras útil y comprensible. ¡Gracias!

Estoy tratando de hacer que Streams funcione a través de WebRTC y Websocket (nodejs-server). Hasta donde puedo ver el handshake vía SDP funciona y se establece el Peerconnection. El problema es que el video remoto no se está reproduciendo. El src-Attribute obtiene el Blob y se establece la reproducción automática, pero simplemente no se reproduce. Tal vez estoy haciendo algo mal con los candidatos a ICE (se usan para transmisión de medios, ¿no?). ¿Hay alguna forma de verificar si la conexión entre pares está configurada correctamente?

EDITAR: Tal vez debería explicar cómo funciona el código

Al cargar el sitio web, se establece una conexión con el servidor websocket, se crea una conexión entre pares con el servidor STUN de Google y se recopilan y agregan secuencias de audio y video a la conexión entre pares.

Cuando un usuario hace clic en el botón "crear oferta", se envía un mensaje al servidor (función de cliente sendOffer ()) que contiene su descripción de sesión (SDP), que lo transmite al otro usuario.

El otro usuario recibe el mensaje y guarda el SDP que recibió.

Si el usuario hace clic en "aceptar oferta", el SDP se agrega a RemoteDescription (func createAnswer ()) que luego envía un mensaje de respuesta (que contiene el SDP del usuario que responde) al usuario de la oferta.

En el lado del usuario que ofrece la oferta, se ejecuta la función offerAccepted (), que agrega el SDP del otro usuario a su RemoteDesription.

No estoy seguro de en qué punto se llaman exactamente los controladores de icecandidate, pero creo que deberían funcionar porque tengo ambos registros en ambos lados.

Aquí está mi código (esto es solo para pruebas, por lo que incluso si hay una función llamada difusión significa que solo 2 usuarios pueden estar en el mismo sitio web a la vez):

Marca de index.html:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <style>
            #acceptOffer  {
                display: none;
            }
        </style>
    </head>
    <body>
        <h2>Chat</h2>
        <div>
            <textarea class="output" name="" id="" cols="30" rows="10"></textarea>
        </div>
        <button id="createOffer">create Offer</button>
        <button id="acceptOffer">accept Offer</button>

        <h2>My Stream</h2>
        <video id="myStream" autoplay src=""></video>
        <h2>Remote Stream</h2>
        <video id="remoteStream" autoplay src=""></video>

        <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
        <script src="websocketClient.js"></script>
</body>
</html>

Aquí está el código del servidor:

"use strict";

var webSocketsServerPort = 61122;

var webSocketServer = require('websocket').server,
http = require('http'),
clients = [];


var server = http.createServer(function(request, response) {
    // Not important for us. We're writing WebSocket server, not HTTP server
});
server.listen(webSocketsServerPort, function() {
    console.log((new Date()) + " Server is listening on port " + webSocketsServerPort);
});

var wsServer = new webSocketServer({
    httpServer: server
});

wsServer.on('request', function(request) {
    console.log((new Date()) + ' Connection from origin ' + request.origin + '.');

    var connection = request.accept(null, request.origin),
    index = clients.push(connection) - 1,
    userName=false;
    console.log((new Date()) + ' Connection accepted from '+connection.remoteAddress);

    // user sent some message
    connection.on('message', function(message) {
        var json = JSON.parse(message.utf8Data);

        console.log(json.type);
        switch (json.type) {
            case 'broadcast':
                broadcast(json);
            break;

            case 'emit':
                emit({type:'offer', data:json.data.data});
            break;

            case 'client':
                respondToClient(json, clients[index]);
            break;

            default:
                respondToClient({type:'error', data:'Sorry, i dont understand that.'}, clients[index]);
            break;

        }

    });

    connection.on('close', function(connection) {
        clients.splice(index,1);
        console.log((new Date()) + " Peer " + connection.remoteAddress + " disconnected.");
        broadcast({type:'text', data: userName+' has left the channel.'});
    });

    var respondToClient = function(data, client){
        client.sendUTF(JSON.stringify( data ));
    };

    var broadcast = function(data){
        for(var i = 0; i < clients.length; i++ ) {
            if(i != index ) {
                clients[i].sendUTF(JSON.stringify( data ));
            }
        }
    };
    var emit = function(){
        // TBD
    };
});

Y aquí el código del cliente:

$(function () {
    "use strict";

    /**
    * Websocket Stuff
    **/

    window.WebSocket = window.WebSocket || window.MozWebSocket;

    // open connection
    var connection = new WebSocket('ws://url-to-node-server:61122'),
    myName = false,
    mySDP = false,
    otherSDP = false;

    connection.onopen = function () {
        console.log("connection to WebSocketServer successfull");
    };

    connection.onerror = function (error) {
        console.log("WebSocket connection error");
    };

    connection.onmessage = function (message) {
        try {
            var json = JSON.parse(message.data),
            output = document.getElementsByClassName('output')[0];

            switch(json.callback) {
                case 'offer':
                    otherSDP = json.data;
                    document.getElementById('acceptOffer').style.display = 'block';
                break;

                case 'setIceCandidate':
                console.log('ICE CANDITATE ADDED');
                    peerConnection.addIceCandidate(json.data);
                break;

                case 'text':
                    var text = output.value;
                    output.value = json.data+'\n'+output.value;
                break;

                case 'answer':
                    otherSDP = json.data;
                    offerAccepted();
                break;

            }

        } catch (e) {
            console.log('This doesn\'t look like a valid JSON or something else went wrong.');
            return;
        }
    };
    /**
    * P2P Stuff
    **/
    navigator.getMedia = ( navigator.getUserMedia ||
       navigator.webkitGetUserMedia ||
       navigator.mozGetUserMedia ||
       navigator.msGetUserMedia);

    // create Connection
    var peerConnection = new webkitRTCPeerConnection(
        { "iceServers": [{ "url": "stun:stun.l.google.com:19302" }] }
    );


    var remoteVideo = document.getElementById('remoteStream'),
        myVideo = document.getElementById('myStream'),

        // get local video-Stream and add to Peerconnection
        stream = navigator.webkitGetUserMedia({ audio: false, video: true }, function (stream) {
            myVideo.src = webkitURL.createObjectURL(stream);
            console.log(stream);
            peerConnection.addStream(stream);
    });

    // executes if other side adds stream
    peerConnection.onaddstream = function(e){
        console.log("stream added");
        if (!e)
        {
            return;
        }
        remoteVideo.setAttribute("src",URL.createObjectURL(e.stream));
        console.log(e.stream);
    };

    // executes if my icecandidate is received, then send it to other side
    peerConnection.onicecandidate  = function(candidate){
        console.log('ICE CANDITATE RECEIVED');
        var json = JSON.stringify( { type: 'broadcast', callback:'setIceCandidate', data:candidate});
        connection.send(json);
    };

    // send offer via Websocket
    var sendOffer = function(){
        peerConnection.createOffer(function (sessionDescription) {
            peerConnection.setLocalDescription(sessionDescription);
            // POST-Offer-SDP-For-Other-Peer(sessionDescription.sdp, sessionDescription.type);
            var json = JSON.stringify( { type: 'broadcast', callback:'offer',data:{sdp:sessionDescription.sdp,type:'offer'}});
            connection.send(json);

        }, null, { 'mandatory': { 'OfferToReceiveAudio': true, 'OfferToReceiveVideo': true } });
    };

    // executes if offer is received and has been accepted
    var createAnswer = function(){

        peerConnection.setRemoteDescription(new RTCSessionDescription(otherSDP));

        peerConnection.createAnswer(function (sessionDescription) {
            peerConnection.setLocalDescription(sessionDescription);
            // POST-answer-SDP-back-to-Offerer(sessionDescription.sdp, sessionDescription.type);
            var json = JSON.stringify( { type: 'broadcast', callback:'answer',data:{sdp:sessionDescription.sdp,type:'answer'}});
            connection.send(json);
        }, null, { 'mandatory': { 'OfferToReceiveAudio': true, 'OfferToReceiveVideo': true } });

    };

    // executes if other side accepted my offer
    var offerAccepted = function(){
        peerConnection.setRemoteDescription(new RTCSessionDescription(otherSDP));
        console.log('it should work now');
    };

    $('#acceptOffer').on('click',function(){
        createAnswer();
    });

    $('#createOffer').on('click',function(){
        sendOffer();
    });
});

También leí que la transmisión local-media-stream debe ser recopilada antes de enviar cualquier oferta. ¿Significa que tengo que agregarlo cuando se crea la conexión entre pares? Es decir. algo como esto:

// create Connection
var peerConnection = new webkitRTCPeerConnection(
    { 
        "iceServers": [{ "url": "stun:stun.l.google.com:19302" }],
        "mediaStream": stream // attach media stream here?
    }
);

Gracias de antemano, agradezco cualquier ayuda!

EDIT2: Estoy un poco más lejos ahora. parece que la adición de los candidatos de hielo remotos (switch-case setIceCandidate en cliente-código) no está funcionando debido a "Se especificó una cadena no válida o ilegal". El objeto json.data.candidate se ve así:

candidate: "a=candidate:1663431597 2 udp 1845501695 141.84.69.86 57538 typ srflx raddr 10.150.16.92 rport 57538 generation 0
↵"
sdpMLineIndex: 1
sdpMid: "video"

Intenté crear un nuevo candidato como este

 var remoteCandidate = new RTCIceCandidate(json.data.candidate);
 peerConnection.addIceCandidate(remoteCandidate);

pero todavía tengo un error de sintaxis

Respuestas a la pregunta(1)

Su respuesta a la pregunta