mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-06 13:26:38 +08:00
refactor: split ffmpeg transcode args
This commit is contained in:
@@ -29,7 +29,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -406,131 +405,6 @@ func hasFFmpegListEntry(output, name string) bool {
|
|||||||
return false
|
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.
|
// HumanFFmpegProfile is exposed for the admin UI / settings view.
|
||||||
func (t *TranscoderService) HumanFFmpegProfile() string {
|
func (t *TranscoderService) HumanFFmpegProfile() string {
|
||||||
return fmt.Sprintf("ffmpeg=%s, encoder=%s, output=%s",
|
return fmt.Sprintf("ffmpeg=%s, encoder=%s, output=%s",
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user