Compare commits

...

44 Commits

Author SHA1 Message Date
801652170e LINXD-2180: Added recordings 2022-08-26 10:01:08 +03:00
cd887142f7 Add log on consume 2022-08-18 09:34:22 +03:00
afb328ea3b Add log on consume 2022-08-18 09:08:55 +03:00
9b36a78be2 Add log on consume 2022-08-16 14:36:23 +03:00
d67debc70b Add log on createRecvTransport 2022-08-16 14:21:02 +03:00
b64c49c1cd Refactor transportclose 21 2022-08-12 15:08:30 +03:00
383e34bf22 Refactor transportclose 20 2022-08-12 15:07:13 +03:00
004c0f66a7 Refactor transportclose 19 2022-08-12 15:02:42 +03:00
89ee1f301a Refactor transportclose 18 2022-08-12 14:56:20 +03:00
7a564d4a61 Refactor transportclose 17 2022-08-11 22:24:33 +03:00
3473c10608 Refactor transportclose 16 2022-08-11 22:11:49 +03:00
f6e3203198 Refactor transportclose 15 2022-08-11 15:03:00 +03:00
12516b59b8 Refactor transportclose 14.5 2022-08-11 15:00:28 +03:00
d9a53ecf20 Refactor transportclose 14 2022-08-11 14:59:50 +03:00
01fec638c9 Refactor transportclose 13 2022-08-11 14:54:46 +03:00
6d8f22939a Refactor transportclose 12.5 2022-08-11 14:45:25 +03:00
cd8153e3e7 Refactor transportclose 12 2022-08-11 14:43:58 +03:00
0f1c268fa5 Refactor transportclose 11 2022-08-11 14:40:40 +03:00
6147d3bede Refactor transportclose 10 2022-08-11 14:22:28 +03:00
1df588bac7 Refactor transportclose 9 2022-08-11 14:17:05 +03:00
3f3048e54f Refactor transportclose 8 2022-08-11 13:17:44 +03:00
ac5d5d7fd0 Refactor transportclose 7 2022-08-11 13:15:23 +03:00
65b5b563ff Refactor transportclose 6 2022-08-11 12:52:50 +03:00
8f7b60c78a Refactor transportclose 5 2022-08-11 12:50:18 +03:00
9c46ed4944 Refactor transportclose 4 2022-08-11 12:46:24 +03:00
7bb45dca19 Refactor transportclose 3 2022-08-11 12:18:01 +03:00
e5fbb1e5a7 Refactor transportclose 2 2022-08-11 12:14:39 +03:00
05f4985075 Refactor transportclose 2022-08-11 12:10:29 +03:00
c49da6696c Added room it when close transport; Added ts config file 2022-08-11 11:55:11 +03:00
112ab0b229 Added room it when close transport; Added ts config file 2022-08-11 11:45:48 +03:00
625fb28e65 Added room it when close transport; Added ts config file 2022-08-11 11:33:16 +03:00
c8a774a903 Update package.json(build/dev start commands); Added README 2022-08-09 18:58:17 +03:00
9cc00c05fd Added my assetName and assetType in URL 2022-08-07 16:54:39 +03:00
6d80943907 Remove callId from transportClose(from producer) 2022-08-06 10:15:51 +03:00
a2223529da Close transport, producer/consumer and reset room when we close the video call 2022-08-06 10:05:43 +03:00
3cc5b0dea8 Added logs 2022-08-05 11:51:07 +03:00
c31922f93a Added logs 2022-08-05 11:49:15 +03:00
e07ffb3181 Added logs 2022-08-05 11:43:37 +03:00
d07df95652 Added logs 2022-08-05 11:32:40 +03:00
222ff46655 Close producer when dispatcher close the window 2022-08-02 19:52:06 +03:00
b947318142 Close producer when dispatcher close the window 2022-08-02 19:50:04 +03:00
1ab77c1f17 Close producer when dispatcher close the window 2022-08-02 19:43:08 +03:00
14d96fa7c5 Close producer when dispatcher close the window 2022-08-02 19:33:39 +03:00
ea8d4268ec Close producer when dispatcher close the window 2022-08-02 19:10:52 +03:00
10 changed files with 8857 additions and 92 deletions

4
.env
View File

@ -1,7 +1,3 @@
PORT=3000 PORT=3000
IP=0.0.0.0 # Listening IPv4 or IPv6. IP=0.0.0.0 # Listening IPv4 or IPv6.
<<<<<<< HEAD
ANNOUNCED_IP=185.8.154.190 # Announced IPv4 or IPv6 (useful when running mediasoup behind NAT with private IP). ANNOUNCED_IP=185.8.154.190 # Announced IPv4 or IPv6 (useful when running mediasoup behind NAT with private IP).
=======
ANNOUNCED_IP=185.8.154.190 # Announced IPv4 or IPv6 (useful when running mediasoup behind NAT with private IP).
>>>>>>> aa5b8770c643600acb5b7aca91bc03437d610295

View File

@ -1,2 +1,29 @@
Generate keys command: # Video server
openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.pem -out cert.pem
### Development
##### To start in development mode you must:
1. Install the dependencies `npm install`.
2. Run the `npm start:dev` command to start the server in dev mode.
(Any change will trigger a refresh of the server)
### Production
##### To start in production mode you must:
1. Install the dependencies `npm install`.
2. Run the `npm start:prod` command to start the server in production mode.
(To connect to the terminal, use `pm2 log video-server`)
---
- 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
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
accountId = account id of the unit on which you are doing the test
producer = it will always be true because you are the producer
(it's possible to put false, but then you have to have another client with producer true)
assetName = asset name of the unit on which you are doing the test
assetType = asset type of the unit on which you are doing the test

372
app.js
View File

@ -14,8 +14,11 @@ import fs from 'fs'
import path from 'path' import path from 'path'
const __dirname = path.resolve() const __dirname = path.resolve()
// const FFmpegStatic = require("ffmpeg-static")
import FFmpegStatic from 'ffmpeg-static'
import Server from 'socket.io' import Server from 'socket.io'
import mediasoup, { getSupportedRtpCapabilities } from 'mediasoup' import mediasoup, { getSupportedRtpCapabilities } from 'mediasoup'
import Process from 'child_process'
let worker let worker
let router = {} let router = {}
@ -42,6 +45,257 @@ httpsServer.listen(process.env.PORT, () => {
console.log('Listening on port:', process.env.PORT) console.log('Listening on port:', process.env.PORT)
}) })
const startRecordingFfmpeg = () => {
// Return a Promise that can be awaited
let recResolve;
const promise = new Promise((res, _rej) => {
recResolve = res;
});
// const useAudio = audioEnabled();
// const useVideo = videoEnabled();
// const useH264 = h264Enabled();
// const cmdProgram = "ffmpeg"; // Found through $PATH
const cmdProgram = FFmpegStatic; // From package "ffmpeg-static"
let cmdInputPath = `${__dirname}/recording/input-vp8.sdp`;
let cmdOutputPath = `${__dirname}/recording/output-ffmpeg-vp8.webm`;
let cmdCodec = "";
let cmdFormat = "-f webm -flags +global_header";
// Ensure correct FFmpeg version is installed
const ffmpegOut = Process.execSync(cmdProgram + " -version", {
encoding: "utf8",
});
const ffmpegVerMatch = /ffmpeg version (\d+)\.(\d+)\.(\d+)/.exec(ffmpegOut);
let ffmpegOk = false;
if (ffmpegOut.startsWith("ffmpeg version git")) {
// Accept any Git build (it's up to the developer to ensure that a recent
// enough version of the FFmpeg source code has been built)
ffmpegOk = true;
} else if (ffmpegVerMatch) {
const ffmpegVerMajor = parseInt(ffmpegVerMatch[1], 10);
if (ffmpegVerMajor >= 4) {
ffmpegOk = true;
}
}
if (!ffmpegOk) {
console.error("FFmpeg >= 4.0.0 not found in $PATH; please install it");
process.exit(1);
}
// if (useAudio) {
// cmdCodec += " -map 0:a:0 -c:a copy";
// }
// if (useVideo) {
cmdCodec += " -map 0:v:0 -c:v copy";
// if (useH264) {
cmdInputPath = `${__dirname}/recording/input-h264.sdp`;
cmdOutputPath = `${__dirname}/recording/output-ffmpeg-h264.mp4`;
// "-strict experimental" is required to allow storing
// OPUS audio into MP4 container
cmdFormat = "-f mp4 -strict experimental";
// }
// }
// Run process
const cmdArgStr = [
"-nostdin",
"-protocol_whitelist file,rtp,udp",
"-loglevel debug",
"-analyzeduration 5M",
"-probesize 5M",
"-fflags +genpts",
`-i ${cmdInputPath}`,
cmdCodec,
cmdFormat,
`-y ${cmdOutputPath}`,
]
.join(" ")
.trim();
console.log('💗', cmdCodec);
console.log(`Run command: ${cmdProgram} ${cmdArgStr}`);
let recProcess = Process.spawn(cmdProgram, cmdArgStr.split(/\s+/));
global.recProcess = recProcess;
recProcess.on("error", (err) => {
console.error("Recording process error:", err);
});
recProcess.on("exit", (code, signal) => {
console.log("Recording process exit, code: %d, signal: %s", code, signal);
global.recProcess = null;
stopMediasoupRtp();
if (!signal || signal === "SIGINT") {
console.log("Recording stopped");
} else {
console.warn(
"Recording process didn't exit cleanly, output file might be corrupt"
);
}
});
// FFmpeg writes its logs to stderr
recProcess.stderr.on("data", (chunk) => {
chunk
.toString()
.split(/\r?\n/g)
.filter(Boolean) // Filter out empty strings
.forEach((line) => {
console.log(line);
if (line.startsWith("ffmpeg version")) {
setTimeout(() => {
recResolve();
}, 1000);
}
});
});
return promise;
}
const startRecordingGstreamer = () => {
// Return a Promise that can be awaited
let recResolve;
const promise = new Promise((res, _rej) => {
recResolve = res;
});
// const useAudio = audioEnabled();
// const useVideo = videoEnabled();
// const useH264 = h264Enabled();
let cmdInputPath = `${__dirname}/recording/input-vp8.sdp`;
let cmdOutputPath = `${__dirname}/recording/output-gstreamer-vp8.webm`;
let cmdMux = "webmmux";
let cmdAudioBranch = "";
let cmdVideoBranch = "";
// if (useAudio) {
// // prettier-ignore
// cmdAudioBranch =
// "demux. ! queue \
// ! rtpopusdepay \
// ! opusparse \
// ! mux.";
// }
// if (useVideo) {
// if (useH264) {
cmdInputPath = `${__dirname}/recording/input-h264.sdp`;
cmdOutputPath = `${__dirname}/recording/output-gstreamer-h264.mp4`;
cmdMux = `mp4mux faststart=true faststart-file=${cmdOutputPath}.tmp`;
// prettier-ignore
cmdVideoBranch =
"demux. ! queue \
! rtph264depay \
! h264parse \
! mux.";
// } else {
// // prettier-ignore
// cmdVideoBranch =
// "demux. ! queue \
// ! rtpvp8depay \
// ! mux.";
// }
// }
// Run process
const cmdProgram = "gst-launch-1.0"; // Found through $PATH
const cmdArgStr = [
"--eos-on-shutdown",
`filesrc location=${cmdInputPath}`,
"! sdpdemux timeout=0 name=demux",
`${cmdMux} name=mux`,
`! filesink location=${cmdOutputPath}`,
cmdAudioBranch,
cmdVideoBranch,
]
.join(" ")
.trim();
console.log(
`Run command: ${cmdProgram} ${cmdArgStr}`
);
let recProcess = Process.spawn(cmdProgram, cmdArgStr.split(/\s+/));
global.recProcess = recProcess;
recProcess.on("error", (err) => {
console.error("Recording process error:", err);
});
recProcess.on("exit", (code, signal) => {
console.log("Recording process exit, code: %d, signal: %s", code, signal);
global.recProcess = null;
stopMediasoupRtp();
if (!signal || signal === "SIGINT") {
console.log("Recording stopped");
} else {
console.warn(
"Recording process didn't exit cleanly, output file might be corrupt"
);
}
});
// GStreamer writes some initial logs to stdout
recProcess.stdout.on("data", (chunk) => {
chunk
.toString()
.split(/\r?\n/g)
.filter(Boolean) // Filter out empty strings
.forEach((line) => {
console.log(line);
if (line.startsWith("Setting pipeline to PLAYING")) {
setTimeout(() => {
recResolve();
}, 1000);
}
});
});
// GStreamer writes its progress logs to stderr
recProcess.stderr.on("data", (chunk) => {
chunk
.toString()
.split(/\r?\n/g)
.filter(Boolean) // Filter out empty strings
.forEach((line) => {
console.log(line);
});
});
return promise;
}
function stopMediasoupRtp() {
console.log("Stop mediasoup RTP transport and consumer");
// const useAudio = audioEnabled();
// const useVideo = videoEnabled();
// if (useAudio) {
// global.mediasoup.rtp.audioConsumer.close();
// global.mediasoup.rtp.audioTransport.close();
// }
// if (useVideo) {
// global.mediasoup.rtp.videoConsumer.close();
// global.mediasoup.rtp.videoTransport.close();
// }
}
const io = new Server(httpsServer) const io = new Server(httpsServer)
// socket.io namespace (could represent a room?) // socket.io namespace (could represent a room?)
@ -58,8 +312,8 @@ const peers = io.of('/mediasoup')
const createWorker = async () => { const createWorker = async () => {
worker = await mediasoup.createWorker({ worker = await mediasoup.createWorker({
rtcMinPort: 2000, rtcMinPort: 32256,
rtcMaxPort: 2020, rtcMaxPort: 65535,
}) })
console.log(`[createWorker] worker pid ${worker.pid}`) console.log(`[createWorker] worker pid ${worker.pid}`)
@ -81,17 +335,31 @@ worker = createWorker()
// 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",
preferredPayloadType: 111,
clockRate: 48000, clockRate: 48000,
channels: 2, channels: 2,
parameters: {
minptime: 10,
useinbandfec: 1,
},
}, },
{ {
kind: 'video', kind: "video",
mimeType: 'video/VP8', mimeType: "video/VP8",
preferredPayloadType: 96,
clockRate: 90000,
},
{
kind: "video",
mimeType: "video/H264",
preferredPayloadType: 125,
clockRate: 90000, clockRate: 90000,
parameters: { parameters: {
'x-google-start-bitrate': 1000, "level-asymmetry-allowed": 1,
"packetization-mode": 1,
"profile-level-id": "42e01f",
}, },
}, },
] ]
@ -109,6 +377,8 @@ peers.on('connection', async socket => {
}) })
socket.on('createRoom', async ({ callId }, callback) => { socket.on('createRoom', async ({ callId }, callback) => {
console.log('[createRoom] callId', callId);
console.log('Router length:', Object.keys(router).length);
if (router[callId] === undefined) { if (router[callId] === undefined) {
// worker.createRouter(options) // worker.createRouter(options)
// options = { mediaCodecs, appData } // options = { mediaCodecs, appData }
@ -147,7 +417,7 @@ peers.on('connection', async socket => {
}) })
// see client's socket.emit('transport-produce', ...) // see client's socket.emit('transport-produce', ...)
socket.on('transport-produce', async ({ kind, rtpParameters, appData }, callback) => { socket.on('transport-produce', async ({ kind, rtpParameters, callId }, callback) => {
// call produce based on the prameters from the client // call produce based on the prameters from the client
producer = await producerTransport.produce({ producer = await producerTransport.produce({
kind, kind,
@ -157,14 +427,65 @@ peers.on('connection', async socket => {
console.log(`[transport-produce] Producer ID: ${producer.id} | kind: ${producer.kind}`) console.log(`[transport-produce] Producer ID: ${producer.id} | kind: ${producer.kind}`)
producer.on('transportclose', () => { producer.on('transportclose', () => {
console.log('transport for this producer closed ') console.log('transport for this producer closed', callId)
// https://mediasoup.org/documentation/v3/mediasoup/api/#producer-close
producer.close() producer.close()
// https://mediasoup.org/documentation/v3/mediasoup/api/#router-close
router[callId].close()
delete router[callId]
}) })
// Send back to the client the Producer's id // Send back to the client the Producer's id
callback({ callback({
id: producer.id id: producer.id
}) })
console.log('🔴', callId);
const rtpTransport = await router[callId].createPlainTransport({
comedia: false,
rtcpMux: false,
listenIp: { ip: "127.0.0.1", announcedIp: null }
});
await rtpTransport.connect({
ip: "127.0.0.1",
port: 5006,
rtcpPort: 5007,
});
console.log(
"mediasoup VIDEO RTP SEND transport connected: %s:%d <--> %s:%d (%s)",
rtpTransport.tuple.localIp,
rtpTransport.tuple.localPort,
rtpTransport.tuple.remoteIp,
rtpTransport.tuple.remotePort,
rtpTransport.tuple.protocol
);
console.log(
"mediasoup VIDEO RTCP SEND transport connected: %s:%d <--> %s:%d (%s)",
rtpTransport.rtcpTuple.localIp,
rtpTransport.rtcpTuple.localPort,
rtpTransport.rtcpTuple.remoteIp,
rtpTransport.rtcpTuple.remotePort,
rtpTransport.rtcpTuple.protocol
);
const rtpConsumer = await rtpTransport.consume({
// producerId: global.mediasoup.webrtc.videoProducer.id,
producerId: producer.id,
// rtpCapabilities: router.rtpCapabilities,
rtpCapabilities: router[callId].rtpCapabilities,
paused: true,
});
// console.log('🟡 producerId:', producer.id, 'rtpCapabilities:', router[callId].rtpCapabilities, 'paused:', true);
await startRecordingFfmpeg();
// await startRecordingGstreamer();
rtpConsumer.resume();
}) })
// see client's socket.emit('transport-recv-connect', ...) // see client's socket.emit('transport-recv-connect', ...)
@ -175,6 +496,7 @@ peers.on('connection', async socket => {
socket.on('consume', async ({ rtpCapabilities, callId }, callback) => { socket.on('consume', async ({ rtpCapabilities, callId }, callback) => {
try { try {
console.log('consume', rtpCapabilities, callId);
// check if the router can consume the specified producer // check if the router can consume the specified producer
if (router[callId].canConsume({ if (router[callId].canConsume({
producerId: producer.id, producerId: producer.id,
@ -188,11 +510,17 @@ peers.on('connection', async socket => {
}) })
consumer.on('transportclose', () => { consumer.on('transportclose', () => {
console.log('transport close from consumer') console.log('transport close from consumer', callId)
// closeRoom(callId)
delete router[callId]
}) })
consumer.on('producerclose', () => { consumer.on('producerclose', () => {
console.log('producer of consumer closed') console.log('producer of consumer closed', callId)
// https://mediasoup.org/documentation/v3/mediasoup/api/#router-close
router[callId].close()
delete router[callId]
}) })
// from the consumer extract the following params // from the consumer extract the following params
@ -225,6 +553,7 @@ peers.on('connection', async socket => {
const createWebRtcTransportLayer = async (callId, callback) => { const createWebRtcTransportLayer = async (callId, callback) => {
try { try {
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: [
@ -236,8 +565,12 @@ const createWebRtcTransportLayer = async (callId, callback) => {
enableUdp: true, enableUdp: true,
enableTcp: true, enableTcp: true,
preferUdp: true, preferUdp: true,
initialAvailableOutgoingBitrate: 300000
} }
// console.log('webRtcTransport_options', webRtcTransport_options);
// console.log('router', router, '| router[callId]', router[callId]);
// https://mediasoup.org/documentation/v3/mediasoup/api/#router-createWebRtcTransport // https://mediasoup.org/documentation/v3/mediasoup/api/#router-createWebRtcTransport
let transport = await router[callId].createWebRtcTransport(webRtcTransport_options) let transport = await router[callId].createWebRtcTransport(webRtcTransport_options)
console.log(`callId: ${callId} | transport id: ${transport.id}`) console.log(`callId: ${callId} | transport id: ${transport.id}`)
@ -252,20 +585,25 @@ const createWebRtcTransportLayer = async (callId, callback) => {
console.log('transport closed') console.log('transport closed')
}) })
const params = {
id: transport.id,
iceParameters: transport.iceParameters,
iceCandidates: transport.iceCandidates,
dtlsParameters: transport.dtlsParameters,
}
console.log('params', params);
// send back to the client the following prameters // send back to the client the following prameters
callback({ callback({
// https://mediasoup.org/documentation/v3/mediasoup-client/api/#TransportOptions // https://mediasoup.org/documentation/v3/mediasoup-client/api/#TransportOptions
params: { params
id: transport.id,
iceParameters: transport.iceParameters,
iceCandidates: transport.iceCandidates,
dtlsParameters: transport.dtlsParameters,
}
}) })
return transport return transport
} catch (error) { } catch (error) {
console.log('[createWebRtcTransportLayer] ERROR', JSON.stringify(error));
callback({ callback({
params: { params: {
error: error error: error

8231
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,8 @@
"main": "app.js", "main": "app.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon app.js", "start:dev": "nodemon app.ts",
"start:prod": "pm2 start ./app.js -n video-server",
"watch": "watchify public/index.js -o public/bundle.js -v" "watch": "watchify public/index.js -o public/bundle.js -v"
}, },
"keywords": [], "keywords": [],
@ -13,18 +14,22 @@
"license": "ISC", "license": "ISC",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@types/express": "^4.17.13",
"dotenv": "^16.0.1", "dotenv": "^16.0.1",
"express": "^4.18.1", "express": "^4.18.1",
"ffmpeg-static": "^5.0.2",
"httpolyglot": "^0.1.2", "httpolyglot": "^0.1.2",
"mediasoup": "^3.10.4", "mediasoup": "^3.10.4",
"mediasoup-client": "^3.6.54", "mediasoup-client": "^3.6.54",
"parcel": "^2.7.0",
"socket.io": "^2.0.3", "socket.io": "^2.0.3",
"socket.io-client": "^2.0.3", "socket.io-client": "^2.0.3"
"nodemon": "^2.0.19",
"watchify": "^4.0.0"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^2.0.19", "nodemon": "^2.0.19",
"pm2": "^5.2.0",
"ts-node": "^10.9.1",
"ts-node-dev": "^2.0.0",
"watchify": "^4.0.0" "watchify": "^4.0.0"
} }
} }

View File

@ -20653,10 +20653,9 @@ module.exports = yeast;
},{}],95:[function(require,module,exports){ },{}],95:[function(require,module,exports){
module.exports = { module.exports = {
hubAddress: 'https://hub.dev.linx.safemobile.com/', hubAddress: 'https://hub.dev.linx.safemobile.com/',
mediasoupAddress: 'https://video.safemobile.org/mediasoup', // mediasoupAddress: 'https://video.safemobile.org/mediasoup',
mediasoupAddress: 'http://localhost:3000/mediasoup',
} }
},{}],96:[function(require,module,exports){ },{}],96:[function(require,module,exports){
const io = require('socket.io-client') const io = require('socket.io-client')
const mediasoupClient = require('mediasoup-client') const mediasoupClient = require('mediasoup-client')
@ -20666,6 +20665,8 @@ console.log('[CONFIG]', config);
const ASSET_ID = parseInt(urlParams.get('assetId')) || null; const ASSET_ID = parseInt(urlParams.get('assetId')) || null;
const ACCOUNT_ID = parseInt(urlParams.get('accountId')) || null; const ACCOUNT_ID = parseInt(urlParams.get('accountId')) || null;
const ASSET_NAME = urlParams.get('assetName') || 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
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)
@ -20675,7 +20676,12 @@ hub = io(config.hubAddress)
const connectToMediasoup = () => { const connectToMediasoup = () => {
socket = io(config.mediasoupAddress) socket = io(config.mediasoupAddress, {
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax : 5000,
reconnectionAttempts: Infinity
})
socket.on('connection-success', ({ _socketId, existsProducer }) => { socket.on('connection-success', ({ _socketId, existsProducer }) => {
console.log(`[MEDIA] ${config.mediasoupAddress} | connected: ${socket.connected} | existsProducer: ${existsProducer}`) console.log(`[MEDIA] ${config.mediasoupAddress} | connected: ${socket.connected} | existsProducer: ${existsProducer}`)
@ -20699,21 +20705,26 @@ if (IS_PRODUCER === true) {
asset_id: ASSET_ID, asset_id: ASSET_ID,
account_id: ACCOUNT_ID, account_id: ACCOUNT_ID,
}) })
); )
hub.on('video', (data) => { hub.on('video', (data) => {
const parsedData = JSON.parse(data); const parsedData = JSON.parse(data);
if (parsedData.type === 'notify-request') {
console.log('video', parsedData) console.log('video', parsedData)
originAssetId = parsedData.origin_asset_id; originAssetId = parsedData.origin_asset_id;
originAssetName = parsedData.origin_asset_name; // originAssetName = parsedData.origin_asset_name;
originAssetTypeName = parsedData.origin_asset_type_name; // originAssetTypeName = parsedData.origin_asset_type_name;
callId = parsedData.video_call_id; callId = parsedData.video_call_id;
console.log('IS_PRODUCER', IS_PRODUCER, 'callId', callId); console.log('[VIDEO] notify-request | IS_PRODUCER', IS_PRODUCER, 'callId', callId);
if (parsedData.type === 'notify-request' && IS_PRODUCER) {
getLocalStream() getLocalStream()
} }
if (parsedData.type === 'notify-end') {
console.log('[VIDEO] notify-end | IS_PRODUCER', IS_PRODUCER, 'callId', callId);
resetCallSettings()
}
}) })
}) })
@ -20739,8 +20750,8 @@ let consumerTransport
let producer let producer
let consumer let consumer
let originAssetId let originAssetId
let originAssetName // let originAssetName = 'Adi'
let originAssetTypeName // let originAssetTypeName = 'linx'
// 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
@ -20770,6 +20781,7 @@ let params = {
} }
const streamSuccess = (stream) => { const streamSuccess = (stream) => {
console.log('[streamSuccess]');
localVideo.srcObject = stream localVideo.srcObject = stream
const track = stream.getVideoTracks()[0] const track = stream.getVideoTracks()[0]
params = { params = {
@ -20796,15 +20808,17 @@ const getLocalStream = () => {
}) })
.then(streamSuccess) .then(streamSuccess)
.catch(error => { .catch(error => {
console.log(error.message) console.log('getLocalStream', error)
}) })
} }
const goConnect = () => { const goConnect = () => {
console.log('[goConnect] device:', device);
device === undefined ? getRtpCapabilities() : goCreateTransport() device === undefined ? getRtpCapabilities() : goCreateTransport()
} }
const goCreateTransport = () => { const goCreateTransport = () => {
console.log('[goCreateTransport] IS_PRODUCER:', IS_PRODUCER);
IS_PRODUCER ? createSendTransport() : createRecvTransport() IS_PRODUCER ? createSendTransport() : createRecvTransport()
} }
@ -20812,6 +20826,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
@ -20834,6 +20849,7 @@ const createDevice = async () => {
} }
const getRtpCapabilities = () => { const getRtpCapabilities = () => {
console.log('[getRtpCapabilities]');
// make a request to the server for Router RTP Capabilities // make a request to the server for Router RTP Capabilities
// see server's socket.on('getRtpCapabilities', ...) // see server's socket.on('getRtpCapabilities', ...)
// the server sends back data object which contains rtpCapabilities // the server sends back data object which contains rtpCapabilities
@ -20887,7 +20903,7 @@ const createSendTransport = () => {
}) })
producerTransport.on('produce', async (parameters, callback, errback) => { producerTransport.on('produce', async (parameters, callback, errback) => {
console.log(parameters) console.log('produce', parameters)
try { try {
// tell the server to create a Producer // tell the server to create a Producer
@ -20897,7 +20913,7 @@ const createSendTransport = () => {
await socket.emit('transport-produce', { await socket.emit('transport-produce', {
kind: parameters.kind, kind: parameters.kind,
rtpParameters: parameters.rtpParameters, rtpParameters: parameters.rtpParameters,
appData: parameters.appData, callId: callId
}, ({ id }) => { }, ({ id }) => {
// Tell the transport that parameters were transmitted and provide it with the // Tell the transport that parameters were transmitted and provide it with the
// server side producer's id. // server side producer's id.
@ -20921,7 +20937,6 @@ const connectSendTransport = async () => {
producer.on('trackended', () => { producer.on('trackended', () => {
console.log('track ended') console.log('track ended')
// close video track // close video track
}) })
@ -20935,8 +20950,8 @@ const connectSendTransport = async () => {
dest_asset_id: originAssetId || parseInt(urlParams.get('dest_asset_id')), dest_asset_id: originAssetId || parseInt(urlParams.get('dest_asset_id')),
type: 'notify-answer', type: 'notify-answer',
origin_asset_priority: 1, origin_asset_priority: 1,
origin_asset_type_name: originAssetTypeName, origin_asset_type_name: ASSET_TYPE,
origin_asset_name: originAssetName, origin_asset_name: ASSET_NAME,
video_call_id: callId, video_call_id: callId,
answer: 'accepted', // answer: 'rejected' answer: 'accepted', // answer: 'rejected'
}; };
@ -20946,6 +20961,10 @@ const connectSendTransport = async () => {
'video', 'video',
JSON.stringify(answer) JSON.stringify(answer)
); );
// Enable Close call button
const closeCallBtn = document.getElementById('btnCloseCall');
closeCallBtn.removeAttribute('disabled');
} }
const createRecvTransport = async () => { const createRecvTransport = async () => {
@ -20989,6 +21008,16 @@ const createRecvTransport = async () => {
}) })
} }
const resetCallSettings = () => {
localVideo.srcObject = null
remoteVideo.srcObject = null
consumer = null
producer = null
producerTransport = null
consumerTransport = null
device = undefined
}
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
@ -21025,6 +21054,28 @@ const connectRecvTransport = async () => {
}) })
} }
const closeCall = () => {
console.log('closeCall');
// Emit 'notify-end' to Hub so the consumer will know to close the video
const notifyEnd = {
origin_asset_id: ASSET_ID,
dest_asset_id: originAssetId || parseInt(urlParams.get('dest_asset_id')),
type: 'notify-end',
video_call_id: callId
}
console.log('notifyEnd', notifyEnd)
hub.emit('video', JSON.stringify(notifyEnd))
// Disable Close call button
const closeCallBtn = document.getElementById('btnCloseCall')
closeCallBtn.setAttribute('disabled', '')
// Reset settings
resetCallSettings()
}
btnLocalVideo.addEventListener('click', getLocalStream) btnLocalVideo.addEventListener('click', getLocalStream)
btnRecvSendTransport.addEventListener('click', goConnect) btnRecvSendTransport.addEventListener('click', goConnect)
btnCloseCall.addEventListener('click', closeCall)
},{"./config":95,"mediasoup-client":67,"socket.io-client":83}]},{},[96]); },{"./config":95,"mediasoup-client":67,"socket.io-client":83}]},{},[96]);

View File

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

View File

@ -21,6 +21,14 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
#closeCallBtn {
padding: 5;
background-color: papayawhip;
display: flex;
justify-content: center;
width: 736px;
}
</style> </style>
</head> </head>
<body> <body>
@ -55,9 +63,6 @@
<button id="btnRecvSendTransport">Consume</button> <button id="btnRecvSendTransport">Consume</button>
</div> </div>
</td> </td>
<td class="remoteColumn">
<div id="videoContainer"></div>
</td>
</tr> </tr>
<!-- <tr> <!-- <tr>
<td colspan="2"> <td colspan="2">
@ -85,6 +90,9 @@
</tr> --> </tr> -->
</tbody> </tbody>
</table> </table>
<div id="closeCallBtn">
<button id="btnCloseCall" disabled>Close Call</button>
</div>
</div> </div>
</body> </body>
</body> </body>

View File

@ -6,6 +6,8 @@ console.log('[CONFIG]', config);
const ASSET_ID = parseInt(urlParams.get('assetId')) || null; const ASSET_ID = parseInt(urlParams.get('assetId')) || null;
const ACCOUNT_ID = parseInt(urlParams.get('accountId')) || null; const ACCOUNT_ID = parseInt(urlParams.get('accountId')) || null;
const ASSET_NAME = urlParams.get('assetName') || 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
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)
@ -15,7 +17,12 @@ hub = io(config.hubAddress)
const connectToMediasoup = () => { const connectToMediasoup = () => {
socket = io(config.mediasoupAddress) socket = io(config.mediasoupAddress, {
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax : 5000,
reconnectionAttempts: Infinity
})
socket.on('connection-success', ({ _socketId, existsProducer }) => { socket.on('connection-success', ({ _socketId, existsProducer }) => {
console.log(`[MEDIA] ${config.mediasoupAddress} | connected: ${socket.connected} | existsProducer: ${existsProducer}`) console.log(`[MEDIA] ${config.mediasoupAddress} | connected: ${socket.connected} | existsProducer: ${existsProducer}`)
@ -39,21 +46,26 @@ if (IS_PRODUCER === true) {
asset_id: ASSET_ID, asset_id: ASSET_ID,
account_id: ACCOUNT_ID, account_id: ACCOUNT_ID,
}) })
); )
hub.on('video', (data) => { hub.on('video', (data) => {
const parsedData = JSON.parse(data); const parsedData = JSON.parse(data);
if (parsedData.type === 'notify-request') {
console.log('video', parsedData) console.log('video', parsedData)
originAssetId = parsedData.origin_asset_id; originAssetId = parsedData.origin_asset_id;
originAssetName = parsedData.origin_asset_name; // originAssetName = parsedData.origin_asset_name;
originAssetTypeName = parsedData.origin_asset_type_name; // originAssetTypeName = parsedData.origin_asset_type_name;
callId = parsedData.video_call_id; callId = parsedData.video_call_id;
console.log('IS_PRODUCER', IS_PRODUCER, 'callId', callId); console.log('[VIDEO] notify-request | IS_PRODUCER', IS_PRODUCER, 'callId', callId);
if (parsedData.type === 'notify-request' && IS_PRODUCER) {
getLocalStream() getLocalStream()
} }
if (parsedData.type === 'notify-end') {
console.log('[VIDEO] notify-end | IS_PRODUCER', IS_PRODUCER, 'callId', callId);
resetCallSettings()
}
}) })
}) })
@ -79,8 +91,8 @@ let consumerTransport
let producer let producer
let consumer let consumer
let originAssetId let originAssetId
let originAssetName // let originAssetName = 'Adi'
let originAssetTypeName // let originAssetTypeName = 'linx'
// 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
@ -110,6 +122,7 @@ let params = {
} }
const streamSuccess = (stream) => { const streamSuccess = (stream) => {
console.log('[streamSuccess]');
localVideo.srcObject = stream localVideo.srcObject = stream
const track = stream.getVideoTracks()[0] const track = stream.getVideoTracks()[0]
params = { params = {
@ -136,15 +149,17 @@ const getLocalStream = () => {
}) })
.then(streamSuccess) .then(streamSuccess)
.catch(error => { .catch(error => {
console.log(error.message) console.log('getLocalStream', error)
}) })
} }
const goConnect = () => { const goConnect = () => {
console.log('[goConnect] device:', device);
device === undefined ? getRtpCapabilities() : goCreateTransport() device === undefined ? getRtpCapabilities() : goCreateTransport()
} }
const goCreateTransport = () => { const goCreateTransport = () => {
console.log('[goCreateTransport] IS_PRODUCER:', IS_PRODUCER);
IS_PRODUCER ? createSendTransport() : createRecvTransport() IS_PRODUCER ? createSendTransport() : createRecvTransport()
} }
@ -152,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
@ -174,6 +190,7 @@ const createDevice = async () => {
} }
const getRtpCapabilities = () => { const getRtpCapabilities = () => {
console.log('[getRtpCapabilities]');
// make a request to the server for Router RTP Capabilities // make a request to the server for Router RTP Capabilities
// see server's socket.on('getRtpCapabilities', ...) // see server's socket.on('getRtpCapabilities', ...)
// the server sends back data object which contains rtpCapabilities // the server sends back data object which contains rtpCapabilities
@ -227,7 +244,7 @@ const createSendTransport = () => {
}) })
producerTransport.on('produce', async (parameters, callback, errback) => { producerTransport.on('produce', async (parameters, callback, errback) => {
console.log(parameters) console.log('produce', parameters)
try { try {
// tell the server to create a Producer // tell the server to create a Producer
@ -237,7 +254,7 @@ const createSendTransport = () => {
await socket.emit('transport-produce', { await socket.emit('transport-produce', {
kind: parameters.kind, kind: parameters.kind,
rtpParameters: parameters.rtpParameters, rtpParameters: parameters.rtpParameters,
appData: parameters.appData, callId: callId
}, ({ id }) => { }, ({ id }) => {
// Tell the transport that parameters were transmitted and provide it with the // Tell the transport that parameters were transmitted and provide it with the
// server side producer's id. // server side producer's id.
@ -261,7 +278,6 @@ const connectSendTransport = async () => {
producer.on('trackended', () => { producer.on('trackended', () => {
console.log('track ended') console.log('track ended')
// close video track // close video track
}) })
@ -275,8 +291,8 @@ const connectSendTransport = async () => {
dest_asset_id: originAssetId || parseInt(urlParams.get('dest_asset_id')), dest_asset_id: originAssetId || parseInt(urlParams.get('dest_asset_id')),
type: 'notify-answer', type: 'notify-answer',
origin_asset_priority: 1, origin_asset_priority: 1,
origin_asset_type_name: originAssetTypeName, origin_asset_type_name: ASSET_TYPE,
origin_asset_name: originAssetName, origin_asset_name: ASSET_NAME,
video_call_id: callId, video_call_id: callId,
answer: 'accepted', // answer: 'rejected' answer: 'accepted', // answer: 'rejected'
}; };
@ -286,6 +302,10 @@ const connectSendTransport = async () => {
'video', 'video',
JSON.stringify(answer) JSON.stringify(answer)
); );
// Enable Close call button
const closeCallBtn = document.getElementById('btnCloseCall');
closeCallBtn.removeAttribute('disabled');
} }
const createRecvTransport = async () => { const createRecvTransport = async () => {
@ -329,6 +349,16 @@ const createRecvTransport = async () => {
}) })
} }
const resetCallSettings = () => {
localVideo.srcObject = null
remoteVideo.srcObject = null
consumer = null
producer = null
producerTransport = null
consumerTransport = null
device = undefined
}
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
@ -365,5 +395,27 @@ const connectRecvTransport = async () => {
}) })
} }
const closeCall = () => {
console.log('closeCall');
// Emit 'notify-end' to Hub so the consumer will know to close the video
const notifyEnd = {
origin_asset_id: ASSET_ID,
dest_asset_id: originAssetId || parseInt(urlParams.get('dest_asset_id')),
type: 'notify-end',
video_call_id: callId
}
console.log('notifyEnd', notifyEnd)
hub.emit('video', JSON.stringify(notifyEnd))
// Disable Close call button
const closeCallBtn = document.getElementById('btnCloseCall')
closeCallBtn.setAttribute('disabled', '')
// Reset settings
resetCallSettings()
}
btnLocalVideo.addEventListener('click', getLocalStream) btnLocalVideo.addEventListener('click', getLocalStream)
btnRecvSendTransport.addEventListener('click', goConnect) btnRecvSendTransport.addEventListener('click', goConnect)
btnCloseCall.addEventListener('click', closeCall)

103
tsconfig.json Normal file
View File

@ -0,0 +1,103 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}