Rework FFmpeg hardware support

This commit is contained in:
Alexey Khit
2023-05-04 01:24:37 +03:00
parent 5387e88fe3
commit 2e8be342ef
11 changed files with 360 additions and 257 deletions

View File

@@ -9,7 +9,9 @@ import (
"sync" "sync"
) )
func Init() { func Init(bin string) {
Bin = bin
api.HandleFunc("api/ffmpeg/devices", apiDevices) api.HandleFunc("api/ffmpeg/devices", apiDevices)
} }

View File

@@ -1,16 +1,16 @@
package ffmpeg package ffmpeg
import ( import (
"bytes"
"errors" "errors"
"github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/exec" "github.com/AlexxIT/go2rtc/internal/exec"
"github.com/AlexxIT/go2rtc/internal/ffmpeg/device" "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/rtsp"
"github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
"net/url" "net/url"
"strconv"
"strings" "strings"
) )
@@ -35,8 +35,8 @@ func Init() {
return exec.Handle("exec:" + args.String()) return exec.Handle("exec:" + args.String())
}) })
device.Bin = defaults["bin"] device.Init(defaults["bin"])
device.Init() hardware.Init(defaults["bin"])
} }
var defaults = map[string]string{ 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) return strings.Replace(template, "{input}", s, 1)
} }
func parseArgs(s string) *Args { func parseArgs(s string) *ffmpeg.Args {
// init FFmpeg arguments // init FFmpeg arguments
args := &Args{ args := &ffmpeg.Args{
bin: defaults["bin"], Bin: defaults["bin"],
global: defaults["global"], Global: defaults["global"],
output: defaults["output"], Output: defaults["output"],
} }
var query url.Values var query url.Values
if i := strings.IndexByte(s, '#'); i > 0 { if i := strings.IndexByte(s, '#'); i > 0 {
query = parseQuery(s[i+1:]) query = parseQuery(s[i+1:])
args.video = len(query["video"]) args.Video = len(query["video"])
args.audio = len(query["audio"]) args.Audio = len(query["audio"])
s = s[:i] s = s[:i]
} }
@@ -139,46 +139,46 @@ func parseArgs(s string) *Args {
if i := strings.Index(s, "://"); i > 0 { if i := strings.Index(s, "://"); i > 0 {
switch s[:i] { switch s[:i] {
case "http", "https", "rtmp": case "http", "https", "rtmp":
args.input = inputTemplate("http", s, query) args.Input = inputTemplate("http", s, query)
case "rtsp", "rtsps": case "rtsp", "rtsps":
// https://ffmpeg.org/ffmpeg-protocols.html#rtsp // https://ffmpeg.org/ffmpeg-protocols.html#rtsp
// skip unnecessary input tracks // skip unnecessary input tracks
switch { switch {
case (args.video > 0 && args.audio > 0) || (args.video == 0 && args.audio == 0): case (args.Video > 0 && args.Audio > 0) || (args.Video == 0 && args.Audio == 0):
args.input = "-allowed_media_types video+audio " args.Input = "-allowed_media_types video+audio "
case args.video > 0: case args.Video > 0:
args.input = "-allowed_media_types video " args.Input = "-allowed_media_types video "
case args.audio > 0: case args.Audio > 0:
args.input = "-allowed_media_types audio " args.Input = "-allowed_media_types audio "
} }
args.input += inputTemplate("rtsp", s, query) args.Input += inputTemplate("rtsp", s, query)
default: default:
args.input = "-i " + s args.Input = "-i " + s
} }
} else if streams.Get(s) != nil { } else if streams.Get(s) != nil {
s = "rtsp://127.0.0.1:" + rtsp.Port + "/" + s s = "rtsp://127.0.0.1:" + rtsp.Port + "/" + s
switch { switch {
case args.video > 0 && args.audio == 0: case args.Video > 0 && args.Audio == 0:
s += "?video" s += "?video"
case args.audio > 0 && args.video == 0: case args.Audio > 0 && args.Video == 0:
s += "?audio" s += "?audio"
default: default:
s += "?video&audio" s += "?video&audio"
} }
args.input = inputTemplate("rtsp", s, query) args.Input = inputTemplate("rtsp", s, query)
} else if strings.HasPrefix(s, "device?") { } else if strings.HasPrefix(s, "device?") {
var err error var err error
args.input, err = device.GetInput(s) args.Input, err = device.GetInput(s)
if err != nil { if err != nil {
return nil return nil
} }
} else { } else {
args.input = inputTemplate("file", s, query) args.Input = inputTemplate("file", s, query)
} }
if query["async"] != nil { 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: // Parse query params:
@@ -226,7 +226,7 @@ func parseArgs(s string) *Args {
} }
// 3. Process video codecs // 3. Process video codecs
if args.video > 0 { if args.Video > 0 {
for _, video := range query["video"] { for _, video := range query["video"] {
if video != "copy" { if video != "copy" {
if codec := defaults[video]; codec != "" { if codec := defaults[video]; codec != "" {
@@ -243,7 +243,7 @@ func parseArgs(s string) *Args {
} }
// 4. Process audio codecs // 4. Process audio codecs
if args.audio > 0 { if args.Audio > 0 {
for _, audio := range query["audio"] { for _, audio := range query["audio"] {
if audio != "copy" { if audio != "copy" {
if codec := defaults[audio]; codec != "" { if codec := defaults[audio]; codec != "" {
@@ -260,11 +260,11 @@ func parseArgs(s string) *Args {
} }
if query["hardware"] != nil { 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") args.AddCodec("-c copy")
} }
@@ -283,76 +283,3 @@ func parseQuery(s string) map[string][]string {
} }
return query 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()
}

View File

@@ -1,6 +1,9 @@
package ffmpeg package hardware
import ( import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
"net/http"
"os/exec" "os/exec"
"strings" "strings"
@@ -16,12 +19,16 @@ const (
EngineVideoToolbox = "videotoolbox" // macOS 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 // MakeHardware converts software FFmpeg args to hardware args
// empty engine for autoselect // empty engine for autoselect
func MakeHardware(args *Args, engine string) { func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string) {
for i, codec := range args.codecs { for i, codec := range args.Codecs {
if len(codec) < 12 { if len(codec) < 12 {
continue // skip short line (-c:v libx264...) continue // skip short line (-c:v libx264...)
} }
@@ -41,25 +48,25 @@ func MakeHardware(args *Args, engine string) {
// temporary disable probe for H265 and MJPEG // temporary disable probe for H265 and MJPEG
if engine == "" && name == "h264" { if engine == "" && name == "h264" {
if engine = cache[name]; engine == "" { if engine = cache[name]; engine == "" {
engine = ProbeHardware(name) engine = ProbeHardware(args.Bin, name)
cache[name] = engine cache[name] = engine
} }
} }
switch engine { switch engine {
case EngineVAAPI: case EngineVAAPI:
args.input = "-hwaccel vaapi -hwaccel_output_format vaapi " + args.input args.Input = "-hwaccel vaapi -hwaccel_output_format vaapi " + args.Input
args.codecs[i] = defaults[name+"/"+engine] args.Codecs[i] = defaults[name+"/"+engine]
for i, filter := range args.filters { for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") { 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 strings.HasPrefix(filter, "transpose=") {
if filter == "transpose=1,transpose=1" { // 180 degrees half-turn 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 { } 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") args.InsertFilter("format=vaapi|nv12,hwupload")
case EngineCUDA: case EngineCUDA:
args.input = "-hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 2 " + args.input args.Input = "-hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 2 " + args.Input
args.codecs[i] = defaults[name+"/"+engine] args.Codecs[i] = defaults[name+"/"+engine]
for i, filter := range args.filters { for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") { if strings.HasPrefix(filter, "scale=") {
args.filters[i] = "scale_cuda=" + filter[6:] args.Filters[i] = "scale_cuda=" + filter[6:]
} }
} }
case EngineDXVA2: case EngineDXVA2:
args.input = "-hwaccel dxva2 -hwaccel_output_format dxva2_vld " + args.input args.Input = "-hwaccel dxva2 -hwaccel_output_format dxva2_vld " + args.Input
args.codecs[i] = defaults[name+"/"+engine] args.Codecs[i] = defaults[name+"/"+engine]
for i, filter := range args.filters { for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") { 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") args.InsertFilter("hwmap=derive_device=qsv,format=qsv")
case EngineVideoToolbox: case EngineVideoToolbox:
args.input = "-hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld " + args.input args.Input = "-hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld " + args.Input
args.codecs[i] = defaults[name+"/"+engine] args.Codecs[i] = defaults[name+"/"+engine]
case EngineV4L2M2M: case EngineV4L2M2M:
args.codecs[i] = defaults[name+"/"+engine] args.Codecs[i] = defaults[name+"/"+engine]
} }
} }
} }
func run(arg ...string) bool { var cache = map[string]string{}
err := exec.Command(defaults["bin"], arg...).Run()
log.Printf("%v %v", arg, err) func run(bin string, args string) bool {
err := exec.Command(bin, strings.Split(args, " ")...).Run()
log.Printf("%v %v", args, err)
return err == nil 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 { func cut(s string, sep byte, pos int) string {
for n := 0; n < pos; n++ { for n := 0; n < pos; n++ {
if i := strings.IndexByte(s, sep); i > 0 { if i := strings.IndexByte(s, sep); i > 0 {

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

80
pkg/ffmpeg/ffmpeg.go Normal file
View File

@@ -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()
}

View File

@@ -184,6 +184,19 @@
</script> </script>
<button id="hardware">FFmpeg Hardware</button>
<div class="module">
<table id="hardware-table">
</table>
</div>
<script>
document.getElementById('hardware').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block'
await getStreams('api/ffmpeg/hardware', 'hardware-table')
})
</script>
<button id="hass">Home Assistant</button> <button id="hass">Home Assistant</button>
<div class="module"> <div class="module">
<table id="hass-table"></table> <table id="hass-table"></table>