refactor: split ffmpeg transcode args

This commit is contained in:
ShukeBta
2026-06-24 20:43:36 +08:00
parent 68e6b5050b
commit 9bdf1a73e7
2 changed files with 166 additions and 126 deletions
-126
View File
@@ -29,7 +29,6 @@ import (
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
@@ -406,131 +405,6 @@ func hasFFmpegListEntry(output, name string) bool {
return false
}
// buildFFmpegArgs assembles the ffmpeg command line for the configured
// encoder. The function is package-level so the unit test can pin its
// behaviour without spawning a real ffmpeg process.
func buildFFmpegArgs(cfg *config.Config, source, playlist, segments string) []string {
enc := ""
if cfg.Transcoder.HardwareAccel {
enc = normalizedHardwareEncoder(cfg.Transcoder.Encoder)
}
bitrate := cfg.Transcoder.VideoBitrate
if bitrate == "" {
bitrate = "1500k"
}
maxrate := cfg.Transcoder.MaxRate
if maxrate == "" {
maxrate = "1800k"
}
bufsize := cfg.Transcoder.BufSize
if bufsize == "" {
bufsize = "3000k"
}
preset := cfg.Transcoder.Preset
if preset == "" {
preset = "veryfast"
}
height := cfg.Transcoder.MaxHeight
if height <= 0 {
height = 720
}
segDur := cfg.Transcoder.SegmentSeconds
if segDur <= 0 {
segDur = 4
}
// Hardware-accel arguments differ in three places:
// - Optional input flags (-hwaccel + device init)
// - Optional input pixel-format upload filter
// - The actual -c:v encoder name + preset/quality flag
var pre, vf, vcodec, vpreset string
switch enc {
case "nvenc":
pre = "-hwaccel cuda -hwaccel_output_format cuda"
vf = fmt.Sprintf("scale_cuda=-2:min(%d\\,ih)", height)
vcodec = "h264_nvenc"
vpreset = "p4"
case "qsv":
pre = "-hwaccel qsv -hwaccel_output_format qsv"
vf = fmt.Sprintf("scale_qsv=-1:min(%d\\,ih)", height)
vcodec = "h264_qsv"
vpreset = preset
case "vaapi":
device := cfg.App.VAAPIDevice
if device == "" {
device = "/dev/dri/renderD128"
}
pre = fmt.Sprintf("-hwaccel vaapi -vaapi_device %s -hwaccel_output_format vaapi", device)
vf = fmt.Sprintf("scale_vaapi=-2:min(%d\\,ih),format=nv12|vaapi,hwupload", height)
vcodec = "h264_vaapi"
vpreset = ""
default:
// software
pre = ""
vf = fmt.Sprintf("scale=-2:min(%d\\,ih)", height)
vcodec = "libx264"
vpreset = preset
}
args := []string{"-y", "-hide_banner", "-nostdin", "-fflags", "+genpts"}
for _, p := range splitNonEmptyArgs(pre) {
args = append(args, p)
}
if cfg.Transcoder.Realtime {
args = append(args, "-re")
}
args = append(args, "-i", source, "-map", "0:v:0?", "-map", "0:a:0?", "-vf", vf, "-c:v", vcodec)
if cfg.Transcoder.Threads > 0 && vcodec == "libx264" {
args = append(args, "-threads", strconv.Itoa(cfg.Transcoder.Threads))
}
if vpreset != "" {
args = append(args, "-preset", vpreset)
}
args = append(args,
"-pix_fmt", "yuv420p",
"-b:v", bitrate,
"-maxrate", maxrate,
"-bufsize", bufsize,
"-c:a", "aac",
"-ar", "48000",
"-b:a", "128k",
"-ac", "2",
"-force_key_frames", fmt.Sprintf("expr:gte(t,n_forced*%d)", segDur),
"-f", "hls",
"-hls_time", fmt.Sprintf("%d", segDur),
"-hls_list_size", "0",
"-hls_segment_filename", segments,
playlist,
)
return args
}
// splitNonEmptyArgs is a tiny helper that mirrors strings.Fields for the
// pre-input flag string without dragging the strings import into the hot
// path of every call to buildFFmpegArgs.
func splitNonEmptyArgs(s string) []string {
if s == "" {
return nil
}
out := make([]string, 0, 4)
field := make([]rune, 0, 16)
flush := func() {
if len(field) > 0 {
out = append(out, string(field))
field = field[:0]
}
}
for _, r := range s {
if r == ' ' || r == '\t' {
flush()
continue
}
field = append(field, r)
}
flush()
return out
}
// HumanFFmpegProfile is exposed for the admin UI / settings view.
func (t *TranscoderService) HumanFFmpegProfile() string {
return fmt.Sprintf("ffmpeg=%s, encoder=%s, output=%s",
+166
View File
@@ -0,0 +1,166 @@
package service
import (
"fmt"
"strconv"
"github.com/ShukeBta/MediaStationGo/internal/config"
)
type ffmpegArgSettings struct {
encoder string
bitrate string
maxrate string
bufsize string
preset string
height int
segmentSeconds int
realtime bool
threads int
vaapiDevice string
}
type ffmpegVideoPlan struct {
preInput string
filter string
codec string
preset string
}
// buildFFmpegArgs assembles the ffmpeg command line for the configured
// encoder. The function is package-level so the unit test can pin its
// behaviour without spawning a real ffmpeg process.
func buildFFmpegArgs(cfg *config.Config, source, playlist, segments string) []string {
settings := ffmpegArgSettingsFromConfig(cfg)
video := ffmpegVideoPlanForSettings(settings)
args := baseFFmpegArgs(video.preInput, settings.realtime)
args = appendInputAndVideoArgs(args, source, settings, video)
args = appendOutputHLSArgs(args, settings, segments, playlist)
return args
}
func ffmpegArgSettingsFromConfig(cfg *config.Config) ffmpegArgSettings {
settings := ffmpegArgSettings{
bitrate: ffmpegDefaultString(cfg.Transcoder.VideoBitrate, "1500k"),
maxrate: ffmpegDefaultString(cfg.Transcoder.MaxRate, "1800k"),
bufsize: ffmpegDefaultString(cfg.Transcoder.BufSize, "3000k"),
preset: ffmpegDefaultString(cfg.Transcoder.Preset, "veryfast"),
height: cfg.Transcoder.MaxHeight,
segmentSeconds: cfg.Transcoder.SegmentSeconds,
realtime: cfg.Transcoder.Realtime,
threads: cfg.Transcoder.Threads,
vaapiDevice: ffmpegDefaultString(cfg.App.VAAPIDevice, "/dev/dri/renderD128"),
}
if cfg.Transcoder.HardwareAccel {
settings.encoder = normalizedHardwareEncoder(cfg.Transcoder.Encoder)
}
if settings.height <= 0 {
settings.height = 720
}
if settings.segmentSeconds <= 0 {
settings.segmentSeconds = 4
}
return settings
}
func ffmpegDefaultString(value, fallback string) string {
if value == "" {
return fallback
}
return value
}
func ffmpegVideoPlanForSettings(settings ffmpegArgSettings) ffmpegVideoPlan {
switch settings.encoder {
case "nvenc":
return ffmpegVideoPlan{
preInput: "-hwaccel cuda -hwaccel_output_format cuda",
filter: fmt.Sprintf("scale_cuda=-2:min(%d\\,ih)", settings.height),
codec: "h264_nvenc",
preset: "p4",
}
case "qsv":
return ffmpegVideoPlan{
preInput: "-hwaccel qsv -hwaccel_output_format qsv",
filter: fmt.Sprintf("scale_qsv=-1:min(%d\\,ih)", settings.height),
codec: "h264_qsv",
preset: settings.preset,
}
case "vaapi":
return ffmpegVideoPlan{
preInput: fmt.Sprintf("-hwaccel vaapi -vaapi_device %s -hwaccel_output_format vaapi", settings.vaapiDevice),
filter: fmt.Sprintf("scale_vaapi=-2:min(%d\\,ih),format=nv12|vaapi,hwupload", settings.height),
codec: "h264_vaapi",
}
default:
return ffmpegVideoPlan{
filter: fmt.Sprintf("scale=-2:min(%d\\,ih)", settings.height),
codec: "libx264",
preset: settings.preset,
}
}
}
func baseFFmpegArgs(preInput string, realtime bool) []string {
args := []string{"-y", "-hide_banner", "-nostdin", "-fflags", "+genpts"}
args = append(args, splitNonEmptyArgs(preInput)...)
if realtime {
args = append(args, "-re")
}
return args
}
func appendInputAndVideoArgs(args []string, source string, settings ffmpegArgSettings, video ffmpegVideoPlan) []string {
args = append(args, "-i", source, "-map", "0:v:0?", "-map", "0:a:0?", "-vf", video.filter, "-c:v", video.codec)
if settings.threads > 0 && video.codec == "libx264" {
args = append(args, "-threads", strconv.Itoa(settings.threads))
}
if video.preset != "" {
args = append(args, "-preset", video.preset)
}
return args
}
func appendOutputHLSArgs(args []string, settings ffmpegArgSettings, segments, playlist string) []string {
return append(args,
"-pix_fmt", "yuv420p",
"-b:v", settings.bitrate,
"-maxrate", settings.maxrate,
"-bufsize", settings.bufsize,
"-c:a", "aac",
"-ar", "48000",
"-b:a", "128k",
"-ac", "2",
"-force_key_frames", fmt.Sprintf("expr:gte(t,n_forced*%d)", settings.segmentSeconds),
"-f", "hls",
"-hls_time", fmt.Sprintf("%d", settings.segmentSeconds),
"-hls_list_size", "0",
"-hls_segment_filename", segments,
playlist,
)
}
// splitNonEmptyArgs mirrors the old whitespace split for pre-input flags.
func splitNonEmptyArgs(s string) []string {
if s == "" {
return nil
}
out := make([]string, 0, 4)
field := make([]rune, 0, 16)
flush := func() {
if len(field) > 0 {
out = append(out, string(field))
field = field[:0]
}
}
for _, r := range s {
if r == ' ' || r == '\t' {
flush()
continue
}
field = append(field, r)
}
flush()
return out
}