veza/veza-backend-api/internal/handlers/live_stream_handler.go
senke eb2862092d
Some checks failed
Backend API CI / test-unit (push) Failing after 0s
Backend API CI / test-integration (push) Failing after 0s
Frontend CI / test (push) Failing after 0s
Storybook Audit / Build & audit Storybook (push) Failing after 0s
feat(v0.10.6): Livestreaming basique F471-F476
- Backend: callbacks on_publish/on_publish_done, UpdateStreamURL, GetByStreamKey
- Nginx-RTMP: config infra, docker-compose service (profil live)
- Frontend: stream_url dans LiveStream, HLS.js dans LiveViewPlayer, état Stream terminé
- Chat: rate limit send_live_message 1 msg/3s pour rooms live_streams
- Env: RTMP_CALLBACK_SECRET, STREAM_HLS_BASE_URL, NGINX_RTMP_HOST
- Roadmap v0.10.6 marquée DONE
2026-03-10 10:21:57 +01:00

211 lines
6.3 KiB
Go

package handlers
import (
"net/http"
"os"
"strconv"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
apperrors "veza-backend-api/internal/errors"
"veza-backend-api/internal/models"
"veza-backend-api/internal/services"
)
// LiveStreamHandler handles live stream HTTP requests
type LiveStreamHandler struct {
service *services.LiveStreamService
logger *zap.Logger
}
// NewLiveStreamHandler creates a new LiveStreamHandler
func NewLiveStreamHandler(service *services.LiveStreamService, logger *zap.Logger) *LiveStreamHandler {
return &LiveStreamHandler{service: service, logger: logger}
}
// CreateLiveStreamRequest represents the request body for creating a live stream
type CreateLiveStreamRequest struct {
Title string `json:"title" binding:"required"`
Description string `json:"description"`
Category string `json:"category"`
ThumbnailURL string `json:"thumbnailUrl"`
StreamerName string `json:"streamer"`
Tags []string `json:"tags"`
}
// ListLiveStreams returns all live streams (public - optionally filter by is_live)
func (h *LiveStreamHandler) ListLiveStreams(c *gin.Context) {
var isLive *bool
if q := c.Query("is_live"); q != "" {
b, err := strconv.ParseBool(q)
if err == nil {
isLive = &b
}
}
streams, err := h.service.List(c.Request.Context(), isLive)
if err != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to list streams", err))
return
}
RespondSuccess(c, http.StatusOK, gin.H{"streams": streams})
}
// GetLiveStream returns a single live stream by ID
func (h *LiveStreamHandler) GetLiveStream(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
RespondWithAppError(c, apperrors.NewValidationError("invalid stream ID"))
return
}
stream, err := h.service.Get(c.Request.Context(), id)
if err != nil {
RespondWithAppError(c, apperrors.NewNotFoundError("stream not found"))
return
}
RespondSuccess(c, http.StatusOK, gin.H{"stream": stream})
}
// CreateLiveStream creates a new live stream (requires auth)
func (h *LiveStreamHandler) CreateLiveStream(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return
}
var req CreateLiveStreamRequest
if appErr := NewCommonHandler(h.logger).BindAndValidateJSON(c, &req); appErr != nil {
RespondWithAppError(c, appErr)
return
}
stream := &models.LiveStream{
Title: req.Title,
Description: req.Description,
Category: req.Category,
ThumbnailURL: req.ThumbnailURL,
StreamerName: req.StreamerName,
Tags: req.Tags,
}
if stream.StreamerName == "" {
stream.StreamerName = "Streamer"
}
created, err := h.service.Create(c.Request.Context(), userID, stream)
if err != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to create stream", err))
return
}
RespondSuccess(c, http.StatusCreated, gin.H{"stream": created})
}
// GetMyStreams returns the authenticated user's streams (including stream_key)
func (h *LiveStreamHandler) GetMyStreams(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return
}
streams, err := h.service.ListByUser(c.Request.Context(), userID)
if err != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to list streams", err))
return
}
type streamWithKey struct {
*models.LiveStream
StreamKey string `json:"stream_key"`
}
result := make([]streamWithKey, len(streams))
for i, s := range streams {
result[i] = streamWithKey{LiveStream: s, StreamKey: s.StreamKey}
}
RespondSuccess(c, http.StatusOK, gin.H{"streams": result})
}
// GetMyStreamKey returns the user's stream key (creates draft if none exist)
func (h *LiveStreamHandler) GetMyStreamKey(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return
}
streams, err := h.service.ListByUser(c.Request.Context(), userID)
if err != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get streams", err))
return
}
var streamKey string
if len(streams) > 0 {
streamKey = streams[0].StreamKey
} else {
draft := &models.LiveStream{Title: "My Stream"}
created, createErr := h.service.Create(c.Request.Context(), userID, draft)
if createErr != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to create stream", createErr))
return
}
streamKey = created.StreamKey
}
rtmpHost := os.Getenv("NGINX_RTMP_HOST")
if rtmpHost == "" {
rtmpHost = "stream.veza.app"
}
RespondSuccess(c, http.StatusOK, gin.H{
"stream_key": streamKey,
"rtmp_url": "rtmp://" + rtmpHost + "/live",
})
}
// RegenerateStreamKey generates a new stream key for the user's first stream
func (h *LiveStreamHandler) RegenerateStreamKey(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return
}
streams, err := h.service.ListByUser(c.Request.Context(), userID)
if err != nil || len(streams) == 0 {
RespondWithAppError(c, apperrors.NewNotFoundError("no stream found"))
return
}
newKey, err := h.service.RegenerateStreamKey(c.Request.Context(), streams[0].ID, userID)
if err != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to regenerate key", err))
return
}
rtmpHost := os.Getenv("NGINX_RTMP_HOST")
if rtmpHost == "" {
rtmpHost = "stream.veza.app"
}
RespondSuccess(c, http.StatusOK, gin.H{
"stream_key": newKey,
"rtmp_url": "rtmp://" + rtmpHost + "/live",
})
}
// UpdateLiveStream updates a live stream's metadata (ownership check)
func (h *LiveStreamHandler) UpdateLiveStream(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return
}
id, err := uuid.Parse(c.Param("id"))
if err != nil {
RespondWithAppError(c, apperrors.NewValidationError("invalid stream ID"))
return
}
var req CreateLiveStreamRequest
if appErr := NewCommonHandler(h.logger).BindAndValidateJSON(c, &req); appErr != nil {
RespondWithAppError(c, appErr)
return
}
stream := &models.LiveStream{
Title: req.Title,
Description: req.Description,
Category: req.Category,
ThumbnailURL: req.ThumbnailURL,
StreamerName: req.StreamerName,
Tags: req.Tags,
}
updated, updateErr := h.service.Update(c.Request.Context(), id, userID, stream)
if updateErr != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to update stream", updateErr))
return
}
RespondSuccess(c, http.StatusOK, gin.H{"stream": updated})
}