@readymade/transmit
Version:
Swiss-army knife for communicating over WebRTC DataChannel, WebSocket or Touch OSC
339 lines (337 loc) • 12.4 kB
JavaScript
const uuid = function () {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
};
const randomSharedKey = function () {
let text = '';
const possible = 'ABCDEFGHJKMNOPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz023456789';
for (let i = 0; i < 5; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text.charAt(0).toUpperCase() + text.slice(1);
};
class Transmitter {
constructor(config) {
this.debug = true;
this.config = config;
this.id = this.config.id || uuid();
this.sharedKey = this.config.sharedKey || randomSharedKey();
this.channelName = this.config.channelName
? this.config.channelName
: 'channel';
this.hasPulse = false;
this.isOpen = false;
this.connections = {};
this.remotePeerId = null;
this.store = { messages: [] };
this.rtcConfiguration = config.rtcConfig
? config.rtcConfig
: {
iceServers: [
{
urls: ['stun:stun.l.google.com:19302'],
},
],
};
this.dataChannelConfig = {
ordered: false,
maxPacketLifeTime: 1000,
};
this.ws = {
osc: this.config.serverConfig?.ws?.osc
? new WebSocket(this.getWebSocketAddress(this.config.serverConfig?.ws?.osc))
: null,
signal: this.config.serverConfig?.ws?.signal
? new WebSocket(this.getWebSocketAddress(this.config.serverConfig?.ws?.signal))
: null,
announce: null,
message: this.config.serverConfig?.ws?.message
? new WebSocket(this.getWebSocketAddress(this.config.serverConfig?.ws?.message))
: null,
};
const connectMessage = {
sharedKey: this.sharedKey,
id: this.id,
type: 'connect',
method: 'webrtc',
};
if (this.debug) {
console.log('id: ', this.id);
console.log('sharedKey: ', this.sharedKey);
}
this.ws.signal?.addEventListener('open', () => {
if (this.debug)
console.log('Connected to Signal WebSocket server');
this.ws.signal.send(JSON.stringify(connectMessage));
});
this.ws.signal?.addEventListener('message', (event) => {
if (this.debug)
console.log('Message from Signal server:', JSON.parse(event.data));
const data = JSON.parse(event.data);
if (data.id !== this.id) {
this.onSignal(data);
}
});
this.ws.message?.addEventListener('open', () => {
if (this.debug)
console.log('Connected to Message WebSocket server');
this.ws.message.send(JSON.stringify(connectMessage));
});
this.ws.message?.addEventListener('message', (event) => {
if (this.debug)
console.log('Message from Message server:', JSON.parse(event.data));
const data = JSON.parse(event.data);
if (data.id !== this.id) {
this.onWebSocketMessage(event);
}
});
if (this.config.serverConfig?.ws?.signal &&
this.config.serverConfig?.ws?.announce) {
this.init();
this.sendAnnounce();
}
}
init() {
const RTCPeerConnection = window.RTCPeerConnection ||
window.mozRTCPeerConnection ||
window.webkitRTCPeerConnection;
this.peerConnection = new RTCPeerConnection(this.rtcConfiguration);
this.peerConnection.onicecandidate = this.onICECandidate.bind(this);
this.peerConnection.oniceconnectionstatechange =
this.onICEStateChange.bind(this);
this.channel = this.peerConnection.createDataChannel(this.channelName, this.dataChannelConfig);
this.channel.onopen = this.onDataChannelOpen.bind(this);
this.channel.onmessage = this.onDataChannelMessage.bind(this);
this.connections[this.remotePeerId] = this.peerConnection;
if (this.debug)
console.log('Setting up peer connection with ' + this.remotePeerId);
}
sendAnnounce() {
const connectMessage = {
sharedKey: this.sharedKey,
id: this.id,
type: 'connect',
method: 'webrtc',
};
this.ws.announce = new WebSocket(this.getWebSocketAddress(this.config.serverConfig?.ws?.announce));
this.ws.announce.addEventListener('open', () => {
if (this.debug)
console.log('Connected to Announce WebSocket server');
this.ws.announce.send(JSON.stringify(connectMessage));
if (this.debug)
console.log('Announced our sharedKey is ' + this.sharedKey);
if (this.debug)
console.log('Announced our ID is ' + this.id);
});
this.ws.announce.addEventListener('close', () => {
if (this.debug)
console.log('Disconnected from WebSocket server');
});
this.ws.announce.addEventListener('error', (event) => {
console.error('WebSocket error:', event);
});
this.ws.announce.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
this.onAnnounce(data);
});
}
onAnnounce(snapshot) {
const msg = snapshot;
if (msg.id != this.id && msg.sharedKey == this.sharedKey) {
if (this.debug)
console.log('Discovered matching announcement from ' + msg.id);
this.remotePeerId = msg.id;
this.connect();
}
}
sendSignal(msg) {
msg.source = this.id;
msg.target = this.remotePeerId;
this.ws.signal?.send(JSON.stringify(msg));
}
connect() {
this.peerConnection
.createOffer()
.then((sessionDescription) => {
if (this.debug)
console.log('Sending offer to ' + this.remotePeerId);
this.peerConnection.setLocalDescription(new RTCSessionDescription(sessionDescription));
this.sendSignal(sessionDescription);
})
.catch(function (err) {
console.error('Could not create offer for ' + this.remotePeerId, err);
});
}
onOffer(msg) {
const RTCSessionDescription = window.RTCSessionDescription ||
window.mozRTCSessionDescription;
this.hasPulse = true;
if (this.debug)
console.log('Client has pulse');
this.remotePeerId = msg.source;
this.init();
this.peerConnection.setRemoteDescription(new RTCSessionDescription(msg));
this.peerConnection
.createAnswer()
.then((sessionDescription) => {
if (this.debug)
console.log('Sending answer to ' + msg.source);
this.sendSignal(sessionDescription);
this.peerConnection.setLocalDescription(new RTCSessionDescription(sessionDescription));
})
.catch(function (err) {
console.error('Could not create answer for ' + this.remotePeerId, err);
});
}
onAnswerSignal(msg) {
const RTCSessionDescription = window.RTCSessionDescription ||
window.mozRTCSessionDescription;
if (this.debug)
console.log('Handling answer from ' + this.remotePeerId);
this.peerConnection.setRemoteDescription(new RTCSessionDescription(msg));
}
onCandidateSignal(msg) {
const candidate = new window.RTCIceCandidate(msg);
if (this.debug)
console.log('Adding candidate to peerConnection: ' + this.remotePeerId);
this.peerConnection.addIceCandidate(candidate);
}
onSignal(snapshot) {
const msg = snapshot;
const sender = msg.source;
const type = msg.type;
if (!this.isOpen) {
if (this.debug)
console.log("Received a '" +
type +
"' signal from " +
sender +
' of type ' +
type);
if (type == 'offer') {
this.onOffer(msg);
}
else if (type == 'answer') {
this.onAnswerSignal(msg);
}
else if (type == 'candidate' && this.hasPulse) {
this.onCandidateSignal(msg);
}
}
}
onICEStateChange() {
if (this.peerConnection.iceConnectionState == 'disconnected') {
if (this.debug)
console.log('Client disconnected!');
if (this.config.onDisconnect) {
this.config.onDisconnect({ remotePeerId: this.remotePeerId });
}
}
}
onICECandidate(ev) {
const candidate = ev.candidate;
if (candidate) {
if (this.debug)
console.log('Sending candidate to ' + this.remotePeerId);
this.sendSignal(candidate);
}
else {
if (this.debug)
console.log('All candidates sent');
}
}
onDataChannelOpen() {
if (this.debug)
console.log('Data channel created! The channel is: ' + this.channel.readyState);
if (this.channel.readyState == 'open') {
this.isOpen = true;
if (this.config.onConnect) {
this.config.onConnect();
}
}
}
onDataChannelClosed() {
if (this.debug)
console.log('The data channel has been closed!');
if (this.config.onDisconnect) {
this.config.onDisconnect({ remotePeerId: this.remotePeerId });
}
}
onWebSocketSignal(snapshot) {
const msg = snapshot.val();
const sender = msg.source;
const type = msg.type;
if (sender === this.remotePeerId) {
if (this.debug)
console.log("Received a '" +
type +
"' signal from " +
sender +
' of type ' +
type);
if (type == 'offer') {
this.onOffer(msg);
}
else if (type == 'answer') {
this.onAnswerSignal(msg);
}
else if (type == 'candidate' && this.hasPulse) {
this.onCandidateSignal(msg);
}
}
}
createMessage(data) {
const msg = {
id: uuid(),
type: 'message',
source: this.id,
target: this.remotePeerId,
payload: data,
};
if (this.debug)
console.log('Sending message from: ' + msg.source + ' to: ' + msg.target);
return JSON.stringify(msg);
}
send(data) {
const msg = this.createMessage(data);
this.channel.send(msg);
}
sendSocketMessage(data) {
const msg = this.createMessage(data);
this.ws.message?.send(msg);
}
sendTouchOSCMessage(address, data) {
const jsonString = JSON.stringify({
address,
args: data,
});
this.ws.osc?.send(jsonString);
}
onMessage(message) {
const data = JSON.parse(message.data);
this.store.messages.push(data);
if (data.target === this.id) {
if (this.debug)
console.log('Received Message: ', message);
if (this.config.onMessage) {
this.config.onMessage(data);
}
}
}
onDataChannelMessage(message) {
this.onMessage(message);
}
onWebSocketMessage(message) {
this.onMessage(message);
}
getWebSocketAddress(config) {
return `${config?.protocol ? config.protocol : 'ws'}://${config?.hostname ? config.hostname : 'localhost'}:${config?.port ? config.port : 4448}`;
}
findMessagesByProperty(prop, value) {
return this.store.messages.filter((message) => message[prop] === value);
}
}
export { Transmitter };
//# sourceMappingURL=index.js.map