From beafcc20da055047476a2abc7fddd635e44b3408 Mon Sep 17 00:00:00 2001 From: TSO Date: Thu, 14 Dec 2023 11:30:51 +0100 Subject: [PATCH] [IMP] add simulcast This commit adds the support for simulcast: Simulcast is a way to let producers sent multiple version of the streams as different layers of quality. This allows the server to select the optimal layer based on the available bandwidth for each client. Two env variables are used to control this feature: `VIDEO_MAX_BITRATE`: Specifies the bitrate for the highest encoding layer per stream. It sets the `maxBitrate` property of the highest encoding of the `RTCRtpEncodingParameters` and is used to compute the bitrate attributed to the other layers. `MAX_BITRATE_OUT`: The maximum outgoing bitrate (=from the server) per session (meaning that this cap is shared between all the incoming streams for any given user). The appropriate simulcast layers used by each consumer will be selected to honor this limit. If the bitrate is still too high, packets will be dropped. Without this limit, the connections will use as much bandwidth as possible, which means that the simulcast layer will be chosen based on the client (or server) max available bandwidth. This commits also bumps the minor version as the bundle and the server need to be updated to benefit from the feature (although both the client and the server are backwards compatible). --- README.md | 5 ++-- package.json | 2 +- src/client.js | 14 +++++++++--- src/config.js | 53 +++++++++++++++++++++++++++++++++++++------ src/models/session.js | 1 + 5 files changed, 62 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 442335f..994fcf5 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,9 @@ The available environment variables are: - **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) -- **MAX_BITRATE_OUT**: if set, limits the outgoing bitrate 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" diff --git a/package.json b/package.json index da6e6de..f670667 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "odoo-sfu", "description": "Odoo's SFU server", - "version": "1.0.0", + "version": "1.1.0", "author": "Odoo", "license": "LGPL-3.0", "type": "module", diff --git a/src/client.js b/src/client.js index 23e4924..95e02e3 100644 --- a/src/client.js +++ b/src/client.js @@ -17,7 +17,7 @@ const RECOVERY_DELAY = 1_000; // how much time after an error should pass before const SUPPORTED_TYPES = new Set(["audio", "camera", "screen"]); // https://mediasoup.org/documentation/v3/mediasoup-client/api/#ProducerOptions -const PRODUCER_OPTIONS = { +const DEFAULT_PRODUCER_OPTIONS = { stopTracks: false, disableTrackOnPause: false, zeroRtpOnPause: true, @@ -117,6 +117,11 @@ export class SfuClient extends EventTarget { camera: null, screen: null, }; + /** @type {Object<"audio" | "video", import("mediasoup-client").types.ProducerOptions>} */ + _producerOptionsByKind = { + audio: DEFAULT_PRODUCER_OPTIONS, + video: DEFAULT_PRODUCER_OPTIONS, + }; /** @type {Function[]} */ _cleanups = []; @@ -284,7 +289,7 @@ export class SfuClient extends EventTarget { } try { this._producers[type] = await this._ctsTransport.produce({ - ...PRODUCER_OPTIONS, + ...this._producerOptionsByKind[track.kind], track, appData: { type }, }); @@ -599,7 +604,10 @@ export class SfuClient extends EventTarget { return; } case SERVER_REQUEST.INIT_TRANSPORTS: { - const { capabilities, stcConfig, ctsConfig } = payload; + const { capabilities, stcConfig, ctsConfig, producerOptionsByKind } = payload; + if (producerOptionsByKind) { + this._producerOptionsByKind = producerOptionsByKind; + } if (!this._device.loaded) { await this._device.load({ routerRtpCapabilities: capabilities }); } diff --git a/src/config.js b/src/config.js index ed5677b..94dbb58 100644 --- a/src/config.js +++ b/src/config.js @@ -16,7 +16,7 @@ const FALSY_INPUT = new Set(["disable", "false", "none", "no", "0"]); export const AUTH_KEY = process.env.AUTH_KEY; if (!AUTH_KEY && !process.env.JEST_WORKER_ID) { throw new Error( - "AUTH_KEY env variable is required, it is not possible to authenticate requests without it", + "AUTH_KEY env variable is required, it is not possible to authenticate requests without it" ); } /** @@ -29,7 +29,7 @@ if (!AUTH_KEY && !process.env.JEST_WORKER_ID) { export const PUBLIC_IP = process.env.PUBLIC_IP; if (!PUBLIC_IP && !process.env.JEST_WORKER_ID) { throw new Error( - "PUBLIC_IP env variable is required, clients cannot establish webRTC connections without it", + "PUBLIC_IP env variable is required, clients cannot establish webRTC connections without it" ); } /** @@ -69,7 +69,7 @@ export const PORT = Number(process.env.PORT) || 8070; */ export const NUM_WORKERS = Math.min( Number(process.env.NUM_WORKERS) || Infinity, - os.availableParallelism(), + os.availableParallelism() ); /** * A comma separated list of the audio codecs to use, if not provided the server will support all available codecs (listed below). @@ -111,19 +111,29 @@ export const MAX_BUF_IN = (process.env.MAX_BUF_IN && Number(process.env.MAX_BUF_ */ export const MAX_BUF_OUT = (process.env.MAX_BUF_OUT && Number(process.env.MAX_BUF_OUT)) || 0; /** - * The maximum incoming bitrate in bps for per session + * The maximum incoming bitrate in bps per session, + * This is what each user can upload. * * @type {number} */ export const MAX_BITRATE_IN = - (process.env.MAX_BITRATE_IN && Number(process.env.MAX_BITRATE_IN)) || 0; + (process.env.MAX_BITRATE_IN && Number(process.env.MAX_BITRATE_IN)) || 8_000_000; /** - * The maximum outgoing bitrate in bps for per session + * The maximum outgoing bitrate in bps per session, + * this is what each user can download. * * @type {number} */ export const MAX_BITRATE_OUT = - (process.env.MAX_BITRATE_OUT && Number(process.env.MAX_BITRATE_OUT)) || 0; + (process.env.MAX_BITRATE_OUT && Number(process.env.MAX_BITRATE_OUT)) || 10_000_000; +/** + * The maximum bitrate (in bps) for the highest encoding layer (simulcast) per video producer (= per video stream). + * see: `maxBitrate` @ https://www.w3.org/TR/webrtc/#dictionary-rtcrtpencodingparameters-members + * + * @type {number} + */ +export const MAX_VIDEO_BITRATE = + (process.env.MAX_VIDEO_BITRATE && Number(process.env.MAX_VIDEO_BITRATE)) || 4_000_000; /** * The maximum amount of concurrent users per channel * @@ -181,6 +191,16 @@ export const timeouts = Object.freeze({ // how many errors can occur before the session is closed, recovery attempts will be made until this limit is reached export const maxSessionErrors = 6; +/** + * @type {import("mediasoup-client").types.ProducerOptions} + * https://mediasoup.org/documentation/v3/mediasoup-client/api/#ProducerOptions + */ +const baseProducerOptions = { + stopTracks: false, + disableTrackOnPause: false, + zeroRtpOnPause: true, +}; + export const rtc = Object.freeze({ // https://mediasoup.org/documentation/v3/mediasoup/api/#WorkerSettings workerSettings: { @@ -208,6 +228,25 @@ export const rtc = Object.freeze({ maxSctpMessageSize: MAX_BUF_IN, sctpSendBufferSize: MAX_BUF_OUT, }, + producerOptionsByKind: { + /** @type {import("mediasoup-client").types.ProducerOptions} */ + audio: baseProducerOptions, + /** @type {import("mediasoup-client").types.ProducerOptions} */ + video: { + ...baseProducerOptions, + // for browsers using libwebrtc, values are set to allow simulcast layers to be made in that range + codecOptions: { + videoGoogleMinBitrate: 1_000, + videoGoogleStartBitrate: 1_000_000, + videoGoogleMaxBitrate: MAX_VIDEO_BITRATE * 2, + }, + encodings: [ + { scaleResolutionDownBy: 4, maxBitrate: Math.floor(MAX_VIDEO_BITRATE / 4) }, + { scaleResolutionDownBy: 2, maxBitrate: Math.floor(MAX_VIDEO_BITRATE / 2) }, + { scaleResolutionDownBy: 1, maxBitrate: MAX_VIDEO_BITRATE }, + ], + }, + }, }); // ------------------------------------------------------------ diff --git a/src/models/session.js b/src/models/session.js index 6fe3ef4..00f1ffe 100644 --- a/src/models/session.js +++ b/src/models/session.js @@ -315,6 +315,7 @@ export class Session extends EventEmitter { dtlsParameters: this._ctsTransport.dtlsParameters, sctpParameters: this._ctsTransport.sctpParameters, }, + producerOptionsByKind: config.rtc.producerOptionsByKind, }, }); await Promise.all([