提示:本文将集中分mediasoup-demo的server.js部分以及mediasoup源码Node.js部分。由于笔者对Node.js并不熟悉,有错误的地方请谅解。
mediasoup-demo提供了一个可以部署的webrtc服务器,具体的部署流程此处不赘述,感兴趣的朋友可以进行相关搜索。
打开mediasoup-demo/server/server.js文件。
此处将代码分成两块:初始化部分、函数部分
初始化部分:主要通过require引入了其他模块,并进行必要的成员创建。包括:日志、异步队列、id对应房间的map、httpserver、express框架app、websocket服务、worker模块等。
#!/usr/bin/env node
process.title = 'mediasoup-demo-server';
process.env.DEBUG = process.env.DEBUG || '*INFO* *WARN* *ERROR*';
const config = require('./config');
/* eslint-disable no-console */
console.log('process.env.DEBUG:', process.env.DEBUG);
console.log('config.js:\n%s', JSON.stringify(config, null, ' '));
/* eslint-enable no-console */
const fs = require('fs');
const https = require('https');
const url = require('url');
const protoo = require('protoo-server');
const mediasoup = require('mediasoup');
const express = require('express');
const bodyParser = require('body-parser');
const { AwaitQueue } = require('awaitqueue');
const Logger = require('./lib/Logger');
const Room = require('./lib/Room');
const interactiveServer = require('./lib/interactiveServer');
const interactiveClient = require('./lib/interactiveClient');
const logger = new Logger();
// Async queue to manage rooms.
// @type {AwaitQueue}
const queue = new AwaitQueue();
// Map of Room instances indexed by roomId.
// @type {Map}
const rooms = new Map();
// HTTPS server.
// @type {https.Server}
let httpsServer;
// Express application.
// @type {Function}
let expressApp;
// Protoo WebSocket server.
// @type {protoo.WebSocketServer}
let protooWebSocketServer;
// mediasoup Workers.
// @type {Array}
const mediasoupWorkers = [];
// Index of next mediasoup Worker to use.
// @type {Number}
let nextMediasoupWorkerIdx = 0;
函数部分:从调用run()函数开始。
async function run()
{
// Open the interactive server.
await interactiveServer();
// Open the interactive client.
if (process.env.INTERACTIVE === 'true' || process.env.INTERACTIVE === '1')
await interactiveClient();
// Run a mediasoup Worker.
await runMediasoupWorkers();
// Create Express app.
await createExpressApp();
// Run HTTPS server.
await runHttpsServer();
// Run a protoo WebSocketServer.
await runProtooWebSocketServer();
// Log rooms status every X seconds.
setInterval(() =>
{
for (const room of rooms.values())
{
room.logStatus();
}
}, 120000);
}
async function runMediasoupWorkers()
{
const { numWorkers } = config.mediasoup;
logger.info('running %d mediasoup Workers...', numWorkers);
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)
});
worker.on('died', () =>
{
logger.error(
'mediasoup Worker died, exiting in 2 seconds... [pid:%d]', worker.pid);
setTimeout(() => process.exit(1), 2000);
});
mediasoupWorkers.push(worker);
// Log worker resource usage every X seconds.
setInterval(async () =>
{
const usage = await worker.getResourceUsage();
logger.info('mediasoup Worker resource usage [pid:%d]: %o', worker.pid, usage);
}, 120000);
}
}
//......
//config.mediasoup 部分代码
// Number of mediasoup workers to launch.
numWorkers : Object.keys(os.cpus()).length,
//......
async function runProtooWebSocketServer()
{
logger.info('running protoo WebSocketServer...');
// Create the protoo WebSocket server.
protooWebSocketServer = new protoo.WebSocketServer(httpsServer,
{
maxReceivedFrameSize : 960000, // 960 KBytes.
maxReceivedMessageSize : 960000,
fragmentOutgoingMessages : true,
fragmentationThreshold : 960000
});
// Handle connections from clients.
protooWebSocketServer.on('connectionrequest', (info, accept, reject) =>
{
// The client indicates the roomId and peerId in the URL query.
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;
}
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.
queue.push(async () =>
{
const room = await getOrCreateRoom({ roomId });
// Accept the protoo WebSocket connection.
const protooWebSocketTransport = accept();
room.handleProtooConnection({ peerId, protooWebSocketTransport });
})
.catch((error) =>
{
logger.error('room creation or room joining failed:%o', error);
reject(error);
});
});
}
其中重要的目录为以下几个:
worker目录为mediasoup的c++模块,用于生产 mediasoup-work进程的二进制文件和源代码;
lib目录为mediasoup的Node.js模块,用于对外提供接口,用于创建mediasoup-work进程,并且充当第三方程序和该进程通信的中间层;
test目录为具体的示例代码,可以看看如同启动mediasoup-work模块,如何创建router(room)等
接着标题一的server.js继续分析,在runMediasoupWorkers()函数中,按核数创建了多个worker。实际上调用的是该部分的worker.js代码。
class Worker extends EnhancedEventEmitter_1.EnhancedEventEmitter {
/**
* @private
* @emits died - (error: Error)
* @emits @success
* @emits @failure - (error: Error)
*/
constructor({ logLevel, logTags, rtcMinPort, rtcMaxPort, dtlsCertificateFile, dtlsPrivateKeyFile, appData }) {
...
this._child = child_process_1.spawn(
// command
spawnBin,
// args
spawnArgs,
// options
{
env: {
MEDIASOUP_VERSION: '3.6.13'
},
detached: false,
// fd 0 (stdin) : Just ignore it.
// fd 1 (stdout) : Pipe it for 3rd libraries that log their own stuff.
// fd 2 (stderr) : Same as stdout.
// fd 3 (channel) : Producer Channel fd.
// fd 4 (channel) : Consumer Channel fd.
// fd 5 (channel) : Producer PayloadChannel fd.
// fd 6 (channel) : Consumer PayloadChannel fd.
stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe', 'pipe', 'pipe'],
windowsHide: true
});
this._pid = this._child.pid;
this._channel = new Channel_1.Channel({
producerSocket: this._child.stdio[3],
consumerSocket: this._child.stdio[4],
pid: this._pid
});
...
}
代码中,经过参数校验后创建了子进程(c++程序)。
his._child = child_process_1.spawn
而spawn可以将c++进程执行的结果以流的形式返回给node.js程序。
后续创建了管道对c++进程进行了通信。
this._channel = new Channel_1.Channel({
producerSocket: this._child.stdio[3],
consumerSocket: this._child.stdio[4],
pid: this._pid
});
channel中,同样是一个生产者消费者模型。
在写入时当对方已关闭,会抛出错误。
this._producerSocket.write(ns);
在接收数据部分则是通过读取buffer。buffer有以下几种内容:信息(json格式)、debug日志、警告日志、错误日志、dump日志。
this._consumerSocket.on('data', (buffer)
通信时使用的是string类型,当第一个字符为 ‘{’ 可知为json类型的字符串,则回调以下函数:
this._processMessage(JSON.parse(nsPayload.toString('utf8')));
当返回的信息携带id时,则为消息确认;不携带id则为事件通知。
接着标题一的server.js继续分析,在runProtooWebSocketServer()函数中,引入了房间的概念。实际上房间的创建在mediasoup-demo下room.js实现。
在创建protoo房间后立刻创建了router对象。
const mediasoupRouter = await mediasoupWorker.createRouter({ mediaCodecs });
router的具体内容在mediasoup/lib/router.js下实现。主要实现了以下几个方法:
router.close():关闭路由的事件通知;
router.createWebRtcTransport(options):创建webrtc传输通道;
router.createPlainRtpTransport(options):创建普通rtp协议流传输通道(目前已弃用)(支持普通rtp协议以及srtp协议);
router.createPlainTransport(options):创建一个普通传输通道;
router.createPipeTransport(options):创建一个管道通道;
createAudioLevelObserver(options) :创建音频观察者
canConsume(options = { producerId, rtpCapabilities }) :是否可以消费生产者的数据
Node.js部分负责整个服务器信令以及其他功能的运转,c++部分则负责了所有流传输的内容。这样的设计让c++部分的性能得到了充分的发挥。由于对Node.js语言了解不足,导致整体的细节没有充分掌握,后续再针对信令系统进行详细的研究。
作者:Guo_IT 地址:https://blog.csdn.net/gjy_it/article/details/104550811