[*] 结构调整

This commit is contained in:
acgist
2023-02-02 19:43:26 +08:00
parent 1de076ae16
commit d09a9dbc1f
201 changed files with 4 additions and 4 deletions

View File

@@ -0,0 +1,20 @@
# 媒体
只要负责媒体处理,不要添加任何业务逻辑,所有业务逻辑都由[taoyao-signal](../taoyao-signal)处理。
## Mediasoup
```
# 更新
git submodule init
git submodule update --remote
# 编译
```
## 使用
```
sudo npm install
```

View File

@@ -0,0 +1,164 @@
/**
* 配置
*/
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
}
}
};

View File

@@ -0,0 +1,16 @@
{
"name": "taoyao-media",
"author": "acgist",
"version": "1.0.0",
"private": true,
"license": "All Rights Reserved",
"description": "taoyao media server",
"scripts": {
"start": "DEBUG=${DEBUG:='*mediasoup* *INFO* *WARN* *ERROR*'} INTERACTIVE=${INTERACTIVE:='true'} node server.js"
},
"dependencies": {
"mediasoup": "file:./mediasoup"
},
"devDependencies": {
}
}

View File

@@ -0,0 +1,557 @@
/**
* 媒体服务
*/
#!/usr/bin/env node
process.title = 'taoyao-client';
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 url = require('url');
const https = require('https');
const protoo = require('protoo-server');
const express = require('express');
const mediasoup = require('mediasoup');
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 = [];
// 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);
}
/**
* 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(
{
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);
// 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.
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;
}
// 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);
}
}
/**
* Create an Express based API server to manage Broadcaster requests.
*/
async function createExpressApp()
{
logger.info('creating Express app...');
expressApp = express();
expressApp.use(bodyParser.json());
/**
* For every API request, verify that the roomId in the path matches and
* existing room.
*/
expressApp.param(
'roomId', (req, res, next, roomId) =>
{
// The room must exist for all API requests.
if (!rooms.has(roomId))
{
const error = new Error(`room with id "${roomId}" not found`);
error.status = 404;
throw error;
}
req.room = rooms.get(roomId);
next();
});
/**
* API GET resource that returns the mediasoup Router RTP capabilities of
* the room.
*/
expressApp.get(
'/rooms/:roomId', (req, res) =>
{
const data = req.room.getRouterRtpCapabilities();
res.status(200).json(data);
});
/**
* POST API to create a Broadcaster.
*/
expressApp.post(
'/rooms/:roomId/broadcasters', async (req, res, next) =>
{
const {
id,
displayName,
device,
rtpCapabilities
} = req.body;
try
{
const data = await req.room.createBroadcaster(
{
id,
displayName,
device,
rtpCapabilities
});
res.status(200).json(data);
}
catch (error)
{
next(error);
}
});
/**
* DELETE API to delete a Broadcaster.
*/
expressApp.delete(
'/rooms/:roomId/broadcasters/:broadcasterId', (req, res) =>
{
const { broadcasterId } = req.params;
req.room.deleteBroadcaster({ broadcasterId });
res.status(200).send('broadcaster deleted');
});
/**
* POST API to create a mediasoup Transport associated to a Broadcaster.
* It can be a PlainTransport or a WebRtcTransport depending on the
* type parameters in the body. There are also additional parameters for
* PlainTransport.
*/
expressApp.post(
'/rooms/:roomId/broadcasters/:broadcasterId/transports',
async (req, res, next) =>
{
const { broadcasterId } = req.params;
const { type, rtcpMux, comedia, sctpCapabilities } = req.body;
try
{
const data = await req.room.createBroadcasterTransport(
{
broadcasterId,
type,
rtcpMux,
comedia,
sctpCapabilities
});
res.status(200).json(data);
}
catch (error)
{
next(error);
}
});
/**
* POST API to connect a Transport belonging to a Broadcaster. Not needed
* for PlainTransport if it was created with comedia option set to true.
*/
expressApp.post(
'/rooms/:roomId/broadcasters/:broadcasterId/transports/:transportId/connect',
async (req, res, next) =>
{
const { broadcasterId, transportId } = req.params;
const { dtlsParameters } = req.body;
try
{
const data = await req.room.connectBroadcasterTransport(
{
broadcasterId,
transportId,
dtlsParameters
});
res.status(200).json(data);
}
catch (error)
{
next(error);
}
});
/**
* POST API to create a mediasoup Producer associated to a Broadcaster.
* The exact Transport in which the Producer must be created is signaled in
* the URL path. Body parameters include kind and rtpParameters of the
* Producer.
*/
expressApp.post(
'/rooms/:roomId/broadcasters/:broadcasterId/transports/:transportId/producers',
async (req, res, next) =>
{
const { broadcasterId, transportId } = req.params;
const { kind, rtpParameters } = req.body;
try
{
const data = await req.room.createBroadcasterProducer(
{
broadcasterId,
transportId,
kind,
rtpParameters
});
res.status(200).json(data);
}
catch (error)
{
next(error);
}
});
/**
* POST API to create a mediasoup Consumer associated to a Broadcaster.
* The exact Transport in which the Consumer must be created is signaled in
* the URL path. Query parameters must include the desired producerId to
* consume.
*/
expressApp.post(
'/rooms/:roomId/broadcasters/:broadcasterId/transports/:transportId/consume',
async (req, res, next) =>
{
const { broadcasterId, transportId } = req.params;
const { producerId } = req.query;
try
{
const data = await req.room.createBroadcasterConsumer(
{
broadcasterId,
transportId,
producerId
});
res.status(200).json(data);
}
catch (error)
{
next(error);
}
});
/**
* POST API to create a mediasoup DataConsumer associated to a Broadcaster.
* The exact Transport in which the DataConsumer must be created is signaled in
* the URL path. Query body must include the desired producerId to
* consume.
*/
expressApp.post(
'/rooms/:roomId/broadcasters/:broadcasterId/transports/:transportId/consume/data',
async (req, res, next) =>
{
const { broadcasterId, transportId } = req.params;
const { dataProducerId } = req.body;
try
{
const data = await req.room.createBroadcasterDataConsumer(
{
broadcasterId,
transportId,
dataProducerId
});
res.status(200).json(data);
}
catch (error)
{
next(error);
}
});
/**
* POST API to create a mediasoup DataProducer associated to a Broadcaster.
* The exact Transport in which the DataProducer must be created is signaled in
*/
expressApp.post(
'/rooms/:roomId/broadcasters/:broadcasterId/transports/:transportId/produce/data',
async (req, res, next) =>
{
const { broadcasterId, transportId } = req.params;
const { label, protocol, sctpStreamParameters, appData } = req.body;
try
{
const data = await req.room.createBroadcasterDataProducer(
{
broadcasterId,
transportId,
label,
protocol,
sctpStreamParameters,
appData
});
res.status(200).json(data);
}
catch (error)
{
next(error);
}
});
/**
* Error handler.
*/
expressApp.use(
(error, req, res, next) =>
{
if (error)
{
logger.warn('Express app %s', String(error));
error.status = error.status || (error.name === 'TypeError' ? 400 : 500);
res.statusMessage = error.message;
res.status(error.status).send(String(error));
}
else
{
next();
}
});
}
/**
* 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);
await new Promise((resolve) =>
{
httpsServer.listen(
Number(config.https.listenPort), config.https.listenIp, resolve);
});
}
/**
* 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,
{
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;
}
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.
queue.push(async () =>
{
const room = await getOrCreateRoom({ roomId, consumerReplicas });
// 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);
});
});
}
/**
* Get next mediasoup Worker.
*/
function getMediasoupWorker()
{
const worker = mediasoupWorkers[nextMediasoupWorkerIdx];
if (++nextMediasoupWorkerIdx === mediasoupWorkers.length)
nextMediasoupWorkerIdx = 0;
return worker;
}
/**
* Get a Room instance (or create one if it does not exist).
*/
async function getOrCreateRoom({ roomId, consumerReplicas })
{
let room = rooms.get(roomId);
// If the Room does not exist create a new one.
if (!room)
{
logger.info('creating a new Room [roomId:%s]', roomId);
const mediasoupWorker = getMediasoupWorker();
room = await Room.create({ mediasoupWorker, roomId, consumerReplicas });
rooms.set(roomId, room);
room.on('close', () => rooms.delete(roomId));
}
return room;
}

View File

@@ -0,0 +1,5 @@
/**
* 适配媒体信令
* 1. 终端媒体流向
* 2. 处理音频视频:降噪、水印等等
*/