diff --git a/app/src/Room.js b/app/src/Room.js index 13577fb1..8df6c7b2 100644 --- a/app/src/Room.js +++ b/app/src/Room.js @@ -27,7 +27,10 @@ module.exports = class Room { this._roomPassword = null; this._hostOnlyRecording = false; // ########################## - this._recSyncServerRecording = config?.server?.recording?.enabled || false; + this.recording = { + recSyncServerRecording: config?.server?.recording?.enabled || false, + recSyncServerEndpoint: config?.server?.recording?.endpoint || '', + }; // ########################## this._moderator = { audio_start_muted: false, @@ -57,7 +60,7 @@ module.exports = class Room { return { id: this.id, broadcasting: this._isBroadcasting, - recSyncServerRecording: this._recSyncServerRecording, + recording: this.recording, config: { isLocked: this._isLocked, isLobbyEnabled: this._isLobbyEnabled, diff --git a/app/src/Server.js b/app/src/Server.js index cdefc28d..02985c7e 100644 --- a/app/src/Server.js +++ b/app/src/Server.js @@ -42,7 +42,7 @@ dependencies: { * @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon * @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970 * @author Miroslav Pejic - miroslav.pejic.85@gmail.com - * @version 1.4.45 + * @version 1.4.46 * */ diff --git a/app/src/config.template.js b/app/src/config.template.js index cf27e2a9..ff985deb 100644 --- a/app/src/config.template.js +++ b/app/src/config.template.js @@ -55,8 +55,9 @@ module.exports = { Note: if you use Docker: Create the "app/rec" directory, configure it as a volume in docker-compose.yml, ensure proper permissions, and start the Docker container. */ + enabled: true, + endpoint: '', // Change the URL if you want to save the recording to a different server or cloud service (http://localhost:8080), otherwise leave it as is (empty). dir: 'rec', - enabled: false, }, }, middleware: { diff --git a/cloud/README.md b/cloud/README.md new file mode 100644 index 00000000..1385cf95 --- /dev/null +++ b/cloud/README.md @@ -0,0 +1,27 @@ +# Cloud Recording + +![cloud](./assets/cloud.png) + +To save `recordings` on a different `server` or `cloud service` copy this `cloud folder` to the desired server. + +## Quick Start + +```bash +# Install dependencies +npm install + +# Start the server +npm start +``` + +## Edit config.js + +In the MiroTalk SFU `app/src/config.js` file, change the endpoint to send recording chunks: + +```js +recording: { + endpoint: 'http://localhost:8080', // Change it with your Server endpoint + dir: 'rec', + enabled: true, +}, +``` diff --git a/cloud/assets/cloud.png b/cloud/assets/cloud.png new file mode 100644 index 00000000..6c3958e1 Binary files /dev/null and b/cloud/assets/cloud.png differ diff --git a/cloud/package.json b/cloud/package.json new file mode 100644 index 00000000..44bf524f --- /dev/null +++ b/cloud/package.json @@ -0,0 +1,23 @@ +{ + "name": "cloud", + "version": "1.0.0", + "description": "Cloud server to handle MiroTalk SFU recording uploads", + "main": "server.js", + "scripts": { + "start": "node server.js", + "start-dev": "nodemon server.js" + }, + "keywords": [ + "cloud", + "recording" + ], + "author": "Miroslav Pejic", + "license": "AGPLv3", + "dependencies": { + "cors": "2.8.5", + "express": "^4.18.2" + }, + "devDependencies": { + "nodemon": "^3.1.3" + } +} diff --git a/cloud/server.js b/cloud/server.js new file mode 100644 index 00000000..b6cd1848 --- /dev/null +++ b/cloud/server.js @@ -0,0 +1,77 @@ +'use strict'; + +const express = require('express'); +const cors = require('cors'); +const fs = require('fs'); +const path = require('path'); +const app = express(); +const port = process.env.PORT || 8080; + +// Replace with your actual logging mechanism +const log = { + error: console.error, + debug: console.log, +}; + +// Directory where recordings will be stored +const recordingDirectory = path.join(__dirname, 'rec'); + +// Flag to enable/disable server recording +const isServerRecordingEnabled = true; + +// CORS options +const corsOptions = { + origin: '*', + methods: ['POST'], +}; + +// Middleware +app.use(express.json()); +app.use(cors(corsOptions)); + +// Ensure the recording directory exists +function ensureRecordingDirectoryExists() { + if (!fs.existsSync(recordingDirectory)) { + fs.mkdirSync(recordingDirectory, { recursive: true }); + } +} + +// Endpoint to handle recording uploads +app.post('/recSync', (req, res) => { + if (!isServerRecordingEnabled) { + return res.status(403).send('Server recording is disabled'); + } + + const { fileName } = req.query; + + if (!fileName) { + return res.status(400).send('Filename not provided'); + } + + ensureRecordingDirectoryExists(); + + const filePath = path.join(recordingDirectory, fileName); + const writeStream = fs.createWriteStream(filePath, { flags: 'a' }); + + req.pipe(writeStream); + + writeStream.on('error', (err) => { + log.error('Error writing to file:', err.message); + res.status(500).send('Internal Server Error'); + }); + + writeStream.on('finish', () => { + log.debug('File saved successfully:', fileName); + res.status(200).send('File uploaded successfully'); + }); + + req.on('error', (err) => { + log.error('Error processing request:', err.message); + res.status(500).send('Internal Server Error'); + }); +}); + +// Start the server +app.listen(port, () => { + log.debug(`Server is running on http://localhost:${port}`); +}); diff --git a/package.json b/package.json index 0e20f712..392cf073 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mirotalksfu", - "version": "1.4.45", + "version": "1.4.46", "description": "WebRTC SFU browser-based video calls", "main": "Server.js", "scripts": { @@ -58,7 +58,7 @@ "mediasoup-client": "3.7.8", "ngrok": "^5.0.0-beta.2", "nodemailer": "^6.9.13", - "openai": "^4.47.3", + "openai": "^4.48.2", "qs": "6.12.1", "socket.io": "4.7.5", "swagger-ui-express": "5.0.1", diff --git a/public/js/Room.js b/public/js/Room.js index d8c598d9..b8c95854 100644 --- a/public/js/Room.js +++ b/public/js/Room.js @@ -11,7 +11,7 @@ if (location.href.substr(0, 5) !== 'https') location.href = 'https' + location.h * @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon * @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970 * @author Miroslav Pejic - miroslav.pejic.85@gmail.com - * @version 1.4.45 + * @version 1.4.46 * */ @@ -1281,7 +1281,7 @@ function roomIsReady() { BUTTONS.settings.broadcastingButton && show(broadcastingButton); BUTTONS.settings.lobbyButton && show(lobbyButton); BUTTONS.settings.sendEmailInvitation && show(sendEmailInvitation); - if (rc.recSyncServerRecording) show(roomRecordingServer); + if (rc.recording.recSyncServerRecording) show(roomRecordingServer); BUTTONS.main.aboutButton && show(aboutButton); if (!DetectRTC.isMobileDevice) show(pinUnpinGridDiv); if (!isSpeechSynthesisSupported) hide(speechMsgDiv); @@ -2196,9 +2196,9 @@ function handleSelects() { e.target.blur(); }; switchServerRecording.onchange = (e) => { - rc.recSyncServerRecording = e.currentTarget.checked; - rc.roomMessage('recSyncServer', rc.recSyncServerRecording); - localStorageSettings.rec_server = rc.recSyncServerRecording; + rc.recording.recSyncServerRecording = e.currentTarget.checked; + rc.roomMessage('recSyncServer', rc.recording.recSyncServerRecording); + localStorageSettings.rec_server = rc.recording.recSyncServerRecording; lS.setSettings(localStorageSettings); e.target.blur(); }; diff --git a/public/js/RoomClient.js b/public/js/RoomClient.js index 978a179f..c5ae5f82 100644 --- a/public/js/RoomClient.js +++ b/public/js/RoomClient.js @@ -9,7 +9,7 @@ * @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon * @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970 * @author Miroslav Pejic - miroslav.pejic.85@gmail.com - * @version 1.4.45 + * @version 1.4.46 * */ @@ -290,7 +290,10 @@ class RoomClient { this.mediaRecorder = null; this.audioRecorder = null; this.recScreenStream = null; - this.recSyncServerRecording = false; + this.recording = { + recSyncServerRecording: false, + recSyncServerEndpoint: '', + }; this.recSyncTime = 4000; // 4 sec this.recSyncChunkSize = 1000000; // 1MB @@ -464,15 +467,16 @@ class RoomClient { } // ################################################################################################### - if (room.recSyncServerRecording) { + if (room.recording) this.recording = room.recording; + if (room.recording && room.recording.recSyncServerRecording) { console.log('07.1 WARNING ----> SERVER SYNC RECORDING ENABLED!'); - this.recSyncServerRecording = localStorageSettings.rec_server; + this.recording.recSyncServerRecording = localStorageSettings.rec_server; if (BUTTONS.settings.tabRecording && !room.config.hostOnlyRecording) { show(roomRecordingServer); } - switchServerRecording.checked = this.recSyncServerRecording; + switchServerRecording.checked = this.recording.recSyncServerRecording; } - console.log('07.1 ----> SERVER SYNC RECORDING', this.recSyncServerRecording); + console.log('07.1 ----> SERVER SYNC RECORDING', this.recording); // ################################################################################################### // Handle Room moderator rules @@ -4349,7 +4353,9 @@ class RoomClient { handleMediaRecorder() { if (this.mediaRecorder) { this.recServerFileName = this.getServerRecFileName(); - rc.recSyncServerRecording ? this.mediaRecorder.start(this.recSyncTime) : this.mediaRecorder.start(); + rc.recording.recSyncServerRecording + ? this.mediaRecorder.start(this.recSyncTime) + : this.mediaRecorder.start(); this.mediaRecorder.addEventListener('start', this.handleMediaRecorderStart); this.mediaRecorder.addEventListener('dataavailable', this.handleMediaRecorderData); this.mediaRecorder.addEventListener('stop', this.handleMediaRecorderStop); @@ -4371,7 +4377,7 @@ class RoomClient { handleMediaRecorderData(evt) { // console.log('MediaRecorder data: ', evt); if (evt.data && evt.data.size > 0) { - rc.recSyncServerRecording ? rc.syncRecordingInCloud(evt.data) : recordedBlobs.push(evt.data); + rc.recording.recSyncServerRecording ? rc.syncRecordingInCloud(evt.data) : recordedBlobs.push(evt.data); } } @@ -4382,11 +4388,15 @@ class RoomClient { for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) { const chunk = arrayBuffer.slice(chunkIndex * chunkSize, (chunkIndex + 1) * chunkSize); try { - await axios.post('/recSync?fileName=' + rc.recServerFileName, chunk, { - headers: { - 'Content-Type': 'application/octet-stream', + await axios.post( + `${this.recording.recSyncServerEndpoint}/recSync?fileName=` + rc.recServerFileName, + chunk, + { + headers: { + 'Content-Type': 'application/octet-stream', + }, }, - }); + ); } catch (error) { console.error('Error syncing chunk:', error.message); } @@ -4396,7 +4406,7 @@ class RoomClient { handleMediaRecorderStop(evt) { try { console.log('MediaRecorder stopped: ', evt); - rc.recSyncServerRecording ? rc.handleServerRecordingStop() : rc.handleLocalRecordingStop(); + rc.recording.recSyncServerRecording ? rc.handleServerRecordingStop() : rc.handleLocalRecordingStop(); rc.disableRecordingOptions(false); } catch (err) { console.error('Recording save failed', err);