Fifth item of the v1.0.6 backlog. "Go Live" was silent when the
nginx-rtmp profile wasn't up — an artist could copy the RTMP URL +
stream key, fire up OBS, hit "Start Streaming" and broadcast into the
void with no in-UI signal that the ingest wasn't listening. The audit
flagged this 🟡 ("livestream sans feedback UI si nginx-rtmp down").
Backend (`GET /api/v1/live/health`)
* `LiveHealthHandler` TCP-dials `NGINX_RTMP_ADDR` (default
`localhost:1935`) with a 2s timeout. Reports `rtmp_reachable`,
`rtmp_addr`, a UI-safe `error` string (no raw dial target in the
body — avoids leaking internal hostnames to the browser), and
`last_check_at`.
* 15s TTL cache protected by a mutex so a burst of page loads can't
hammer the ingest. First call dials; subsequent calls within TTL
serve the cached verdict.
* Response ships `Cache-Control: private, max-age=15` so browsers
piggy-back the same quarter-minute window.
* When the dial fails the handler emits a WARN log so an operator
watching backend logs sees the outage before a user does.
* Public endpoint — no auth. The "RTMP is up / down" signal has no
sensitive payload and is useful pre-login too.
Frontend
* `useLiveHealth()` hook: react-query with 15s stale time, 1 retry,
then falls back to an optimistic `{ rtmpReachable: true }` — we'd
rather miss a banner than flash a false negative during a transient
blip on the health endpoint itself.
* `LiveRtmpHealthBanner`: amber, non-blocking banner with a Retry
button that invalidates the health query. Copy explicitly tells the
artist their stream key is still valid but broadcasting now won't
reach anyone.
* `GoLivePage` wraps `GoLiveView` in a vertical stack with the banner
above — the view itself stays unchanged (the key + instructions
remain readable even when the ingest is down).
Tests
* 3 Go tests: live listener reports reachable + Cache-Control header;
dead address reports unreachable + UI-safe error (asserts no
`127.0.0.1` leak); TTL cache survives listener teardown within
window.
* 3 Vitest tests: banner renders nothing when reachable; banner
visible + Retry enabled when unreachable; Retry invalidates the
right query key.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
59 lines
1.9 KiB
TypeScript
59 lines
1.9 KiB
TypeScript
/**
|
|
* GoLivePage — Page Go Live (wrapper with fetch stream key)
|
|
* v0.703: Fetches stream key on mount, renders GoLiveView or ErrorDisplay
|
|
*/
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { GoLiveView, GoLiveViewSkeleton } from './go-live-page/GoLiveView';
|
|
import { ErrorDisplay } from '@/components/ui/ErrorDisplay';
|
|
import { liveService } from '@/services/liveService';
|
|
import { LiveRtmpHealthBanner } from '../components/LiveRtmpHealthBanner';
|
|
|
|
export function GoLivePage() {
|
|
const [streamKey, setStreamKey] = useState<string | null>(null);
|
|
const [rtmpUrl, setRtmpUrl] = useState('rtmp://stream.veza.app/live');
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<Error | null>(null);
|
|
|
|
useEffect(() => {
|
|
liveService
|
|
.getMyStreamKey()
|
|
.then((res) => {
|
|
setStreamKey(res.stream_key);
|
|
setRtmpUrl(res.rtmp_url);
|
|
})
|
|
.catch((e) => setError(e instanceof Error ? e : new Error(String(e))))
|
|
.finally(() => setLoading(false));
|
|
}, []);
|
|
|
|
if (loading) return <GoLiveViewSkeleton />;
|
|
if (error)
|
|
return (
|
|
<div className="min-h-layout-page flex items-center justify-center p-6">
|
|
<ErrorDisplay error={error} onRetry={() => window.location.reload()} />
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<LiveRtmpHealthBanner />
|
|
<GoLiveView
|
|
streamKey={streamKey}
|
|
rtmpUrl={rtmpUrl}
|
|
onCreateStream={async (data) => {
|
|
await liveService.createStream(data);
|
|
const res = await liveService.getMyStreamKey();
|
|
setStreamKey(res.stream_key);
|
|
setRtmpUrl(res.rtmp_url);
|
|
}}
|
|
onRegenerateKey={async () => {
|
|
const res = await liveService.regenerateStreamKey();
|
|
setStreamKey(res.stream_key);
|
|
}}
|
|
isLoading={false}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default GoLivePage;
|