diff --git a/README.md b/README.md index 442335f..1250cee 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 40mbps +- **MAX_VIDEO_BITRATE**: if set, defines the `maxBitrate` of the highest encoding layer (simulcast), default 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/src/client.js b/src/client.js index 23e4924..a6e0714 100644 --- a/src/client.js +++ b/src/client.js @@ -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 @@ -117,6 +110,8 @@ export class SfuClient extends EventTarget { camera: null, screen: null, }; + /** @type {Object<"audio" | "video", import("mediasoup-client").types.ProducerOptions>} */ + _producerOptionsByKind; /** @type {Function[]} */ _cleanups = []; @@ -284,7 +279,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 +594,8 @@ export class SfuClient extends EventTarget { return; } case SERVER_REQUEST.INIT_TRANSPORTS: { - const { capabilities, stcConfig, ctsConfig } = payload; + const { capabilities, stcConfig, ctsConfig, _producerOptionsByKind } = payload; + 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..1020d3e 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). @@ -112,18 +112,35 @@ 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 + * 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 for per session, + * this is what each user can download. + * + * Considering a generous 4mbps per 1080p/60fps stream, using a 40mbps cap means that a user can download 10 streams + * of that quality at the same time before a lower encoding (simulcast) layer is used. + * + * Note that it is uncommon to watch that many streams at that quality at the same time as very few screens have + * the resolution required to benefit from it. In most cases, lower resolutions are enough when watching multiple streams, + * and when watching a single stream, the client API offers the possibility to pause the download of unwatched streams. * * @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)) || 40_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 +198,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 +235,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: 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 }, + ], + }, + }, }); // ------------------------------------------------------------ diff --git a/src/models/session.js b/src/models/session.js index 6fe3ef4..adaac32 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([