UNPKG

@readymade/transmit

Version:

Swiss-army knife for communicating over WebRTC DataChannel, WebSocket or Touch OSC

339 lines (337 loc) 12.4 kB
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