WebRTC进阶流媒体服务器开发(三)Mediasoup源码分析之应用层(代码组成、Server.js、Room.js)

一:Mediasoup Demo分析

了解Mediasoup运行机制,以及如何调用Mediasoup核心库

(一)Mediasoup Demo组成

其中mediasoup-demo为整个代码框架:(包含所有)

app应用:提供客户端所需要的应用代码

broadcasters:用于广播使用,用于推流的模块。单向传输,只有去或者只有回

server端:信令服务和媒体流服务,两者通过管道通信。细分为下面几部分:

--->config.js:配置文件/js文件,通过js获取一些基本信息。将配置信息交给servere.js使用

--->server.js:从config.js中去获取基本信息,获取信息之后去启动基本服务,比如wensocket服务、信令服务

--->lib:server.js使用的库文件,细分为以下几部分

------->Room.js:所有的真正的信令处理逻辑都是在这里实现,还描述了房间相关信息

------->interactiveClient.js:运行时内部信息查询客户端,与客户端交互(debug使用)

------->interactiveServer.js:运行时内部信息查询服务端,与服务端交换(debug使用)

mediasoup C++:C++部分,用于处理流媒体传输,包括lib与worker两部分

--->lib:一些js文件组成,主要用于对mediasoup的管理工作

--->worker:C++核心代码

二:Server.js分析

(一)配置环境,从config.js获取参数

process.title = 'mediasoup-demo-server';                            //启动进程之后,进程的名字
process.env.DEBUG = process.env.DEBUG || '*INFO* *WARN* *ERROR*';    //Debug环境变量

/*引入config.js,内部定义了一些参数:
https(证书位置,监听IP、端口)、
mediasoup(CPU个数,worker进程参数定义如日志级别、端口范围,router:room的概念在C++表示定义了音视频编解码参数)
webRtcTransport传输(IP地址、端口、输入输出码率)
*/
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, '  '));

(二)引入模块,初始化变量

const fs = require('fs');
const https = require('https');
const url = require('url');
const protoo = require('protoo-server');
const mediasoup = require('mediasoup');            //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<Number, Room>}
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<mediasoup.Worker>}
const mediasoupWorkers = [];      //数组,存放所有创建的worker进程

// Index of next mediasoup Worker to use.
// @type {Number}
let nextMediasoupWorkerIdx = 0;

(三)进入主函数,分析主函数run方法

run();                                            //进入run方法,开始执行

async function run()
{
    // Open the interactive server.
    await interactiveServer();                    //启动interactive server,用于交互---不是重点

    // Open the interactive client.
    if (process.env.INTERACTIVE === 'true' || process.env.INTERACTIVE === '1')
        await interactiveClient();                //启动interactive client,用于交互---不是重点

    // Run a mediasoup Worker.
    await runMediasoupWorkers();                //将所有的需要的进程启动,重点!!!

    // Create Express app.
    await createExpressApp();                    //https业务管理,主要用于broadcast---不是重点,但是内部创建了expressApp全局变量,在下面创建https服务中使用

    // Run HTTPS server.
    await runHttpsServer();                        //https server运行

    // Run a protoo WebSocketServer.
    await runProtooWebSocketServer();            //启动websocket,用于处理接受发送信令,重点!!!

    // Log rooms status every X seconds.
    setInterval(() =>
    {
        for (const room of rooms.values())
        {
            room.logStatus();
        }
    }, 120000);
}

(四)分析runMediasoupWorkers方法,启动相关进程服务

/**
 * Launch as many mediasoup Workers as given in the configuration file.
 */
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(            //底层调用fork创建子进程,传入相关参数
            {
                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', () =>                                    //每个worker进程,监听一个退出事件
        {
            logger.error(
                'mediasoup Worker died, exiting  in 2 seconds... [pid:%d]', worker.pid);

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

        mediasoupWorkers.push(worker);                            //将创建完成的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);
    }
}

查看createWorker方法:

/**
 * Create a Worker.
 */
async function createWorker({ logLevel = 'error', logTags, rtcMinPort = 10000, rtcMaxPort = 59999, dtlsCertificateFile, dtlsPrivateKeyFile, appData = {} } = {}) {
    logger.debug('createWorker()');
    if (appData && typeof appData !== 'object')
        throw new TypeError('if given, appData must be an object');
    const worker = new Worker_1.Worker({          //创建worker对象
        logLevel,
        logTags,
        rtcMinPort,
        rtcMaxPort,
        dtlsCertificateFile,
        dtlsPrivateKeyFile,
        appData
    });
    return new Promise((resolve, reject) => {       //监听worker创建是否成功
        worker.on('@success', () => {
            // Emit observer event.
            observer.safeEmit('newworker', worker);
            resolve(worker);                 //成功则直接返回
        });
        worker.on('@failure', reject);
    });
}
exports.createWorker = createWorker;            //模块导出,其他文件可以使用

查看Worker类:

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 }) {  //构造函数
        super();
        // Closed flag.
        this._closed = false;
        // Routers set.
        this._routers = new Set();
        // Observer instance.
        this._observer = new EnhancedEventEmitter_1.EnhancedEventEmitter();
        logger.debug('constructor()');
        let spawnBin = workerBin;
        let spawnArgs = [];
        if (process.env.MEDIASOUP_USE_VALGRIND === 'true') {
            spawnBin = process.env.MEDIASOUP_VALGRIND_BIN || 'valgrind';
            if (process.env.MEDIASOUP_VALGRIND_OPTIONS) {
                spawnArgs = spawnArgs.concat(process.env.MEDIASOUP_VALGRIND_OPTIONS.split(/\s+/));
            }
            spawnArgs.push(workerBin);
        }
        if (typeof logLevel === 'string' && logLevel)
            spawnArgs.push(`--logLevel=${logLevel}`);
        for (const logTag of (Array.isArray(logTags) ? logTags : [])) {
            if (typeof logTag === 'string' && logTag)
                spawnArgs.push(`--logTag=${logTag}`);
        }
        if (typeof rtcMinPort === 'number' && !Number.isNaN(rtcMinPort))
            spawnArgs.push(`--rtcMinPort=${rtcMinPort}`);
        if (typeof rtcMaxPort === 'number' && !Number.isNaN(rtcMaxPort))
            spawnArgs.push(`--rtcMaxPort=${rtcMaxPort}`);
        if (typeof dtlsCertificateFile === 'string' && dtlsCertificateFile)
            spawnArgs.push(`--dtlsCertificateFile=${dtlsCertificateFile}`);
        if (typeof dtlsPrivateKeyFile === 'string' && dtlsPrivateKeyFile)
            spawnArgs.push(`--dtlsPrivateKeyFile=${dtlsPrivateKeyFile}`);
        logger.debug('spawning worker process: %s %s', spawnBin, spawnArgs.join(' '));
        this._child = child_process_1.spawn(          //传入参数,启动进程
        // command
        spawnBin, 
        // args
        spawnArgs, 
        // options
        {
            env: {
                MEDIASOUP_VERSION: '3.7.11',
                // Let the worker process inherit all environment variables, useful
                // if a custom and not in the path GCC is used so the user can set
                // LD_LIBRARY_PATH environment variable for runtime.
                ...process.env
            },
            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
        });
        this._payloadChannel = new PayloadChannel_1.PayloadChannel({
            // NOTE: TypeScript does not like more than 5 fds.
            // @ts-ignore
            producerSocket: this._child.stdio[5],
            // @ts-ignore
            consumerSocket: this._child.stdio[6]
        });
        this._appData = appData;
        let spawnDone = false;
        // Listen for 'running' notification.
        this._channel.once(String(this._pid), (event) => {
            if (!spawnDone && event === 'running') {
                spawnDone = true;
                logger.debug('worker process running [pid:%s]', this._pid);
                this.emit('@success');
            }
        });
        this._child.on('exit', (code, signal) => {
            this._child = undefined;
            this.close();
            if (!spawnDone) {
                spawnDone = true;
                if (code === 42) {
                    logger.error('worker process failed due to wrong settings [pid:%s]', this._pid);
                    this.emit('@failure', new TypeError('wrong settings'));
                }
                else {
                    logger.error('worker process failed unexpectedly [pid:%s, code:%s, signal:%s]', this._pid, code, signal);
                    this.emit('@failure', new Error(`[pid:${this._pid}, code:${code}, signal:${signal}]`));
                }
            }
            else {
                logger.error('worker process died unexpectedly [pid:%s, code:%s, signal:%s]', this._pid, code, signal);
                this.safeEmit('died', new Error(`[pid:${this._pid}, code:${code}, signal:${signal}]`));
            }
        });
        this._child.on('error', (error) => {
            this._child = undefined;
            this.close();
            if (!spawnDone) {
                spawnDone = true;
                logger.error('worker process failed [pid:%s]: %s', this._pid, error.message);
                this.emit('@failure', error);
            }
            else {
                logger.error('worker process error [pid:%s]: %s', this._pid, error.message);
                this.safeEmit('died', error);
            }
        });
        // Be ready for 3rd party worker libraries logging to stdout.
        this._child.stdout.on('data', (buffer) => {
            for (const line of buffer.toString('utf8').split('\n')) {
                if (line)
                    workerLogger.debug(`(stdout) ${line}`);
            }
        });
        // In case of a worker bug, mediasoup will log to stderr.
        this._child.stderr.on('data', (buffer) => {
            for (const line of buffer.toString('utf8').split('\n')) {
                if (line)
                    workerLogger.error(`(stderr) ${line}`);
            }
        });
    }
    /**
     * Worker process identifier (PID).
     */
    get pid() {
        return this._pid;
    }
    /**
     * Whether the Worker is closed.
     */
    get closed() {
        return this._closed;
    }
    /**
     * App custom data.
     */
    get appData() {
        return this._appData;
    }
    /**
     * Invalid setter.
     */
    set appData(appData) {
        throw new Error('cannot override appData object');
    }
    /**
     * Observer.
     *
     * @emits close
     * @emits newrouter - (router: Router)
     */
    get observer() {
        return this._observer;
    }
    /**
     * Close the Worker.
     */
    close() {
        if (this._closed)
            return;
        logger.debug('close()');
        this._closed = true;
        // Kill the worker process.
        if (this._child) {
            // Remove event listeners but leave a fake 'error' hander to avoid
            // propagation.
            this._child.removeAllListeners('exit');
            this._child.removeAllListeners('error');
            this._child.on('error', () => { });
            this._child.kill('SIGTERM');
            this._child = undefined;
        }
        // Close the Channel instance.
        this._channel.close();
        // Close the PayloadChannel instance.
        this._payloadChannel.close();
        // Close every Router.
        for (const router of this._routers) {
            router.workerClosed();
        }
        this._routers.clear();
        // Emit observer event.
        this._observer.safeEmit('close');
    }
    /**
     * Dump Worker.
     */
    async dump() {
        logger.debug('dump()');
        return this._channel.request('worker.dump');
    }
    /**
     * Get mediasoup-worker process resource usage.
     */
    async getResourceUsage() {
        logger.debug('getResourceUsage()');
        return this._channel.request('worker.getResourceUsage');
    }
    /**
     * Update settings.
     */
    async updateSettings({ logLevel, logTags } = {}) {
        logger.debug('updateSettings()');
        const reqData = { logLevel, logTags };
        await this._channel.request('worker.updateSettings', undefined, reqData);
    }
    /**
     * Create a Router.
     */
    async createRouter({ mediaCodecs, appData = {} } = {}) {
        logger.debug('createRouter()');
        if (appData && typeof appData !== 'object')
            throw new TypeError('if given, appData must be an object');
        // This may throw.
        const rtpCapabilities = ortc.generateRouterRtpCapabilities(mediaCodecs);
        const internal = { routerId: uuid_1.v4() };
        await this._channel.request('worker.createRouter', internal);
        const data = { rtpCapabilities };
        const router = new Router_1.Router({
            internal,
            data,
            channel: this._channel,
            payloadChannel: this._payloadChannel,
            appData
        });
        this._routers.add(router);
        router.on('@close', () => this._routers.delete(router));
        // Emit observer event.
        this._observer.safeEmit('newrouter', router);
        return router;
    }
}
exports.Worker = Worker;

(五)分析runHttpsServer方法,启动https服务

/**
 * 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.
    const tls =
    {
        cert : fs.readFileSync(config.https.tls.cert),
        key  : fs.readFileSync(config.https.tls.key)
    };

    httpsServer = https.createServer(tls, expressApp);    //传入证书和expressApp,创建https服务,存放在全局变量中,在后面使用websocket时使用

    await new Promise((resolve) =>
    {
        httpsServer.listen(
            Number(config.https.listenPort), config.https.listenIp, resolve);  //进行监听端口
    });
}

(六)分析runProtooWebSocketServer方法,用于处理接受发送信令

/**
 * Create a protoo WebSocketServer to allow WebSocket connections from browsers.
 */
async function runProtooWebSocketServer()
{
    logger.info('running protoo WebSocketServer...');

    // Create the protoo WebSocket server.
    protooWebSocketServer = new protoo.WebSocketServer(httpsServer,  //创建websocket对象,依赖于前面创建的httpsServer
        {
            maxReceivedFrameSize     : 960000, // 960 KBytes.
            maxReceivedMessageSize   : 960000,
            fragmentOutgoingMessages : true,
            fragmentationThreshold   : 960000
        });

    // Handle connections from clients.
    protooWebSocketServer.on('connectionrequest', (info, accept, reject) =>  //侦听connectionrequest事件,处理请求
    {
        // The client indicates the roomId and peerId in the URL query.
        const u = url.parse(info.request.url, true);
        const roomId = u.query['roomId'];              //请求参数包含roomid
        const peerId = u.query['peerId'];              //用户id

        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 });  //各种消息处理,后面分析Room.js会分析这个方法
        })
            .catch((error) =>
            {
                logger.error('room creation or room joining failed:%o', error);

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

三:Room.js分析

(一)Mediasoup基本概念

Room/Router:在业务层称为Room,在C++层称为Router。

Transport/WebRtcTransport:Transport是基类,WebRtcTransport是子类,所以还有其他Transport的子类。Transport是客户端与服务端建立连接的管理层

Produce/Consume:生产者/消费者,每个用户本身是一个生产者,同时又是多个用户的消费者。数据传输通过Transport进行传输。通过Transport,可以将数据上传到流媒体服务器,同样流媒体服务器可以通过Transport下发数据到消费者。

(二)Room主要逻辑

(三)Mediasoup支持的信令

createWebRtcTransport:建立WebRTC连接,在服务端创建一个与客户端对等的点(含有信息,比如IP、端口),有了这个点之后才能建立连接

connectWebRtcTransport:真正的与客户端建立连接,数据可以开始传输

setConsumerPreferedLayers:设置更喜欢的层,比如simulcast分层,选取其中最合适的分辨率传输

requestConsumerKeyFrame:请求关键帧,避免花屏,比如一个新的用户加入视频,如果不及时请求IDR帧,那么可能获取的P、B帧无法解析,导致花屏

如果有特殊需要,可以设置自己定义的信令!!!

(四)代码分析

class Room extends EventEmitter
{
    static async create({ mediasoupWorker, roomId })
    {
        logger.info('create() [roomId:%s]', roomId);

        // Create a protoo Room instance.
        const protooRoom = new protoo.Room();                    //protoo是websocket库,实现两种功能,一个是实现房间管理(socket.io中也有这个概念),一个是websocket功能

        // Router media codecs.
        const { mediaCodecs } = config.mediasoup.routerOptions;    //获取媒体流编解码信息

        // Create a mediasoup Router.
        const mediasoupRouter = await mediasoupWorker.createRouter({ mediaCodecs });    //创建Router,后面在new Room的时候将protoo.Room和C++中的Router绑定在一起了

        // Create a mediasoup AudioLevelObserver.
        const audioLevelObserver = await mediasoupRouter.createAudioLevelObserver(        //音频音量信息
            {
                maxEntries : 1,
                threshold  : -80,
                interval   : 800
            });

        const bot = await Bot.create({ mediasoupRouter });

        return new Room(    //关联信息,创建Room,调用构造函数
            {
                roomId,
                protooRoom,
                mediasoupRouter,
                audioLevelObserver,
                bot
            });
    }


    constructor({ roomId, protooRoom, mediasoupRouter, audioLevelObserver, bot })
    {
        super();
        this.setMaxListeners(Infinity);
        this._roomId = roomId;
        this._closed = false;
        this._protooRoom = protooRoom;

        this._broadcasters = new Map();

        this._mediasoupRouter = mediasoupRouter;
        this._audioLevelObserver = audioLevelObserver;

        this._bot = bot;

        this._networkThrottled = false;
        this._handleAudioLevelObserver();    //处理音频音量事件
        global.audioLevelObserver = this._audioLevelObserver;
        global.bot = this._bot;
    }



    handleProtooConnection({ peerId, consume, protooWebSocketTransport })    //由server.js中runProtooWebSocketServer调用,用于处理客户端的连接
    {
        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);    //创建一个新的peer用户
        }
        catch (error)
        {
            logger.error('protooRoom.createPeer() failed:%o', error);
        }

        // 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();

        peer.on('request', (request, accept, reject) =>        //监听request信令
        {
            logger.debug(
                'protoo Peer "request" event [method:%s, peerId:%s]',
                request.method, peer.id);

            this._handleProtooRequest(peer, request, accept, reject)    //调用私有方法,处理请求。内部实现状态机switch...case...处理各种状态(信令,来自于request.method)
                .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 the Peer was joined, notify all Peers.
            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 is the latest Peer in the room, close the room.
            if (this._protooRoom.peers.length === 0)
            {
                logger.info(
                    'last Peer in the room left, closing the room [roomId:%s]',
                    this._roomId);

                this.close();
            }
        });
    }
}

 

posted @ 2021-06-04 16:01  山上有风景  阅读(3010)  评论(2编辑  收藏  举报