Created
August 17, 2021 16:54
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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