How to start a basic WebRTC data channel?
This is what I have so far, but it doesn't even seem to try and connect. Im sure I am just missing something basic.
var RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection || window.msRTCPeerConnection;
var peerConnection = new RTCPeerConnection({
iceServers: [
{url: 'stun:stun1.l.google.com:19302'},
{url: 'stun:stun2.l.google.com:19302'},
{url: 'stun:stun3.l.google.com:19302'},
{url: 'stun:stun4.l.google.com:19302'},
]
});
peerConnection.ondatachannel = function () {
console.log('peerConnection.ondatachannel');
};
peerConnection.onicecandidate = function () {
console.log('peerConnection.onicecandidate');
};
var dataChannel = peerConnection.createDataChannel('myLabel', {
});
dataChannel.onerror = function (error) {
console.log('dataChannel.onerror');
};
dataChannel.onmessage = function (event) {
console.log('dataChannel.onmessage');
};
dataChannel.onopen = function () {
console.log('dataChannel.onopen');
dataChannel.send('Hello World!');
};
dataChannel.onclose = function () {
console.log('dataChannel.onclose');
};
console.log(peerConnection, dataChannel);
WebRTC assumes you have a way to signal (send an offer-string to, and receive an answer-string from) whomever you wish to contact. Without some server, how will you do that?
To illustrate, here's some code that does everything but that (works in Firefox and Chrome 45):
var config = { iceServers: [{ urls: "stun:stun.l.google.com:19302" }]};
var dc, pc = new RTCPeerConnection(config);
pc.ondatachannel = e => {
dc = e.channel;
dc.onopen = e => (log("Chat!"), chat.select());
dc.onmessage = e => log(e.data);
}
function createOffer() {
button.disabled = true;
pc.ondatachannel({ channel: pc.createDataChannel("chat") });
pc.createOffer().then(d => pc.setLocalDescription(d)).catch(failed);
pc.onicecandidate = e => {
if (e.candidate) return;
offer.value = pc.localDescription.sdp;
offer.select();
answer.placeholder = "Paste answer here";
};
};
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 RTCSessionDescription(obj))
.then(() => pc.createAnswer()).then(d => pc.setLocalDescription(d))
.catch(failed);
pc.onicecandidate = e => {
if (e.candidate) return;
answer.focus();
answer.value = pc.localDescription.sdp;
answer.select();
};
};
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 RTCSessionDescription(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 + ", line " + e.lineNumber);
<script src="https://rawgit.com/webrtc/adapter/master/adapter.js"></script>
<button id="button" onclick="createOffer()">Offer:</button>
<textarea id="offer" placeholder="Paste offer here"></textarea><br>
Answer: <textarea id="answer"></textarea><br><div id="div"></div>
Chat: <input id="chat"></input><br>
Open this page in a second tab, and you can chat from one tab to the other (or to a different machine around the world). What stinks is that you must get the offer there yourself:
Press the Offer button in Tab A (only) and wait 1-20 seconds till you see the offer-text,
copy-paste the offer-text from Tab A to Tab B, and hit Enter
copy-paste the answer-text that appears from Tab B to Tab A, and hit Enter.
You should now be able to chat between tabs, without a server.
As you can see, this is a sub-par experience, which is why you need some basic websocket server to pass offer/answer (as well as trickle ice candidates if you want connecting to happen fast) between A and B, to get things started. Once you have a connection, you can use data-channels for this, with a little extra work.
Related
I built a barcode scanning system using Chrome's built-in BarcodeDetector. I based the work on Paul Kinlan's QR code scanner which works fine on my phone, but when I run my own code on my phone, it often causes Chrome or the whole System UI to freeze. Sometimes it gets so bad that I need to restart the phone by holding down the power button.
I have tried debugging in the Chrome developer console, but when the phone freezes, so does the developer console.
When I comment out the actual QR code detection, I can leave the page open in Chrome for 10 minutes and it just keeps running. With QR code detection running, the phone will freeze anywhere from immediately to 3 minutes later.
I put the support files (js and HTML) in a Gist - I don't think they are the issue because everything works when the barcode scanning is commented out.
My first attempt:
// Inspired by/based on on https://github.com/PaulKinlan/qrcode/blob/production/app/scripts/main.mjs
import WebCamManager from './scan/WebCamManager.js';
(function () {
'use strict';
var QRCodeCamera = function (element) {
var root = document.getElementById(element);
var cameraRoot = root.querySelector('.CameraRealtime');
var cameraManager = new WebCamManager(cameraRoot);
var cameraVideo = root.querySelector('.Camera-video');
// Offscreen canvas is supposed to help with processing speed
var cameraCanvas = new OffscreenCanvas(1,1);
var context = cameraCanvas.getContext('2d');
const detector = new BarcodeDetector({
formats: ['qr_code'],
});
cameraManager.onframeready = async function (frameData) {
cameraCanvas.width = cameraVideo.videoWidth;
cameraCanvas.height = cameraVideo.videoHeight;
context.drawImage(frameData, 0, 0, cameraVideo.videoWidth, cameraVideo.videoHeight);
if (self.onframe) {
// Comment out the line below to stop processing QR codes
await self.onframe(cameraCanvas);
}
};
var processingFrame = false;
self.onframe = async function (cameraCanvas) {
// There is a frame in the camera, what should we do with it?
if (processingFrame == false) {
processingFrame = true;
let result = await detector.detect(cameraCanvas);
processingFrame = false;
if (result === undefined || result === null || result.length === 0) {
return
};
if ('vibrate' in navigator) {
navigator.vibrate([200]);
}
cameraManager.stop();
var currentURL = new URL(window.location.href);
var newURL;
if (result[0].rawValue
&& (newURL = new URL(result[0].rawValue))
&& newURL.hostname == currentURL.hostname
&& newURL.pathname.startsWith('/pickup/qr/')
) {
window.location.href = newURL;
} else {
alert('Unsupported QR Code: ' + result[0].rawValue);
}
cameraManager.start();
}
};
};
window.addEventListener('load', function () {
var camera = new QRCodeCamera('camera');
});
})();
I also tried splitting the QR detection to a worker (worker code in Gist), but I have the same issue:
const worker = new Worker('/js/scan-worker.js');
worker.addEventListener('message', async (e) => {
if (Array.isArray(e.data)) {
var result = e.data;
if (result === undefined || result === null || result.length === 0) {
processingFrame = false;
return
};
if ('vibrate' in navigator) {
navigator.vibrate([200]);
}
var currentURL = new URL(window.location.href);
var newURL;
if (result[0].rawValue
&& (newURL = new URL(result[0].rawValue))
&& newURL.hostname == currentURL.hostname
&& newURL.pathname.startsWith('/pickup/qr/')
) {
worker.terminate();
window.location.href = newURL;
} else {
alert('Unsupported QR Code: ' + result[0].rawValue);
}
} else {
var newError = document.createElement('div');
newError.classList.add('alert', 'alert-danger');
newError.innerHTML = e.data;
errorContainer.prepend(newError);
worker.terminate();
}
processingFrame = false;
});
cameraManager.onframeready = async function (frameData) {
if (processingFrame == false) {
cameraCanvas.width = cameraVideo.videoWidth;
cameraCanvas.height = cameraVideo.videoHeight;
context.drawImage(frameData, 0, 0, cameraVideo.videoWidth, cameraVideo.videoHeight);
if (self.onframe) {
await self.onframe(cameraCanvas, context);
}
}
};
self.onframe = async function (cameraCanvas, context) {
// There is a frame in the camera, what should we do with it?
if (processingFrame == false) {
processingFrame = true;
worker.postMessage(context.getImageData(0, 0, cameraCanvas.width, cameraCanvas.height));
}
};
Not sure if this would make a difference - all the JS code is run through Laravel Mix.
I'm creating a web-app where a computer needs to communicate with another device, an iPhone XR (iOS 13). I have created the shell of the program and works fine with two computers running Chrome, but am having trouble getting it to work on the phone.
Here is the code for the 'creator' of the WebRTC server:
<textarea id="creater-sdp"></textarea>
<textarea id="joiner-sdp"></textarea>
<button onclick="start()">Start</button>
<div id="chat"></div>
<input type="text" id="msg"><button onclick="sendMSG()">Send</button>
<script>
let id = (x) => {return document.getElementById(x);};
let constraints = {optional: [{RtpDataChannels: true}]};
let pc = new RTCPeerConnection(null);
let dc;
pc.oniceconnectionstatechange = function(e) {
let state = pc.iceConnectionState;
id("status").innerHTML = state;
};
pc.onicecandidate = function(e) {
if (e.candidate) return;
id("creater-sdp").value = JSON.stringify(pc.localDescription);
}
function createOfferSDP() {
dc = pc.createDataChannel("chat");
pc.createOffer().then(function(e) {
pc.setLocalDescription(e)
});
dc.onopen = function() {
addMSG("CONNECTED!", "info")
};
dc.onmessage = function(e) {
if (e.data) addMSG(e.data, "other");
}
};
function start() {
let answerSDP = id("joiner-sdp").value;
let answerDesc = new RTCSessionDescription(JSON.parse(answerSDP));
pc.setRemoteDescription(answerDesc);
}
let addMSG = function(msg, who) {
let node = document.createElement("div");
let textnode = document.createTextNode(`[${who}] ${msg}`);
node.appendChild(textnode);
id("chat").appendChild(node);
}
createOfferSDP();
let sendMSG = function() {
let value = id("msg").value;
if(value) {
dc.send(value);
addMSG(value, "me");
id("msg").value = "";
}
}
</script>
First, the SDP is copied from the textarea to the other 'joiner' client, and then another SDP is created which is returned to the 'creator' with the following code:
<textarea id="creater-sdp"></textarea>
<textarea id="joiner-sdp"></textarea>
<button onclick="createAnswerSDP()">Create</button>
<div id="chat"></div>
<input type="text" id="msg"><button onclick="sendMSG()">Send</button>
<script>
let id = (x) => {return document.getElementById(x);};
let constraints = {optional: [{RtpDataChannels: true}]};
let pc = new RTCPeerConnection(null);
let dc;
pc.ondatachannel = function(e) {dc = e.channel; dcInit(dc)};
pc.onicecandidate = function(e) {
if (e.candidate) return;
id("joiner-sdp").value = JSON.stringify(pc.localDescription);
};
pc.oniceconnectionstatechange = function(e) {
let state = pc.iceConnectionState;
id("status").innerHTML = state;
};
function dcInit(dc) {
dc.onopen = function() {
addMSG("CONNECTED!", "info")
};
dc.onmessage = function(e) {
if (e.data) addMSG(e.data, "other");
}
}
function createAnswerSDP() {
let offerDesc = new RTCSessionDescription(JSON.parse(id("creater-sdp").value));
pc.setRemoteDescription(offerDesc)
pc.createAnswer(function (answerDesc) {
pc.setLocalDescription(answerDesc)
}, function() {alert("Couldn't create offer")},
constraints);
};
let sendMSG = function() {
let value = id("msg").value;
if(value) {
dc.send(value);
addMSG(value, "me");
id("msg").value = "";
}
}
let addMSG = function(msg, who) {
let node = document.createElement("div");
let textnode = document.createTextNode(`[${who}] ${msg}`);
node.appendChild(textnode);
id("chat").appendChild(node);
}
</script>
This entire process works flawlessly on the computers, but for some reason cannot be done on the iPhone, even when switching the roles. Am I doing something wrong? Or could it be a feature I'm using isn't implemented yet? I've tried both Safari and Chrome on the phone.
I'm trying to get the amount of currently viewer of a stream.
I tried to trigger some codes whenever onUserStatusChanged is fired (used as if user onjoin event).
Something like:
connection.onUserStatusChanged = function(status) {
console.log("onUserStatusChanged");
updateViewers();
};
function updateViewers(){
connection.getAllParticipants().forEach(function(participantId) {
var peer = connection.peers[participantId];
if(viewers && viewers.indexOf(peer.extra.nickname) === -1 && "UserA" !== peer.extra.nickname)
viewers.push(peer.extra.nickname);
});
console.log(viewers.length);
}
So far so good it works, But now it pushes also the user who has cam and not watching user A his cam.
I only want the extra.nickname of the user who is connected to User A his cam/channel/connection
Using "beforeAddingStream":
Please download latest codes from github to use this "beforeAddingStream" method.
var listOfStreamReceivers = {};
connection.beforeAddingStream = function(stream, peer) {
var remoteUserExtra = connection.peers[peer.userid].extra;
listOfStreamReceivers[remoteUserExtra.nickname] = peer;
return stream;
};
Using "onExtraDataUpdated" and "updateExtraData"
var listOfStreamReceivers = {};
connection.onstream = function(event) {
if (event.type == 'remote') {
connection.extra.receivedStreamById = event.streamid;
connection.updateExtraData();
}
};
connection.onExtraDataUpdated = function(event) {
if (event.extra.receivedStreamById) {
listOfStreamReceivers[event.extra.nickname] = event;
}
};
Using data channels
connection.session.data = true;
var listOfStreamReceivers = {};
connection.onstream = function(event) {
if (event.type == 'remote') {
var data = {
receivedStreamById: event.streamid
};
// second parameter sends direct message to the broadcast initiator
connection.send(data, connection.sessionid);
}
};
connection.onmessage = function(event) {
if (event.data.receivedStreamById) {
listOfStreamReceivers[event.extra.nickname] = event;
}
};
"listOfStreamReceivers" to array
var array = Object.keys(listOfStreamReceivers);
console.log('number of receivers', array.length);
array.forEach(function(key) {
var peer = listOfStreamReceivers[key];
var userid = peer.userid;
var extra = peer.extra;
});
I'm trying to implement a mechanism to send textual data (JSON for instance) in from page to page, using javascript at the same machine.
I found some code and wrapped it but it only works at the same page.
At the moment I don't want to use a WwebRTC framework, only adapter.js.
//Must include adapter.js before
var WebRTCManager = (function () {
'use strict';
//Ctor
function WebRTCManagerFn() {
console.log('WebRTCManagerFn ctor reached');
this._events = {};
this._localConnection = null
this._remoteConnection = null;
this._sendChannel = null;
this._receiveChannel = null;
}
WebRTCManagerFn.prototype.addEventListener = function (name, handler) {
if (this._events.hasOwnProperty(name))
this._events[name].push(handler);
else
this._events[name] = [handler];
};
WebRTCManagerFn.prototype._fireEvent = function (name, event) {
if (!this._events.hasOwnProperty(name))
return;
if (!event)
event = {};
var listeners = this._events[name], l = listeners.length;
for (var i = 0; i < l; i++) {
listeners[i].call(null, event);
}
};
WebRTCManagerFn.prototype.createConnection = function () {
var servers = null;
var pcConstraint = null;
var dataConstraint = null;
console.log('Using SCTP based data channels');
// SCTP is supported from Chrome 31 and is supported in FF.
// No need to pass DTLS constraint as it is on by default in Chrome 31.
// For SCTP, reliable and ordered is true by default.
// Add localConnection to global scope to make it visible
// from the browser console.
window.localConnection = this._localConnection =
new RTCPeerConnection(servers, pcConstraint);
console.log('Created local peer connection object localConnection');
this._sendChannel = this._localConnection.createDataChannel('sendDataChannel',
dataConstraint);
console.log('Created send data channel');
this._localConnection.onicecandidate = this._localIceCallback.bind(this);
this._sendChannel.onopen = this._onSendChannelStateChange.bind(this);
this._sendChannel.onclose = this._onSendChannelStateChange.bind(this);
// Add remoteConnection to global scope to make it visible
// from the browser console.
window.remoteConnection = this._remoteConnection =
new RTCPeerConnection(servers, pcConstraint);
console.log('Created remote peer connection object remoteConnection');
this._remoteConnection.onicecandidate = this._remoteIceCallback.bind(this);
this._remoteConnection.ondatachannel = this._receiveChannelCallback.bind(this);
this._localConnection.createOffer(this._gotOfferFromLocalConnection.bind(this), this._onCreateSessionDescriptionError.bind(this));
}
WebRTCManagerFn.prototype._onCreateSessionDescriptionError = function (error) {
console.log('Failed to create session description: ' + error.toString());
}
WebRTCManagerFn.prototype.sendMessage = function (msgText) {
var msg = new Message(msgText);
// Send the msg object as a JSON-formatted string.
var data = JSON.stringify(msg);
this._sendChannel.send(data);
console.log('Sent Data: ' + data);
}
WebRTCManagerFn.prototype.closeDataChannels = function () {
console.log('Closing data channels');
this._sendChannel.close();
console.log('Closed data channel with label: ' + this._sendChannel.label);
this._receiveChannel.close();
console.log('Closed data channel with label: ' + this._receiveChannel.label);
this._localConnection.close();
this._remoteConnection.close();
this._localConnection = null;
this._remoteConnection = null;
console.log('Closed peer connections');
}
WebRTCManagerFn.prototype._gotOfferFromLocalConnection = function (desc) {
console.log('reached _gotOfferFromLocalConnection');
if (this && this._localConnection != 'undefined' && this._remoteConnection != 'undefined') {
this._localConnection.setLocalDescription(desc);
console.log('Offer from localConnection \n' + desc.sdp);
this._remoteConnection.setRemoteDescription(desc);
this._remoteConnection.createAnswer(this._gotAnswerFromRemoteConnection.bind(this),
this._onCreateSessionDescriptionError.bind(this));
}
}
WebRTCManagerFn.prototype._gotAnswerFromRemoteConnection = function (desc) {
console.log('reached _gotAnswerFromRemoteConnection');
if (this && this._localConnection != 'undefined' && this._remoteConnection != 'undefined') {
this._remoteConnection.setLocalDescription(desc);
console.log('Answer from remoteConnection \n' + desc.sdp);
this._localConnection.setRemoteDescription(desc);
}
}
WebRTCManagerFn.prototype._localIceCallback = function (event) {
console.log('local ice callback');
if (event.candidate) {
this._remoteConnection.addIceCandidate(event.candidate,
this._onAddIceCandidateSuccess.bind(this), this._onAddIceCandidateError.bind(this));
console.log('Local ICE candidate: \n' + event.candidate.candidate);
}
}
WebRTCManagerFn.prototype._remoteIceCallback = function (event) {
console.log('remote ice callback');
if (event.candidate) {
this._localConnection.addIceCandidate(event.candidate,
this._onAddIceCandidateSuccess.bind(this), this._onAddIceCandidateError.bind(this));
console.log('Remote ICE candidate: \n ' + event.candidate.candidate);
}
}
WebRTCManagerFn.prototype._onAddIceCandidateSuccess = function (evt) {
debugger;
console.log('AddIceCandidate success. evt: '+ evt);
}
WebRTCManagerFn.prototype._onAddIceCandidateError = function (error) {
console.log('Failed to add Ice Candidate: ' + error.toString());
}
WebRTCManagerFn.prototype._receiveChannelCallback = function (event) {
console.log('Receive Channel Callback');
this._receiveChannel = event.channel;
this._receiveChannel.onmessage = this._onReceiveMessageCallback.bind(this);
this._receiveChannel.onopen = this._onReceiveChannelStateChange.bind(this);
this._receiveChannel.onclose = this._onReceiveChannelStateChange.bind(this);
}
WebRTCManagerFn.prototype._onReceiveMessageCallback = function (event) {
console.log('Received Message: ' + event.data);
console.log('Received Message this is: ' + this);
var msgObj = JSON.parse(event.data);
this._fireEvent("messageRecieved", {
details: {
msg: msgObj
}
});
}
WebRTCManagerFn.prototype._onSendChannelStateChange = function () {
console.log('_onSendChannelStateChange');
var readyState = this._sendChannel.readyState;
console.log('Send channel state is: ' + readyState);
}
WebRTCManagerFn.prototype._onReceiveChannelStateChange = function () {
var readyState = this._receiveChannel.readyState;
console.log('Receive channel state is: ' + readyState);
}
return WebRTCManagerFn;
})();
My question is how to pass data between two pages on the same machine using WebRTC?
This WebRTC tab chat demo works across tabs or windows in the same browser without a server: https://jsfiddle.net/f5y48hcd/ (I gave up making it work in a code snippet due to a SecurityError.)
Open the fiddle in two windows and try it out. For reference, here's the WebRTC code:
var pc = new RTCPeerConnection(), dc, enterPressed = e => e.keyCode == 13;
var connect = () => init(dc = pc.createDataChannel("chat"));
pc.ondatachannel = e => init(dc = e.channel);
var init = dc => {
dc.onopen = e => (dc.send("Hi!"), chat.select());
dc.onclose = e => log("Bye!");
dc.onmessage = e => log(e.data);
};
chat.onkeypress = e => {
if (!enterPressed(e)) return;
dc.send(chat.value);
log("> " + chat.value);
chat.value = "";
};
var sc = new localSocket(), send = obj => sc.send(JSON.stringify(obj));
var incoming = msg => msg.sdp &&
pc.setRemoteDescription(new RTCSessionDescription(msg.sdp))
.then(() => pc.signalingState == "stable" || pc.createAnswer()
.then(answer => pc.setLocalDescription(answer))
.then(() => send({ sdp: pc.localDescription })))
.catch(log) || msg.candidate &&
pc.addIceCandidate(new RTCIceCandidate(msg.candidate)).catch(log);
sc.onmessage = e => incoming(JSON.parse(e.data));
pc.oniceconnectionstatechange = e => log(pc.iceConnectionState);
pc.onicecandidate = e => send({ candidate: e.candidate });
pc.onnegotiationneeded = e => pc.createOffer()
.then(offer => pc.setLocalDescription(offer))
.then(() => send({ sdp: pc.localDescription }))
.catch(log);
var log = msg => div.innerHTML += "<br>" + msg;
I use this for demoing WebRTC data channels. Note that the secret sauce is the localSocket.js that I wrote for this, which looks like this:
function localSocket() {
localStorage.a = localStorage.b = JSON.stringify([]);
this.index = 0;
this.interval = setInterval(() => {
if (!this.in) {
if (!JSON.parse(localStorage.a).length) return;
this.in = "a"; this.out = "b";
}
var arr = JSON.parse(localStorage[this.in]);
if (arr.length <= this.index) return;
if (this.onmessage) this.onmessage({ data: arr[this.index] });
this.index++;
}, 200);
setTimeout(() => this.onopen && this.onopen({}));
}
localSocket.prototype = {
send: function(msg) {
if (!this.out) {
this.out = "a"; this.in = "b";
}
var arr = JSON.parse(localStorage[this.out]);
arr.push(msg);
localStorage[this.out] = JSON.stringify(arr);
},
close: function() {
clearInterval(this.interval);
}
};
It basically uses localStorage to simulate web sockets locally between two tabs. If this is all you want to do, then you don't even need WebRTC data channels.
Disclaimer: It's not very robust, and relies on two pages being ready to communicate, so not production-ready by any means.
I am trying to create a simple webpage using WebRTC DataChannels that sends pings/pongs between browsers.
When Chrome initiates the connection and then Chrome connects, it works.
When Firefox initiates the connection and then Firefox connects, it works.
When Chrome initiates the connection and then Firefox connects, it works.
But when Firefox initiates the connection and then Chrome connects, it doesn't work. Chrome never receives data sent by Firefox.
I'm using Firefox 26 and Chromium 32, on Archlinux.
Here is my JavaScript code:
<!DOCTYPE html>
<html>
<head>
<title>WebRTC test</title>
<meta charset="utf-8">
</head>
<body>
<button id="create" disabled>Create data channel</button>
<script type="text/javascript">
// DOM
var create = document.getElementById('create');
// Compatibility
window.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
window.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription;
window.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate || window.webkitRTCIceCandidate;
// Create a WebRTC object
var rtc = new RTCPeerConnection(null);
// Create a data channel
var sendChannel = rtc.createDataChannel('pingtest', {reliable: false});
var myMsg = 'ping';
function setRecvChannel(recvChannel) {
recvChannel.onmessage = function(event) {
if(event.data.indexOf('\x03\x00\x00\x00\x00\x00\x00\x00\x00') === 0) {
console.log('-> ' + window.btoa(event.data));
return; // Received channel's name, ignore
}
console.log('-> ' + event.data);
window.setTimeout(function() {
console.log('<- ' + myMsg);
sendChannel.send(myMsg);
}, 500);
};
}
// Chrome and Firefox
sendChannel.onopen = function(event) {
setRecvChannel(sendChannel);
if(myMsg === 'ping') {
console.log('<- ' + myMsg);
sendChannel.send(myMsg);
}
};
// Firefox
rtc.ondatachannel = function(event) {
setRecvChannel(event.channel);
};
// ICE
rtc.onicecandidate = function(event) {
if(event.candidate) {
console.log('<- ' + JSON.stringify(event.candidate));
ws.send(JSON.stringify(event.candidate));
}
};
// Signaling channel
var ws = new WebSocket('ws://127.0.0.1:49300/');
ws.onopen = function() {
create.disabled = false;
};
ws.onmessage = function(event) {
console.log('-> ' + event.data);
var data = JSON.parse(event.data);
if(data.sdp) {
rtc.setRemoteDescription(new RTCSessionDescription(data));
if(data.type === 'offer') {
myMsg = 'pong';
rtc.createAnswer(function(anwser) {
rtc.setLocalDescription(anwser, function () {
console.log('<- ' + JSON.stringify(anwser));
ws.send(JSON.stringify(anwser));
});
}, console.error);
}
}
else {
rtc.addIceCandidate(new RTCIceCandidate(data));
}
};
ws.onclose = function() {
create.disabled = true;
};
// Create an offer
create.onclick = function() {
rtc.createOffer(function(offer) {
rtc.setLocalDescription(offer, function () {
offer.sdp = offer.sdp;
console.log(offer.sdp);
console.log('<- ' + JSON.stringify(offer));
ws.send(JSON.stringify(offer));
});
}, console.error);
};
</script>
</body>
</html>
Here is the WebSocket-based signaling server I've created only for test purposes, it simply listens on port 49300 and broadcasts data received from clients to other clients:
#!/usr/bin/python
#-*- encoding: Utf-8 -*-
from socket import socket, AF_INET, SOCK_STREAM, SOL_SOCKET, SO_REUSEADDR
from string import printable
from threading import Thread
from base64 import b64encode
from struct import unpack
from hashlib import sha1
PORT = 49300
activeSocks = []
def SignalingChannel(ip, port, sock):
print 'Connection from %s:%s' % (ip, port)
# Handling the HTTP request
try:
headers = sock.recv(8184)
assert headers.upper().startswith('GET')
assert headers.endswith('\r\n\r\n')
data = headers.strip().replace('\r', '').split('\n')[1:]
headers = {}
for header in data:
name, value = header.split(':', 1)
headers[name.strip().lower()] = value.strip()
assert headers['host']
assert 'upgrade' in headers['connection'].lower()
assert 'websocket' in headers['upgrade'].lower()
assert headers['sec-websocket-version'] == '13'
assert len(headers['sec-websocket-key']) == 24
guid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
accept = b64encode(sha1(headers['sec-websocket-key'] + guid).digest())
sock.send('HTTP/1.1 101 Switching Protocols\r\n' +
'Connection: Upgrade\r\n' +
'Upgrade: websocket\r\n' +
'Sec-WebSocket-Accept: %s\r\n' % accept +
'\r\n')
except:
try:
msg = 'This is a RFC 6455 WebSocket server.\n'
sock.send('HTTP/1.1 400 Bad Request\r\n' +
'Connection: Close\r\n' +
'Content-Length: %d\r\n' % len(msg) +
'Content-Type: text/plain; charset=us-ascii\r\n' +
'Sec-WebSocket-Version: 13\r\n' +
'\r\n' +
msg)
except:
pass
sock.close()
print 'Disconnection from %s:%s' % (ip, port)
return
activeSocks.append(sock)
try:
data = sock.recv(2)
while len(data) == 2:
frame = data[0] + chr(ord(data[1]) & 0b01111111)
opcode = ord(data[0]) & 0b00001111
mask = ord(data[1]) & 0b10000000
paylen = ord(data[1]) & 0b01111111
if paylen == 126:
data = sock.recv(2)
frame += data
paylen = unpack('>H', data)[0]
elif paylen == 127:
data = sock.recv(8)
frame += data
paylen = unpack('>Q', data)[0]
if mask:
mask = sock.recv(4)
data = ''
received = True
while received and len(data) < paylen:
received = sock.recv(paylen - len(data))
data += received
if mask:
unmasked = ''
for i in xrange(len(data)):
unmasked += chr(ord(data[i]) ^ ord(mask[i % 4]))
else:
unmasked = data
frame += unmasked
if opcode != 8:
print '-- From port %d --' % port
if all(ord(c) < 127 and c in printable for c in unmasked):
print unmasked
else:
print repr(unmasked)
for destSock in activeSocks:
if destSock != sock:
destSock.send(frame)
else:
break
data = sock.recv(2)
except:
pass
activeSocks.remove(sock)
sock.close()
print 'Disconnection from %s:%s' % (ip, port)
listenSock = socket(AF_INET, SOCK_STREAM)
listenSock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
listenSock.bind(('0.0.0.0', PORT))
listenSock.listen(20)
print 'Listening on port 49300...'
while True:
clientSock, (ip, port) = listenSock.accept()
Thread(target=SignalingChannel, args=(ip, port, clientSock)).start()
To run the code, launch the signaling server, open the webpage in two browser tabs, click the "Create data channel" button and look at the web console.
Any idea?
Looking through the Chrome/Firefox bug trackers it looks like this issue has been identified and resolved, but only in Chrome Canary 33.0.1715.0 or higher.
If you're unwilling to require the Chrome Canary build mentioned above, you can detect the bad peer combination, and have your 'offer' button signal the other client to make an offer.
Pseudocode:
socket.onMessage(msg) {
if(msg == "request-offer"){
doOffer();
}
...
}
createDataChannelButton.onClick() {
if(!canCreateChannelBasedOnBrowser){
socket.send("request-offer");
}
else {
doOffer();
}
}
With your example code:
<!DOCTYPE html>
<html>
<head>
<title>WebRTC test</title>
<meta charset="utf-8">
</head>
<body>
<button id="create" disabled>Create data channel</button>
<script type="text/javascript">
// DOM
// CHANGE: Add basic browser detection based on google's adapter.js file.
var rtcBrowserVersion = 0;
var rtcCanInitiateDataOffer = false;
if (navigator.mozGetUserMedia) {
rtcBrowserVersion = parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10);
rtcCanInitiateDataOffer = true;
} else if (navigator.webkitGetUserMedia) {
// Chrome Canary reports major version 35 for me. Can't find a reliable resource to confirm
// canary versions.
rtcBrowserVersion = parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2], 10);
rtcCanInitiateDataOffer = rtcBrowserVersion >= 35;
}
var create = document.getElementById('create');
// Compatibility
window.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
window.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription;
window.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate || window.webkitRTCIceCandidate;
// Create a WebRTC object
var rtc = new RTCPeerConnection(null);
// Create a data channel
var sendChannel = rtc.createDataChannel('pingtest', {reliable: false});
var myMsg = 'ping';
function setRecvChannel(recvChannel) {
recvChannel.onmessage = function(event) {
if(event.data.indexOf('\x03\x00\x00\x00\x00\x00\x00\x00\x00') === 0) {
console.log('-> ' + window.btoa(event.data));
return; // Received channel's name, ignore
}
console.log('-> ' + event.data);
window.setTimeout(function() {
console.log('<- ' + myMsg);
sendChannel.send(myMsg);
}, 500);
};
}
// Chrome and Firefox
sendChannel.onopen = function(event) {
setRecvChannel(sendChannel);
if(myMsg === 'ping') {
console.log('<- ' + myMsg);
sendChannel.send(myMsg);
}
};
// Firefox
rtc.ondatachannel = function(event) {
setRecvChannel(event.channel);
};
// ICE
rtc.onicecandidate = function(event) {
if(event.candidate) {
console.log('<- ' + JSON.stringify(event.candidate));
ws.send(JSON.stringify(event.candidate));
}
};
// Signaling channel
var ws = new WebSocket('ws://127.0.0.1:49300/');
ws.onopen = function() {
create.disabled = false;
};
ws.onmessage = function(event) {
console.log('-> ' + event.data);
var data = JSON.parse(event.data);
if(data.sdp) {
rtc.setRemoteDescription(new RTCSessionDescription(data));
if(data.type === 'offer') {
myMsg = 'pong';
rtc.createAnswer(function(anwser) {
rtc.setLocalDescription(anwser, function () {
console.log('<- ' + JSON.stringify(anwser));
ws.send(JSON.stringify(anwser));
});
}, console.error);
}
}
// CHANGE: Chrome with offer bug asked to initiate the offer.
else if(data.initiate === true){
doOffer();
}
else {
rtc.addIceCandidate(new RTCIceCandidate(data));
}
};
ws.onclose = function() {
create.disabled = true;
};
// Create an offer
// CHANGE: Create function for offer, so that it may be called from ws.onmessage
function doOffer(){
rtc.createOffer(function(offer) {
rtc.setLocalDescription(offer, function () {
offer.sdp = offer.sdp;
console.log(offer.sdp);
console.log('<- ' + JSON.stringify(offer));
ws.send(JSON.stringify(offer));
});
}, console.error);
}
create.onclick = function() {
// CHANGE: If this client is not able to negotiate a data channel, send a
// message to the peer asking them to offer the channel.
if(rtcCanInitiateDataOffer){
doOffer();
}
else {
ws.send(JSON.stringify({initiate:true}));
}
};
Related Issues:
WebRTC - Test SCTP data channel interop with Firefox Closed as Fixed 11/25/13
Chromium - SCTP Data Channel fails to connect if Chrome offers and FF answers - Closed as Fixed 11/4/13
It is also worth noting that there is currently a max message buffer size of 16KB when sending data to a Chrome peer. If you are sending large data messages, you will need to break them into 16KB chunks before transmitting them over the data channel.
FYI, I revised the HTML further to try and troubleshoot a problem I'm having talking to the C++ libjingle (neither Chrome, nor Firefox) ;)
This version shows on-page status. Not pretty, but useful.
<!DOCTYPE html>
<html>
<head>
<title>WebRTC test</title>
<meta charset="utf-8">
</head>
<body>
<button id="create" disabled>Create data channel</button>
<div id="output1">output1</div>
<div id="output2">output2</div>
<div id="output3">output3</div>
<div id="output4">output4</div>
<div id="output5">output5</div>
<div id="output6">output6</div>
<div id="output7">output7</div>
<div id="output8">output8</div>
<script type="text/javascript">
var myCounts = {
output1: 0,
output2: 0
};
var server1 = 'ws://127.0.0.1:49300/';
// CHANGE: Add basic browser detection based on google's adapter.js file.
var rtcBrowserVersion = 0;
var rtcCanInitiateDataOffer = false;
if (navigator.mozGetUserMedia) {
rtcBrowserVersion = parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10);
rtcCanInitiateDataOffer = true;
} else if (navigator.webkitGetUserMedia) {
// Chrome Canary reports major version 35 for me. Can't find a reliable resource to confirm
// canary versions.
rtcBrowserVersion = parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2], 10);
rtcCanInitiateDataOffer = rtcBrowserVersion >= 35;
}
// DOM
var create = document.getElementById('create');
// Compatibility
window.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
window.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription;
window.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate || window.webkitRTCIceCandidate;
// Create a WebRTC object
var rtc = new RTCPeerConnection(null);
// Create a data channel
var sendChannel = rtc.createDataChannel('PingTest', {reliable: false});
var myMsg = 'ping';
function setRecvChannel(recvChannel) {
recvChannel.onmessage = function(event) {
myCounts.output1++;
document.getElementById("output1").innerHTML = myCounts.output1 + ": " + event.data;
if(event.data.indexOf('\x03\x00\x00\x00\x00\x00\x00\x00\x00') === 0) {
console.log('-> ' + window.btoa(event.data));
return; // Received channel's name, ignore
};
console.log('-> ' + event.data);
window.setTimeout(function() {
console.log('<- ' + myMsg);
sendChannel.send(myMsg);
}, 500);
};
}
// Chrome and Firefox
sendChannel.onopen = function(event) {
setRecvChannel(sendChannel);
if(myMsg === 'ping') {
console.log('<- ' + myMsg);
sendChannel.send(myMsg);
}
};
// Firefox
rtc.ondatachannel = function(event) {
myCounts.output2++;
document.getElementById("output2").innerHTML = myCounts.output2 + " channel: " + event.channel.label;
setRecvChannel(event.channel);
};
// ICE
rtc.onicecandidate = function(event) {
if(event.candidate) {
console.log('<- ' + JSON.stringify(event.candidate));
ws.send(JSON.stringify(event.candidate));
}
};
// Signaling channel
var ws = new WebSocket(server1);
document.getElementById("output3").innerHTML="created WebSocket (not connected) " + server1;
ws.onopen = function() {
create.disabled = false;
document.getElementById("output3").innerHTML="onOpen WebSocket " + server1;
};
ws.onmessage = function(event) {
document.getElementById("output3").innerHTML="onMessage WebSocket " + event.data;
console.log('-> ' + event.data);
var data = JSON.parse(event.data);
if (data.sdp) {
rtc.setRemoteDescription(new RTCSessionDescription(data));
if (data.type === 'offer') {
document.getElementById("output4").innerHTML="received SessiionDescription offer";
myMsg = 'pong';
rtc.createAnswer(function(anwser) {
rtc.setLocalDescription(anwser, function () {
console.log('<- ' + JSON.stringify(anwser));
ws.send(JSON.stringify(anwser));
});
}, console.error);
}
else if (data.type == 'answer') {
document.getElementById("output6").innerHTML="received SessiionDescription answer";
};
}
// CHANGE: Chrome with offer bug asked to initiate the offer.
else if (data.reverseInitiate === true) {
document.getElementById("output8").innerHTML="was asked to reverseInitiate, I doOffer";
doOffer();
}
else {
rtc.addIceCandidate(new RTCIceCandidate(data));
}
};
ws.onclose = function() {
create.disabled = true;
};
// Create an offer
// CHANGE: Create function for offer, so that it may be called from ws.onmessage
function doOffer(){
rtc.createOffer(function(offer) {
rtc.setLocalDescription(offer, function () {
offer.sdp = offer.sdp;
console.log(offer.sdp);
console.log('<- ' + JSON.stringify(offer));
ws.send(JSON.stringify(offer));
});
}, console.error);
}
create.onclick = function() {
// CHANGE: If this client is not able to negotiate a data channel, send a
// message to the peer asking them to offer the channel.
if (rtcCanInitiateDataOffer){
doOffer();
}
else {
document.getElementById("output7").innerHTML="sending reverseInitiate";
ws.send(JSON.stringify( {reverseInitiate:true} ));
};
};
</script>
</body>
</html>