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>
44 lines
1.8 KiB
Go
44 lines
1.8 KiB
Go
package api
|
|
|
|
import (
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"veza-backend-api/internal/handlers"
|
|
"veza-backend-api/internal/repositories"
|
|
"veza-backend-api/internal/services"
|
|
)
|
|
|
|
// setupLiveRoutes configure les routes live streams
|
|
func (r *APIRouter) setupLiveRoutes(router *gin.RouterGroup) {
|
|
liveStreamRepo := repositories.NewLiveStreamRepository(r.db.GormDB)
|
|
roomRepo := repositories.NewRoomRepository(r.db.GormDB)
|
|
liveStreamService := services.NewLiveStreamService(liveStreamRepo, roomRepo)
|
|
liveStreamHandler := handlers.NewLiveStreamHandler(liveStreamService, r.logger)
|
|
callbackHandler := handlers.NewLiveStreamCallbackHandler(liveStreamService, r.logger)
|
|
|
|
live := router.Group("/live")
|
|
{
|
|
// v0.10.6 F471: Nginx-RTMP callbacks (secret-protected, no auth)
|
|
live.POST("/callback/publish", callbackHandler.HandlePublish)
|
|
live.POST("/callback/publish_done", callbackHandler.HandlePublishDone)
|
|
// Protected routes (me/* MUST come before :id)
|
|
if r.config != nil && r.config.AuthMiddleware != nil {
|
|
protected := live.Group("")
|
|
protected.Use(r.config.AuthMiddleware.RequireAuth())
|
|
r.applyCSRFProtection(protected)
|
|
protected.GET("/streams/me", liveStreamHandler.GetMyStreams)
|
|
protected.GET("/streams/me/key", liveStreamHandler.GetMyStreamKey)
|
|
protected.POST("/streams/me/key/regenerate", liveStreamHandler.RegenerateStreamKey)
|
|
protected.POST("/streams", liveStreamHandler.CreateLiveStream)
|
|
protected.PUT("/streams/:id", liveStreamHandler.UpdateLiveStream)
|
|
}
|
|
// Public routes (after protected)
|
|
live.GET("/streams", liveStreamHandler.ListLiveStreams)
|
|
live.GET("/streams/:id", liveStreamHandler.GetLiveStream)
|
|
|
|
// v1.0.6: RTMP ingest health probe — lets the Go Live UI render a
|
|
// warning banner when nginx-rtmp isn't up (so artists aren't
|
|
// silently broadcasting into a void).
|
|
live.GET("/health", handlers.LiveHealthHandler(r.logger))
|
|
}
|
|
}
|