Contains the code for the SFU (Selective Forwarding Unit) server used in Odoo Discuss. The SFU server is responsible for handling the WebRTC connections between users and providing channels to coordinate these connections.
The server is not stand-alone, it does not serve any HTML or any interface code for calls. It only contains the SFU and a client bundle/library to connect to it.
Build the client bundle
npm install
npm run build
Once the bundle is built, it can be added to the assets of your main server, and interacted with as described here.
- Install dependencies.
npm ci -omit=dev
- Run the SFU server.
npm PROXY=1 PUBLIC_IP=134.123.222.111 AUTH_KEY=u6bsUQEWrHdKIuYplirRnbBmLbrKV5PxKG7DtA71mng= run start
The available environment variables are:
- PUBLIC_IP (required): used to establish webRTC connections to the server
- AUTH_KEY (required): the base64 encoded encryption key used for authentication
- HTTP_INTERFACE: HTTP/WS interface, defaults to "0.0.0.0" (listen on all interfaces)
- PORT: port for HTTP/WS, defaults to standard ports
- RTC_INTERFACE: Interface address for RTC, defaults to "0.0.0.0"
- PROXY: set if behind a proxy, the proxy must properly implement "x-forwarded-for", "x-forwarded-proto" and "x-forwarded-host"
- AUDIO_CODECS: comma separated list of audio codecs to use, default to all available
- VIDEO_CODECS: comma separated list of video codecs to use, default to all available
- RTC_MIN_PORT: Lower bound for the range of ports used by the RTC server, must be open in both TCP and UDP
- RTC_MAX_PORT: Upper bound for the range of ports used by the RTC server, must be open in both TCP and UDP
- MAX_BUF_IN: if set, limits the incoming buffer size per session (user)
- MAX_BUF_OUT: if set, limits the outgoing buffer size per session (user)
- MAX_BITRATE_IN: if set, limits the incoming bitrate per session (user), defaults to 8mbps
- MAX_BITRATE_OUT: if set, limits the outgoing bitrate per session (user), defaults to 10mbps
- MAX_VIDEO_BITRATE: if set, defines the
maxBitrate
of the highest encoding layer (simulcast), defaults to 4mbps - CHANNEL_SIZE: the maximum amount of users per channel, defaults to 100
- WORKER_LOG_LEVEL: "none" | "error" | "warn" | "debug", will only work if
DEBUG
is properly set. - LOG_LEVEL: "none" | "error" | "warn" | "info" | "debug" | "verbose"
- LOG_TIMESTAMP: adds a timestamp to the log lines, defaults to true, to disable it, set to "disable", "false", "none", "no" or "0"
- LOG_COLOR: If set, colors the log lines based on their level
- DEBUG: an env variable used by the debug module. e.g.:
DEBUG=*
,DEBUG=mediasoup*
See config.js for more details and examples.
Set the AUTH_KEY
env variable with the base64 encryption key that can be used to authenticate connections to the server.
Go to the Discuss settings and configure the RTC Server URL
and RTC server KEY
fields. The RTC server KEY
must be the same base64 encoded string as AUTH_KEY
on the SFU server.
The SFU server responds to the following IPC signals:
SIGFPE(8)
: Restarts the server.SIGALRM(14)
: Initiates a soft reset by closing all sessions, but keeps services alive.SIGIO(29)
: Prints server statistics, such as the number of channels, sessions, bitrate.
See server.js for more details.
-
GET
/v1/stats
: returns the server statistics as an array with one entry per channel, in JSON:[ { "createDate": "2023-10-25T04:57:45.453Z", "uuid": "86079c25-9cf8-4d58-9dea-cef44cf845e2", "remoteAddress": "whoever-requested-the-room.com", "sessionsStats": { "incomingBitRate": { "audio": 5, "camera": 700000, "screen": 0, "total": 700005 }, "count": 3, "cameraCount": 2, "screenCount": 0 }, "webRtcEnabled": true } ]
-
GET
/v1/channel
: create a channel and returns information required to connect to it in JSON:{ "uuid": "31dcc5dc-4d26-453e-9bca-ab1f5d268303", "url": "https://example-odoo-sfu.com" }
-
POST
/v1/disconnect
disconnects sessions, expects the body to be a Json Web Token formed as such:jwt.sign( { "sessionIdsByChannel": { [channelUUID]: [sessionId1, sessionId2] } }, "HS256", );
See http.js for more details.
The bundle built with the build
script in package.json can be imported
in the client(js) code that implements the call feature like this:
import { SfuClient, SFU_CLIENT_STATE } from "/bundle/odoo_sfu.js";
const sfu = new SfuClient();
SfuClient
exposes the following API:
- connect()
sfu.connect("https://my-sfu.com", jsonWebToken, { iceServers });
- disconnect()
sfu.disconnect(); sfu.state === SFU_CLIENT_STATE.DISCONNECTED; // true
- broadcast()
// in the sender's client sfu.broadcast("hello");
// in the clients of other members of that channel sfu.addEventListener("update", ({ detail: { name, payload } }) => { switch (name) { case "broadcast": { const { senderId, message } = payload; console.log(`${senderId} says: "${message}"`); // 87 says "hello" } return; // ... } });
- updateUpload()
const audioStream = await window.navigator.mediaDevices.getUserMedia({ audio: true, }); const audioTrack = audioStream.getAudioTracks()[0]; await sfu.updateUpload("audio", audioTrack); // we upload a new audio track to the server await sfu.updateUpload("audio", undefined); // we stop uploading audio
- updateDownload()
sfu.updateDownload(remoteSessionId, { camera: false, // we want to stop downloading their camera screen: true, // we want to download their screen });
- updateInfo()
sfu.updateInfo({ isMuted: true, isCameraOn: false, // ... });
- getStats()
const { uploadStats, downloadStats, ...producerStats } = await sfu.getStats(); typeof uploadStats === "RTCStatsReport"; // true typeof producerStats["camera"] === "RTCStatsReport"; // true // see https://w3c.github.io/webrtc-pc/#rtcstatsreport-object
- @fires "update"
sfu.addEventListener("update", ({ detail: { name, payload } }) => { switch (name) { case "track": { const { sessionId, type, track, active } = payload; const remoteParticipantViewer = findParticipantById(sessionId); if (type === "camera") { remoteParticipantViewer.cameraTrack = track; remoteParticipantViewer.isCameraOn = active; // indicates whether the track is active or paused } } return; // ... } });
- @fires "stateChange"
sfu.addEventListener("stateChange", ({ detail: { state, cause } }) => { switch (state) { case SFU_CLIENT_STATE.CONNECTED: console.log("Connected to the SFU server."); // we can start uploading now client.updateUpload("audio", myMicrophoneTrack); client.updateUpload("camera", myWebcamTrack); break; case SFU_CLIENT_STATE.CLOSED: console.log("Connection to the SFU server closed."); break; // ... } });
see client.js for more details.