Compare commits

..

7 Commits

Author SHA1 Message Date
13829c5e6c Update build.sh 2022-11-18 11:42:01 +02:00
9047e6c47d Update log 2022-11-18 11:35:18 +02:00
77da298944 Update log 2022-11-18 11:03:12 +02:00
c25e34bc37 Update log 2022-11-18 10:57:19 +02:00
9a25f16924 Update log 2022-11-18 10:33:53 +02:00
473f94fcbb Update log 2022-11-18 10:28:21 +02:00
9245373625 Update log 2022-11-18 10:21:32 +02:00
20 changed files with 1454 additions and 1709 deletions

View File

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

View File

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`

475
app.js
View File

@ -1,4 +1,4 @@
require('dotenv').config(); require('dotenv').config()
const express = require('express'); const express = require('express');
const app = express(); const app = express();
@ -13,47 +13,50 @@ try {
} }
const mediasoup = require('mediasoup'); const mediasoup = require('mediasoup');
let worker; let worker
/** /**
* videoCalls
* |-> Router
* |-> Producer
* |-> Consumer
* |-> Producer Transport
* |-> Consumer Transport
* *
* videoCalls - Dictionary of Object(s)
* '<callId>': { * '<callId>': {
* router: Router, * router: Router,
* 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,
* initiatorConsumerTransport: Consumer Transport
* initiatorSocket
* receiverSocket
* } * }
* *
**/ **/
let videoCalls = {}; let videoCalls = {}
let socketDetails = {}; let socketDetails = {}
app.get('/', (_req, res) => { app.get('/', (_req, res) => {
res.send('OK'); res.send('Hello from mediasoup app!')
}); })
app.use('/sfu', express.static(path.join(__dirname, 'public'))); app.use('/sfu', express.static(path.join(__dirname, 'public')))
// SSL cert for HTTPS access // SSL cert for HTTPS access
const options = { const options = {
key: fs.readFileSync(process.env.SERVER_KEY, 'utf-8'), key: fs.readFileSync(process.env.SERVER_KEY, 'utf-8'),
cert: fs.readFileSync(process.env.SERVER_CERT, 'utf-8'), cert: fs.readFileSync(process.env.SERVER_CERT, 'utf-8'),
}; }
const httpsServer = https.createServer(options, app); const httpsServer = https.createServer(options, app);
const io = new Server(httpsServer, { const io = new Server(httpsServer, {
allowEIO3: true, allowEIO3: true,
origins: ['*:*'], origins: ["*:*"],
// allowRequest: (req, next) => {
// console.log('req', req);
// next(null, true)
// }
}); });
// const io = new Server(server, { origins: '*:*', allowEIO3: true });
httpsServer.listen(process.env.PORT, () => { httpsServer.listen(process.env.PORT, () => {
console.log('Video server listening on port:', process.env.PORT); console.log('Video server listening on port:', process.env.PORT);
@ -66,19 +69,19 @@ const createWorker = async () => {
worker = await mediasoup.createWorker({ worker = await mediasoup.createWorker({
rtcMinPort: parseInt(process.env.RTC_MIN_PORT), rtcMinPort: parseInt(process.env.RTC_MIN_PORT),
rtcMaxPort: parseInt(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.error(`[createWorker] | ERROR | error: ${error.message}`); console.log(`ERROR | createWorker | ${error.message}`);
} }
}; }
// We create a Worker as soon as our application starts // We create a Worker as soon as our application starts
worker = createWorker(); worker = createWorker();
@ -101,70 +104,36 @@ const mediaCodecs = [
parameters: { parameters: {
'x-google-start-bitrate': 1000, 'x-google-start-bitrate': 1000,
}, },
channels: 2,
},
{
kind: 'video',
mimeType: 'video/VP9',
clockRate: 90000,
parameters: {
'profile-id': 2,
'x-google-start-bitrate': 1000,
},
},
{
kind: 'video',
mimeType: 'video/h264',
clockRate: 90000,
parameters: {
'packetization-mode': 1,
'profile-level-id': '4d0032',
'level-asymmetry-allowed': 1,
'x-google-start-bitrate': 1000,
},
},
{
kind: 'video',
mimeType: 'video/h264',
clockRate: 90000,
parameters: {
'packetization-mode': 1,
'profile-level-id': '42e01f',
'level-asymmetry-allowed': 1,
'x-google-start-bitrate': 1000,
},
}, },
]; ];
const closeCall = (callId) => { const closeCall = (callId) => {
try { try {
if (callId && videoCalls[callId]) { if (callId && videoCalls[callId]) {
videoCalls[callId].receiverVideoProducer?.close(); videoCalls[callId].producer?.close();
videoCalls[callId].receiverAudioProducer?.close(); videoCalls[callId].consumer?.close();
videoCalls[callId].initiatorConsumerVideo?.close(); videoCalls[callId]?.consumerTransport?.close();
videoCalls[callId].initiatorConsumerAudio?.close(); videoCalls[callId]?.producerTransport?.close();
videoCalls[callId]?.initiatorConsumerTransport?.close();
videoCalls[callId]?.receiverProducerTransport?.close();
videoCalls[callId]?.router?.close(); videoCalls[callId]?.router?.close();
delete videoCalls[callId]; delete videoCalls[callId];
console.log(`[closeCall] | callId: ${callId}`); } else {
console.log(`The call with id ${callId} has already been deleted`);
} }
} catch (error) { } catch (error) {
console.error(`[closeCall] | ERROR | callId: ${callId} | error: ${error.message}`); console.log(`ERROR | closeCall | callid ${callId} | ${error.message}`);
} }
}; }
/* /*
- Handlers for WS events - Handlers for WS events
- These are created only when we have a connection with a peer - These are created only when we have a connection with a peer
*/ */
peers.on('connection', async (socket) => { peers.on('connection', async socket => {
console.log('[connection] socketId:', socket.id); console.log('[connection] socketId:', socket.id);
// After making the connection successfully, we send the client a 'connection-success' event // After making the connection successfully, we send the client a 'connection-success' event
socket.emit('connection-success', { socket.emit('connection-success', {
socketId: socket.id, socketId: socket.id
}); });
// It is triggered when the peer is disconnected // It is triggered when the peer is disconnected
@ -185,24 +154,24 @@ peers.on('connection', async (socket) => {
try { try {
// We can continue with the room creation process only if we have a callId // We can continue with the room creation process only if we have a callId
if (callId) { if (callId) {
console.log(`[createRoom] socket.id ${socket.id} callId ${callId}`); console.log(`[createRoom] --🟥-- socket.id ${socket.id} callId ${callId}`);
if (!videoCalls[callId]) { if (!videoCalls[callId]) {
videoCalls[callId] = { router: await worker.createRouter({ mediaCodecs }) }; console.log('[createRoom] callId', callId);
console.log(`[createRoom] Generate Router ID: ${videoCalls[callId].router.id}`); videoCalls[callId] = { router: await worker.createRouter({ mediaCodecs }) }
videoCalls[callId].receiverSocket = socket; console.log(`[createRoom] Router ID: ${videoCalls[callId].router.id}`);
} else {
videoCalls[callId].initiatorSocket = socket;
} }
socketDetails[socket.id] = callId; socketDetails[socket.id] = callId;
// rtpCapabilities is set for callback // rtpCapabilities is set for callback
console.log('[getRtpCapabilities] callId', callId);
callbackResponse = { callbackResponse = {
rtpCapabilities: videoCalls[callId].router.rtpCapabilities, rtpCapabilities :videoCalls[callId].router.rtpCapabilities
}; };
} else { } else {
console.log(`[createRoom] missing callId: ${callId}`); console.log(`[createRoom] missing callId ${callId}`);
} }
} catch (error) { } catch (error) {
console.error(`[createRoom] | ERROR | callId: ${callId} | error: ${error.message}`); console.log(`ERROR | createRoom | callId ${callId} | ${error.message}`);
} finally { } finally {
callback(callbackResponse); callback(callbackResponse);
} }
@ -210,7 +179,7 @@ peers.on('connection', async (socket) => {
/* /*
- Client emits a request to create server side Transport - Client emits a request to create server side Transport
- Depending on the sender, a producer or consumer is created is created on that router - Depending on the sender, producerTransport or consumerTransport is created on that router
- It will return parameters, these are required for the client to create the RecvTransport - It will return parameters, these are required for the client to create the RecvTransport
from the client. from the client.
- If the client is producer(sender: true) then it will use parameters for device.createSendTransport(params) - If the client is producer(sender: true) then it will use parameters for device.createSendTransport(params)
@ -219,29 +188,24 @@ peers.on('connection', async (socket) => {
socket.on('createWebRtcTransport', async ({ sender }, callback) => { socket.on('createWebRtcTransport', async ({ sender }, callback) => {
try { try {
const callId = socketDetails[socket.id]; const callId = socketDetails[socket.id];
console.log(`[createWebRtcTransport] socket ${socket.id} | sender ${sender} | callId ${callId}`); console.log(`[createWebRtcTransport] sender ${sender} | callId ${callId}`);
if (sender) { if (sender) {
if (!videoCalls[callId].receiverProducerTransport && !isInitiator(callId, socket.id)) { if (!videoCalls[callId].producerTransport) {
videoCalls[callId].receiverProducerTransport = await createWebRtcTransportLayer(callId, callback); videoCalls[callId].producerTransport = await createWebRtcTransportLayer(callId, callback);
} else if (!videoCalls[callId].initiatorProducerTransport && isInitiator(callId, socket.id)) {
videoCalls[callId].initiatorProducerTransport = await createWebRtcTransportLayer(callId, callback);
} else { } else {
console.log(`producerTransport has already been defined | callId ${callId}`); console.log(`producerTransport has already been defined | callId ${callId}`);
callback(null); callback(null);
} }
} else if (!sender) { } else if (!sender) {
if (!videoCalls[callId].receiverConsumerTransport && !isInitiator(callId, socket.id)) { if (!videoCalls[callId].consumerTransport) {
videoCalls[callId].receiverConsumerTransport = await createWebRtcTransportLayer(callId, callback); videoCalls[callId].consumerTransport = await createWebRtcTransportLayer(callId, callback);
} else if (!videoCalls[callId].initiatorConsumerTransport && isInitiator(callId, socket.id)) { } else {
videoCalls[callId].initiatorConsumerTransport = await createWebRtcTransportLayer(callId, callback); console.log(`consumerTransport has already been defined | callId ${callId}`);
callback(null);
} }
} }
} catch (error) { } catch (error) {
console.error( console.log(`ERROR | createWebRtcTransport | callId ${socketDetails[socket.id]} | sender ${sender} | ${error.message}`);
`[createWebRtcTransport] | ERROR | callId: ${socketDetails[socket.id]} | sender: ${sender} | error: ${
error.message
}`
);
callback(error); callback(error);
} }
}); });
@ -255,106 +219,42 @@ peers.on('connection', async (socket) => {
const callId = socketDetails[socket.id]; const callId = socketDetails[socket.id];
if (typeof dtlsParameters === 'string') dtlsParameters = JSON.parse(dtlsParameters); if (typeof dtlsParameters === 'string') dtlsParameters = JSON.parse(dtlsParameters);
console.log(`[transport-connect] socket ${socket.id} | callId ${callId}`); console.log(`[transport-connect] socket.id ${socket.id} | callId ${callId}`);
await videoCalls[callId].producerTransport.connect({ dtlsParameters });
isInitiator(callId, socket.id)
? await videoCalls[callId].initiatorProducerTransport.connect({ dtlsParameters })
: await videoCalls[callId].receiverProducerTransport.connect({ dtlsParameters });
} catch (error) { } catch (error) {
console.error(`[transport-connect] | ERROR | callId: ${socketDetails[socket.id]} | error: ${error.message}`); console.log(`ERROR | transport-connect | callId ${socketDetails[socket.id]} | ${error.message}`);
} }
}); });
/* /*
- The event sent by the client (PRODUCER) after successfully connecting to receiverProducerTransport/initiatorProducerTransport - The event sent by the client (PRODUCER) after successfully connecting to producerTransport
- For the router with the id callId, we make produce on receiverProducerTransport/initiatorProducerTransport - For the router with the id callId, we make produce on producerTransport
- Create the handler on producer at the 'transportclose' event - Create the handler on producer at the 'transportclose' event
*/ */
socket.on('transport-produce', async ({ kind, rtpParameters, appData }, callback) => { socket.on('transport-produce', async ({ kind, rtpParameters, appData }, callback) => {
try { try {
const callId = socketDetails[socket.id]; const callId = socketDetails[socket.id];
if (typeof rtpParameters === 'string') rtpParameters = JSON.parse(rtpParameters); if (typeof rtpParameters === 'string') rtpParameters = JSON.parse(rtpParameters);
console.log(`[transport-produce] callId: ${callId} | kind: ${kind} | socket: ${socket.id}`); console.log('[transport-produce] | socket.id', socket.id, '| callId', callId);
videoCalls[callId].producer = await videoCalls[callId].producerTransport.produce({
kind,
rtpParameters,
});
console.log(`[transport-produce] Producer ID: ${videoCalls[callId].producer.id} | kind: ${videoCalls[callId].producer.kind}`);
if (kind === 'video') { videoCalls[callId].producer.on('transportclose', () => {
if (!isInitiator(callId, socket.id)) { const callId = socketDetails[socket.id];
videoCalls[callId].receiverVideoProducer = await videoCalls[callId].receiverProducerTransport.produce({ console.log('transport for this producer closed', callId)
kind, closeCall(callId);
rtpParameters, });
});
videoCalls[callId].receiverVideoProducer.on('transportclose', () => { // Send back to the client the Producer's id
console.log('transport for this producer closed', callId); callback && callback({
closeCall(callId); id: videoCalls[callId].producer.id
}); });
// Send back to the client the Producer's id
callback &&
callback({
id: videoCalls[callId].receiverVideoProducer.id,
});
} else {
videoCalls[callId].initiatorVideoProducer = await videoCalls[callId].initiatorProducerTransport.produce({
kind,
rtpParameters,
});
videoCalls[callId].initiatorVideoProducer.on('transportclose', () => {
console.log('transport for this producer closed', callId);
closeCall(callId);
});
callback &&
callback({
id: videoCalls[callId].initiatorVideoProducer.id,
});
}
} else if (kind === 'audio') {
if (!isInitiator(callId, socket.id)) {
videoCalls[callId].receiverAudioProducer = await videoCalls[callId].receiverProducerTransport.produce({
kind,
rtpParameters,
});
videoCalls[callId].receiverAudioProducer.on('transportclose', () => {
console.log('transport for this producer closed', callId);
closeCall(callId);
});
// Send back to the client the Producer's id
callback &&
callback({
id: videoCalls[callId].receiverAudioProducer.id,
});
} else {
videoCalls[callId].initiatorAudioProducer = await videoCalls[callId].initiatorProducerTransport.produce({
kind,
rtpParameters,
});
videoCalls[callId].initiatorAudioProducer.on('transportclose', () => {
console.log('transport for this producer closed', callId);
closeCall(callId);
});
// Send back to the client the Producer's id
callback &&
callback({
id: videoCalls[callId].initiatorAudioProducer.id,
});
}
}
const socketToEmit = isInitiator(callId, socket.id)
? videoCalls[callId].receiverSocket
: videoCalls[callId].initiatorSocket;
// callId - Id of the call
// kind - producer type: audio/video
socketToEmit?.emit('new-producer', { callId, kind });
} catch (error) { } catch (error) {
console.error(`[transport-produce] | ERROR | callId: ${socketDetails[socket.id]} | error: ${error.message}`); console.log(`ERROR | transport-produce | callId ${socketDetails[socket.id]} | ${error.message}`);
} }
}); });
@ -365,18 +265,12 @@ peers.on('connection', async (socket) => {
socket.on('transport-recv-connect', async ({ dtlsParameters }) => { socket.on('transport-recv-connect', async ({ dtlsParameters }) => {
try { try {
const callId = socketDetails[socket.id]; const callId = socketDetails[socket.id];
console.log(`[transport-recv-connect] socket ${socket.id} | callId ${callId}`); console.log(`[transport-recv-connect] socket.id ${socket.id} | callId ${callId}`);
if (typeof dtlsParameters === 'string') dtlsParameters = JSON.parse(dtlsParameters); await videoCalls[callId].consumerTransport.connect({ dtlsParameters });
// await videoCalls[callId].consumerTransport.connect({ dtlsParameters });
if (!isInitiator(callId, socket.id)) {
await videoCalls[callId].receiverConsumerTransport.connect({ dtlsParameters });
} else if (isInitiator(callId, socket.id)) {
await videoCalls[callId].initiatorConsumerTransport.connect({ dtlsParameters });
}
} catch (error) { } catch (error) {
console.error(`[transport-recv-connect] | ERROR | callId: ${socketDetails[socket.id]} | error: ${error.message}`); console.log(`ERROR | transport-recv-connect | callId ${socketDetails[socket.id]} | ${error.message}`);
} }
}); })
/* /*
- The customer consumes after successfully connecting to consumerTransport - The customer consumes after successfully connecting to consumerTransport
@ -386,157 +280,72 @@ peers.on('connection', async (socket) => {
- The consumer does consumerTransport.consume(params) - The consumer does consumerTransport.consume(params)
*/ */
socket.on('consume', async ({ rtpCapabilities }, callback) => { socket.on('consume', async ({ rtpCapabilities }, callback) => {
const callId = socketDetails[socket.id]; try {
const socketId = socket.id; const callId = socketDetails[socket.id];
console.log('[consume] callId', callId);
console.log(`[consume] socket ${socketId} | callId: ${callId}`); // Check if the router can consume the specified producer
if (videoCalls[callId].router.canConsume({
producerId: videoCalls[callId].producer.id,
rtpCapabilities
})) {
console.log('[consume] Can consume', callId);
// Transport can now consume and return a consumer
videoCalls[callId].consumer = await videoCalls[callId].consumerTransport.consume({
producerId: videoCalls[callId].producer.id,
rtpCapabilities,
paused: true,
});
if (typeof rtpCapabilities === 'string') rtpCapabilities = JSON.parse(rtpCapabilities); // https://mediasoup.org/documentation/v3/mediasoup/api/#consumer-on-transportclose
videoCalls[callId].consumer.on('transportclose', () => {
const callId = socketDetails[socket.id];
console.log('transport close from consumer', callId);
closeCall(callId);
});
callback({ // https://mediasoup.org/documentation/v3/mediasoup/api/#consumer-on-producerclose
videoParams: await consumeVideo({ callId, socketId, rtpCapabilities }), videoCalls[callId].consumer.on('producerclose', () => {
audioParams: await consumeAudio({ callId, socketId, rtpCapabilities }), const callId = socketDetails[socket.id];
}); console.log('producer of consumer closed', callId);
closeCall(callId);
});
// From the consumer extract the following params to send back to the Client
const params = {
id: videoCalls[callId].consumer.id,
producerId: videoCalls[callId].producer.id,
kind: videoCalls[callId].consumer.kind,
rtpParameters: videoCalls[callId].consumer.rtpParameters,
};
// Send the parameters to the client
callback({ params });
} else {
console.log(`[canConsume] Can't consume | callId ${callId}`);
callback(null);
}
} catch (error) {
console.log(`ERROR | consume | callId ${socketDetails[socket.id]} | ${error.message}`)
callback({ params: { error } });
}
}); });
/* /*
- Event sent by the consumer after consuming to resume the pause - Event sent by the consumer after consuming to resume the pause
- When consuming on consumerTransport, it is initially done with paused: true, here we will resume - When consuming on consumerTransport, it is initially done with paused: true, here we will resume
- For the initiator we resume the initiatorConsumerAUDIO/VIDEO and for receiver the receiverConsumerAUDIO/VIDEO
*/ */
socket.on('consumer-resume', () => { socket.on('consumer-resume', async () => {
try { try {
const callId = socketDetails[socket.id]; const callId = socketDetails[socket.id];
const isInitiatorValue = isInitiator(callId, socket.id); console.log(`[consumer-resume] callId ${callId}`)
console.log(`[consumer-resume] callId: ${callId} | isInitiator: ${isInitiatorValue}`); await videoCalls[callId].consumer.resume();
const consumerVideo = isInitiatorValue
? videoCalls[callId].initiatorConsumerVideo
: videoCalls[callId].receiverConsumerVideo;
const consumerAudio = isInitiatorValue
? videoCalls[callId].initiatorConsumerAudio
: videoCalls[callId].receiverConsumerAudio;
consumerVideo?.resume();
consumerAudio?.resume();
} catch (error) { } catch (error) {
console.error( console.log(`ERROR | consumer-resume | callId ${socketDetails[socket.id]} | ${error.message}`);
`[consumer-resume] | ERROR | callId: ${socketDetails[socket.id]} | isInitiator: ${isInitiator} | error: ${
error.message
}`
);
}
});
socket.on('close-producer', ({ callId, kind }) => {
try {
if (isInitiator(callId, socket.id)) {
console.log(`[close-producer] initiator --EMIT--> receiver | callId: ${callId} | kind: ${kind}`);
videoCalls[callId].receiverSocket.emit('close-producer', { callId, kind });
} else {
console.log(`[close-producer] receiver --EMIT--> initiator | callId: ${callId} | kind: ${kind}`);
videoCalls[callId].initiatorSocket.emit('close-producer', { callId, kind });
}
} catch (error) {
console.error(`[close-producer] | ERROR | callId: ${socketDetails[socket.id]} | error: ${error.message}`);
} }
}); });
}); });
const canConsume = ({ callId, producerId, rtpCapabilities }) => {
return !!videoCalls[callId].router.canConsume({
producerId,
rtpCapabilities,
});
};
const consumeVideo = async ({ callId, socketId, rtpCapabilities }) => {
// Handlers for consumer transport https://mediasoup.org/documentation/v3/mediasoup/api/#consumer-on-transportclose
if (isInitiator(callId, socketId) && videoCalls[callId].receiverVideoProducer) {
const producerId = videoCalls[callId].receiverVideoProducer.id;
if (!canConsume({ callId, producerId, rtpCapabilities })) return null;
videoCalls[callId].initiatorConsumerVideo = await videoCalls[callId].initiatorConsumerTransport.consume({
producerId,
rtpCapabilities,
paused: true,
});
return {
id: videoCalls[callId].initiatorConsumerVideo.id,
producerId,
kind: 'video',
rtpParameters: videoCalls[callId].initiatorConsumerVideo.rtpParameters,
};
} else if (videoCalls[callId].initiatorVideoProducer) {
const producerId = videoCalls[callId].initiatorVideoProducer.id;
if (!canConsume({ callId, producerId, rtpCapabilities })) return null;
videoCalls[callId].receiverConsumerVideo = await videoCalls[callId].receiverConsumerTransport.consume({
producerId,
rtpCapabilities,
paused: true,
});
return {
id: videoCalls[callId].receiverConsumerVideo.id,
producerId,
kind: 'video',
rtpParameters: videoCalls[callId].receiverConsumerVideo.rtpParameters,
};
} else {
return null;
}
};
const consumeAudio = async ({ callId, socketId, rtpCapabilities }) => {
try {
// Handlers for consumer transport https://mediasoup.org/documentation/v3/mediasoup/api/#consumer-on-transportclose
if (isInitiator(callId, socketId) && videoCalls[callId].receiverAudioProducer) {
const producerId = videoCalls[callId].receiverAudioProducer.id;
if (!canConsume({ callId, producerId, rtpCapabilities })) return null;
videoCalls[callId].initiatorConsumerAudio = await videoCalls[callId].initiatorConsumerTransport.consume({
producerId,
rtpCapabilities,
paused: true,
});
return {
id: videoCalls[callId].initiatorConsumerAudio.id,
producerId,
kind: 'audio',
rtpParameters: videoCalls[callId].initiatorConsumerAudio.rtpParameters,
};
} else if (videoCalls[callId].initiatorAudioProducer) {
const producerId = videoCalls[callId].initiatorAudioProducer.id;
if (!canConsume({ callId, producerId, rtpCapabilities })) return null;
videoCalls[callId].receiverConsumerAudio = await videoCalls[callId].receiverConsumerTransport.consume({
producerId,
rtpCapabilities,
paused: true,
});
return {
id: videoCalls[callId].receiverConsumerAudio.id,
producerId,
kind: 'audio',
rtpParameters: videoCalls[callId].receiverConsumerAudio.rtpParameters,
};
} else {
return null;
}
} catch (error) {
console.error(`[consumeAudio] | ERROR | error: ${error}`);
}
};
const isInitiator = (callId, socketId) => {
return videoCalls[callId]?.initiatorSocket?.id === socketId;
};
/* /*
- Called from at event 'createWebRtcTransport' and assigned to the consumer or producer transport - Called from at event 'createWebRtcTransport' and assigned to the consumer or producer transport
- It will return parameters, these are required for the client to create the RecvTransport - It will return parameters, these are required for the client to create the RecvTransport
@ -546,14 +355,14 @@ const isInitiator = (callId, socketId) => {
*/ */
const createWebRtcTransportLayer = async (callId, callback) => { const createWebRtcTransportLayer = async (callId, callback) => {
try { try {
console.log(`[createWebRtcTransportLayer] callId: ${callId}`); console.log('[createWebRtcTransportLayer] callId', callId);
// https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcTransportOptions // https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcTransportOptions
const webRtcTransport_options = { const webRtcTransport_options = {
listenIps: [ listenIps: [
{ {
ip: process.env.IP, // Listening IPv4 or IPv6. ip: process.env.IP, // Listening IPv4 or IPv6.
announcedIp: process.env.ANNOUNCED_IP, // Announced IPv4 or IPv6 (useful when running mediasoup behind NAT with private IP). announcedIp: process.env.ANNOUNCED_IP, // Announced IPv4 or IPv6 (useful when running mediasoup behind NAT with private IP).
}, }
], ],
enableUdp: true, enableUdp: true,
enableTcp: true, enableTcp: true,
@ -561,10 +370,11 @@ const createWebRtcTransportLayer = async (callId, callback) => {
}; };
// https://mediasoup.org/documentation/v3/mediasoup/api/#router-createWebRtcTransport // https://mediasoup.org/documentation/v3/mediasoup/api/#router-createWebRtcTransport
let transport = await videoCalls[callId].router.createWebRtcTransport(webRtcTransport_options); let transport = await videoCalls[callId].router.createWebRtcTransport(webRtcTransport_options)
console.log(`callId: ${callId} | transport id: ${transport.id}`)
// Handler for when DTLS(Datagram Transport Layer Security) changes // Handler for when DTLS(Datagram Transport Layer Security) changes
transport.on('dtlsstatechange', (dtlsState) => { transport.on('dtlsstatechange', dtlsState => {
console.log(`transport | dtlsstatechange | calldId ${callId} | dtlsState ${dtlsState}`); console.log(`transport | dtlsstatechange | calldId ${callId} | dtlsState ${dtlsState}`);
if (dtlsState === 'closed') { if (dtlsState === 'closed') {
transport.close(); transport.close();
@ -588,10 +398,9 @@ const createWebRtcTransportLayer = async (callId, callback) => {
// Set transport to producerTransport or consumerTransport // Set transport to producerTransport or consumerTransport
return transport; return transport;
} catch (error) { } catch (error) {
console.error( console.log(`ERROR | createWebRtcTransportLayer | callId ${socketDetails[socket.id]} | ${error.message}`);
`[createWebRtcTransportLayer] | ERROR | callId: ${socketDetails[socket.id]} | error: ${error.message}`
);
callback({ params: { error } }); callback({ params: { error } });
} }
}; }

View File

@ -1,40 +1,4 @@
#/!bin/bash #/!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 ## PREBUILD PROCESS
# check dist dir to be present and empty # check dist dir to be present and empty
if [ ! -d "dist" ]; then if [ ! -d "dist" ]; then
@ -45,31 +9,40 @@ else
## CLEANUP ## CLEANUP
rm -fr dist/* rm -fr dist/*
fi fi
if [ -d "node_modules" ]; then
rm -fr node_modules
fi
# Install dependencies # Install dependencies
#npm install #npm install
## PROJECT NEEDS ## PROJECT NEEDS
echo "Building app... from $(git rev-parse --abbrev-ref HEAD)" echo "Building app... from $(git rev-parse --abbrev-ref HEAD)"
#npm run-script build #npm run-script build
cp -r {.env,app.js,package.json,server,public,doc,Dockerfile} dist/ cp .env.example .env
#cp -r ./* dist/ cp -r {.env,app.js,package.json,server,public} dist/
# Generate Git log #Add version control for pm2
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 cd dist
addVersionPm2 #Add version control for pm2
version=$(git describe)
file_pkg="package.json"
key=" \"version\": \""
count=$(echo ${version%%-*} | grep -o "\." | wc -l)
if (( $count > 1 )); then
version=${version%%-*}
else
version="${version%%-*}.0"
fi
if [ -f "$file_pkg" ] && [ ! -z "$version" ]; then
version=" \"version\": \"$version\","
sed -i "s|^.*$key.*|${version//\//\\/}|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
## POST BUILD ## POST BUILD
cd - cd -
rm .env

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,7 +178,6 @@ 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,46 +270,22 @@ 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 producer.on('trackended', () => {
let producerVideoHandler = await producerTransport.produce(videoParams)
console.log('videoParams', videoParams);
console.log('producerVideo', producerVideo);
producerVideoHandler.on('trackended', () => {
console.log('track ended') console.log('track ended')
// close video track // close video track
}) })
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,
dest_asset_id: originAssetId || parseInt(urlParams.get('dest_asset_id')), dest_asset_id: originAssetId || parseInt(urlParams.get('dest_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)