I\'ve been reading this article for a signaling solution. The author mentions about signaling with RTCDataChannel when connections are established.
Each side initially declares which audio and/or video tracks it is going to send, so that the right number of ports can be opened, and resolutions and formats that work for both peers can be determined. A signaling channel is needed to send the resulting SDP offer/answer, as well as trickle ICE candidates for each of the ports, to the other side.
Once connected, if you leave this setup alone - basically never add tracks to the connection, remove any, or alter track attributes significantly - then you wont need the signaling server again.
If you do change any of those things however, then a re-negotiation is needed, which is just what it sounds like: another round over the signaling channel much like the first one.
Reasons to add a track may be a second camera, another video-source (from another participant perhaps), or maybe screen-sharing, something like that.
The article is correct that a data channel may be used. Here's a demo! (Firefox only for now.)
The article is wrong about a signaling service being required - provided you have another means of discovery - as this demo lamely proves.
The initial connection is chat-only, but either side can push to add video to the mix. The re-negotiation for this is done over a data channel (since there's no signaling server!)
Instructions for using the fiddle:
Offer
button and copy the offer.addTrack
yet!)addTrack
on either end and video should show up on the other end.addTrack
in the other direction, and you should have video going both ways.var dc = null, sc = null, pc = new mozRTCPeerConnection(), live = false;
pc.onaddstream = e => v2.mozSrcObject = e.stream;
pc.ondatachannel = e => dc? scInit(sc = e.channel) : dcInit(dc = e.channel);
v2.onloadedmetadata = e => { log("Face time!"); };
function addTrack() {
navigator.mediaDevices.getUserMedia({video:true, audio:true})
.then(stream => pc.addStream(v1.mozSrcObject = stream));
}
pc.onnegotiationneeded = e => {
pc.createOffer().then(d => pc.setLocalDescription(d)).then(() => {
if (live) sc.send(JSON.stringify({ "sdp": pc.localDescription }));
}).catch(failed);
};
function scInit() {
sc.onmessage = e => {
var msg = JSON.parse(e.data);
if (msg.sdp) {
var desc = new mozRTCSessionDescription(JSON.parse(e.data).sdp);
if (desc.type == "offer") {
pc.setRemoteDescription(desc).then(() => pc.createAnswer())
.then(answer => pc.setLocalDescription(answer)).then(() => {
sc.send(JSON.stringify({ "sdp": pc.localDescription }));
}).catch(failed);
} else {
pc.setRemoteDescription(desc).catch(failed);
}
} else if (msg.candidate) {
pc.addIceCandidate(new mozRTCIceCandidate(msg.candidate)).catch(failed);
}
};
}
function dcInit() {
dc.onopen = () => { live = true; log("Chat!"); };
dc.onmessage = e => log(e.data);
}
function createOffer() {
button.disabled = true;
dcInit(dc = pc.createDataChannel("chat"));
scInit(sc = pc.createDataChannel("signaling"));
pc.createOffer().then(d => pc.setLocalDescription(d)).catch(failed);
pc.onicecandidate = e => {
if (e.candidate) return;
if (!live) {
offer.value = pc.localDescription.sdp;
offer.select();
answer.placeholder = "Paste answer here";
} else {
sc.send(JSON.stringify({ "candidate": e.candidate }));
}
};
};
offer.onkeypress = e => {
if (e.keyCode != 13 || pc.signalingState != "stable") return;
button.disabled = offer.disabled = true;
var obj = { type:"offer", sdp:offer.value };
pc.setRemoteDescription(new mozRTCSessionDescription(obj))
.then(() => pc.createAnswer()).then(d => pc.setLocalDescription(d))
.catch(failed);
pc.onicecandidate = e => {
if (e.candidate) return;
if (!live) {
answer.focus();
answer.value = pc.localDescription.sdp;
answer.select();
} else {
sc.send(JSON.stringify({ "candidate": e.candidate }));
}
};
};
answer.onkeypress = e => {
if (e.keyCode != 13 || pc.signalingState != "have-local-offer") return;
answer.disabled = true;
var obj = { type:"answer", sdp:answer.value };
pc.setRemoteDescription(new mozRTCSessionDescription(obj)).catch(failed);
};
chat.onkeypress = e => {
if (e.keyCode != 13) return;
dc.send(chat.value);
log(chat.value);
chat.value = "";
};
var log = msg => div.innerHTML += "<p>" + msg + "</p>";
var failed = e => log(e.name + ": " + e.message + " line " + e.lineNumber);
<video id="v1" height="120" width="160" autoplay muted></video>
<video id="v2" height="120" width="160" autoplay></video><br>
<button id="button" onclick="createOffer()">Offer:</button>
<textarea id="offer" placeholder="Paste offer here"></textarea><br>
Answer: <textarea id="answer"></textarea><br>
<button id="button" onclick="addTrack()">AddTrack</button>
<div id="div"></div><br>
Chat: <input id="chat"></input><br>