{"id":19000280,"url":"https://github.com/odoo/sfu","last_synced_at":"2025-04-22T17:04:56.306Z","repository":{"id":210857442,"uuid":"727265561","full_name":"odoo/sfu","owner":"odoo","description":"Odoo's Selective Forwarding Unit","archived":false,"fork":false,"pushed_at":"2025-04-09T09:14:49.000Z","size":209,"stargazers_count":10,"open_issues_count":2,"forks_count":6,"subscribers_count":12,"default_branch":"master","last_synced_at":"2025-04-09T10:30:57.247Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/odoo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-12-04T14:18:13.000Z","updated_at":"2025-04-09T09:14:50.000Z","dependencies_parsed_at":"2024-04-12T08:37:42.075Z","dependency_job_id":"52e30bcf-a1bc-410d-a983-b609b43533a6","html_url":"https://github.com/odoo/sfu","commit_stats":null,"previous_names":["odoo/sfu"],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/odoo%2Fsfu","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/odoo%2Fsfu/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/odoo%2Fsfu/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/odoo%2Fsfu/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/odoo","download_url":"https://codeload.github.com/odoo/sfu/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249279418,"owners_count":21242923,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-08T18:06:46.451Z","updated_at":"2025-04-16T21:31:43.752Z","avatar_url":"https://github.com/odoo.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Odoo SFU\n\n## Overview\n\nContains the code for the SFU (Selective Forwarding Unit) server \nused in [Odoo Discuss](https://www.odoo.com/app/discuss). The SFU server is responsible for handling the WebRTC connections\nbetween users and providing channels to coordinate these connections.\n\nThe server is not stand-alone, it does not serve any HTML or any interface code for calls. It only contains\nthe SFU and a [client bundle/library](#client-api-bundle) to connect to it.\n\n## Prerequisites\n- [Node.js 20.9.0 (LTS)](https://nodejs.org/)\n\n## Before deployment\n\nBuild the client bundle\n\n ```bash\n     npm install\n     npm run build\n ```\n\nOnce the bundle is built, it can be added to the assets of your main server, and\ninteracted with as described [here](#client-api-bundle).\n\n## Deployment\n\n1. Install dependencies.\n    ```bash\n        npm ci -omit=dev\n    ```\n2. Run the SFU server.\n    ```bash\n        npm PROXY=1 PUBLIC_IP=134.123.222.111 AUTH_KEY=u6bsUQEWrHdKIuYplirRnbBmLbrKV5PxKG7DtA71mng= run start\n    ```\n\nThe available environment variables are:\n\n- **PUBLIC_IP** (required): used to establish webRTC connections to the server\n- **AUTH_KEY** (required): the base64 encoded encryption key used for authentication\n- **HTTP_INTERFACE**:  HTTP/WS interface, defaults to \"0.0.0.0\" (listen on all interfaces)\n- **PORT**: port for HTTP/WS, defaults to standard ports\n- **RTC_INTERFACE**: Interface address for RTC, defaults to \"0.0.0.0\"\n- **PROXY**: set if behind a proxy, the proxy must properly implement \"x-forwarded-for\", \"x-forwarded-proto\" and \"x-forwarded-host\"\n- **AUDIO_CODECS**: comma separated list of audio codecs to use, default to all available\n- **VIDEO_CODECS**: comma separated list of video codecs to use, default to all available\n- **RTC_MIN_PORT**: Lower bound for the range of ports used by the RTC server, must be open in both TCP and UDP\n- **RTC_MAX_PORT**: Upper bound for the range of ports used by the RTC server, must be open in both TCP and UDP\n- **MAX_BUF_IN**: if set, limits the incoming buffer size per session (user)\n- **MAX_BUF_OUT**: if set, limits the outgoing buffer size per session (user)\n- **MAX_BITRATE_IN**: if set, limits the incoming bitrate per session (user), defaults to 8mbps\n- **MAX_BITRATE_OUT**: if set, limits the outgoing bitrate per session (user), defaults to 10mbps\n- **MAX_VIDEO_BITRATE**: if set, defines the `maxBitrate` of the highest encoding layer (simulcast), defaults to 4mbps\n- **CHANNEL_SIZE**: the maximum amount of users per channel, defaults to 100\n- **WORKER_LOG_LEVEL**: \"none\" | \"error\" | \"warn\" | \"debug\", will only work if `DEBUG` is properly set.\n- **LOG_LEVEL**: \"none\" | \"error\" | \"warn\" | \"info\" | \"debug\" | \"verbose\"\n- **LOG_TIMESTAMP**: adds a timestamp to the log lines, defaults to true, to disable it, set to \"disable\", \"false\", \"none\", \"no\" or \"0\"\n- **LOG_COLOR**: If set, colors the log lines based on their level\n- **DEBUG**: an env variable used by the [debug](https://www.npmjs.com/package/debug) module. e.g.: `DEBUG=*`, `DEBUG=mediasoup*`\n\n\nSee [config.js](./src/config.js) for more details and examples.\n\n## Binding the SFU and the Odoo server together\n\n### On the SFU\nSet the `AUTH_KEY` env variable with  the base64 encryption key that can be used to authenticate connections to the server.\n\n### On Odoo \nGo to the Discuss settings and configure the `RTC Server URL` and `RTC server KEY` fields. The `RTC server KEY`\nmust be the same base64 encoded string as `AUTH_KEY` on the SFU server.\n\n## Interacting with the SFU server\n\nThe SFU server responds to the following IPC signals:\n\n- `SIGFPE(8)`: Restarts the server.\n- `SIGALRM(14)`: Initiates a soft reset by closing all sessions, but keeps services alive.\n- `SIGIO(29)`: Prints server statistics, such as the number of channels, sessions, bitrate.\n\nSee [server.js](./src/server.js) for more details.\n\n## HTTP API\n\n- GET `/v1/stats`: returns the server statistics as an array with one entry per channel, in JSON:\n    ```json\n    [\n        {\n            \"createDate\": \"2023-10-25T04:57:45.453Z\",\n            \"uuid\": \"86079c25-9cf8-4d58-9dea-cef44cf845e2\",\n            \"remoteAddress\": \"whoever-requested-the-room.com\",\n            \"sessionsStats\": {\n                \"incomingBitRate\": {\n                    \"audio\": 5,\n                    \"camera\": 700000,\n                    \"screen\": 0,\n                    \"total\": 700005\n                    },\n                \"count\": 3,\n                \"cameraCount\": 2,\n                \"screenCount\": 0\n            },\n            \"webRtcEnabled\": true\n        }\n    ]\n    ```\n- GET `/v1/channel`: create a channel and returns information required to connect to it in JSON:\n   ```json\n   {\n      \"uuid\": \"31dcc5dc-4d26-453e-9bca-ab1f5d268303\",\n      \"url\": \"https://example-odoo-sfu.com\"\n  }\n  ```\n\n- POST `/v1/disconnect` disconnects sessions, expects the body to be a Json Web Token formed as such:\n    ```js\n  jwt.sign(\n    {\n      \"sessionIdsByChannel\": {\n        [channelUUID]: [sessionId1, sessionId2]\n      }\n    },\n    \"HS256\",\n  );\n    ```\n\nSee [http.js](./src/services/http.js) for more details.\n\n## Client API (bundle)\n\nThe bundle built with the `build` script in [package.json](./package.json) can be imported\nin the client(js) code that implements the call feature like this:\n\n```js\nimport { SfuClient, SFU_CLIENT_STATE } from \"/bundle/odoo_sfu.js\";\nconst sfu = new SfuClient();\n```\n`SfuClient` exposes the following API:\n\n- connect()\n    ```js\n    sfu.connect(\"https://my-sfu.com\", jsonWebToken, { iceServers });\n    ```\n- disconnect()\n    ```js\n    sfu.disconnect();\n    sfu.state === SFU_CLIENT_STATE.DISCONNECTED; // true\n    ```\n- broadcast()\n    ```js\n    // in the sender's client\n    sfu.broadcast(\"hello\");\n    ```\n    ```js\n    // in the clients of other members of that channel\n    sfu.addEventListener(\"update\", ({ detail: { name, payload } }) =\u003e {\n        switch (name) {\n            case \"broadcast\":\n                {\n                    const { senderId, message } = payload;\n                    console.log(`${senderId} says: \"${message}\"`); // 87 says \"hello\"\n                }\n                return;\n            // ...\n        }\n    });\n    ```\n- updateUpload()\n    ```js\n    const audioStream = await window.navigator.mediaDevices.getUserMedia({\n        audio: true,\n    });\n    const audioTrack = audioStream.getAudioTracks()[0];\n    await sfu.updateUpload(\"audio\", audioTrack); // we upload a new audio track to the server\n    await sfu.updateUpload(\"audio\", undefined); // we stop uploading audio\n    ```\n- updateDownload()\n    ```js\n    sfu.updateDownload(remoteSessionId, {\n        camera: false, // we want to stop downloading their camera\n        screen: true, // we want to download their screen\n    });\n    ```\n- updateInfo()\n    ```js\n    sfu.updateInfo({\n        isMuted: true,\n        isCameraOn: false,\n        // ...\n    });\n    ```\n- getStats()\n    ```js\n    const { uploadStats, downloadStats, ...producerStats } = await sfu.getStats();\n    typeof uploadStats === \"RTCStatsReport\"; // true\n    typeof producerStats[\"camera\"] === \"RTCStatsReport\"; // true\n    // see https://w3c.github.io/webrtc-pc/#rtcstatsreport-object\n    ```\n- @fires \"update\"\n    ```js\n    sfu.addEventListener(\"update\", ({ detail: { name, payload } }) =\u003e {\n        switch (name) {\n            case \"track\":\n                {\n                    const { sessionId, type, track, active } = payload;\n                    const remoteParticipantViewer = findParticipantById(sessionId);\n                    if (type === \"camera\") {\n                        remoteParticipantViewer.cameraTrack = track;\n                        remoteParticipantViewer.isCameraOn = active; // indicates whether the track is active or paused\n                    }\n                }\n                return;\n            // ...\n        }\n    });\n    ```\n- @fires \"stateChange\"\n    ```js\n    sfu.addEventListener(\"stateChange\", ({ detail: { state, cause } }) =\u003e {\n        switch (state) {\n            case SFU_CLIENT_STATE.CONNECTED:\n                console.log(\"Connected to the SFU server.\");\n                // we can start uploading now\n                client.updateUpload(\"audio\", myMicrophoneTrack);\n                client.updateUpload(\"camera\", myWebcamTrack);\n                break;\n            case SFU_CLIENT_STATE.CLOSED:\n                console.log(\"Connection to the SFU server closed.\");\n                break;\n            // ...\n        }\n    });\n    ```\n\nsee [client.js](./src/client.js) for more details.\n\n## Architecture\n\n![](./data/architecture.svg)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fodoo%2Fsfu","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fodoo%2Fsfu","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fodoo%2Fsfu/lists"}