diff --git a/internal/ffmpeg/device/devices.go b/internal/ffmpeg/device/devices.go index e951aa76..a02cb896 100644 --- a/internal/ffmpeg/device/devices.go +++ b/internal/ffmpeg/device/devices.go @@ -9,7 +9,9 @@ import ( "sync" ) -func Init() { +func Init(bin string) { + Bin = bin + api.HandleFunc("api/ffmpeg/devices", apiDevices) } diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index e33afba0..c69ad822 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -1,16 +1,16 @@ package ffmpeg import ( - "bytes" "errors" "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/internal/exec" "github.com/AlexxIT/go2rtc/internal/ffmpeg/device" + "github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware" "github.com/AlexxIT/go2rtc/internal/rtsp" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/ffmpeg" "net/url" - "strconv" "strings" ) @@ -35,8 +35,8 @@ func Init() { return exec.Handle("exec:" + args.String()) }) - device.Bin = defaults["bin"] - device.Init() + device.Init(defaults["bin"]) + hardware.Init(defaults["bin"]) } var defaults = map[string]string{ @@ -116,19 +116,19 @@ func inputTemplate(name, s string, query url.Values) string { return strings.Replace(template, "{input}", s, 1) } -func parseArgs(s string) *Args { +func parseArgs(s string) *ffmpeg.Args { // init FFmpeg arguments - args := &Args{ - bin: defaults["bin"], - global: defaults["global"], - output: defaults["output"], + args := &ffmpeg.Args{ + Bin: defaults["bin"], + Global: defaults["global"], + Output: defaults["output"], } var query url.Values if i := strings.IndexByte(s, '#'); i > 0 { query = parseQuery(s[i+1:]) - args.video = len(query["video"]) - args.audio = len(query["audio"]) + args.Video = len(query["video"]) + args.Audio = len(query["audio"]) s = s[:i] } @@ -139,46 +139,46 @@ func parseArgs(s string) *Args { if i := strings.Index(s, "://"); i > 0 { switch s[:i] { case "http", "https", "rtmp": - args.input = inputTemplate("http", s, query) + args.Input = inputTemplate("http", s, query) case "rtsp", "rtsps": // https://ffmpeg.org/ffmpeg-protocols.html#rtsp // skip unnecessary input tracks switch { - case (args.video > 0 && args.audio > 0) || (args.video == 0 && args.audio == 0): - args.input = "-allowed_media_types video+audio " - case args.video > 0: - args.input = "-allowed_media_types video " - case args.audio > 0: - args.input = "-allowed_media_types audio " + case (args.Video > 0 && args.Audio > 0) || (args.Video == 0 && args.Audio == 0): + args.Input = "-allowed_media_types video+audio " + case args.Video > 0: + args.Input = "-allowed_media_types video " + case args.Audio > 0: + args.Input = "-allowed_media_types audio " } - args.input += inputTemplate("rtsp", s, query) + args.Input += inputTemplate("rtsp", s, query) default: - args.input = "-i " + s + args.Input = "-i " + s } } else if streams.Get(s) != nil { s = "rtsp://127.0.0.1:" + rtsp.Port + "/" + s switch { - case args.video > 0 && args.audio == 0: + case args.Video > 0 && args.Audio == 0: s += "?video" - case args.audio > 0 && args.video == 0: + case args.Audio > 0 && args.Video == 0: s += "?audio" default: s += "?video&audio" } - args.input = inputTemplate("rtsp", s, query) + args.Input = inputTemplate("rtsp", s, query) } else if strings.HasPrefix(s, "device?") { var err error - args.input, err = device.GetInput(s) + args.Input, err = device.GetInput(s) if err != nil { return nil } } else { - args.input = inputTemplate("file", s, query) + args.Input = inputTemplate("file", s, query) } if query["async"] != nil { - args.input = "-use_wallclock_as_timestamps 1 -async 1 " + args.input + args.Input = "-use_wallclock_as_timestamps 1 -async 1 " + args.Input } // Parse query params: @@ -226,7 +226,7 @@ func parseArgs(s string) *Args { } // 3. Process video codecs - if args.video > 0 { + if args.Video > 0 { for _, video := range query["video"] { if video != "copy" { if codec := defaults[video]; codec != "" { @@ -243,7 +243,7 @@ func parseArgs(s string) *Args { } // 4. Process audio codecs - if args.audio > 0 { + if args.Audio > 0 { for _, audio := range query["audio"] { if audio != "copy" { if codec := defaults[audio]; codec != "" { @@ -260,11 +260,11 @@ func parseArgs(s string) *Args { } if query["hardware"] != nil { - MakeHardware(args, query["hardware"][0]) + hardware.MakeHardware(args, query["hardware"][0], defaults) } } - if args.codecs == nil { + if args.Codecs == nil { args.AddCodec("-c copy") } @@ -283,76 +283,3 @@ func parseQuery(s string) map[string][]string { } return query } - -type Args struct { - bin string // ffmpeg - global string // -hide_banner -v error - input string // -re -stream_loop -1 -i /media/bunny.mp4 - codecs []string // -c:v libx264 -g:v 30 -preset:v ultrafast -tune:v zerolatency - filters []string // scale=1920:1080 - output string // -f rtsp {output} - - video, audio int // count of video and audio params -} - -func (a *Args) AddCodec(codec string) { - a.codecs = append(a.codecs, codec) -} - -func (a *Args) AddFilter(filter string) { - a.filters = append(a.filters, filter) -} - -func (a *Args) InsertFilter(filter string) { - a.filters = append([]string{filter}, a.filters...) -} - -func (a *Args) String() string { - b := bytes.NewBuffer(make([]byte, 0, 512)) - - b.WriteString(a.bin) - - if a.global != "" { - b.WriteByte(' ') - b.WriteString(a.global) - } - - b.WriteByte(' ') - b.WriteString(a.input) - - multimode := a.video > 1 || a.audio > 1 - var iv, ia int - - for _, codec := range a.codecs { - // support multiple video and/or audio codecs - if multimode && len(codec) >= 5 { - switch codec[:5] { - case "-c:v ": - codec = "-map 0:v:0? " + strings.ReplaceAll(codec, ":v ", ":v:"+strconv.Itoa(iv)+" ") - iv++ - case "-c:a ": - codec = "-map 0:a:0? " + strings.ReplaceAll(codec, ":a ", ":a:"+strconv.Itoa(ia)+" ") - ia++ - } - } - - b.WriteByte(' ') - b.WriteString(codec) - } - - if a.filters != nil { - for i, filter := range a.filters { - if i == 0 { - b.WriteString(" -vf ") - } else { - b.WriteByte(',') - } - b.WriteString(filter) - } - } - - b.WriteByte(' ') - b.WriteString(a.output) - - return b.String() -} diff --git a/internal/ffmpeg/hardware.go b/internal/ffmpeg/hardware/hardware.go similarity index 53% rename from internal/ffmpeg/hardware.go rename to internal/ffmpeg/hardware/hardware.go index 45001271..e560ac54 100644 --- a/internal/ffmpeg/hardware.go +++ b/internal/ffmpeg/hardware/hardware.go @@ -1,6 +1,9 @@ -package ffmpeg +package hardware import ( + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/pkg/ffmpeg" + "net/http" "os/exec" "strings" @@ -16,12 +19,16 @@ const ( EngineVideoToolbox = "videotoolbox" // macOS ) -var cache = map[string]string{} +func Init(bin string) { + api.HandleFunc("api/ffmpeg/hardware", func(w http.ResponseWriter, r *http.Request) { + api.ResponseStreams(w, ProbeAll(bin)) + }) +} // MakeHardware converts software FFmpeg args to hardware args // empty engine for autoselect -func MakeHardware(args *Args, engine string) { - for i, codec := range args.codecs { +func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string) { + for i, codec := range args.Codecs { if len(codec) < 12 { continue // skip short line (-c:v libx264...) } @@ -41,25 +48,25 @@ func MakeHardware(args *Args, engine string) { // temporary disable probe for H265 and MJPEG if engine == "" && name == "h264" { if engine = cache[name]; engine == "" { - engine = ProbeHardware(name) + engine = ProbeHardware(args.Bin, name) cache[name] = engine } } switch engine { case EngineVAAPI: - args.input = "-hwaccel vaapi -hwaccel_output_format vaapi " + args.input - args.codecs[i] = defaults[name+"/"+engine] + args.Input = "-hwaccel vaapi -hwaccel_output_format vaapi " + args.Input + args.Codecs[i] = defaults[name+"/"+engine] - for i, filter := range args.filters { + for i, filter := range args.Filters { if strings.HasPrefix(filter, "scale=") { - args.filters[i] = "scale_vaapi=" + filter[6:] + args.Filters[i] = "scale_vaapi=" + filter[6:] } if strings.HasPrefix(filter, "transpose=") { if filter == "transpose=1,transpose=1" { // 180 degrees half-turn - args.filters[i] = "transpose_vaapi=4" // reversal + args.Filters[i] = "transpose_vaapi=4" // reversal } else { - args.filters[i] = "transpose_vaapi=" + filter[10:] + args.Filters[i] = "transpose_vaapi=" + filter[10:] } } } @@ -68,43 +75,53 @@ func MakeHardware(args *Args, engine string) { args.InsertFilter("format=vaapi|nv12,hwupload") case EngineCUDA: - args.input = "-hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 2 " + args.input - args.codecs[i] = defaults[name+"/"+engine] + args.Input = "-hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 2 " + args.Input + args.Codecs[i] = defaults[name+"/"+engine] - for i, filter := range args.filters { + for i, filter := range args.Filters { if strings.HasPrefix(filter, "scale=") { - args.filters[i] = "scale_cuda=" + filter[6:] + args.Filters[i] = "scale_cuda=" + filter[6:] } } case EngineDXVA2: - args.input = "-hwaccel dxva2 -hwaccel_output_format dxva2_vld " + args.input - args.codecs[i] = defaults[name+"/"+engine] + args.Input = "-hwaccel dxva2 -hwaccel_output_format dxva2_vld " + args.Input + args.Codecs[i] = defaults[name+"/"+engine] - for i, filter := range args.filters { + for i, filter := range args.Filters { if strings.HasPrefix(filter, "scale=") { - args.filters[i] = "scale_qsv=" + filter[6:] + args.Filters[i] = "scale_qsv=" + filter[6:] } } args.InsertFilter("hwmap=derive_device=qsv,format=qsv") case EngineVideoToolbox: - args.input = "-hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld " + args.input - args.codecs[i] = defaults[name+"/"+engine] + args.Input = "-hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld " + args.Input + args.Codecs[i] = defaults[name+"/"+engine] case EngineV4L2M2M: - args.codecs[i] = defaults[name+"/"+engine] + args.Codecs[i] = defaults[name+"/"+engine] } } } -func run(arg ...string) bool { - err := exec.Command(defaults["bin"], arg...).Run() - log.Printf("%v %v", arg, err) +var cache = map[string]string{} + +func run(bin string, args string) bool { + err := exec.Command(bin, strings.Split(args, " ")...).Run() + log.Printf("%v %v", args, err) return err == nil } +func runToString(bin string, args string) string { + if run(bin, args) { + return "OK" + } else { + return "ERROR" + } +} + func cut(s string, sep byte, pos int) string { for n := 0; n < pos; n++ { if i := strings.IndexByte(s, sep); i > 0 { diff --git a/internal/ffmpeg/hardware/hardware_darwin.go b/internal/ffmpeg/hardware/hardware_darwin.go new file mode 100644 index 00000000..923f4159 --- /dev/null +++ b/internal/ffmpeg/hardware/hardware_darwin.go @@ -0,0 +1,37 @@ +package hardware + +import ( + "github.com/AlexxIT/go2rtc/internal/api" +) + +const ProbeVideoToolboxH264 = "-f lavfi -i testsrc2 -t 1 -c h264_videotoolbox -f null -" +const ProbeVideoToolboxH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_videotoolbox -f null -" + +func ProbeAll(bin string) []api.Stream { + return []api.Stream{ + { + Name: runToString(bin, ProbeVideoToolboxH264), + URL: "ffmpeg:...#video=h264#hardware=" + EngineVideoToolbox, + }, + { + Name: runToString(bin, ProbeVideoToolboxH265), + URL: "ffmpeg:...#video=h265#hardware=" + EngineVideoToolbox, + }, + } +} + +func ProbeHardware(bin, name string) string { + switch name { + case "h264": + if run(bin, ProbeVideoToolboxH264) { + return EngineVideoToolbox + } + + case "h265": + if run(bin, ProbeVideoToolboxH265) { + return EngineVideoToolbox + } + } + + return EngineSoftware +} diff --git a/internal/ffmpeg/hardware/hardware_linux.go b/internal/ffmpeg/hardware/hardware_linux.go new file mode 100644 index 00000000..cacfb10e --- /dev/null +++ b/internal/ffmpeg/hardware/hardware_linux.go @@ -0,0 +1,94 @@ +package hardware + +import ( + "github.com/AlexxIT/go2rtc/internal/api" + "runtime" +) + +const ProbeV4L2M2MH264 = "-f lavfi -i testsrc2 -t 1 -c h264_v4l2m2m -f null -" +const ProbeV4L2M2MH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_v4l2m2m -f null -" +const ProbeVAAPIH264 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c h264_vaapi -f null -" +const ProbeVAAPIH265 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c hevc_vaapi -f null -" +const ProbeVAAPIJPEG = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c mjpeg_vaapi -f null -" +const ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -" +const ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -" + +func ProbeAll(bin string) []api.Stream { + if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" { + return []api.Stream{ + { + Name: runToString(bin, ProbeV4L2M2MH264), + URL: "ffmpeg:...#video=h264#hardware=" + EngineV4L2M2M, + }, + { + Name: runToString(bin, ProbeV4L2M2MH265), + URL: "ffmpeg:...#video=h265#hardware=" + EngineV4L2M2M, + }, + } + } + + return []api.Stream{ + { + Name: runToString(bin, ProbeVAAPIH264), + URL: "ffmpeg:...#video=h264#hardware=" + EngineVAAPI, + }, + { + Name: runToString(bin, ProbeVAAPIH265), + URL: "ffmpeg:...#video=h265#hardware=" + EngineVAAPI, + }, + { + Name: runToString(bin, ProbeVAAPIJPEG), + URL: "ffmpeg:...#video=mjpeg#hardware=" + EngineVAAPI, + }, + { + Name: runToString(bin, ProbeCUDAH264), + URL: "ffmpeg:...#video=h264#hardware=" + EngineCUDA, + }, + { + Name: runToString(bin, ProbeCUDAH265), + URL: "ffmpeg:...#video=h265#hardware=" + EngineCUDA, + }, + } +} + +func ProbeHardware(bin, name string) string { + if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" { + switch name { + case "h264": + if run(bin, ProbeV4L2M2MH264) { + return EngineV4L2M2M + } + case "h265": + if run(bin, ProbeV4L2M2MH265) { + return EngineV4L2M2M + } + } + + return EngineSoftware + } + + switch name { + case "h264": + if run(bin, ProbeCUDAH264) { + return EngineCUDA + } + if run(bin, ProbeVAAPIH264) { + return EngineVAAPI + } + + case "h265": + if run(bin, ProbeCUDAH265) { + return EngineCUDA + } + if run(bin, ProbeVAAPIH265) { + return EngineVAAPI + } + + case "mjpeg": + if run(bin, ProbeVAAPIJPEG) { + return EngineVAAPI + } + } + + return EngineSoftware +} diff --git a/internal/ffmpeg/hardware/hardware_windows.go b/internal/ffmpeg/hardware/hardware_windows.go new file mode 100644 index 00000000..6a8898f2 --- /dev/null +++ b/internal/ffmpeg/hardware/hardware_windows.go @@ -0,0 +1,61 @@ +package hardware + +import "github.com/AlexxIT/go2rtc/internal/api" + +const ProbeDXVA2H264 = "-init_hw_device dxva2 -f lavfi -i testsrc2 -t 1 -c h264_qsv -f null -" +const ProbeDXVA2H265 = "-init_hw_device dxva2 -f lavfi -i testsrc2 -t 1 -c hevc_qsv -f null -" +const ProbeDXVA2JPEG = "-init_hw_device dxva2 -f lavfi -i testsrc2 -t 1 -c mjpeg_qsv -f null -" +const ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -" +const ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -" + +func ProbeAll(bin string) []api.Stream { + return []api.Stream{ + { + Name: runToString(bin, ProbeDXVA2H264), + URL: "ffmpeg:...#video=h264#hardware=" + EngineDXVA2, + }, + { + Name: runToString(bin, ProbeDXVA2H265), + URL: "ffmpeg:...#video=h265#hardware=" + EngineDXVA2, + }, + { + Name: runToString(bin, ProbeDXVA2JPEG), + URL: "ffmpeg:...#video=mjpeg#hardware=" + EngineDXVA2, + }, + { + Name: runToString(bin, ProbeCUDAH264), + URL: "ffmpeg:...#video=h264#hardware=" + EngineCUDA, + }, + { + Name: runToString(bin, ProbeCUDAH265), + URL: "ffmpeg:...#video=h265#hardware=" + EngineCUDA, + }, + } +} + +func ProbeHardware(bin, name string) string { + switch name { + case "h264": + if run(bin, ProbeCUDAH264) { + return EngineCUDA + } + if run(bin, ProbeDXVA2H264) { + return EngineDXVA2 + } + + case "h265": + if run(bin, ProbeCUDAH265) { + return EngineCUDA + } + if run(bin, ProbeDXVA2H265) { + return EngineDXVA2 + } + + case "mjpeg": + if run(bin, ProbeDXVA2JPEG) { + return EngineDXVA2 + } + } + + return EngineSoftware +} diff --git a/internal/ffmpeg/hardware_darwin.go b/internal/ffmpeg/hardware_darwin.go deleted file mode 100644 index fb4a7170..00000000 --- a/internal/ffmpeg/hardware_darwin.go +++ /dev/null @@ -1,21 +0,0 @@ -package ffmpeg - -func ProbeHardware(name string) string { - switch name { - case "h264": - if run( - "-f", "lavfi", "-i", "testsrc2", "-t", "1", - "-c", "h264_videotoolbox", "-f", "null", "-") { - return EngineVideoToolbox - } - - case "h265": - if run( - "-f", "lavfi", "-i", "testsrc2", "-t", "1", - "-c", "hevc_videotoolbox", "-f", "null", "-") { - return EngineVideoToolbox - } - } - - return EngineSoftware -} diff --git a/internal/ffmpeg/hardware_linux.go b/internal/ffmpeg/hardware_linux.go deleted file mode 100644 index 00839bd1..00000000 --- a/internal/ffmpeg/hardware_linux.go +++ /dev/null @@ -1,67 +0,0 @@ -package ffmpeg - -import ( - "runtime" -) - -func ProbeHardware(name string) string { - if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" { - switch name { - case "h264": - if run( - "-f", "lavfi", "-i", "testsrc2", "-t", "1", - "-c", "h264_v4l2m2m", "-f", "null", "-") { - return EngineV4L2M2M - } - - case "h265": - if run( - "-f", "lavfi", "-i", "testsrc2", "-t", "1", - "-c", "hevc_v4l2m2m", "-f", "null", "-") { - return EngineV4L2M2M - } - } - - return EngineSoftware - } - - switch name { - case "h264": - if run("-init_hw_device", "cuda", - "-f", "lavfi", "-i", "testsrc2", "-t", "1", - "-c", "h264_nvenc", "-f", "null", "-") { - return EngineCUDA - } - - if run("-init_hw_device", "vaapi", - "-f", "lavfi", "-i", "testsrc2", "-t", "1", - "-vf", "format=nv12,hwupload", - "-c", "h264_vaapi", "-f", "null", "-") { - return EngineVAAPI - } - - case "h265": - if run("-init_hw_device", "cuda", - "-f", "lavfi", "-i", "testsrc2", "-t", "1", - "-c", "hevc_nvenc", "-f", "null", "-") { - return EngineCUDA - } - - if run("-init_hw_device", "vaapi", - "-f", "lavfi", "-i", "testsrc2", "-t", "1", - "-vf", "format=nv12,hwupload", - "-c", "hevc_vaapi", "-f", "null", "-") { - return EngineVAAPI - } - - case "mjpeg": - if run("-init_hw_device", "vaapi", - "-f", "lavfi", "-i", "testsrc2", "-t", "1", - "-vf", "format=nv12,hwupload", - "-c", "mjpeg_vaapi", "-f", "null", "-") { - return EngineVAAPI - } - } - - return EngineSoftware -} diff --git a/internal/ffmpeg/hardware_windows.go b/internal/ffmpeg/hardware_windows.go deleted file mode 100644 index 4a259fe6..00000000 --- a/internal/ffmpeg/hardware_windows.go +++ /dev/null @@ -1,40 +0,0 @@ -package ffmpeg - -func ProbeHardware(name string) string { - switch name { - case "h264": - if run("-init_hw_device", "cuda", - "-f", "lavfi", "-i", "testsrc2", "-t", "1", - "-c", "h264_nvenc", "-f", "null", "-") { - return EngineCUDA - } - - if run("-init_hw_device", "dxva2", - "-f", "lavfi", "-i", "testsrc2", "-t", "1", - "-c", "h264_qsv", "-f", "null", "-") { - return EngineDXVA2 - } - - case "h265": - if run("-init_hw_device", "cuda", - "-f", "lavfi", "-i", "testsrc2", "-t", "1", - "-c", "hevc_nvenc", "-f", "null", "-") { - return EngineCUDA - } - - if run("-init_hw_device", "dxva2", - "-f", "lavfi", "-i", "testsrc2", "-t", "1", - "-c", "hevc_qsv", "-f", "null", "-") { - return EngineDXVA2 - } - - case "mjpeg": - if run("-init_hw_device", "dxva2", - "-f", "lavfi", "-i", "testsrc2", "-t", "1", - "-c", "mjpeg_qsv", "-f", "null", "-") { - return EngineDXVA2 - } - } - - return EngineSoftware -} diff --git a/pkg/ffmpeg/ffmpeg.go b/pkg/ffmpeg/ffmpeg.go new file mode 100644 index 00000000..10fb5bed --- /dev/null +++ b/pkg/ffmpeg/ffmpeg.go @@ -0,0 +1,80 @@ +package ffmpeg + +import ( + "bytes" + "strconv" + "strings" +) + +type Args struct { + Bin string // ffmpeg + Global string // -hide_banner -v error + Input string // -re -stream_loop -1 -i /media/bunny.mp4 + Codecs []string // -c:v libx264 -g:v 30 -preset:v ultrafast -tune:v zerolatency + Filters []string // scale=1920:1080 + Output string // -f rtsp {output} + + Video, Audio int // count of Video and Audio params +} + +func (a *Args) AddCodec(codec string) { + a.Codecs = append(a.Codecs, codec) +} + +func (a *Args) AddFilter(filter string) { + a.Filters = append(a.Filters, filter) +} + +func (a *Args) InsertFilter(filter string) { + a.Filters = append([]string{filter}, a.Filters...) +} + +func (a *Args) String() string { + b := bytes.NewBuffer(make([]byte, 0, 512)) + + b.WriteString(a.Bin) + + if a.Global != "" { + b.WriteByte(' ') + b.WriteString(a.Global) + } + + b.WriteByte(' ') + b.WriteString(a.Input) + + multimode := a.Video > 1 || a.Audio > 1 + var iv, ia int + + for _, codec := range a.Codecs { + // support multiple video and/or audio codecs + if multimode && len(codec) >= 5 { + switch codec[:5] { + case "-c:v ": + codec = "-map 0:v:0? " + strings.ReplaceAll(codec, ":v ", ":v:"+strconv.Itoa(iv)+" ") + iv++ + case "-c:a ": + codec = "-map 0:a:0? " + strings.ReplaceAll(codec, ":a ", ":a:"+strconv.Itoa(ia)+" ") + ia++ + } + } + + b.WriteByte(' ') + b.WriteString(codec) + } + + if a.Filters != nil { + for i, filter := range a.Filters { + if i == 0 { + b.WriteString(" -vf ") + } else { + b.WriteByte(',') + } + b.WriteString(filter) + } + } + + b.WriteByte(' ') + b.WriteString(a.Output) + + return b.String() +} diff --git a/www/add.html b/www/add.html index 7e3ce3b6..06daa33d 100644 --- a/www/add.html +++ b/www/add.html @@ -184,6 +184,19 @@ + +
+ +
+
+ + +