all
This commit is contained in:
577
Video/MiroTalk SFU/app/src/Room.js
Normal file
577
Video/MiroTalk SFU/app/src/Room.js
Normal file
@ -0,0 +1,577 @@
|
||||
'use strict';
|
||||
|
||||
const config = require('./config');
|
||||
const Logger = require('./Logger');
|
||||
const log = new Logger('Room');
|
||||
|
||||
module.exports = class Room {
|
||||
constructor(room_id, worker, io) {
|
||||
this.id = room_id;
|
||||
this.worker = worker;
|
||||
this.webRtcServer = worker.appData.webRtcServer;
|
||||
this.webRtcServerActive = config.mediasoup.webRtcServerActive;
|
||||
this.io = io;
|
||||
this.audioLevelObserver = null;
|
||||
this.audioLevelObserverEnabled = true;
|
||||
this.audioLastUpdateTime = 0;
|
||||
// ##########################
|
||||
this._isBroadcasting = false;
|
||||
// ##########################
|
||||
this._isLocked = false;
|
||||
this._isLobbyEnabled = false;
|
||||
this._roomPassword = null;
|
||||
this._hostOnlyRecording = false;
|
||||
// ##########################
|
||||
this._recSyncServerRecording = config?.server?.recording?.enabled || false;
|
||||
// ##########################
|
||||
this._moderator = {
|
||||
audio_start_muted: false,
|
||||
video_start_hidden: false,
|
||||
audio_cant_unmute: false,
|
||||
video_cant_unhide: false,
|
||||
screen_cant_share: false,
|
||||
chat_cant_privately: false,
|
||||
chat_cant_chatgpt: false,
|
||||
};
|
||||
this.survey = config.survey;
|
||||
this.redirect = config.redirect;
|
||||
this.peers = new Map();
|
||||
this.bannedPeers = [];
|
||||
this.webRtcTransport = config.mediasoup.webRtcTransport;
|
||||
this.router = null;
|
||||
this.routerSettings = config.mediasoup.router;
|
||||
this.createTheRouter();
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// ROOM INFO
|
||||
// ####################################################
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
id: this.id,
|
||||
broadcasting: this._isBroadcasting,
|
||||
recSyncServerRecording: this._recSyncServerRecording,
|
||||
config: {
|
||||
isLocked: this._isLocked,
|
||||
isLobbyEnabled: this._isLobbyEnabled,
|
||||
hostOnlyRecording: this._hostOnlyRecording,
|
||||
},
|
||||
moderator: this._moderator,
|
||||
survey: this.survey,
|
||||
redirect: this.redirect,
|
||||
peers: JSON.stringify([...this.peers]),
|
||||
};
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// ROUTER
|
||||
// ####################################################
|
||||
|
||||
createTheRouter() {
|
||||
const { mediaCodecs } = this.routerSettings;
|
||||
this.worker
|
||||
.createRouter({
|
||||
mediaCodecs,
|
||||
})
|
||||
.then((router) => {
|
||||
this.router = router;
|
||||
if (this.audioLevelObserverEnabled) {
|
||||
this.startAudioLevelObservation();
|
||||
}
|
||||
this.router.observer.on('close', () => {
|
||||
log.info('---------------> Router is now closed as the last peer has left the room', {
|
||||
room: this.id,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
closeRouter() {
|
||||
this.router.close();
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// PRODUCER AUDIO LEVEL OBSERVER
|
||||
// ####################################################
|
||||
|
||||
async startAudioLevelObservation() {
|
||||
log.debug('Start audioLevelObserver for signaling active speaker...');
|
||||
|
||||
this.audioLevelObserver = await this.router.createAudioLevelObserver({
|
||||
maxEntries: 1,
|
||||
threshold: -70,
|
||||
interval: 100,
|
||||
});
|
||||
|
||||
this.audioLevelObserver.on('volumes', (volumes) => {
|
||||
this.sendActiveSpeakerVolume(volumes);
|
||||
});
|
||||
this.audioLevelObserver.on('silence', () => {
|
||||
//log.debug('audioLevelObserver', { volume: 'silence' });
|
||||
});
|
||||
}
|
||||
|
||||
sendActiveSpeakerVolume(volumes) {
|
||||
try {
|
||||
if (!Array.isArray(volumes) || volumes.length === 0) {
|
||||
throw new Error('Invalid volumes array');
|
||||
}
|
||||
|
||||
if (Date.now() > this.audioLastUpdateTime + 100) {
|
||||
this.audioLastUpdateTime = Date.now();
|
||||
|
||||
const { producer, volume } = volumes[0];
|
||||
const audioVolume = Math.round(Math.pow(10, volume / 70) * 10); // Scale volume to 1-10
|
||||
|
||||
if (audioVolume > 1) {
|
||||
this.peers.forEach((peer) => {
|
||||
const { id, peer_audio, peer_name } = peer;
|
||||
peer.producers.forEach((peerProducer) => {
|
||||
if (peerProducer.id === producer.id && peerProducer.kind === 'audio' && peer_audio) {
|
||||
const data = {
|
||||
peer_id: id,
|
||||
peer_name: peer_name,
|
||||
audioVolume: audioVolume,
|
||||
};
|
||||
// Uncomment the following line for debugging
|
||||
// log.debug('Sending audio volume', data);
|
||||
this.sendToAll('audioVolume', data);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('Error sending active speaker volume', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
addProducerToAudioLevelObserver(producer) {
|
||||
if (this.audioLevelObserverEnabled) {
|
||||
this.audioLevelObserver.addProducer(producer);
|
||||
}
|
||||
}
|
||||
|
||||
getRtpCapabilities() {
|
||||
return this.router.rtpCapabilities;
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// ROOM MODERATOR
|
||||
// ####################################################
|
||||
|
||||
updateRoomModeratorALL(data) {
|
||||
this._moderator = data;
|
||||
log.debug('Update room moderator all data', this._moderator);
|
||||
}
|
||||
|
||||
updateRoomModerator(data) {
|
||||
log.debug('Update room moderator', data);
|
||||
switch (data.type) {
|
||||
case 'audio_start_muted':
|
||||
this._moderator.audio_start_muted = data.status;
|
||||
break;
|
||||
case 'video_start_hidden':
|
||||
this._moderator.video_start_hidden = data.status;
|
||||
case 'audio_cant_unmute':
|
||||
this._moderator.audio_cant_unmute = data.status;
|
||||
break;
|
||||
case 'video_cant_unhide':
|
||||
this._moderator.video_cant_unhide = data.status;
|
||||
case 'screen_cant_share':
|
||||
this._moderator.screen_cant_share = data.status;
|
||||
break;
|
||||
case 'chat_cant_privately':
|
||||
this._moderator.chat_cant_privately = data.status;
|
||||
break;
|
||||
case 'chat_cant_chatgpt':
|
||||
this._moderator.chat_cant_chatgpt = data.status;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// PEERS
|
||||
// ####################################################
|
||||
|
||||
addPeer(peer) {
|
||||
this.peers.set(peer.id, peer);
|
||||
}
|
||||
|
||||
getPeer(socket_id) {
|
||||
//
|
||||
if (!this.peers.has(socket_id)) {
|
||||
log.error('---> Peer not found for socket ID', socket_id);
|
||||
return null;
|
||||
}
|
||||
|
||||
const peer = this.peers.get(socket_id);
|
||||
|
||||
return peer;
|
||||
}
|
||||
|
||||
getPeers() {
|
||||
return this.peers;
|
||||
}
|
||||
|
||||
getPeersCount() {
|
||||
return this.peers.size;
|
||||
}
|
||||
|
||||
getProducerListForPeer() {
|
||||
const producerList = [];
|
||||
this.peers.forEach((peer) => {
|
||||
const { peer_name, peer_info } = peer;
|
||||
peer.producers.forEach((producer) => {
|
||||
producerList.push({
|
||||
producer_id: producer.id,
|
||||
peer_name: peer_name,
|
||||
peer_info: peer_info,
|
||||
type: producer.appData.mediaType,
|
||||
});
|
||||
});
|
||||
});
|
||||
return producerList;
|
||||
}
|
||||
|
||||
async removePeer(socket_id) {
|
||||
const peer = this.getPeer(socket_id);
|
||||
|
||||
if (!peer || typeof peer !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { id, peer_name } = peer;
|
||||
|
||||
peer.close();
|
||||
|
||||
this.peers.delete(socket_id);
|
||||
|
||||
if (this.getPeers().size === 0) {
|
||||
this.closeRouter();
|
||||
}
|
||||
|
||||
const peerTransports = peer.getTransports();
|
||||
const peerProducers = peer.getProducers();
|
||||
const peerConsumers = peer.getConsumers();
|
||||
|
||||
log.debug('REMOVE PEER', {
|
||||
peer_id: id,
|
||||
peer_name: peer_name,
|
||||
peerTransports: peerTransports,
|
||||
peerProducers: peerProducers,
|
||||
peerConsumers: peerConsumers,
|
||||
});
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// WebRTC TRANSPORT
|
||||
// ####################################################
|
||||
|
||||
async createWebRtcTransport(socket_id) {
|
||||
const { maxIncomingBitrate, initialAvailableOutgoingBitrate, listenInfos } = this.webRtcTransport;
|
||||
|
||||
const webRtcTransportOptions = {
|
||||
...(this.webRtcServerActive ? { webRtcServer: this.webRtcServer } : { listenInfos: listenInfos }),
|
||||
enableUdp: true,
|
||||
enableTcp: true,
|
||||
preferUdp: true,
|
||||
iceConsentTimeout: 20,
|
||||
initialAvailableOutgoingBitrate,
|
||||
};
|
||||
|
||||
log.debug('webRtcTransportOptions ----->', webRtcTransportOptions);
|
||||
|
||||
const transport = await this.router.createWebRtcTransport(webRtcTransportOptions);
|
||||
|
||||
if (!transport) {
|
||||
return this.callback('[Room|createWebRtcTransport] Failed to create WebRTC transport');
|
||||
}
|
||||
|
||||
const { id, iceParameters, iceCandidates, dtlsParameters } = transport;
|
||||
|
||||
if (maxIncomingBitrate) {
|
||||
try {
|
||||
await transport.setMaxIncomingBitrate(maxIncomingBitrate);
|
||||
} catch (error) {
|
||||
log.debug('Transport setMaxIncomingBitrate error', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
const peer = this.getPeer(socket_id);
|
||||
|
||||
if (!peer || typeof peer !== 'object') {
|
||||
return this.callback(`[Room|createWebRtcTransport] Peer object not found for socket ID: ${socket_id}`);
|
||||
}
|
||||
|
||||
const { peer_name } = peer;
|
||||
|
||||
transport.on('icestatechange', (iceState) => {
|
||||
if (iceState === 'disconnected' || iceState === 'closed') {
|
||||
log.debug('Transport closed "icestatechange" event', {
|
||||
peer_name: peer_name,
|
||||
transport_id: id,
|
||||
iceState: iceState,
|
||||
});
|
||||
transport.close();
|
||||
}
|
||||
});
|
||||
|
||||
transport.on('sctpstatechange', (sctpState) => {
|
||||
log.debug('Transport "sctpstatechange" event', {
|
||||
peer_name: peer_name,
|
||||
transport_id: id,
|
||||
sctpState: sctpState,
|
||||
});
|
||||
});
|
||||
|
||||
transport.on('dtlsstatechange', (dtlsState) => {
|
||||
if (dtlsState === 'failed' || dtlsState === 'closed') {
|
||||
log.debug('Transport closed "dtlsstatechange" event', {
|
||||
peer_name: peer_name,
|
||||
transport_id: id,
|
||||
dtlsState: dtlsState,
|
||||
});
|
||||
transport.close();
|
||||
}
|
||||
});
|
||||
|
||||
transport.on('close', () => {
|
||||
log.debug('Transport closed', { peer_name: peer_name, transport_id: transport.id });
|
||||
});
|
||||
|
||||
peer.addTransport(transport);
|
||||
|
||||
log.debug('Transport created', { transportId: id });
|
||||
|
||||
return {
|
||||
id: id,
|
||||
iceParameters: iceParameters,
|
||||
iceCandidates: iceCandidates,
|
||||
dtlsParameters: dtlsParameters,
|
||||
};
|
||||
}
|
||||
|
||||
async connectPeerTransport(socket_id, transport_id, dtlsParameters) {
|
||||
try {
|
||||
if (!socket_id || !transport_id || !dtlsParameters) {
|
||||
return this.callback('[Room|connectPeerTransport] Invalid input parameters');
|
||||
}
|
||||
|
||||
const peer = this.getPeer(socket_id);
|
||||
|
||||
if (!peer || typeof peer !== 'object') {
|
||||
return this.callback(`[Room|connectPeerTransport] Peer object not found for socket ID: ${socket_id}`);
|
||||
}
|
||||
|
||||
const connectTransport = await peer.connectTransport(transport_id, dtlsParameters);
|
||||
|
||||
if (!connectTransport) {
|
||||
return this.callback(`[Room|connectPeerTransport] error: Transport with ID ${transport_id} not found`);
|
||||
}
|
||||
|
||||
return '[Room|connectPeerTransport] done';
|
||||
} catch (error) {
|
||||
log.error('Error connecting peer transport', error.message);
|
||||
return this.callback(`[Room|connectPeerTransport] error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// PRODUCE
|
||||
// ####################################################
|
||||
|
||||
async produce(socket_id, producerTransportId, rtpParameters, kind, type) {
|
||||
//
|
||||
if (!socket_id || !producerTransportId || !rtpParameters || !kind || !type) {
|
||||
return this.callback('[Room|produce] Invalid input parameters');
|
||||
}
|
||||
|
||||
const peer = this.getPeer(socket_id);
|
||||
|
||||
if (!peer || typeof peer !== 'object') {
|
||||
return this.callback(`[Room|produce] Peer object not found for socket ID: ${socket_id}`);
|
||||
}
|
||||
|
||||
const peerProducer = await peer.createProducer(producerTransportId, rtpParameters, kind, type);
|
||||
|
||||
if (!peerProducer || !peerProducer.id) {
|
||||
return this.callback(`[Room|produce] Peer producer error: '${peerProducer}'`);
|
||||
}
|
||||
|
||||
const { id } = peerProducer;
|
||||
|
||||
const { peer_name, peer_info } = peer;
|
||||
|
||||
this.broadCast(socket_id, 'newProducers', [
|
||||
{
|
||||
producer_id: id,
|
||||
producer_socket_id: socket_id,
|
||||
peer_name: peer_name,
|
||||
peer_info: peer_info,
|
||||
type: type,
|
||||
},
|
||||
]);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
closeProducer(socket_id, producer_id) {
|
||||
if (!socket_id || !producer_id) return;
|
||||
|
||||
const peer = this.getPeer(socket_id);
|
||||
|
||||
if (!peer || typeof peer !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
peer.closeProducer(producer_id);
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// CONSUME
|
||||
// ####################################################
|
||||
|
||||
async consume(socket_id, consumer_transport_id, producer_id, rtpCapabilities) {
|
||||
try {
|
||||
if (!socket_id || !consumer_transport_id || !producer_id || !rtpCapabilities) {
|
||||
return this.callback('[Room|consume] Invalid input parameters');
|
||||
}
|
||||
|
||||
if (!this.router.canConsume({ producerId: producer_id, rtpCapabilities })) {
|
||||
log.warn('Cannot consume', {
|
||||
socket_id,
|
||||
consumer_transport_id,
|
||||
producer_id,
|
||||
});
|
||||
return this.callback(`[Room|consume] Room router cannot consume producer_id: '${producer_id}'`);
|
||||
}
|
||||
|
||||
const peer = this.getPeer(socket_id);
|
||||
|
||||
if (!peer || typeof peer !== 'object') {
|
||||
return this.callback(`[Room|consume] Peer object not found for socket ID: ${socket_id}`);
|
||||
}
|
||||
|
||||
const peerConsumer = await peer.createConsumer(consumer_transport_id, producer_id, rtpCapabilities);
|
||||
|
||||
if (!peerConsumer || !peerConsumer.consumer || !peerConsumer.params) {
|
||||
log.debug('peerConsumer or params are not defined');
|
||||
return this.callback(`[Room|consume] peerConsumer error: '${peerConsumer}'`);
|
||||
}
|
||||
|
||||
const { consumer, params } = peerConsumer;
|
||||
|
||||
const { id, kind } = consumer;
|
||||
|
||||
consumer.on('producerclose', () => {
|
||||
log.debug('Consumer closed due to "producerclose" event');
|
||||
peer.removeConsumer(id);
|
||||
|
||||
// Notify the client that consumer is closed
|
||||
this.send(socket_id, 'consumerClosed', {
|
||||
consumer_id: id,
|
||||
consumer_kind: kind,
|
||||
});
|
||||
});
|
||||
|
||||
return params;
|
||||
} catch (error) {
|
||||
log.error('Error occurred during consumption', error.message);
|
||||
return this.callback(`[Room|consume] ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// HANDLE BANNED PEERS
|
||||
// ####################################################
|
||||
|
||||
addBannedPeer(uuid) {
|
||||
if (!this.bannedPeers.includes(uuid)) {
|
||||
this.bannedPeers.push(uuid);
|
||||
log.debug('Added to the banned list', {
|
||||
uuid: uuid,
|
||||
banned: this.bannedPeers,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
isBanned(uuid) {
|
||||
return this.bannedPeers.includes(uuid);
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// ROOM STATUS
|
||||
// ####################################################
|
||||
|
||||
// GET
|
||||
isBroadcasting() {
|
||||
return this._isBroadcasting;
|
||||
}
|
||||
getPassword() {
|
||||
return this._roomPassword;
|
||||
}
|
||||
|
||||
// BOOL
|
||||
isLocked() {
|
||||
return this._isLocked;
|
||||
}
|
||||
isLobbyEnabled() {
|
||||
return this._isLobbyEnabled;
|
||||
}
|
||||
isHostOnlyRecording() {
|
||||
return this._hostOnlyRecording;
|
||||
}
|
||||
|
||||
// SET
|
||||
setIsBroadcasting(status) {
|
||||
this._isBroadcasting = status;
|
||||
}
|
||||
setLocked(status, password) {
|
||||
this._isLocked = status;
|
||||
this._roomPassword = password;
|
||||
}
|
||||
setLobbyEnabled(status) {
|
||||
this._isLobbyEnabled = status;
|
||||
}
|
||||
setHostOnlyRecording(status) {
|
||||
this._hostOnlyRecording = status;
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// ERRORS
|
||||
// ####################################################
|
||||
|
||||
callback(message) {
|
||||
return { error: message };
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// SENDER
|
||||
// ####################################################
|
||||
|
||||
broadCast(socket_id, action, data) {
|
||||
for (let otherID of Array.from(this.peers.keys()).filter((id) => id !== socket_id)) {
|
||||
this.send(otherID, action, data);
|
||||
}
|
||||
}
|
||||
|
||||
sendTo(socket_id, action, data) {
|
||||
for (let peer_id of Array.from(this.peers.keys()).filter((id) => id === socket_id)) {
|
||||
this.send(peer_id, action, data);
|
||||
}
|
||||
}
|
||||
|
||||
sendToAll(action, data) {
|
||||
for (let peer_id of Array.from(this.peers.keys())) {
|
||||
this.send(peer_id, action, data);
|
||||
}
|
||||
}
|
||||
|
||||
send(socket_id, action, data) {
|
||||
this.io.to(socket_id).emit(action, data);
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user