Some checks failed
CI/CD Pipeline / build-and-deploy (push) Failing after 2s
Streamers log in with Google on a new landing page and get a dashboard with two auto-generated, permanent links: one for their OBS/StreamLabs Browser Source, one to share with viewers. Messages are now scoped to the right streamer via Socket.IO rooms instead of broadcasting globally. Streamer records and tokens are persisted in SQLite (node:sqlite) so links survive restarts/redeploys. Also adds a Forgejo Actions pipeline mirroring t2sTelegramBot's: build, smoke-test, push to the local registry, then deploy to the prod host over SSH. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
78 lines
2.4 KiB
JavaScript
78 lines
2.4 KiB
JavaScript
const box = document.getElementById('box');
|
|
const fromEl = document.getElementById('from');
|
|
const textEl = document.getElementById('text');
|
|
|
|
const token = location.pathname.split('/').filter(Boolean).pop();
|
|
const socket = io({ query: { role: 'overlay', token } });
|
|
|
|
let hideTimer = null;
|
|
let audioCtx = null;
|
|
|
|
function getAudioCtx() {
|
|
audioCtx = audioCtx || new (window.AudioContext || window.webkitAudioContext)();
|
|
return audioCtx;
|
|
}
|
|
|
|
// Browsers suspend AudioContext until a user gesture happens on the page.
|
|
// OBS's Browser Source doesn't enforce this, but for testing in a normal
|
|
// tab, unlock it on the first click/keypress anywhere on the page.
|
|
['click', 'keydown'].forEach((eventName) => {
|
|
document.addEventListener(eventName, () => {
|
|
const ctx = getAudioCtx();
|
|
if (ctx.state === 'suspended') ctx.resume();
|
|
}, { once: true });
|
|
});
|
|
|
|
function playNotificationSound() {
|
|
try {
|
|
const ctx = getAudioCtx();
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
osc.type = 'sine';
|
|
osc.frequency.setValueAtTime(880, ctx.currentTime);
|
|
osc.frequency.setValueAtTime(1320, ctx.currentTime + 0.1);
|
|
gain.gain.setValueAtTime(0.2, ctx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.35);
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
osc.start();
|
|
osc.stop(ctx.currentTime + 0.35);
|
|
} catch (err) {
|
|
console.error('Could not play notification sound:', err);
|
|
}
|
|
}
|
|
|
|
function showMessage(text, from) {
|
|
clearTimeout(hideTimer);
|
|
|
|
fromEl.textContent = from || '';
|
|
fromEl.style.display = from ? 'block' : 'none';
|
|
textEl.textContent = text;
|
|
// force reflow so the transition re-triggers even if a message is already visible
|
|
void box.offsetWidth;
|
|
box.classList.add('visible');
|
|
|
|
playNotificationSound();
|
|
|
|
const durationMs = 10000;
|
|
hideTimer = setTimeout(() => {
|
|
box.classList.remove('visible');
|
|
}, durationMs);
|
|
}
|
|
|
|
function playSpeech(base64Audio, mimeType) {
|
|
const audio = new Audio(`data:${mimeType};base64,${base64Audio}`);
|
|
audio.play().catch((err) => console.error('Could not play TTS audio:', err));
|
|
}
|
|
|
|
socket.on('display_message', (payload) => {
|
|
showMessage(payload.text, payload.from);
|
|
});
|
|
|
|
socket.on('display_audio', (payload) => {
|
|
playSpeech(payload.audio, payload.mimeType);
|
|
});
|
|
|
|
socket.on('invalid_link', () => {
|
|
console.error('Invalid overlay link — check the URL from your dashboard.');
|
|
});
|