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.
  • Loading branch information
ThanhDodeurOdoo committed Dec 14, 2023
1 parent ce01356 commit 59a00a5
Show file tree
Hide file tree
Showing 4 changed files with 47 additions and 12 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ The available environment variables are:
- **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_VIDEO_BITRATE**: if set, defines the `maxBitrate` of the highest simulcast layer, default to 8mbps
- **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
14 changes: 5 additions & 9 deletions src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,6 @@ const MAX_ERRORS = 6; // how many errors should occur before trying a full resta
const RECOVERY_DELAY = 1_000; // how much time after an error should pass before a soft recovery attempt (retry the operation and not the whole connection)
const SUPPORTED_TYPES = new Set(["audio", "camera", "screen"]);

// https://mediasoup.org/documentation/v3/mediasoup-client/api/#ProducerOptions
const PRODUCER_OPTIONS = {
stopTracks: false,
disableTrackOnPause: false,
zeroRtpOnPause: true,
};

/**
* @typedef {Object} Consumers
* @property {import("mediasoup-client").types.Consumer | null} audio
Expand Down Expand Up @@ -117,6 +110,8 @@ export class SfuClient extends EventTarget {
camera: null,
screen: null,
};
/** @type {Object<[streamType, import("mediasoup-client").types.ProducerOptions]>} */
_producerOptionsByType;
/** @type {Function[]} */
_cleanups = [];

Expand Down Expand Up @@ -284,7 +279,7 @@ export class SfuClient extends EventTarget {
}
try {
this._producers[type] = await this._ctsTransport.produce({
...PRODUCER_OPTIONS,
...this._producerOptionsByType[type],
track,
appData: { type },
});
Expand Down Expand Up @@ -599,7 +594,8 @@ export class SfuClient extends EventTarget {
return;
}
case SERVER_REQUEST.INIT_TRANSPORTS: {
const { capabilities, stcConfig, ctsConfig } = payload;
const { capabilities, stcConfig, ctsConfig, producerOptionsByType } = payload;
this._producerOptionsByType = producerOptionsByType;
if (!this._device.loaded) {
await this._device.load({ routerRtpCapabilities: capabilities });
}
Expand Down
43 changes: 40 additions & 3 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 @@ -124,6 +124,14 @@ export const MAX_BITRATE_IN =
*/
export const MAX_BITRATE_OUT =
(process.env.MAX_BITRATE_OUT && Number(process.env.MAX_BITRATE_OUT)) || 0;
/**
* The maximum bitrate (in bps) for the simulcast highest layer per video producer, if not set, defaults to 8mbps.
* 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)) || 8_000_000;
/**
* The maximum amount of concurrent users per channel
*
Expand Down Expand Up @@ -181,6 +189,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 +226,25 @@ export const rtc = Object.freeze({
maxSctpMessageSize: MAX_BUF_IN,
sctpSendBufferSize: MAX_BUF_OUT,
},
producerOptionsByType: {
/** @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: 20_000_000,
},
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,
},
producerOptionsByType: config.rtc.producerOptionsByType,
},
});
await Promise.all([
Expand Down

0 comments on commit 59a00a5

Please sign in to comment.