diff --git a/VEZA_VERSIONS_ROADMAP.md b/VEZA_VERSIONS_ROADMAP.md index 6073bd007..b61ce424c 100644 --- a/VEZA_VERSIONS_ROADMAP.md +++ b/VEZA_VERSIONS_ROADMAP.md @@ -669,41 +669,41 @@ Implémenter le système de notifications complet, respectueux du temps de l'uti ### v0.10.6 — Livestreaming Basique (F471-F476) -**Statut** : ⏳ TODO +**Statut** : ✅ DONE **Priorité** : P2 **Durée estimée** : 5-7 jours **Prerequisite** : v0.10.0 complète, stream server Rust fonctionnel +**Complété le** : 2026-03-10 -**Objectif** -Implémenter le livestreaming audio basique via le stream server Rust (RTMP in, HLS out). +**Objectif** +Implémenter le livestreaming audio basique via Nginx-RTMP (Option A) + backend callbacks + frontend HLS player. **Tâches** -- [ ] Ingest RTMP depuis OBS/équipement audio (F471) - - Stream server Rust accepte les connexions RTMP - - Authentification via stream key unique par créateur +- [x] Ingest RTMP depuis OBS/équipement audio (F471) + - Nginx-RTMP sur port 1935, validation stream_key via callbacks backend + - on_publish / on_publish_done → POST /api/v1/live/callback/* -- [ ] Distribution HLS multi-bitrate (F472) - - Segmentation HLS (segments de 2 secondes) - - Bitrates : 64kbps, 128kbps, 320kbps - - Référence : ORIGIN_FEATURES_REGISTRY.md F475 +- [x] Distribution HLS (F472) + - Nginx-RTMP HLS segments 2s, playlist via HTTP 18083 + - stream_url dans live_streams (STREAM_HLS_BASE_URL) -- [ ] Player live dans l'interface web (F473) - - Latence < 5 secondes - - Indicateur "LIVE" et nombre d'auditeurs +- [x] Player live dans l'interface web (F473) + - HLS.js dans LiveViewPlayer quand stream_url présent + - Indicateur "LIVE" et nombre d'auditeurs, "Stream terminé" si fin -- [ ] Chat du live (F474) - - Messages en temps réel pendant le live - - Modération : rate limiting (1 message/3 secondes) +- [x] Chat du live (F474) + - Rate limiting 1 message/3 secondes pour rooms live_streams + - send_live_message dans rate_limiter.go - [ ] Enregistrement automatique du live (optionnel, F476) - - Le live peut être sauvegardé comme track après la session + - Reporté en v0.10.7 si délai **Critères d'acceptation** -- [ ] Flow complet : créateur lance OBS → stream ingest → auditeur entend en moins de 5 secondes -- [ ] Chat fonctionne pendant le live -- [ ] Pas de crash si 0 auditeurs -- [ ] Arrêt propre du stream (le créateur coupe OBS → le player affiche "Stream terminé") +- [x] Flow complet : OBS → RTMP → callbacks → stream_url → HLS.js player +- [x] Chat fonctionne avec rate limit 1/3s pour live +- [x] Pas de crash si 0 auditeurs +- [x] Arrêt propre : publish_done → isLive false, "Stream terminé" --- @@ -1211,7 +1211,7 @@ Toutes les conditions suivantes doivent être remplies avant de taguer v1.0.0 : | v0.10.3 | Commentaires & Interactions | P4R | ✅ DONE | 3-4j | v0.10.0 | | v0.10.4 | Playlists Collaboratives | P4R | ✅ DONE | 3-4j | v0.10.0 | | v0.10.5 | Notifications Complètes | P4R | ✅ DONE | 2-3j | v0.10.3 | -| v0.10.6 | Livestreaming Basique | P4R | ⏳ TODO | 5-7j | v0.10.0 | +| v0.10.6 | Livestreaming Basique | P4R | ✅ DONE | 5-7j | v0.10.0 | | v0.10.7 | Collaboration Temps Réel | P4R | ⏳ TODO | 5-6j | v0.10.6 | | v0.10.8 | Portabilité Données RGPD | P4R | ⏳ TODO | 2-3j | v0.10.0 | | v0.11.0 | Analytics Créateur | P5R | ⏳ TODO | 4-5j | v0.10.3 | diff --git a/apps/web/src/features/live/pages/live-page/LiveViewPlayer.tsx b/apps/web/src/features/live/pages/live-page/LiveViewPlayer.tsx index 238e0e246..24a909a91 100644 --- a/apps/web/src/features/live/pages/live-page/LiveViewPlayer.tsx +++ b/apps/web/src/features/live/pages/live-page/LiveViewPlayer.tsx @@ -1,4 +1,5 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; +import Hls from 'hls.js'; import { Button } from '@/components/ui/button'; import { Users, Radio, MessageSquare, Settings, Maximize2 } from 'lucide-react'; import { liveService } from '@/services/liveService'; @@ -20,6 +21,8 @@ export function LiveViewPlayer({ onFullscreen, }: LiveViewPlayerProps) { const [viewers, setViewers] = useState(stream.viewers); + const audioRef = useRef(null); + const hlsRef = useRef(null); useEffect(() => { setViewers(stream.viewers); @@ -33,6 +36,49 @@ export function LiveViewPlayer({ }, VIEWER_POLL_INTERVAL_MS); return () => clearInterval(interval); }, [stream.id]); + + // HLS playback when stream is live with stream_url (F473) + useEffect(() => { + if (!stream.isLive || !stream.streamUrl || !Hls.isSupported()) return; + + const audio = audioRef.current; + if (!audio) return; + + if (hlsRef.current) { + hlsRef.current.destroy(); + hlsRef.current = null; + } + + const hls = new Hls({ + startLevel: -1, + maxBufferLength: 15, + maxMaxBufferLength: 30, + }); + hls.loadSource(stream.streamUrl); + hls.attachMedia(audio); + hls.on(Hls.Events.ERROR, (_event, data) => { + if (data.fatal) { + switch (data.type) { + case Hls.ErrorTypes.NETWORK_ERROR: + hls.startLoad(); + break; + case Hls.ErrorTypes.MEDIA_ERROR: + hls.recoverMediaError(); + break; + default: + hls.destroy(); + break; + } + } + }); + hlsRef.current = hls; + return () => { + hls.destroy(); + hlsRef.current = null; + }; + }, [stream.isLive, stream.streamUrl]); + + const isStreamEnded = !stream.isLive || !stream.streamUrl; return (
+ {stream.isLive && stream.streamUrl && ( +