Initial commit: overlay + sender pages for StreamLabs browser source
Express + Socket.IO backend with Google login on the sender page, a transparent overlay page for OBS/StreamLabs Browser Source, and TTS voice-over via the edge-tts Python CLI (same engine as t2sTelegramBot). Dockerized for deployment. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
commit
7882db1f37
12 changed files with 1930 additions and 0 deletions
159
server.js
Normal file
159
server.js
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
require('dotenv').config();
|
||||
|
||||
const path = require('path');
|
||||
const express = require('express');
|
||||
const session = require('express-session');
|
||||
const { createServer } = require('http');
|
||||
const { Server } = require('socket.io');
|
||||
const { OAuth2Client } = require('google-auth-library');
|
||||
const { execFile } = require('child_process');
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
|
||||
const SESSION_SECRET = process.env.SESSION_SECRET;
|
||||
const TTS_VOICE = process.env.TTS_VOICE || 'ru-RU-DmitryNeural';
|
||||
|
||||
if (!GOOGLE_CLIENT_ID) {
|
||||
console.error('Missing GOOGLE_CLIENT_ID in .env (see .env.example)');
|
||||
process.exit(1);
|
||||
}
|
||||
if (!SESSION_SECRET) {
|
||||
console.error('Missing SESSION_SECRET in .env (see .env.example)');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const oauthClient = new OAuth2Client(GOOGLE_CLIENT_ID);
|
||||
|
||||
const app = express();
|
||||
const httpServer = createServer(app);
|
||||
const io = new Server(httpServer);
|
||||
|
||||
const sessionMiddleware = session({
|
||||
secret: SESSION_SECRET,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
maxAge: 1000 * 60 * 60 * 24 * 7, // 7 days
|
||||
// set to true once served over HTTPS in production
|
||||
secure: false,
|
||||
sameSite: 'lax',
|
||||
},
|
||||
});
|
||||
|
||||
app.use(sessionMiddleware);
|
||||
app.use(express.json());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// Share the express session with socket.io connections.
|
||||
const wrap = (middleware) => (socket, next) => middleware(socket.request, {}, next);
|
||||
io.use(wrap(sessionMiddleware));
|
||||
|
||||
app.get('/config', (req, res) => {
|
||||
res.json({ googleClientId: GOOGLE_CLIENT_ID });
|
||||
});
|
||||
|
||||
app.get('/me', (req, res) => {
|
||||
res.json({ user: req.session.user || null });
|
||||
});
|
||||
|
||||
app.post('/auth/google', async (req, res) => {
|
||||
const { credential } = req.body;
|
||||
if (!credential) {
|
||||
return res.status(400).json({ error: 'missing credential' });
|
||||
}
|
||||
|
||||
try {
|
||||
const ticket = await oauthClient.verifyIdToken({
|
||||
idToken: credential,
|
||||
audience: GOOGLE_CLIENT_ID,
|
||||
});
|
||||
const payload = ticket.getPayload();
|
||||
|
||||
req.session.user = {
|
||||
email: payload.email,
|
||||
name: payload.name,
|
||||
picture: payload.picture,
|
||||
};
|
||||
|
||||
console.log('[auth] logged in:', payload.email);
|
||||
res.json({ user: req.session.user });
|
||||
} catch (err) {
|
||||
console.error('Google token verification failed:', err.message);
|
||||
res.status(401).json({ error: 'invalid token' });
|
||||
}
|
||||
});
|
||||
|
||||
// Shells out to the `edge-tts` Python CLI (same engine as t2sTelegramBot uses)
|
||||
// instead of the edge-tts-node npm package, whose request-signing algorithm
|
||||
// currently gets rejected by Microsoft's servers with a 403.
|
||||
function synthesizeSpeech(text) {
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile(
|
||||
'edge-tts',
|
||||
['-t', text, '-v', TTS_VOICE],
|
||||
{ encoding: 'buffer', maxBuffer: 10 * 1024 * 1024 },
|
||||
(err, stdout) => {
|
||||
if (err) return reject(err);
|
||||
resolve(stdout);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
app.post('/auth/logout', (req, res) => {
|
||||
req.session.destroy(() => res.json({ ok: true }));
|
||||
});
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
const connectedUser = socket.request.session.user;
|
||||
console.log(`[socket] connected id=${socket.id} user=${connectedUser ? connectedUser.email : '(none)'}`);
|
||||
|
||||
socket.on('disconnect', (reason) => {
|
||||
console.log(`[socket] disconnected id=${socket.id} reason=${reason}`);
|
||||
});
|
||||
|
||||
socket.on('send_message', (text) => {
|
||||
// The session snapshot on socket.request is captured once, at connect time.
|
||||
// If the socket connected before login (page load opens it immediately),
|
||||
// it never sees a session updated later by the separate /auth/google request
|
||||
// unless we explicitly reload it from the store here.
|
||||
socket.request.session.reload((err) => {
|
||||
const user = !err && socket.request.session.user;
|
||||
console.log(`[send_message] id=${socket.id} user=${user ? user.email : '(none)'} text=${JSON.stringify(text)}`);
|
||||
|
||||
if (!user) {
|
||||
socket.emit('send_error', 'Not authenticated');
|
||||
return;
|
||||
}
|
||||
if (typeof text !== 'string' || !text.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = text.trim().slice(0, 500);
|
||||
const clientCount = io.engine.clientsCount;
|
||||
console.log(`[broadcast] "${message}" -> ${clientCount} connected client(s)`);
|
||||
io.emit('display_message', {
|
||||
text: message,
|
||||
from: user.name,
|
||||
at: Date.now(),
|
||||
});
|
||||
|
||||
// Voice is generated after the fact so the text shows up immediately
|
||||
// instead of waiting on the round trip to the TTS service.
|
||||
synthesizeSpeech(message)
|
||||
.then((audioBuffer) => {
|
||||
io.emit('display_audio', {
|
||||
audio: audioBuffer.toString('base64'),
|
||||
mimeType: 'audio/mpeg',
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('[tts] synthesis failed:', err);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
httpServer.listen(PORT, () => {
|
||||
console.log(`Listening on http://localhost:${PORT}`);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue