fix: use ffprobe for SAR detection and calculate zscale width
This commit is contained in:
+106
-23
@@ -1,6 +1,7 @@
|
|||||||
package encoder
|
package encoder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -13,12 +14,26 @@ import (
|
|||||||
|
|
||||||
type Encoder struct {
|
type Encoder struct {
|
||||||
ffmpegPath string
|
ffmpegPath string
|
||||||
|
ffprobePath string
|
||||||
opusencPath 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 {
|
func New() *Encoder {
|
||||||
return &Encoder{
|
return &Encoder{
|
||||||
ffmpegPath: "ffmpeg",
|
ffmpegPath: "ffmpeg",
|
||||||
|
ffprobePath: "ffprobe",
|
||||||
opusencPath: "opusenc",
|
opusencPath: "opusenc",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,6 +42,9 @@ func (e *Encoder) CheckDeps() error {
|
|||||||
if _, err := exec.LookPath(e.ffmpegPath); err != nil {
|
if _, err := exec.LookPath(e.ffmpegPath); err != nil {
|
||||||
return fmt.Errorf("ffmpeg not found")
|
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 {
|
if _, err := exec.LookPath(e.opusencPath); err != nil {
|
||||||
return fmt.Errorf("opusenc not found")
|
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) {
|
func (e *Encoder) GetMediaInfo(path string) (width, height int, interlaced bool, err error) {
|
||||||
cmd := exec.Command(e.ffmpegPath, "-i", path, "-hide_banner", "-vf", "idet")
|
cmd := exec.Command(e.ffprobePath, "-v", "error", "-show_streams", "-print_format", "json", path)
|
||||||
output, _ := cmd.StderrPipe()
|
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()
|
cmd.Start()
|
||||||
|
|
||||||
buf := make([]byte, 16384)
|
buf := make([]byte, 16384)
|
||||||
n, _ := output.Read(buf)
|
n, _ := stderr.Read(buf)
|
||||||
output.Close()
|
stderr.Close()
|
||||||
|
|
||||||
outputStr := string(buf[:n])
|
outputStr := string(buf[:n])
|
||||||
|
interlaced = strings.Contains(outputStr, "TFF") || strings.Contains(outputStr, "BFF")
|
||||||
|
|
||||||
var w, h int
|
fmt.Printf("DEBUG interlaced: %v\n", interlaced)
|
||||||
for i := 0; i < len(outputStr)-10; i++ {
|
|
||||||
if outputStr[i] >= '0' && outputStr[i] <= '9' {
|
return width, height, interlaced, nil
|
||||||
fmt.Sscanf(outputStr[i:], "%dx%d", &w, &h)
|
}
|
||||||
if w > 0 && h > 0 {
|
|
||||||
break
|
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")
|
zscale := fmt.Sprintf("zscale=w=%d:h=%d:filter=spline36", newWidth, height)
|
||||||
|
return zscale, newWidth, nil
|
||||||
return w, h, interlaced, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Encoder) Transcode(input string, job *types.Job, metadata *types.Metadata, interlaced bool) error {
|
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)
|
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 {
|
if interlaced {
|
||||||
vf = "bwdif=mode=0:par=-1:-1," + vf
|
vf = "bwdif=mode=0:par=-1:-1," + vf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Printf("DEBUG final vf: %s\n", vf)
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"-y",
|
"-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 {
|
for _, opus := range opusFiles {
|
||||||
args = append(args, "-i", opus)
|
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:v")
|
||||||
args = append(args, "-map", "0:s?")
|
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(args, outFile)
|
||||||
|
|
||||||
|
args = append([]string{"-hide_banner", "-v", "error"}, args...)
|
||||||
cmd := exec.Command(e.ffmpegPath, 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 {
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
return fmt.Errorf("ffmpeg encode: %s %w", out, err)
|
return fmt.Errorf("ffmpeg encode: %s %w", out, err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user