install go2rtc on bob

This commit is contained in:
2026-04-04 19:36:14 +02:00
parent f0b56e63d1
commit ccf88187b8
537 changed files with 69213 additions and 0 deletions
@@ -0,0 +1,62 @@
# FFmpeg
You can get any stream, file or device via FFmpeg and push it to go2rtc. The app will automatically start FFmpeg with the proper arguments when someone starts watching the stream.
- FFmpeg preinstalled for **Docker** and **Home Assistant add-on** users
- **Home Assistant add-on** users can target files from [/media](https://www.home-assistant.io/more-info/local-media/setup-media/) folder
## Configuration
Format: `ffmpeg:{input}#{param1}#{param2}#{param3}`. Examples:
```yaml
streams:
# [FILE] all tracks will be copied without transcoding codecs
file1: ffmpeg:/media/BigBuckBunny.mp4
# [FILE] video will be transcoded to H264, audio will be skipped
file2: ffmpeg:/media/BigBuckBunny.mp4#video=h264
# [FILE] video will be copied, audio will be transcoded to PCMU
file3: ffmpeg:/media/BigBuckBunny.mp4#video=copy#audio=pcmu
# [HLS] video will be copied, audio will be skipped
hls: ffmpeg:https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear5/prog_index.m3u8#video=copy
# [MJPEG] video will be transcoded to H264
mjpeg: ffmpeg:http://185.97.122.128/cgi-bin/faststream.jpg#video=h264
# [RTSP] video with rotation, should be transcoded, so select H264
rotate: ffmpeg:rtsp://12345678@192.168.1.123/av_stream/ch0#video=h264#rotate=90
```
All transcoding formats have [built-in templates](ffmpeg.go): `h264`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac`, `aac/16000`.
But you can override them via YAML config. You can also add your own formats to the config and use them with source params.
```yaml
ffmpeg:
bin: ffmpeg # path to ffmpeg binary
global: "-hide_banner"
timeout: 5 # default timeout in seconds for rtsp inputs
h264: "-codec:v libx264 -g:v 30 -preset:v superfast -tune:v zerolatency -profile:v main -level:v 4.1"
mycodec: "-any args that supported by ffmpeg..."
myinput: "-fflags nobuffer -flags low_delay -timeout {timeout} -i {input}"
myraw: "-ss 00:00:20"
```
- You can use go2rtc stream name as ffmpeg input (ex. `ffmpeg:camera1#video=h264`)
- You can use `video` and `audio` params multiple times (ex. `#video=copy#audio=copy#audio=pcmu`)
- You can use `rotate` param with `90`, `180`, `270` or `-90` values, important with transcoding (ex. `#video=h264#rotate=90`)
- You can use `width` and/or `height` params, important with transcoding (ex. `#video=h264#width=1280`)
- You can use `drawtext` to add a timestamp (ex. `drawtext=x=2:y=2:fontsize=12:fontcolor=white:box=1:boxcolor=black`)
- This will greatly increase the CPU of the server, even with hardware acceleration
- You can use `timeout` param to set RTSP input timeout in seconds (ex. `#timeout=10`)
- You can use `raw` param for any additional FFmpeg arguments (ex. `#raw=-vf transpose=1`)
- You can use `input` param to override default input template (ex. `#input=rtsp/udp` will change RTSP transport from TCP to UDP+TCP)
- You can use raw input value (ex. `#input=-timeout {timeout} -i {input}`)
- You can add your own input templates
Read more about [hardware acceleration](hardware/README.md).
**PS.** It is recommended to check the available hardware in the WebUI add page.
@@ -0,0 +1,51 @@
package ffmpeg
import (
"net/http"
"strings"
"github.com/AlexxIT/go2rtc/internal/streams"
)
func apiFFmpeg(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "", http.StatusMethodNotAllowed)
return
}
query := r.URL.Query()
dst := query.Get("dst")
stream := streams.Get(dst)
if stream == nil {
http.Error(w, "", http.StatusNotFound)
return
}
var src string
if s := query.Get("file"); s != "" {
if streams.Validate(s) == nil {
src = "ffmpeg:" + s + "#audio=auto#input=file"
}
} else if s = query.Get("live"); s != "" {
if streams.Validate(s) == nil {
src = "ffmpeg:" + s + "#audio=auto"
}
} else if s = query.Get("text"); s != "" {
if strings.IndexAny(s, `'"&%$`) < 0 {
src = "ffmpeg:tts?text=" + s
if s = query.Get("voice"); s != "" {
src += "&voice=" + s
}
src += "#audio=auto"
}
}
if src == "" {
http.Error(w, "", http.StatusBadRequest)
return
}
if err := stream.Play(src); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
@@ -0,0 +1,22 @@
# FFmpeg Device
You can get video from any USB camera or Webcam as RTSP or WebRTC stream. This is part of FFmpeg integration.
- check available devices in web interface
- `video_size` and `framerate` must be supported by your camera!
- for Linux supported only video for now
- for macOS you can stream FaceTime camera or whole desktop!
- for macOS important to set right framerate
## Configuration
Format: `ffmpeg:device?{input-params}#{param1}#{param2}#{param3}`
```yaml
streams:
linux_usbcam: ffmpeg:device?video=0&video_size=1280x720#video=h264
windows_webcam: ffmpeg:device?video=0#video=h264
macos_facetime: ffmpeg:device?video=0&audio=1&video_size=1280x720&framerate=30#video=h264#audio=pcma
```
**PS.** It is recommended to check the available devices in the WebUI add page.
@@ -0,0 +1,99 @@
//go:build freebsd || netbsd || openbsd || dragonfly
package device
import (
"net/url"
"os"
"os/exec"
"regexp"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func queryToInput(query url.Values) string {
if video := query.Get("video"); video != "" {
// https://ffmpeg.org/ffmpeg-devices.html#video4linux2_002c-v4l2
input := "-f v4l2"
for key, value := range query {
switch key {
case "resolution":
input += " -video_size " + value[0]
case "video_size", "pixel_format", "input_format", "framerate", "use_libv4l2":
input += " -" + key + " " + value[0]
}
}
return input + " -i " + indexToItem(videos, video)
}
if audio := query.Get("audio"); audio != "" {
input := "-f oss"
for key, value := range query {
switch key {
case "channels", "sample_rate":
input += " -" + key + " " + value[0]
}
}
return input + " -i " + indexToItem(audios, audio)
}
return ""
}
func initDevices() {
files, err := os.ReadDir("/dev")
if err != nil {
return
}
for _, file := range files {
if !strings.HasPrefix(file.Name(), core.KindVideo) {
continue
}
name := "/dev/" + file.Name()
cmd := exec.Command(
Bin, "-hide_banner", "-f", "v4l2", "-list_formats", "all", "-i", name,
)
b, _ := cmd.CombinedOutput()
// [video4linux2,v4l2 @ 0x860b92280] Raw : yuyv422 : YUYV 4:2:2 : 640x480 160x120 176x144 320x176 320x240 352x288 432x240 544x288 640x360 752x416 800x448 800x600 864x480 960x544 960x720 1024x576 1184x656 1280x720 1280x960
// [video4linux2,v4l2 @ 0x860b92280] Compressed: mjpeg : Motion-JPEG : 640x480 160x120 176x144 320x176 320x240 352x288 432x240 544x288 640x360 752x416 800x448 800x600 864x480 960x544 960x720 1024x576 1184x656 1280x720 1280x960
re := regexp.MustCompile("(Raw *|Compressed): +(.+?) : +(.+?) : (.+)")
m := re.FindAllStringSubmatch(string(b), -1)
for _, i := range m {
size, _, _ := strings.Cut(i[4], " ")
stream := &api.Source{
Name: i[3],
Info: i[4],
URL: "ffmpeg:device?video=" + name + "&input_format=" + i[2] + "&video_size=" + size,
}
if i[1] != "Compressed" {
stream.URL += "#video=h264#hardware"
}
videos = append(videos, name)
streams = append(streams, stream)
}
}
err = exec.Command(Bin, "-f", "oss", "-i", "/dev/dsp", "-t", "1", "-f", "null", "-").Run()
if err == nil {
stream := &api.Source{
Name: "OSS default",
Info: " ",
URL: "ffmpeg:device?audio=default&channels=1&sample_rate=16000&#audio=opus",
}
audios = append(audios, "default")
streams = append(streams, stream)
}
}
@@ -0,0 +1,88 @@
//go:build darwin || ios
package device
import (
"net/url"
"os/exec"
"regexp"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func queryToInput(query url.Values) string {
video := query.Get("video")
audio := query.Get("audio")
if video == "" && audio == "" {
return ""
}
// https://ffmpeg.org/ffmpeg-devices.html#avfoundation
input := "-f avfoundation"
if video != "" {
video = indexToItem(videos, video)
for key, value := range query {
switch key {
case "resolution":
input += " -video_size " + value[0]
case "pixel_format", "framerate", "video_size", "capture_cursor", "capture_mouse_clicks", "capture_raw_data":
input += " -" + key + " " + value[0]
}
}
}
if audio != "" {
audio = indexToItem(audios, audio)
}
return input + ` -i "` + video + `:` + audio + `"`
}
func initDevices() {
// [AVFoundation indev @ 0x147f04510] AVFoundation video devices:
// [AVFoundation indev @ 0x147f04510] [0] FaceTime HD Camera
// [AVFoundation indev @ 0x147f04510] [1] Capture screen 0
// [AVFoundation indev @ 0x147f04510] AVFoundation audio devices:
// [AVFoundation indev @ 0x147f04510] [0] MacBook Pro Microphone
cmd := exec.Command(
Bin, "-hide_banner", "-list_devices", "true", "-f", "avfoundation", "-i", "",
)
b, _ := cmd.CombinedOutput()
re := regexp.MustCompile(`\[\d+] (.+)`)
var kind string
for _, line := range strings.Split(string(b), "\n") {
switch {
case strings.HasSuffix(line, "video devices:"):
kind = core.KindVideo
continue
case strings.HasSuffix(line, "audio devices:"):
kind = core.KindAudio
continue
}
m := re.FindStringSubmatch(line)
if m == nil {
continue
}
name := m[1]
switch kind {
case core.KindVideo:
videos = append(videos, name)
case core.KindAudio:
audios = append(audios, name)
}
streams = append(streams, &api.Source{
Name: name, URL: "ffmpeg:device?" + kind + "=" + name,
})
}
}
@@ -0,0 +1,101 @@
//go:build unix && !darwin && !freebsd && !netbsd && !openbsd && !dragonfly
package device
import (
"net/url"
"os"
"os/exec"
"regexp"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func queryToInput(query url.Values) string {
if video := query.Get("video"); video != "" {
// https://ffmpeg.org/ffmpeg-devices.html#video4linux2_002c-v4l2
input := "-f v4l2"
for key, value := range query {
switch key {
case "resolution":
input += " -video_size " + value[0]
case "video_size", "pixel_format", "input_format", "framerate", "use_libv4l2":
input += " -" + key + " " + value[0]
}
}
return input + " -i " + indexToItem(videos, video)
}
if audio := query.Get("audio"); audio != "" {
// https://trac.ffmpeg.org/wiki/Capture/ALSA
input := "-f alsa"
for key, value := range query {
switch key {
case "channels", "sample_rate":
input += " -" + key + " " + value[0]
}
}
return input + " -i " + indexToItem(audios, audio)
}
return ""
}
func initDevices() {
files, err := os.ReadDir("/dev")
if err != nil {
return
}
for _, file := range files {
if !strings.HasPrefix(file.Name(), core.KindVideo) {
continue
}
name := "/dev/" + file.Name()
cmd := exec.Command(
Bin, "-hide_banner", "-f", "v4l2", "-list_formats", "all", "-i", name,
)
b, _ := cmd.CombinedOutput()
// [video4linux2,v4l2 @ 0x204e1c0] Compressed: mjpeg : Motion-JPEG : 640x360 1280x720 1920x1080
// [video4linux2,v4l2 @ 0x204e1c0] Raw : yuyv422 : YUYV 4:2:2 : 640x360 1280x720 1920x1080
// [video4linux2,v4l2 @ 0x204e1c0] Compressed: h264 : H.264 : 640x360 1280x720 1920x1080
re := regexp.MustCompile("(Raw *|Compressed): +(.+?) : +(.+?) : (.+)")
m := re.FindAllStringSubmatch(string(b), -1)
for _, i := range m {
size, _, _ := strings.Cut(i[4], " ")
stream := &api.Source{
Name: i[3],
Info: i[4],
URL: "ffmpeg:device?video=" + name + "&input_format=" + i[2] + "&video_size=" + size,
}
if i[1] != "Compressed" {
stream.URL += "#video=h264#hardware"
}
videos = append(videos, name)
streams = append(streams, stream)
}
}
err = exec.Command(Bin, "-f", "alsa", "-i", "default", "-t", "1", "-f", "null", "-").Run()
if err == nil {
stream := &api.Source{
Name: "ALSA default",
Info: " ",
URL: "ffmpeg:device?audio=default&channels=1&sample_rate=16000&#audio=opus",
}
audios = append(audios, "default")
streams = append(streams, stream)
}
}
@@ -0,0 +1,90 @@
//go:build windows
package device
import (
"net/url"
"os/exec"
"regexp"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func queryToInput(query url.Values) string {
video := query.Get("video")
audio := query.Get("audio")
if video == "" && audio == "" {
return ""
}
// https://ffmpeg.org/ffmpeg-devices.html#dshow
input := "-f dshow"
if video != "" {
video = indexToItem(videos, video)
for key, value := range query {
switch key {
case "resolution":
input += " -video_size " + value[0]
case "video_size", "framerate", "pixel_format":
input += " -" + key + " " + value[0]
}
}
}
if audio != "" {
audio = indexToItem(audios, audio)
for key, value := range query {
switch key {
case "sample_rate", "sample_size", "channels", "audio_buffer_size":
input += " -" + key + " " + value[0]
}
}
}
if video != "" {
input += ` -i "video=` + video
if audio != "" {
input += `:audio=` + audio
}
input += `"`
} else {
input += ` -i "audio=` + audio + `"`
}
return input
}
func initDevices() {
cmd := exec.Command(
Bin, "-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "",
)
b, _ := cmd.CombinedOutput()
re := regexp.MustCompile(`"([^"]+)" \((video|audio)\)`)
for _, m := range re.FindAllStringSubmatch(string(b), -1) {
name := m[1]
kind := m[2]
stream := &api.Source{
Name: name, URL: "ffmpeg:device?" + kind + "=" + name,
}
switch kind {
case core.KindVideo:
videos = append(videos, name)
stream.URL += "#video=h264#hardware"
case core.KindAudio:
audios = append(audios, name)
stream.URL += "&channels=1&sample_rate=16000&audio_buffer_size=10"
}
streams = append(streams, stream)
}
}
@@ -0,0 +1,46 @@
package device
import (
"net/http"
"net/url"
"strconv"
"sync"
"github.com/AlexxIT/go2rtc/internal/api"
)
func Init(bin string) {
Bin = bin
api.HandleFunc("api/ffmpeg/devices", apiDevices)
}
func GetInput(src string) string {
query, err := url.ParseQuery(src)
if err != nil {
return ""
}
runonce.Do(initDevices)
return queryToInput(query)
}
var Bin string
var videos, audios []string
var streams []*api.Source
var runonce sync.Once
func apiDevices(w http.ResponseWriter, r *http.Request) {
runonce.Do(initDevices)
api.ResponseSources(w, streams)
}
func indexToItem(items []string, index string) string {
if i, err := strconv.Atoi(index); err == nil && i < len(items) {
return items[i]
}
return index
}
@@ -0,0 +1,388 @@
package ffmpeg
import (
"net/url"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/ffmpeg/device"
"github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware"
"github.com/AlexxIT/go2rtc/internal/ffmpeg/virtual"
"github.com/AlexxIT/go2rtc/internal/rtsp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
"github.com/rs/zerolog"
)
func Init() {
var cfg struct {
Mod map[string]string `yaml:"ffmpeg"`
Log struct {
Level string `yaml:"ffmpeg"`
} `yaml:"log"`
}
cfg.Mod = defaults // will be overriden from yaml
cfg.Log.Level = "error"
app.LoadConfig(&cfg)
log = app.GetLogger("ffmpeg")
// zerolog levels: trace debug info warn error fatal panic disabled
// FFmpeg levels: trace debug verbose info warning error fatal panic quiet
if cfg.Log.Level == "warn" {
cfg.Log.Level = "warning"
}
defaults["global"] += " -v " + cfg.Log.Level
streams.RedirectFunc("ffmpeg", func(url string) (string, error) {
if _, err := Version(); err != nil {
return "", err
}
args := parseArgs(url[7:])
if core.Contains(args.Codecs, "auto") {
return "", nil // force call streams.HandleFunc("ffmpeg")
}
return "exec:" + args.String(), nil
})
streams.HandleFunc("ffmpeg", NewProducer)
api.HandleFunc("api/ffmpeg", apiFFmpeg)
device.Init(defaults["bin"])
hardware.Init(defaults["bin"])
}
var defaults = map[string]string{
"bin": "ffmpeg",
"global": "-hide_banner",
"timeout": "5",
// inputs
"file": "-re -i {input}",
"http": "-fflags nobuffer -flags low_delay -i {input}",
"rtsp": "-fflags nobuffer -flags low_delay -timeout {timeout} -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i {input}",
"rtsp/udp": "-fflags nobuffer -flags low_delay -timeout {timeout} -user_agent go2rtc/ffmpeg -i {input}",
// output
"output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}",
"output/mjpeg": "-f mjpeg -",
"output/raw": "-f yuv4mpegpipe -",
"output/aac": "-f adts -",
"output/wav": "-f wav -",
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
// `-tune zerolatency` - for minimal latency
// `-profile high -level 4.1` - most used streaming profile
// `-pix_fmt:v yuv420p` - important for Telegram
"h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p",
"h265": "-c:v libx265 -g 50 -profile:v main -x265-params level=5.1:high-tier=0 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p",
"mjpeg": "-c:v mjpeg",
//"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
"raw": "-c:v rawvideo",
"raw/gray8": "-c:v rawvideo -pix_fmt:v gray8",
"raw/yuv420p": "-c:v rawvideo -pix_fmt:v yuv420p",
"raw/yuv422p": "-c:v rawvideo -pix_fmt:v yuv422p",
"raw/yuv444p": "-c:v rawvideo -pix_fmt:v yuv444p",
// https://ffmpeg.org/ffmpeg-codecs.html#libopus-1
// https://github.com/pion/webrtc/issues/1514
// https://ffmpeg.org/ffmpeg-resampler.html
// `-async 1` or `-min_comp 0` - force resampling for static timestamp inc, important for WebRTC audio quality
"opus": "-c:a libopus -application:a lowdelay -min_comp 0",
"opus/16000": "-c:a libopus -application:a lowdelay -min_comp 0 -ar:a 16000 -ac:a 1",
"pcmu": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
"pcmu/8000": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
"pcmu/16000": "-c:a pcm_mulaw -ar:a 16000 -ac:a 1",
"pcmu/48000": "-c:a pcm_mulaw -ar:a 48000 -ac:a 1",
"pcma": "-c:a pcm_alaw -ar:a 8000 -ac:a 1",
"pcma/8000": "-c:a pcm_alaw -ar:a 8000 -ac:a 1",
"pcma/16000": "-c:a pcm_alaw -ar:a 16000 -ac:a 1",
"pcma/48000": "-c:a pcm_alaw -ar:a 48000 -ac:a 1",
"aac": "-c:a aac", // keep sample rate and channels
"aac/16000": "-c:a aac -ar:a 16000 -ac:a 1",
"mp3": "-c:a libmp3lame -q:a 8",
"pcm": "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
"pcm/8000": "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
"pcm/16000": "-c:a pcm_s16be -ar:a 16000 -ac:a 1",
"pcm/48000": "-c:a pcm_s16be -ar:a 48000 -ac:a 1",
"pcml": "-c:a pcm_s16le -ar:a 8000 -ac:a 1",
"pcml/8000": "-c:a pcm_s16le -ar:a 8000 -ac:a 1",
"pcml/16000": "-c:a pcm_s16le -ar:a 16000 -ac:a 1",
"pcml/44100": "-c:a pcm_s16le -ar:a 44100 -ac:a 1",
// 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
"h264/vaapi": "-c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0",
"h265/vaapi": "-c:v hevc_vaapi -g 50 -bf 0 -profile:v main -level:v 5.1 -sei:v 0",
"mjpeg/vaapi": "-c:v mjpeg_vaapi",
// hardware Raspberry
"h264/v4l2m2m": "-c:v h264_v4l2m2m -g 50 -bf 0",
"h265/v4l2m2m": "-c:v hevc_v4l2m2m -g 50 -bf 0",
// hardware Rockchip
// important to use custom ffmpeg https://github.com/AlexxIT/go2rtc/issues/768
// hevc - doesn't have a profile setting
"h264/rkmpp": "-c:v h264_rkmpp -g 50 -bf 0 -profile:v high -level:v 4.1",
"h265/rkmpp": "-c:v hevc_rkmpp -g 50 -bf 0 -profile:v main -level:v 5.1",
"mjpeg/rkmpp": "-c:v mjpeg_rkmpp",
// hardware NVidia on Linux and Windows
// preset=p2 - faster, tune=ll - low latency
"h264/cuda": "-c:v h264_nvenc -g 50 -bf 0 -profile:v high -level:v auto -preset:v p2 -tune:v ll",
"h265/cuda": "-c:v hevc_nvenc -g 50 -bf 0 -profile:v main -level:v auto",
// hardware Intel on Windows
"h264/dxva2": "-c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1",
"h265/dxva2": "-c:v hevc_qsv -g 50 -bf 0 -profile:v main -level:v 5.1 -async_depth:v 1",
"mjpeg/dxva2": "-c:v mjpeg_qsv",
// hardware macOS
"h264/videotoolbox": "-c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1",
"h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v main -level:v 5.1",
}
var log zerolog.Logger
// configTemplate - return template from config (defaults) if exist or return raw template
func configTemplate(template string) string {
if s := defaults[template]; s != "" {
return s
}
return template
}
// inputTemplate - select input template from YAML config by template name
// if query has input param - select another template by this name
// if there is no another template - use input param as template
func inputTemplate(name, s string, query url.Values) string {
var template string
if input := query.Get("input"); input != "" {
template = configTemplate(input)
} else {
template = defaults[name]
}
if strings.Contains(template, "{timeout}") {
timeout := query.Get("timeout")
if timeout == "" {
timeout = defaults["timeout"]
}
template = strings.Replace(template, "{timeout}", timeout+"000000", 1)
}
return strings.Replace(template, "{input}", s, 1)
}
func parseArgs(s string) *ffmpeg.Args {
// init FFmpeg arguments
args := &ffmpeg.Args{
Bin: defaults["bin"],
Global: defaults["global"],
Output: defaults["output"],
Version: verAV,
}
var source = s
var query url.Values
if i := strings.IndexByte(s, '#'); i >= 0 {
query = streams.ParseQuery(s[i+1:])
args.Video = len(query["video"])
args.Audio = len(query["audio"])
s = s[:i]
}
// Parse input:
// 1. Input as xxxx:// link (http or rtsp or any other)
// 2. Input as stream name
// 3. Input as FFmpeg device (local USB camera)
if i := strings.Index(s, "://"); i > 0 {
switch s[:i] {
case "http", "https", "rtmp":
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 "
}
args.Input += inputTemplate("rtsp", s, query)
default:
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:
s += "?video"
case args.Audio > 0 && args.Video == 0:
s += "?audio"
default:
s += "?video&audio"
}
s += "&source=ffmpeg:" + url.QueryEscape(source)
for _, v := range query["query"] {
s += "&" + v
}
args.Input = inputTemplate("rtsp", s, query)
} else if i = strings.Index(s, "?"); i > 0 {
switch s[:i] {
case "device":
args.Input = device.GetInput(s[i+1:])
case "virtual":
args.Input = virtual.GetInput(s[i+1:])
case "tts":
args.Input = virtual.GetInputTTS(s[i+1:])
}
} else {
args.Input = inputTemplate("file", s, query)
}
if query["async"] != nil {
args.Input = "-use_wallclock_as_timestamps 1 -async 1 " + args.Input
}
// Parse query params:
// 1. `width`/`height` params
// 2. `rotate` param
// 3. `video` params (support multiple)
// 4. `audio` params (support multiple)
// 5. `hardware` param
if query != nil {
// 1. Process raw params for FFmpeg
for _, raw := range query["raw"] {
// support templates https://github.com/AlexxIT/go2rtc/issues/487
raw = configTemplate(raw)
args.AddCodec(raw)
}
// 2. Process video filters (resize and rotation)
if query["width"] != nil || query["height"] != nil {
filter := "scale="
if query["width"] != nil {
filter += query["width"][0]
} else {
filter += "-1"
}
filter += ":"
if query["height"] != nil {
filter += query["height"][0]
} else {
filter += "-1"
}
args.AddFilter(filter)
}
if query["rotate"] != nil {
var filter string
switch query["rotate"][0] {
case "90":
filter = "transpose=1" // 90 degrees clockwise
case "180":
filter = "transpose=1,transpose=1"
case "-90", "270":
filter = "transpose=2" // 90 degrees counterclockwise
}
if filter != "" {
args.AddFilter(filter)
}
}
for _, drawtext := range query["drawtext"] {
// support templates https://github.com/AlexxIT/go2rtc/issues/487
drawtext = configTemplate(drawtext)
// support default timestamp format
if !strings.Contains(drawtext, "text=") {
drawtext += `:text='%{localtime\:%Y-%m-%d %X}'`
}
args.AddFilter("drawtext=" + drawtext)
}
// 3. Process video codecs
if args.Video > 0 {
for _, video := range query["video"] {
if video != "copy" {
if codec := defaults[video]; codec != "" {
args.AddCodec(codec)
} else {
args.AddCodec(video)
}
} else {
args.AddCodec("-c:v copy")
}
}
}
if query["bitrate"] != nil {
// https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
b := query["bitrate"][0]
args.AddCodec("-b:v " + b + " -maxrate " + b + " -bufsize " + b)
}
// 4. Process audio codecs
if args.Audio > 0 {
for _, audio := range query["audio"] {
if audio != "copy" {
if codec := defaults[audio]; codec != "" {
args.AddCodec(codec)
} else {
args.AddCodec(audio)
}
} else {
args.AddCodec("-c:a copy")
}
}
}
if query["hardware"] != nil {
hardware.MakeHardware(args, query["hardware"][0], defaults)
}
}
switch {
case args.Video == 0 && args.Audio == 0:
args.AddCodec("-c copy")
case args.Video == 0:
args.AddCodec("-vn")
case args.Audio == 0:
args.AddCodec("-an")
}
// change otput from RTSP to some other pipe format
switch {
case args.Video == 0 && args.Audio == 0:
// no transcoding from mjpeg input (ffmpeg device with support output as raw MJPEG)
if strings.Contains(args.Input, " mjpeg ") {
args.Output = defaults["output/mjpeg"]
}
case args.Video == 1 && args.Audio == 0:
switch core.Before(query.Get("video"), "/") {
case "mjpeg":
args.Output = defaults["output/mjpeg"]
case "raw":
args.Output = defaults["output/raw"]
}
case args.Video == 0 && args.Audio == 1:
switch core.Before(query.Get("audio"), "/") {
case "aac":
args.Output = defaults["output/aac"]
case "pcma", "pcmu", "pcml":
args.Output = defaults["output/wav"]
}
}
return args
}
@@ -0,0 +1,396 @@
package ffmpeg
import (
"testing"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
"github.com/stretchr/testify/require"
)
func TestParseArgsFile(t *testing.T) {
tests := []struct {
name string
source string
expect string
}{
{
name: "[FILE] all tracks will be copied without transcoding codecs",
source: "/media/bbb.mp4",
expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[FILE] video will be transcoded to H264, audio will be skipped",
source: "/media/bbb.mp4#video=h264",
expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[FILE] video will be copied, audio will be transcoded to pcmu",
source: "/media/bbb.mp4#video=copy#audio=pcmu",
expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v copy -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[FILE] video will be transcoded to H265 and rotate 270º, audio will be skipped",
source: "/media/bbb.mp4#video=h265#rotate=-90",
expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "transpose=2" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[FILE] video will be output for MJPEG to pipe, audio will be skipped",
source: "/media/bbb.mp4#video=mjpeg",
expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v mjpeg -an -f mjpeg -`,
},
{
name: "https://github.com/AlexxIT/go2rtc/issues/509",
source: "ffmpeg:test.mp4#raw=-ss 00:00:20",
expect: `ffmpeg -hide_banner -re -i ffmpeg:test.mp4 -ss 00:00:20 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
args := parseArgs(test.source)
require.Equal(t, test.expect, args.String())
})
}
}
func TestParseArgsDevice(t *testing.T) {
tests := []struct {
name string
source string
expect string
}{
{
name: "[DEVICE] video will be output for MJPEG to pipe, with size 1920x1080",
source: "device?video=0&video_size=1920x1080",
expect: `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i "video=0" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[DEVICE] video will be transcoded to H265 with framerate 20, audio will be skipped",
source: "device?video=0&framerate=20#video=h265",
expect: `ffmpeg -hide_banner -f dshow -framerate 20 -i "video=0" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[DEVICE] video/audio",
source: "device?video=FaceTime HD Camera&audio=Microphone (High Definition Audio Device)",
expect: `ffmpeg -hide_banner -f dshow -i "video=FaceTime HD Camera:audio=Microphone (High Definition Audio Device)" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
args := parseArgs(test.source)
require.Equal(t, test.expect, args.String())
})
}
}
func TestParseArgsIpCam(t *testing.T) {
tests := []struct {
name string
source string
expect string
}{
{
name: "[HTTP] video will be copied",
source: "http://example.com",
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[HTTP-MJPEG] video will be transcoded to H264",
source: "http://example.com#video=h264",
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[HLS] video will be copied, audio will be skipped",
source: "https://example.com#video=copy",
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i https://example.com -c:v copy -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[RTSP] video will be copied without transcoding codecs",
source: "rtsp://example.com",
expect: `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[RTSP] video with resize to 1280x720, should be transcoded, so select H265",
source: "rtsp://example.com#video=h265#width=1280#height=720",
expect: `ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "scale=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[RTSP] video will be copied, changing RTSP transport from TCP to UDP+TCP",
source: "rtsp://example.com#input=rtsp/udp",
expect: `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[RTMP] video will be copied, changing RTSP transport from TCP to UDP+TCP",
source: "rtmp://example.com#input=rtsp/udp",
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtmp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[RTSP] custom timeout",
source: "rtsp://example.com#timeout=10",
expect: `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 10000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
args := parseArgs(test.source)
require.Equal(t, test.expect, args.String())
})
}
}
func TestParseArgsAudio(t *testing.T) {
tests := []struct {
name string
source string
expect string
}{
{
name: "[AUDIO] audio will be transcoded to AAC, video will be skipped",
source: "rtsp://example.com#audio=aac",
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a aac -vn -f adts -`,
},
{
name: "[AUDIO] audio will be transcoded to AAC/16000, video will be skipped",
source: "rtsp://example.com#audio=aac/16000",
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a aac -ar:a 16000 -ac:a 1 -vn -f adts -`,
},
{
name: "[AUDIO] audio will be transcoded to OPUS, video will be skipped",
source: "rtsp://example.com#audio=opus",
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a libopus -application:a lowdelay -min_comp 0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[AUDIO] audio will be transcoded to PCMU, video will be skipped",
source: "rtsp://example.com#audio=pcmu",
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -vn -f wav -`,
},
{
name: "[AUDIO] audio will be transcoded to PCMU/16000, video will be skipped",
source: "rtsp://example.com#audio=pcmu/16000",
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_mulaw -ar:a 16000 -ac:a 1 -vn -f wav -`,
},
{
name: "[AUDIO] audio will be transcoded to PCMU/48000, video will be skipped",
source: "rtsp://example.com#audio=pcmu/48000",
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_mulaw -ar:a 48000 -ac:a 1 -vn -f wav -`,
},
{
name: "[AUDIO] audio will be transcoded to PCMA, video will be skipped",
source: "rtsp://example.com#audio=pcma",
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_alaw -ar:a 8000 -ac:a 1 -vn -f wav -`,
},
{
name: "[AUDIO] audio will be transcoded to PCMA/16000, video will be skipped",
source: "rtsp://example.com#audio=pcma/16000",
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_alaw -ar:a 16000 -ac:a 1 -vn -f wav -`,
},
{
name: "[AUDIO] audio will be transcoded to PCMA/48000, video will be skipped",
source: "rtsp://example.com#audio=pcma/48000",
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_alaw -ar:a 48000 -ac:a 1 -vn -f wav -`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
args := parseArgs(test.source)
require.Equal(t, test.expect, args.String())
})
}
}
func TestParseArgsHwVaapi(t *testing.T) {
tests := []struct {
name string
source string
expect string
}{
{
name: "[HTTP-MJPEG] video will be transcoded to H264",
source: "http:///example.com#video=h264#hardware=vaapi",
expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_color_matrix=bt709:out_range=tv:format=nv12" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[RTSP] video with rotation, should be transcoded, so select H264",
source: "rtsp://example.com#video=h264#rotate=180#hardware=vaapi",
expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,transpose_vaapi=4,scale_vaapi=out_color_matrix=bt709:out_range=tv:format=nv12" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[RTSP] video with resize to 1280x720, should be transcoded, so select H265",
source: "rtsp://example.com#video=h265#width=1280#height=720#hardware=vaapi",
expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_vaapi -g 50 -bf 0 -profile:v main -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[FILE] video will be output for MJPEG to pipe, audio will be skipped",
source: "/media/bbb.mp4#video=mjpeg#hardware=vaapi",
expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -re -i /media/bbb.mp4 -c:v mjpeg_vaapi -an -vf "format=vaapi|nv12,hwupload" -f mjpeg -`,
},
{
name: "[DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265",
source: "device?video=0&video_size=1920x1080#video=h265#hardware=vaapi",
expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -f dshow -video_size 1920x1080 -i "video=0" -c:v hevc_vaapi -g 50 -bf 0 -profile:v main -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
args := parseArgs(test.source)
require.Equal(t, test.expect, args.String())
})
}
}
func _TestParseArgsHwV4l2m2m(t *testing.T) {
// [HTTP-MJPEG] video will be transcoded to H264
args := parseArgs("http:///example.com#video=h264#hardware=v4l2m2m")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_v4l2m2m -g 50 -bf 0 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with rotation, should be transcoded, so select H264
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=v4l2m2m")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_v4l2m2m -g 50 -bf 0 -an -vf "transpose=1,transpose=1" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720#hardware=v4l2m2m")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_v4l2m2m -g 50 -bf 0 -an -vf "scale=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=v4l2m2m")
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_v4l2m2m -g 50 -bf 0 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestParseArgsHwRKMPP(t *testing.T) {
tests := []struct {
name string
source string
expect string
}{
{
name: "[FILE] transcoding to H264",
source: "bbb.mp4#video=h264#hardware=rkmpp",
expect: `ffmpeg -hide_banner -hwaccel rkmpp -hwaccel_output_format drm_prime -afbc rga -re -i bbb.mp4 -c:v h264_rkmpp -g 50 -bf 0 -profile:v high -level:v 4.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[FILE] transcoding with rotation",
source: "bbb.mp4#video=h264#rotate=180#hardware=rkmpp",
expect: `ffmpeg -hide_banner -hwaccel rkmpp -hwaccel_output_format drm_prime -afbc rga -re -i bbb.mp4 -c:v h264_rkmpp -g 50 -bf 0 -profile:v high -level:v 4.1 -an -vf "format=drm_prime|nv12,hwupload,vpp_rkrga=transpose=4" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[FILE] transcoding with scaling",
source: "bbb.mp4#video=h264#height=320#hardware=rkmpp",
expect: `ffmpeg -hide_banner -hwaccel rkmpp -hwaccel_output_format drm_prime -afbc rga -re -i bbb.mp4 -c:v h264_rkmpp -g 50 -bf 0 -profile:v high -level:v 4.1 -an -vf "format=drm_prime|nv12,hwupload,scale_rkrga=-1:320:force_original_aspect_ratio=0" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
args := parseArgs(test.source)
require.Equal(t, test.expect, args.String())
})
}
}
func _TestParseArgsHwCuda(t *testing.T) {
// [HTTP-MJPEG] video will be transcoded to H264
args := parseArgs("http:///example.com#video=h264#hardware=cuda")
require.Equal(t, `ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_nvenc -g 50 -bf 0 -profile:v high -level:v auto -preset:v p2 -tune:v ll -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with rotation, should be transcoded, so select H264
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=cuda")
require.Equal(t, `ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format nv12 -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_nvenc -g 50 -bf 0 -profile:v high -level:v auto -preset:v p2 -tune:v ll -an -vf "transpose=1,transpose=1,hwupload" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720#hardware=cuda")
require.Equal(t, `ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_nvenc -g 50 -bf 0 -profile:v high -level:v auto -an -vf "scale_cuda=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=cuda")
require.Equal(t, `ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_nvenc -g 50 -bf 0 -profile:v high -level:v auto -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func _TestParseArgsHwDxva2(t *testing.T) {
// [HTTP-MJPEG] video will be transcoded to H264
args := parseArgs("http:///example.com#video=h264#hardware=dxva2")
require.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 -an -vf "hwmap=derive_device=qsv,format=qsv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with rotation, should be transcoded, so select H264
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=dxva2")
require.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 -an -vf "hwmap=derive_device=qsv,format=qsv,transpose=1,transpose=1" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720#hardware=dxva2")
require.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_qsv -g 50 -bf 0 -profile:v high -level:v 5.1 -async_depth:v 1 -an -vf "hwmap=derive_device=qsv,format=qsv,scale_qsv=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [FILE] video will be output for MJPEG to pipe, audio will be skipped
args = parseArgs("/media/bbb.mp4#video=mjpeg#hardware=dxva2")
require.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -re -i /media/bbb.mp4 -c:v mjpeg_qsv -profile:v high -level:v 5.1 -an -vf "hwmap=derive_device=qsv,format=qsv" -f mjpeg -`, args.String())
// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=dxva2")
require.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_qsv -g 50 -bf 0 -profile:v high -level:v 5.1 -async_depth:v 1 -an -vf "hwmap=derive_device=qsv,format=qsv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func _TestParseArgsHwVideotoolbox(t *testing.T) {
// [HTTP-MJPEG] video will be transcoded to H264
args := parseArgs("http:///example.com#video=h264#hardware=videotoolbox")
require.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with rotation, should be transcoded, so select H264
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=videotoolbox")
require.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1 -an -vf "transpose=1,transpose=1" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720#hardware=videotoolbox")
require.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1 -an -vf "scale=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=videotoolbox")
require.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestDeckLink(t *testing.T) {
args := parseArgs(`DeckLink SDI (2)#video=h264#hardware=vaapi#input=-format_code Hp29 -f decklink -i "{input}"`)
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -format_code Hp29 -f decklink -i "DeckLink SDI (2)" -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_color_matrix=bt709:out_range=tv:format=nv12" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestDrawText(t *testing.T) {
tests := []struct {
name string
source string
expect string
}{
{
source: "http:///example.com#video=h264#drawtext=fontsize=12",
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}'" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
source: "http:///example.com#video=h264#width=640#drawtext=fontsize=12",
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "scale=640:-1,drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}'" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
source: "http:///example.com#video=h264#width=640#drawtext=fontsize=12#hardware=vaapi",
expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format nv12 -hwaccel_flags allow_profile_mismatch -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "scale=640:-1,drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}',hwupload" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
args := parseArgs(test.source)
require.Equal(t, test.expect, args.String())
})
}
}
func TestVersion(t *testing.T) {
verAV = ffmpeg.Version61
tests := []struct {
name string
source string
expect string
}{
{
source: "/media/bbb.mp4",
expect: `ffmpeg -hide_banner -readrate_initial_burst 0.001 -re -i /media/bbb.mp4 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
args := parseArgs(test.source)
require.Equal(t, test.expect, args.String())
})
}
}
@@ -0,0 +1,106 @@
# Hardware
You **DON'T** need hardware acceleration if:
- you're not using the [FFmpeg source](../README.md)
- you're using only `#video=copy` for the FFmpeg source
- you're using only `#audio=...` (any audio) transcoding for the FFmpeg source
You **NEED** hardware acceleration if you're using `#video=h264`, `#video=h265`, `#video=mjpeg` (video) transcoding.
## Important
- Acceleration is disabled by default because it can be unstable (this may change in the future)
- go2rtc can automatically detect supported hardware acceleration if enabled
- go2rtc will enable hardware decoding only if hardware encoding is supported
- go2rtc will use the same GPU for decoder and encoder
- Intel and AMD will switch to a software decoder if the input codec isn't supported by the hardware decoder
- NVIDIA will fail if the input codec isn't supported by the hardware decoder
- Raspberry Pi always uses a software decoder
```yaml
streams:
# auto select hardware encoder
camera1_hw: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=h264#hardware
# manual select hardware encoder (vaapi, cuda, v4l2m2m, dxva2, videotoolbox)
camera1_vaapi: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=h264#hardware=vaapi
```
## Docker and Hass Addon
There are two versions of the Docker container and Hass Add-on:
- Latest (Alpine) supports hardware acceleration for Intel iGPU (CPU with graphics) and Raspberry Pi.
- Hardware (Debian 12) supports Intel iGPU, AMD GPU, NVIDIA GPU.
## Intel iGPU
**Supported on:** Windows binary, Linux binary, Docker, Hass Addon.
If you have an Intel Sandy Bridge (2011) CPU with graphics, you already have hardware decoding/encoding support for `AVC/H.264`.
If you have an Intel Skylake (2015) CPU with graphics, you already have hardware decoding/encoding support for `AVC/H.264`, `HEVC/H.265` and `MJPEG`.
Read more [here](https://en.wikipedia.org/wiki/Intel_Quick_Sync_Video#Hardware_decoding_and_encoding) and [here](https://en.wikipedia.org/wiki/Intel_Graphics_Technology#Capabilities_(GPU_video_acceleration)).
Linux and Docker:
- It may be important to have a recent OS and Linux kernel. For example, on my **Debian 10 (kernel 4.19)** it did not work, but after updating to **Debian 11 (kernel 5.10)** everything was fine.
- If you run into trouble, check that you have the `/dev/dri/` folder on your host.
Docker users should add the `--privileged` option to the container for access to the hardware.
**PS.** Supported via [VAAPI](https://trac.ffmpeg.org/wiki/Hardware/VAAPI) engine on Linux and [DXVA2+QSV](https://trac.ffmpeg.org/wiki/Hardware/QuickSync) engine on Windows.
## AMD GPU
*I don't have the hardware to test this!!!*
**Supported on:** Linux binary, Docker, Hass Addon.
Docker users should install: `alexxit/go2rtc:master-hardware`. Docker users should add the `--privileged` option to the container for access to the hardware.
Hass Addon users should install **go2rtc master hardware** version.
**PS.** Supported via [VAAPI](https://trac.ffmpeg.org/wiki/Hardware/VAAPI) engine.
## NVIDIA GPU
**Supported on:** Windows binary, Linux binary, Docker.
Docker users should install: `alexxit/go2rtc:master-hardware`.
Read more [here](https://docs.frigate.video/configuration/hardware_acceleration) and [here](https://jellyfin.org/docs/general/administration/hardware-acceleration/#nvidia-hardware-acceleration-on-docker-linux).
**PS.** Supported via [CUDA](https://trac.ffmpeg.org/wiki/HWAccelIntro#CUDANVENCNVDEC) engine.
## Raspberry Pi 3
**Supported on:** Linux binary, Docker, Hass Addon.
I don't recommend using transcoding on the Raspberry Pi 3. It's extremely slow, even with hardware acceleration. Also, it may fail when transcoding a 2K+ stream.
## Raspberry Pi 4
*I don't have the hardware to test this!!!*
**Supported on:** Linux binary, Docker, Hass Addon.
**PS.** Supported via [v4l2m2m](https://lalitm.com/hw-encoding-raspi/) engine.
## macOS
In my tests, transcoding is faster on the M1 CPU than on the M1 GPU. Transcoding time on the M1 CPU is better than any Intel iGPU and comparable to an NVIDIA RTX 2070.
**PS.** Supported via [videotoolbox](https://trac.ffmpeg.org/wiki/HWAccelIntro#VideoToolbox) engine.
## Rockchip
- It's important to use a custom FFmpeg build with Rockchip support from [@nyanmisaka](https://github.com/nyanmisaka/ffmpeg-rockchip)
- Static binaries from [@MarcA711](https://github.com/MarcA711/Rockchip-FFmpeg-Builds/releases/)
- It's important to have Linux kernel 5.10 or 6.1
**Tested**
- [Orange Pi 3B](https://www.armbian.com/orangepi3b/) with Armbian 6.1, supports transcoding H.264, H.265, MJPEG
@@ -0,0 +1,210 @@
package hardware
import (
"net/http"
"os/exec"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
)
const (
EngineSoftware = "software"
EngineVAAPI = "vaapi" // Intel iGPU and AMD GPU
EngineV4L2M2M = "v4l2m2m" // Raspberry Pi 3 and 4
EngineCUDA = "cuda" // NVidia on Windows and Linux
EngineDXVA2 = "dxva2" // Intel on Windows
EngineVideoToolbox = "videotoolbox" // macOS
EngineRKMPP = "rkmpp" // Rockchip
)
func Init(bin string) {
api.HandleFunc("api/ffmpeg/hardware", func(w http.ResponseWriter, r *http.Request) {
api.ResponseSources(w, ProbeAll(bin))
})
}
// MakeHardware converts software FFmpeg args to hardware args
// empty engine for autoselect
func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string) {
for i, codec := range args.Codecs {
if len(codec) < 10 {
continue // skip short line (-c:v mjpeg...)
}
// get current codec name
name := cut(codec, ' ', 1)
switch name {
case "libx264":
name = "h264"
case "libx265":
name = "h265"
case "mjpeg":
default:
continue // skip unsupported codec
}
// temporary disable probe for H265
if engine == "" && name != "h265" {
if engine = cache[name]; engine == "" {
engine = ProbeHardware(args.Bin, name)
cache[name] = engine
}
}
switch engine {
case EngineVAAPI:
args.Codecs[i] = defaults[name+"/"+engine]
if !args.HasFilters("drawtext=") {
args.Input = "-hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch " + args.Input
if name == "h264" {
fixPixelFormat(args)
}
for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
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
} else {
args.Filters[i] = "transpose_vaapi=" + filter[10:]
}
}
}
// fix if input doesn't support hwaccel, do nothing when support
// insert as first filter before hardware scale and transpose
args.InsertFilter("format=vaapi|nv12,hwupload")
} else {
// enable software pixel for drawtext, scale and transpose
args.Input = "-hwaccel vaapi -hwaccel_output_format nv12 -hwaccel_flags allow_profile_mismatch " + args.Input
args.AddFilter("hwupload")
}
case EngineCUDA:
args.Codecs[i] = defaults[name+"/"+engine]
// CUDA doesn't support hardware transpose
// https://github.com/AlexxIT/go2rtc/issues/389
if !args.HasFilters("drawtext=", "transpose=") {
args.Input = "-hwaccel cuda -hwaccel_output_format cuda " + args.Input
for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
args.Filters[i] = "scale_cuda=" + filter[6:]
}
}
} else {
args.Input = "-hwaccel cuda -hwaccel_output_format nv12 " + args.Input
args.AddFilter("hwupload")
}
case EngineDXVA2:
args.Input = "-hwaccel dxva2 -hwaccel_output_format dxva2_vld " + args.Input
args.Codecs[i] = defaults[name+"/"+engine]
for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
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]
case EngineV4L2M2M:
args.Codecs[i] = defaults[name+"/"+engine]
case EngineRKMPP:
args.Codecs[i] = defaults[name+"/"+engine]
if !args.HasFilters("drawtext=") {
args.Input = "-hwaccel rkmpp -hwaccel_output_format drm_prime -afbc rga " + args.Input
for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
args.Filters[i] = "scale_rkrga=" + filter[6:] + ":force_original_aspect_ratio=0"
}
if strings.HasPrefix(filter, "transpose=") {
if filter == "transpose=1,transpose=1" { // 180 degrees half-turn
args.Filters[i] = "vpp_rkrga=transpose=4" // reversal
} else {
args.Filters[i] = "vpp_rkrga=transpose=" + filter[10:]
}
}
}
if len(args.Filters) > 0 {
// fix if input doesn't support hwaccel, do nothing when support
// insert as first filter before hardware scale and transpose
args.InsertFilter("format=drm_prime|nv12,hwupload")
}
} else {
// enable software pixel for drawtext, scale and transpose
args.Input = "-hwaccel rkmpp -hwaccel_output_format nv12 -afbc rga " + args.Input
args.AddFilter("hwupload")
}
}
}
}
var cache = map[string]string{}
func run(bin string, args string) bool {
err := exec.Command(bin, strings.Split(args, " ")...).Run()
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 {
s = s[i+1:]
} else {
return ""
}
}
if i := strings.IndexByte(s, sep); i > 0 {
return s[:i]
}
return s
}
// fixPixelFormat:
// - good h264 pixel: yuv420p(tv, bt709) == yuv420p (mpeg/limited/tv)
// - bad h264 pixel: yuvj420p(pc, bt709) == yuvj420p (jpeg/full/pc)
// - bad jpeg pixel: yuvj422p(pc, bt470bg)
func fixPixelFormat(args *ffmpeg.Args) {
// in my tests this filters has same CPU/GPU load:
// - "hwupload"
// - "hwupload,scale_vaapi=out_color_matrix=bt709:out_range=tv"
// - "hwupload,scale_vaapi=out_color_matrix=bt709:out_range=tv:format=nv12"
const fixPixFmt = "out_color_matrix=bt709:out_range=tv:format=nv12"
for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
args.Filters[i] = filter + ":" + fixPixFmt
return
}
}
args.Filters = append(args.Filters, "scale="+fixPixFmt)
}
@@ -0,0 +1,62 @@
//go:build freebsd || netbsd || openbsd || dragonfly
package hardware
import (
"runtime"
"github.com/AlexxIT/go2rtc/internal/api"
)
const (
ProbeV4L2M2MH264 = "-f lavfi -i testsrc2 -t 1 -c h264_v4l2m2m -f null -"
ProbeV4L2M2MH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_v4l2m2m -f null -"
ProbeRKMPPH264 = "-f lavfi -i testsrc2 -t 1 -c h264_rkmpp_encoder -f null -"
ProbeRKMPPH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_rkmpp_encoder -f null -"
)
func ProbeAll(bin string) []*api.Source {
return []*api.Source{
{
Name: runToString(bin, ProbeV4L2M2MH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineV4L2M2M,
},
{
Name: runToString(bin, ProbeV4L2M2MH265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineV4L2M2M,
},
{
Name: runToString(bin, ProbeRKMPPH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineRKMPP,
},
{
Name: runToString(bin, ProbeRKMPPH265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineRKMPP,
},
}
}
func ProbeHardware(bin, name string) string {
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
switch name {
case "h264":
if run(bin, ProbeV4L2M2MH264) {
return EngineV4L2M2M
}
if run(bin, ProbeRKMPPH264) {
return EngineRKMPP
}
case "h265":
if run(bin, ProbeV4L2M2MH265) {
return EngineV4L2M2M
}
if run(bin, ProbeRKMPPH265) {
return EngineRKMPP
}
}
return EngineSoftware
}
return EngineSoftware
}
@@ -0,0 +1,39 @@
//go:build darwin || ios
package hardware
import (
"github.com/AlexxIT/go2rtc/internal/api"
)
const ProbeVideoToolboxH264 = "-f lavfi -i testsrc2=size=svga -t 1 -c h264_videotoolbox -f null -"
const ProbeVideoToolboxH265 = "-f lavfi -i testsrc2=size=svga -t 1 -c hevc_videotoolbox -f null -"
func ProbeAll(bin string) []*api.Source {
return []*api.Source{
{
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
}
@@ -0,0 +1,124 @@
//go:build unix && !darwin && !freebsd && !netbsd && !openbsd && !dragonfly
package hardware
import (
"runtime"
"github.com/AlexxIT/go2rtc/internal/api"
)
const (
ProbeV4L2M2MH264 = "-f lavfi -i testsrc2 -t 1 -c h264_v4l2m2m -f null -"
ProbeV4L2M2MH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_v4l2m2m -f null -"
ProbeRKMPPH264 = "-f lavfi -i testsrc2 -t 1 -c h264_rkmpp -f null -"
ProbeRKMPPH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_rkmpp -f null -"
ProbeRKMPPJPEG = "-f lavfi -i testsrc2 -t 1 -c mjpeg_rkmpp -f null -"
ProbeVAAPIH264 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c h264_vaapi -f null -"
ProbeVAAPIH265 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c hevc_vaapi -f null -"
ProbeVAAPIJPEG = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c mjpeg_vaapi -f null -"
ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -"
ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -"
)
func ProbeAll(bin string) []*api.Source {
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
return []*api.Source{
{
Name: runToString(bin, ProbeV4L2M2MH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineV4L2M2M,
},
{
Name: runToString(bin, ProbeV4L2M2MH265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineV4L2M2M,
},
{
Name: runToString(bin, ProbeRKMPPH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineRKMPP,
},
{
Name: runToString(bin, ProbeRKMPPH265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineRKMPP,
},
{
Name: runToString(bin, ProbeRKMPPJPEG),
URL: "ffmpeg:...#video=mjpeg#hardware=" + EngineRKMPP,
},
}
}
return []*api.Source{
{
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
}
if run(bin, ProbeRKMPPH264) {
return EngineRKMPP
}
case "h265":
if run(bin, ProbeV4L2M2MH265) {
return EngineV4L2M2M
}
if run(bin, ProbeRKMPPH265) {
return EngineRKMPP
}
case "mjpeg":
if run(bin, ProbeRKMPPJPEG) {
return EngineRKMPP
}
}
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
}
@@ -0,0 +1,63 @@
//go:build windows
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.Source {
return []*api.Source{
{
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
}
@@ -0,0 +1,83 @@
package ffmpeg
import (
"bytes"
"fmt"
"net/url"
"os/exec"
"github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
"github.com/AlexxIT/go2rtc/pkg/shell"
)
func JPEGWithQuery(b []byte, query url.Values) ([]byte, error) {
args := parseQuery(query)
return transcode(b, args.String())
}
func JPEGWithScale(b []byte, width, height int) ([]byte, error) {
args := defaultArgs()
args.AddFilter(fmt.Sprintf("scale=%d:%d", width, height))
return transcode(b, args.String())
}
func transcode(b []byte, args string) ([]byte, error) {
cmdArgs := shell.QuoteSplit(args)
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
cmd.Stdin = bytes.NewBuffer(b)
return cmd.Output()
}
func defaultArgs() *ffmpeg.Args {
return &ffmpeg.Args{
Bin: defaults["bin"],
Global: defaults["global"],
Input: "-i -",
Codecs: []string{defaults["mjpeg"]},
Output: defaults["output/mjpeg"],
}
}
func parseQuery(query url.Values) *ffmpeg.Args {
args := defaultArgs()
var width = -1
var height = -1
var r, hw string
for k, v := range query {
switch k {
case "width", "w":
width = core.Atoi(v[0])
case "height", "h":
height = core.Atoi(v[0])
case "rotate":
r = v[0]
case "hardware", "hw":
hw = v[0]
}
}
if width > 0 || height > 0 {
args.AddFilter(fmt.Sprintf("scale=%d:%d", width, height))
}
if r != "" {
switch r {
case "90":
args.AddFilter("transpose=1") // 90 degrees clockwise
case "180":
args.AddFilter("transpose=1,transpose=1")
case "-90", "270":
args.AddFilter("transpose=2") // 90 degrees counterclockwise
}
}
if hw != "" {
hardware.MakeHardware(args, hw, defaults)
}
return args
}
@@ -0,0 +1,23 @@
package ffmpeg
import (
"net/url"
"testing"
"github.com/stretchr/testify/require"
)
func TestParseQuery(t *testing.T) {
args := parseQuery(nil)
require.Equal(t, `ffmpeg -hide_banner -i - -c:v mjpeg -f mjpeg -`, args.String())
query, err := url.ParseQuery("h=480")
require.Nil(t, err)
args = parseQuery(query)
require.Equal(t, `ffmpeg -hide_banner -i - -c:v mjpeg -vf "scale=-1:480" -f mjpeg -`, args.String())
query, err = url.ParseQuery("hw=vaapi")
require.Nil(t, err)
args = parseQuery(query)
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -i - -c:v mjpeg_vaapi -vf "format=vaapi|nv12,hwupload" -f mjpeg -`, args.String())
}
@@ -0,0 +1,122 @@
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.Connection
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.ID = core.NewID()
p.FormatName = "ffmpeg"
p.Medias = []*core.Media{
{
// we can support only audio, because don't know FmtpLine for H264 and PayloadType for MJPEG
Kind: core.KindAudio,
Direction: core.DirectionRecvonly,
// codecs in order from best to worst
Codecs: []*core.Codec{
// OPUS will always marked as OPUS/48000/2
{Name: core.CodecOpus, ClockRate: 48000, Channels: 2},
{Name: core.CodecPCML, ClockRate: 16000},
{Name: core.CodecPCM, ClockRate: 16000},
{Name: core.CodecPCMA, ClockRate: 16000},
{Name: core.CodecPCMU, ClockRate: 16000},
{Name: core.CodecPCML, ClockRate: 8000},
{Name: core.CodecPCM, ClockRate: 8000},
{Name: core.CodecPCMA, ClockRate: 8000},
{Name: core.CodecPCMU, ClockRate: 8000},
// AAC has unknown problems on Dahua two way
{Name: core.CodecAAC, ClockRate: 16000, FmtpLine: aac.FMTP + "1408"},
},
},
}
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.Connection)
}
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.CodecOpus:
s += "#audio=opus/16000"
case core.CodecAAC:
s += "#audio=aac/16000"
case core.CodecPCML:
s += "#audio=pcml/" + strconv.Itoa(int(codec.ClockRate))
case core.CodecPCM:
s += "#audio=pcm/" + strconv.Itoa(int(codec.ClockRate))
case core.CodecPCMA:
s += "#audio=pcma/" + strconv.Itoa(int(codec.ClockRate))
case core.CodecPCMU:
s += "#audio=pcmu/" + strconv.Itoa(int(codec.ClockRate))
}
}
// add other params
for key, values := range p.query {
if key != "audio" {
for _, value := range values {
s += "#" + key + "=" + value
}
}
}
return s
}
@@ -0,0 +1,46 @@
package ffmpeg
import (
"errors"
"os/exec"
"sync"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
)
var verMu sync.Mutex
var verErr error
var verFF string
var verAV string
func Version() (string, error) {
verMu.Lock()
defer verMu.Unlock()
if verFF != "" {
return verFF, verErr
}
cmd := exec.Command(defaults["bin"], "-version")
b, err := cmd.Output()
if err != nil {
verFF = "-"
verErr = err
return verFF, verErr
}
verFF, verAV = ffmpeg.ParseVersion(b)
if verFF == "" {
verFF = "?"
}
// better to compare libavformat, because nightly/master builds
if verAV != "" && verAV < ffmpeg.Version50 {
verErr = errors.New("ffmpeg: unsupported version: " + verFF)
}
log.Debug().Str("version", verFF).Str("libavformat", verAV).Msgf("[ffmpeg] bin")
return verFF, verErr
}
@@ -0,0 +1,79 @@
package virtual
import (
"net/url"
)
func GetInput(src string) string {
query, err := url.ParseQuery(src)
if err != nil {
return ""
}
input := "-re"
for _, video := range query["video"] {
// https://ffmpeg.org/ffmpeg-filters.html
sep := "=" // first separator
if video == "" {
video = "testsrc=decimals=2" // default video
sep = ":"
}
input += " -f lavfi -i " + video
// set defaults (using Add instead of Set)
query.Add("size", "1920x1080")
for key, values := range query {
value := values[0]
// https://ffmpeg.org/ffmpeg-utils.html#video-size-syntax
switch key {
case "color", "rate", "duration", "sar", "decimals":
case "size":
switch value {
case "720":
value = "1280x720" // crf=1 -> 12 Mbps
case "1080":
value = "1920x1080" // crf=1 -> 25 Mbps
case "2K":
value = "2560x1440" // crf=1 -> 43 Mbps
case "4K":
value = "3840x2160" // crf=1 -> 103 Mbps
case "8K":
value = "7680x4230" // https://reolink.com/blog/8k-resolution/
}
default:
continue
}
input += sep + key + "=" + value
sep = ":" // next separator
}
if s := query.Get("format"); s != "" {
input += ",format=" + s
}
}
return input
}
func GetInputTTS(src string) string {
query, err := url.ParseQuery(src)
if err != nil {
return ""
}
input := `-re -f lavfi -i "flite=text='` + query.Get("text") + `'`
// ffmpeg -f lavfi -i flite=list_voices=1
// awb, kal, kal16, rms, slt
if voice := query.Get("voice"); voice != "" {
input += ":voice" + voice
}
return input + `"`
}
@@ -0,0 +1,20 @@
package virtual
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestGetInput(t *testing.T) {
s := GetInput("video")
require.Equal(t, "-re -f lavfi -i testsrc=decimals=2:size=1920x1080", s)
s = GetInput("video=testsrc2&size=4K")
require.Equal(t, "-re -f lavfi -i testsrc2=size=3840x2160", s)
}
func TestGetInputTTS(t *testing.T) {
s := GetInputTTS("text=hello world&voice=slt")
require.Equal(t, `-re -f lavfi -i "flite=text='hello world':voiceslt"`, s)
}