Mediasoup-demo サーバーのソースコード分析

Mediasoup-demo サーバーのソースコード分析

0. ディレクトリ

  1. mediasoup-demo とは何ですか?
  2. mediasoup-デモ コードの構造
  3. Mediasoup-demo サーバーコード分析
    1. config.js コード分析
    2. server.js の主なロジック
    3. Room.js 固有のコード分析

関連記事:

  1. mediasoup の基本的な導入と Ubuntu/Docker 環境での mediasoup のデプロイ
  2. Mediasoup-demo サーバーのソースコード分析

1. mediasoup-demo とは?

  1. mediasoup-demo は、mediasoup ライブラリを使用して実装されたビデオ会議アプリケーションのサンプル コードです。
  2. The mediasoup-demo sample application provides a WebRTC-based video conferencing solution, including room management, media stream management, network transmission, and audio and video codec functions. 簡単に学習して参照できるように、フロントエンドとバックエンドのコード例も提供します。 .
  3. その中でも、mediasoup は、リアルタイムのオーディオおよびビデオ通信アプリケーションを構築するための、オープン ソースの WebRTC シグナリングおよびメディア サーバーです。

2. mediasoup-demo コード構造

  1. mediasoup-demo コードの構造図は次のとおりです。

画像.png
画像.png

  1. 各ディレクトリまたはファイルの機能は次のとおりです。
目次 次のレベルのディレクトリまたはファイル 効果
アプリ クライアントコード
放送局 ブロードキャスト、プッシュまたはプル
サーバ サーバーデモ
サーバー.js サーバーデモのメインプログラム
config.js 構成ファイル
証明書 証明書と鍵
connect.js 次の interactiveClient.js ファイルをカプセル化します
ライブラリ server.js が使用するライブラリ ファイル
Logger.js ログの印刷
Room.js ルーム管理と信号処理
interactiveClient.js ランタイム内部情報クエリ クライアント
InteractiveServer.js ランタイム内部情報クエリ サーバー
  1. サーバー ディレクトリ ファイルの内容に注目してください。

3. Mediasoup-demo サーバーコード分析

  1. mediasoup-demo プロジェクトのサーバー ディレクトリは、mediasoup との統合、WebSocket 通信、ルーム管理、ロギング、およびその他の機能を含む、ビデオ会議アプリケーションのバックエンド機能を実装します。

画像.png

1. config.js コード分析

  1. config.js は構成ファイルであり、mediasoup 構成やサーバー ポートなどのパラメーターを定義するために使用されます。
  2. 構成ファイルは次のとおりです。
/**
 * IMPORTANT (PLEASE READ THIS):
 *
 * This is not the "configuration file" of mediasoup. This is the configuration
 * file of the mediasoup-demo app. mediasoup itself is a server-side library, it
 * does not read any "configuration file". Instead it exposes an API. This demo
 * application just reads settings from this file (once copied to config.js) and
 * calls the mediasoup API with those settings when appropriate.
 */

const os = require('os');

module.exports =
{
    
    
	// Listening hostname (just for `gulp live` task).
	domain : process.env.DOMAIN || 'localhost',
	// Signaling settings (protoo WebSocket server and HTTP API server).
	https  : //
	{
    
    
		listenIp   : '0.0.0.0',
		// NOTE: Don't change listenPort (client app assumes 4443).
		listenPort : process.env.PROTOO_LISTEN_PORT || 4443,
		// NOTE: Set your own valid certificate files.
		tls        :
		{
    
    
			cert : process.env.HTTPS_CERT_FULLCHAIN || `${
      
      __dirname}/certs/fullchain.pem`,
			key  : process.env.HTTPS_CERT_PRIVKEY || `${
      
      __dirname}/certs/privkey.pem`
		}
	},
	// mediasoup settings.
	mediasoup :
	{
    
    
		// Number of mediasoup workers to launch.
		numWorkers     : Object.keys(os.cpus()).length,
		// mediasoup WorkerSettings.
		// See https://mediasoup.org/documentation/v3/mediasoup/api/#WorkerSettings
		workerSettings :
		{
    
    
			logLevel : 'warn',
			logTags  :
			[
				'info',
				'ice',
				'dtls',
				'rtp',
				'srtp',
				'rtcp',
				'rtx',
				'bwe',
				'score',
				'simulcast',
				'svc',
				'sctp'
			],
			rtcMinPort : process.env.MEDIASOUP_MIN_PORT || 40000,
			rtcMaxPort : process.env.MEDIASOUP_MAX_PORT || 49999
		},
		// mediasoup Router options.
		// See https://mediasoup.org/documentation/v3/mediasoup/api/#RouterOptions
		routerOptions :
		{
    
    
			mediaCodecs :
			[
				{
    
    
					kind      : 'audio',
					mimeType  : 'audio/opus',
					clockRate : 48000,
					channels  : 2
				},
				{
    
    
					kind       : 'video',
					mimeType   : 'video/VP8',
					clockRate  : 90000,
					parameters :
					{
    
    
						'x-google-start-bitrate' : 1000
					}
				},
				{
    
    
					kind       : 'video',
					mimeType   : 'video/VP9',
					clockRate  : 90000,
					parameters :
					{
    
    
						'profile-id'             : 2,
						'x-google-start-bitrate' : 1000
					}
				},
				{
    
    
					kind       : 'video',
					mimeType   : 'video/h264',
					clockRate  : 90000,
					parameters :
					{
    
    
						'packetization-mode'      : 1,
						'profile-level-id'        : '4d0032',
						'level-asymmetry-allowed' : 1,
						'x-google-start-bitrate'  : 1000
					}
				},
				{
    
    
					kind       : 'video',
					mimeType   : 'video/h264',
					clockRate  : 90000,
					parameters :
					{
    
    
						'packetization-mode'      : 1,
						'profile-level-id'        : '42e01f',
						'level-asymmetry-allowed' : 1,
						'x-google-start-bitrate'  : 1000
					}
				}
			]
		},
		// mediasoup WebRtcServer options for WebRTC endpoints (mediasoup-client,
		// libmediasoupclient).
		// See https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcServerOptions
		// NOTE: mediasoup-demo/server/lib/Room.js will increase this port for
		// each mediasoup Worker since each Worker is a separate process.
		webRtcServerOptions :
		{
    
    
			listenInfos :
			[
				{
    
    
					protocol    : 'udp',
					ip          : process.env.MEDIASOUP_LISTEN_IP || '0.0.0.0',
					announcedIp : process.env.MEDIASOUP_ANNOUNCED_IP,
					port        : 44444
				},
				{
    
    
					protocol    : 'tcp',
					ip          : process.env.MEDIASOUP_LISTEN_IP || '0.0.0.0',
					announcedIp : process.env.MEDIASOUP_ANNOUNCED_IP,
					port        : 44444
				}
			],
		},
		// mediasoup WebRtcTransport options for WebRTC endpoints (mediasoup-client,
		// libmediasoupclient).
		// See https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcTransportOptions
		webRtcTransportOptions :
		{
    
    
			// listenIps is not needed since webRtcServer is used.
			// However passing MEDIASOUP_USE_WEBRTC_SERVER=false will change it.
			listenIps :
			[
				{
    
    
					ip          : process.env.MEDIASOUP_LISTEN_IP || '0.0.0.0',
					announcedIp : process.env.MEDIASOUP_ANNOUNCED_IP
				}
			],
			initialAvailableOutgoingBitrate : 1000000,
			minimumAvailableOutgoingBitrate : 600000,
			maxSctpMessageSize              : 262144,
			// Additional options that are not part of WebRtcTransportOptions.
			maxIncomingBitrate              : 1500000
		},
		// mediasoup PlainTransport options for legacy RTP endpoints (FFmpeg,
		// GStreamer).
		// See https://mediasoup.org/documentation/v3/mediasoup/api/#PlainTransportOptions
		plainTransportOptions :
		{
    
    
			listenIp :
			{
    
    
				ip          : process.env.MEDIASOUP_LISTEN_IP || '0.0.0.0',
				announcedIp : process.env.MEDIASOUP_ANNOUNCED_IP
			},
			maxSctpMessageSize : 262144
		}
	}
};

  1. config.js 構成情報 コメント:
親フィールド サブフィールド 効果
https

聞くIP サーバーがリッスンする IP アドレス
listenポート サーバーがリッスンするポート
メディアスープ
ワーカー設定 ワーカー数、ワーカー プロセスのファイル パス、logLevel などを含む、mediasoup のワーカー構成パラメーター。
ルーターオプション メディア コーデック構成パラメーターを含む、mediasoup のルーター構成パラメーター
webRtcServerOptions Mediasoup の WebRtcServer 構成パラメーター (リッスン プロトコル、IP、およびポートを含む)
webRtcTransportOptions UDP/TCP 転送、最大帯域幅、最大データ パケットなどのリッスン ポートを含む、mediasoup の WebRtcTransport 構成パラメーター。
プレーントランスポートオプション UDP/TCP 転送のリッスン ポート、最大帯域幅、最大データ パケットなどを含む、mediasoup の PlainTransport 構成パラメーター。
  1. 詳細なフィールドの意味については、公式ドキュメントを参照してください: https://mediasoup.org/documentation/v3/mediasoup/api

2.server.jsコード分析

  1. server.js は mediasoup-demo アプリケーションのコア部分であり、さまざまなコンポーネントの開始と管理、およびオーディオ データとビデオ データの送信と処理を担当します。

1. server.js の主なロジック

  1. 構成ファイルをロードします。HTTP および WebSocket サーバーのポート番号、mediasoup ワーカー プロセスの数など、config.js ファイル内の構成パラメーターを読み取ります。
  2. HTTP サーバーの開始: HTTP サーバーを作成し、指定されたポートで要求をリッスンします。
    1. HTTP サーバーは主に、フロントエンド ページ、CSS および JS ファイルなどの静的リソースを提供するために使用されます。
  3. WebSocket サーバーを開始します。WebSocket サーバーを作成し、指定されたポートで接続要求をリッスンします。
    1. WebSocket サーバーは、リアルタイムのオーディオおよびビデオ データの送信を処理するために使用されます。
  4. mediasoup ワーカー プロセスを開始します。構成ファイルの worker.numWorkers パラメータに従って、指定された数の mediasoup ワーカー プロセスを作成します。
    1. 各ワーカー プロセスは、mediasoup Router オブジェクトと Transport オブジェクトの管理、およびオーディオ ストリームとビデオ ストリームの転送と処理を担当します。
  5. WebSocket 接続の処理: 新しい WebSocket 接続が確立されると、server.js は新しい mediasoup Router オブジェクトを作成し、それを指定された mediasoup ワーカー プロセスにバインドします。
    1. 同時に、server.js は WebSocket 接続用の Transport オブジェクトを作成し、対応する mediasoup Router オブジェクトにバインドします。
    2. このようにして、WebSocket クライアントは、Transport オブジェクトと mediasoup Router オブジェクトを介してオーディオ ストリームとビデオ ストリームを送信および処理できます。
  6. HTTP リクエストの処理: HTTP リクエストが到着すると、server.js はリクエスト パスに従って対応する静的リソースを返します。
    1. たとえば、"/" パスが要求されると、index.html ページが返されます。
  7. エラー処理: server.js は、HTTP および WebSocket サーバー エラー、mediasoup ワーカー プロセス エラー、WebSocket クライアント エラーなど、さまざまなエラーの処理も担当します。
    1. エラーが発生すると、server.js はエラー情報をログ ファイルに記録し、関連するサービスまたはプロセスの復元または再起動を試みます。

2. server.js 固有のコード分析

  1. サーバー ディレクトリの server.js は、サーバー アプリケーションのエントリ ファイルであり、アプリケーションの起動、HTTP および WebSocket サーバーの作成、および mediasoup ワーカー プロセスの管理を担当します。
  2. run() 関数は server.js エントリ関数です。
async function run()
{
    
    
	// 开启交互式服务器
	await interactiveServer();

	// 如果设置了环境变量 INTERACTIVE 为 true 或 1,则开启交互式客户端
	if (process.env.INTERACTIVE === 'true' || process.env.INTERACTIVE === '1')
		await interactiveClient();

	// 运行 mediasoup worker
	await runMediasoupWorkers();

	// 创建 Express 应用
	await createExpressApp();

	// 运行 HTTPS 服务器
	await runHttpsServer();

	// 运行 protoo WebSocket 服务器
	await runProtooWebSocketServer();

	// 每 X 秒记录一次房间的状态
	setInterval(() =>
	{
    
    
		for (const room of rooms.values())
		{
    
    
			room.logStatus();
		}
	}, 120000);
}

1. runMediasoupWorkers() 関数の分析
  1. mediasoup-demo では、mediasoup Worker は mediasoup のコア部分であり、オーディオおよびビデオ ストリームの処理と、mediasoup の基盤となるリソースの管理を担当します。
  2. mediasoup ワーカーの作成と破棄には時間がかかるため、システムの処理能力を向上させるために、mediasoup-demo で複数の mediasoup ワーカーを作成します。
  3. runMediasoupWorkers() 関数では、複数の mediasoup ワーカーを作成、管理、および監視して、mediasoup-demo の正常な動作を確保できます。
  4. 具体的なコードとコメントは次のとおりです。
/**
 * Launch as many mediasoup Workers as given in the configuration file.
 */
async function runMediasoupWorkers()
{
    
    
	// 从配置文件中获取mediasoup的配置项numWorkers,即需要启动的mediasoup Worker数量。
	const {
    
     numWorkers } = config.mediasoup;

	logger.info('running %d mediasoup Workers...', numWorkers);

	// 使用mediasoup.createWorker()创建指定数量的mediasoup Worker,并将其存储到全局数组mediasoupWorkers中。
	for (let i = 0; i < numWorkers; ++i)
	{
    
    
		const worker = await mediasoup.createWorker(
			{
    
    
				logLevel   : config.mediasoup.workerSettings.logLevel,
				logTags    : config.mediasoup.workerSettings.logTags,
				rtcMinPort : Number(config.mediasoup.workerSettings.rtcMinPort),
				rtcMaxPort : Number(config.mediasoup.workerSettings.rtcMaxPort)
			});

		// 每个mediasoup Worker创建成功后,为其添加一个died事件监听器,当Worker死亡时自动退出进程。
		// 该事件监听器通过setTimeout()设置了一个2秒的定时器,定时器到期后退出进程。
		worker.on('died', () =>
		{
    
    
			logger.error(
				'mediasoup Worker died, exiting  in 2 seconds... [pid:%d]', worker.pid);

			setTimeout(() => process.exit(1), 2000);
		});

		mediasoupWorkers.push(worker);

		// 对于每个mediasoup Worker,如果配置文件中的MEDIASOUP_USE_WEBRTC_SERVER环境变量的值不为false,则在其中创建一个WebRtcServer。
		// Create a WebRtcServer in this Worker.
		if (process.env.MEDIASOUP_USE_WEBRTC_SERVER !== 'false')
		{
    
    
			// Each mediasoup Worker will run its own WebRtcServer, so those cannot
			// share the same listening ports. Hence we increase the value in config.js
			// for each Worker.
			// 为了避免多个mediasoup Worker之间端口冲突,配置文件中WebRtcServer的监听端口需要逐个增加,这样每个mediasoup Worker创建的
			// WebRtcServer监听端口才不会相同。因此,在为每个mediasoup Worker创建WebRtcServer时,会逐个增加WebRtcServer配置中的端口号。
			const webRtcServerOptions = utils.clone(config.mediasoup.webRtcServerOptions);
			const portIncrement = mediasoupWorkers.length - 1;

			for (const listenInfo of webRtcServerOptions.listenInfos)
			{
    
    
				listenInfo.port += portIncrement;
			}

			const webRtcServer = await worker.createWebRtcServer(webRtcServerOptions);

			//每个mediasoup Worker创建的WebRtcServer将被存储到其appData属性中。
			worker.appData.webRtcServer = webRtcServer;
		}

		// Log worker resource usage every X seconds.
		// 每个mediasoup Worker将会定时记录其资源使用情况,并使用setInterval()函数设置一个时间间隔,
		// 即每隔一段时间输出一次mediasoup Worker的资源使用情况。
		setInterval(async () =>
		{
    
    
			const usage = await worker.getResourceUsage();

			logger.info('mediasoup Worker resource usage [pid:%d]: %o', worker.pid, usage);
		}, 120000);
	}
}

2. mediasoup.createWorker() 関数の分析
  1. The mediasoup.createWorker() method in the runMediasoupWorkers() function is asynchronous method provided by the mediasoup library, which is used to create a mediasoup Worker instance. 特定の実装は、mediasoup の node/src/index.ts コードにあります。計画。
  2. mediasoup-demo v3 バージョンでは、mediasoup.createWorker() メソッドが複数回呼び出され、各呼び出しで Worker インスタンスが作成され、そのインスタンスがグローバル変数 mediasoupWorkers 配列にプッシュされます。
    1. v3 バージョンの実装では、各 Worker インスタンスは独自の独立した mediasoup Router インスタンスを持ち、異なる部屋がメディア処理に独立した Router インスタンスを使用するようにし、システムのスケーラビリティと安定性を向上させます。
    2. 同時に、各 Worker インスタンスは、対応するポートに WebRtcServer インスタンスを作成して、WebRTC クライアントの接続を管理します。
  3. 以下は、mediasoup.createWorker() メソッドの詳細な分析です。
    1. options: オブジェクト タイプ。Worker インスタンスを作成するためのオプションを示します。オプションは次のとおりです。
      1. logLevel: Worker インスタンスのログレベルを示す文字列型。デフォルトは警告です。
      2. logTags: Worker インスタンスのログタグを示す配列型。デフォルトは ['info', 'ice', 'dtls', 'rtp', 'srtp', 'rtcp', 'rtx', 'bwe', 'score', 'simulcast', 'svc'] です。
      3. rtcMinPort: Worker インスタンスが使用する最小 UDP ポートを示す数値型。デフォルト値は 10000 です。
      4. rtcMaxPort: Worker インスタンスが使用する最大 UDP ポートを示す数値型。デフォルト値は 59999 です。
      5. dtlsCertificateFile と dtlsPrivateKeyFile は、DTLS の証明書と秘密鍵ファイルへのパスです。
        1. これらのファイルへのパスが設定されていない場合、mediasoup はデフォルトの証明書と秘密鍵を使用します。
      6. libwebrtcFieldTrials は、Google WebRTC ライブラリの実験的な機能です。
      7. appData を使用して、ワーカー インスタンスに関連付けられたアプリケーション データを格納できます。
        1. appData が設定されている場合は、オブジェクトである必要があります。デフォルトでは、appData は未定義です。
    2. 戻り値: 作成された Worker インスタンス オブジェクトを表す Promise タイプ。
/**
 * Create a Worker.
 */
export async function createWorker(
	{
    
    
		logLevel = 'error',
		logTags,
		rtcMinPort = 10000,
		rtcMaxPort = 59999,
		dtlsCertificateFile,
		dtlsPrivateKeyFile,
		libwebrtcFieldTrials,
		appData
	}: WorkerSettings = {
    
    }
): Promise<Worker>
{
    
    
	// 在函数中打印了一个调试信息,表示正在创建一个 Worker 实例。
	logger.debug('createWorker()');

	// 函数检查了 appData 是否是一个对象。如果 appData 存在但不是对象,则抛出 TypeError 异常。
	if (appData && typeof appData !== 'object')
		throw new TypeError('if given, appData must be an object');

	// 创建了一个新的 Worker 实例,将配置对象作为参数传递给该实例的构造函数。
	const worker = new Worker(
		{
    
    
			logLevel,
			logTags,
			rtcMinPort,
			rtcMaxPort,
			dtlsCertificateFile,
			dtlsPrivateKeyFile,
			libwebrtcFieldTrials,
			appData
		});

	// createWorker 函数返回一个 Promise,当 worker 进程启动成功时会调用 resolve 回调函数,并将创建的 Worker 实例作为参数传递进去;
	// 如果 worker 进程启动失败,则会调用 reject 回调函数。
	return new Promise((resolve, reject) =>
	{
    
    
		worker.on('@success', () =>
		{
    
    
			// Emit observer event.
			// 在 resolve 回调函数中,会发射一个 newworker 事件,通知 mediasoup 监听该事件的观察者,表示创建了一个新的 worker 进程。
			observer.safeEmit('newworker', worker);

			resolve(worker);
		});

		worker.on('@failure', reject);
	});
}
  1. その中で、新しい Worker は Worker 作業プロセスを作成し、作業プロセスとメイン プロセスの間に IPC チャネルを確立して、プロセス間通信を実現します。
    1. Worker は、RTP ストリーミング、ルーム管理など、mediasoup のコア機能を処理するために使用されます。
3. runHttpsServer() 関数の分析
  1. runHttpsServer() 関数の主な機能は、HTTPS ベースの Web サーバーを作成して実行し、構成ファイルで指定された IP アドレスとポートをリッスンすることです。
  2. 具体的なコードとコメントは次のとおりです。
/**
 * Create a Node.js HTTPS server. It listens in the IP and port given in the
 * configuration file and reuses the Express application as request listener.
 */
async function runHttpsServer()
{
    
    
	logger.info('running an HTTPS server...');

	// HTTPS server for the protoo WebSocket server.
	// 定义了tls的对象,用于存储HTTPS服务器所需的TLS证书和密钥
	const tls =
	{
    
    
		cert : fs.readFileSync(config.https.tls.cert),
		key  : fs.readFileSync(config.https.tls.key)
	};

	// 创建一个HTTPS服务器实例,并将tls对象和expressApp作为参数传递进去。
	httpsServer = https.createServer(tls, expressApp);

	// 监听HTTPS服务器的端口和IP地址,并返回一个Promise。
	await new Promise((resolve) =>
	{
    
    
		httpsServer.listen(
			Number(config.https.listenPort), config.https.listenIp, resolve);
	});
}

4. runProtooWebSocketServer() 関数の分析
  1. runProtooWebSocketServer() 関数は、主に WebSocket サーバーを起動して、ブラウザーが WebSocket を介してサーバーに接続できるようにするために使用されます。
/**
 * Create a protoo WebSocketServer to allow WebSocket connections from browsers.
 */
async function runProtooWebSocketServer()
{
    
    
	logger.info('running protoo WebSocketServer...');

	// Create the protoo WebSocket server.
	// 创建一个 WebSocket 服务器 protooWebSocketServer
	protooWebSocketServer = new protoo.WebSocketServer(httpsServer,
		{
    
    
			maxReceivedFrameSize     : 960000, // 960 KBytes.
			maxReceivedMessageSize   : 960000,
			fragmentOutgoingMessages : true,
			fragmentationThreshold   : 960000
		});

	// Handle connections from clients.
	// 注册 connectionrequest 事件的监听器,有新的 WebSocket 连接请求时被触发。
	protooWebSocketServer.on('connectionrequest', (info, accept, reject) =>
	{
    
    
		// The client indicates the roomId and peerId in the URL query.
		// 在监听器中,从连接请求的 URL 中解析出房间 ID 和 peer ID,如果解析失败或者没有这些参数,就拒绝连接请求。
		const u = url.parse(info.request.url, true);
		const roomId = u.query['roomId'];
		const peerId = u.query['peerId'];

		if (!roomId || !peerId)
		{
    
    
			reject(400, 'Connection request without roomId and/or peerId');

			return;
		}

		// 从URL中解析出消费者实例的数量 consumerReplicas,如果解析失败则将其设置为默认值 0。
		let consumerReplicas = Number(u.query['consumerReplicas']);

		if (isNaN(consumerReplicas))
		{
    
    
			consumerReplicas = 0;
		}

		logger.info(
			'protoo connection request [roomId:%s, peerId:%s, address:%s, origin:%s]',
			roomId, peerId, info.socket.remoteAddress, info.origin);

		// Serialize this code into the queue to avoid that two peers connecting at
		// the same time with the same roomId create two separate rooms with same
		// roomId.
    // 为了避免同时有两个连接请求使用相同的roomId创建两个不同的房间,使用了一个queue队列将代码序列化。
		// 在队列中,调用 getOrCreateRoom() 函数获取或创建一个房间,并返回一个 Room 实例。
		// 如果房间创建或加入失败,就会在 catch 代码块中将错误信息返回给客户端,并拒绝连接请求。
		queue.push(async () =>
		{
    
    
			const room = await getOrCreateRoom({
    
     roomId, consumerReplicas });

			// Accept the protoo WebSocket connection.
			// 如果任务执行成功,使用 accept() 接受 WebSocket 连接
			const protooWebSocketTransport = accept();

			room.handleProtooConnection({
    
     peerId, protooWebSocketTransport });
		})
			.catch((error) =>
			{
    
    
				logger.error('room creation or room joining failed:%o', error);

				reject(error);
			});
	});
}

3. Room.js コード分析

  1. Room.js は、単一のルームの管理ロジックをアプリケーションに実装し、ルーム メンバーの参加/退出、プロデューサー/コンシューマーの作成と管理などの一連の操作を管理します。

1. Room.js 固有のコード分析

1. create() 関数の分析
  1. create() 関数は新しい Room インスタンスを作成します。
  2. 具体的なコードとコメントは次のとおりです。
	/**
	 * Factory function that creates and returns Room instance.
	 *
	 * @async
	 *
	 * @param {mediasoup.Worker} mediasoupWorker - The mediasoup Worker in which a new
	 *   mediasoup Router must be created.
	 * @param {String} roomId - Id of the Room instance.
	 */
	static async create({
     
      mediasoupWorker, roomId, consumerReplicas })
	{
    
    
		logger.info('create() [roomId:%s]', roomId);

		// Create a protoo Room instance.
		// 创建了一个新的protoo Room实例,protoo是一种基于WebSocket的信令协议,用于在客户端和服务端之间传递信令消息。
		const protooRoom = new protoo.Room();

		// Router media codecs.
		//根据配置文件中的mediasoup router选项,使用mediasoup worker创建一个mediasoup Router实例
		const {
    
     mediaCodecs } = config.mediasoup.routerOptions;

		// Create a mediasoup Router.
		const mediasoupRouter = await mediasoupWorker.createRouter({
    
     mediaCodecs });

		// Create a mediasoup AudioLevelObserver.
		// 使用mediasoup Router实例,创建一个mediasoup AudioLevelObserver实例,用于观察媒体流中的音频级别。
		// 这个观察者的作用是用来检测房间中正在说话的用户,以便对其进行相关操作,如推送媒体流等。
		const audioLevelObserver = await mediasoupRouter.createAudioLevelObserver(
			{
    
    
				maxEntries : 1,
				threshold  : -80,
				interval   : 800
			});

		// Create a mediasoup ActiveSpeakerObserver.
		// 使用mediasoup Router实例上,创建一个mediasoup ActiveSpeakerObserver实例,用于观察房间中当前发言者的情况。
		// 这个观察者的作用是用来检测房间中的活跃发言者,以便在客户端中实时显示。
		const activeSpeakerObserver = await mediasoupRouter.createActiveSpeakerObserver();

		// 最后,使用同样的mediasoup Router实例,创建一个Bot实例,Bot是一个虚拟的用户,用于在房间中扮演一个音频流的源。
		// 它的主要作用是在测试和演示中使用,例如在没有实际用户的情况下测试房间的性能等。
		const bot = await Bot.create({
    
     mediasoupRouter });

		// 最终返回一个新的Room实例,包含上述创建的protoo Room实例、mediasoup Router实例、以及用于观察音频级别和活跃发言者的mediasoup观察者实例等信息。
		return new Room(
			{
    
    
				roomId,
				protooRoom,
				webRtcServer : mediasoupWorker.appData.webRtcServer,
				mediasoupRouter,
				audioLevelObserver,
				activeSpeakerObserver,
				consumerReplicas,
				bot
			});
	}

2. handleProtooConnection() 関数の分析
  1. handleProtooConnection() 関数は、クライアントとの接続を管理し、各接続が一意であることを確認し、クライアントから送信された要求を処理して対応する結果を返すために使用されます。
  2. server.js の runProtooWebSocketServer() 関数が WebSocket サーバーを作成した後、サーバーは connectionrequest イベントのリスナーを登録します。これは、新しい WebSocket 接続要求があるときにトリガーされ、最終的に handleProtooConnection() 関数を呼び出します。
  3. 具体的なコードとコメントは次のとおりです。
	/**
	 * Called from server.js upon a protoo WebSocket connection request from a
	 * browser.
	 *
	 * @param {String} peerId - The id of the protoo peer to be created.
	 * @param {Boolean} consume - Whether this peer wants to consume from others.
	 * @param {protoo.WebSocketTransport} protooWebSocketTransport - The associated
	 *   protoo WebSocket transport.
	 */
	handleProtooConnection({
     
      peerId, consume, protooWebSocketTransport })
	{
    
    
		// 为了确保每个连接都是唯一的,根据peerId查找是否已经存在同名的protoo Peer对象,如果存在,则关闭该protoo Peer对象。
		const existingPeer = this._protooRoom.getPeer(peerId);

		if (existingPeer)
		{
    
    
			logger.warn(
				'handleProtooConnection() | there is already a protoo Peer with same peerId, closing it [peerId:%s]',
				peerId);

			existingPeer.close();
		}

		// 如果不存在同名的protoo Peer对象,则创建一个新的protoo Peer对象。
		// 并通过peer.data对象存储相关数据,如是否已经加入房间、用户名称等等。
		// 同时,也创建了几个map对象,用于存储房间中的生产者、消费者、数据生产者和数据消费者对象。
		let peer;

		// Create a new protoo Peer with the given peerId.
		try
		{
    
    
			peer = this._protooRoom.createPeer(peerId, protooWebSocketTransport);
		}
		catch (error)
		{
    
    
			logger.error('protooRoom.createPeer() failed:%o', error);
		}

		// Use the peer.data object to store mediasoup related objects.

		// Not joined after a custom protoo 'join' request is later received.
		peer.data.consume = consume;
		peer.data.joined = false;
		peer.data.displayName = undefined;
		peer.data.device = undefined;
		peer.data.rtpCapabilities = undefined;
		peer.data.sctpCapabilities = undefined;

		// Have mediasoup related maps ready even before the Peer joins since we
		// allow creating Transports before joining.
		peer.data.transports = new Map();
		peer.data.producers = new Map();
		peer.data.consumers = new Map();
		peer.data.dataProducers = new Map();
		peer.data.dataConsumers = new Map();

		// 监听protoo Peer对象的request事件,当有请求到达时,执行_handleProtooRequest函数来处理请求,并返回相应的结果。
		// 如果出现异常,则通过reject函数返回错误信息。
		peer.on('request', (request, accept, reject) =>
		{
    
    
			logger.debug(
				'protoo Peer "request" event [method:%s, peerId:%s]',
				request.method, peer.id);

			this._handleProtooRequest(peer, request, accept, reject)
				.catch((error) =>
				{
    
    
					logger.error('request failed:%o', error);

					reject(error);
				});
		});

		// 当一个protoo连接关闭时,触发protoo Peer对象的close事件。
		peer.on('close', () =>
		{
    
    
			// 首先判断房间是否已经关闭,如果已经关闭则直接返回,避免重复关闭。
			if (this._closed)
				return;

			logger.debug('protoo Peer "close" event [peerId:%s]', peer.id);

			// If the Peer was joined, notify all Peers.
			// 如果该Peer已经加入房间,通知所有其他Peers该Peer已经关闭。
			if (peer.data.joined)
			{
    
    
				for (const otherPeer of this._getJoinedPeers({
    
     excludePeer: peer }))
				{
    
    
					otherPeer.notify('peerClosed', {
    
     peerId: peer.id })
						.catch(() => {
    
    });
				}
			}

			// Iterate and close all mediasoup Transport associated to this Peer, so all
			// its Producers and Consumers will also be closed.
			// 遍历所有该Peer创建的Transport并关闭,这样该Peer创建的所有Producers和Consumers都将被关闭。
			for (const transport of peer.data.transports.values())
			{
    
    
				transport.close();
			}

			// If this is the latest Peer in the room, close the room.
			// 判断该Peer是否是房间中的最后一个Peer,如果是,则关闭整个房间。
			if (this._protooRoom.peers.length === 0)
			{
    
    
				logger.info(
					'last Peer in the room left, closing the room [roomId:%s]',
					this._roomId);

				this.close();
			}
		});
	}

3. _handleProtooRequest() 関数の分析
  1. _handleProtooRequest 関数は、WebSocket 接続を介してクライアントから送信されたメッセージを処理するためのものです。
  2. _handleProtooRequest 関数は次のように定義されます。
async _handleProtooRequest(peer, request, accept, reject)
  1. パラメータの説明:
    1. ピア: メッセージを送信するクライアント接続。
    2. request: クライアントから送信されたメッセージの内容。
    3. accept: クライアントに応答メッセージを送信するために使用されるコールバック関数。
    4. reject: エラー応答メッセージをクライアントに送信するために使用されるコールバック関数。
  2. request パラメータは、クライアントから送信されるメッセージの内容であり、request.method と request.data の 2 つのフィールドが含まれます。
    1. request.method はメッセージ タイプを示し、request.data はメッセージによって運ばれるデータを示します。
    2. 異なる request.method に従って、メッセージは異なる処理機能に割り当てられます。
  3. 常に 22 種類のメッセージがあり、その意味は次のとおりです。
シグナリング 例証する
getRouterRtpCapabilities クライアントは、mediasoupRouter の rtpCapabilities、つまりメディア ストリーミングの関連する機能パラメータを取得するように要求します。
加入 クライアントがルームに参加
createWebRtcTransport WebRTC トランスポート オブジェクトを作成し、peer.data.transsports に保存して、その後のオーディオおよびビデオの送信を行います。
connectWebRtcTransport WebRtcTransport が確立された後、クライアントは WebRtcTransport への接続時に、ローカルで生成された DTLS パラメーターを使用して送信します。
再起動アイス WebRtcTransport で ICE エージェントを再起動して、新しい ICE 候補を取得し、接続を更新します。
生産 新しいオーディオまたはビデオ プロデューサーを作成し、それをクライアントに関連付けます。
closeプロデューサー 指定されたプロデューサーを閉じて、そのプロデューサーを peer.data.producers マップから削除します
一時停止プロデューサー 指定されたプロデューサーを一時停止しようとしています
履歴書プロデューサー 指定されたプロデューサーの復元を試みます
一時停止消費者 指定されたコンシューマを一時停止しようとしています
消費者を再開する 指定されたコンシューマの復元を試みます
setConsumerPreferredLayers 指定されたコンシューマーの優先レイヤーの設定を試みます
setConsumerPriority 消費者の優先度 (プライオリティ) を設定してみてください
requestConsumerKeyFrame コンシューマーのキー フレーム (キー フレーム) を取得しようとする
プロデュースデータ データ プロデューサーを作成し、peer.data.dataProducers マップのデータ オブジェクトに格納します。
変更表示名 表示名を変更するクライアント要求を処理します
getTransportStats トランスポートの統計を取得する
getProducerStats コンシューマーの統計を取得する
getDataProducerStats データ プロデューサーの統計を取得する
getDataConsumerStats データ コンシューマーの統計を取得する
applyNetworkThrottle ネットワークの帯域幅と遅延を変更して、ネットワークの制限をシミュレートする
resetNetworkThrottle ネットワーク制限のリセット
  1. 具体的なコード分析とコメントは次のとおりです。
	/**
	 * Handle protoo requests from browsers.
	 *
	 * @async
	 */
	async _handleProtooRequest(peer, request, accept, reject)
	{
    
    
		switch (request.method)
		{
    
    
			// 客户端请求获取 mediasoupRouter 的 rtpCapabilities,即媒体流传输的相关能力参数。
			case 'getRouterRtpCapabilities':
			{
    
    
				accept(this._mediasoupRouter.rtpCapabilities);

				break;
			}

			case 'join':
			{
    
    
				// Ensure the Peer is not already joined.
				// 检查该客户端是否已经加入了房间。如果已经加入了,就抛出异常。
				if (peer.data.joined)
					throw new Error('Peer already joined');

				// 从 request.data 中获取客户端传入的 displayName,device,rtpCapabilities 和 sctpCapabilities 参数,
				// 并将这些参数保存到该客户端对象的 data 属性中。
				const {
    
    
					displayName,
					device,
					rtpCapabilities,
					sctpCapabilities
				} = request.data;

				// Store client data into the protoo Peer data object.
				peer.data.joined = true;
				peer.data.displayName = displayName;
				peer.data.device = device;
				peer.data.rtpCapabilities = rtpCapabilities;
				peer.data.sctpCapabilities = sctpCapabilities;

				// Tell the new Peer about already joined Peers.
				// And also create Consumers for existing Producers.
				// 告诉新 Peer 关于已加入房间的 Peer, 并为现有的 Producer 创建 Consumer。
				const joinedPeers =
				[
					...this._getJoinedPeers(),
					...this._broadcasters.values()
				];

				// Reply now the request with the list of joined peers (all but the new one).
				// 回复请求并返回除了新加入的 Peer 外的所有已加入的 Peer 列表。
				// 即获取已经加入房间的其他客户端对象和广播者对象,然后将这些对象的信息打包成一个列表,
				// 发送给新加入的客户端,告诉他有哪些其他客户端和广播者已经在房间中。
				const peerInfos = joinedPeers
					.filter((joinedPeer) => joinedPeer.id !== peer.id)
					.map((joinedPeer) => ({
    
    
						id          : joinedPeer.id,
						displayName : joinedPeer.data.displayName,
						device      : joinedPeer.data.device
					}));

				accept({
    
     peers: peerInfos });

				// Mark the new Peer as joined.
				// 将该客户端对象标记为已经加入房间。
				peer.data.joined = true;

				for (const joinedPeer of joinedPeers)
				{
    
    
					// Create Consumers for existing Producers.
					// 为现有的 Producer 创建 Consumer。
					// 即对于已经加入房间的其他客户端对象,为其创建对应的消费者对象,并将新加入的客户端对象标记为其对应的消费者。
					for (const producer of joinedPeer.data.producers.values())
					{
    
    
						this._createConsumer(
							{
    
    
								consumerPeer : peer,
								producerPeer : joinedPeer,
								producer
							});
					}

					// Create DataConsumers for existing DataProducers.
					// 为现有的 DataProducer 创建 DataConsumer。
					// 即对于已经加入房间的其他客户端对象,为其创建对应的数据消费者对象,并将新加入的客户端对象标记为其对应的数据消费者。
					for (const dataProducer of joinedPeer.data.dataProducers.values())
					{
    
    
						// 排除 bot DataProducer,即如果数据生产者的标签是 'bot',则不会创建数据消费者。
						if (dataProducer.label === 'bot')
							continue;

						this._createDataConsumer(
							{
    
    
								dataConsumerPeer : peer,
								dataProducerPeer : joinedPeer,
								dataProducer
							});
					}
				}

				// Create DataConsumers for bot DataProducer.
				// 为 bot DataProducer 创建 DataConsumer。
				this._createDataConsumer(
					{
    
    
						dataConsumerPeer : peer,
						dataProducerPeer : null,
						dataProducer     : this._bot.dataProducer
					});

				// Notify the new Peer to all other Peers.
				// 将新加入的客户端对象的信息通知给其他已经加入房间的客户端对象,告诉它们新客户端的信息。
				for (const otherPeer of this._getJoinedPeers({
    
     excludePeer: peer }))
				{
    
    
					otherPeer.notify(
						'newPeer',
						{
    
    
							id          : peer.id,
							displayName : peer.data.displayName,
							device      : peer.data.device
						})
						.catch(() => {
    
    });
				}

				break;
			}

			case 'createWebRtcTransport':
			{
    
    
				// NOTE: Don't require that the Peer is joined here, so the client can
				// initiate mediasoup Transports and be ready when he later joins.

				// 从请求数据中解析出所需的参数
				const {
    
    
					forceTcp,
					producing,
					consuming,
					sctpCapabilities
				} = request.data;

				// 设置 WebRtcTransport 的选项
				const webRtcTransportOptions =
				{
    
    
					...config.mediasoup.webRtcTransportOptions,
					enableSctp     : Boolean(sctpCapabilities),
					numSctpStreams : (sctpCapabilities || {
    
    }).numStreams,
					appData        : {
    
     producing, consuming }
				};

				// 如果设置了 forceTcp,则使用 TCP 连接,否则使用 UDP 连接
				if (forceTcp)
				{
    
    
					webRtcTransportOptions.enableUdp = false;
					webRtcTransportOptions.enableTcp = true;
				}

				// 创建 WebRtcTransport 对象
				const transport = await this._mediasoupRouter.createWebRtcTransport(
					{
    
    
						...webRtcTransportOptions,
						webRtcServer : this._webRtcServer
					});

				// 监听一些事件,用于处理异常情况和跟踪传输状态
				transport.on('sctpstatechange', (sctpState) =>
				{
    
    
					logger.debug('WebRtcTransport "sctpstatechange" event [sctpState:%s]', sctpState);
				});

				transport.on('dtlsstatechange', (dtlsState) =>
				{
    
    
					if (dtlsState === 'failed' || dtlsState === 'closed')
						logger.warn('WebRtcTransport "dtlsstatechange" event [dtlsState:%s]', dtlsState);
				});

				// NOTE: For testing.
				// await transport.enableTraceEvent([ 'probation', 'bwe' ]);
				// 启用一些跟踪事件
				await transport.enableTraceEvent([ 'bwe' ]);

				transport.on('trace', (trace) =>
				{
    
    
					logger.debug(
						'transport "trace" event [transportId:%s, trace.type:%s, trace:%o]',
						transport.id, trace.type, trace);

					if (trace.type === 'bwe' && trace.direction === 'out')
					{
    
    
						peer.notify(
							'downlinkBwe',
							{
    
    
								desiredBitrate          : trace.info.desiredBitrate,
								effectiveDesiredBitrate : trace.info.effectiveDesiredBitrate,
								availableBitrate        : trace.info.availableBitrate
							})
							.catch(() => {
    
    });
					}
				});

				// Store the WebRtcTransport into the protoo Peer data Object.
				// 将创建的 WebRtcTransport 对象存储到 peer.data.transports 中
				peer.data.transports.set(transport.id, transport);

				// 发送响应消息,包含一些重要参数
				accept(
					{
    
    
						id             : transport.id,
						iceParameters  : transport.iceParameters,
						iceCandidates  : transport.iceCandidates,
						dtlsParameters : transport.dtlsParameters,
						sctpParameters : transport.sctpParameters
					});

				const {
    
     maxIncomingBitrate } = config.mediasoup.webRtcTransportOptions;

				// If set, apply max incoming bitrate limit.
				// 如果设置了 maxIncomingBitrate,则将其应用于创建的 WebRtcTransport 对象
				if (maxIncomingBitrate)
				{
    
    
					try {
    
     await transport.setMaxIncomingBitrate(maxIncomingBitrate); }
					catch (error) {
    
    }
				}

				break;
			}

			case 'connectWebRtcTransport':
			{
    
    
				// 从请求中获取 transportId 和 dtlsParameters。
				const {
    
     transportId, dtlsParameters } = request.data;
				// 从 Peer 数据对象中获取 ID 为 transportId 的 WebRtcTransport 对象。
				const transport = peer.data.transports.get(transportId);

				// 如果没有找到对应的 WebRtcTransport,则抛出一个错误。
				if (!transport)
					throw new Error(`transport with id "${
      
      transportId}" not found`);

				// 调用 transport.connect() 建立一个 DTLS 连接。
				await transport.connect({
    
     dtlsParameters });

				// 发送 accept 消息给客户端,表明连接建立成功。
				accept();

				break;
			}

			case 'restartIce':
			{
    
    
				// 从请求中获取transportId,并通过该id从peer.data.transports中获取transport对象。
				const {
    
     transportId } = request.data;
				const transport = peer.data.transports.get(transportId);

				// 如果找不到该transport对象,则抛出错误。
				if (!transport)
					throw new Error(`transport with id "${
      
      transportId}" not found`);

				// 否则,调用transport的restartIce方法重新启动ICE Agent,并返回新的ICE Parameters。
				const iceParameters = await transport.restartIce();

				// 将新的ICE Parameters传递给accept方法返回给客户端。
				accept(iceParameters);

				break;
			}

			case 'produce':
			{
    
    
				// Ensure the Peer is joined.
				// 首先需要检查客户端是否已经加入到 Room 中,如果没有加入则抛出异常。
				if (!peer.data.joined)
					throw new Error('Peer not yet joined');

				// 解析客户端传来的请求数据,包括 transportId(传输 ID)、kind(音频还是视频)、
				// rtpParameters(RTP 参数)和 appData(应用数据)。
				const {
    
     transportId, kind, rtpParameters } = request.data;
				let {
    
     appData } = request.data;

				// 从 peer 对象中获取传输对象,如果不存在则抛出异常。
				const transport = peer.data.transports.get(transportId);

				if (!transport)
					throw new Error(`transport with id "${
      
      transportId}" not found`);

				// Add peerId into appData to later get the associated Peer during
				// the 'loudest' event of the audioLevelObserver.
				// 将 peerId 加入到 appData 中以便稍后在 audioLevelObserver(音频级别观察器)的“loudest”的事件中获取关联的 Peer。
				appData = {
    
     ...appData, peerId: peer.id };

				// 使用 transport.produce() 方法创建一个新的 Producer 对象。并将 Producer 存储到 peer.data.producers 中以便稍后使用。
				const producer = await transport.produce(
					{
    
    
						kind,
						rtpParameters,
						appData
						// keyFrameRequestDelay: 5000
					});

				// Store the Producer into the protoo Peer data Object.
				peer.data.producers.set(producer.id, producer);

				// Set Producer events.
				// 设置 Producer 的事件监听器,包括 score、videoorientationchange 和 trace 事件。
				// 其中,score 事件在 Producer 的“质量”改变时触发,通常用于在客户端中显示相关信息;
				producer.on('score', (score) =>
				{
    
    
					// logger.debug(
					// 	'producer "score" event [producerId:%s, score:%o]',
					// 	producer.id, score);

					peer.notify('producerScore', {
    
     producerId: producer.id, score })
						.catch(() => {
    
    });
				});

				// videoorientationchange 事件在视频流的方向改变时触发;
				producer.on('videoorientationchange', (videoOrientation) =>
				{
    
    
					logger.debug(
						'producer "videoorientationchange" event [producerId:%s, videoOrientation:%o]',
						producer.id, videoOrientation);
				});

				// NOTE: For testing.
				// await producer.enableTraceEvent([ 'rtp', 'keyframe', 'nack', 'pli', 'fir' ]);
				// await producer.enableTraceEvent([ 'pli', 'fir' ]);
				// await producer.enableTraceEvent([ 'keyframe' ]);

				// trace 事件用于记录调试信息。
				producer.on('trace', (trace) =>
				{
    
    
					logger.debug(
						'producer "trace" event [producerId:%s, trace.type:%s, trace:%o]',
						producer.id, trace.type, trace);
				});

				// 向客户端发送 accept 响应,携带创建的 Producer 对象的 id。
				accept({
    
     id: producer.id });

				// Optimization: Create a server-side Consumer for each Peer.
				// 为每个已经加入 Room 的 Peer 创建一个新的 Consumer 对象。这是为了让每个客户端都能接收到新创建的 Producer 产生的音视频流。
				for (const otherPeer of this._getJoinedPeers({
    
     excludePeer: peer }))
				{
    
    
					this._createConsumer(
						{
    
    
							consumerPeer : otherPeer,
							producerPeer : peer,
							producer
						});
				}

				// Add into the AudioLevelObserver and ActiveSpeakerObserver.
				// 如果创建的 Producer 是音频流,则还会将其添加到 _audioLevelObserver 和 _activeSpeakerObserver 中,
				// 这两个对象都是用来监控音频流的。_audioLevelObserver 用于监测音频的音量级别,_activeSpeakerObserver
				// 用于监测当前说话人的身份。当有新的音频流加入时,需要将其添加到这两个对象中以进行监控。
				if (producer.kind === 'audio')
				{
    
    
					this._audioLevelObserver.addProducer({
    
     producerId: producer.id })
						.catch(() => {
    
    });

					this._activeSpeakerObserver.addProducer({
    
     producerId: producer.id })
						.catch(() => {
    
    });
				}

				break;
			}

			case 'closeProducer':
			{
    
    
				// Ensure the Peer is joined.
				// 确保对等端已经加入房间,如果对等端未加入房间,则会抛出错误。这是为了确保对等端具有足够的权限来关闭生产者。
				if (!peer.data.joined)
					throw new Error('Peer not yet joined');

				// 从 peer.data.producers Map 中查找具有相应 producerId 的生产者对象。如果没有找到该生产者对象,则会抛出错误。
				const {
    
     producerId } = request.data;
				const producer = peer.data.producers.get(producerId);

				if (!producer)
					throw new Error(`producer with id "${
      
      producerId}" not found`);

				// 关闭生产者。
				producer.close();

				// Remove from its map.
				// 从 peer.data.producers Map 中删除该生产者对象,以确保不再使用该生产者对象。
				peer.data.producers.delete(producer.id);

				// 向客户端发送成功响应
				accept();

				break;
			}

			case 'pauseProducer':
			{
    
    
				// Ensure the Peer is joined.
				// 确保对等端已经加入房间,如果对等端未加入房间,则会抛出错误。这是为了确保对等端具有足够的权限来暂停生产者。
				if (!peer.data.joined)
					throw new Error('Peer not yet joined');

				// 从 peer.data.producers Map 中查找具有相应 producerId 的生产者对象。如果没有找到该生产者对象,则会抛出错误。
				const {
    
     producerId } = request.data;
				const producer = peer.data.producers.get(producerId);

				if (!producer)
					throw new Error(`producer with id "${
      
      producerId}" not found`);

				// 暂停生产者,此时,生产者将停止发送媒体流,但不会关闭生产者对象。
				// await 关键字来等待 producer.pause() 方法完成后再继续执行。
				await producer.pause();

				// 向客户端发送成功响应。
				accept();

				break;
			}

			case 'resumeProducer':
			{
    
    
				// Ensure the Peer is joined.
				// 保对等端已经加入房间,如果对等端未加入房间,则会抛出错误。这是为了确保对等端具有足够的权限来恢复生产者。
				if (!peer.data.joined)
					throw new Error('Peer not yet joined');

				// 从 peer.data.producers Map 中查找具有相应 producerId 的生产者对象。如果没有找到该生产者对象,则会抛出错误。
				const {
    
     producerId } = request.data;
				const producer = peer.data.producers.get(producerId);

				if (!producer)
					throw new Error(`producer with id "${
      
      producerId}" not found`);

				// 恢复生产者,此时,生产者将重新开始发送媒体流。
				await producer.resume();

				// 向客户端发送成功响应。
				accept();

				break;
			}

			case 'pauseConsumer':
			{
    
    
				// Ensure the Peer is joined.
				// 确保对等端已经加入房间,如果对等端未加入房间,则会抛出错误。这是为了确保对等端具有足够的权限来暂停消费者。
				if (!peer.data.joined)
					throw new Error('Peer not yet joined');

				// 从 peer.data.consumers Map 中查找具有相应 consumerId 的消费者对象。如果没有找到该消费者对象,则会抛出错误。
				const {
    
     consumerId } = request.data;
				const consumer = peer.data.consumers.get(consumerId);

				if (!consumer)
					throw new Error(`consumer with id "${
      
      consumerId}" not found`);

				// 暂停消费者,此时,消费者将停止接收媒体流。
				await consumer.pause();

				// 向客户端发送成功响应。
				accept();

				break;
			}

			case 'resumeConsumer':
			{
    
    
				// Ensure the Peer is joined.
				// 确保对等端已经加入房间,如果对等端未加入房间,则会抛出错误。这是为了确保对等端具有足够的权限来恢复消费者。
				if (!peer.data.joined)
					throw new Error('Peer not yet joined');

				// 从 peer.data.consumers Map 中查找具有相应 consumerId 的消费者对象。如果没有找到该消费者对象,则会抛出错误。
				const {
    
     consumerId } = request.data;
				const consumer = peer.data.consumers.get(consumerId);

				if (!consumer)
					throw new Error(`consumer with id "${
      
      consumerId}" not found`);

				// 找到了该消费者对象,则调用其 resume() 方法来恢复消费者。此时,消费者将重新开始接收媒体流。
				await consumer.resume();

				// 向客户端发送成功响应。
				accept();

				break;
			}

			case 'setConsumerPreferredLayers':
			{
    
    
				// Ensure the Peer is joined.
				// 确保对等端已经加入房间,如果对等端未加入房间,则会抛出错误。这是为了确保对等端具有足够的权限来设置消费者的首选图层。
				if (!peer.data.joined)
					throw new Error('Peer not yet joined');

				// 检查传入的请求中是否包含消费者的 consumerId、spatialLayer 和 temporalLayer。
				// 然后,函数会从 peer.data.consumers Map 中查找具有相应 consumerId 的消费者对象。如果没有找到该消费者对象,则会抛出错误。
				const {
    
     consumerId, spatialLayer, temporalLayer } = request.data;
				const consumer = peer.data.consumers.get(consumerId);

				if (!consumer)
					throw new Error(`consumer with id "${
      
      consumerId}" not found`);

				// 设置消费者的首选图层,此时,消费者将会尝试根据指定的首选图层来选择合适的媒体流。
				await consumer.setPreferredLayers({
    
     spatialLayer, temporalLayer });

				// 向客户端发送成功响应。
				accept();

				break;
			}

			case 'setConsumerPriority':
			{
    
    
				// Ensure the Peer is joined.
				// 判断该 Peer 是否已经加入了房间,如果没有加入,则抛出异常
				if (!peer.data.joined)
					throw new Error('Peer not yet joined');

				// 检查传入的请求中是否包含消费者的 consumerId、priority。
				// 然后,函数会从 peer.data.consumers Map 中查找具有相应 consumerId 的消费者对象。如果没有找到该消费者对象,则会抛出错误。
				const {
    
     consumerId, priority } = request.data;
				const consumer = peer.data.consumers.get(consumerId);

				if (!consumer)
					throw new Error(`consumer with id "${
      
      consumerId}" not found`);

				// 设置优先级,参数 priority,表示优先级,其取值范围为 1~10。
				// 该方法会向消费者所在的生产者(Producer)发送一个 RTCP feedback message,以通知生产者当前消费者的优先级已发生变化。
				await consumer.setPriority(priority);

				// 向客户端发送成功响应。
				accept();

				break;
			}

			case 'requestConsumerKeyFrame':
			{
    
    
				// Ensure the Peer is joined.
				// 确保 Peer 已经加入房间,否则抛出异常。
				if (!peer.data.joined)
					throw new Error('Peer not yet joined');

				// 检查传入的请求中是否包含消费者的 consumerId。
				// 然后,函数会从 peer.data.consumers Map 中查找具有相应 consumerId 的消费者对象。如果没有找到该消费者对象,则会抛出错误。
				const {
    
     consumerId } = request.data;
				const consumer = peer.data.consumers.get(consumerId);

				if (!consumer)
					throw new Error(`consumer with id "${
      
      consumerId}" not found`);

				// 向生产者(Producer)发送请求,要求它在当前视频流中生成关键帧。
				await consumer.requestKeyFrame();

				// 向客户端发送成功响应。
				accept();

				break;
			}

			case 'produceData':
			{
    
    
				// Ensure the Peer is joined.
				// 首先判断Peer是否已经加入房间,如果没有加入则抛出错误。
				if (!peer.data.joined)
					throw new Error('Peer not yet joined');

				// 从请求中获取所需参数,包括transportId、sctpStreamParameters、label、protocol和appData。
				const {
    
    
					transportId,
					sctpStreamParameters,
					label,
					protocol,
					appData
				} = request.data;

				// 根据transportId获取Transport对象,如果不存在则抛出错误。
				const transport = peer.data.transports.get(transportId);

				if (!transport)
					throw new Error(`transport with id "${
      
      transportId}" not found`);

				// 调用transport对象的produceData方法创建数据生产者,传入sctpStreamParameters、label、protocol和appData等参数。
				const dataProducer = await transport.produceData(
					{
    
    
						sctpStreamParameters,
						label,
						protocol,
						appData
					});

				// Store the Producer into the protoo Peer data Object.
				// 将创建的数据生产者存储在Peer对象的dataProducers属性中,使用dataProducer.id作为key。
				peer.data.dataProducers.set(dataProducer.id, dataProducer);

				// 使用accept方法向客户端返回数据生产者的id。
				accept({
    
     id: dataProducer.id });

				// 根据数据生产者的label值进行相应的处理:
				switch (dataProducer.label)
				{
    
    
					// 如果是"chat",则为每个已加入的Peer创建一个数据消费者。
					case 'chat':
					{
    
    
						// Create a server-side DataConsumer for each Peer.
						for (const otherPeer of this._getJoinedPeers({
    
     excludePeer: peer }))
						{
    
    
							this._createDataConsumer(
								{
    
    
									dataConsumerPeer : otherPeer,
									dataProducerPeer : peer,
									dataProducer
								});
						}

						break;
					}

					// 如果是"bot",则将数据生产者传递给bot进行处理。
					case 'bot':
					{
    
    
						// Pass it to the bot.
						this._bot.handlePeerDataProducer(
							{
    
    
								dataProducerId : dataProducer.id,
								peer
							});

						break;
					}
				}

				break;
			}

			case 'changeDisplayName':
			{
    
    
				// Ensure the Peer is joined.
				// 首先判断Peer是否已经加入房间,如果没有加入则抛出错误。
				if (!peer.data.joined)
					throw new Error('Peer not yet joined');

				// 从请求数据中获取新的显示名称,并保存到protoo Peer的自定义数据对象中。同时,获取旧的显示名称。
				const {
    
     displayName } = request.data;
				const oldDisplayName = peer.data.displayName;

				// Store the display name into the custom data Object of the protoo
				// Peer.
				// 更新peer对象中的displayName字段为新的显示名称。
				peer.data.displayName = displayName;

				// Notify other joined Peers.
				// 通知其他已加入房间的用户,该用户的显示名称已经更改。遍历其他加入的用户并使用notify函数发送通知。
				for (const otherPeer of this._getJoinedPeers({
    
     excludePeer: peer }))
				{
    
    
					otherPeer.notify(
						'peerDisplayNameChanged',
						{
    
    
							peerId : peer.id,
							displayName,
							oldDisplayName
						})
						.catch(() => {
    
    });
				}

				// 向客户端发送成功响应。
				accept();

				break;
			}

			case 'getTransportStats':
			{
    
    
				// 从request.data中获取transportId,然后从peer.data.transports中查找对应的transport对象。如果找不到,将会抛出一个异常。
				const {
    
     transportId } = request.data;
				const transport = peer.data.transports.get(transportId);

				if (!transport)
					throw new Error(`transport with id "${
      
      transportId}" not found`);

				// 获取统计信息,并将返回的结果作为accept函数的参数进行回复给客户端。
				const stats = await transport.getStats();

				accept(stats);

				break;
			}

			case 'getProducerStats':
			{
    
    
				const {
    
     producerId } = request.data;
				const producer = peer.data.producers.get(producerId);

				if (!producer)
					throw new Error(`producer with id "${
      
      producerId}" not found`);

				const stats = await producer.getStats();

				accept(stats);

				break;
			}

			case 'getConsumerStats':
			{
    
    
				// 从请求数据中获取要获取统计信息的消费者的ID。然后,使用该ID从peer对象的data属性中获取消费者实例。
				// 如果没有找到该ID对应的消费者实例,则抛出异常。
				const {
    
     consumerId } = request.data;
				const consumer = peer.data.consumers.get(consumerId);

				if (!consumer)
					throw new Error(`consumer with id "${
      
      consumerId}" not found`);

				// 获取该消费者的统计信息。
				const stats = await consumer.getStats();

				// 使用accept方法将获取到的统计信息作为参数返回给客户端。
				accept(stats);

				break;
			}

			case 'getDataProducerStats':
			{
    
    
				// 从请求的数据中获取dataProducerId。
				// 通过查找peer.data.dataProducers Map中的数据,找到对应的DataProducer。如果没有找到,则抛出异常。
				const {
    
     dataProducerId } = request.data;
				const dataProducer = peer.data.dataProducers.get(dataProducerId);

				if (!dataProducer)
					throw new Error(`dataProducer with id "${
      
      dataProducerId}" not found`);

				// 获取该DataProducer的统计信息。
				const stats = await dataProducer.getStats();

				// 将获取到的统计信息作为参数调用accept函数,向请求的Peer返回获取到的数据。
				accept(stats);

				break;
			}

			case 'getDataConsumerStats':
			{
    
    
				// 从请求中获取DataConsumer的ID,然后通过该ID从客户端对应的Peer对象的dataConsumers Map中获取对应的DataConsumer对象。
				// 如果没有找到该对象,则抛出一个错误。
				const {
    
     dataConsumerId } = request.data;
				const dataConsumer = peer.data.dataConsumers.get(dataConsumerId);

				if (!dataConsumer)
					throw new Error(`dataConsumer with id "${
      
      dataConsumerId}" not found`);

				// 使用获取到的DataConsumer对象调用getStats方法获取统计数据,并将结果通过accept方法返回给客户端。
				// 统计数据包括:包的数量、字节数、丢失的包数量等等。
				const stats = await dataConsumer.getStats();

				accept(stats);

				break;
			}

			case 'applyNetworkThrottle':
			{
    
    
				const DefaultUplink = 1000000;
				const DefaultDownlink = 1000000;
				const DefaultRtt = 0;

				// 检查请求是否包含正确的“secret”值。如果未提供或与预期值不匹配,则服务器将拒绝该请求并返回一个403 Forbidden错误。
				const {
    
     uplink, downlink, rtt, secret } = request.data;

				if (!secret || secret !== process.env.NETWORK_THROTTLE_SECRET)
				{
    
    
					reject(403, 'operation NOT allowed, modda fuckaa');

					return;
				}

				try
				{
    
    
					// 解析请求中提供的上行、下行带宽和往返延迟等参数,并将它们传递给一个名为“throttle”的模块。
					// 该模块的作用是模拟网络限制的效果。
					await throttle.start(
						{
    
    
							up   : uplink || DefaultUplink,
							down : downlink || DefaultDownlink,
							rtt  : rtt || DefaultRtt
						});

					// 在成功应用网络限制后,服务器会将新的带宽和延迟值记录在日志中,并返回一个成功响应给客户端。
					logger.warn(
						'network throttle set [uplink:%s, downlink:%s, rtt:%s]',
						uplink || DefaultUplink,
						downlink || DefaultDownlink,
						rtt || DefaultRtt);

					accept();
				}
				catch (error)
				{
    
    
					// 在发生任何错误时,服务器将记录错误并返回一个500 Internal Server Error响应给客户端。
					logger.error('network throttle apply failed: %o', error);

					reject(500, error.toString());
				}

				break;
			}

			case 'resetNetworkThrottle':
			{
    
    
				const {
    
     secret } = request.data;

				// 如果 secret 值不存在或者不等于环境变量 NETWORK_THROTTLE_SECRET 的值,则拒绝请求并返回 403 错误状态码和相应的错误信息
				if (!secret || secret !== process.env.NETWORK_THROTTLE_SECRET)
				{
    
    
					reject(403, 'operation NOT allowed, modda fuckaa');

					return;
				}

				try
				{
    
    
					// 调用 throttle.stop() 方法停止网络限制
					await throttle.stop({
    
    });

					// 记录日志,表示网络限制已停止
					logger.warn('network throttle stopped');

					// 返回成功状态和空的数据对象
					accept();
				}
				catch (error)
				{
    
    
					// 如果停止网络限制失败,则记录错误日志并返回 500 错误状态码和相应的错误信息
					logger.error('network throttle stop failed: %o', error);

					reject(500, error.toString());
				}

				break;
			}

			default:
			{
    
    
				logger.error('unknown request.method "%s"', request.method);

				reject(500, `unknown request.method "${
      
      request.method}"`);
			}
		}
	}

おすすめ

転載: blog.csdn.net/weixin_41910694/article/details/129279983