Skip to content

Commit

Permalink
[IMP] add simulcast
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
ThanhDodeurOdoo authored and seb-odoo committed Jan 9, 2024
1 parent 5e71c03 commit beafcc2
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 13 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
14 changes: 11 additions & 3 deletions src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 = [];

Expand Down Expand Up @@ -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 },
});
Expand Down Expand Up @@ -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 });
}
Expand Down
53 changes: 46 additions & 7 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
);
}
/**
Expand All @@ -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"
);
}
/**
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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 },
],
},
},
});

// ------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions src/models/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ export class Session extends EventEmitter {
dtlsParameters: this._ctsTransport.dtlsParameters,
sctpParameters: this._ctsTransport.sctpParameters,
},
producerOptionsByKind: config.rtc.producerOptionsByKind,
},
});
await Promise.all([
Expand Down

0 comments on commit beafcc2

Please sign in to comment.