package encoder import ( "fmt" "os" "os/exec" "path/filepath" "strconv" "strings" "videnc-vibe/pkg/types" ) type Encoder struct { ffmpegPath string opusencPath string } func New() *Encoder { return &Encoder{ ffmpegPath: "ffmpeg", opusencPath: "opusenc", } } func (e *Encoder) CheckDeps() error { if _, err := exec.LookPath(e.ffmpegPath); err != nil { return fmt.Errorf("ffmpeg not found") } if _, err := exec.LookPath(e.opusencPath); err != nil { return fmt.Errorf("opusenc not found") } return nil } func (e *Encoder) GetMediaInfo(path string) (width, height int, err error) { cmd := exec.Command(e.ffmpegPath, "-i", path, "-hide_banner") output, _ := cmd.StderrPipe() cmd.Start() buf := make([]byte, 8192) n, _ := output.Read(buf) output.Close() var w, h int fmt.Sscanf(string(buf[:n]), "%dx%d", &w, &h) return w, h, nil } func (e *Encoder) Transcode(input string, job *types.Job, metadata *types.Metadata) error { dir := filepath.Dir(input) audioWavs, err := e.extractAudio(input, dir) if err != nil { return fmt.Errorf("extracting audio: %w", err) } defer e.cleanupWavs(audioWavs) opusFiles, err := e.encodeOpus(audioWavs, dir) if err != nil { return fmt.Errorf("encoding opus: %w", err) } defer e.cleanupOpus(opusFiles) if err := e.encodeVideo(input, opusFiles, job, metadata); err != nil { return fmt.Errorf("encoding video: %w", err) } return nil } func (e *Encoder) extractAudio(input, dir string) ([]string, error) { cmd := exec.Command(e.ffmpegPath, "-i", input, "-vn", "-c:a", "pcm_s16le", "-ar", "48000", "-f", "wav", filepath.Join(dir, "audio_%d.wav")) if out, err := cmd.CombinedOutput(); err != nil { return nil, fmt.Errorf("ffmpeg extract: %s %w", out, err) } matches, _ := filepath.Glob(filepath.Join(dir, "audio_*.wav")) return matches, nil } func (e *Encoder) encodeOpus(wavs []string, dir string) ([]string, error) { var opusFiles []string for _, wav := range wavs { out := strings.TrimSuffix(wav, ".wav") + ".opus" cmd := exec.Command(e.opusencPath, "--bitrate", "128k", wav, out) if out, err := cmd.CombinedOutput(); err != nil { return nil, fmt.Errorf("opusenc: %s %w", out, err) } opusFiles = append(opusFiles, out) } return opusFiles, nil } func (e *Encoder) encodeVideo(input string, opusFiles []string, job *types.Job, metadata *types.Metadata) error { dir := filepath.Dir(input) outFile := filepath.Join(dir, "output.mkv") svtParams := fmt.Sprintf("film-grain=10:film-grain-denoise=1:scd=1:qm-min=4:qm-max=15:preset=%d", job.Preset) args := []string{ "-y", "-i", input, "-c:v", "libsvtav1", "-crf", strconv.Itoa(job.CRF), "-svtav1-params", svtParams, "-pix_fmt", "yuv420p10le", "-vf", "zscale=filter=spline36:mode=16:9", "-c:a", "copy", "-c:s", "copy", } for _, opus := range opusFiles { args = append(args, "-i", opus) } args = append(args, "-map", "0:v") args = append(args, "-map", "0:s?") for i := range opusFiles { args = append(args, "-map", fmt.Sprintf("%d:a", i+1)) } args = append(args, "-metadata", fmt.Sprintf("TITLE=%s", metadata.Title)) args = append(args, "-metadata", fmt.Sprintf("DATE_RELEASED=%s", metadata.DateReleased)) args = append(args, "-metadata", fmt.Sprintf("IMDBID=%s", metadata.IMDBID)) args = append(args, "-metadata", fmt.Sprintf("ORIGINAL_MEDIA_TYPE=%s", metadata.OriginalMedia)) if metadata.IsSeries { args = append(args, "-metadata", fmt.Sprintf("COLLECTION=%s", metadata.Collection)) args = append(args, "-metadata", fmt.Sprintf("SEASON=%s", metadata.Season)) args = append(args, "-metadata", fmt.Sprintf("EPISODE=%s", metadata.Episode)) if metadata.TVMazeID != "" { args = append(args, "-metadata", fmt.Sprintf("TVMAZE_ID=%s", metadata.TVMazeID)) } } args = append(args, outFile) cmd := exec.Command(e.ffmpegPath, args...) if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("ffmpeg encode: %s %w", out, err) } return nil } func (e *Encoder) cleanupWavs(files []string) { for _, f := range files { os.Remove(f) } } func (e *Encoder) cleanupOpus(files []string) { for _, f := range files { os.Remove(f) } }