install go2rtc on bob
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user