- Added GetStreamInfo method to HLSService - Added GetStreamInfo handler in HLSHandler - Standardized GetStreamStatus handler to use RespondSuccess/RespondWithAppError - Added routes: GET /tracks/:id/hls/info and GET /tracks/:id/hls/status - GetStreamInfo returns general stream information - GetStreamStatus returns status with processing info if applicable - Handlers use standard API response format Phase: PHASE-2 Priority: P1 Progress: 29/267 (10.9%)
319 lines
11 KiB
Go
319 lines
11 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"veza-backend-api/internal/models"
|
|
|
|
"github.com/google/uuid"
|
|
"gorm.io/gorm"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// HLSService gère la récupération et le service des fichiers HLS
|
|
type HLSService struct {
|
|
db *gorm.DB
|
|
outputDir string
|
|
logger *zap.Logger
|
|
transcodeService *HLSTranscodeService
|
|
queueService *HLSQueueService
|
|
}
|
|
|
|
// NewHLSService crée un nouveau service HLS
|
|
func NewHLSService(db *gorm.DB, outputDir string, logger *zap.Logger) *HLSService {
|
|
if logger == nil {
|
|
logger = zap.NewNop()
|
|
}
|
|
return &HLSService{
|
|
db: db,
|
|
outputDir: outputDir,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// NewHLSServiceWithTranscode crée un nouveau service HLS avec service de transcodage
|
|
func NewHLSServiceWithTranscode(db *gorm.DB, outputDir string, transcodeService *HLSTranscodeService, logger *zap.Logger) *HLSService {
|
|
if logger == nil {
|
|
logger = zap.NewNop()
|
|
}
|
|
return &HLSService{
|
|
db: db,
|
|
outputDir: outputDir,
|
|
logger: logger,
|
|
transcodeService: transcodeService,
|
|
}
|
|
}
|
|
|
|
// SetTranscodeService définit le service de transcodage
|
|
func (s *HLSService) SetTranscodeService(transcodeService *HLSTranscodeService) {
|
|
s.transcodeService = transcodeService
|
|
}
|
|
|
|
// SetQueueService définit le service de queue HLS
|
|
func (s *HLSService) SetQueueService(queueService *HLSQueueService) {
|
|
s.queueService = queueService
|
|
}
|
|
|
|
// GetMasterPlaylist récupère le contenu du master playlist pour un track
|
|
func (s *HLSService) GetMasterPlaylist(ctx context.Context, trackID uuid.UUID) (string, error) {
|
|
var hlsStream models.HLSStream
|
|
if err := s.db.WithContext(ctx).Where("track_id = ? AND status = ?", trackID, models.HLSStatusReady).First(&hlsStream).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return "", fmt.Errorf("HLS stream not found for track %s", trackID)
|
|
}
|
|
return "", fmt.Errorf("failed to query HLS stream: %w", err)
|
|
}
|
|
|
|
// Lire le fichier master.m3u8
|
|
// Le PlaylistURL est relatif au outputDir (ex: track_123/master.m3u8)
|
|
masterPlaylistPath := hlsStream.PlaylistURL
|
|
if !filepath.IsAbs(masterPlaylistPath) {
|
|
// Si c'est un chemin relatif, il devrait déjà être relatif à outputDir
|
|
// Vérifier si c'est déjà un chemin complet ou relatif
|
|
if !strings.HasPrefix(masterPlaylistPath, s.outputDir) {
|
|
masterPlaylistPath = filepath.Join(s.outputDir, masterPlaylistPath)
|
|
}
|
|
}
|
|
|
|
content, err := os.ReadFile(masterPlaylistPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return "", fmt.Errorf("master playlist file not found: %s", masterPlaylistPath)
|
|
}
|
|
return "", fmt.Errorf("failed to read master playlist: %w", err)
|
|
}
|
|
|
|
return string(content), nil
|
|
}
|
|
|
|
// GetQualityPlaylist récupère le contenu d'une quality playlist pour un track et bitrate
|
|
func (s *HLSService) GetQualityPlaylist(ctx context.Context, trackID uuid.UUID, bitrate string) (string, error) {
|
|
var hlsStream models.HLSStream
|
|
if err := s.db.WithContext(ctx).Where("track_id = ? AND status = ?", trackID, models.HLSStatusReady).First(&hlsStream).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return "", fmt.Errorf("HLS stream not found for track %s", trackID)
|
|
}
|
|
return "", fmt.Errorf("failed to query HLS stream: %w", err)
|
|
}
|
|
|
|
// Construire le chemin vers la quality playlist
|
|
trackDir := filepath.Join(s.outputDir, fmt.Sprintf("track_%s", trackID))
|
|
qualityPlaylistPath := filepath.Join(trackDir, bitrate, "playlist.m3u8")
|
|
|
|
content, err := os.ReadFile(qualityPlaylistPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return "", fmt.Errorf("quality playlist file not found: %s", qualityPlaylistPath)
|
|
}
|
|
return "", fmt.Errorf("failed to read quality playlist: %w", err)
|
|
}
|
|
|
|
return string(content), nil
|
|
}
|
|
|
|
// GetSegmentPath récupère le chemin complet d'un segment pour un track, bitrate et nom de segment
|
|
func (s *HLSService) GetSegmentPath(ctx context.Context, trackID uuid.UUID, bitrate string, segment string) (string, error) {
|
|
var hlsStream models.HLSStream
|
|
if err := s.db.WithContext(ctx).Where("track_id = ? AND status = ?", trackID, models.HLSStatusReady).First(&hlsStream).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return "", fmt.Errorf("HLS stream not found for track %s", trackID)
|
|
}
|
|
return "", fmt.Errorf("failed to query HLS stream: %w", err)
|
|
}
|
|
|
|
// Construire le chemin vers le segment
|
|
trackDir := filepath.Join(s.outputDir, fmt.Sprintf("track_%s", trackID))
|
|
segmentPath := filepath.Join(trackDir, bitrate, segment)
|
|
|
|
// Vérifier que le fichier existe
|
|
if _, err := os.Stat(segmentPath); os.IsNotExist(err) {
|
|
return "", fmt.Errorf("segment file not found: %s", segmentPath)
|
|
}
|
|
|
|
// Vérifier que le chemin est sécurisé (pas de directory traversal)
|
|
absSegmentPath, err := filepath.Abs(segmentPath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get absolute path: %w", err)
|
|
}
|
|
|
|
absTrackDir, err := filepath.Abs(trackDir)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get absolute track dir: %w", err)
|
|
}
|
|
|
|
// Vérifier que le segment est bien dans le répertoire du track
|
|
if !strings.HasPrefix(absSegmentPath, absTrackDir) {
|
|
return "", fmt.Errorf("invalid segment path: %s", segmentPath)
|
|
}
|
|
|
|
return absSegmentPath, nil
|
|
}
|
|
|
|
// TriggerTranscode déclenche le transcodage d'un track en HLS
|
|
func (s *HLSService) TriggerTranscode(ctx context.Context, track *models.Track) error {
|
|
if s.transcodeService == nil {
|
|
return fmt.Errorf("transcode service not configured")
|
|
}
|
|
|
|
if track == nil {
|
|
return fmt.Errorf("track cannot be nil")
|
|
}
|
|
|
|
// Vérifier si un stream existe déjà pour ce track
|
|
var existingStream models.HLSStream
|
|
err := s.db.WithContext(ctx).Where("track_id = ?", track.ID).First(&existingStream).Error
|
|
if err == nil {
|
|
// Un stream existe déjà, vérifier son statut
|
|
if existingStream.Status == models.HLSStatusReady {
|
|
return fmt.Errorf("HLS stream already exists and is ready for track %s", track.ID)
|
|
}
|
|
// Si le stream est en cours de traitement ou a échoué, on peut le retranscoder
|
|
if existingStream.Status == models.HLSStatusProcessing {
|
|
return fmt.Errorf("HLS stream is already being processed for track %s", track.ID)
|
|
}
|
|
// Supprimer l'ancien stream si nécessaire
|
|
if err := s.db.WithContext(ctx).Delete(&existingStream).Error; err != nil {
|
|
s.logger.Warn("Failed to delete existing stream", zap.Error(err), zap.String("track_id", track.ID.String()))
|
|
}
|
|
}
|
|
|
|
// Mettre à jour le statut du track si nécessaire
|
|
if err := s.db.WithContext(ctx).Model(track).Update("status", models.TrackStatusProcessing).Error; err != nil {
|
|
s.logger.Warn("Failed to update track status", zap.Error(err), zap.String("track_id", track.ID.String()))
|
|
}
|
|
|
|
// Créer un stream en statut "processing"
|
|
hlsStream := &models.HLSStream{
|
|
TrackID: track.ID,
|
|
Status: models.HLSStatusProcessing,
|
|
}
|
|
if err := s.db.WithContext(ctx).Create(hlsStream).Error; err != nil {
|
|
return fmt.Errorf("failed to create HLS stream record: %w", err)
|
|
}
|
|
|
|
// Transcoder le track
|
|
transcodedStream, err := s.transcodeService.TranscodeTrack(ctx, track)
|
|
if err != nil {
|
|
// Mettre à jour le statut en "failed"
|
|
s.db.WithContext(ctx).Model(hlsStream).Update("status", models.HLSStatusFailed)
|
|
return fmt.Errorf("failed to transcode track: %w", err)
|
|
}
|
|
|
|
// Mettre à jour le stream avec les données du transcodage
|
|
hlsStream.PlaylistURL = transcodedStream.PlaylistURL
|
|
hlsStream.SegmentsCount = transcodedStream.SegmentsCount
|
|
hlsStream.Bitrates = transcodedStream.Bitrates
|
|
hlsStream.Status = models.HLSStatusReady
|
|
|
|
if err := s.db.WithContext(ctx).Save(hlsStream).Error; err != nil {
|
|
return fmt.Errorf("failed to update HLS stream: %w", err)
|
|
}
|
|
|
|
// Mettre à jour le statut du track
|
|
if err := s.db.WithContext(ctx).Model(track).Update("status", models.TrackStatusCompleted).Error; err != nil {
|
|
s.logger.Warn("Failed to update track status to completed", zap.Error(err), zap.String("track_id", track.ID.String()))
|
|
}
|
|
|
|
s.logger.Info("HLS transcoding completed", zap.String("track_id", track.ID.String()), zap.String("stream_id", hlsStream.ID.String()))
|
|
return nil
|
|
}
|
|
|
|
// TriggerTranscodeQueue déclenche le transcodage HLS via la queue (T0343)
|
|
// Vérifie les permissions et ajoute un job dans la queue
|
|
// MIGRATION UUID: userID migré vers uuid.UUID
|
|
func (s *HLSService) TriggerTranscodeQueue(ctx context.Context, trackID uuid.UUID, userID uuid.UUID) (uuid.UUID, error) {
|
|
if s.queueService == nil {
|
|
return uuid.Nil, fmt.Errorf("queue service not configured")
|
|
}
|
|
|
|
// Vérifier que le track existe et que l'utilisateur est propriétaire
|
|
var track models.Track
|
|
if err := s.db.WithContext(ctx).First(&track, "id = ?", trackID).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return uuid.Nil, fmt.Errorf("track not found")
|
|
}
|
|
return uuid.Nil, fmt.Errorf("failed to query track: %w", err)
|
|
}
|
|
|
|
// Vérifier les permissions (UUID comparison)
|
|
if track.UserID != userID {
|
|
return uuid.Nil, fmt.Errorf("forbidden: user does not own this track")
|
|
}
|
|
|
|
// Ajouter le job dans la queue avec priorité par défaut (5)
|
|
priority := 5
|
|
|
|
jobID, err := s.queueService.EnqueueWithID(ctx, trackID, priority)
|
|
if err != nil {
|
|
return uuid.Nil, fmt.Errorf("failed to enqueue transcode job: %w", err)
|
|
}
|
|
|
|
s.logger.Info("HLS transcode job enqueued", zap.String("job_id", jobID.String()), zap.String("track_id", trackID.String()), zap.String("user_id", userID.String()))
|
|
return jobID, nil
|
|
}
|
|
|
|
// GetStreamStatus récupère le statut d'un stream HLS pour un track
|
|
func (s *HLSService) GetStreamStatus(ctx context.Context, trackID uuid.UUID) (map[string]interface{}, error) {
|
|
var stream models.HLSStream
|
|
if err := s.db.WithContext(ctx).Where("track_id = ?", trackID).First(&stream).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return nil, fmt.Errorf("HLS stream not found for track %s", trackID)
|
|
}
|
|
return nil, fmt.Errorf("failed to query HLS stream: %w", err)
|
|
}
|
|
|
|
status := map[string]interface{}{
|
|
"status": stream.Status,
|
|
"bitrates": stream.Bitrates,
|
|
"segments_count": stream.SegmentsCount,
|
|
"playlist_url": stream.PlaylistURL,
|
|
"track_id": stream.TrackID,
|
|
"created_at": stream.CreatedAt,
|
|
"updated_at": stream.UpdatedAt,
|
|
}
|
|
|
|
// Ajouter des informations supplémentaires si le stream est en cours de traitement
|
|
if stream.Status == models.HLSStatusProcessing {
|
|
// Vérifier s'il y a un job de transcodage en cours
|
|
var queueJob models.HLSTranscodeQueue
|
|
if err := s.db.WithContext(ctx).
|
|
Where("track_id = ? AND status = ?", trackID, models.QueueStatusProcessing).
|
|
First(&queueJob).Error; err == nil {
|
|
status["queue_job_id"] = queueJob.ID
|
|
status["retry_count"] = queueJob.RetryCount
|
|
if queueJob.StartedAt != nil {
|
|
status["started_at"] = queueJob.StartedAt
|
|
}
|
|
}
|
|
}
|
|
|
|
return status, nil
|
|
}
|
|
|
|
// GetStreamInfo récupère les informations générales d'un stream HLS pour un track
|
|
// BE-API-020: Implement HLS stream info endpoint
|
|
func (s *HLSService) GetStreamInfo(ctx context.Context, trackID uuid.UUID) (map[string]interface{}, error) {
|
|
var stream models.HLSStream
|
|
if err := s.db.WithContext(ctx).Where("track_id = ?", trackID).First(&stream).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return nil, fmt.Errorf("HLS stream not found for track %s", trackID)
|
|
}
|
|
return nil, fmt.Errorf("failed to query HLS stream: %w", err)
|
|
}
|
|
|
|
info := map[string]interface{}{
|
|
"track_id": stream.TrackID,
|
|
"playlist_url": stream.PlaylistURL,
|
|
"bitrates": stream.Bitrates,
|
|
"segments_count": stream.SegmentsCount,
|
|
"created_at": stream.CreatedAt,
|
|
"updated_at": stream.UpdatedAt,
|
|
}
|
|
|
|
return info, nil
|
|
}
|