├── Dockerfile ├── README.md ├── docker-compose.yml ├── index.js └── package.json /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Node.js image as the base image 2 | FROM node:16 3 | 4 | # Set the working directory inside the container 5 | WORKDIR /app 6 | 7 | # Copy package.json and package-lock.json (if available) 8 | COPY package*.json ./ 9 | 10 | # Install dependencies 11 | RUN npm install 12 | 13 | # Copy the rest of the application code 14 | COPY . . 15 | 16 | # Make the script executable 17 | RUN chmod +x index.js 18 | 19 | # Expose the port the app runs on 20 | EXPOSE 4444 21 | 22 | # Command to run the app 23 | CMD ["node", "index.js"] 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # y-webrtc-signaling 2 | 3 | 4 | the deploy document: https://lobehub.com/docs/self-hosting/advanced/webrtc 5 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | y-webrtc-signaling: 5 | build: . 6 | ports: 7 | - "4444:4444" 8 | environment: 9 | - PORT=4444 10 | restart: always 11 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { WebSocketServer } from 'ws' 4 | import http from 'http' 5 | import * as map from 'lib0/map' 6 | 7 | const wsReadyStateConnecting = 0 8 | const wsReadyStateOpen = 1 9 | const wsReadyStateClosing = 2 // eslint-disable-line 10 | const wsReadyStateClosed = 3 // eslint-disable-line 11 | 12 | const pingTimeout = 30000 13 | 14 | const port = process.env.PORT || 4444 15 | const wss = new WebSocketServer({ noServer: true }) 16 | 17 | const server = http.createServer((request, response) => { 18 | response.writeHead(200, { 'Content-Type': 'text/plain' }) 19 | response.end('okay') 20 | }) 21 | 22 | /** 23 | * Map froms topic-name to set of subscribed clients. 24 | * @type {Map>} 25 | */ 26 | const topics = new Map() 27 | 28 | /** 29 | * @param {any} conn 30 | * @param {object} message 31 | */ 32 | const send = (conn, message) => { 33 | if (conn.readyState !== wsReadyStateConnecting && conn.readyState !== wsReadyStateOpen) { 34 | conn.close() 35 | } 36 | try { 37 | conn.send(JSON.stringify(message)) 38 | } catch (e) { 39 | conn.close() 40 | } 41 | } 42 | 43 | /** 44 | * Setup a new client 45 | * @param {any} conn 46 | */ 47 | const onconnection = conn => { 48 | /** 49 | * @type {Set} 50 | */ 51 | const subscribedTopics = new Set() 52 | let closed = false 53 | // Check if connection is still alive 54 | let pongReceived = true 55 | const pingInterval = setInterval(() => { 56 | if (!pongReceived) { 57 | conn.close() 58 | clearInterval(pingInterval) 59 | } else { 60 | pongReceived = false 61 | try { 62 | conn.ping() 63 | } catch (e) { 64 | conn.close() 65 | } 66 | } 67 | }, pingTimeout) 68 | conn.on('pong', () => { 69 | pongReceived = true 70 | }) 71 | conn.on('close', () => { 72 | subscribedTopics.forEach(topicName => { 73 | const subs = topics.get(topicName) || new Set() 74 | subs.delete(conn) 75 | if (subs.size === 0) { 76 | topics.delete(topicName) 77 | } 78 | }) 79 | subscribedTopics.clear() 80 | closed = true 81 | }) 82 | conn.on('message', /** @param {object} message */ message => { 83 | if (typeof message === 'string' || message instanceof Buffer) { 84 | message = JSON.parse(message) 85 | } 86 | if (message && message.type && !closed) { 87 | switch (message.type) { 88 | case 'subscribe': 89 | /** @type {Array} */ (message.topics || []).forEach(topicName => { 90 | if (typeof topicName === 'string') { 91 | // add conn to topic 92 | const topic = map.setIfUndefined(topics, topicName, () => new Set()) 93 | topic.add(conn) 94 | // add topic to conn 95 | subscribedTopics.add(topicName) 96 | } 97 | }) 98 | break 99 | case 'unsubscribe': 100 | /** @type {Array} */ (message.topics || []).forEach(topicName => { 101 | const subs = topics.get(topicName) 102 | if (subs) { 103 | subs.delete(conn) 104 | } 105 | }) 106 | break 107 | case 'publish': 108 | if (message.topic) { 109 | const receivers = topics.get(message.topic) 110 | if (receivers) { 111 | message.clients = receivers.size 112 | receivers.forEach(receiver => 113 | send(receiver, message) 114 | ) 115 | } 116 | } 117 | break 118 | case 'ping': 119 | send(conn, { type: 'pong' }) 120 | } 121 | } 122 | }) 123 | } 124 | wss.on('connection', onconnection) 125 | 126 | server.on('upgrade', (request, socket, head) => { 127 | // You may check auth of request here.. 128 | /** 129 | * @param {any} ws 130 | */ 131 | const handleAuth = ws => { 132 | wss.emit('connection', ws, request) 133 | } 134 | wss.handleUpgrade(request, socket, head, handleAuth) 135 | }) 136 | 137 | server.listen(port) 138 | 139 | console.log('Signaling server running on localhost:', port) 140 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "y-webrtc-signaling", 3 | "version": "1.0.0", 4 | "description": "WebRTC provider for Yjs", 5 | "main": "./index.js", 6 | "sideEffects": false, 7 | "type": "module", 8 | "license": "MIT", 9 | "dependencies": { 10 | "lib0": "^0.2.42", 11 | "ws": "^8.14.2" 12 | } 13 | } 14 | --------------------------------------------------------------------------------