- V0_503_RELEASE_SCOPE.md: scope complet (4 lots SS1/CH1/CL1/QA1) - PLAN_V0_503_IMPLEMENTATION.md: plan détaillé 5 sprints, 39 tâches - SCOPE_CONTROL.md: références mises à jour v0.502 → v0.503 - PROJECT_STATE.md: prochaine version v0.503, stack technique corrigée - FEATURE_STATUS.md: chat Go opérationnel, HLS en intégration v0.503 - .cursorrules: scope autorisé v0.503 (SS1, CH1, CL1, QA1)
833 lines
26 KiB
Markdown
833 lines
26 KiB
Markdown
# Plan d'implémentation v0.503 — Stream Server E2E + Chat Hardening + Cleanup
|
|
|
|
**Date** : 2026-02-22
|
|
**Base** : v0.502 taguée
|
|
**Durée estimée** : 5 sprints (~25 jours ouvrés)
|
|
**Référence** : [V0_503_RELEASE_SCOPE.md](V0_503_RELEASE_SCOPE.md)
|
|
|
|
---
|
|
|
|
## Vue d'ensemble
|
|
|
|
```
|
|
Sprint 1 (j1-5) → CL1 : Cleanup chat-server Rust (CI/CD, config, archivage)
|
|
Sprint 2 (j6-12) → CH1 : Chat hardening (Redis rate limiter, présence, FTS)
|
|
Sprint 3 (j13-18) → SS1a : Stream server HLS backend (endpoints, transcoding)
|
|
Sprint 4 (j19-23) → SS1b : Stream server HLS frontend (player, ABR, stories)
|
|
Sprint 5 (j24-25) → QA1 : Tests, documentation, tag
|
|
```
|
|
|
|
---
|
|
|
|
## Diagramme d'architecture cible
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
Browser["Browser (React)"]
|
|
|
|
subgraph GoBackend["Go Backend (veza-backend-api)"]
|
|
GinRouter["Gin Router"]
|
|
HLSHandler["HLS Handler"]
|
|
StreamSvc["StreamService"]
|
|
ChatHub["Chat Hub (WebSocket)"]
|
|
PresenceSvc["PresenceService (Redis)"]
|
|
RateLimiter["RateLimiter (Redis)"]
|
|
MsgRepo["ChatMessageRepository (FTS)"]
|
|
end
|
|
|
|
subgraph StreamServer["Stream Server (Rust)"]
|
|
HLSGen["HLS Generator"]
|
|
Transcoder["Transcoding Engine"]
|
|
FFmpeg["FFmpeg"]
|
|
end
|
|
|
|
subgraph Storage["Storage"]
|
|
PostgreSQL["PostgreSQL 16"]
|
|
Redis["Redis 7"]
|
|
MinIO["MinIO (S3)"]
|
|
end
|
|
|
|
Browser -->|"GET /api/v1/tracks/:id/hls/*"| GinRouter
|
|
Browser -->|"WS /api/v1/ws"| ChatHub
|
|
GinRouter --> HLSHandler
|
|
HLSHandler -->|"proxy"| HLSGen
|
|
StreamSvc -->|"POST /internal/jobs/transcode"| Transcoder
|
|
Transcoder --> FFmpeg
|
|
FFmpeg -->|"segments .ts + .m3u8"| MinIO
|
|
HLSGen -->|"read segments"| MinIO
|
|
ChatHub --> RateLimiter
|
|
ChatHub --> PresenceSvc
|
|
ChatHub --> MsgRepo
|
|
RateLimiter --> Redis
|
|
PresenceSvc --> Redis
|
|
MsgRepo -->|"ts_query FTS"| PostgreSQL
|
|
```
|
|
|
|
---
|
|
|
|
## Sprint 1 — Cleanup chat-server Rust (jours 1-5)
|
|
|
|
> **Objectif** : Supprimer toute trace opérationnelle du chat server Rust supprimé en v0.502.
|
|
|
|
### Tâche CL1-01 : Archiver le dossier `veza-chat-server/`
|
|
|
|
**Action** : Supprimer le dossier du repo (l'historique Git le préserve).
|
|
|
|
```bash
|
|
rm -rf veza-chat-server/
|
|
```
|
|
|
|
**Fichiers** : `veza-chat-server/` (~78 fichiers Rust + Cargo.toml + Dockerfile)
|
|
|
|
### Tâche CL1-02 : Nettoyer CI workflow `ci.yml`
|
|
|
|
**Fichier** : `.github/workflows/ci.yml`
|
|
|
|
Supprimer les sections qui buildent/auditent/lintent/testent le chat-server :
|
|
- Lignes ~84-101 : `Auditing Chat Server`, `cd veza-chat-server`, `cargo audit`
|
|
- Lignes ~95-101 : `--filter=veza-chat-server` dans `turbo run lint`, `turbo run build`, `turbo run test`
|
|
|
|
**Résultat** : Conserver uniquement `--filter=veza-stream-server` dans les commandes turbo.
|
|
|
|
### Tâche CL1-03 : Nettoyer CD workflow `cd.yml`
|
|
|
|
**Fichier** : `.github/workflows/cd.yml`
|
|
|
|
Supprimer toutes les références à `veza-chat-server` :
|
|
- Ligne 41 : `docker build -t veza-chat-server:...`
|
|
- Lignes 60-66 : Trivy scan `veza-chat-server`
|
|
- Lignes 79, 92, 110 : Retirer `veza-chat-server` des boucles `for svc in`
|
|
- Ligne 120 : Retirer la ligne summary chat-server
|
|
- Ligne 137 : Retirer `veza-chat-server` de la boucle deploy
|
|
|
|
### Tâche CL1-04 : Nettoyer `rust-ci.yml`
|
|
|
|
**Fichier** : `.github/workflows/rust-ci.yml`
|
|
|
|
Supprimer les paths `veza-chat-server/**` et le job `clippy-chat` (lignes ~4-24).
|
|
|
|
### Tâche CL1-05 : Nettoyer `chat-ci.yml`
|
|
|
|
**Fichier** : `.github/workflows/chat-ci.yml`
|
|
|
|
Supprimer entièrement ce workflow (il est dédié au chat-server Rust).
|
|
|
|
### Tâche CL1-06 : Nettoyer Prometheus config
|
|
|
|
**Fichier** : `config/prometheus.yml`
|
|
|
|
Supprimer le job `veza-chat` (lignes 18-21) :
|
|
```yaml
|
|
- job_name: 'veza-chat'
|
|
static_configs:
|
|
- targets: ['chat-server:8081']
|
|
metrics_path: '/metrics'
|
|
```
|
|
|
|
### Tâche CL1-07 : Nettoyer Caddyfile staging
|
|
|
|
**Fichier** : `config/caddy/Caddyfile.staging`
|
|
|
|
Remplacer `reverse_proxy /ws chat-server:8081` par `# Chat WebSocket is now handled by Go backend at /api/v1/ws`.
|
|
|
|
### Tâche CL1-08 : Nettoyer HAProxy config
|
|
|
|
**Fichier** : `config/haproxy/haproxy.cfg`
|
|
|
|
Supprimer la section backend chat (`server chat1 chat-server:3000`, lignes ~90-95).
|
|
|
|
### Tâche CL1-09 : Nettoyer scripts Incus
|
|
|
|
**Fichiers** :
|
|
- `config/incus/deploy-service-native.sh` : retirer le case `chat-server)` (lignes ~112-457)
|
|
- `config/incus/deploy-service.sh` : retirer le case `chat-server)` (lignes ~62-63)
|
|
- `config/incus/build-native.sh` : retirer `build_chat_server()` et le case `chat-server)` (lignes ~48-89, ~181-182)
|
|
- `config/incus/check-deployment.sh` : retirer `veza-chat-server` des listes de containers/services
|
|
- `config/incus/verify-deployment.sh` : retirer `veza-chat-server` des listes
|
|
- `config/incus/fix-network-now.sh` : retirer le case `*chat-server*)`
|
|
- `config/incus/setup-basic-incus.sh` : retirer mention chat-server
|
|
- `config/incus/haproxy.cfg` : retirer les commentaires/sections chat-server
|
|
|
|
### Tâche CL1-10 : Nettoyer Dependabot
|
|
|
|
**Fichier** : `.github/dependabot.yml`
|
|
|
|
Supprimer l'entrée cargo pour `/veza-chat-server`.
|
|
|
|
### Tâche CL1-11 : Nettoyer templates GitHub
|
|
|
|
**Fichiers** :
|
|
- `.github/pull_request_template.md` : retirer `chat-server` du scope et la checkbox `cargo test (chat-server)`
|
|
- `.github/ISSUE_TEMPLATE/bug_report.md` : retirer `chat-server` de la liste des services
|
|
|
|
### Tâche CL1-12 : Vérifier `go.work` et `turbo.json`
|
|
|
|
**Fichiers** : `go.work`, `turbo.json`, `package.json` (root)
|
|
|
|
Retirer toute référence au chat-server de ces fichiers de configuration monorepo.
|
|
|
|
### Tâche CL1-13 : Mettre à jour documentation Incus
|
|
|
|
**Fichiers** :
|
|
- `config/incus/README.md` : retirer les lignes chat-server du tableau et des instructions
|
|
- `config/incus/DEPLOYMENT_STATUS.md` : mettre à jour le statut
|
|
- `config/incus/DEBIAN13_MIGRATION.md` : retirer mentions chat-server
|
|
- `config/docker/README.md` : retirer la ligne `veza-chat-server/docker-compose.yml`
|
|
- `config/incus/env/env.example` : retirer la section `=== chat-server.env ===`
|
|
|
|
**Validation Sprint 1** :
|
|
```bash
|
|
# Vérifier qu'aucune référence opérationnelle ne reste
|
|
git grep -l 'chat-server' -- ':(exclude)docs/' ':(exclude)CHANGELOG.md' ':(exclude)*.md'
|
|
# Devrait retourner 0 résultat (hors docs historiques)
|
|
```
|
|
|
|
**Commit Sprint 1** : `chore(cleanup): remove veza-chat-server and all operational references`
|
|
|
|
---
|
|
|
|
## Sprint 2 — Chat Hardening (jours 6-12)
|
|
|
|
> **Objectif** : Migrer le rate limiter vers Redis, ajouter la présence persistante, et full-text search.
|
|
|
|
### Tâche CH1-01 : Rate limiter Redis
|
|
|
|
**Fichier** : `veza-backend-api/internal/websocket/chat/rate_limiter.go`
|
|
|
|
Remplacer l'implémentation in-memory par Redis avec fallback.
|
|
|
|
```go
|
|
package chat
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/redis/go-redis/v9"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
type RateLimiter struct {
|
|
redis *redis.Client
|
|
limits map[string]rateConfig
|
|
logger *zap.Logger
|
|
// fallback in-memory (si Redis indisponible)
|
|
fallback *inMemoryRateLimiter
|
|
}
|
|
|
|
type rateConfig struct {
|
|
maxRequests int
|
|
window time.Duration
|
|
}
|
|
|
|
func NewRateLimiter(redisClient *redis.Client, logger *zap.Logger) *RateLimiter {
|
|
limits := map[string]rateConfig{
|
|
"send_message": {maxRequests: 10, window: time.Second},
|
|
"typing": {maxRequests: 5, window: time.Second},
|
|
"search": {maxRequests: 2, window: time.Second},
|
|
"fetch_history": {maxRequests: 5, window: time.Second},
|
|
}
|
|
return &RateLimiter{
|
|
redis: redisClient,
|
|
limits: limits,
|
|
logger: logger,
|
|
fallback: newInMemoryRateLimiter(limits),
|
|
}
|
|
}
|
|
|
|
func (rl *RateLimiter) Allow(userID uuid.UUID, action string) bool {
|
|
cfg, ok := rl.limits[action]
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
if rl.redis == nil {
|
|
return rl.fallback.Allow(userID, action)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
|
defer cancel()
|
|
|
|
key := fmt.Sprintf("chat:ratelimit:%s:%s", userID.String(), action)
|
|
now := time.Now().UnixMilli()
|
|
windowStart := now - cfg.window.Milliseconds()
|
|
|
|
pipe := rl.redis.Pipeline()
|
|
pipe.ZRemRangeByScore(ctx, key, "0", fmt.Sprintf("%d", windowStart))
|
|
pipe.ZAdd(ctx, key, redis.Z{Score: float64(now), Member: now})
|
|
pipe.ZCard(ctx, key)
|
|
pipe.Expire(ctx, key, cfg.window*2)
|
|
|
|
results, err := pipe.Exec(ctx)
|
|
if err != nil {
|
|
rl.logger.Warn("Redis rate limiter failed, using fallback", zap.Error(err))
|
|
return rl.fallback.Allow(userID, action)
|
|
}
|
|
|
|
count := results[2].(*redis.IntCmd).Val()
|
|
return count <= int64(cfg.maxRequests)
|
|
}
|
|
```
|
|
|
|
L'`inMemoryRateLimiter` reprend l'ancien code (sliding window en mémoire) comme fallback.
|
|
|
|
### Tâche CH1-02 : Présence service Redis
|
|
|
|
**Nouveau fichier** : `veza-backend-api/internal/websocket/chat/presence_service.go`
|
|
|
|
```go
|
|
package chat
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/redis/go-redis/v9"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
type PresenceInfo struct {
|
|
UserID uuid.UUID `json:"user_id"`
|
|
Status string `json:"status"`
|
|
LastSeen time.Time `json:"last_seen"`
|
|
TrackID *string `json:"track_id,omitempty"`
|
|
TrackName *string `json:"track_name,omitempty"`
|
|
}
|
|
|
|
type ChatPresenceService struct {
|
|
redis *redis.Client
|
|
logger *zap.Logger
|
|
ttl time.Duration
|
|
}
|
|
|
|
func NewChatPresenceService(redisClient *redis.Client, logger *zap.Logger) *ChatPresenceService {
|
|
return &ChatPresenceService{
|
|
redis: redisClient,
|
|
logger: logger,
|
|
ttl: 2 * time.Minute,
|
|
}
|
|
}
|
|
|
|
func (s *ChatPresenceService) SetOnline(ctx context.Context, userID uuid.UUID) error {
|
|
key := fmt.Sprintf("chat:presence:%s", userID.String())
|
|
info := PresenceInfo{
|
|
UserID: userID,
|
|
Status: "online",
|
|
LastSeen: time.Now(),
|
|
}
|
|
data, _ := json.Marshal(info)
|
|
return s.redis.Set(ctx, key, data, s.ttl).Err()
|
|
}
|
|
|
|
func (s *ChatPresenceService) SetOffline(ctx context.Context, userID uuid.UUID) error {
|
|
key := fmt.Sprintf("chat:presence:%s", userID.String())
|
|
return s.redis.Del(ctx, key).Err()
|
|
}
|
|
|
|
func (s *ChatPresenceService) Heartbeat(ctx context.Context, userID uuid.UUID) error {
|
|
key := fmt.Sprintf("chat:presence:%s", userID.String())
|
|
return s.redis.Expire(ctx, key, s.ttl).Err()
|
|
}
|
|
|
|
func (s *ChatPresenceService) GetPresence(ctx context.Context, userID uuid.UUID) (*PresenceInfo, error) {
|
|
key := fmt.Sprintf("chat:presence:%s", userID.String())
|
|
data, err := s.redis.Get(ctx, key).Result()
|
|
if err == redis.Nil {
|
|
return &PresenceInfo{UserID: userID, Status: "offline"}, nil
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get presence: %w", err)
|
|
}
|
|
var info PresenceInfo
|
|
if err := json.Unmarshal([]byte(data), &info); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal presence: %w", err)
|
|
}
|
|
return &info, nil
|
|
}
|
|
```
|
|
|
|
**Intégration Hub** : `hub.go` → appeler `presenceService.SetOnline()` sur Register et `SetOffline()` sur Unregister.
|
|
|
|
### Tâche CH1-03 : Migration FTS
|
|
|
|
**Nouveau fichier** : `veza-backend-api/migrations/113_messages_fts.sql`
|
|
|
|
```sql
|
|
-- Full-text search on messages
|
|
ALTER TABLE messages ADD COLUMN IF NOT EXISTS content_tsv tsvector;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_messages_content_fts
|
|
ON messages USING GIN (content_tsv);
|
|
|
|
-- Trigger to auto-update tsvector on insert/update
|
|
CREATE OR REPLACE FUNCTION messages_content_tsv_trigger() RETURNS trigger AS $$
|
|
BEGIN
|
|
NEW.content_tsv := to_tsvector('simple', COALESCE(NEW.content, ''));
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DROP TRIGGER IF EXISTS trg_messages_content_tsv ON messages;
|
|
CREATE TRIGGER trg_messages_content_tsv
|
|
BEFORE INSERT OR UPDATE OF content ON messages
|
|
FOR EACH ROW EXECUTE FUNCTION messages_content_tsv_trigger();
|
|
|
|
-- Backfill existing messages
|
|
UPDATE messages SET content_tsv = to_tsvector('simple', COALESCE(content, ''))
|
|
WHERE content_tsv IS NULL;
|
|
```
|
|
|
|
### Tâche CH1-04 : Remplacer ILIKE par FTS
|
|
|
|
**Fichier** : `veza-backend-api/internal/repositories/chat_message_repository.go`
|
|
|
|
Modifier la méthode `Search` :
|
|
|
|
```go
|
|
func (r *ChatMessageRepository) Search(ctx context.Context, roomID uuid.UUID, query string, limit, offset int) ([]models.ChatMessage, int64, error) {
|
|
tsQuery := plainto_tsquery(query) // helper to sanitize
|
|
|
|
var total int64
|
|
r.db.WithContext(ctx).Model(&models.ChatMessage{}).
|
|
Where("room_id = ? AND is_deleted = ? AND content_tsv @@ plainto_tsquery('simple', ?)", roomID, false, query).
|
|
Count(&total)
|
|
|
|
var messages []models.ChatMessage
|
|
err := r.db.WithContext(ctx).
|
|
Where("room_id = ? AND is_deleted = ? AND content_tsv @@ plainto_tsquery('simple', ?)", roomID, false, query).
|
|
Order("ts_rank(content_tsv, plainto_tsquery('simple', ?)) DESC, created_at DESC", query).
|
|
Limit(limit).
|
|
Offset(offset).
|
|
Find(&messages).Error
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("failed to search messages: %w", err)
|
|
}
|
|
return messages, total, nil
|
|
}
|
|
```
|
|
|
|
### Tâche CH1-05 : Mettre à jour le modèle ChatMessage GORM
|
|
|
|
**Fichier** : `veza-backend-api/internal/models/message.go`
|
|
|
|
Ajouter le champ `ContentTsv` (ignoré par JSON, géré par trigger) :
|
|
|
|
```go
|
|
ContentTsv *string `gorm:"type:tsvector;->" json:"-"`
|
|
```
|
|
|
|
Le tag `->` indique à GORM que ce champ est en lecture seule (géré par le trigger PostgreSQL).
|
|
|
|
### Tâche CH1-06 : Intégrer Redis dans le constructeur MessageHandler
|
|
|
|
**Fichier** : `veza-backend-api/internal/api/router.go`
|
|
|
|
Modifier la construction du `RateLimiter` et du `MessageHandler` pour injecter le client Redis et le `ChatPresenceService`.
|
|
|
|
### Tâche CH1-07 : Tests unitaires rate limiter Redis
|
|
|
|
**Nouveau fichier** : `veza-backend-api/internal/websocket/chat/rate_limiter_test.go`
|
|
|
|
Tests :
|
|
- `TestRedisRateLimiter_Allow` — vérifie le sliding window Redis
|
|
- `TestRedisRateLimiter_FallbackInMemory` — vérifie le fallback quand Redis est nil
|
|
- `TestRedisRateLimiter_UnknownAction` — toujours autorisé
|
|
|
|
### Tâche CH1-08 : Tests unitaires presence service
|
|
|
|
**Nouveau fichier** : `veza-backend-api/internal/websocket/chat/presence_service_test.go`
|
|
|
|
Tests :
|
|
- `TestPresence_SetOnline_GetPresence` — round-trip
|
|
- `TestPresence_SetOffline` — supprime la clé
|
|
- `TestPresence_Heartbeat_ExtendsExpiry` — vérifie le TTL
|
|
|
|
### Tâche CH1-09 : Benchmark WebSocket
|
|
|
|
**Nouveau fichier** : `veza-backend-api/internal/websocket/chat/benchmark_test.go`
|
|
|
|
Benchmark :
|
|
- 100 connexions simultanées
|
|
- Chaque client envoie 10 messages
|
|
- Mesurer le temps de bout en bout
|
|
- Vérifier 0 erreur
|
|
|
|
**Validation Sprint 2** :
|
|
```bash
|
|
cd veza-backend-api && go test ./internal/websocket/chat/... -v -count=1
|
|
cd veza-backend-api && go test ./internal/websocket/chat/ -bench=. -benchtime=10s
|
|
```
|
|
|
|
**Commit Sprint 2** : `feat(chat): add Redis rate limiter, persistent presence, full-text search`
|
|
|
|
---
|
|
|
|
## Sprint 3 — HLS Backend Integration (jours 13-18)
|
|
|
|
> **Objectif** : Rendre le pipeline HLS fonctionnel côté backend.
|
|
|
|
### État actuel
|
|
|
|
| Élément | Existe ? | Fonctionne ? |
|
|
|---------|----------|-------------|
|
|
| `HLSHandler.GetStreamInfo` | ✅ | Retourne info track |
|
|
| `HLSHandler.GetStreamStatus` | ✅ | Retourne statut transcoding |
|
|
| `HLSHandler.ServeMasterPlaylist` | ✅ | Lit depuis filesystem/S3 |
|
|
| `HLSHandler.ServeQualityPlaylist` | ✅ | Lit depuis filesystem/S3 |
|
|
| `HLSHandler.ServeSegment` | ✅ | Lit depuis filesystem/S3 |
|
|
| `HLSHandler.TriggerTranscode` | ✅ | Appelle stream server |
|
|
| `StreamService.StartProcessing` | ✅ | POST /internal/jobs/transcode |
|
|
| Stream server HLS endpoints | ✅ | Compilation OK |
|
|
| Route `GET /tracks/:id/hls/info` | ✅ | Enregistrée dans router |
|
|
| Route `GET /tracks/:id/hls/status` | ✅ | Enregistrée dans router |
|
|
| Route `GET /tracks/:id/hls/master.m3u8` | ❌ | **Non enregistrée** |
|
|
| Route `GET /tracks/:id/hls/quality.m3u8` | ❌ | **Non enregistrée** |
|
|
| Route `GET /tracks/:id/hls/segment` | ❌ | **Non enregistrée** |
|
|
| Config `HLSEnabled` | ❌ | **Absent** |
|
|
| Callback HLS-ready | ❓ | **À vérifier** |
|
|
|
|
### Tâche SS1-01 : Ajouter config HLS
|
|
|
|
**Fichier** : `veza-backend-api/internal/config/config.go`
|
|
|
|
```go
|
|
// Ajout dans le struct Config
|
|
HLSEnabled bool // Active le streaming HLS adaptatif
|
|
HLSStorageDir string // Répertoire de stockage des segments HLS
|
|
|
|
// Ajout dans le chargement
|
|
HLSEnabled: getEnvBool("HLS_STREAMING", false),
|
|
HLSStorageDir: getEnv("HLS_STORAGE_DIR", "/tmp/veza-hls"),
|
|
```
|
|
|
|
### Tâche SS1-02 : Enregistrer les routes HLS dans le router
|
|
|
|
**Fichier** : `veza-backend-api/internal/api/router.go`
|
|
|
|
Ajouter les routes HLS manquantes (protégées par stream-token auth) :
|
|
|
|
```go
|
|
// HLS streaming routes (stream token auth)
|
|
if r.config.HLSEnabled {
|
|
hlsGroup := tracks.Group("/:id/hls")
|
|
hlsGroup.Use(streamTokenAuth)
|
|
{
|
|
hlsGroup.GET("/master.m3u8", hlsHandler.ServeMasterPlaylist)
|
|
hlsGroup.GET("/:quality/playlist.m3u8", hlsHandler.ServeQualityPlaylist)
|
|
hlsGroup.GET("/:quality/:segment", hlsHandler.ServeSegment)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Tâche SS1-03 : Étendre StreamService pour HLS
|
|
|
|
**Fichier** : `veza-backend-api/internal/services/stream_service.go`
|
|
|
|
Ajouter les méthodes :
|
|
|
|
```go
|
|
// GetHLSStatus vérifie si les segments HLS sont disponibles pour un track
|
|
func (s *StreamService) GetHLSStatus(ctx context.Context, trackID uuid.UUID) (*HLSStatus, error)
|
|
|
|
// TriggerHLSTranscode déclenche le transcoding HLS adaptatif
|
|
func (s *StreamService) TriggerHLSTranscode(ctx context.Context, trackID uuid.UUID, filePath string) error
|
|
```
|
|
|
|
`HLSStatus` retourne les qualités disponibles, le nombre de segments, et la durée totale.
|
|
|
|
### Tâche SS1-04 : Vérifier callback stream-ready
|
|
|
|
**Fichier** : `veza-backend-api/internal/handlers/` (chercher le handler de callback interne)
|
|
|
|
Vérifier que le callback `POST /api/v1/internal/tracks/:id/stream-ready` :
|
|
1. Met à jour `track.stream_manifest_url` avec l'URL HLS
|
|
2. Met à jour le statut de transcoding
|
|
3. Envoie une notification (optionnel)
|
|
|
|
Si le handler n'est pas complet, le compléter pour mettre à jour le champ `stream_manifest_url` du track avec l'URL du master playlist HLS.
|
|
|
|
### Tâche SS1-05 : Mettre à jour docker-compose pour HLS
|
|
|
|
**Fichiers** : `docker-compose.yml`, `docker-compose.staging.yml`, `docker-compose.prod.yml`
|
|
|
|
Ajouter les variables d'environnement HLS :
|
|
```yaml
|
|
environment:
|
|
HLS_STREAMING: "true"
|
|
HLS_STORAGE_DIR: "/data/hls"
|
|
```
|
|
|
|
Ajouter un volume partagé pour les segments HLS entre le stream server et le backend (ou utiliser MinIO/S3).
|
|
|
|
### Tâche SS1-06 : Mettre à jour Caddy pour HLS
|
|
|
|
**Fichier** : `config/caddy/Caddyfile.staging`
|
|
|
|
S'assurer que les routes `/hls/*` sont correctement proxiées :
|
|
```
|
|
reverse_proxy /hls/* stream-server:3001
|
|
```
|
|
|
|
(Déjà présent, vérifier que c'est correct.)
|
|
|
|
### Tâche SS1-07 : Test E2E HLS pipeline
|
|
|
|
**Nouveau fichier** : `veza-backend-api/internal/integration/e2e_hls_test.go`
|
|
|
|
Test :
|
|
1. Upload un fichier audio
|
|
2. Vérifier que le transcoding est déclenché
|
|
3. Attendre le callback stream-ready
|
|
4. Vérifier que le master playlist est accessible
|
|
5. Vérifier qu'au moins 1 qualité est disponible
|
|
|
|
**Validation Sprint 3** :
|
|
```bash
|
|
cd veza-backend-api && go test ./internal/... -v -count=1 -run HLS
|
|
cd veza-backend-api && go build ./...
|
|
```
|
|
|
|
**Commit Sprint 3** : `feat(streaming): wire HLS pipeline end-to-end (backend)`
|
|
|
|
---
|
|
|
|
## Sprint 4 — HLS Frontend Integration (jours 19-23)
|
|
|
|
> **Objectif** : Le player utilise HLS quand disponible, avec ABR et quality selector.
|
|
|
|
### Tâche SS1-05 : Intégrer `useHLSPlayer` dans le player
|
|
|
|
**Fichier** : `apps/web/src/features/player/hooks/usePlayer.ts` (ou équivalent)
|
|
|
|
Logique :
|
|
1. Quand un track a un `stream_manifest_url` non-null → utiliser `useHLSPlayer`
|
|
2. Sinon → fallback sur l'URL directe (comportement actuel)
|
|
|
|
```typescript
|
|
// Pseudo-code d'intégration
|
|
const hlsPlayer = useHLSPlayer(audioRef, currentTrack?.id, currentTrack?.streamStatus);
|
|
|
|
useEffect(() => {
|
|
if (currentTrack?.stream_manifest_url && hlsPlayer.isHLSActive) {
|
|
// HLS gère le chargement via hls.js
|
|
return;
|
|
}
|
|
// Fallback : chargement direct
|
|
audioPlayerService.loadTrack(currentTrack);
|
|
}, [currentTrack]);
|
|
```
|
|
|
|
### Tâche SS1-06 : Quality selector dans le player
|
|
|
|
**Fichier** : `apps/web/src/features/player/components/` (nouveau composant ou intégré dans le player existant)
|
|
|
|
Composant `QualitySelector` :
|
|
- Affiche les niveaux disponibles (128k, 256k, 320k, Auto)
|
|
- Appelle `hlsPlayer.setQuality(levelIndex)` au clic
|
|
- Affiche le niveau actuel et la bande passante estimée
|
|
- Caché si HLS non actif
|
|
|
|
### Tâche SS1-07 : Mettre à jour le type Track
|
|
|
|
**Fichier** : `apps/web/src/features/` (types track)
|
|
|
|
Ajouter `stream_manifest_url?: string` et `stream_status?: string` au type Track si pas déjà présent.
|
|
|
|
### Tâche SS1-08 : MSW handler HLS
|
|
|
|
**Nouveau fichier** : `apps/web/src/mocks/handlers-streaming.ts`
|
|
|
|
```typescript
|
|
import { http, HttpResponse } from 'msw';
|
|
|
|
export const streamingHandlers = [
|
|
http.get('/api/v1/tracks/:id/hls/info', ({ params }) => {
|
|
return HttpResponse.json({
|
|
success: true,
|
|
data: {
|
|
track_id: params.id,
|
|
hls_available: true,
|
|
qualities: [
|
|
{ name: 'low', bitrate: 128000 },
|
|
{ name: 'medium', bitrate: 256000 },
|
|
{ name: 'high', bitrate: 320000 },
|
|
],
|
|
duration: 240,
|
|
},
|
|
});
|
|
}),
|
|
|
|
http.get('/api/v1/tracks/:id/hls/status', ({ params }) => {
|
|
return HttpResponse.json({
|
|
success: true,
|
|
data: {
|
|
track_id: params.id,
|
|
status: 'ready',
|
|
progress: 100,
|
|
},
|
|
});
|
|
}),
|
|
];
|
|
```
|
|
|
|
Enregistrer dans `handlers.ts`.
|
|
|
|
### Tâche SS1-09 : Story Storybook player HLS
|
|
|
|
**Fichier** : `apps/web/src/features/player/` (story existante ou nouvelle)
|
|
|
|
États :
|
|
- `Default` — player avec HLS actif, quality selector visible
|
|
- `Loading` — transcoding en cours (progress bar)
|
|
- `Error` — HLS indisponible, fallback sur URL directe
|
|
- `NoHLS` — track sans HLS, quality selector caché
|
|
|
|
### Tâche SS1-10 : Mettre à jour env.ts et .env
|
|
|
|
**Fichier** : `apps/web/src/config/env.ts`
|
|
|
|
Vérifier que `HLS_BASE_URL` est correctement dérivé :
|
|
```typescript
|
|
export const HLS_BASE_URL = import.meta.env.VITE_STREAM_URL || deriveStreamURL();
|
|
```
|
|
|
|
**Fichiers** : `.env.example`, `.env.storybook`
|
|
|
|
Ajouter/vérifier :
|
|
```
|
|
VITE_FEATURE_HLS_STREAMING=true
|
|
```
|
|
|
|
**Validation Sprint 4** :
|
|
```bash
|
|
cd apps/web && npx tsc --noEmit
|
|
cd apps/web && npm run build
|
|
cd apps/web && npm run storybook -- --ci
|
|
```
|
|
|
|
**Commit Sprint 4** : `feat(player): integrate HLS streaming with ABR quality switching`
|
|
|
|
---
|
|
|
|
## Sprint 5 — Tests, Documentation & Tag (jours 24-25)
|
|
|
|
> **Objectif** : Finaliser, documenter, tagger.
|
|
|
|
### Tâche QA1-01 : Mettre à jour FEATURE_STATUS.md
|
|
|
|
**Fichier** : `docs/FEATURE_STATUS.md`
|
|
|
|
- HLS Streaming : passer de "en intégration" à "opérationnel (v0.503)"
|
|
- Chat WebSocket : mettre à jour la note "Partiel" → "Opérationnel (Go, v0.502+)"
|
|
- Ajouter section "Livré en v0.503"
|
|
|
|
### Tâche QA1-02 : Mettre à jour PROJECT_STATE.md
|
|
|
|
**Fichier** : `docs/PROJECT_STATE.md`
|
|
|
|
- Ajouter la section v0.503 dans "Ce qui est livré"
|
|
- Mettre à jour "Prochaine version" → v0.601
|
|
- Mettre à jour la stack technique (retirer Chat Server Rust)
|
|
- Incrémenter les indicateurs
|
|
|
|
### Tâche QA1-03 : Smoke test
|
|
|
|
**Nouveau fichier** : `docs/SMOKE_TEST_V0503.md`
|
|
|
|
Checklist :
|
|
- [ ] HLS : upload → transcode → playback HLS
|
|
- [ ] HLS : ABR switch de qualité
|
|
- [ ] Chat : rate limiter Redis fonctionne
|
|
- [ ] Chat : présence persistée dans Redis
|
|
- [ ] Chat : recherche full-text retourne des résultats pertinents
|
|
- [ ] Chat : benchmark 100 connexions OK
|
|
- [ ] Cleanup : `git grep chat-server` (hors docs) = 0 résultat
|
|
- [ ] CI : workflows passent sans erreur
|
|
|
|
### Tâche QA1-04 : CHANGELOG.md
|
|
|
|
**Fichier** : `CHANGELOG.md`
|
|
|
|
```markdown
|
|
## v0.503 — Stream Server E2E + Chat Hardening + Cleanup
|
|
|
|
### Nouveautés
|
|
- **HLS Streaming E2E** : pipeline upload → transcode → HLS player fonctionnel
|
|
- **Player ABR** : quality selector (128k, 256k, 320k, Auto)
|
|
- **Chat FTS** : recherche full-text PostgreSQL (tsvector + GIN index)
|
|
- **Chat présence** : tracking persistant Redis avec heartbeat
|
|
|
|
### Améliorations
|
|
- **Rate limiter** : migré in-memory → Redis (sliding window, fallback)
|
|
- **Benchmark** : 100+ connexions WebSocket simultanées validé
|
|
|
|
### Suppressions
|
|
- **veza-chat-server/** : dossier Rust archivé/supprimé du repo
|
|
- **chat-ci.yml** : workflow CI dédié au chat Rust supprimé
|
|
- Références chat-server retirées de CI/CD, Prometheus, Caddy, HAProxy, scripts Incus
|
|
|
|
### Infrastructure
|
|
- Migration 113 : tsvector + GIN index sur messages.content
|
|
```
|
|
|
|
### Tâche QA1-05 : Archiver scope et préparer v0.601
|
|
|
|
- Déplacer `V0_503_RELEASE_SCOPE.md` → `docs/archive/`
|
|
- Créer placeholder `V0_601_RELEASE_SCOPE.md`
|
|
- Mettre à jour `SCOPE_CONTROL.md` → référence v0.601
|
|
|
|
### Tâche QA1-06 : Rétrospective
|
|
|
|
**Nouveau fichier** : `docs/RETROSPECTIVE_V0503.md`
|
|
|
|
### Tâche QA1-07 : Tag
|
|
|
|
```bash
|
|
git tag -a v0.503 -m "v0.503 — Stream Server E2E + Chat Hardening + Cleanup"
|
|
```
|
|
|
|
**Commit Sprint 5** : `docs(v0.503): update documentation, changelog, tag release`
|
|
|
|
---
|
|
|
|
## Résumé
|
|
|
|
| Sprint | Jours | Tâches | Focus |
|
|
|--------|-------|--------|-------|
|
|
| 1 | 1-5 | CL1-01 à CL1-13 | Cleanup chat-server Rust |
|
|
| 2 | 6-12 | CH1-01 à CH1-09 | Chat hardening (Redis, FTS) |
|
|
| 3 | 13-18 | SS1-01 à SS1-07 | HLS backend E2E |
|
|
| 4 | 19-23 | SS1-05 à SS1-10 | HLS frontend (player, ABR) |
|
|
| 5 | 24-25 | QA1-01 à QA1-07 | Documentation & tag |
|
|
|
|
**Total** : ~39 tâches, 25 jours ouvrés
|
|
|
|
---
|
|
|
|
## Dépendances entre lots
|
|
|
|
```
|
|
CL1 (Cleanup) → indépendant, peut commencer en premier
|
|
CH1 (Chat Hardening) → indépendant de CL1 et SS1
|
|
SS1a (HLS Backend) → indépendant de CL1 et CH1
|
|
SS1b (HLS Frontend) → dépend de SS1a (backend endpoints disponibles)
|
|
QA1 (Documentation) → dépend de CL1, CH1, SS1a, SS1b
|
|
```
|
|
|
|
---
|
|
|
|
## Risques et mitigations
|
|
|
|
| Risque | Probabilité | Impact | Mitigation |
|
|
|--------|------------|--------|-----------|
|
|
| Stream server HLS ne produit pas de segments | Moyenne | Élevé | Tester manuellement avec FFmpeg avant d'intégrer |
|
|
| FFmpeg absent du container stream-server | Faible | Élevé | Vérifier le Dockerfile du stream server |
|
|
| Redis down en production | Faible | Moyen | Fallback in-memory pour rate limiter et présence |
|
|
| FTS performance sur gros volume | Faible | Moyen | Index GIN + `simple` config (pas de stemming) |
|
|
| Breaking change frontend player | Moyenne | Moyen | Fallback sur URL directe si HLS échoue |
|