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, interlaced bool, err error) { cmd := exec.Command(e.ffmpegPath, "-i", path, "-hide_banner", "-vf", "idet") output, _ := cmd.StderrPipe() cmd.Start() buf := make([]byte, 16384) n, _ := output.Read(buf) output.Close() outputStr := string(buf[:n]) var w, h int for i := 0; i < len(outputStr)-10; i++ { if outputStr[i] >= '0' && outputStr[i] <= '9' { fmt.Sscanf(outputStr[i:], "%dx%d", &w, &h) if w > 0 && h > 0 { break } } } interlaced = strings.Contains(outputStr, "TFF") || strings.Contains(outputStr, "BFF") return w, h, interlaced, nil } func (e *Encoder) Transcode(input string, job *types.Job, metadata *types.Metadata, interlaced bool) 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, interlaced); 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.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.Replace(wav, ".wav", ".opus", 1) 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, interlaced bool) 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) vf := "zscale=filter=spline36:mode=16:9" if interlaced { vf = "bwdif=mode=0:par=-1:-1," + vf } args := []string{ "-y", } args = append(args, "-i", input) args = append(args, "-c:v", "libsvtav1") args = append(args, "-crf", strconv.Itoa(job.CRF)) args = append(args, "-svtav1-params", svtParams) args = append(args, "-pix_fmt", "yuv420p10le") args = append(args, "-filter:v:0", vf) 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, "-c:a", "copy") args = append(args, "-c:s", "copy") args = append(args, "-map_metadata", "0") 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) } }