All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 29s
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.
109 lines
3.4 KiB
HTML
109 lines
3.4 KiB
HTML
<!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>
|