send2streamer/public/index.html
vrubelroman 8aeca230bc
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 29s
Fix TTS overlap deterministically, preserve message order, fix flaky Google login button
Overlay display duration is now computed server-side from estimated
reading time (text length / speaking rate), not from the audio
element's 'ended' event — edge-tts streams MP3 without a duration
header, and OBS's embedded browser fires 'ended' unreliably for that,
which is why the earlier event-based fix didn't hold up.

Per-streamer broadcasts are now serialized through a queue, so a
message waiting on an AI reply can no longer be overtaken by a later
message from a different viewer that doesn't need one.

Also fix the Google Sign-In button intermittently not rendering: the
GSI script tag is async/defer, and both index.html and send.html/js
called google.accounts.id.initialize() without waiting for it to
actually finish loading — a race that failed silently until the
script was cached from a previous load.
2026-07-08 18:21:57 +00:00

109 lines
3.4 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<title>Send2Streamer — сообщения от зрителей прямо на стрим</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
window.googleGsiLoaded = new Promise((resolve) => { window.resolveGoogleGsiLoaded = resolve; });
</script>
<script src="https://accounts.google.com/gsi/client" async defer onload="resolveGoogleGsiLoaded()"></script>
<style>
body {
font-family: system-ui, sans-serif;
max-width: 640px;
margin: 60px auto;
padding: 0 20px;
line-height: 1.5;
}
h1 {
font-size: 32px;
margin-bottom: 8px;
}
.subtitle {
color: #666;
font-size: 18px;
margin-bottom: 32px;
}
.steps {
background: #f6f6f6;
border-radius: 12px;
padding: 20px 24px;
margin-bottom: 32px;
}
.steps li {
margin-bottom: 8px;
}
#login-container {
margin-top: 8px;
}
#status {
margin-top: 12px;
color: #888;
font-size: 14px;
}
</style>
</head>
<body>
<h1>Send2Streamer</h1>
<p class="subtitle">Зрители пишут сообщения — они всплывают у вас на стриме голосом.</p>
<div class="steps">
<p><strong>Как это работает:</strong></p>
<ol>
<li>Вы входите через Google — сервис выдаёт вам две ссылки: для OBS/StreamLabs и для зрителей.</li>
<li>Первую вставляете в Browser Source в OBS/StreamLabs.</li>
<li>Вторую публикуете для зрителей (например, в описании канала или в чате).</li>
<li>Зритель авторизуется через Google по этой ссылке и пишет вам сообщение — оно появляется на стриме с текстом и озвучкой.</li>
</ol>
</div>
<p><strong>Войдите как стример, чтобы получить свои ссылки:</strong></p>
<div id="login-container"></div>
<div id="status"></div>
<script>
async function handleCredentialResponse(response) {
const statusEl = document.getElementById('status');
const res = await fetch('/auth/google', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: response.credential }),
});
if (res.ok) {
window.location.href = '/dashboard.html';
} else {
statusEl.textContent = 'Не удалось войти, попробуйте ещё раз';
}
}
async function init() {
const meRes = await fetch('/me');
const { user } = await meRes.json();
if (user) {
window.location.href = '/dashboard.html';
return;
}
const configRes = await fetch('/config');
const { googleClientId } = await configRes.json();
// The GSI script tag is async/defer — without waiting for it to actually
// finish loading, this can run before `google` exists (a race that fails
// silently), which is why the button sometimes only appeared after a
// reload (once the script was already cached).
await window.googleGsiLoaded;
google.accounts.id.initialize({
client_id: googleClientId,
callback: handleCredentialResponse,
});
google.accounts.id.renderButton(document.getElementById('login-container'), { theme: 'outline', size: 'large' });
}
init();
</script>
</body>
</html>