Compare commits

..

6 Commits

Author SHA1 Message Date
0c713ed286 Added logs 2022-09-29 18:26:59 +03:00
817781085f Added logs 2022-09-29 18:23:47 +03:00
a95c29659e Added logs 2022-09-29 18:07:32 +03:00
5d451d961f Added logs 2022-09-29 17:50:51 +03:00
1d47d02792 Added logs 2022-09-29 16:42:21 +03:00
8ac58f0d9d added log for dtls transport-connect 2022-09-29 14:24:44 +03:00
21 changed files with 1422 additions and 1737 deletions

View File

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

4
.env
View File

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

1
.gitignore vendored
View File

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

View File

@ -1,25 +1,11 @@
FROM ubuntu:22.04 FROM ubuntu
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_18.x | bash - RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash -
RUN apt-get install -y nodejs RUN apt-get install -y nodejs
RUN npm install -g watchify
COPY . /app/ EXPOSE 3000
EXPOSE 2000-2020
RUN npm install EXPOSE 10000-10100
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,20 +22,18 @@
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: https://HOST/sfu/?assetId=1&&accountId=1&producer=true&dest_asset_id=75&assetName=Adi ex: http://localhost:3000/sfu/?assetId=1&&accountId=1&producer=true&assetName=Adi&assetType=linx
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
dest_asset_id= the addressee with whom the call is made assetType = asset type of the unit on which you are doing the test
- 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`

996
app.js
View File

@ -1,597 +1,399 @@
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');
let https; let https = require('https');
try { try {
https = require('node:https'); https = require('node:https');
} catch (err) { } catch (err) {
console.log('https support is disabled!'); console.log('https support is disabled!');
} }
const mediasoup = require('mediasoup'); const mediasoup = require('mediasoup');
let worker; let worker
/** /**
* * videoCalls
* videoCalls - Dictionary of Object(s) * |-> Router
* '<callId>': { * |-> Producer
* router: Router, * |-> Consumer
* initiatorAudioProducer: Producer, * |-> Producer Transport
* initiatorVideoProducer: Producer, * |-> Consumer Transport
* receiverVideoProducer: Producer, *
* receiverAudioProducer: Producer, * '<callId>': {
* initiatorProducerTransport: Producer Transport, * router: Router,
* receiverProducerTransport: Producer Transport, * producer: Producer,
* initiatorConsumerVideo: Consumer, * producerTransport: Producer Transport,
* initiatorConsumerAudio: Consumer, * consumer: Consumer,
* initiatorConsumerTransport: Consumer Transport * consumerTransport: Consumer Transport
* initiatorSocket * }
* receiverSocket *
* } **/
* let videoCalls = {}
**/ let socketDetails = {}
let videoCalls = {};
let socketDetails = {}; app.get('/', (_req, res) => {
res.send('Hello from mediasoup app!')
app.get('/', (_req, res) => { })
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
const options = {
// SSL cert for HTTPS access key: fs.readFileSync('./server/ssl/key.pem', 'utf-8'),
const options = { 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,
const io = new Server(httpsServer, { origins: ["*:*"],
allowEIO3: true, // allowRequest: (req, next) => {
origins: ['*:*'], // console.log('req', req);
}); // next(null, true)
// }
httpsServer.listen(process.env.PORT, () => { });
console.log('Video server listening on port:', process.env.PORT); // const io = new Server(server, { origins: '*:*', allowEIO3: true });
});
httpsServer.listen(process.env.PORT, () => {
const peers = io.of('/'); console.log('Video server listening on port:', process.env.PORT)
})
const createWorker = async () => {
try { const peers = io.of('/')
worker = await mediasoup.createWorker({
rtcMinPort: parseInt(process.env.RTC_MIN_PORT), const createWorker = async () => {
rtcMaxPort: parseInt(process.env.RTC_MAX_PORT), try {
}); worker = await mediasoup.createWorker({
console.log(`[createWorker] worker pid ${worker.pid}`); rtcMinPort: 2000,
rtcMaxPort: 2020,
worker.on('died', (error) => { })
// This implies something serious happened, so kill the application console.log(`[createWorker] worker pid ${worker.pid}`);
console.error('mediasoup worker has died', error);
setTimeout(() => process.exit(1), 2000); // exit in 2 seconds worker.on('died', error => {
}); // This implies something serious happened, so kill the application
return worker; console.error('mediasoup worker has died', error);
} catch (error) { setTimeout(() => process.exit(1), 2000); // exit in 2 seconds
console.error(`[createWorker] | ERROR | error: ${error.message}`); })
} return worker;
}; } catch (error) {
console.log(`ERROR | createWorker | ${error.message}`);
// We create a Worker as soon as our application starts }
worker = createWorker(); }
// This is an Array of RtpCapabilities // We create a Worker as soon as our application starts
// https://mediasoup.org/documentation/v3/mediasoup/rtp-parameters-and-capabilities/#RtpCodecCapability worker = createWorker()
// list of media codecs supported by mediasoup ...
// https://github.com/versatica/mediasoup/blob/v3/src/supportedRtpCapabilities.ts // This is an Array of RtpCapabilities
const mediaCodecs = [ // https://mediasoup.org/documentation/v3/mediasoup/rtp-parameters-and-capabilities/#RtpCodecCapability
{ // list of media codecs supported by mediasoup ...
kind: 'audio', // https://github.com/versatica/mediasoup/blob/v3/src/supportedRtpCapabilities.ts
mimeType: 'audio/opus', const mediaCodecs = [
clockRate: 48000, {
channels: 2, kind: 'audio',
}, mimeType: 'audio/opus',
{ clockRate: 48000,
kind: 'video', channels: 2,
mimeType: 'video/VP8', },
clockRate: 90000, {
parameters: { kind: 'video',
'x-google-start-bitrate': 1000, mimeType: 'video/VP8',
}, clockRate: 90000,
channels: 2, parameters: {
}, 'x-google-start-bitrate': 1000,
{ },
kind: 'video', },
mimeType: 'video/VP9', ]
clockRate: 90000,
parameters: { const closeCall = (callId) => {
'profile-id': 2, try {
'x-google-start-bitrate': 1000, if (videoCalls[callId]) {
}, videoCalls[callId].producer?.close();
}, videoCalls[callId].consumer?.close();
{ videoCalls[callId]?.consumerTransport?.close();
kind: 'video', videoCalls[callId]?.producerTransport?.close();
mimeType: 'video/h264', videoCalls[callId]?.router?.close();
clockRate: 90000, delete videoCalls[callId];
parameters: { } else {
'packetization-mode': 1, console.log(`The call with id ${callId} has already been deleted`);
'profile-level-id': '4d0032', }
'level-asymmetry-allowed': 1, } catch (error) {
'x-google-start-bitrate': 1000, console.log(`ERROR | closeCall | callid ${callId} | ${error.message}`);
}, }
}, }
{
kind: 'video', const getRtpCapabilities = (callId, callback) => {
mimeType: 'video/h264', try {
clockRate: 90000, console.log('[getRtpCapabilities] callId', callId);
parameters: { const rtpCapabilities = videoCalls[callId].router.rtpCapabilities;
'packetization-mode': 1, callback({ rtpCapabilities });
'profile-level-id': '42e01f', } catch (error) {
'level-asymmetry-allowed': 1, console.log(`ERROR | getRtpCapabilities | callId ${callId} | ${error.message}`);
'x-google-start-bitrate': 1000, }
}, }
},
]; /*
- Handlers for WS events
const closeCall = (callId) => { - These are created only when we have a connection with a peer
try { */
if (callId && videoCalls[callId]) { peers.on('connection', async socket => {
videoCalls[callId].receiverVideoProducer?.close(); console.log('[connection] socketId:', socket.id);
videoCalls[callId].receiverAudioProducer?.close();
videoCalls[callId].initiatorConsumerVideo?.close(); // After making the connection successfully, we send the client a 'connection-success' event
videoCalls[callId].initiatorConsumerAudio?.close(); socket.emit('connection-success', {
socketId: socket.id
videoCalls[callId]?.initiatorConsumerTransport?.close(); });
videoCalls[callId]?.receiverProducerTransport?.close();
videoCalls[callId]?.router?.close(); // It is triggered when the peer is disconnected
delete videoCalls[callId]; socket.on('disconnect', () => {
console.log(`[closeCall] | callId: ${callId}`); const callId = socketDetails[socket.id];
} console.log(`disconnect | socket ${socket.id} | callId ${callId}`);
} catch (error) { delete socketDetails[socket.id];
console.error(`[closeCall] | ERROR | callId: ${callId} | error: ${error.message}`); closeCall(callId);
} });
};
/*
/* - This event creates a room with the roomId and the callId sent
- Handlers for WS events - It will return the rtpCapabilities of that room
- These are created only when we have a connection with a peer - If the room already exists, it will not create it, but will only return rtpCapabilities
*/ */
peers.on('connection', async (socket) => { socket.on('createRoom', async ({ callId }, callback) => {
console.log('[connection] socketId:', socket.id); try {
if (callId) {
// After making the connection successfully, we send the client a 'connection-success' event console.log(`[createRoom] socket.id ${socket.id} callId ${callId}`);
socket.emit('connection-success', { if (!videoCalls[callId]) {
socketId: socket.id, console.log('[createRoom] callId', callId);
}); videoCalls[callId] = { router: await worker.createRouter({ mediaCodecs }) }
console.log(`[createRoom] Router ID: ${videoCalls[callId].router.id}`);
// It is triggered when the peer is disconnected }
socket.on('disconnect', () => { socketDetails[socket.id] = callId;
const callId = socketDetails[socket.id]; getRtpCapabilities(callId, callback);
console.log(`disconnect | socket ${socket.id} | callId ${callId}`); } else {
delete socketDetails[socket.id]; console.log(`[createRoom] missing callId ${callId}`);
closeCall(callId); }
}); } catch (error) {
console.log(`ERROR | createRoom | callId ${callId} | ${error.message}`);
/* }
- 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 /*
*/ - Client emits a request to create server side Transport
socket.on('createRoom', async ({ callId }, callback) => { - Depending on the sender, producerTransport or consumerTransport is created on that router
let callbackResponse = null; - It will return parameters, these are required for the client to create the RecvTransport
try { from the client.
// We can continue with the room creation process only if we have a callId - If the client is producer(sender: true) then it will use parameters for device.createSendTransport(params)
if (callId) { - If the client is a consumer(sender: false) then it will use parameters for device.createRecvTransport(params)
console.log(`[createRoom] socket.id ${socket.id} callId ${callId}`); */
if (!videoCalls[callId]) { socket.on('createWebRtcTransport', async ({ sender }, callback) => {
videoCalls[callId] = { router: await worker.createRouter({ mediaCodecs }) }; try {
console.log(`[createRoom] Generate Router ID: ${videoCalls[callId].router.id}`); const callId = socketDetails[socket.id];
videoCalls[callId].receiverSocket = socket; console.log(`[createWebRtcTransport] sender ${sender} | callId ${callId}`);
} else { if (sender) {
videoCalls[callId].initiatorSocket = socket; if (!videoCalls[callId].producerTransport) {
} videoCalls[callId].producerTransport = await createWebRtcTransportLayer(callId, callback);
socketDetails[socket.id] = callId; } else {
// rtpCapabilities is set for callback console.log(`producerTransport has already been defined | callId ${callId}`);
callbackResponse = { }
rtpCapabilities: videoCalls[callId].router.rtpCapabilities, } else if (!sender) {
}; if (!videoCalls[callId].consumerTransport) {
} else { videoCalls[callId].consumerTransport = await createWebRtcTransportLayer(callId, callback);
console.log(`[createRoom] missing callId: ${callId}`); } else {
} console.log(`consumerTransport has already been defined | callId ${callId}`);
} catch (error) { }
console.error(`[createRoom] | ERROR | callId: ${callId} | error: ${error.message}`); }
} finally { } catch (error) {
callback(callbackResponse); console.log(`ERROR | createWebRtcTransport | callId ${socketDetails[socket.id]} | sender ${sender} | ${error.message}`);
} }
}); });
/* /*
- Client emits a request to create server side Transport - The client sends this event after successfully creating a createSendTransport(AS PRODUCER)
- Depending on the sender, a producer or consumer is created is created on that router - The connection is made to the created transport
- It will return parameters, these are required for the client to create the RecvTransport */
from the client. socket.on('transport-connect', async ({ dtlsParameters }) => {
- If the client is producer(sender: true) then it will use parameters for device.createSendTransport(params) try {
- If the client is a consumer(sender: false) then it will use parameters for device.createRecvTransport(params) const callId = socketDetails[socket.id];
*/ // console.log('🔴 typeof dtlsParameters', typeof dtlsParameters);
socket.on('createWebRtcTransport', async ({ sender }, callback) => { // console.log('🟢 dtlsParameters', JSON.parse(dtlsParameters));
try { // console.log('🟡 dtlsParameters', dtlsParameters);
const callId = socketDetails[socket.id]; if (typeof dtlsParameters === 'string') dtlsParameters = JSON.parse(dtlsParameters);
console.log(`[createWebRtcTransport] socket ${socket.id} | sender ${sender} | callId ${callId}`); console.log(`[transport-connect] socket.id ${socket.id} | callId ${callId}`);
if (sender) { await videoCalls[callId].producerTransport.connect({ dtlsParameters });
if (!videoCalls[callId].receiverProducerTransport && !isInitiator(callId, socket.id)) { } catch (error) {
videoCalls[callId].receiverProducerTransport = await createWebRtcTransportLayer(callId, callback); console.log(`ERROR | transport-connect | callId ${socketDetails[socket.id]} | ${error.stack}`);
} else if (!videoCalls[callId].initiatorProducerTransport && isInitiator(callId, socket.id)) { }
videoCalls[callId].initiatorProducerTransport = await createWebRtcTransportLayer(callId, callback); });
} else {
console.log(`producerTransport has already been defined | callId ${callId}`); /*
callback(null); - The event sent by the client (PRODUCER) after successfully connecting to producerTransport
} - For the router with the id callId, we make produce on producerTransport
} else if (!sender) { - Create the handler on producer at the 'transportclose' event
if (!videoCalls[callId].receiverConsumerTransport && !isInitiator(callId, socket.id)) { */
videoCalls[callId].receiverConsumerTransport = await createWebRtcTransportLayer(callId, callback); socket.on('transport-produce', async ({ kind, rtpParameters, appData }) => {
} else if (!videoCalls[callId].initiatorConsumerTransport && isInitiator(callId, socket.id)) { try {
videoCalls[callId].initiatorConsumerTransport = await createWebRtcTransportLayer(callId, callback); const callId = socketDetails[socket.id];
} console.log('[transport-produce] | socket.id', socket.id, '| callId', callId);
} videoCalls[callId].producer = await videoCalls[callId].producerTransport.produce({
} catch (error) { kind,
console.error( rtpParameters,
`[createWebRtcTransport] | ERROR | callId: ${socketDetails[socket.id]} | sender: ${sender} | error: ${ });
error.message console.log(`[transport-produce] Producer ID: ${videoCalls[callId].producer.id} | kind: ${videoCalls[callId].producer.kind}`);
}`
); videoCalls[callId].producer.on('transportclose', () => {
callback(error); const callId = socketDetails[socket.id];
} console.log('transport for this producer closed', callId)
}); closeCall(callId);
});
/* } catch (error) {
- The client sends this event after successfully creating a createSendTransport(AS PRODUCER) console.log(`ERROR | transport-produce | callId ${socketDetails[socket.id]} | ${error.message}`);
- The connection is made to the created transport }
*/ });
socket.on('transport-connect', async ({ dtlsParameters }) => {
try { /*
const callId = socketDetails[socket.id]; - The client sends this event after successfully creating a createRecvTransport(AS CONSUMER)
if (typeof dtlsParameters === 'string') dtlsParameters = JSON.parse(dtlsParameters); - The connection is made to the created consumerTransport
*/
console.log(`[transport-connect] socket ${socket.id} | callId ${callId}`); socket.on('transport-recv-connect', async ({ dtlsParameters }) => {
try {
isInitiator(callId, socket.id) const callId = socketDetails[socket.id];
? await videoCalls[callId].initiatorProducerTransport.connect({ dtlsParameters }) console.log(`[transport-recv-connect] socket.id ${socket.id} | callId ${callId}`);
: await videoCalls[callId].receiverProducerTransport.connect({ dtlsParameters }); await videoCalls[callId].consumerTransport.connect({ dtlsParameters });
} catch (error) { } catch (error) {
console.error(`[transport-connect] | ERROR | callId: ${socketDetails[socket.id]} | error: ${error.message}`); console.log(`ERROR | transport-recv-connect | callId ${socketDetails[socket.id]} | ${error.message}`);
} }
}); })
/* /*
- The event sent by the client (PRODUCER) after successfully connecting to receiverProducerTransport/initiatorProducerTransport - The customer consumes after successfully connecting to consumerTransport
- For the router with the id callId, we make produce on receiverProducerTransport/initiatorProducerTransport - The previous step was 'transport-recv-connect', and before that 'createWebRtcTransport'
- Create the handler on producer at the 'transportclose' event - This event is only sent by the consumer
*/ - The parameters that the consumer consumes are returned
socket.on('transport-produce', async ({ kind, rtpParameters, appData }, callback) => { - The consumer does consumerTransport.consume(params)
try { */
const callId = socketDetails[socket.id]; socket.on('consume', async ({ rtpCapabilities }, callback) => {
if (typeof rtpParameters === 'string') rtpParameters = JSON.parse(rtpParameters); try {
const callId = socketDetails[socket.id];
console.log(`[transport-produce] callId: ${callId} | kind: ${kind} | socket: ${socket.id}`); console.log('[consume] callId', callId);
if (kind === 'video') { // Check if the router can consume the specified producer
if (!isInitiator(callId, socket.id)) { if (videoCalls[callId].router.canConsume({
videoCalls[callId].receiverVideoProducer = await videoCalls[callId].receiverProducerTransport.produce({ producerId: videoCalls[callId].producer.id,
kind, rtpCapabilities
rtpParameters, })) {
}); console.log('[consume] Can consume', callId);
// Transport can now consume and return a consumer
videoCalls[callId].receiverVideoProducer.on('transportclose', () => { videoCalls[callId].consumer = await videoCalls[callId].consumerTransport.consume({
console.log('transport for this producer closed', callId); producerId: videoCalls[callId].producer.id,
closeCall(callId); rtpCapabilities,
}); paused: true,
});
// Send back to the client the Producer's id
callback && // https://mediasoup.org/documentation/v3/mediasoup/api/#consumer-on-transportclose
callback({ videoCalls[callId].consumer.on('transportclose', () => {
id: videoCalls[callId].receiverVideoProducer.id, const callId = socketDetails[socket.id];
}); console.log('transport close from consumer', callId);
} else { closeCall();
videoCalls[callId].initiatorVideoProducer = await videoCalls[callId].initiatorProducerTransport.produce({ });
kind,
rtpParameters, // https://mediasoup.org/documentation/v3/mediasoup/api/#consumer-on-producerclose
}); videoCalls[callId].consumer.on('producerclose', () => {
const callId = socketDetails[socket.id];
videoCalls[callId].initiatorVideoProducer.on('transportclose', () => { console.log('producer of consumer closed', callId);
console.log('transport for this producer closed', callId); closeCall();
closeCall(callId); });
});
// From the consumer extract the following params to send back to the Client
callback && const params = {
callback({ id: videoCalls[callId].consumer.id,
id: videoCalls[callId].initiatorVideoProducer.id, producerId: videoCalls[callId].producer.id,
}); kind: videoCalls[callId].consumer.kind,
} rtpParameters: videoCalls[callId].consumer.rtpParameters,
} else if (kind === 'audio') { };
if (!isInitiator(callId, socket.id)) {
videoCalls[callId].receiverAudioProducer = await videoCalls[callId].receiverProducerTransport.produce({ // Send the parameters to the client
kind, callback({ params });
rtpParameters, } else {
}); console.log(`[canConsume] Can't consume | callId ${callId}`);
}
videoCalls[callId].receiverAudioProducer.on('transportclose', () => { } catch (error) {
console.log('transport for this producer closed', callId); console.log(`ERROR | consume | callId ${socketDetails[socket.id]} | ${error.message}`)
closeCall(callId); callback({ params: { error } });
}); }
});
// Send back to the client the Producer's id
callback && /*
callback({ - Event sent by the consumer after consuming to resume the pause
id: videoCalls[callId].receiverAudioProducer.id, - When consuming on consumerTransport, it is initially done with paused: true, here we will resume
}); */
} else { socket.on('consumer-resume', async () => {
videoCalls[callId].initiatorAudioProducer = await videoCalls[callId].initiatorProducerTransport.produce({ try {
kind, const callId = socketDetails[socket.id];
rtpParameters, console.log(`[consumer-resume] callId ${callId}`)
}); await videoCalls[callId].consumer.resume();
} catch (error) {
videoCalls[callId].initiatorAudioProducer.on('transportclose', () => { console.log(`ERROR | consumer-resume | callId ${socketDetails[socket.id]} | ${error.message}`);
console.log('transport for this producer closed', callId); }
closeCall(callId); });
}); });
// Send back to the client the Producer's id /*
callback && - Called from at event 'createWebRtcTransport' and assigned to the consumer or producer transport
callback({ - It will return parameters, these are required for the client to create the RecvTransport
id: videoCalls[callId].initiatorAudioProducer.id, from the client.
}); - If the client is producer(sender: true) then it will use parameters for device.createSendTransport(params)
} - If the client is a consumer(sender: false) then it will use parameters for device.createRecvTransport(params)
} */
const createWebRtcTransportLayer = async (callId, callback) => {
const socketToEmit = isInitiator(callId, socket.id) try {
? videoCalls[callId].receiverSocket console.log('[createWebRtcTransportLayer] callId', callId);
: videoCalls[callId].initiatorSocket; // https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcTransportOptions
const webRtcTransport_options = {
// callId - Id of the call listenIps: [
// kind - producer type: audio/video {
socketToEmit?.emit('new-producer', { callId, kind }); ip: process.env.IP, // Listening IPv4 or IPv6.
} catch (error) { announcedIp: process.env.ANNOUNCED_IP, // Announced IPv4 or IPv6 (useful when running mediasoup behind NAT with private IP).
console.error(`[transport-produce] | ERROR | callId: ${socketDetails[socket.id]} | error: ${error.message}`); }
} ],
}); enableUdp: true,
enableTcp: true,
/* preferUdp: true,
- The client sends this event after successfully creating a createRecvTransport(AS CONSUMER) };
- The connection is made to the created consumerTransport
*/ // https://mediasoup.org/documentation/v3/mediasoup/api/#router-createWebRtcTransport
socket.on('transport-recv-connect', async ({ dtlsParameters }) => { let transport = await videoCalls[callId].router.createWebRtcTransport(webRtcTransport_options)
try { console.log(`callId: ${callId} | transport id: ${transport.id}`)
const callId = socketDetails[socket.id];
console.log(`[transport-recv-connect] socket ${socket.id} | callId ${callId}`); // Handler for when DTLS(Datagram Transport Layer Security) changes
if (typeof dtlsParameters === 'string') dtlsParameters = JSON.parse(dtlsParameters); transport.on('dtlsstatechange', dtlsState => {
// await videoCalls[callId].consumerTransport.connect({ dtlsParameters }); console.log(`transport | dtlsstatechange | calldId ${callId} | dtlsState ${dtlsState}`);
if (!isInitiator(callId, socket.id)) { if (dtlsState === 'closed') {
await videoCalls[callId].receiverConsumerTransport.connect({ dtlsParameters }); transport.close();
} else if (isInitiator(callId, socket.id)) { }
await videoCalls[callId].initiatorConsumerTransport.connect({ dtlsParameters }); });
}
} catch (error) { // Handler if the transport layer has closed (for various reasons)
console.error(`[transport-recv-connect] | ERROR | callId: ${socketDetails[socket.id]} | error: ${error.message}`); transport.on('close', () => {
} console.log(`transport | closed | calldId ${callId}`);
}); });
/* const params = {
- The customer consumes after successfully connecting to consumerTransport id: transport.id,
- The previous step was 'transport-recv-connect', and before that 'createWebRtcTransport' iceParameters: transport.iceParameters,
- This event is only sent by the consumer iceCandidates: transport.iceCandidates,
- The parameters that the consumer consumes are returned dtlsParameters: transport.dtlsParameters,
- The consumer does consumerTransport.consume(params) };
*/
socket.on('consume', async ({ rtpCapabilities }, callback) => { console.log(`createWebRtcTransportLayer | params.dtlsParameters ${params.dtlsParameters}`);
const callId = socketDetails[socket.id]; // Send back to the client the params
const socketId = socket.id; callback({ params });
console.log(`[consume] socket ${socketId} | callId: ${callId}`); // Set transport to producerTransport or consumerTransport
return transport;
if (typeof rtpCapabilities === 'string') rtpCapabilities = JSON.parse(rtpCapabilities);
} catch (error) {
callback({ console.log(`ERROR | createWebRtcTransportLayer | callId ${socketDetails[socket.id]} | ${error.message}`);
videoParams: await consumeVideo({ callId, socketId, rtpCapabilities }), callback({ params: { error } });
audioParams: await consumeAudio({ callId, socketId, rtpCapabilities }), }
}); }
});
/*
- 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
- For the initiator we resume the initiatorConsumerAUDIO/VIDEO and for receiver the receiverConsumerAUDIO/VIDEO
*/
socket.on('consumer-resume', () => {
try {
const callId = socketDetails[socket.id];
const isInitiatorValue = isInitiator(callId, socket.id);
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) {
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
- 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}`);
// https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcTransportOptions
const webRtcTransport_options = {
listenIps: [
{
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).
},
],
enableUdp: true,
enableTcp: true,
preferUdp: true,
};
// https://mediasoup.org/documentation/v3/mediasoup/api/#router-createWebRtcTransport
let transport = await videoCalls[callId].router.createWebRtcTransport(webRtcTransport_options);
// 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();
}
});
// Handler if the transport layer has closed (for various reasons)
transport.on('close', () => {
console.log(`transport | closed | calldId ${callId}`);
});
const params = {
id: transport.id,
iceParameters: transport.iceParameters,
iceCandidates: transport.iceCandidates,
dtlsParameters: transport.dtlsParameters,
};
// Send back to the client the params
callback({ params });
// Set transport to producerTransport or consumerTransport
return transport;
} catch (error) {
console.error(
`[createWebRtcTransportLayer] | ERROR | callId: ${socketDetails[socket.id]} | error: ${error.message}`
);
callback({ params: { error } });
}
};

View File

@ -1,75 +0,0 @@
#/!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.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 419 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 606 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 412 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 567 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 660 KiB

BIN
doc/[video] Workflow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 KiB

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -34,9 +34,6 @@
<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>
@ -46,24 +43,12 @@
<tr> <tr>
<td> <td>
<div id="sharedBtns"> <div id="sharedBtns">
<video <video id="localVideo" autoplay class="video" ></video>
id="localVideo"
class="video"
autoplay
muted
playsinline
></video>
</div> </div>
</td> </td>
<td> <td>
<div id="sharedBtns"> <div id="sharedBtns">
<video <video id="remoteVideo" autoplay class="video" ></video>
id="remoteVideo"
class="video"
autoplay
muted
playsinline
></video>
</div> </div>
</td> </td>
</tr> </tr>
@ -75,11 +60,34 @@
</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,201 +10,147 @@ 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)
console.log('🟩 config', config) let socket
hub = io(config.hubAddress)
produceAudioSelector = document.getElementById('produceAudio'); const connectToMediasoup = () => {
produceAudioSelector.addEventListener('change', e => {
if(e.target.checked) { socket = io(config.mediasoupAddress, {
produceAudio = true reconnection: true,
console.log('produce audio'); reconnectionDelay: 1000,
} else { reconnectionDelayMax : 5000,
produceAudio = false 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()
// 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 producerVideo let producer
let producerAudio
let consumer let consumer
let originAssetId let originAssetId
let consumerVideo // local consumer video(consumer not transport) // let originAssetName = 'Adi'
let consumerAudio // local consumer audio(consumer not transport) // let originAssetTypeName = 'linx'
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 videoParams = { let params = {
// mediasoup params
encodings: [ encodings: [
{ scaleResolutionDownBy: 4, maxBitrate: 500000 }, {
{ scaleResolutionDownBy: 2, maxBitrate: 1000000 }, rid: 'r0',
{ scaleResolutionDownBy: 1, maxBitrate: 5000000 }, maxBitrate: 100000,
{ scalabilityMode: 'S3T3_KEY' } scalabilityMode: 'S1T3',
},
{
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
} }
} }
let audioParams = {
codecOptions :
{
opusStereo : true,
opusDtx : true
}
}
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) => { const streamSuccess = (stream) => {
console.log('[streamSuccess] device', device); console.log('[streamSuccess]');
localVideo.srcObject = stream localVideo.srcObject = stream
console.log('stream', stream); const track = stream.getVideoTracks()[0]
const videoTrack = stream.getVideoTracks()[0] params = {
const audioTrack = stream.getAudioTracks()[0] track,
...params
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: produceAudio ? true : false, audio: false,
video: { video: {
qvga : { width: { ideal: 320 }, height: { ideal: 240 } }, width: {
vga : { width: { ideal: 640 }, height: { ideal: 480 } }, min: 640,
hd : { width: { ideal: 1280 }, height: { ideal: 720 } } max: 1920,
},
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 = () => {
@ -221,6 +167,7 @@ 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
@ -231,8 +178,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()
@ -261,20 +207,18 @@ 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 }, (value) => { socket.emit('createWebRtcTransport', { sender: true, callId }, ({ params }) => {
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
@ -300,10 +244,10 @@ const createSendTransport = () => {
}) })
producerTransport.on('produce', async (parameters, callback, errback) => { producerTransport.on('produce', async (parameters, callback, errback) => {
console.log('[produce] parameters', parameters) console.log(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', ...)
@ -326,45 +270,21 @@ 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)
// Produce video
let producerVideoHandler = await producerTransport.produce(videoParams)
console.log('videoParams', videoParams);
console.log('producerVideo', producerVideo);
producerVideoHandler.on('trackended', () => { producer.on('trackended', () => {
console.log('track ended') console.log('track ended')
// close video track // close video track
}) })
producerVideoHandler.on('transportclose', () => { producer.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,
@ -374,7 +294,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: accepted/rejected answer: 'accepted', // answer: 'rejected'
}; };
console.log('SEND answer', answer); console.log('SEND answer', answer);
@ -386,13 +306,11 @@ 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
@ -402,15 +320,15 @@ const createRecvTransport = async () => {
return return
} }
console.log('[createRecvTransport] params', params) console.log(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 {
@ -427,8 +345,7 @@ const createRecvTransport = async () => {
errback(error) errback(error)
} }
}) })
// We call it in new-rpoducer, we don't need it here anymore connectRecvTransport()
// connectRecvTransport()
}) })
} }
@ -436,8 +353,7 @@ const resetCallSettings = () => {
localVideo.srcObject = null localVideo.srcObject = null
remoteVideo.srcObject = null remoteVideo.srcObject = null
consumer = null consumer = null
producerVideo = null producer = null
producerAudio = null
producerTransport = null producerTransport = null
consumerTransport = null consumerTransport = null
device = undefined device = undefined
@ -445,97 +361,40 @@ 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 ({videoParams, audioParams}) => { }, async ({ params }) => {
console.log(`[consume] 🟩 videoParams`, videoParams) if (params.error) {
console.log(`[consume] 🟩 audioParams`, audioParams) console.log('Cannot Consume')
console.log('[consume] 🟩 consumerTransport', consumerTransport) return
}
// 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)
// Maybe the unit does not produce video or audio, so we must only consume what is produced // stream.removeTrack(track)
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.srcObject = stream
remoteVideo.setAttribute('autoplay', true) socket.emit('consumer-resume')
console.log('consumer', consumer);
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');
@ -557,30 +416,6 @@ 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', consume) btnRecvSendTransport.addEventListener('click', goConnect)
btnCloseCall.addEventListener('click', closeCall) btnCloseCall.addEventListener('click', closeCall)