Files
silo-server/internal/playback/ffmpeg_log_sink.go
T

110 lines
3.3 KiB
Go

package playback
import (
"context"
"log/slog"
"strings"
)
const (
ffmpegComponent = "ffmpeg"
ffmpegEventKey = "ffmpeg_event"
ffmpegLineKey = "ffmpeg_line"
ffmpegDroppedLinesKey = "dropped_lines"
ffmpegLineIndexKey = "line_index"
ffmpegExecutionModeKey = "execution_mode"
)
// FFmpegLogSink persists ffmpeg stderr and lifecycle events for a transcode
// session without coupling playback to a specific logging backend.
type FFmpegLogSink interface {
WriteLine(ctx context.Context, sessionID string, attrs FFmpegLogAttrs, line string)
WriteEvent(ctx context.Context, sessionID string, attrs FFmpegLogAttrs, message string)
}
// FFmpegLogAttrs captures stable context for ffmpeg logs.
type FFmpegLogAttrs struct {
NodeType string
ExecutionMode string
InputPath string
OutputDir string
TargetResolution string
TargetVideoCodec string
TargetAudioCodec string
HWAccel string
SeekSeconds float64
StartSegmentNumber int
RestartCount int
DroppedLines int
LineIndex int
ExitError string
}
// SlogFFmpegLogSink emits ffmpeg logs through slog so the existing opslog
// handler captures them into operational_logs.
type SlogFFmpegLogSink struct {
logger *slog.Logger
nodeID string
}
func NewSlogFFmpegLogSink(logger *slog.Logger, nodeID string) *SlogFFmpegLogSink {
if logger == nil {
logger = slog.Default()
}
return &SlogFFmpegLogSink{logger: logger, nodeID: strings.TrimSpace(nodeID)}
}
func (s *SlogFFmpegLogSink) WriteLine(ctx context.Context, sessionID string, attrs FFmpegLogAttrs, line string) {
if s == nil || s.logger == nil {
return
}
args := s.baseArgs(sessionID, attrs)
args = append(args, ffmpegLineKey, line)
if attrs.LineIndex > 0 {
args = append(args, ffmpegLineIndexKey, attrs.LineIndex)
}
if attrs.DroppedLines > 0 {
args = append(args, ffmpegDroppedLinesKey, attrs.DroppedLines)
}
s.logger.InfoContext(ctx, "ffmpeg stderr", args...)
}
func (s *SlogFFmpegLogSink) WriteEvent(ctx context.Context, sessionID string, attrs FFmpegLogAttrs, message string) {
if s == nil || s.logger == nil {
return
}
args := s.baseArgs(sessionID, attrs)
args = append(args, ffmpegEventKey, message)
if attrs.ExitError != "" {
args = append(args, "exit_error", attrs.ExitError)
}
if attrs.DroppedLines > 0 {
args = append(args, ffmpegDroppedLinesKey, attrs.DroppedLines)
}
s.logger.InfoContext(ctx, message, args...)
}
func (s *SlogFFmpegLogSink) baseArgs(sessionID string, attrs FFmpegLogAttrs) []any {
args := []any{
"component", ffmpegComponent,
"playback_session_id", sessionID,
"node_id", s.nodeID,
"node_type", strings.TrimSpace(attrs.NodeType),
ffmpegExecutionModeKey, strings.TrimSpace(attrs.ExecutionMode),
"restart_count", attrs.RestartCount,
"target_resolution", strings.TrimSpace(attrs.TargetResolution),
"target_video_codec", strings.TrimSpace(attrs.TargetVideoCodec),
"target_audio_codec", strings.TrimSpace(attrs.TargetAudioCodec),
"hw_accel", strings.TrimSpace(attrs.HWAccel),
"seek_seconds", attrs.SeekSeconds,
"start_segment_number", attrs.StartSegmentNumber,
}
if v := strings.TrimSpace(attrs.InputPath); v != "" {
args = append(args, "input_path", v)
}
if v := strings.TrimSpace(attrs.OutputDir); v != "" {
args = append(args, "output_dir", v)
}
return args
}