Compare commits

..

81 Commits

Author SHA1 Message Date
ce1fe3ca8d Merge pull request 'Update 'app.js'' (#33) from sergiu-patch-1 into develop
Reviewed-on: #33
Reviewed-by: bmamihai <mihai.bozieru@safemobile.com>
2023-06-07 12:51:12 +00:00
96b770d9c5 Update 'app.js' 2023-06-07 12:50:24 +00:00
4b1f4fab70 Merge pull request 'LH-284: Update duplex documentation workflow diagrams' (#32) from LH-284-update-and-centralize-diagrams-for-video-calls into develop
Reviewed-on: #32
Reviewed-by: Adriana <adriana.epure@safemobile.com>
2023-05-30 11:27:09 +00:00
ba7c2186af LH-284: Update duplex documentation workflow diagrams 2023-05-30 11:57:02 +03:00
636e8a9fab Merge pull request 'added build commit no' (#31) from build-commit-no into develop
Reviewed-on: #31
Reviewed-by: Cristi Ene <cristi.ene@safemobile.com>
2023-04-10 23:36:34 +00:00
5da2eb6927 update build.sh after review 2023-04-05 01:57:58 +03:00
97948a5d8c added build commit no 2023-04-02 01:17:18 +03:00
b94b1bff86 Merge pull request 'docker' (#18) from docker into develop
Reviewed-on: #18
Reviewed-by: Cristi Ene <cristi.ene@safemobile.com>
2023-02-26 22:11:02 +00:00
5c4f2bf13e Merge branch 'develop' into docker 2023-02-26 22:10:34 +00:00
df7f1987f6 Merge pull request 'LH-276: Add close-producer event handler; Update client' (#30) from LH-276-close-producer into develop
Reviewed-on: #30
Reviewed-by: Cristi Ene <cristi.ene@safemobile.com>
2023-02-23 09:46:41 +00:00
7842953faf LH-276: Refactor consumer-resume 2023-02-22 18:57:57 +02:00
4591617b1e LH-276: Fix video consume when initiator is not set yet(at start) 2023-02-22 18:50:37 +02:00
9b3f2f94c8 LH-276: Fix audio when initiator is not set yet(at start) 2023-02-22 18:46:01 +02:00
29a4cd7227 LH-276: Format code 2023-02-22 18:40:39 +02:00
c80265fe25 LH-276: Format code 2023-02-22 18:36:50 +02:00
abb1533c9b LH-276: Format code 2023-02-22 18:32:41 +02:00
6e3ce9fbb3 LH-276: Format code 2023-02-22 18:30:28 +02:00
4d8adf9eac LH-276: Refactor consume(consumeAudio/consumeVideo); Format code 2023-02-22 18:21:53 +02:00
bde02fe250 LH-276: Add close-producer event handler; Update client 2023-02-21 02:41:48 +02:00
0ddb43b4b5 Merge pull request 'LINXD-2342-dispatcher-audio' (#29) from LINXD-2342-dispatcher-audio into develop
Reviewed-on: #29
Reviewed-by: Cristi Ene <cristi.ene@safemobile.com>
2023-02-15 09:14:57 +00:00
f02d6af726 LINXD-2342: Added documentation for kind 2023-02-15 11:14:02 +02:00
7ee93d7963 LINXD-2342: Update video client 2023-02-15 10:06:39 +02:00
98212a78cb Update client to consume audio correctly 2023-02-11 21:32:53 +02:00
33ea44ac02 Update client to consume audio correctly 2023-02-11 21:30:21 +02:00
6b822142d0 Add the kind of producer on new-producer event 2023-02-11 21:13:57 +02:00
50e95b93a4 Update client 2023-02-08 19:37:32 +02:00
6eca0808c9 Merge pull request 'Update web client to work with dev' (#27) from update-web-client into develop
Reviewed-on: #27
Reviewed-by: bmamihai <mihai.bozieru@safemobile.com>
2023-01-23 20:37:27 +00:00
f728b23b7f Update web client to work with dev 2023-01-23 20:45:08 +02:00
33c0234fb0 Merge pull request 'LINXD-2303: Added client-to-client workflow; Added client-mediadoup workflow' (#26) from LINXD-2303-video-documentation-mobile-mobile into develop
Reviewed-on: #26
Reviewed-by: Cristi Ene <cristi.ene@safemobile.com>
2023-01-19 14:44:24 +00:00
f81d5b2dfa LINXD-2303: Update diagrams Mediasoup connect & produce; Update diagram Client-Client workflow 2023-01-19 16:43:46 +02:00
ae87b5bd71 LINXD-2303: Added client-to-client workflow; Added client-mediadoup workflow 2023-01-19 16:31:16 +02:00
09e82fb224 Merge pull request 'Moved 'new-producer' in 'transport-produce' handler' (#25) from new-producer-in-transport-produce into develop
Reviewed-on: #25
Reviewed-by: Cristi Ene <cristi.ene@safemobile.com>
2023-01-19 06:54:53 +00:00
1da0170261 Moved 'new-producer' in 'transport-produce' handler 2023-01-18 11:49:02 +02:00
aca0a6eac0 improved build to get also the git log to know what is on server 2023-01-17 16:00:52 +00:00
23ca56c8c6 rever build.sh 2023-01-17 15:51:55 +00:00
a2d0b6771b Merge pull request 'Add new-producer event; Update client to consume when receives new-producer event' (#24) from LAPI-675-generate-new-producer-in-mediasoup-and-client into develop
Reviewed-on: #24
Reviewed-by: Cristi Ene <cristi.ene@safemobile.com>
2023-01-16 15:40:06 +00:00
9382fd11fb LAPI-675: Remove commented code 2023-01-16 17:39:15 +02:00
fb99fd57f8 LAPI-675: Refactor transport-connect new-producer 2023-01-16 17:32:31 +02:00
0f4342777a LAPI-675: Update bundle 2023-01-13 20:12:59 +02:00
69c167e4e9 Add new-producer event; Update client to consume when receives new-producer event 2023-01-13 20:11:24 +02:00
ad4c1f27e7 Merge pull request 'LAPI-674: Parse transport-recv-connect and consume param' (#23) from LAPI-674-change-events-for-consumer-in-mediasoup-server into develop
Reviewed-on: #23
Reviewed-by: Cristi Ene <cristi.ene@safemobile.com>
2023-01-13 07:52:27 +00:00
b2128c4754 fix build 2023-01-12 16:42:44 +00:00
af53dc1610 fix build 2023-01-12 16:30:39 +00:00
f48fe4f9aa LAPI-674: Parse transport-recv-connect and consume param 2023-01-12 16:23:30 +02:00
6fd6ca9755 Merge pull request 'comsume-undefined' (#22) from comsume-undefined into develop
Reviewed-on: #22
Reviewed-by: Cristi Ene <cristi.ene@safemobile.com>
2023-01-12 13:31:46 +00:00
1b72941a32 Moved callId in consumer-resume 2023-01-12 15:09:21 +02:00
c037240f47 Refactor consumer-resume 2023-01-12 14:32:48 +02:00
9716bddd71 Added log 2023-01-12 12:50:40 +02:00
e6c08a2c1d Refactor consumer-resume v3 2023-01-12 12:46:46 +02:00
1342e812e6 Refactor consumer-resume 2023-01-12 12:45:23 +02:00
d8405eccc7 Refactor consumer-resume 2023-01-12 12:44:11 +02:00
5bf31d452f Check for initiatorConsumerAudio before resume() 2023-01-12 12:38:40 +02:00
c034610471 Rollback 2023-01-12 12:38:01 +02:00
df5b3eab90 Check for initiatorConsumerAudio before resume() 2023-01-12 12:31:52 +02:00
b0293230b6 Merge pull request 'add-diagrams-doc' (#21) from add-diagrams-doc into develop
Reviewed-on: #21
Reviewed-by: Cristi Ene <cristi.ene@safemobile.com>
2023-01-10 17:51:05 +00:00
391e8eb6f0 Added diagram for close call before accept/reject 2023-01-10 13:17:57 +02:00
3139a625a2 Added workflow diagrams 2023-01-10 12:05:29 +02:00
48eee903a5 added command for limiting cpu/memory 2023-01-05 14:31:51 +02:00
d5bc0cd1d3 run under host network 2023-01-05 13:18:13 +02:00
1c353d7c88 Merge branch 'develop' into docker 2023-01-05 01:14:56 +02:00
9d43b7ec0c Merge pull request 'LINXD-2270: Allow server and web client to have full duplex' (#20) from LINXD-2270-full-duplex into develop
Reviewed-on: #20
Reviewed-by: Cristi Ene <cristi.ene@safemobile.com>
2022-12-27 11:12:38 +00:00
342d09c3e6 LINXD-2270: Removed console logs; Update commented code 2022-12-27 13:11:06 +02:00
b3409de3ba LINXD-2270: Allow server and web client to have full duplex 2022-12-19 19:31:33 +02:00
3e31ba21bd replace individual copy with copy all 2022-12-15 13:20:22 +02:00
cdf02756d3 fix start 2022-12-14 01:32:17 +02:00
b2f9f5affa remove pm2 and watchify 2022-12-14 00:48:14 +02:00
5b9bfeaa01 improve docker 2022-12-10 03:21:59 +02:00
e3bef9b3e5 New dockerfile 2022-12-10 02:38:09 +02:00
8a9c370f02 Merge pull request 'LH-265-audio' (#17) from LH-265-audio into develop
Reviewed-on: #17
Reviewed-by: Cristi Ene <cristi.ene@safemobile.com>
2022-12-06 12:45:08 +00:00
652019b07d LH-265: Update doc; Update bundle 2022-11-29 15:35:28 +02:00
09c4a4b90e LH-265: Added audio on client and server 2022-11-29 14:19:02 +02:00
75d0e3aee7 exe right for build.sh 2022-10-31 22:26:08 +00:00
30ac997634 Merge pull request 'added build.sh' (#14) from temp-build into develop
Reviewed-on: #14
2022-10-31 22:22:20 +00:00
5aea138f6a added build.sh 2022-10-31 12:17:07 +02:00
5b01ddc2a8 Merge pull request 'LH-259-mediasoup-always-return-a-callback-response-to-clients' (#13) from LH-259-mediasoup-always-return-a-callback-response-to-clients into develop
Reviewed-on: #13
2022-10-25 16:18:54 +00:00
084ff36ebe LH-259: Refactor createRoom 2022-10-24 22:38:06 +03:00
f4ebf92783 LH-259: Added callback from transport-produce 2022-10-24 22:35:22 +03:00
b59a157b18 LH-259: Comment callback from transport-produce 2022-10-24 22:19:37 +03:00
9f8347bec5 LH-259: Update createRoom callback 2022-10-24 22:16:47 +03:00
24390c98e5 LH-259: Added callbacks 2022-10-24 22:11:14 +03:00
1a7371fe18 Parse RTC_MIN_PORT and RTC_MAX_PORT 2022-10-18 18:27:02 +03:00
20 changed files with 1738 additions and 1427 deletions

2
.dockerignore Normal file
View File

@ -0,0 +1,2 @@
node_modules
doc

1
.gitignore vendored
View File

@ -1 +1,2 @@
/node_modules /node_modules
/dist

View File

@ -1,11 +1,25 @@
FROM ubuntu FROM ubuntu:22.04
WORKDIR /app
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y build-essential pip net-tools iputils-ping iproute2 curl apt-get install -y build-essential pip net-tools iputils-ping iproute2 curl
RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash - RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash -
RUN apt-get install -y nodejs RUN apt-get install -y nodejs
RUN npm install -g watchify
EXPOSE 3000 COPY . /app/
EXPOSE 2000-2020
EXPOSE 10000-10100 RUN npm install
EXPOSE 3000/tcp
EXPOSE 2000-2200/udp
CMD node app.js
#docker build -t linx-video .
# docker run -it -d --restart always -p 3000:3000/tcp -p 2000-2200:2000-2200/udp linx-video
#Run under host network
# docker run -it -d --network host --restart always -p 3000:3000/tcp -p 2000-2200:2000-2200/udp linx-video
#https://docs.docker.com/config/containers/resource_constraints/
#docker run -it -d --network host --cpus="0.25" --memory="512m" --restart always -p 3000:3000/tcp -p 2000-2200:2000-2200/udp linx-video

View File

@ -22,18 +22,20 @@
2. Run the `npm start:prod` command to start the server in production mode. 2. Run the `npm start:prod` command to start the server in production mode.
(To connect to the terminal, use `pm2 log video-server`) (To connect to the terminal, use `pm2 log video-server`)
---
### Web client
- The server will start by default on port 3000, and the ssl certificates will have to be configured - The server will start by default on port 3000, and the ssl certificates will have to be configured
- The web client can be accessed using the /sfu path - The web client can be accessed using the /sfu path
ex: http://localhost:3000/sfu/?assetId=1&&accountId=1&producer=true&assetName=Adi&assetType=linx ex: https://HOST/sfu/?assetId=1&&accountId=1&producer=true&dest_asset_id=75&assetName=Adi
assetId = asset id of the unit on which you are doing the test assetId = asset id of the unit on which you are doing the test
accountId = account id of the unit on which you are doing the test accountId = account id of the unit on which you are doing the test
producer = it will always be true because you are the producer 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 dest_asset_id= the addressee with whom the call is made
- To make a call using this client, you need a microphone and permission to use it
- For any changes related to the client, the command `npm run watch' will have to be used to generate the bundle.js used by the web client
### Demo project ### Demo project
The demo project used initially and then modified for our needs `https://github.com/jamalag/mediasoup2` The demo project used initially and then modified for our needs `https://github.com/jamalag/mediasoup2`

506
app.js
View File

@ -1,83 +1,84 @@
require('dotenv').config() require('dotenv').config();
const express = require('express'); const express = require('express');
const app = express(); const app = express();
const Server = require('socket.io'); const Server = require('socket.io');
const path = require('node:path'); const path = require('node:path');
const fs = require('node:fs'); const fs = require('node:fs');
const https = require('https'); let https;
try {
https = require('node:https');
} catch (err) {
console.log('https support is disabled!');
}
const mediasoup = require('mediasoup'); const mediasoup = require('mediasoup');
let worker let worker;
/** /**
* videoCalls
* |-> Router
* |-> Producer
* |-> Consumer
* |-> Producer Transport
* |-> Consumer Transport
* *
* videoCalls - Dictionary of Object(s)
* '<callId>': { * '<callId>': {
* router: Router, * router: Router,
* producer: Producer, * initiatorAudioProducer: Producer,
* producerTransport: Producer Transport, * initiatorVideoProducer: Producer,
* consumer: Consumer, * receiverVideoProducer: Producer,
* consumerTransport: Consumer Transport * receiverAudioProducer: Producer,
* initiatorProducerTransport: Producer Transport,
* receiverProducerTransport: Producer Transport,
* initiatorConsumerVideo: Consumer,
* initiatorConsumerAudio: Consumer,
* initiatorConsumerTransport: Consumer Transport
* initiatorSocket
* receiverSocket
* } * }
* *
**/ **/
let videoCalls = {} let videoCalls = {};
let socketDetails = {} let socketDetails = {};
app.get('/', (_req, res) => { app.get('/', (_req, res) => {
res.send('Hello from mediasoup app!') res.send('OK');
}) });
app.use('/sfu', express.static(path.join(__dirname, 'public'))) 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('./server/ssl/key.pem', 'utf-8'), key: fs.readFileSync(process.env.SERVER_KEY, 'utf-8'),
cert: fs.readFileSync('./server/ssl/cert.pem', '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, { const io = new Server(httpsServer, {
allowEIO3: true, allowEIO3: true,
origins: ["*:*"], 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('Video server listening on port:', process.env.PORT);
}); });
const peers = io.of('/'); const peers = io.of('/');
console.log('process.env.RTC_MIN_PORT', process.env.RTC_MIN_PORT);
console.log('process.env.RTC_MAX_PORT', process.env.RTC_MAX_PORT, process.env.RTC_MAX_PORT.length);
const createWorker = async () => { const createWorker = async () => {
try { try {
worker = await mediasoup.createWorker({ worker = await mediasoup.createWorker({
rtcMinPort: process.env.RTC_MIN_PORT, rtcMinPort: parseInt(process.env.RTC_MIN_PORT),
rtcMaxPort: process.env.RTC_MAX_PORT, rtcMaxPort: parseInt(process.env.RTC_MAX_PORT),
}) });
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; return worker;
} catch (error) { } catch (error) {
console.log(`ERROR | createWorker | ${error.message}`); console.error(`[createWorker] | ERROR | error: ${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();
@ -100,46 +101,70 @@ const mediaCodecs = [
parameters: { parameters: {
'x-google-start-bitrate': 1000, 'x-google-start-bitrate': 1000,
}, },
channels: 2,
},
{
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,
},
}, },
]; ];
const closeCall = (callId) => { const closeCall = (callId) => {
try { try {
if (callId && videoCalls[callId]) { if (callId && videoCalls[callId]) {
videoCalls[callId].producer?.close(); videoCalls[callId].receiverVideoProducer?.close();
videoCalls[callId].consumer?.close(); videoCalls[callId].receiverAudioProducer?.close();
videoCalls[callId]?.consumerTransport?.close(); videoCalls[callId].initiatorConsumerVideo?.close();
videoCalls[callId]?.producerTransport?.close(); videoCalls[callId].initiatorConsumerAudio?.close();
videoCalls[callId]?.initiatorConsumerTransport?.close();
videoCalls[callId]?.receiverProducerTransport?.close();
videoCalls[callId]?.router?.close(); videoCalls[callId]?.router?.close();
delete videoCalls[callId]; delete videoCalls[callId];
} else { console.log(`[closeCall] | callId: ${callId}`);
console.log(`The call with id ${callId} has already been deleted`);
} }
} catch (error) { } catch (error) {
console.log(`ERROR | closeCall | callid ${callId} | ${error.message}`); console.error(`[closeCall] | ERROR | callId: ${callId} | error: ${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 - Handlers for WS events
- These are created only when we have a connection with a peer - 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 // 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,
}); });
// It is triggered when the peer is disconnected // It is triggered when the peer is disconnected
@ -156,27 +181,36 @@ peers.on('connection', async socket => {
- If the room already exists, it will not create it, but will only return rtpCapabilities - 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) => {
let callbackResponse = null;
try { try {
// We can continue with the room creation process only if we have a callId
if (callId) { if (callId) {
console.log(`[createRoom] socket.id ${socket.id} callId ${callId}`); console.log(`[createRoom] socket.id ${socket.id} callId ${callId}`);
if (!videoCalls[callId]) { if (!videoCalls[callId]) {
console.log('[createRoom] callId', callId); videoCalls[callId] = { router: await worker.createRouter({ mediaCodecs }) };
videoCalls[callId] = { router: await worker.createRouter({ mediaCodecs }) } console.log(`[createRoom] Generate Router ID: ${videoCalls[callId].router.id}`);
console.log(`[createRoom] Router ID: ${videoCalls[callId].router.id}`); videoCalls[callId].receiverSocket = socket;
} else {
videoCalls[callId].initiatorSocket = socket;
} }
socketDetails[socket.id] = callId; socketDetails[socket.id] = callId;
getRtpCapabilities(callId, callback); // rtpCapabilities is set for callback
callbackResponse = {
rtpCapabilities: videoCalls[callId].router.rtpCapabilities,
};
} else { } else {
console.log(`[createRoom] missing callId ${callId}`); console.log(`[createRoom] missing callId: ${callId}`);
} }
} catch (error) { } catch (error) {
console.log(`ERROR | createRoom | callId ${callId} | ${error.message}`); console.error(`[createRoom] | ERROR | callId: ${callId} | error: ${error.message}`);
} finally {
callback(callbackResponse);
} }
}); });
/* /*
- Client emits a request to create server side Transport - Client emits a request to create server side Transport
- Depending on the sender, producerTransport or consumerTransport is created on that router - Depending on the sender, a producer or consumer is created is created on that router
- It will return parameters, these are required for the client to create the RecvTransport - It will return parameters, these are required for the client to create the RecvTransport
from the client. from the client.
- If the client is producer(sender: true) then it will use parameters for device.createSendTransport(params) - If the client is producer(sender: true) then it will use parameters for device.createSendTransport(params)
@ -185,22 +219,30 @@ peers.on('connection', async socket => {
socket.on('createWebRtcTransport', async ({ sender }, callback) => { socket.on('createWebRtcTransport', async ({ sender }, callback) => {
try { try {
const callId = socketDetails[socket.id]; const callId = socketDetails[socket.id];
console.log(`[createWebRtcTransport] sender ${sender} | callId ${callId}`); console.log(`[createWebRtcTransport] socket ${socket.id} | sender ${sender} | callId ${callId}`);
if (sender) { if (sender) {
if (!videoCalls[callId].producerTransport) { if (!videoCalls[callId].receiverProducerTransport && !isInitiator(callId, socket.id)) {
videoCalls[callId].producerTransport = await createWebRtcTransportLayer(callId, callback); videoCalls[callId].receiverProducerTransport = await createWebRtcTransportLayer(callId, callback);
} else if (!videoCalls[callId].initiatorProducerTransport && isInitiator(callId, socket.id)) {
videoCalls[callId].initiatorProducerTransport = await createWebRtcTransportLayer(callId, callback);
} else { } else {
console.log(`producerTransport has already been defined | callId ${callId}`); console.log(`producerTransport has already been defined | callId ${callId}`);
callback(null);
} }
} else if (!sender) { } else if (!sender) {
if (!videoCalls[callId].consumerTransport) { if (!videoCalls[callId].receiverConsumerTransport && !isInitiator(callId, socket.id)) {
videoCalls[callId].consumerTransport = await createWebRtcTransportLayer(callId, callback); videoCalls[callId].receiverConsumerTransport = await createWebRtcTransportLayer(callId, callback);
} else { } else if (!videoCalls[callId].initiatorConsumerTransport && isInitiator(callId, socket.id)) {
console.log(`consumerTransport has already been defined | callId ${callId}`); videoCalls[callId].initiatorConsumerTransport = await createWebRtcTransportLayer(callId, callback);
} }
} }
} catch (error) { } catch (error) {
console.log(`ERROR | createWebRtcTransport | callId ${socketDetails[socket.id]} | sender ${sender} | ${error.message}`); console.error(
`[createWebRtcTransport] | ERROR | callId: ${socketDetails[socket.id]} | sender: ${sender} | error: ${
error.message
}`
);
callback(error);
} }
}); });
@ -213,42 +255,106 @@ peers.on('connection', async socket => {
const callId = socketDetails[socket.id]; const callId = socketDetails[socket.id];
if (typeof dtlsParameters === 'string') dtlsParameters = JSON.parse(dtlsParameters); if (typeof dtlsParameters === 'string') dtlsParameters = JSON.parse(dtlsParameters);
console.log(`[transport-connect] socket.id ${socket.id} | callId ${callId}`); console.log(`[transport-connect] socket ${socket.id} | callId ${callId}`);
await videoCalls[callId].producerTransport.connect({ dtlsParameters });
isInitiator(callId, socket.id)
? await videoCalls[callId].initiatorProducerTransport.connect({ dtlsParameters })
: await videoCalls[callId].receiverProducerTransport.connect({ dtlsParameters });
} catch (error) { } catch (error) {
console.log(`ERROR | transport-connect | callId ${socketDetails[socket.id]} | ${error.message}`); console.error(`[transport-connect] | ERROR | callId: ${socketDetails[socket.id]} | error: ${error.message}`);
} }
}); });
/* /*
- The event sent by the client (PRODUCER) after successfully connecting to producerTransport - The event sent by the client (PRODUCER) after successfully connecting to receiverProducerTransport/initiatorProducerTransport
- For the router with the id callId, we make produce on producerTransport - For the router with the id callId, we make produce on receiverProducerTransport/initiatorProducerTransport
- Create the handler on producer at the 'transportclose' event - Create the handler on producer at the 'transportclose' event
*/ */
socket.on('transport-produce', async ({ kind, rtpParameters, appData }, callback) => { socket.on('transport-produce', async ({ kind, rtpParameters, appData }, callback) => {
try { try {
const callId = socketDetails[socket.id]; const callId = socketDetails[socket.id];
if (typeof rtpParameters === 'string') rtpParameters = JSON.parse(rtpParameters); if (typeof rtpParameters === 'string') rtpParameters = JSON.parse(rtpParameters);
console.log('[transport-produce] | socket.id', socket.id, '| callId', callId); console.log(`[transport-produce] callId: ${callId} | kind: ${kind} | socket: ${socket.id}`);
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', () => { if (kind === 'video') {
const callId = socketDetails[socket.id]; if (!isInitiator(callId, socket.id)) {
console.log('transport for this producer closed', callId) videoCalls[callId].receiverVideoProducer = await videoCalls[callId].receiverProducerTransport.produce({
closeCall(callId); kind,
}); rtpParameters,
});
// Send back to the client the Producer's id videoCalls[callId].receiverVideoProducer.on('transportclose', () => {
// callback({ console.log('transport for this producer closed', callId);
// id: videoCalls[callId].producer.id closeCall(callId);
// }); });
// Send back to the client the Producer's id
callback &&
callback({
id: videoCalls[callId].receiverVideoProducer.id,
});
} else {
videoCalls[callId].initiatorVideoProducer = await videoCalls[callId].initiatorProducerTransport.produce({
kind,
rtpParameters,
});
videoCalls[callId].initiatorVideoProducer.on('transportclose', () => {
console.log('transport for this producer closed', callId);
closeCall(callId);
});
callback &&
callback({
id: videoCalls[callId].initiatorVideoProducer.id,
});
}
} else if (kind === 'audio') {
if (!isInitiator(callId, socket.id)) {
videoCalls[callId].receiverAudioProducer = await videoCalls[callId].receiverProducerTransport.produce({
kind,
rtpParameters,
});
videoCalls[callId].receiverAudioProducer.on('transportclose', () => {
console.log('transport for this producer closed', callId);
closeCall(callId);
});
// Send back to the client the Producer's id
callback &&
callback({
id: videoCalls[callId].receiverAudioProducer.id,
});
} else {
videoCalls[callId].initiatorAudioProducer = await videoCalls[callId].initiatorProducerTransport.produce({
kind,
rtpParameters,
});
videoCalls[callId].initiatorAudioProducer.on('transportclose', () => {
console.log('transport for this producer closed', callId);
closeCall(callId);
});
// Send back to the client the Producer's id
callback &&
callback({
id: videoCalls[callId].initiatorAudioProducer.id,
});
}
}
const socketToEmit = isInitiator(callId, socket.id)
? videoCalls[callId].receiverSocket
: videoCalls[callId].initiatorSocket;
// callId - Id of the call
// kind - producer type: audio/video
socketToEmit?.emit('new-producer', { callId, kind });
} catch (error) { } catch (error) {
console.log(`ERROR | transport-produce | callId ${socketDetails[socket.id]} | ${error.message}`); console.error(`[transport-produce] | ERROR | callId: ${socketDetails[socket.id]} | error: ${error.message}`);
} }
}); });
@ -259,12 +365,18 @@ peers.on('connection', async socket => {
socket.on('transport-recv-connect', async ({ dtlsParameters }) => { socket.on('transport-recv-connect', async ({ dtlsParameters }) => {
try { try {
const callId = socketDetails[socket.id]; const callId = socketDetails[socket.id];
console.log(`[transport-recv-connect] socket.id ${socket.id} | callId ${callId}`); console.log(`[transport-recv-connect] socket ${socket.id} | callId ${callId}`);
await videoCalls[callId].consumerTransport.connect({ dtlsParameters }); if (typeof dtlsParameters === 'string') dtlsParameters = JSON.parse(dtlsParameters);
// await videoCalls[callId].consumerTransport.connect({ dtlsParameters });
if (!isInitiator(callId, socket.id)) {
await videoCalls[callId].receiverConsumerTransport.connect({ dtlsParameters });
} else if (isInitiator(callId, socket.id)) {
await videoCalls[callId].initiatorConsumerTransport.connect({ dtlsParameters });
}
} catch (error) { } catch (error) {
console.log(`ERROR | transport-recv-connect | callId ${socketDetails[socket.id]} | ${error.message}`); console.error(`[transport-recv-connect] | ERROR | callId: ${socketDetails[socket.id]} | error: ${error.message}`);
} }
}) });
/* /*
- The customer consumes after successfully connecting to consumerTransport - The customer consumes after successfully connecting to consumerTransport
@ -274,71 +386,157 @@ peers.on('connection', async socket => {
- The consumer does consumerTransport.consume(params) - The consumer does consumerTransport.consume(params)
*/ */
socket.on('consume', async ({ rtpCapabilities }, callback) => { socket.on('consume', async ({ rtpCapabilities }, callback) => {
try { const callId = socketDetails[socket.id];
const callId = socketDetails[socket.id]; const socketId = socket.id;
console.log('[consume] callId', callId);
// Check if the router can consume the specified producer console.log(`[consume] socket ${socketId} | callId: ${callId}`);
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 if (typeof rtpCapabilities === 'string') rtpCapabilities = JSON.parse(rtpCapabilities);
videoCalls[callId].consumer.on('transportclose', () => {
const callId = socketDetails[socket.id];
console.log('transport close from consumer', callId);
closeCall(callId);
});
// https://mediasoup.org/documentation/v3/mediasoup/api/#consumer-on-producerclose callback({
videoCalls[callId].consumer.on('producerclose', () => { videoParams: await consumeVideo({ callId, socketId, rtpCapabilities }),
const callId = socketDetails[socket.id]; audioParams: await consumeAudio({ callId, socketId, rtpCapabilities }),
console.log('producer of consumer closed', callId); });
closeCall(callId);
});
// 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 - 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 - When consuming on consumerTransport, it is initially done with paused: true, here we will resume
- For the initiator we resume the initiatorConsumerAUDIO/VIDEO and for receiver the receiverConsumerAUDIO/VIDEO
*/ */
socket.on('consumer-resume', async () => { socket.on('consumer-resume', () => {
try { try {
const callId = socketDetails[socket.id]; const callId = socketDetails[socket.id];
console.log(`[consumer-resume] callId ${callId}`) const isInitiatorValue = isInitiator(callId, socket.id);
await videoCalls[callId].consumer.resume(); console.log(`[consumer-resume] callId: ${callId} | isInitiator: ${isInitiatorValue}`);
const consumerVideo = isInitiatorValue
? videoCalls[callId].initiatorConsumerVideo
: videoCalls[callId].receiverConsumerVideo;
const consumerAudio = isInitiatorValue
? videoCalls[callId].initiatorConsumerAudio
: videoCalls[callId].receiverConsumerAudio;
consumerVideo?.resume();
consumerAudio?.resume();
} catch (error) { } catch (error) {
console.log(`ERROR | consumer-resume | callId ${socketDetails[socket.id]} | ${error.message}`); console.error(
`[consumer-resume] | ERROR | callId: ${socketDetails[socket.id]} | isInitiator: ${isInitiator} | error: ${
error.message
}`
);
}
});
socket.on('close-producer', ({ callId, kind }) => {
try {
if (isInitiator(callId, socket.id)) {
console.log(`[close-producer] initiator --EMIT--> receiver | callId: ${callId} | kind: ${kind}`);
videoCalls[callId].receiverSocket.emit('close-producer', { callId, kind });
} else {
console.log(`[close-producer] receiver --EMIT--> initiator | callId: ${callId} | kind: ${kind}`);
videoCalls[callId].initiatorSocket.emit('close-producer', { callId, kind });
}
} catch (error) {
console.error(`[close-producer] | ERROR | callId: ${socketDetails[socket.id]} | error: ${error.message}`);
} }
}); });
}); });
const canConsume = ({ callId, producerId, rtpCapabilities }) => {
return !!videoCalls[callId].router.canConsume({
producerId,
rtpCapabilities,
});
};
const consumeVideo = async ({ callId, socketId, rtpCapabilities }) => {
// Handlers for consumer transport https://mediasoup.org/documentation/v3/mediasoup/api/#consumer-on-transportclose
if (isInitiator(callId, socketId) && videoCalls[callId].receiverVideoProducer) {
const producerId = videoCalls[callId].receiverVideoProducer.id;
if (!canConsume({ callId, producerId, rtpCapabilities })) return null;
videoCalls[callId].initiatorConsumerVideo = await videoCalls[callId].initiatorConsumerTransport.consume({
producerId,
rtpCapabilities,
paused: true,
});
return {
id: videoCalls[callId].initiatorConsumerVideo.id,
producerId,
kind: 'video',
rtpParameters: videoCalls[callId].initiatorConsumerVideo.rtpParameters,
};
} else if (videoCalls[callId].initiatorVideoProducer) {
const producerId = videoCalls[callId].initiatorVideoProducer.id;
if (!canConsume({ callId, producerId, rtpCapabilities })) return null;
videoCalls[callId].receiverConsumerVideo = await videoCalls[callId].receiverConsumerTransport.consume({
producerId,
rtpCapabilities,
paused: true,
});
return {
id: videoCalls[callId].receiverConsumerVideo.id,
producerId,
kind: 'video',
rtpParameters: videoCalls[callId].receiverConsumerVideo.rtpParameters,
};
} else {
return null;
}
};
const consumeAudio = async ({ callId, socketId, rtpCapabilities }) => {
try {
// Handlers for consumer transport https://mediasoup.org/documentation/v3/mediasoup/api/#consumer-on-transportclose
if (isInitiator(callId, socketId) && videoCalls[callId].receiverAudioProducer) {
const producerId = videoCalls[callId].receiverAudioProducer.id;
if (!canConsume({ callId, producerId, rtpCapabilities })) return null;
videoCalls[callId].initiatorConsumerAudio = await videoCalls[callId].initiatorConsumerTransport.consume({
producerId,
rtpCapabilities,
paused: true,
});
return {
id: videoCalls[callId].initiatorConsumerAudio.id,
producerId,
kind: 'audio',
rtpParameters: videoCalls[callId].initiatorConsumerAudio.rtpParameters,
};
} else if (videoCalls[callId].initiatorAudioProducer) {
const producerId = videoCalls[callId].initiatorAudioProducer.id;
if (!canConsume({ callId, producerId, rtpCapabilities })) return null;
videoCalls[callId].receiverConsumerAudio = await videoCalls[callId].receiverConsumerTransport.consume({
producerId,
rtpCapabilities,
paused: true,
});
return {
id: videoCalls[callId].receiverConsumerAudio.id,
producerId,
kind: 'audio',
rtpParameters: videoCalls[callId].receiverConsumerAudio.rtpParameters,
};
} else {
return null;
}
} catch (error) {
console.error(`[consumeAudio] | ERROR | error: ${error}`);
}
};
const isInitiator = (callId, socketId) => {
return videoCalls[callId]?.initiatorSocket?.id === socketId;
};
/* /*
- Called from at event 'createWebRtcTransport' and assigned to the consumer or producer transport - 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 - It will return parameters, these are required for the client to create the RecvTransport
@ -348,14 +546,14 @@ peers.on('connection', async socket => {
*/ */
const createWebRtcTransportLayer = async (callId, callback) => { const createWebRtcTransportLayer = async (callId, callback) => {
try { try {
console.log('[createWebRtcTransportLayer] callId', callId); console.log(`[createWebRtcTransportLayer] callId: ${callId}`);
// https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcTransportOptions // https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcTransportOptions
const webRtcTransport_options = { const webRtcTransport_options = {
listenIps: [ listenIps: [
{ {
ip: process.env.IP, // Listening IPv4 or IPv6. ip: process.env.IP, // Listening IPv4 or IPv6.
announcedIp: process.env.ANNOUNCED_IP, // Announced IPv4 or IPv6 (useful when running mediasoup behind NAT with private IP). announcedIp: process.env.ANNOUNCED_IP, // Announced IPv4 or IPv6 (useful when running mediasoup behind NAT with private IP).
} },
], ],
enableUdp: true, enableUdp: true,
enableTcp: true, enableTcp: true,
@ -363,11 +561,10 @@ const createWebRtcTransportLayer = async (callId, callback) => {
}; };
// 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 videoCalls[callId].router.createWebRtcTransport(webRtcTransport_options);
console.log(`callId: ${callId} | transport id: ${transport.id}`)
// Handler for when DTLS(Datagram Transport Layer Security) changes // 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}`); console.log(`transport | dtlsstatechange | calldId ${callId} | dtlsState ${dtlsState}`);
if (dtlsState === 'closed') { if (dtlsState === 'closed') {
transport.close(); transport.close();
@ -391,9 +588,10 @@ const createWebRtcTransportLayer = async (callId, callback) => {
// Set transport to producerTransport or consumerTransport // Set transport to producerTransport or consumerTransport
return transport; return transport;
} catch (error) { } catch (error) {
console.log(`ERROR | createWebRtcTransportLayer | callId ${socketDetails[socket.id]} | ${error.message}`); console.error(
`[createWebRtcTransportLayer] | ERROR | callId: ${socketDetails[socket.id]} | error: ${error.message}`
);
callback({ params: { error } }); callback({ params: { error } });
} }
} };

75
build.sh Executable file
View File

@ -0,0 +1,75 @@
#/!bin/bash
## FUNCTIONS
function getGitVersion(){
version=$(git describe)
count=$(echo ${version%%-*} | grep -o "\." | wc -l)
if (( $count > 1 )); then
version=${version%%-*}
elif (( $count == 0 ));then
echo -e "Error: Git version \"${version%%-*}\" not respecting Safemobile standard.\n Must be like 4.xx or 4.xx.xx"
version="0.0.0"
else
if [[ "$1" == "dev" ]];then
cleanprefix=${version#*-} # remove everything before `-` including `-`
cleansuffix=${cleanprefix%-*} # remove everything after `-` including `-`
version="${version%%-*}.${cleansuffix}"
else
version="${version%%-*}.0" # one `%` remove everything after last `-`, two `%%` remove everything after all `-`
fi
fi
}
function addVersionPm2(){
file_pkg="package.json"
key=" \"version\": \""
if [ -f "$file_pkg" ] && [ ! -z "$version" ]; then
versionApp=" \"version\": \"$version\","
sed -i "s|^.*$key.*|${versionApp//\//\\/}|g" $file_pkg
text=$(cat $file_pkg | grep -c "$version")
if [ $text -eq 0 ]; then
echo "Version couldn't be set"
else
echo "Version $version successfully applied to App"
fi
fi
}
## PREBUILD PROCESS
# check dist dir to be present and empty
if [ ! -d "dist" ]; then
## MAKE DIR
mkdir "dist"
echo "Directory dist created."
else
## CLEANUP
rm -fr dist/*
fi
if [ -d "node_modules" ]; then
rm -fr node_modules
fi
# Install dependencies
#npm install
## PROJECT NEEDS
echo "Building app... from $(git rev-parse --abbrev-ref HEAD)"
#npm run-script build
cp -r {.env,app.js,package.json,server,public,doc,Dockerfile} dist/
#cp -r ./* dist/
# Generate Git log
dateString=$(date +"%Y%m%d-%H%M%S")
git log --pretty=format:"%ad%x09%an%x09%s" --no-merges -20 > "dist/git-$dateString.log"
# Get Git version control
getGitVersion $1
# Add version control for pm2
cd dist
addVersionPm2
## POST BUILD
cd -

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 571 KiB

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,4 @@
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://testing.video.safemobile.org/',
// mediasoupAddress: 'http://localhost:3000/mediasoup',
} }

View File

@ -34,6 +34,9 @@
<body> <body>
<body> <body>
<div id="video"> <div id="video">
<legend>Client options:</legend>
<input type="checkbox" id="produceAudio" name="produceAudio">
<label for="produceAudio">Produce audio</label><br>
<table> <table>
<thead> <thead>
<th>Local Video</th> <th>Local Video</th>
@ -43,12 +46,24 @@
<tr> <tr>
<td> <td>
<div id="sharedBtns"> <div id="sharedBtns">
<video id="localVideo" autoplay class="video" ></video> <video
id="localVideo"
class="video"
autoplay
muted
playsinline
></video>
</div> </div>
</td> </td>
<td> <td>
<div id="sharedBtns"> <div id="sharedBtns">
<video id="remoteVideo" autoplay class="video" ></video> <video
id="remoteVideo"
class="video"
autoplay
muted
playsinline
></video>
</div> </div>
</td> </td>
</tr> </tr>
@ -60,34 +75,11 @@
</td> </td>
<td> <td>
<div id="sharedBtns"> <div id="sharedBtns">
<button id="btnRecvSendTransport">Consume</button> <!-- <button id="btnRecvSendTransport">Consume</button> -->
<button id="remoteSoundControl">Unmute</button>
</div> </div>
</td> </td>
</tr> </tr>
<!-- <tr>
<td colspan="2">
<div id="sharedBtns">
<button id="btnRtpCapabilities">2. Get Rtp Capabilities</button>
<br />
<button id="btnDevice">3. Create Device</button>
</div>
</td>
</tr>
<tr>
<td>
<div id="sharedBtns">
<button id="btnCreateSendTransport">4. Create Send Transport</button>
<br />
<button id="btnConnectSendTransport">5. Connect Send Transport & Produce</button></td>
</div>
<td>
<div id="sharedBtns">
<button id="btnRecvSendTransport">6. Create Recv Transport</button>
<br />
<button id="btnConnectRecvTransport">7. Connect Recv Transport & Consume</button>
</div>
</td>
</tr> -->
</tbody> </tbody>
</table> </table>
<div id="closeCallBtn"> <div id="closeCallBtn">

View File

@ -10,147 +10,201 @@ const ASSET_NAME = urlParams.get('assetName') || null;
const ASSET_TYPE = urlParams.get('assetType') || null; const ASSET_TYPE = urlParams.get('assetType') || null;
let callId = parseInt(urlParams.get('callId')) || null; let callId = parseInt(urlParams.get('callId')) || null;
const IS_PRODUCER = urlParams.get('producer') === 'true' ? true : false const IS_PRODUCER = urlParams.get('producer') === 'true' ? true : false
let remoteVideo = document.getElementById('remoteVideo')
remoteVideo.defaultMuted = true
let produceAudio = false
console.log('[URL] ASSET_ID', ASSET_ID, '| ACCOUNT_ID', ACCOUNT_ID, '| callId', callId, ' | IS_PRODUCER', IS_PRODUCER) console.log('[URL] ASSET_ID', ASSET_ID, '| ACCOUNT_ID', ACCOUNT_ID, '| callId', callId, ' | IS_PRODUCER', IS_PRODUCER)
let socket console.log('🟩 config', config)
hub = io(config.hubAddress)
const connectToMediasoup = () => { produceAudioSelector = document.getElementById('produceAudio');
produceAudioSelector.addEventListener('change', e => {
socket = io(config.mediasoupAddress, { if(e.target.checked) {
reconnection: true, produceAudio = true
reconnectionDelay: 1000, console.log('produce audio');
reconnectionDelayMax : 5000, } else {
reconnectionAttempts: Infinity produceAudio = false
})
socket.on('connection-success', ({ _socketId, existsProducer }) => {
console.log(`[MEDIA] ${config.mediasoupAddress} | connected: ${socket.connected} | existsProducer: ${existsProducer}`)
if (!IS_PRODUCER && existsProducer && consumer === undefined) {
goConnect()
// document.getElementById('btnRecvSendTransport').click();
} }
if (IS_PRODUCER && urlParams.get('testing') === 'true') { getLocalStream() } });
})
}
if (IS_PRODUCER === true) {
hub.on('connect', async () => {
console.log(`[HUB] ${config.hubAddress} | connected: ${hub.connected}`)
connectToMediasoup()
hub.emit(
'ars',
JSON.stringify({
ars: true,
asset_id: ASSET_ID,
account_id: ACCOUNT_ID,
})
)
hub.on('video', (data) => {
const parsedData = JSON.parse(data);
if (parsedData.type === 'notify-request') {
console.log('video', parsedData)
originAssetId = parsedData.origin_asset_id;
// originAssetName = parsedData.origin_asset_name;
// originAssetTypeName = parsedData.origin_asset_type_name;
callId = parsedData.video_call_id;
console.log('[VIDEO] notify-request | IS_PRODUCER', IS_PRODUCER, 'callId', callId);
getLocalStream()
}
if (parsedData.type === 'notify-end') {
console.log('[VIDEO] notify-end | IS_PRODUCER', IS_PRODUCER, 'callId', callId);
resetCallSettings()
}
})
})
hub.on('connect_error', (error) => {
console.log('connect_error', error);
});
hub.on('connection', () => {
console.log('connection')
})
hub.on('disconnect', () => {
console.log('disconnect')
})
} else {
connectToMediasoup()
}
let socket, hub
let device let device
let rtpCapabilities let rtpCapabilities
let producerTransport let producerTransport
let consumerTransport let consumerTransport
let producer let producerVideo
let producerAudio
let consumer let consumer
let originAssetId let originAssetId
// let originAssetName = 'Adi' let consumerVideo // local consumer video(consumer not transport)
// let originAssetTypeName = 'linx' let consumerAudio // local consumer audio(consumer not transport)
const remoteSoundControl = document.getElementById('remoteSoundControl');
remoteSoundControl.addEventListener('click', function handleClick() {
console.log('remoteSoundControl.textContent', remoteSoundControl.textContent);
if (remoteSoundControl.textContent === 'Unmute') {
remoteVideo.muted = false
remoteSoundControl.textContent = 'Mute';
} else {
remoteVideo.muted = true
remoteSoundControl.textContent = 'Unmute';
}
});
// https://mediasoup.org/documentation/v3/mediasoup-client/api/#ProducerOptions // https://mediasoup.org/documentation/v3/mediasoup-client/api/#ProducerOptions
// https://mediasoup.org/documentation/v3/mediasoup-client/api/#transport-produce // https://mediasoup.org/documentation/v3/mediasoup-client/api/#transport-produce
let params = { let videoParams = {
// mediasoup params
encodings: [ encodings: [
{ { scaleResolutionDownBy: 4, maxBitrate: 500000 },
rid: 'r0', { scaleResolutionDownBy: 2, maxBitrate: 1000000 },
maxBitrate: 100000, { scaleResolutionDownBy: 1, maxBitrate: 5000000 },
scalabilityMode: 'S1T3', { scalabilityMode: 'S3T3_KEY' }
},
{
rid: 'r1',
maxBitrate: 300000,
scalabilityMode: 'S1T3',
},
{
rid: 'r2',
maxBitrate: 900000,
scalabilityMode: 'S1T3',
},
], ],
// https://mediasoup.org/documentation/v3/mediasoup-client/api/#ProducerCodecOptions
codecOptions: { codecOptions: {
videoGoogleStartBitrate: 1000 videoGoogleStartBitrate: 1000
} }
} }
const streamSuccess = (stream) => { let audioParams = {
console.log('[streamSuccess]'); codecOptions :
localVideo.srcObject = stream {
const track = stream.getVideoTracks()[0] opusStereo : true,
params = { opusDtx : true
track,
...params
} }
}
setTimeout(() => {
hub = io(config.hubAddress)
const connectToMediasoup = () => {
socket = io(config.mediasoupAddress, {
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax : 5000,
reconnectionAttempts: Infinity
})
socket.on('connection-success', ({ _socketId, existsProducer }) => {
console.log(`[MEDIA] ${config.mediasoupAddress} | connected: ${socket.connected} | existsProducer: ${existsProducer}`)
if (!IS_PRODUCER && existsProducer && consumer === undefined) {
goConnect()
}
if (IS_PRODUCER && urlParams.get('testing') === 'true') { getLocalStream() }
})
socket.on('new-producer', ({ callId, kind }) => {
console.log(`🟢 new-producer | callId: ${callId} | kind: ${kind} | Ready to consume`);
connectRecvTransport();
})
socket.on('close-producer', ({ callId, kind }) => {
console.log(`🔴 close-producer | callId: ${callId} | kind: ${kind}`);
if (kind === 'video') {
consumerVideo.close()
remoteVideo.srcObject = null
}
else if (kind === 'audio') consumerAudio.close()
})
}
if (IS_PRODUCER === true) {
hub.on('connect', async () => {
console.log(`[HUB]! ${config.hubAddress} | connected: ${hub.connected}`)
connectToMediasoup()
hub.emit(
'ars',
JSON.stringify({
ars: true,
asset_id: ASSET_ID,
account_id: ACCOUNT_ID,
})
)
hub.on('video', (data) => {
const parsedData = JSON.parse(data);
if (parsedData.type === 'notify-request') {
console.log('video', parsedData)
originAssetId = parsedData.origin_asset_id;
// originAssetName = parsedData.origin_asset_name;
// originAssetTypeName = parsedData.origin_asset_type_name;
callId = parsedData.video_call_id;
console.log('[VIDEO] notify-request | IS_PRODUCER', IS_PRODUCER, 'callId', callId);
getLocalStream()
}
if (parsedData.type === 'notify-end') {
console.log('[VIDEO] notify-end | IS_PRODUCER', IS_PRODUCER, 'callId', callId);
resetCallSettings()
}
})
})
hub.on('connect_error', (error) => {
console.log('connect_error', error);
});
hub.on('connection', () => {
console.log('connection')
})
hub.on('disconnect', () => {
console.log('disconnect')
})
} else {
connectToMediasoup()
}
}, 1600);
const streamSuccess = (stream) => {
console.log('[streamSuccess] device', device);
localVideo.srcObject = stream
console.log('stream', stream);
const videoTrack = stream.getVideoTracks()[0]
const audioTrack = stream.getAudioTracks()[0]
videoParams = {
track: videoTrack,
...videoParams
}
audioParams = {
track: audioTrack,
...audioParams
}
console.log('[streamSuccess] videoParams', videoParams, ' | audioParams', audioParams);
goConnect() goConnect()
} }
const getLocalStream = () => { const getLocalStream = () => {
console.log('[getLocalStream]'); console.log('[getLocalStream]');
navigator.mediaDevices.getUserMedia({ navigator.mediaDevices.getUserMedia({
audio: false, audio: produceAudio ? true : false,
video: { video: {
width: { qvga : { width: { ideal: 320 }, height: { ideal: 240 } },
min: 640, vga : { width: { ideal: 640 }, height: { ideal: 480 } },
max: 1920, hd : { width: { ideal: 1280 }, height: { ideal: 720 } }
},
height: {
min: 400,
max: 1080,
}
} }
}) })
.then(streamSuccess) .then(streamSuccess)
.catch(error => { .catch(error => {
console.log(error.message) console.log(error.message)
}) })
navigator.permissions.query(
{ name: 'microphone' }
).then((permissionStatus) =>{
console.log('🟨 [PERMISSION] permissionStatus', permissionStatus); // granted, denied, prompt
// It will block the code from execution and display "Permission denied" if we don't have microphone permissions
})
} }
const goConnect = () => { const goConnect = () => {
@ -167,7 +221,6 @@ const goCreateTransport = () => {
// server side to send/recive media // server side to send/recive media
const createDevice = async () => { const createDevice = async () => {
try { try {
console.log('[createDevice]');
device = new mediasoupClient.Device() device = new mediasoupClient.Device()
// https://mediasoup.org/documentation/v3/mediasoup-client/api/#device-load // https://mediasoup.org/documentation/v3/mediasoup-client/api/#device-load
@ -178,6 +231,7 @@ const createDevice = async () => {
}) })
console.log('Device RTP Capabilities', device.rtpCapabilities) console.log('Device RTP Capabilities', device.rtpCapabilities)
console.log('[createDevice] device', device);
// once the device loads, create transport // once the device loads, create transport
goCreateTransport() goCreateTransport()
@ -207,18 +261,20 @@ const getRtpCapabilities = () => {
} }
const createSendTransport = () => { const createSendTransport = () => {
console.log('[createSendTransport');
// see server's socket.on('createWebRtcTransport', sender?, ...) // see server's socket.on('createWebRtcTransport', sender?, ...)
// this is a call from Producer, so sender = true // this is a call from Producer, so sender = true
socket.emit('createWebRtcTransport', { sender: true, callId }, ({ params }) => { socket.emit('createWebRtcTransport', { sender: true }, (value) => {
console.log(`[createWebRtcTransport] value: ${JSON.stringify(value)}`);
const params = value.params;
// The server sends back params needed // The server sends back params needed
// to create Send Transport on the client side // to create Send Transport on the client side
if (params.error) { if (params.error) {
console.log(params.error) console.log(params.error)
return return
} }
console.log(params)
// creates a new WebRTC Transport to send media // creates a new WebRTC Transport to send media
// based on the server's producer transport params // based on the server's producer transport params
// https://mediasoup.org/documentation/v3/mediasoup-client/api/#TransportOptions // https://mediasoup.org/documentation/v3/mediasoup-client/api/#TransportOptions
@ -244,10 +300,10 @@ const createSendTransport = () => {
}) })
producerTransport.on('produce', async (parameters, callback, errback) => { producerTransport.on('produce', async (parameters, callback, errback) => {
console.log(parameters) console.log('[produce] parameters', parameters)
try { try {
// tell the server to create a Producer // Tell the server to create a Producer
// with the following parameters and produce // with the following parameters and produce
// and expect back a server side producer id // and expect back a server side producer id
// see server's socket.on('transport-produce', ...) // see server's socket.on('transport-produce', ...)
@ -270,22 +326,46 @@ const createSendTransport = () => {
} }
const connectSendTransport = async () => { const connectSendTransport = async () => {
// we now call produce() to instruct the producer transport
console.log('[connectSendTransport] producerTransport');
// We now call produce() to instruct the producer transport
// to send media to the Router // to send media to the Router
// https://mediasoup.org/documentation/v3/mediasoup-client/api/#transport-produce // https://mediasoup.org/documentation/v3/mediasoup-client/api/#transport-produce
// this action will trigger the 'connect' and 'produce' events above // this action will trigger the 'connect' and 'produce' events above
producer = await producerTransport.produce(params)
producer.on('trackended', () => { // Produce video
let producerVideoHandler = await producerTransport.produce(videoParams)
console.log('videoParams', videoParams);
console.log('producerVideo', producerVideo);
producerVideoHandler.on('trackended', () => {
console.log('track ended') console.log('track ended')
// close video track // close video track
}) })
producer.on('transportclose', () => { producerVideoHandler.on('transportclose', () => {
console.log('transport ended') console.log('transport ended')
// close video track // close video track
}) })
// Produce audio
if (produceAudio) {
let producerAudioHandler = await producerTransport.produce(audioParams)
console.log('audioParams', audioParams);
console.log('producerAudio', producerAudio);
producerAudioHandler.on('trackended', () => {
console.log('track ended')
// close audio track
})
producerAudioHandler.on('transportclose', () => {
console.log('transport ended')
// close audio track
})
}
const answer = { const answer = {
origin_asset_id: ASSET_ID, origin_asset_id: ASSET_ID,
dest_asset_id: originAssetId || parseInt(urlParams.get('dest_asset_id')), dest_asset_id: originAssetId || parseInt(urlParams.get('dest_asset_id')),
@ -294,7 +374,7 @@ const connectSendTransport = async () => {
origin_asset_type_name: ASSET_TYPE, origin_asset_type_name: ASSET_TYPE,
origin_asset_name: ASSET_NAME, origin_asset_name: ASSET_NAME,
video_call_id: callId, video_call_id: callId,
answer: 'accepted', // answer: 'rejected' answer: 'accepted', // answer: accepted/rejected
}; };
console.log('SEND answer', answer); console.log('SEND answer', answer);
@ -306,11 +386,13 @@ const connectSendTransport = async () => {
// Enable Close call button // Enable Close call button
const closeCallBtn = document.getElementById('btnCloseCall'); const closeCallBtn = document.getElementById('btnCloseCall');
closeCallBtn.removeAttribute('disabled'); closeCallBtn.removeAttribute('disabled');
createRecvTransport();
} }
const createRecvTransport = async () => { const createRecvTransport = async () => {
console.log('createRecvTransport'); console.log('createRecvTransport');
// see server's socket.on('consume', sender?, ...) // See server's socket.on('consume', sender?, ...)
// this is a call from Consumer, so sender = false // this is a call from Consumer, so sender = false
await socket.emit('createWebRtcTransport', { sender: false, callId }, ({ params }) => { await socket.emit('createWebRtcTransport', { sender: false, callId }, ({ params }) => {
// The server sends back params needed // The server sends back params needed
@ -320,15 +402,15 @@ const createRecvTransport = async () => {
return return
} }
console.log(params) console.log('[createRecvTransport] params', params)
// creates a new WebRTC Transport to receive media // Creates a new WebRTC Transport to receive media
// based on server's consumer transport params // based on server's consumer transport params
// https://mediasoup.org/documentation/v3/mediasoup-client/api/#device-createRecvTransport // https://mediasoup.org/documentation/v3/mediasoup-client/api/#device-createRecvTransport
consumerTransport = device.createRecvTransport(params) consumerTransport = device.createRecvTransport(params)
// https://mediasoup.org/documentation/v3/communication-between-client-and-server/#producing-media // https://mediasoup.org/documentation/v3/communication-between-client-and-server/#producing-media
// this event is raised when a first call to transport.produce() is made // This event is raised when a first call to transport.produce() is made
// see connectRecvTransport() below // see connectRecvTransport() below
consumerTransport.on('connect', async ({ dtlsParameters }, callback, errback) => { consumerTransport.on('connect', async ({ dtlsParameters }, callback, errback) => {
try { try {
@ -345,7 +427,8 @@ const createRecvTransport = async () => {
errback(error) errback(error)
} }
}) })
connectRecvTransport() // We call it in new-rpoducer, we don't need it here anymore
// connectRecvTransport()
}) })
} }
@ -353,7 +436,8 @@ const resetCallSettings = () => {
localVideo.srcObject = null localVideo.srcObject = null
remoteVideo.srcObject = null remoteVideo.srcObject = null
consumer = null consumer = null
producer = null producerVideo = null
producerAudio = null
producerTransport = null producerTransport = null
consumerTransport = null consumerTransport = null
device = undefined device = undefined
@ -361,40 +445,97 @@ const resetCallSettings = () => {
const connectRecvTransport = async () => { const connectRecvTransport = async () => {
console.log('connectRecvTransport'); console.log('connectRecvTransport');
// for consumer, we need to tell the server first // For consumer, we need to tell the server first
// to create a consumer based on the rtpCapabilities and consume // to create a consumer based on the rtpCapabilities and consume
// if the router can consume, it will send back a set of params as below // if the router can consume, it will send back a set of params as below
await socket.emit('consume', { await socket.emit('consume', {
rtpCapabilities: device.rtpCapabilities, rtpCapabilities: device.rtpCapabilities,
callId callId
}, async ({ params }) => { }, async ({videoParams, audioParams}) => {
if (params.error) { console.log(`[consume] 🟩 videoParams`, videoParams)
console.log('Cannot Consume') console.log(`[consume] 🟩 audioParams`, audioParams)
return console.log('[consume] 🟩 consumerTransport', consumerTransport)
}
// then consume with the local consumer transport
// which creates a consumer
consumer = await consumerTransport.consume({
id: params.id,
producerId: params.producerId,
kind: params.kind,
rtpParameters: params.rtpParameters
})
// destructure and retrieve the video track from the producer
const { track } = consumer
let stream = new MediaStream() let stream = new MediaStream()
stream.addTrack(track)
// stream.removeTrack(track)
remoteVideo.srcObject = stream
socket.emit('consumer-resume')
console.log('consumer', consumer);
// Maybe the unit does not produce video or audio, so we must only consume what is produced
if (videoParams) {
console.log('❗ Have VIDEO stream to consume');
stream.addTrack(await getVideoTrask(videoParams))
} else {
console.log('❗ Don\'t have VIDEO stream to consume');
}
if (audioParams) {
console.log('❗ Have AUDIO stream to consume');
let audioTrack = await getAudioTrask(audioParams)
stream.addTrack(audioTrack)
} else {
console.log('❗ Don\'t have AUDIO stream to consume');
}
socket.emit('consumer-resume')
remoteVideo.srcObject = stream
remoteVideo.setAttribute('autoplay', true)
remoteVideo.play()
.then(() => {
console.log('remoteVideo PLAY')
})
.catch((error) => {
console.error(`remoteVideo PLAY ERROR | ${error.message}`)
})
}) })
} }
const getVideoTrask = async (videoParams) => {
consumerVideo = await consumerTransport.consume({
id: videoParams.id,
producerId: videoParams.producerId,
kind: videoParams.kind,
rtpParameters: videoParams.rtpParameters
})
return consumerVideo.track
}
const getAudioTrask = async (audioParams) => {
consumerAudio = await consumerTransport.consume({
id: audioParams.id,
producerId: audioParams.producerId,
kind: audioParams.kind,
rtpParameters: audioParams.rtpParameters
})
consumerAudio.on('transportclose', () => {
console.log('transport closed so consumer closed')
})
const audioTrack = consumerAudio.track
audioTrack.applyConstraints({
audio: {
advanced: [
{
echoCancellation: {exact: true}
},
{
autoGainControl: {exact: true}
},
{
noiseSuppression: {exact: true}
},
{
highpassFilter: {exact: true}
}
]
}
})
return audioTrack
}
const closeCall = () => { const closeCall = () => {
console.log('closeCall'); console.log('closeCall');
@ -416,6 +557,30 @@ const closeCall = () => {
resetCallSettings() resetCallSettings()
} }
// const consume = async (kind) => {
// console.log(`[consume] kind: ${kind}`)
// console.log('createRecvTransport Consumer')
// await socket.emit('createWebRtcTransport', { sender: false, callId, dispatcher: true }, ({ params }) => {
// if (params.error) {
// console.log('createRecvTransport | createWebRtcTransport | Error', params.error)
// return
// }
// consumerTransport = device.createRecvTransport(params)
// consumerTransport.on('connect', async ({ dtlsParameters }, callback, errback) => {
// try {
// await socket.emit('transport-recv-connect', {
// dtlsParameters,
// })
// callback()
// } catch (error) {
// errback(error)
// }
// })
// connectRecvTransport()
// })
// }
btnLocalVideo.addEventListener('click', getLocalStream) btnLocalVideo.addEventListener('click', getLocalStream)
btnRecvSendTransport.addEventListener('click', goConnect) // btnRecvSendTransport.addEventListener('click', consume)
btnCloseCall.addEventListener('click', closeCall) btnCloseCall.addEventListener('click', closeCall)