Compare commits

..

1 Commits

Author SHA1 Message Date
801652170e LINXD-2180: Added recordings 2022-08-26 10:01:08 +03:00
9 changed files with 1042 additions and 641 deletions

4
.env
View File

@ -1,7 +1,3 @@
PORT=3000 PORT=3000
IP=0.0.0.0 # Listening IPv4 or IPv6. IP=0.0.0.0 # Listening IPv4 or IPv6.
ANNOUNCED_IP=185.8.154.190 # Announced IPv4 or IPv6 (useful when running mediasoup behind NAT with private IP). ANNOUNCED_IP=185.8.154.190 # Announced IPv4 or IPv6 (useful when running mediasoup behind NAT with private IP).
RTC_MIN_PORT=2000
RTC_MAX_PORT=2020
SERVER_CERT="./server/ssl/cert.pem"
SERVER_KEY="./server/ssl/key.pem"

View File

@ -1,11 +1,5 @@
# Video server # Video server
### Generating certificates
##### To generate SSL certificates you must:
1. Go to `/server/ssl`
2. Execute `openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.pem -out cert.pem`
### Development ### Development
@ -33,7 +27,3 @@ producer = it will always be true because you are the producer
(it's possible to put false, but then you have to have another client with producer true) (it's possible to put false, but then you have to have another client with producer true)
assetName = asset name of the unit on which you are doing the test assetName = asset name of the unit on which you are doing the test
assetType = asset type of the unit on which you are doing the test assetType = asset type of the unit on which you are doing the test
### Demo project
The demo project used initially and then modified for our needs `https://github.com/jamalag/mediasoup2`

802
app.js
View File

@ -1,38 +1,31 @@
require('dotenv').config() import 'dotenv/config'
const express = require('express'); /**
const app = express(); * integrating mediasoup server with a node.js application
const Server = require('socket.io'); */
const path = require('node:path');
const fs = require('node:fs'); /* Please follow mediasoup installation requirements */
let https; /* https://mediasoup.org/documentation/v3/mediasoup/installation/ */
try { import express from 'express'
https = require('node:https'); const app = express()
} catch (err) {
console.log('https support is disabled!'); import https from 'httpolyglot'
} import fs from 'fs'
const mediasoup = require('mediasoup'); import path from 'path'
const __dirname = path.resolve()
// const FFmpegStatic = require("ffmpeg-static")
import FFmpegStatic from 'ffmpeg-static'
import Server from 'socket.io'
import mediasoup, { getSupportedRtpCapabilities } from 'mediasoup'
import Process from 'child_process'
let worker let worker
/** let router = {}
* videoCalls let producerTransport
* |-> Router let consumerTransport
* |-> Producer let producer
* |-> Consumer let consumer
* |-> Producer Transport
* |-> Consumer Transport
*
* '<callId>': {
* router: Router,
* producer: Producer,
* producerTransport: Producer Transport,
* consumer: Consumer,
* consumerTransport: Consumer Transport
* }
*
**/
let videoCalls = {}
let socketDetails = {}
app.get('/', (_req, res) => { app.get('/', (_req, res) => {
res.send('Hello from mediasoup app!') res.send('Hello from mediasoup app!')
@ -42,49 +35,299 @@ app.use('/sfu', express.static(path.join(__dirname, 'public')))
// SSL cert for HTTPS access // SSL cert for HTTPS access
const options = { const options = {
key: fs.readFileSync(process.env.SERVER_KEY, 'utf-8'), key: fs.readFileSync('./server/ssl/key.pem', 'utf-8'),
cert: fs.readFileSync(process.env.SERVER_CERT, 'utf-8'), cert: fs.readFileSync('./server/ssl/cert.pem', 'utf-8')
} }
const httpsServer = https.createServer(options, app); const httpsServer = https.createServer(options, app)
const io = new Server(httpsServer, {
allowEIO3: true,
origins: ["*:*"],
// allowRequest: (req, next) => {
// console.log('req', req);
// next(null, true)
// }
});
// const io = new Server(server, { origins: '*:*', allowEIO3: true });
httpsServer.listen(process.env.PORT, () => { httpsServer.listen(process.env.PORT, () => {
console.log('Video server listening on port:', process.env.PORT); console.log('Listening on port:', process.env.PORT)
}); })
const peers = io.of('/'); const startRecordingFfmpeg = () => {
// Return a Promise that can be awaited
let recResolve;
const promise = new Promise((res, _rej) => {
recResolve = res;
});
// const useAudio = audioEnabled();
// const useVideo = videoEnabled();
// const useH264 = h264Enabled();
// const cmdProgram = "ffmpeg"; // Found through $PATH
const cmdProgram = FFmpegStatic; // From package "ffmpeg-static"
let cmdInputPath = `${__dirname}/recording/input-vp8.sdp`;
let cmdOutputPath = `${__dirname}/recording/output-ffmpeg-vp8.webm`;
let cmdCodec = "";
let cmdFormat = "-f webm -flags +global_header";
// Ensure correct FFmpeg version is installed
const ffmpegOut = Process.execSync(cmdProgram + " -version", {
encoding: "utf8",
});
const ffmpegVerMatch = /ffmpeg version (\d+)\.(\d+)\.(\d+)/.exec(ffmpegOut);
let ffmpegOk = false;
if (ffmpegOut.startsWith("ffmpeg version git")) {
// Accept any Git build (it's up to the developer to ensure that a recent
// enough version of the FFmpeg source code has been built)
ffmpegOk = true;
} else if (ffmpegVerMatch) {
const ffmpegVerMajor = parseInt(ffmpegVerMatch[1], 10);
if (ffmpegVerMajor >= 4) {
ffmpegOk = true;
}
}
if (!ffmpegOk) {
console.error("FFmpeg >= 4.0.0 not found in $PATH; please install it");
process.exit(1);
}
// if (useAudio) {
// cmdCodec += " -map 0:a:0 -c:a copy";
// }
// if (useVideo) {
cmdCodec += " -map 0:v:0 -c:v copy";
// if (useH264) {
cmdInputPath = `${__dirname}/recording/input-h264.sdp`;
cmdOutputPath = `${__dirname}/recording/output-ffmpeg-h264.mp4`;
// "-strict experimental" is required to allow storing
// OPUS audio into MP4 container
cmdFormat = "-f mp4 -strict experimental";
// }
// }
// Run process
const cmdArgStr = [
"-nostdin",
"-protocol_whitelist file,rtp,udp",
"-loglevel debug",
"-analyzeduration 5M",
"-probesize 5M",
"-fflags +genpts",
`-i ${cmdInputPath}`,
cmdCodec,
cmdFormat,
`-y ${cmdOutputPath}`,
]
.join(" ")
.trim();
console.log('💗', cmdCodec);
console.log(`Run command: ${cmdProgram} ${cmdArgStr}`);
let recProcess = Process.spawn(cmdProgram, cmdArgStr.split(/\s+/));
global.recProcess = recProcess;
recProcess.on("error", (err) => {
console.error("Recording process error:", err);
});
recProcess.on("exit", (code, signal) => {
console.log("Recording process exit, code: %d, signal: %s", code, signal);
global.recProcess = null;
stopMediasoupRtp();
if (!signal || signal === "SIGINT") {
console.log("Recording stopped");
} else {
console.warn(
"Recording process didn't exit cleanly, output file might be corrupt"
);
}
});
// FFmpeg writes its logs to stderr
recProcess.stderr.on("data", (chunk) => {
chunk
.toString()
.split(/\r?\n/g)
.filter(Boolean) // Filter out empty strings
.forEach((line) => {
console.log(line);
if (line.startsWith("ffmpeg version")) {
setTimeout(() => {
recResolve();
}, 1000);
}
});
});
return promise;
}
const startRecordingGstreamer = () => {
// Return a Promise that can be awaited
let recResolve;
const promise = new Promise((res, _rej) => {
recResolve = res;
});
// const useAudio = audioEnabled();
// const useVideo = videoEnabled();
// const useH264 = h264Enabled();
let cmdInputPath = `${__dirname}/recording/input-vp8.sdp`;
let cmdOutputPath = `${__dirname}/recording/output-gstreamer-vp8.webm`;
let cmdMux = "webmmux";
let cmdAudioBranch = "";
let cmdVideoBranch = "";
// if (useAudio) {
// // prettier-ignore
// cmdAudioBranch =
// "demux. ! queue \
// ! rtpopusdepay \
// ! opusparse \
// ! mux.";
// }
// if (useVideo) {
// if (useH264) {
cmdInputPath = `${__dirname}/recording/input-h264.sdp`;
cmdOutputPath = `${__dirname}/recording/output-gstreamer-h264.mp4`;
cmdMux = `mp4mux faststart=true faststart-file=${cmdOutputPath}.tmp`;
// prettier-ignore
cmdVideoBranch =
"demux. ! queue \
! rtph264depay \
! h264parse \
! mux.";
// } else {
// // prettier-ignore
// cmdVideoBranch =
// "demux. ! queue \
// ! rtpvp8depay \
// ! mux.";
// }
// }
// Run process
const cmdProgram = "gst-launch-1.0"; // Found through $PATH
const cmdArgStr = [
"--eos-on-shutdown",
`filesrc location=${cmdInputPath}`,
"! sdpdemux timeout=0 name=demux",
`${cmdMux} name=mux`,
`! filesink location=${cmdOutputPath}`,
cmdAudioBranch,
cmdVideoBranch,
]
.join(" ")
.trim();
console.log(
`Run command: ${cmdProgram} ${cmdArgStr}`
);
let recProcess = Process.spawn(cmdProgram, cmdArgStr.split(/\s+/));
global.recProcess = recProcess;
recProcess.on("error", (err) => {
console.error("Recording process error:", err);
});
recProcess.on("exit", (code, signal) => {
console.log("Recording process exit, code: %d, signal: %s", code, signal);
global.recProcess = null;
stopMediasoupRtp();
if (!signal || signal === "SIGINT") {
console.log("Recording stopped");
} else {
console.warn(
"Recording process didn't exit cleanly, output file might be corrupt"
);
}
});
// GStreamer writes some initial logs to stdout
recProcess.stdout.on("data", (chunk) => {
chunk
.toString()
.split(/\r?\n/g)
.filter(Boolean) // Filter out empty strings
.forEach((line) => {
console.log(line);
if (line.startsWith("Setting pipeline to PLAYING")) {
setTimeout(() => {
recResolve();
}, 1000);
}
});
});
// GStreamer writes its progress logs to stderr
recProcess.stderr.on("data", (chunk) => {
chunk
.toString()
.split(/\r?\n/g)
.filter(Boolean) // Filter out empty strings
.forEach((line) => {
console.log(line);
});
});
return promise;
}
function stopMediasoupRtp() {
console.log("Stop mediasoup RTP transport and consumer");
// const useAudio = audioEnabled();
// const useVideo = videoEnabled();
// if (useAudio) {
// global.mediasoup.rtp.audioConsumer.close();
// global.mediasoup.rtp.audioTransport.close();
// }
// if (useVideo) {
// global.mediasoup.rtp.videoConsumer.close();
// global.mediasoup.rtp.videoTransport.close();
// }
}
const io = new Server(httpsServer)
// socket.io namespace (could represent a room?)
const peers = io.of('/mediasoup')
/**
* Worker
* |-> Router(s)
* |-> Producer Transport(s)
* |-> Producer
* |-> Consumer Transport(s)
* |-> Consumer
**/
const createWorker = async () => { const createWorker = async () => {
try {
worker = await mediasoup.createWorker({ worker = await mediasoup.createWorker({
rtcMinPort: process.env.RTC_MIN_PORT, rtcMinPort: 32256,
rtcMaxPort: process.env.RTC_MAX_PORT, rtcMaxPort: 65535,
}) })
console.log(`[createWorker] worker pid ${worker.pid}`); console.log(`[createWorker] worker pid ${worker.pid}`)
worker.on('died', error => { worker.on('died', error => {
// This implies something serious happened, so kill the application // This implies something serious happened, so kill the application
console.error('mediasoup worker has died', error); console.error('mediasoup worker has died', error)
setTimeout(() => process.exit(1), 2000); // exit in 2 seconds setTimeout(() => process.exit(1), 2000) // exit in 2 seconds
}) })
return worker;
} catch (error) { return worker
console.log(`ERROR | createWorker | ${error.message}`);
}
} }
// We create a Worker as soon as our application starts // We create a Worker as soon as our application starts
worker = createWorker(); worker = createWorker()
// This is an Array of RtpCapabilities // This is an Array of RtpCapabilities
// https://mediasoup.org/documentation/v3/mediasoup/rtp-parameters-and-capabilities/#RtpCodecCapability // https://mediasoup.org/documentation/v3/mediasoup/rtp-parameters-and-capabilities/#RtpCodecCapability
@ -92,264 +335,222 @@ worker = createWorker();
// https://github.com/versatica/mediasoup/blob/v3/src/supportedRtpCapabilities.ts // https://github.com/versatica/mediasoup/blob/v3/src/supportedRtpCapabilities.ts
const mediaCodecs = [ const mediaCodecs = [
{ {
kind: 'audio', kind: "audio",
mimeType: 'audio/opus', mimeType: "audio/opus",
preferredPayloadType: 111,
clockRate: 48000, clockRate: 48000,
channels: 2, channels: 2,
parameters: {
minptime: 10,
useinbandfec: 1,
},
}, },
{ {
kind: 'video', kind: "video",
mimeType: 'video/VP8', mimeType: "video/VP8",
preferredPayloadType: 96,
clockRate: 90000,
},
{
kind: "video",
mimeType: "video/H264",
preferredPayloadType: 125,
clockRate: 90000, clockRate: 90000,
parameters: { parameters: {
'x-google-start-bitrate': 1000, "level-asymmetry-allowed": 1,
"packetization-mode": 1,
"profile-level-id": "42e01f",
}, },
}, },
]; ]
const closeCall = (callId) => {
try {
if (videoCalls[callId]) {
videoCalls[callId].producer?.close();
videoCalls[callId].consumer?.close();
videoCalls[callId]?.consumerTransport?.close();
videoCalls[callId]?.producerTransport?.close();
videoCalls[callId]?.router?.close();
delete videoCalls[callId];
} else {
console.log(`The call with id ${callId} has already been deleted`);
}
} catch (error) {
console.log(`ERROR | closeCall | callid ${callId} | ${error.message}`);
}
}
const getRtpCapabilities = (callId, callback) => {
try {
console.log('[getRtpCapabilities] callId', callId);
const rtpCapabilities = videoCalls[callId].router.rtpCapabilities;
callback({ rtpCapabilities });
} catch (error) {
console.log(`ERROR | getRtpCapabilities | callId ${callId} | ${error.message}`);
}
}
/*
- Handlers for WS events
- These are created only when we have a connection with a peer
*/
peers.on('connection', async socket => { peers.on('connection', async socket => {
console.log('[connection] socketId:', socket.id); console.log('[connection] socketId:', socket.id)
// After making the connection successfully, we send the client a 'connection-success' event
socket.emit('connection-success', { socket.emit('connection-success', {
socketId: socket.id socketId: socket.id,
}); existsProducer: producer ? true : false,
})
// It is triggered when the peer is disconnected
socket.on('disconnect', () => { socket.on('disconnect', () => {
const callId = socketDetails[socket.id]; // do some cleanup
console.log(`disconnect | socket ${socket.id} | callId ${callId}`); console.log('peer disconnected')
delete socketDetails[socket.id]; })
closeCall(callId);
});
/*
- This event creates a room with the roomId and the callId sent
- It will return the rtpCapabilities of that room
- If the room already exists, it will not create it, but will only return rtpCapabilities
*/
socket.on('createRoom', async ({ callId }, callback) => { socket.on('createRoom', async ({ callId }, callback) => {
try {
if (callId) {
console.log(`[createRoom] socket.id ${socket.id} callId ${callId}`);
if (!videoCalls[callId]) {
console.log('[createRoom] callId', callId); console.log('[createRoom] callId', callId);
videoCalls[callId] = { router: await worker.createRouter({ mediaCodecs }) } console.log('Router length:', Object.keys(router).length);
console.log(`[createRoom] Router ID: ${videoCalls[callId].router.id}`); if (router[callId] === undefined) {
// worker.createRouter(options)
// options = { mediaCodecs, appData }
// mediaCodecs -> defined above
// appData -> custom application data - we are not supplying any
// none of the two are required
router[callId] = await worker.createRouter({ mediaCodecs })
console.log(`[createRoom] Router ID: ${router[callId].id}`)
} }
socketDetails[socket.id] = callId;
getRtpCapabilities(callId, callback);
} else {
console.log(`[createRoom] missing callId ${callId}`);
}
} catch (error) {
console.log(`ERROR | createRoom | callId ${callId} | ${error.message}`);
}
});
/* getRtpCapabilities(callId, callback)
- Client emits a request to create server side Transport })
- Depending on the sender, producerTransport or consumerTransport is created on that router
- It will return parameters, these are required for the client to create the RecvTransport
from the client.
- If the client is producer(sender: true) then it will use parameters for device.createSendTransport(params)
- If the client is a consumer(sender: false) then it will use parameters for device.createRecvTransport(params)
*/
socket.on('createWebRtcTransport', async ({ sender }, callback) => {
try {
const callId = socketDetails[socket.id];
console.log(`[createWebRtcTransport] sender ${sender} | callId ${callId}`);
if (sender) {
if (!videoCalls[callId].producerTransport) {
videoCalls[callId].producerTransport = await createWebRtcTransportLayer(callId, callback);
} else {
console.log(`producerTransport has already been defined | callId ${callId}`);
}
} else if (!sender) {
if (!videoCalls[callId].consumerTransport) {
videoCalls[callId].consumerTransport = await createWebRtcTransportLayer(callId, callback);
} else {
console.log(`consumerTransport has already been defined | callId ${callId}`);
}
}
} catch (error) {
console.log(`ERROR | createWebRtcTransport | callId ${socketDetails[socket.id]} | sender ${sender} | ${error.message}`);
}
});
/* const getRtpCapabilities = (callId, callback) => {
- The client sends this event after successfully creating a createSendTransport(AS PRODUCER) const rtpCapabilities = router[callId].rtpCapabilities
- The connection is made to the created transport
*/ callback({ rtpCapabilities })
}
// Client emits a request to create server side Transport
// We need to differentiate between the producer and consumer transports
socket.on('createWebRtcTransport', async ({ sender, callId }, callback) => {
console.log(`[createWebRtcTransport] Is this a sender request? ${sender} | callId ${callId}`)
// The client indicates if it is a producer or a consumer
// if sender is true, indicates a producer else a consumer
if (sender)
producerTransport = await createWebRtcTransportLayer(callId, callback)
else
consumerTransport = await createWebRtcTransportLayer(callId, callback)
})
// see client's socket.emit('transport-connect', ...)
socket.on('transport-connect', async ({ dtlsParameters }) => { socket.on('transport-connect', async ({ dtlsParameters }) => {
try { console.log('[transport-connect] DTLS PARAMS... ', { dtlsParameters })
const callId = socketDetails[socket.id]; await producerTransport.connect({ dtlsParameters })
if (typeof dtlsParameters === 'string') dtlsParameters = JSON.parse(dtlsParameters); })
console.log(`[transport-connect] socket.id ${socket.id} | callId ${callId}`); // see client's socket.emit('transport-produce', ...)
await videoCalls[callId].producerTransport.connect({ dtlsParameters }); socket.on('transport-produce', async ({ kind, rtpParameters, callId }, callback) => {
} catch (error) { // call produce based on the prameters from the client
console.log(`ERROR | transport-connect | callId ${socketDetails[socket.id]} | ${error.message}`); producer = await producerTransport.produce({
}
});
/*
- The event sent by the client (PRODUCER) after successfully connecting to producerTransport
- For the router with the id callId, we make produce on producerTransport
- Create the handler on producer at the 'transportclose' event
*/
socket.on('transport-produce', async ({ kind, rtpParameters, appData }, callback) => {
try {
const callId = socketDetails[socket.id];
if (typeof rtpParameters === 'string') rtpParameters = JSON.parse(rtpParameters);
console.log('[transport-produce] | socket.id', socket.id, '| callId', callId);
videoCalls[callId].producer = await videoCalls[callId].producerTransport.produce({
kind, kind,
rtpParameters, rtpParameters,
}); })
console.log(`[transport-produce] Producer ID: ${videoCalls[callId].producer.id} | kind: ${videoCalls[callId].producer.kind}`);
videoCalls[callId].producer.on('transportclose', () => { console.log(`[transport-produce] Producer ID: ${producer.id} | kind: ${producer.kind}`)
const callId = socketDetails[socket.id];
producer.on('transportclose', () => {
console.log('transport for this producer closed', callId) console.log('transport for this producer closed', callId)
closeCall(callId);
}); // https://mediasoup.org/documentation/v3/mediasoup/api/#producer-close
producer.close()
// https://mediasoup.org/documentation/v3/mediasoup/api/#router-close
router[callId].close()
delete router[callId]
})
// Send back to the client the Producer's id // Send back to the client the Producer's id
// callback({ callback({
// id: videoCalls[callId].producer.id id: producer.id
// }); })
} catch (error) {
console.log(`ERROR | transport-produce | callId ${socketDetails[socket.id]} | ${error.message}`);
} console.log('🔴', callId);
const rtpTransport = await router[callId].createPlainTransport({
comedia: false,
rtcpMux: false,
listenIp: { ip: "127.0.0.1", announcedIp: null }
});
await rtpTransport.connect({
ip: "127.0.0.1",
port: 5006,
rtcpPort: 5007,
}); });
/* console.log(
- The client sends this event after successfully creating a createRecvTransport(AS CONSUMER) "mediasoup VIDEO RTP SEND transport connected: %s:%d <--> %s:%d (%s)",
- The connection is made to the created consumerTransport rtpTransport.tuple.localIp,
*/ rtpTransport.tuple.localPort,
rtpTransport.tuple.remoteIp,
rtpTransport.tuple.remotePort,
rtpTransport.tuple.protocol
);
console.log(
"mediasoup VIDEO RTCP SEND transport connected: %s:%d <--> %s:%d (%s)",
rtpTransport.rtcpTuple.localIp,
rtpTransport.rtcpTuple.localPort,
rtpTransport.rtcpTuple.remoteIp,
rtpTransport.rtcpTuple.remotePort,
rtpTransport.rtcpTuple.protocol
);
const rtpConsumer = await rtpTransport.consume({
// producerId: global.mediasoup.webrtc.videoProducer.id,
producerId: producer.id,
// rtpCapabilities: router.rtpCapabilities,
rtpCapabilities: router[callId].rtpCapabilities,
paused: true,
});
// console.log('🟡 producerId:', producer.id, 'rtpCapabilities:', router[callId].rtpCapabilities, 'paused:', true);
await startRecordingFfmpeg();
// await startRecordingGstreamer();
rtpConsumer.resume();
})
// see client's socket.emit('transport-recv-connect', ...)
socket.on('transport-recv-connect', async ({ dtlsParameters }) => { socket.on('transport-recv-connect', async ({ dtlsParameters }) => {
console.log(`[transport-recv-connect] DTLS PARAMS: ${dtlsParameters}`)
await consumerTransport.connect({ dtlsParameters })
})
socket.on('consume', async ({ rtpCapabilities, callId }, callback) => {
try { try {
const callId = socketDetails[socket.id]; console.log('consume', rtpCapabilities, callId);
console.log(`[transport-recv-connect] socket.id ${socket.id} | callId ${callId}`); // check if the router can consume the specified producer
await videoCalls[callId].consumerTransport.connect({ dtlsParameters }); if (router[callId].canConsume({
producerId: producer.id,
rtpCapabilities
})) {
// transport can now consume and return a consumer
consumer = await consumerTransport.consume({
producerId: producer.id,
rtpCapabilities,
paused: true,
})
consumer.on('transportclose', () => {
console.log('transport close from consumer', callId)
// closeRoom(callId)
delete router[callId]
})
consumer.on('producerclose', () => {
console.log('producer of consumer closed', callId)
// https://mediasoup.org/documentation/v3/mediasoup/api/#router-close
router[callId].close()
delete router[callId]
})
// from the consumer extract the following params
// to send back to the Client
const params = {
id: consumer.id,
producerId: producer.id,
kind: consumer.kind,
rtpParameters: consumer.rtpParameters,
}
// send the parameters to the client
callback({ params })
}
} catch (error) { } catch (error) {
console.log(`ERROR | transport-recv-connect | callId ${socketDetails[socket.id]} | ${error.message}`); console.log(error.message)
callback({
params: {
error: error
}
})
} }
}) })
/*
- The customer consumes after successfully connecting to consumerTransport
- The previous step was 'transport-recv-connect', and before that 'createWebRtcTransport'
- This event is only sent by the consumer
- The parameters that the consumer consumes are returned
- The consumer does consumerTransport.consume(params)
*/
socket.on('consume', async ({ rtpCapabilities }, callback) => {
try {
const callId = socketDetails[socket.id];
console.log('[consume] callId', callId);
// Check if the router can consume the specified producer
if (videoCalls[callId].router.canConsume({
producerId: videoCalls[callId].producer.id,
rtpCapabilities
})) {
console.log('[consume] Can consume', callId);
// Transport can now consume and return a consumer
videoCalls[callId].consumer = await videoCalls[callId].consumerTransport.consume({
producerId: videoCalls[callId].producer.id,
rtpCapabilities,
paused: true,
});
// https://mediasoup.org/documentation/v3/mediasoup/api/#consumer-on-transportclose
videoCalls[callId].consumer.on('transportclose', () => {
const callId = socketDetails[socket.id];
console.log('transport close from consumer', callId);
closeCall();
});
// https://mediasoup.org/documentation/v3/mediasoup/api/#consumer-on-producerclose
videoCalls[callId].consumer.on('producerclose', () => {
const callId = socketDetails[socket.id];
console.log('producer of consumer closed', callId);
closeCall();
});
// From the consumer extract the following params to send back to the Client
const params = {
id: videoCalls[callId].consumer.id,
producerId: videoCalls[callId].producer.id,
kind: videoCalls[callId].consumer.kind,
rtpParameters: videoCalls[callId].consumer.rtpParameters,
};
// Send the parameters to the client
callback({ params });
} else {
console.log(`[canConsume] Can't consume | callId ${callId}`);
}
} catch (error) {
console.log(`ERROR | consume | callId ${socketDetails[socket.id]} | ${error.message}`)
callback({ params: { error } });
}
});
/*
- Event sent by the consumer after consuming to resume the pause
- When consuming on consumerTransport, it is initially done with paused: true, here we will resume
*/
socket.on('consumer-resume', async () => { socket.on('consumer-resume', async () => {
try { console.log(`[consumer-resume]`)
const callId = socketDetails[socket.id]; await consumer.resume()
console.log(`[consumer-resume] callId ${callId}`) })
await videoCalls[callId].consumer.resume(); })
} catch (error) {
console.log(`ERROR | consumer-resume | callId ${socketDetails[socket.id]} | ${error.message}`);
}
});
});
/*
- Called from at event 'createWebRtcTransport' and assigned to the consumer or producer transport
- It will return parameters, these are required for the client to create the RecvTransport
from the client.
- If the client is producer(sender: true) then it will use parameters for device.createSendTransport(params)
- If the client is a consumer(sender: false) then it will use parameters for device.createRecvTransport(params)
*/
const createWebRtcTransportLayer = async (callId, callback) => { const createWebRtcTransportLayer = async (callId, callback) => {
try { try {
console.log('[createWebRtcTransportLayer] callId', callId); console.log('[createWebRtcTransportLayer] callId', callId);
@ -364,40 +565,49 @@ const createWebRtcTransportLayer = async (callId, callback) => {
enableUdp: true, enableUdp: true,
enableTcp: true, enableTcp: true,
preferUdp: true, preferUdp: true,
}; initialAvailableOutgoingBitrate: 300000
}
// console.log('webRtcTransport_options', webRtcTransport_options);
// console.log('router', router, '| router[callId]', router[callId]);
// https://mediasoup.org/documentation/v3/mediasoup/api/#router-createWebRtcTransport // https://mediasoup.org/documentation/v3/mediasoup/api/#router-createWebRtcTransport
let transport = await videoCalls[callId].router.createWebRtcTransport(webRtcTransport_options) let transport = await router[callId].createWebRtcTransport(webRtcTransport_options)
console.log(`callId: ${callId} | transport id: ${transport.id}`) console.log(`callId: ${callId} | transport id: ${transport.id}`)
// Handler for when DTLS(Datagram Transport Layer Security) changes
transport.on('dtlsstatechange', dtlsState => { transport.on('dtlsstatechange', dtlsState => {
console.log(`transport | dtlsstatechange | calldId ${callId} | dtlsState ${dtlsState}`);
if (dtlsState === 'closed') { if (dtlsState === 'closed') {
transport.close(); transport.close()
} }
}); })
// Handler if the transport layer has closed (for various reasons)
transport.on('close', () => { transport.on('close', () => {
console.log(`transport | closed | calldId ${callId}`); console.log('transport closed')
}); })
const params = { const params = {
id: transport.id, id: transport.id,
iceParameters: transport.iceParameters, iceParameters: transport.iceParameters,
iceCandidates: transport.iceCandidates, iceCandidates: transport.iceCandidates,
dtlsParameters: transport.dtlsParameters, dtlsParameters: transport.dtlsParameters,
}; }
// Send back to the client the params console.log('params', params);
callback({ params });
// Set transport to producerTransport or consumerTransport // send back to the client the following prameters
return transport; callback({
// https://mediasoup.org/documentation/v3/mediasoup-client/api/#TransportOptions
params
})
return transport
} catch (error) { } catch (error) {
console.log(`ERROR | createWebRtcTransportLayer | callId ${socketDetails[socket.id]} | ${error.message}`); console.log('[createWebRtcTransportLayer] ERROR', JSON.stringify(error));
callback({ params: { error } }); callback({
params: {
error: error
}
})
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 571 KiB

805
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,17 +5,20 @@
"main": "app.js", "main": "app.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"start:dev": "nodemon app.js", "start:dev": "nodemon app.ts",
"start:prod": "pm2 start ./app.js -n video-server", "start:prod": "pm2 start ./app.js -n video-server",
"watch": "watchify public/index.js -o public/bundle.js -v" "watch": "watchify public/index.js -o public/bundle.js -v"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"type": "module",
"dependencies": { "dependencies": {
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"dotenv": "^16.0.1", "dotenv": "^16.0.1",
"express": "^4.18.1", "express": "^4.18.1",
"ffmpeg-static": "^5.0.2",
"httpolyglot": "^0.1.2",
"mediasoup": "^3.10.4", "mediasoup": "^3.10.4",
"mediasoup-client": "^3.6.54", "mediasoup-client": "^3.6.54",
"parcel": "^2.7.0", "parcel": "^2.7.0",

View File

@ -20808,7 +20808,7 @@ const getLocalStream = () => {
}) })
.then(streamSuccess) .then(streamSuccess)
.catch(error => { .catch(error => {
console.log(error.message) console.log('getLocalStream', error)
}) })
} }
@ -20903,7 +20903,7 @@ const createSendTransport = () => {
}) })
producerTransport.on('produce', async (parameters, callback, errback) => { producerTransport.on('produce', async (parameters, callback, errback) => {
console.log(parameters) console.log('produce', parameters)
try { try {
// tell the server to create a Producer // tell the server to create a Producer
@ -20913,7 +20913,7 @@ const createSendTransport = () => {
await socket.emit('transport-produce', { await socket.emit('transport-produce', {
kind: parameters.kind, kind: parameters.kind,
rtpParameters: parameters.rtpParameters, rtpParameters: parameters.rtpParameters,
appData: parameters.appData, callId: callId
}, ({ id }) => { }, ({ id }) => {
// Tell the transport that parameters were transmitted and provide it with the // Tell the transport that parameters were transmitted and provide it with the
// server side producer's id. // server side producer's id.
@ -21009,7 +21009,6 @@ const createRecvTransport = async () => {
} }
const resetCallSettings = () => { const resetCallSettings = () => {
socket.emit('transportclose', { callId })
localVideo.srcObject = null localVideo.srcObject = null
remoteVideo.srcObject = null remoteVideo.srcObject = null
consumer = null consumer = null
@ -21072,7 +21071,7 @@ const closeCall = () => {
const closeCallBtn = document.getElementById('btnCloseCall') const closeCallBtn = document.getElementById('btnCloseCall')
closeCallBtn.setAttribute('disabled', '') closeCallBtn.setAttribute('disabled', '')
// Reset settings and send closeTransport to video server // Reset settings
resetCallSettings() resetCallSettings()
} }

View File

@ -1,5 +1,5 @@
module.exports = { module.exports = {
hubAddress: 'https://hub.dev.linx.safemobile.com/', hubAddress: 'https://hub.dev.linx.safemobile.com/',
mediasoupAddress: 'https://video.safemobile.org/mediasoup', // mediasoupAddress: 'https://video.safemobile.org/mediasoup',
// mediasoupAddress: 'http://localhost:3000/mediasoup', mediasoupAddress: 'http://localhost:3000/mediasoup',
} }

View File

@ -149,7 +149,7 @@ const getLocalStream = () => {
}) })
.then(streamSuccess) .then(streamSuccess)
.catch(error => { .catch(error => {
console.log(error.message) console.log('getLocalStream', error)
}) })
} }
@ -244,7 +244,7 @@ const createSendTransport = () => {
}) })
producerTransport.on('produce', async (parameters, callback, errback) => { producerTransport.on('produce', async (parameters, callback, errback) => {
console.log(parameters) console.log('produce', parameters)
try { try {
// tell the server to create a Producer // tell the server to create a Producer
@ -254,7 +254,7 @@ const createSendTransport = () => {
await socket.emit('transport-produce', { await socket.emit('transport-produce', {
kind: parameters.kind, kind: parameters.kind,
rtpParameters: parameters.rtpParameters, rtpParameters: parameters.rtpParameters,
appData: parameters.appData, callId: callId
}, ({ id }) => { }, ({ id }) => {
// Tell the transport that parameters were transmitted and provide it with the // Tell the transport that parameters were transmitted and provide it with the
// server side producer's id. // server side producer's id.