const express = require('express'); const http = require('http'); const socketIo = require('socket.io'); const multer = require('multer'); const fs = require('fs'); const path = require('path'); const { OBSWebSocket } = require('obs-websocket-js'); const app = express(); const server = http.createServer(app); const io = socketIo(server); const obs = new OBSWebSocket(); const OBS_HOST = 'localhost'; // If OBS is running on the same machine const OBS_PORT = 4455; // Default WebSocket port const OBS_PASSWORD = 'jBHD5lLvKtHIjIwC'; // Add your WebSocket password here if set // Set up file storage for uploaded speeches const upload = multer({ dest: 'uploads/' }); let speechText = []; let currentPosition = 0; // File to store speech lines const SPEECH_LINES_FILE = 'speechLines.txt'; const SRT_FILE_NAME = 'captions.srt'; // Serve static files (HTML views) app.use(express.static(path.join(__dirname, 'public'))); // Connect to OBS WebSocket async function connectToOBS() { try { await obs.connect(`ws://${OBS_HOST}:${OBS_PORT}`, OBS_PASSWORD); console.log('Connected to OBS WebSocket'); } catch (error) { console.error('Failed to connect to OBS WebSocket:', error); } } connectToOBS(); // Load speech lines from file function loadSpeechLines() { if (fs.existsSync(SPEECH_LINES_FILE)) { const fileContents = fs.readFileSync(SPEECH_LINES_FILE, 'utf-8'); speechText = fileContents.split('\n').map(line => line.trim()).filter(line => line.length > 0); currentPosition = 0; // Reset the position to the start console.log('Loaded speech lines from file.'); } } // Load speech lines when the server starts loadSpeechLines(); // Serve speaker view app.get('/speaker', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'speaker.html')); }); // Serve audience/projector view app.get('/audience', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'audience.html')); }); // Endpoint to handle file upload app.post('/upload', upload.single('speech'), (req, res) => { const filePath = path.join(process.cwd(), req.file.path); const fileContents = fs.readFileSync(filePath, 'utf-8'); speechText = fileContents.split('\n').map(line => line.trim()).filter(line => line.length > 0); currentPosition = 0; // Save the loaded speech lines to a file fs.writeFileSync(SPEECH_LINES_FILE, speechText.join('\n')); io.emit('load_speech', speechText); res.send('File uploaded and speech loaded.'); }); // Handle WebSocket connections io.on('connection', (socket) => { console.log('Client connected'); // Send the full speech to any newly connected client if (speechText.length > 0) { socket.emit('load_speech', speechText); } // Handle position updates from the speaker view socket.on('update_position', (newPosition) => { currentPosition = newPosition; io.emit('update_caption', currentPosition); // Send to all clients const currentLine = speechText[currentPosition]; updateOBSSubtitle(currentLine); generateSRT(currentLine); }); }); async function updateOBSSubtitle(captionText) { try { await obs.call('SendStreamCaption', { captionText: captionText }); console.log('Updated OBS Text Source:', captionText); } catch (error) { console.error('Error updating OBS Text Source:', error); }0 } async function generateSRT(captionText) { try { // Check if recording is enabled const recordStatus = await obs.call('GetRecordStatus'); if (recordStatus && recordStatus.outputActive) { // Get the recording timecode const seconds = recordStatus.outputDuration / 1000; // Assuming this returns a time in seconds // Format timecode for SRT (HH:MM:SS,ms) const startTime = formatTimecode(seconds); const endTime = formatTimecode(seconds + 5); // Assume each line is displayed for 5 seconds // Create SRT entry const srtEntry = `${currentPosition + 1}\n${startTime} --> ${endTime}\n${captionText}\n\n`; fs.appendFileSync(SRT_FILE_NAME, srtEntry); // Append to SRT file console.log('Updated SRT file:', SRT_FILE_NAME); } } catch (error) { console.error('Error generating SRT:', error); } } function formatTimecode(seconds) { const date = new Date(0); date.setSeconds(seconds); const hours = String(date.getUTCHours()).padStart(2, '0'); const minutes = String(date.getUTCMinutes()).padStart(2, '0'); const secs = String(date.getUTCSeconds()).padStart(2, '0'); const millis = String((seconds % 1) * 1000).padStart(3, '0'); return `${hours}:${minutes}:${secs},${millis}`; } const PORT = process.env.PORT || 3000; server.listen(PORT, () => { console.log(`Server running on port ${PORT}`); }); const ngrok = require("@ngrok/ngrok"); async function startTunnel() { const listener = await ngrok.forward({ addr: PORT, authtoken: "2nCZjA9FOMFVdHAhIDyI0Tn8y0A_5oFkSL7kXve8RcCeJMXKU", domain: "explicitly-trusty-javelin.ngrok-free.app", proto: "http" }); console.log(`Ingress established at: ${listener.url()}`); } startTunnel();