mediasoup-demo server源码分析
0. 目录
- mediasoup-demo是什么?
- mediasoup-demo代码结构
- mediasoup-demo server代码分析
- config.js代码解析
- server.js主要逻辑
- Room.js具体代码解析
相关文章:
- mediasoup基本介绍及Ubuntu/Docker环境下部署mediasoup
- mediasoup-demo server源码分析
1. mediasoup-demo是什么?
- mediasoup-demo是一个使用mediasoup库实现的视频会议应用程序的示例代码。
- mediasoup-demo示例应用程序提供了一个基于WebRTC的视频会议解决方案,包括房间管理、媒体流管理、网络传输和音视频编解码等功能,同时提供了前端和后端代码示例,方便进行学习和参考。
- 其中mediasoup是一个开源的WebRTC信令和媒体服务器,用于构建实时音视频通信应用程序。
2. mediasoup-demo代码结构
- mediasoup-demo代码结构图如下:


- 其中各目录或文件作用如下:
目录 |
下一级目录或文件 |
作用 |
app |
|
客户端代码 |
broadcasters |
|
广播,推流或者拉流 |
server |
|
服务端Demo |
|
server.js |
服务端Demo主程序 |
|
config.js |
配置文件 |
|
cert |
证书及秘钥 |
|
connect.js |
对后面的interactiveClient.js文件进行封装 |
lib |
|
server.js使用的库文件 |
|
Logger.js |
打印日志 |
|
Room.js |
房间管理及信令处理 |
|
interactiveClient.js |
运行时内部信息查询客户端 |
|
interactiveServer.js |
运行时内部信息查询服务端 |
- 重点关注server目录文件内容。
3. mediasoup-demo server代码分析
- mediasoup-demo项目中server目录实现了视频会议应用程序的后端功能,包括与mediasoup的集成、WebSocket通信、房间管理、日志记录等功能。

1. config.js代码解析
- config.js是配置文件,用于定义mediasoup的配置和服务器的端口等参数。
- 配置文件如下:
const os = require('os');
module.exports =
{
domain : process.env.DOMAIN || 'localhost',
https :
{
listenIp : '0.0.0.0',
listenPort : process.env.PROTOO_LISTEN_PORT || 4443,
tls :
{
cert : process.env.HTTPS_CERT_FULLCHAIN || `${__dirname}/certs/fullchain.pem`,
key : process.env.HTTPS_CERT_PRIVKEY || `${__dirname}/certs/privkey.pem`
}
},
mediasoup :
{
numWorkers : Object.keys(os.cpus()).length,
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
},
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
}
}
]
},
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
}
],
},
webRtcTransportOptions :
{
listenIps :
[
{
ip : process.env.MEDIASOUP_LISTEN_IP || '0.0.0.0',
announcedIp : process.env.MEDIASOUP_ANNOUNCED_IP
}
],
initialAvailableOutgoingBitrate : 1000000,
minimumAvailableOutgoingBitrate : 600000,
maxSctpMessageSize : 262144,
maxIncomingBitrate : 1500000
},
plainTransportOptions :
{
listenIp :
{
ip : process.env.MEDIASOUP_LISTEN_IP || '0.0.0.0',
announcedIp : process.env.MEDIASOUP_ANNOUNCED_IP
},
maxSctpMessageSize : 262144
}
}
};
- config.js配置信息注释:
父字段 |
子字段 |
作用 |
https |
|
|
|
listenIp |
服务器监听的IP地址 |
|
listenPort |
服务器监听的端口 |
mediasoup |
|
|
|
workerSettings |
mediasoup的Worker配置参数,包括worker的数量、Worker进程的文件路径、logLevel等 |
|
routerOptions |
mediasoup的Router配置参数,包括媒体编解码器配置参数 |
|
webRtcServerOptions |
mediasoup的WebRtcServer配置参数,包括监听协议、IP和端口 |
|
webRtcTransportOptions |
mediasoup的WebRtcTransport配置参数,包括UDP/TCP转发的监听端口、最大带宽、最大数据包等 |
|
plainTransportOptions |
mediasoup的PlainTransport配置参数,包括UDP/TCP转发的监听端口、最大带宽、最大数据包等 |
- 详细字段含义见官方文档:https://mediasoup.org/documentation/v3/mediasoup/api
2. server.js代码解析
- server.js是mediasoup-demo应用程序的核心部分,负责启动和管理各个组件,以及处理音视频数据的传输和处理。
1. server.js主要逻辑
- 加载配置文件:读取config.js文件中的配置参数,包括HTTP和WebSocket服务器的端口号、mediasoup Worker进程的数量等等。
- 启动HTTP服务器:创建一个HTTP服务器,并在指定端口上监听请求。
- HTTP服务器主要用于提供静态资源,例如前端页面、CSS和JS文件等。
- 启动WebSocket服务器:创建一个WebSocket服务器,并在指定端口上监听连接请求。
- WebSocket服务器用于处理实时音视频数据的传输。
- 启动mediasoup Worker进程:根据配置文件中的worker.numWorkers参数,创建指定数量的mediasoup Worker进程。
- 每个Worker进程负责管理mediasoup Router对象和Transport对象,以及处理音视频流的转发和处理。
- 处理WebSocket连接:当有新的WebSocket连接建立时,server.js会创建一个新的mediasoup Router对象,并将其绑定到一个指定的mediasoup Worker进程上。
- 同时,server.js会为该WebSocket连接创建一个Transport对象,并将其绑定到相应的mediasoup Router对象上。
- 这样,WebSocket客户端就可以通过Transport对象和mediasoup Router对象进行音视频流的传输和处理。
- 处理HTTP请求:当有HTTP请求到达时,server.js会根据请求路径返回相应的静态资源。
- 例如,请求"/"路径时返回index.html页面。
- 错误处理:server.js还负责处理各种错误,包括HTTP和WebSocket服务器的错误、mediasoup Worker进程的错误以及WebSocket客户端的错误等等。
- 当出现错误时,server.js会将错误信息记录到日志文件中,并尝试恢复或重新启动相关的服务或进程。
2. server.js具体代码解析
- server目录中server.js是服务端应用程序的入口文件,负责启动应用程序、创建HTTP和WebSocket服务器以及管理mediasoup Worker进程。
- 其中run()函数是server.js入口函数:
async function run()
{
await interactiveServer();
if (process.env.INTERACTIVE === 'true' || process.env.INTERACTIVE === '1')
await interactiveClient();
await runMediasoupWorkers();
await createExpressApp();
await runHttpsServer();
await runProtooWebSocketServer();
setInterval(() =>
{
for (const room of rooms.values())
{
room.logStatus();
}
}, 120000);
}
1. runMediasoupWorkers() 函数解析
- mediasoup-demo中,mediasoup Worker是mediasoup的核心部分,它们负责处理音视频流并管理mediasoup的底层资源。
- 由于mediasoup Worker的创建和销毁比较耗时,因此在mediasoup-demo中会创建多个mediasoup Worker来提高系统的处理能力。
- 在runMediasoupWorkers() 函数中,可以创建多个mediasoup Worker,并对其进行管理和监控,从而保证mediasoup-demo的正常运行。
- 具体代码和注释如下:
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);
if (process.env.MEDIASOUP_USE_WEBRTC_SERVER !== 'false')
{
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);
worker.appData.webRtcServer = webRtcServer;
}
setInterval(async () =>
{
const usage = await worker.getResourceUsage();
logger.info('mediasoup Worker resource usage [pid:%d]: %o', worker.pid, usage);
}, 120000);
}
}
2. mediasoup.createWorker() 函数解析
- runMediasoupWorkers() 函数中mediasoup.createWorker()方法是mediasoup库提供的一个异步方法,用于创建mediasoup Worker实例,其具体实现是在mediasoup项目的node/src/index.ts代码中。
- 在mediasoup-demo v3版本中,mediasoup.createWorker()方法被调用多次,每次调用都会创建一个Worker实例,并把该实例推入全局变量mediasoupWorkers数组中。
- 在v3版本的实现中,每个Worker实例将拥有自己的独立的mediasoup Router实例,从而保证不同的房间使用独立的Router实例进行媒体处理,提高系统的可扩展性和稳定性。
- 同时,每个Worker实例都会在其对应的端口上创建一个WebRtcServer实例,用于管理WebRTC客户端的连接。
- 下面是mediasoup.createWorker() 方法的详细解析:
- options: Object类型,表示创建Worker实例的选项。该选项包括:
- logLevel: String类型,表示Worker实例的日志级别。默认值为warn。
- logTags: Array类型,表示Worker实例的日志标签。默认值为[‘info’, ‘ice’, ‘dtls’, ‘rtp’, ‘srtp’, ‘rtcp’, ‘rtx’, ‘bwe’, ‘score’, ‘simulcast’, ‘svc’]。
- rtcMinPort: Number类型,表示Worker实例使用的最小UDP端口。默认值为10000。
- rtcMaxPort: Number类型,表示Worker实例使用的最大UDP端口。默认值为59999。
- dtlsCertificateFile 和 dtlsPrivateKeyFile 是 DTLS 的证书和私钥文件的路径。
- 如果未设置这些文件的路径,则 mediasoup 将使用默认的证书和私钥。
- libwebrtcFieldTrials 是 Google WebRTC 库中的实验性功能。
- appData 可以用于存储与 worker 实例相关的应用程序数据。
- 如果设置了 appData,则其必须是一个对象。默认情况下,appData 为 undefined。
- 返回值: Promise类型,表示创建的Worker实例对象。
export async function createWorker(
{
logLevel = 'error',
logTags,
rtcMinPort = 10000,
rtcMaxPort = 59999,
dtlsCertificateFile,
dtlsPrivateKeyFile,
libwebrtcFieldTrials,
appData
}: WorkerSettings = {}
): Promise<Worker>
{
logger.debug('createWorker()');
if (appData && typeof appData !== 'object')
throw new TypeError('if given, appData must be an object');
const worker = new Worker(
{
logLevel,
logTags,
rtcMinPort,
rtcMaxPort,
dtlsCertificateFile,
dtlsPrivateKeyFile,
libwebrtcFieldTrials,
appData
});
return new Promise((resolve, reject) =>
{
worker.on('@success', () =>
{
observer.safeEmit('newworker', worker);
resolve(worker);
});
worker.on('@failure', reject);
});
}
- 其中,new Worker会创建一个 Worker 工作进程,并在工作进程与主进程之间建立IPC通道,从而实现进程间通信。
- Worker 用于处理mediasoup的核心功能,如RTP流传输,房间管理等。
3. runHttpsServer() 函数解析
- runHttpsServer() 函数的主要功能是创建和运行一个基于 HTTPS 的 Web 服务器,并监听配置文件中指定的IP地址和端口。
- 具体代码和注释如下:
async function runHttpsServer()
{
logger.info('running an HTTPS server...');
const tls =
{
cert : fs.readFileSync(config.https.tls.cert),
key : fs.readFileSync(config.https.tls.key)
};
httpsServer = https.createServer(tls, expressApp);
await new Promise((resolve) =>
{
httpsServer.listen(
Number(config.https.listenPort), config.https.listenIp, resolve);
});
}
4. runProtooWebSocketServer() 函数解析
- runProtooWebSocketServer() 函数主要用于启动一个 WebSocket 服务器,来允许浏览器通过WebSocket连接到服务器。
async function runProtooWebSocketServer()
{
logger.info('running protoo WebSocketServer...');
protooWebSocketServer = new protoo.WebSocketServer(httpsServer,
{
maxReceivedFrameSize : 960000,
maxReceivedMessageSize : 960000,
fragmentOutgoingMessages : true,
fragmentationThreshold : 960000
});
protooWebSocketServer.on('connectionrequest', (info, accept, reject) =>
{
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;
}
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);
queue.push(async () =>
{
const room = await getOrCreateRoom({ roomId, consumerReplicas });
const protooWebSocketTransport = accept();
room.handleProtooConnection({ peerId, protooWebSocketTransport });
})
.catch((error) =>
{
logger.error('room creation or room joining failed:%o', error);
reject(error);
});
});
}
3. Room.js代码解析
- Room.js实现了应用程序中单个房间的管理逻辑,负责管理房间成员的加入/离开、生产者/消费者的创建和管理等一系列操作。
1. Room.js具体代码解析
1. create() 函数解析
- create() 函数创建一个新的Room实例。
- 具体代码和注释如下:
static async create({ mediasoupWorker, roomId, consumerReplicas })
{
logger.info('create() [roomId:%s]', roomId);
const protooRoom = new protoo.Room();
const { mediaCodecs } = config.mediasoup.routerOptions;
const mediasoupRouter = await mediasoupWorker.createRouter({ mediaCodecs });
const audioLevelObserver = await mediasoupRouter.createAudioLevelObserver(
{
maxEntries : 1,
threshold : -80,
interval : 800
});
const activeSpeakerObserver = await mediasoupRouter.createActiveSpeakerObserver();
const bot = await Bot.create({ mediasoupRouter });
return new Room(
{
roomId,
protooRoom,
webRtcServer : mediasoupWorker.appData.webRtcServer,
mediasoupRouter,
audioLevelObserver,
activeSpeakerObserver,
consumerReplicas,
bot
});
}
2. handleProtooConnection() 函数解析
- handleProtooConnection() 函数用于管理与客户端之间的连接,确保每个连接都是唯一的,并且能够处理客户端发送的请求并返回相应的结果。
- 在server.js中runProtooWebSocketServer() 函数创建一个 WebSocket 服务器后,这个服务器会注册 connectionrequest 事件的监听器,有新的 WebSocket 连接请求时被触发,最终会调用到handleProtooConnection() 函数。
- 具体代码和注释如下:
handleProtooConnection({ peerId, consume, protooWebSocketTransport })
{
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();
}
let peer;
try
{
peer = this._protooRoom.createPeer(peerId, protooWebSocketTransport);
}
catch (error)
{
logger.error('protooRoom.createPeer() failed:%o', error);
}
peer.data.consume = consume;
peer.data.joined = false;
peer.data.displayName = undefined;
peer.data.device = undefined;
peer.data.rtpCapabilities = undefined;
peer.data.sctpCapabilities = undefined;
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();
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);
});
});
peer.on('close', () =>
{
if (this._closed)
return;
logger.debug('protoo Peer "close" event [peerId:%s]', peer.id);
if (peer.data.joined)
{
for (const otherPeer of this._getJoinedPeers({ excludePeer: peer }))
{
otherPeer.notify('peerClosed', { peerId: peer.id })
.catch(() => {});
}
}
for (const transport of peer.data.transports.values())
{
transport.close();
}
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() 函数解析
- _handleProtooRequest 函数是处理客户端通过 WebSocket 连接发送的消息。
- _handleProtooRequest 函数定义如下:
async _handleProtooRequest(peer, request, accept, reject)
- 参数说明:
- peer:发送消息的客户端连接。
- request:客户端发送的消息内容。
- accept:回调函数,用于向客户端发送响应消息。
- reject:回调函数,用于向客户端发送错误响应消息。
- request 参数是客户端发送的消息内容,其包含了 request.method 和 request.data 两个字段。
- 其中 request.method 表示消息类型,request.data 表示消息携带的数据。
- 根据 request.method 的不同,将消息分配到不同的处理函数中。
- 消息类型总有有22种,分别的含义为:
信令 |
说明 |
getRouterRtpCapabilities |
客户端请求获取 mediasoupRouter 的 rtpCapabilities,即媒体流传输的相关能力参数。 |
join |
客户端加入房间 |
createWebRtcTransport |
创建 WebRTC 传输对象并将其存储到 peer.data.transports 中,以供后续的音视频传输使用。 |
connectWebRtcTransport |
在建立 WebRtcTransport 之后,客户端使用本地生成的 DTLS 参数连接 WebRtcTransport 时发送 |
restartIce |
重新启动WebRtcTransport中的ICE Agent,从而获取新的ICE Candidates并更新连接 |
produce |
创建一个新的音频或视频的 Producer 并与客户端关联 |
closeProducer |
关闭一个指定的生产者,并从 peer.data.producers Map 中删除该生产者 |
pauseProducer |
尝试暂停一个指定的生产者 |
resumeProducer |
尝试恢复一个指定的生产者 |
pauseConsumer |
尝试暂停一个指定的消费者 |
resumeConsumer |
尝试恢复一个指定的消费者 |
setConsumerPreferredLayers |
尝试设置指定消费者的首选图层 |
setConsumerPriority |
尝试设置消费者优先级(priority) |
requestConsumerKeyFrame |
尝试获取某个消费者的关键帧(Key Frame) |
produceData |
创建数据生产者,并存储在 peer.data.dataProducers Map的数据对象中 |
changeDisplayName |
处理客户端请求更改显示名称 |
getTransportStats |
获取某个transport的统计信息 |
getProducerStats |
获取某个消费者的统计信息 |
getDataProducerStats |
获取某个数据生产者的统计信息 |
getDataConsumerStats |
获取某个数据消费者的统计信息 |
applyNetworkThrottle |
修改网络带宽和延迟模拟网络限制 |
resetNetworkThrottle |
重置网络限制 |
- 具体代码解析和注释如下:
async _handleProtooRequest(peer, request, accept, reject)
{
switch (request.method)
{
case 'getRouterRtpCapabilities':
{
accept(this._mediasoupRouter.rtpCapabilities);
break;
}
case 'join':
{
if (peer.data.joined)
throw new Error('Peer already joined');
const {
displayName,
device,
rtpCapabilities,
sctpCapabilities
} = request.data;
peer.data.joined = true;
peer.data.displayName = displayName;
peer.data.device = device;
peer.data.rtpCapabilities = rtpCapabilities;
peer.data.sctpCapabilities = sctpCapabilities;
const joinedPeers =
[
...this._getJoinedPeers(),
...this._broadcasters.values()
];
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 });
peer.data.joined = true;
for (const joinedPeer of joinedPeers)
{
for (const producer of joinedPeer.data.producers.values())
{
this._createConsumer(
{
consumerPeer : peer,
producerPeer : joinedPeer,
producer
});
}
for (const dataProducer of joinedPeer.data.dataProducers.values())
{
if (dataProducer.label === 'bot')
continue;
this._createDataConsumer(
{
dataConsumerPeer : peer,
dataProducerPeer : joinedPeer,
dataProducer
});
}
}
this._createDataConsumer(
{
dataConsumerPeer : peer,
dataProducerPeer : null,
dataProducer : this._bot.dataProducer
});
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':
{
const {
forceTcp,
producing,
consuming,
sctpCapabilities
} = request.data;
const webRtcTransportOptions =
{
...config.mediasoup.webRtcTransportOptions,
enableSctp : Boolean(sctpCapabilities),
numSctpStreams : (sctpCapabilities || {}).numStreams,
appData : { producing, consuming }
};
if (forceTcp)
{
webRtcTransportOptions.enableUdp = false;
webRtcTransportOptions.enableTcp = true;
}
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);
});
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(() => {});
}
});
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 (maxIncomingBitrate)
{
try { await transport.setMaxIncomingBitrate(maxIncomingBitrate); }
catch (error) {}
}
break;
}
case 'connectWebRtcTransport':
{
const { transportId, dtlsParameters } = request.data;
const transport = peer.data.transports.get(transportId);
if (!transport)
throw new Error(`transport with id "${transportId}" not found`);
await transport.connect({ dtlsParameters });
accept();
break;
}
case 'restartIce':
{
const { transportId } = request.data;
const transport = peer.data.transports.get(transportId);
if (!transport)
throw new Error(`transport with id "${transportId}" not found`);
const iceParameters = await transport.restartIce();
accept(iceParameters);
break;
}
case 'produce':
{
if (!peer.data.joined)
throw new Error('Peer not yet joined');
const { transportId, kind, rtpParameters } = request.data;
let { appData } = request.data;
const transport = peer.data.transports.get(transportId);
if (!transport)
throw new Error(`transport with id "${transportId}" not found`);
appData = { ...appData, peerId: peer.id };
const producer = await transport.produce(
{
kind,
rtpParameters,
appData
});
peer.data.producers.set(producer.id, producer);
producer.on('score', (score) =>
{
peer.notify('producerScore', { producerId: producer.id, score })
.catch(() => {});
});
producer.on('videoorientationchange', (videoOrientation) =>
{
logger.debug(
'producer "videoorientationchange" event [producerId:%s, videoOrientation:%o]',
producer.id, videoOrientation);
});
producer.on('trace', (trace) =>
{
logger.debug(
'producer "trace" event [producerId:%s, trace.type:%s, trace:%o]',
producer.id, trace.type, trace);
});
accept({ id: producer.id });
for (const otherPeer of this._getJoinedPeers({ excludePeer: peer }))
{
this._createConsumer(
{
consumerPeer : otherPeer,
producerPeer : peer,
producer
});
}
if (producer.kind === 'audio')
{
this._audioLevelObserver.addProducer({ producerId: producer.id })
.catch(() => {});
this._activeSpeakerObserver.addProducer({ producerId: producer.id })
.catch(() => {});
}
break;
}
case 'closeProducer':
{
if (!peer.data.joined)
throw new Error('Peer not yet joined');
const { producerId } = request.data;
const producer = peer.data.producers.get(producerId);
if (!producer)
throw new Error(`producer with id "${producerId}" not found`);
producer.close();
peer.data.producers.delete(producer.id);
accept();
break;
}
case 'pauseProducer':
{
if (!peer.data.joined)
throw new Error('Peer not yet joined');
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();
accept();
break;
}
case 'resumeProducer':
{
if (!peer.data.joined)
throw new Error('Peer not yet joined');
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':
{
if (!peer.data.joined)
throw new Error('Peer not yet joined');
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':
{
if (!peer.data.joined)
throw new Error('Peer not yet joined');
const { consumerId } = request.data;
const consumer = peer.data.consumers.get(consumerId);
if (!consumer)
throw new Error(`consumer with id "${consumerId}" not found`);
await consumer.resume();
accept();
break;
}
case 'setConsumerPreferredLayers':
{
if (!peer.data.joined)
throw new Error('Peer not yet joined');
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':
{
if (!peer.data.joined)
throw new Error('Peer not yet joined');
const { consumerId, priority } = request.data;
const consumer = peer.data.consumers.get(consumerId);
if (!consumer)
throw new Error(`consumer with id "${consumerId}" not found`);
await consumer.setPriority(priority);
accept();
break;
}
case 'requestConsumerKeyFrame':
{
if (!peer.data.joined)
throw new Error('Peer not yet joined');
const { consumerId } = request.data;
const consumer = peer.data.consumers.get(consumerId);
if (!consumer)
throw new Error(`consumer with id "${consumerId}" not found`);
await consumer.requestKeyFrame();
accept();
break;
}
case 'produceData':
{
if (!peer.data.joined)
throw new Error('Peer not yet joined');
const {
transportId,
sctpStreamParameters,
label,
protocol,
appData
} = request.data;
const transport = peer.data.transports.get(transportId);
if (!transport)
throw new Error(`transport with id "${transportId}" not found`);
const dataProducer = await transport.produceData(
{
sctpStreamParameters,
label,
protocol,
appData
});
peer.data.dataProducers.set(dataProducer.id, dataProducer);
accept({ id: dataProducer.id });
switch (dataProducer.label)
{
case 'chat':
{
for (const otherPeer of this._getJoinedPeers({ excludePeer: peer }))
{
this._createDataConsumer(
{
dataConsumerPeer : otherPeer,
dataProducerPeer : peer,
dataProducer
});
}
break;
}
case 'bot':
{
this._bot.handlePeerDataProducer(
{
dataProducerId : dataProducer.id,
peer
});
break;
}
}
break;
}
case 'changeDisplayName':
{
if (!peer.data.joined)
throw new Error('Peer not yet joined');
const { displayName } = request.data;
const oldDisplayName = peer.data.displayName;
peer.data.displayName = displayName;
for (const otherPeer of this._getJoinedPeers({ excludePeer: peer }))
{
otherPeer.notify(
'peerDisplayNameChanged',
{
peerId : peer.id,
displayName,
oldDisplayName
})
.catch(() => {});
}
accept();
break;
}
case 'getTransportStats':
{
const { transportId } = request.data;
const transport = peer.data.transports.get(transportId);
if (!transport)
throw new Error(`transport with id "${transportId}" not found`);
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':
{
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(stats);
break;
}
case 'getDataProducerStats':
{
const { dataProducerId } = request.data;
const dataProducer = peer.data.dataProducers.get(dataProducerId);
if (!dataProducer)
throw new Error(`dataProducer with id "${dataProducerId}" not found`);
const stats = await dataProducer.getStats();
accept(stats);
break;
}
case 'getDataConsumerStats':
{
const { dataConsumerId } = request.data;
const dataConsumer = peer.data.dataConsumers.get(dataConsumerId);
if (!dataConsumer)
throw new Error(`dataConsumer with id "${dataConsumerId}" not found`);
const stats = await dataConsumer.getStats();
accept(stats);
break;
}
case 'applyNetworkThrottle':
{
const DefaultUplink = 1000000;
const DefaultDownlink = 1000000;
const DefaultRtt = 0;
const { uplink, downlink, rtt, secret } = request.data;
if (!secret || secret !== process.env.NETWORK_THROTTLE_SECRET)
{
reject(403, 'operation NOT allowed, modda fuckaa');
return;
}
try
{
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)
{
logger.error('network throttle apply failed: %o', error);
reject(500, error.toString());
}
break;
}
case 'resetNetworkThrottle':
{
const { secret } = request.data;
if (!secret || secret !== process.env.NETWORK_THROTTLE_SECRET)
{
reject(403, 'operation NOT allowed, modda fuckaa');
return;
}
try
{
await throttle.stop({});
logger.warn('network throttle stopped');
accept();
}
catch (error)
{
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}"`);
}
}
}