Compare commits
95 Commits
LINXD-2180
...
4.01
Author | SHA1 | Date | |
---|---|---|---|
bbf23c33d4 | |||
5c2808e75a | |||
2aea7497cc | |||
56835d6660 | |||
fc42c79210 | |||
d81bc8582d | |||
a4d16998cd | |||
de1458bbde | |||
b0fad5f1db | |||
eb5aa12d65 | |||
52b4794a86 | |||
5f8f2ab44c | |||
55455be8e7 | |||
62a82dc3a5 | |||
ac078e72ff | |||
be396e1047 | |||
149876fc70 | |||
adbeb2071b | |||
a6681ffe40 | |||
efc9bfd114 | |||
a8afa8a532 | |||
507c131058 | |||
043f66eb0c | |||
cb5716dd5c | |||
ae39a45f6d | |||
0ec5769ee0 | |||
72ee3e43ab | |||
f20c7fada8 | |||
53a654c50f | |||
d54403299f | |||
177d54ec67 | |||
649c7a3767 | |||
08d6ccbb21 | |||
fd005351b5 | |||
fc111540d8 | |||
c4f4be0aa8 | |||
40c03592df | |||
a59cbcf8cc | |||
7cc3a95b38 | |||
05e3d997f1 | |||
9c731f4085 | |||
f6d862966e | |||
05ccd5cfd4 | |||
43eee11c7e | |||
0033cd528d | |||
5022d88b1d | |||
52b922825f | |||
07be8af9ae | |||
29737fe5d8 | |||
1f5755b72d | |||
a2c878f91c | |||
7b6f78725b | |||
41c6ad281d | |||
f5406f163f | |||
28497fda91 | |||
4a98a79630 | |||
22e8b4d364 | |||
575dbd69b0 | |||
a51a757d17 | |||
c059dd5afc | |||
19808da24e | |||
2f6c25c171 | |||
ead0069aa8 | |||
434c8f744c | |||
7198dc91b1 | |||
f629012712 | |||
41b50d2a11 | |||
b85ba68c9c | |||
6acd276324 | |||
294dbdf38d | |||
c3d50fdc4e | |||
c12ececf47 | |||
47eb302f5f | |||
accf960aa7 | |||
ab685270f1 | |||
6938e751fe | |||
031a7bc4c5 | |||
d7486d0fd6 | |||
38931f0654 | |||
bb684ca4db | |||
25a76c343b | |||
817a49204d | |||
91b4db1982 | |||
8562f6c58c | |||
5abc309502 | |||
ecb5a88a2c | |||
782b749ea3 | |||
b834016dcb | |||
523945271e | |||
0af7ddd786 | |||
ba5add489d | |||
d5cb144799 | |||
4e92f6cdd3 | |||
aaa1c5cea4 | |||
4f302570a2 |
4
.env
4
.env
@ -1,3 +1,7 @@
|
||||
PORT=3000
|
||||
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).
|
||||
RTC_MIN_PORT=2000
|
||||
RTC_MAX_PORT=2020
|
||||
SERVER_CERT="./server/ssl/cert.pem"
|
||||
SERVER_KEY="./server/ssl/key.pem"
|
12
README.md
12
README.md
@ -1,5 +1,11 @@
|
||||
# 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
|
||||
|
||||
@ -26,4 +32,8 @@ accountId = account id of the unit on which you are doing the test
|
||||
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)
|
||||
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`
|
||||
|
||||
|
838
app.js
838
app.js
@ -1,31 +1,38 @@
|
||||
import 'dotenv/config'
|
||||
require('dotenv').config()
|
||||
|
||||
/**
|
||||
* integrating mediasoup server with a node.js application
|
||||
*/
|
||||
|
||||
/* Please follow mediasoup installation requirements */
|
||||
/* https://mediasoup.org/documentation/v3/mediasoup/installation/ */
|
||||
import express from 'express'
|
||||
const app = express()
|
||||
|
||||
import https from 'httpolyglot'
|
||||
import fs from 'fs'
|
||||
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'
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
const Server = require('socket.io');
|
||||
const path = require('node:path');
|
||||
const fs = require('node:fs');
|
||||
let https;
|
||||
try {
|
||||
https = require('node:https');
|
||||
} catch (err) {
|
||||
console.log('https support is disabled!');
|
||||
}
|
||||
const mediasoup = require('mediasoup');
|
||||
|
||||
let worker
|
||||
let router = {}
|
||||
let producerTransport
|
||||
let consumerTransport
|
||||
let producer
|
||||
let consumer
|
||||
/**
|
||||
* videoCalls
|
||||
* |-> Router
|
||||
* |-> Producer
|
||||
* |-> 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) => {
|
||||
res.send('Hello from mediasoup app!')
|
||||
@ -35,299 +42,49 @@ app.use('/sfu', express.static(path.join(__dirname, 'public')))
|
||||
|
||||
// SSL cert for HTTPS access
|
||||
const options = {
|
||||
key: fs.readFileSync('./server/ssl/key.pem', 'utf-8'),
|
||||
cert: fs.readFileSync('./server/ssl/cert.pem', 'utf-8')
|
||||
key: fs.readFileSync(process.env.SERVER_KEY, 'utf-8'),
|
||||
cert: fs.readFileSync(process.env.SERVER_CERT, '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, () => {
|
||||
console.log('Listening on port:', process.env.PORT)
|
||||
})
|
||||
console.log('Video server listening on port:', process.env.PORT);
|
||||
});
|
||||
|
||||
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 peers = io.of('/');
|
||||
|
||||
const createWorker = async () => {
|
||||
worker = await mediasoup.createWorker({
|
||||
rtcMinPort: 32256,
|
||||
rtcMaxPort: 65535,
|
||||
})
|
||||
console.log(`[createWorker] worker pid ${worker.pid}`)
|
||||
|
||||
worker.on('died', error => {
|
||||
// This implies something serious happened, so kill the application
|
||||
console.error('mediasoup worker has died', error)
|
||||
setTimeout(() => process.exit(1), 2000) // exit in 2 seconds
|
||||
})
|
||||
|
||||
return worker
|
||||
try {
|
||||
worker = await mediasoup.createWorker({
|
||||
rtcMinPort: process.env.RTC_MIN_PORT,
|
||||
rtcMaxPort: process.env.RTC_MAX_PORT,
|
||||
})
|
||||
console.log(`[createWorker] worker pid ${worker.pid}`);
|
||||
|
||||
worker.on('died', error => {
|
||||
// This implies something serious happened, so kill the application
|
||||
console.error('mediasoup worker has died', error);
|
||||
setTimeout(() => process.exit(1), 2000); // exit in 2 seconds
|
||||
})
|
||||
return worker;
|
||||
} catch (error) {
|
||||
console.log(`ERROR | createWorker | ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// We create a Worker as soon as our application starts
|
||||
worker = createWorker()
|
||||
worker = createWorker();
|
||||
|
||||
// This is an Array of RtpCapabilities
|
||||
// https://mediasoup.org/documentation/v3/mediasoup/rtp-parameters-and-capabilities/#RtpCodecCapability
|
||||
@ -335,222 +92,264 @@ worker = createWorker()
|
||||
// https://github.com/versatica/mediasoup/blob/v3/src/supportedRtpCapabilities.ts
|
||||
const mediaCodecs = [
|
||||
{
|
||||
kind: "audio",
|
||||
mimeType: "audio/opus",
|
||||
preferredPayloadType: 111,
|
||||
kind: 'audio',
|
||||
mimeType: 'audio/opus',
|
||||
clockRate: 48000,
|
||||
channels: 2,
|
||||
parameters: {
|
||||
minptime: 10,
|
||||
useinbandfec: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "video",
|
||||
mimeType: "video/VP8",
|
||||
preferredPayloadType: 96,
|
||||
clockRate: 90000,
|
||||
},
|
||||
{
|
||||
kind: "video",
|
||||
mimeType: "video/H264",
|
||||
preferredPayloadType: 125,
|
||||
kind: 'video',
|
||||
mimeType: 'video/VP8',
|
||||
clockRate: 90000,
|
||||
parameters: {
|
||||
"level-asymmetry-allowed": 1,
|
||||
"packetization-mode": 1,
|
||||
"profile-level-id": "42e01f",
|
||||
'x-google-start-bitrate': 1000,
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
peers.on('connection', async socket => {
|
||||
console.log('[connection] socketId:', socket.id)
|
||||
socket.emit('connection-success', {
|
||||
socketId: socket.id,
|
||||
existsProducer: producer ? true : false,
|
||||
})
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
// do some cleanup
|
||||
console.log('peer disconnected')
|
||||
})
|
||||
|
||||
socket.on('createRoom', async ({ callId }, callback) => {
|
||||
console.log('[createRoom] callId', callId);
|
||||
console.log('Router length:', Object.keys(router).length);
|
||||
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}`)
|
||||
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`);
|
||||
}
|
||||
|
||||
getRtpCapabilities(callId, callback)
|
||||
})
|
||||
|
||||
const getRtpCapabilities = (callId, callback) => {
|
||||
const rtpCapabilities = router[callId].rtpCapabilities
|
||||
|
||||
callback({ rtpCapabilities })
|
||||
} catch (error) {
|
||||
console.log(`ERROR | closeCall | callid ${callId} | ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
})
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
// see client's socket.emit('transport-connect', ...)
|
||||
socket.on('transport-connect', async ({ dtlsParameters }) => {
|
||||
console.log('[transport-connect] DTLS PARAMS... ', { dtlsParameters })
|
||||
await producerTransport.connect({ dtlsParameters })
|
||||
})
|
||||
/*
|
||||
- Handlers for WS events
|
||||
- These are created only when we have a connection with a peer
|
||||
*/
|
||||
peers.on('connection', async socket => {
|
||||
console.log('[connection] socketId:', socket.id);
|
||||
|
||||
// see client's socket.emit('transport-produce', ...)
|
||||
socket.on('transport-produce', async ({ kind, rtpParameters, callId }, callback) => {
|
||||
// call produce based on the prameters from the client
|
||||
producer = await producerTransport.produce({
|
||||
kind,
|
||||
rtpParameters,
|
||||
})
|
||||
// After making the connection successfully, we send the client a 'connection-success' event
|
||||
socket.emit('connection-success', {
|
||||
socketId: socket.id
|
||||
});
|
||||
|
||||
console.log(`[transport-produce] Producer ID: ${producer.id} | kind: ${producer.kind}`)
|
||||
// It is triggered when the peer is disconnected
|
||||
socket.on('disconnect', () => {
|
||||
const callId = socketDetails[socket.id];
|
||||
console.log(`disconnect | socket ${socket.id} | callId ${callId}`);
|
||||
delete socketDetails[socket.id];
|
||||
closeCall(callId);
|
||||
});
|
||||
|
||||
producer.on('transportclose', () => {
|
||||
console.log('transport for this producer closed', 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
|
||||
callback({
|
||||
id: producer.id
|
||||
})
|
||||
|
||||
|
||||
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(
|
||||
"mediasoup VIDEO RTP SEND transport connected: %s:%d <--> %s:%d (%s)",
|
||||
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 }) => {
|
||||
console.log(`[transport-recv-connect] DTLS PARAMS: ${dtlsParameters}`)
|
||||
await consumerTransport.connect({ dtlsParameters })
|
||||
})
|
||||
|
||||
socket.on('consume', async ({ rtpCapabilities, callId }, callback) => {
|
||||
/*
|
||||
- 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) => {
|
||||
try {
|
||||
console.log('consume', rtpCapabilities, callId);
|
||||
// check if the router can consume the specified producer
|
||||
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,
|
||||
if (callId) {
|
||||
console.log(`[createRoom] socket.id ${socket.id} callId ${callId}`);
|
||||
if (!videoCalls[callId]) {
|
||||
console.log('[createRoom] callId', callId);
|
||||
videoCalls[callId] = { router: await worker.createRouter({ mediaCodecs }) }
|
||||
console.log(`[createRoom] Router ID: ${videoCalls[callId].router.id}`);
|
||||
}
|
||||
|
||||
// send the parameters to the client
|
||||
callback({ params })
|
||||
socketDetails[socket.id] = callId;
|
||||
getRtpCapabilities(callId, callback);
|
||||
} else {
|
||||
console.log(`[createRoom] missing callId ${callId}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error.message)
|
||||
callback({
|
||||
params: {
|
||||
error: error
|
||||
console.log(`ERROR | createRoom | callId ${callId} | ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
- 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}`);
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
- The client sends this event after successfully creating a createSendTransport(AS PRODUCER)
|
||||
- The connection is made to the created transport
|
||||
*/
|
||||
socket.on('transport-connect', async ({ dtlsParameters }) => {
|
||||
try {
|
||||
const callId = socketDetails[socket.id];
|
||||
if (typeof dtlsParameters === 'string') dtlsParameters = JSON.parse(dtlsParameters);
|
||||
|
||||
console.log(`[transport-connect] socket.id ${socket.id} | callId ${callId}`);
|
||||
await videoCalls[callId].producerTransport.connect({ dtlsParameters });
|
||||
} catch (error) {
|
||||
console.log(`ERROR | transport-connect | callId ${socketDetails[socket.id]} | ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
- 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,
|
||||
rtpParameters,
|
||||
});
|
||||
console.log(`[transport-produce] Producer ID: ${videoCalls[callId].producer.id} | kind: ${videoCalls[callId].producer.kind}`);
|
||||
|
||||
videoCalls[callId].producer.on('transportclose', () => {
|
||||
const callId = socketDetails[socket.id];
|
||||
console.log('transport for this producer closed', callId)
|
||||
closeCall(callId);
|
||||
});
|
||||
|
||||
// Send back to the client the Producer's id
|
||||
// callback({
|
||||
// id: videoCalls[callId].producer.id
|
||||
// });
|
||||
} catch (error) {
|
||||
console.log(`ERROR | transport-produce | callId ${socketDetails[socket.id]} | ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
- The client sends this event after successfully creating a createRecvTransport(AS CONSUMER)
|
||||
- The connection is made to the created consumerTransport
|
||||
*/
|
||||
socket.on('transport-recv-connect', async ({ dtlsParameters }) => {
|
||||
try {
|
||||
const callId = socketDetails[socket.id];
|
||||
console.log(`[transport-recv-connect] socket.id ${socket.id} | callId ${callId}`);
|
||||
await videoCalls[callId].consumerTransport.connect({ dtlsParameters });
|
||||
} catch (error) {
|
||||
console.log(`ERROR | transport-recv-connect | callId ${socketDetails[socket.id]} | ${error.message}`);
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('consumer-resume', async () => {
|
||||
console.log(`[consumer-resume]`)
|
||||
await consumer.resume()
|
||||
})
|
||||
})
|
||||
/*
|
||||
- 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 () => {
|
||||
try {
|
||||
const callId = socketDetails[socket.id];
|
||||
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) => {
|
||||
try {
|
||||
console.log('[createWebRtcTransportLayer] callId', callId);
|
||||
@ -565,49 +364,40 @@ const createWebRtcTransportLayer = async (callId, callback) => {
|
||||
enableUdp: true,
|
||||
enableTcp: 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
|
||||
let transport = await router[callId].createWebRtcTransport(webRtcTransport_options)
|
||||
let transport = await videoCalls[callId].router.createWebRtcTransport(webRtcTransport_options)
|
||||
console.log(`callId: ${callId} | transport id: ${transport.id}`)
|
||||
|
||||
// Handler for when DTLS(Datagram Transport Layer Security) changes
|
||||
transport.on('dtlsstatechange', dtlsState => {
|
||||
console.log(`transport | dtlsstatechange | calldId ${callId} | dtlsState ${dtlsState}`);
|
||||
if (dtlsState === 'closed') {
|
||||
transport.close()
|
||||
transport.close();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Handler if the transport layer has closed (for various reasons)
|
||||
transport.on('close', () => {
|
||||
console.log('transport closed')
|
||||
})
|
||||
console.log(`transport | closed | calldId ${callId}`);
|
||||
});
|
||||
|
||||
const params = {
|
||||
id: transport.id,
|
||||
iceParameters: transport.iceParameters,
|
||||
iceCandidates: transport.iceCandidates,
|
||||
dtlsParameters: transport.dtlsParameters,
|
||||
}
|
||||
};
|
||||
|
||||
console.log('params', params);
|
||||
// Send back to the client the params
|
||||
callback({ params });
|
||||
|
||||
// send back to the client the following prameters
|
||||
callback({
|
||||
// https://mediasoup.org/documentation/v3/mediasoup-client/api/#TransportOptions
|
||||
params
|
||||
})
|
||||
|
||||
return transport
|
||||
// Set transport to producerTransport or consumerTransport
|
||||
return transport;
|
||||
|
||||
} catch (error) {
|
||||
console.log('[createWebRtcTransportLayer] ERROR', JSON.stringify(error));
|
||||
callback({
|
||||
params: {
|
||||
error: error
|
||||
}
|
||||
})
|
||||
console.log(`ERROR | createWebRtcTransportLayer | callId ${socketDetails[socket.id]} | ${error.message}`);
|
||||
callback({ params: { error } });
|
||||
}
|
||||
}
|
BIN
doc/[video] Workflow.png
Normal file
BIN
doc/[video] Workflow.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 571 KiB |
805
package-lock.json
generated
805
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -5,20 +5,17 @@
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start:dev": "nodemon app.ts",
|
||||
"start:dev": "nodemon app.js",
|
||||
"start:prod": "pm2 start ./app.js -n video-server",
|
||||
"watch": "watchify public/index.js -o public/bundle.js -v"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@types/express": "^4.17.13",
|
||||
"dotenv": "^16.0.1",
|
||||
"express": "^4.18.1",
|
||||
"ffmpeg-static": "^5.0.2",
|
||||
"httpolyglot": "^0.1.2",
|
||||
"mediasoup": "^3.10.4",
|
||||
"mediasoup-client": "^3.6.54",
|
||||
"parcel": "^2.7.0",
|
||||
|
@ -20808,7 +20808,7 @@ const getLocalStream = () => {
|
||||
})
|
||||
.then(streamSuccess)
|
||||
.catch(error => {
|
||||
console.log('getLocalStream', error)
|
||||
console.log(error.message)
|
||||
})
|
||||
}
|
||||
|
||||
@ -20903,7 +20903,7 @@ const createSendTransport = () => {
|
||||
})
|
||||
|
||||
producerTransport.on('produce', async (parameters, callback, errback) => {
|
||||
console.log('produce', parameters)
|
||||
console.log(parameters)
|
||||
|
||||
try {
|
||||
// tell the server to create a Producer
|
||||
@ -20913,7 +20913,7 @@ const createSendTransport = () => {
|
||||
await socket.emit('transport-produce', {
|
||||
kind: parameters.kind,
|
||||
rtpParameters: parameters.rtpParameters,
|
||||
callId: callId
|
||||
appData: parameters.appData,
|
||||
}, ({ id }) => {
|
||||
// Tell the transport that parameters were transmitted and provide it with the
|
||||
// server side producer's id.
|
||||
@ -21009,6 +21009,7 @@ const createRecvTransport = async () => {
|
||||
}
|
||||
|
||||
const resetCallSettings = () => {
|
||||
socket.emit('transportclose', { callId })
|
||||
localVideo.srcObject = null
|
||||
remoteVideo.srcObject = null
|
||||
consumer = null
|
||||
@ -21056,12 +21057,12 @@ const connectRecvTransport = async () => {
|
||||
|
||||
const closeCall = () => {
|
||||
console.log('closeCall');
|
||||
|
||||
|
||||
// Emit 'notify-end' to Hub so the consumer will know to close the video
|
||||
const notifyEnd = {
|
||||
origin_asset_id: ASSET_ID,
|
||||
dest_asset_id: originAssetId || parseInt(urlParams.get('dest_asset_id')),
|
||||
type: 'notify-end',
|
||||
origin_asset_id: ASSET_ID,
|
||||
dest_asset_id: originAssetId || parseInt(urlParams.get('dest_asset_id')),
|
||||
type: 'notify-end',
|
||||
video_call_id: callId
|
||||
}
|
||||
console.log('notifyEnd', notifyEnd)
|
||||
@ -21071,7 +21072,7 @@ const closeCall = () => {
|
||||
const closeCallBtn = document.getElementById('btnCloseCall')
|
||||
closeCallBtn.setAttribute('disabled', '')
|
||||
|
||||
// Reset settings
|
||||
// Reset settings and send closeTransport to video server
|
||||
resetCallSettings()
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
hubAddress: 'https://hub.dev.linx.safemobile.com/',
|
||||
// mediasoupAddress: 'https://video.safemobile.org/mediasoup',
|
||||
mediasoupAddress: 'http://localhost:3000/mediasoup',
|
||||
mediasoupAddress: 'https://video.safemobile.org/mediasoup',
|
||||
// mediasoupAddress: 'http://localhost:3000/mediasoup',
|
||||
}
|
@ -149,7 +149,7 @@ const getLocalStream = () => {
|
||||
})
|
||||
.then(streamSuccess)
|
||||
.catch(error => {
|
||||
console.log('getLocalStream', error)
|
||||
console.log(error.message)
|
||||
})
|
||||
}
|
||||
|
||||
@ -244,7 +244,7 @@ const createSendTransport = () => {
|
||||
})
|
||||
|
||||
producerTransport.on('produce', async (parameters, callback, errback) => {
|
||||
console.log('produce', parameters)
|
||||
console.log(parameters)
|
||||
|
||||
try {
|
||||
// tell the server to create a Producer
|
||||
@ -254,7 +254,7 @@ const createSendTransport = () => {
|
||||
await socket.emit('transport-produce', {
|
||||
kind: parameters.kind,
|
||||
rtpParameters: parameters.rtpParameters,
|
||||
callId: callId
|
||||
appData: parameters.appData,
|
||||
}, ({ id }) => {
|
||||
// Tell the transport that parameters were transmitted and provide it with the
|
||||
// server side producer's id.
|
||||
|
Reference in New Issue
Block a user