Compare commits

..

7 Commits

Author SHA1 Message Date
2e336a429e fix 2022-10-18 18:23:54 +03:00
b14e82fd87 fix 2022-10-18 18:22:19 +03:00
c4f72eddd5 fix 2022-10-18 18:21:07 +03:00
6e4ceb9977 fixes 2022-10-18 18:18:29 +03:00
2c00de1dd0 fix ssl cert/key 2022-10-18 18:13:24 +03:00
f96fd24e03 Fix https 2022-10-18 18:12:03 +03:00
fce2f30648 Fix https import 2022-10-18 18:07:04 +03:00
20 changed files with 1422 additions and 1733 deletions

View File

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

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