From 8a7712a4c807ad4af1f3dde73618da050c322c8f Mon Sep 17 00:00:00 2001 From: Alex X Date: Wed, 22 May 2024 18:49:43 +0300 Subject: [PATCH] Add ffmpeg auto codec selection logic --- internal/ffmpeg/ffmpeg.go | 12 ++-- internal/ffmpeg/producer.go | 112 ++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 internal/ffmpeg/producer.go diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index ff66c835..48172bcb 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -2,6 +2,7 @@ package ffmpeg import ( "net/url" + "slices" "strings" "github.com/AlexxIT/go2rtc/internal/app" @@ -28,9 +29,14 @@ func Init() { streams.RedirectFunc("ffmpeg", func(url string) (string, error) { args := parseArgs(url[7:]) + if slices.Contains(args.Codecs, "auto") { + return "", nil // force call streams.HandleFunc("ffmpeg") + } return "exec:" + args.String(), nil }) + streams.HandleFunc("ffmpeg", NewProducer) + device.Init(defaults["bin"]) hardware.Init(defaults["bin"]) } @@ -85,6 +91,8 @@ var defaults = map[string]string{ "pcml/8000": "-c:a pcm_s16le -ar:a 8000 -ac:a 1", "pcml/44100": "-c:a pcm_s16le -ar:a 44100 -ac:a 1", + "opus/48000/2": "-c:a libopus -application:a lowdelay -min_comp 0 -ar:a 48000 -ac:a 2", + // hardware Intel and AMD on Linux // better not to set `-async_depth:v 1` like for QSV, because framedrops // `-bf 0` - disable B-frames is very important @@ -202,10 +210,6 @@ func parseArgs(s string) *ffmpeg.Args { args.Input = inputTemplate("file", s, query) } - if args.Input == "" { - return nil - } - if query["async"] != nil { args.Input = "-use_wallclock_as_timestamps 1 -async 1 " + args.Input } diff --git a/internal/ffmpeg/producer.go b/internal/ffmpeg/producer.go new file mode 100644 index 00000000..8cb3cb10 --- /dev/null +++ b/internal/ffmpeg/producer.go @@ -0,0 +1,112 @@ +package ffmpeg + +import ( + "encoding/json" + "errors" + "net/url" + "strconv" + "strings" + + "github.com/AlexxIT/go2rtc/internal/streams" + "github.com/AlexxIT/go2rtc/pkg/aac" + "github.com/AlexxIT/go2rtc/pkg/core" +) + +type Producer struct { + core.SuperProducer + url string + query url.Values + ffmpeg core.Producer +} + +// NewProducer - FFmpeg producer with auto selection video/audio codec based on client capabilities +func NewProducer(url string) (core.Producer, error) { + p := &Producer{} + + i := strings.IndexByte(url, '#') + p.url, p.query = url[:i], streams.ParseQuery(url[i+1:]) + + // ffmpeg.NewProducer support only one audio + if len(p.query["video"]) != 0 || len(p.query["audio"]) != 1 { + return nil, errors.New("ffmpeg: unsupported params: " + url[i:]) + } + + p.Type = "FFmpeg producer" + p.Medias = []*core.Media{ + { + Kind: core.KindAudio, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + {Name: core.CodecOpus, ClockRate: 48000, Channels: 2}, + {Name: core.CodecAAC, ClockRate: 16000, FmtpLine: aac.FMTP + "1408"}, + {Name: core.CodecPCM, ClockRate: 16000}, + {Name: core.CodecPCM, ClockRate: 8000}, + {Name: core.CodecPCMA, ClockRate: 16000}, + {Name: core.CodecPCMA, ClockRate: 8000}, + {Name: core.CodecPCMU, ClockRate: 16000}, + {Name: core.CodecPCMU, ClockRate: 8000}, + }, + }, + } + return p, nil +} + +func (p *Producer) Start() error { + var err error + if p.ffmpeg, err = streams.GetProducer(p.newURL()); err != nil { + return err + } + + for i, media := range p.ffmpeg.GetMedias() { + track, err := p.ffmpeg.GetTrack(media, media.Codecs[0]) + if err != nil { + return err + } + p.Receivers[i].Replace(track) + } + + return p.ffmpeg.Start() +} + +func (p *Producer) Stop() error { + if p.ffmpeg == nil { + return nil + } + return p.ffmpeg.Stop() +} + +func (p *Producer) MarshalJSON() ([]byte, error) { + if p.ffmpeg == nil { + return json.Marshal(p.SuperProducer) + } + return json.Marshal(p.ffmpeg) +} + +func (p *Producer) newURL() string { + s := p.url + // rewrite codecs in url from auto to known presets from defaults + for _, receiver := range p.Receivers { + codec := receiver.Codec + switch codec.Name { + case core.CodecPCMU, core.CodecPCMA: + s += "#audio=" + strings.ToLower(codec.Name) + if codec.ClockRate != 0 { + s += "/" + strconv.Itoa(int(codec.ClockRate)) + } + case core.CodecAAC: + s += "#audio=aac/16000" + case core.CodecOpus: + s += "#audio=opus/48000/2" + } + } + // add other params + for key, values := range p.query { + if key != "audio" { + for _, value := range values { + s += "#" + key + "=" + value + } + } + } + + return s +}