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();
|