Skip to content

Instantly share code, notes, and snippets.

@Gronis
Created August 17, 2021 16:54
Show Gist options
  • Save Gronis/071b93f10ec9db3ea63a959e844bddea to your computer and use it in GitHub Desktop.
Save Gronis/071b93f10ec9db3ea63a959e844bddea to your computer and use it in GitHub Desktop.
A (partially) wrtc compatible wrapper layer around node-datachannel so that node-datachannel can be used as wrtc backend for various webrtc libraries when running inside a nodejs environment.
import libdatachannel from 'node-datachannel';
const do_nothing = (..._) => { };
const decorate_channel = channel => {
Object.defineProperty(channel, "readyState", {
get: function () {
return channel.isOpen() ? 'open' : 'closed'
}
});
Object.defineProperty(channel, "label", {
get: function () {
return channel.getLabel();
}
});
channel.send = data => {
if (!channel.isOpen()) {
return;
}
if (typeof data === 'string' || data instanceof String) {
try {
channel.sendMessage(data);
} catch (e) {
console.log("err, sendMessage", e)
}
} else if (data instanceof Buffer) {
try {
channel.sendMessageBinary(data);
} catch (e) {
console.log("err, sendMessageBinary", e)
}
} else {
throw 'Can only send string or Buffer object';
}
}
channel.onopen = do_nothing
channel.onOpen(() => {
if (!channel.isOpen()) {
channel.onerror('datachannel did not open properly.')
} else {
channel.onopen();
}
})
channel.onmessage = do_nothing
channel.onMessage(data => channel.onmessage({ data }))
channel.onclose = do_nothing
channel.onClosed(() => channel.onclose())
channel.onerror = do_nothing
channel.onError(() => channel.onerror())
return channel;
}
class RTCPeerConnection {
constructor(options) {
this.__state == 'new';
this.__connected = false;
this.__localDescription = null;
this.__remoteDescription = null;
this.__peer = new libdatachannel.PeerConnection('', {
iceServers: [
'stun:stun.l.google.com:19302',
'stun:global.stun.twilio.com:3478',
]
});
this.ondatachannel = do_nothing
this.onicecandidate = do_nothing
this.__peer.onDataChannel(channel => {
this.ondatachannel({
channel: decorate_channel(channel)
})
});
this.__peer.onStateChange((state) => {
this.__state = state;
// console.log("State change:", state);
if (state == 'connecting') {
// This is the initial state
}
if (state == 'connected') {
this.__connected = true;
}
if (state == 'failed') {
this.__connected = false;
this.close()
}
if (state == 'disconnected') {
this.__connected = false;
// Calling close here on __peer will result in error.
}
});
this.__peer.onGatheringStateChange((state) => {
// console.log("this.__peer GatheringState:", state);
});
this.__peer.onLocalDescription((sdp, type) => {
// console.log("this.__peer SDP:", sdp, " Type:", type);
this.__localDescription = {
type: type,
// For firefox, the sdp sometimes create empty 'a=' lines which causes sdp
// parse errors in firefox. Just filter out such lines in case it happens.
sdp: sdp.replace(/a=\r?\n/g, ''),
}
if (this.__localDescriptionCallback) {
this.__localDescriptionCallback()
this.__localDescriptionCallback = null;
}
});
this.__peer.onLocalCandidate((candidate, mid) => {
if (this.__localDescription && candidate) {
this.__localDescription.sdp += candidate + '\r\n';
}
this.onicecandidate({
type: 'candidate',
candidate: {
candidate: candidate.replace('a=candidate:', 'candidate:'),
}
})
});
}
get localDescription(){
return this.__localDescription;
}
setLocalDescription(signal) {
// This function is not necessary for node-datachannel library.
// We still need to define it though for compliance with
// browser wrtc implementation.
return;
}
get remoteDescription(){
return this.__remoteDescription;
}
setRemoteDescription(signal) {
if (this.__connected) return;
if (this.__remoteDescription) return;
this.__remoteDescription = signal;
this.__peer.setRemoteDescription(signal.sdp, signal.type);
}
addIceCandidate(candidate) {
if (this.__connected) return;
// TODO: mid is hardcoded to 0 now. Probably bad...
this.__peer.addRemoteCandidate('a=' + candidate.candidate, '0');
}
createDataChannel(name) {
return decorate_channel(this.__peer.createDataChannel(name));
}
async __createSignal(type) {
const callback = () => {
const offer = this.__localDescription;
if (offer.type == type) {
return offer;
} else {
throw 'Cannot create offer/answer for this peer';
}
}
const offer = this.__localDescription;
if (offer) {
return callback()
} else {
return new Promise((accept, reject) => {
if (this.__localDescriptionCallback) {
reject('Cannot create offer/answer at this stage.')
} else {
this.__localDescriptionCallback = () => {
try { accept(callback()) }
catch (e) { reject(e) }
}
}
});
}
}
createOffer() {
if (this.__connected) return;
return this.__createSignal('offer')
}
createAnswer() {
if (this.__connected) return;
return this.__createSignal('answer')
}
close() {
this.__peer.close();
}
get connectionState() {
return this.__state;
}
};
class RTCIceCandidate {
constructor(candidate) {
this.candidate = candidate;
}
};
export default {
RTCPeerConnection,
RTCIceCandidate,
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment