From 31b5f03e0982a144b924c4b7747593a3128dee00 Mon Sep 17 00:00:00 2001 From: Esa Kataja Date: Sat, 4 Apr 2026 19:18:43 +0300 Subject: [PATCH] fix: use ffprobe for SAR detection and calculate zscale width --- internal/encoder/encoder.go | 129 +++++++++++++++++++++++++++++------- 1 file changed, 106 insertions(+), 23 deletions(-) diff --git a/internal/encoder/encoder.go b/internal/encoder/encoder.go index c868956..3ce082f 100644 --- a/internal/encoder/encoder.go +++ b/internal/encoder/encoder.go @@ -1,6 +1,7 @@ package encoder import ( + "encoding/json" "fmt" "os" "os/exec" @@ -13,12 +14,26 @@ import ( type Encoder struct { ffmpegPath string + ffprobePath string opusencPath string } +type StreamInfo struct { + Width int `json:"width"` + Height int `json:"height"` + SampleAspectRatio string `json:"sample_aspect_ratio"` + DisplayAspectRatio string `json:"display_aspect_ratio"` + CodecName string `json:"codec_name"` +} + +type ProbeResult struct { + Streams []StreamInfo `json:"streams"` +} + func New() *Encoder { return &Encoder{ ffmpegPath: "ffmpeg", + ffprobePath: "ffprobe", opusencPath: "opusenc", } } @@ -27,6 +42,9 @@ func (e *Encoder) CheckDeps() error { if _, err := exec.LookPath(e.ffmpegPath); err != nil { return fmt.Errorf("ffmpeg not found") } + if _, err := exec.LookPath(e.ffprobePath); err != nil { + return fmt.Errorf("ffprobe not found") + } if _, err := exec.LookPath(e.opusencPath); err != nil { return fmt.Errorf("opusenc not found") } @@ -34,29 +52,83 @@ func (e *Encoder) CheckDeps() error { } 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 := exec.Command(e.ffprobePath, "-v", "error", "-show_streams", "-print_format", "json", path) + output, err := cmd.CombinedOutput() + if err != nil { + return 0, 0, false, fmt.Errorf("ffprobe error: %w", err) + } + + fmt.Printf("DEBUG ffprobe output: %s\n", string(output)) + + var result ProbeResult + if err := json.Unmarshal(output, &result); err != nil { + return 0, 0, false, fmt.Errorf("parsing ffprobe output: %w", err) + } + + for _, stream := range result.Streams { + if stream.CodecName == "mpeg2video" || stream.CodecName == "h264" || stream.CodecName == "hevc" { + width = stream.Width + height = stream.Height + fmt.Printf("DEBUG detected video: %dx%d, SAR=%s\n", width, height, stream.SampleAspectRatio) + break + } + } + + cmd = exec.Command(e.ffmpegPath, "-i", path, "-hide_banner", "-vf", "idet") + stderr, _ := cmd.StderrPipe() cmd.Start() - buf := make([]byte, 16384) - n, _ := output.Read(buf) - output.Close() - + n, _ := stderr.Read(buf) + stderr.Close() outputStr := string(buf[:n]) + interlaced = strings.Contains(outputStr, "TFF") || strings.Contains(outputStr, "BFF") - 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 + fmt.Printf("DEBUG interlaced: %v\n", interlaced) + + return width, height, interlaced, nil +} + +func (e *Encoder) calculateZscaleWidth(path string, originalHeight int) (string, int, error) { + cmd := exec.Command(e.ffprobePath, "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height,sample_aspect_ratio", "-print_format", "json", path) + output, err := cmd.CombinedOutput() + if err != nil { + return "", 0, fmt.Errorf("ffprobe error: %w", err) + } + + fmt.Printf("DEBUG ffprobe for zscale: %s\n", string(output)) + + var result ProbeResult + if err := json.Unmarshal(output, &result); err != nil { + return "", 0, fmt.Errorf("parsing ffprobe: %w", err) + } + + if len(result.Streams) == 0 { + return "", 0, fmt.Errorf("no video streams found") + } + + stream := result.Streams[0] + width := stream.Width + height := stream.Height + + sar := stream.SampleAspectRatio + fmt.Printf("DEBUG stream: width=%d, height=%d, sar=%s\n", width, height, sar) + + newWidth := width + + if sar != "1:1" && sar != "N/A" && sar != "" { + parts := strings.Split(sar, ":") + if len(parts) == 2 { + num, err1 := strconv.Atoi(parts[0]) + den, err2 := strconv.Atoi(parts[1]) + if err1 == nil && err2 == nil && den != 0 { + newWidth = (width * num) / den + fmt.Printf("DEBUG calculated new width: %d (sar=%s)\n", newWidth, sar) } } } - interlaced = strings.Contains(outputStr, "TFF") || strings.Contains(outputStr, "BFF") - - return w, h, interlaced, nil + zscale := fmt.Sprintf("zscale=w=%d:h=%d:filter=spline36", newWidth, height) + return zscale, newWidth, nil } func (e *Encoder) Transcode(input string, job *types.Job, metadata *types.Metadata, interlaced bool) error { @@ -111,26 +183,35 @@ func (e *Encoder) encodeVideo(input string, opusFiles []string, job *types.Job, 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" + zscaleStr, newWidth, err := e.calculateZscaleWidth(input, 0) + if err != nil { + return fmt.Errorf("calculating zscale: %w", err) + } + + fmt.Printf("DEBUG zscale: %s (newWidth=%d)\n", zscaleStr, newWidth) + + vf := zscaleStr if interlaced { vf = "bwdif=mode=0:par=-1:-1," + vf } + fmt.Printf("DEBUG final vf: %s\n", vf) + args := []string{ "-y", + "-i", input, } - 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, "-vf", vf) + 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, "-map", "0:v") args = append(args, "-map", "0:s?") @@ -158,7 +239,9 @@ func (e *Encoder) encodeVideo(input string, opusFiles []string, job *types.Job, args = append(args, outFile) + args = append([]string{"-hide_banner", "-v", "error"}, args...) cmd := exec.Command(e.ffmpegPath, args...) + fmt.Printf("DEBUG FFmpeg command: ffmpeg %s\n", strings.Join(args, " ")) if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("ffmpeg encode: %s %w", out, err) }