流媒体学习之路(mediasoup)——Node.js部分简析 (2)

流媒体学习之路(mediasoup)——Node.js部分简析 (2)

提示:本文将集中分mediasoup-demo的server.js部分以及mediasoup源码Node.js部分。由于笔者对Node.js并不熟悉,有错误的地方请谅解。


文章目录

  • 流媒体学习之路(mediasoup)——Node.js部分简析 (2)
  • 一、mediasoup-demo—— server.js分析
  • 二、mediasoup部分分析
    • 2.1 目录结构
    • 2.2 worker.js
    • 2.3 channel.js
    • 2.4 router.js
  • 三、总结


一、mediasoup-demo—— server.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);
}

  await interactiveServer(): 使用await异步调用交互函数,该函数主要进行与终端命令行的交互工作以及worker的状态观察以便在REPL终端上使用。
  await interactiveClient(): 该函数则是创建了交互使用的客户端,内部通过socket实现。
  await runMediasoupWorkers():该函数通过配置文件,启动了worker。而配置文件获取了本机器的cpu核数。存在多少个核就创建多少个worker进程。(后续介绍mediasoup部分时再分写worker.js)
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,
//......


  await createExpressApp():创建express web框架实例进行页面。该函数中定义了相关的路由处理,实现了对传输的控制以及一些状态的获取。(代码较长在此不展示)
  await runHttpsServer():创建https服务,此处监听者重用了express 实例的。这是因为webrtc使用https协议,而默认express为http协议。
  await runProtooWebSocketServer():使用protoo框架下的websocket进行信令控制。在创建房间时使用了同步队列,防止同时创建了相同的房间。(此处可以参考:https://blog.csdn.net/gjy_it/article/details/104550811)
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);
			});
	});
}

二、mediasoup部分分析

2.1 目录结构

  mediasoup部分的目录结构为:
流媒体学习之路(mediasoup)——Node.js部分简析 (2)_第1张图片

  其中重要的目录为以下几个:
  worker目录为mediasoup的c++模块,用于生产 mediasoup-work进程的二进制文件和源代码;
   lib目录为mediasoup的Node.js模块,用于对外提供接口,用于创建mediasoup-work进程,并且充当第三方程序和该进程通信的中间层;
  test目录为具体的示例代码,可以看看如同启动mediasoup-work模块,如何创建router(room)等

2.2 worker.js

  接着标题一的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
        });

2.3 channel.js

  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则为事件通知。

2.4 router.js

  接着标题一的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

你可能感兴趣的:(mediasoup)