Move cmd module to internal
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
## FFplay output
|
||||
|
||||
[FFplay](https://stackoverflow.com/questions/27778678/what-are-mv-fd-aq-vq-sq-and-f-in-a-video-stream) `7.11 A-V: 0.003 fd= 1 aq= 21KB vq= 321KB sq= 0B f=0/0`:
|
||||
|
||||
- `7.11` - master clock, is the time from start of the stream/video
|
||||
- `A-V` - av_diff, difference between audio and video timestamps
|
||||
- `fd` - frames dropped
|
||||
- `aq` - audio queue (0 - no delay)
|
||||
- `vq` - video queue (0 - no delay)
|
||||
- `sq` - subtitle queue
|
||||
- `f` - timestamp error correction rate (Not 100% sure)
|
||||
|
||||
`M-V`, `M-A` means video stream only, audio stream only respectively.
|
||||
|
||||
## Devices Windows
|
||||
|
||||
```
|
||||
>ffmpeg -hide_banner -f dshow -list_options true -i video="VMware Virtual USB Video Device"
|
||||
[dshow @ 0000025695e52900] DirectShow video device options (from video devices)
|
||||
[dshow @ 0000025695e52900] Pin "Record" (alternative pin name "0")
|
||||
[dshow @ 0000025695e52900] pixel_format=yuyv422 min s=1280x720 fps=1 max s=1280x720 fps=10
|
||||
[dshow @ 0000025695e52900] pixel_format=yuyv422 min s=1280x720 fps=1 max s=1280x720 fps=10 (tv, bt470bg/bt709/unknown, topleft)
|
||||
[dshow @ 0000025695e52900] pixel_format=nv12 min s=1280x720 fps=1 max s=1280x720 fps=23
|
||||
[dshow @ 0000025695e52900] pixel_format=nv12 min s=1280x720 fps=1 max s=1280x720 fps=23 (tv, bt470bg/bt709/unknown, topleft)
|
||||
```
|
||||
|
||||
## Devices Mac
|
||||
|
||||
```
|
||||
% ./ffmpeg -hide_banner -f avfoundation -list_devices true -i ""
|
||||
[AVFoundation indev @ 0x7f8b1f504d80] AVFoundation video devices:
|
||||
[AVFoundation indev @ 0x7f8b1f504d80] [0] FaceTime HD Camera
|
||||
[AVFoundation indev @ 0x7f8b1f504d80] [1] Capture screen 0
|
||||
[AVFoundation indev @ 0x7f8b1f504d80] AVFoundation audio devices:
|
||||
[AVFoundation indev @ 0x7f8b1f504d80] [0] Soundflower (2ch)
|
||||
[AVFoundation indev @ 0x7f8b1f504d80] [1] Built-in Microphone
|
||||
[AVFoundation indev @ 0x7f8b1f504d80] [2] Soundflower (64ch)
|
||||
```
|
||||
|
||||
## Devices Linux
|
||||
|
||||
```
|
||||
# ffmpeg -hide_banner -f v4l2 -list_formats all -i /dev/video0
|
||||
[video4linux2,v4l2 @ 0x7f7de7c58bc0] 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 @ 0x7f7de7c58bc0] Compressed: mjpeg : Motion-JPEG : 640x480 160x120 176x144 320x176 320x240 352x288 432x240 544x288 640x360 752x416 800x448 800x600 864x480 960x544 960x720 1024x576 1184x656 1280x720 1280x960
|
||||
```
|
||||
|
||||
## Useful links
|
||||
|
||||
- https://superuser.com/questions/564402/explanation-of-x264-tune
|
||||
- https://stackoverflow.com/questions/33624016/why-sliced-thread-affect-so-much-on-realtime-encoding-using-ffmpeg-x264
|
||||
- https://codec.fandom.com/ru/wiki/X264_-_описание_ключей_кодирования
|
||||
- https://html5test.com/
|
||||
- https://trac.ffmpeg.org/wiki/Capture/Webcam
|
||||
- https://trac.ffmpeg.org/wiki/DirectShow
|
||||
- https://stackoverflow.com/questions/53207692/libav-mjpeg-encoding-and-huffman-table
|
||||
- https://github.com/tuupola/esp_video/blob/master/README.md
|
||||
- https://github.com/leandromoreira/ffmpeg-libav-tutorial
|
||||
- https://www.reddit.com/user/VeritablePornocopium/comments/okw130/ffmpeg_with_libfdk_aac_for_windows_x64/
|
||||
- https://slhck.info/video/2017/02/24/vbr-settings.html
|
||||
- [HomeKit audio samples problem](https://superuser.com/questions/1290996/non-monotonous-dts-with-igndts-flag)
|
||||
@@ -0,0 +1,61 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// https://trac.ffmpeg.org/wiki/Capture/Webcam
|
||||
const deviceInputPrefix = "-f avfoundation"
|
||||
|
||||
func deviceInputSuffix(videoIdx, audioIdx int) string {
|
||||
video := findMedia(core.KindVideo, videoIdx)
|
||||
audio := findMedia(core.KindAudio, audioIdx)
|
||||
switch {
|
||||
case video != nil && audio != nil:
|
||||
return `"` + video.ID + `:` + audio.ID + `"`
|
||||
case video != nil:
|
||||
return `"` + video.ID + `"`
|
||||
case audio != nil:
|
||||
return `"` + audio.ID + `"`
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func loadMedias() {
|
||||
cmd := exec.Command(
|
||||
Bin, "-hide_banner", "-list_devices", "true", "-f", "avfoundation", "-i", "dummy",
|
||||
)
|
||||
|
||||
var buf bytes.Buffer
|
||||
cmd.Stderr = &buf
|
||||
_ = cmd.Run()
|
||||
|
||||
var kind string
|
||||
|
||||
lines := strings.Split(buf.String(), "\n")
|
||||
process:
|
||||
for _, line := range lines {
|
||||
switch {
|
||||
case strings.HasSuffix(line, "video devices:"):
|
||||
kind = core.KindVideo
|
||||
continue
|
||||
case strings.HasSuffix(line, "audio devices:"):
|
||||
kind = core.KindAudio
|
||||
continue
|
||||
case strings.HasPrefix(line, "dummy"):
|
||||
break process
|
||||
}
|
||||
|
||||
// [AVFoundation indev @ 0x7fad54604380] [0] FaceTime HD Camera
|
||||
name := line[42:]
|
||||
media := loadMedia(kind, name)
|
||||
medias = append(medias, media)
|
||||
}
|
||||
}
|
||||
|
||||
func loadMedia(kind, name string) *core.Media {
|
||||
return &core.Media{Kind: kind, ID: name}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"io/ioutil"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// https://trac.ffmpeg.org/wiki/Capture/Webcam
|
||||
const deviceInputPrefix = "-f v4l2"
|
||||
|
||||
func deviceInputSuffix(videoIdx, audioIdx int) string {
|
||||
if video := findMedia(core.KindVideo, videoIdx); video != nil {
|
||||
return video.ID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func loadMedias() {
|
||||
files, err := ioutil.ReadDir("/dev")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, file := range files {
|
||||
log.Trace().Msg("[ffmpeg] " + file.Name())
|
||||
if strings.HasPrefix(file.Name(), core.KindVideo) {
|
||||
media := loadMedia(core.KindVideo, "/dev/"+file.Name())
|
||||
if media != nil {
|
||||
medias = append(medias, media)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadMedia(kind, name string) *core.Media {
|
||||
cmd := exec.Command(
|
||||
Bin, "-hide_banner", "-f", "v4l2", "-list_formats", "all", "-i", name,
|
||||
)
|
||||
var buf bytes.Buffer
|
||||
cmd.Stderr = &buf
|
||||
_ = cmd.Run()
|
||||
|
||||
if !bytes.Contains(buf.Bytes(), []byte("Raw")) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &core.Media{Kind: kind, ID: name}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// https://trac.ffmpeg.org/wiki/DirectShow
|
||||
const deviceInputPrefix = "-f dshow"
|
||||
|
||||
func deviceInputSuffix(videoIdx, audioIdx int) string {
|
||||
video := findMedia(core.KindVideo, videoIdx)
|
||||
audio := findMedia(core.KindAudio, audioIdx)
|
||||
switch {
|
||||
case video != nil && audio != nil:
|
||||
return `video="` + video.ID + `":audio=` + audio.ID + `"`
|
||||
case video != nil:
|
||||
return `video="` + video.ID + `"`
|
||||
case audio != nil:
|
||||
return `audio="` + audio.ID + `"`
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func loadMedias() {
|
||||
cmd := exec.Command(
|
||||
Bin, "-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "",
|
||||
)
|
||||
|
||||
var buf bytes.Buffer
|
||||
cmd.Stderr = &buf
|
||||
_ = cmd.Run()
|
||||
|
||||
lines := strings.Split(buf.String(), "\r\n")
|
||||
for _, line := range lines {
|
||||
var kind string
|
||||
if strings.HasSuffix(line, "(video)") {
|
||||
kind = core.KindVideo
|
||||
} else if strings.HasSuffix(line, "(audio)") {
|
||||
kind = core.KindAudio
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
// hope we have constant prefix and suffix sizes
|
||||
// [dshow @ 00000181e8d028c0] "VMware Virtual USB Video Device" (video)
|
||||
name := line[28 : len(line)-9]
|
||||
media := loadMedia(kind, name)
|
||||
medias = append(medias, media)
|
||||
}
|
||||
}
|
||||
|
||||
func loadMedia(kind, name string) *core.Media {
|
||||
return &core.Media{Kind: kind, ID: name}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/rs/zerolog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
log = app.GetLogger("exec")
|
||||
|
||||
api.HandleFunc("api/devices", handle)
|
||||
}
|
||||
|
||||
func GetInput(src string) (string, error) {
|
||||
if medias == nil {
|
||||
loadMedias()
|
||||
}
|
||||
|
||||
input := deviceInputPrefix
|
||||
|
||||
var videoIdx, audioIdx int
|
||||
if i := strings.IndexByte(src, '?'); i > 0 {
|
||||
query, err := url.ParseQuery(src[i+1:])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for key, value := range query {
|
||||
switch key {
|
||||
case "video":
|
||||
videoIdx, _ = strconv.Atoi(value[0])
|
||||
case "audio":
|
||||
audioIdx, _ = strconv.Atoi(value[0])
|
||||
case "framerate":
|
||||
input += " -framerate " + value[0]
|
||||
case "resolution":
|
||||
input += " -video_size " + value[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input += " -i " + deviceInputSuffix(videoIdx, audioIdx)
|
||||
|
||||
return input, nil
|
||||
}
|
||||
|
||||
var Bin string
|
||||
var log zerolog.Logger
|
||||
var medias []*core.Media
|
||||
|
||||
func findMedia(kind string, index int) *core.Media {
|
||||
for _, media := range medias {
|
||||
if media.Kind != kind {
|
||||
continue
|
||||
}
|
||||
if index == 0 {
|
||||
return media
|
||||
}
|
||||
index--
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func handle(w http.ResponseWriter, r *http.Request) {
|
||||
if medias == nil {
|
||||
loadMedias()
|
||||
}
|
||||
|
||||
var items []api.Stream
|
||||
var iv, ia int
|
||||
|
||||
for _, media := range medias {
|
||||
var source string
|
||||
switch media.Kind {
|
||||
case core.KindVideo:
|
||||
source = "ffmpeg:device?video=" + strconv.Itoa(iv)
|
||||
iv++
|
||||
case core.KindAudio:
|
||||
source = "ffmpeg:device?audio=" + strconv.Itoa(ia)
|
||||
ia++
|
||||
}
|
||||
items = append(items, api.Stream{Name: media.ID, URL: source})
|
||||
}
|
||||
|
||||
api.ResponseStreams(w, items)
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/exec"
|
||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg/device"
|
||||
"github.com/AlexxIT/go2rtc/internal/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
var cfg struct {
|
||||
Mod map[string]string `yaml:"ffmpeg"`
|
||||
}
|
||||
|
||||
cfg.Mod = defaults // will be overriden from yaml
|
||||
|
||||
app.LoadConfig(&cfg)
|
||||
|
||||
if app.GetLogger("exec").GetLevel() >= 0 {
|
||||
defaults["global"] += " -v error"
|
||||
}
|
||||
|
||||
streams.HandleFunc("ffmpeg", func(url string) (core.Producer, error) {
|
||||
args := parseArgs(url[7:]) // remove `ffmpeg:`
|
||||
if args == nil {
|
||||
return nil, errors.New("can't generate ffmpeg command")
|
||||
}
|
||||
return exec.Handle("exec:" + args.String())
|
||||
})
|
||||
|
||||
device.Bin = defaults["bin"]
|
||||
device.Init()
|
||||
}
|
||||
|
||||
var defaults = map[string]string{
|
||||
"bin": "ffmpeg",
|
||||
"global": "-hide_banner",
|
||||
|
||||
// inputs
|
||||
"file": "-re -i {input}",
|
||||
"http": "-fflags nobuffer -flags low_delay -i {input}",
|
||||
"rtsp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i {input}",
|
||||
|
||||
"rtsp/udp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i {input}",
|
||||
|
||||
// output
|
||||
"output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}",
|
||||
|
||||
// `-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
|
||||
"h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency",
|
||||
"h265": "-c:v libx265 -g 50 -profile:v high -level:v 5.1 -preset:v superfast -tune:v zerolatency",
|
||||
"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
|
||||
|
||||
// https://ffmpeg.org/ffmpeg-codecs.html#libopus-1
|
||||
"opus": "-c:a libopus -ar:a 48000 -ac:a 2 -application:a voip -compression_level:a 0",
|
||||
"pcmu": "-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/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/16000": "-c:a pcm_s16be -ar:a 16000 -ac:a 1",
|
||||
"pcm/48000": "-c:a pcm_s16be -ar:a 48000 -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 high -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 NVidia on Linux and Windows
|
||||
// preset=p2 - faster, tune=ll - low latency
|
||||
"h264/cuda": "-c:v h264_nvenc -g 50 -profile:v high -level:v auto -preset:v p2 -tune:v ll",
|
||||
"h265/cuda": "-c:v hevc_nvenc -g 50 -profile:v high -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 high -level:v 5.1 -async_depth:v 1",
|
||||
"mjpeg/dxva2": "-c:v mjpeg_qsv -profile:v high -level:v 5.1",
|
||||
|
||||
// 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 high -level:v 5.1",
|
||||
}
|
||||
|
||||
// inputTemplate - select input template from YAML config by template name
|
||||
// if query has input param - select another tempalte 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 != "" {
|
||||
if template = defaults[input]; template == "" {
|
||||
template = input
|
||||
}
|
||||
} else {
|
||||
template = defaults[name]
|
||||
}
|
||||
return strings.Replace(template, "{input}", s, 1)
|
||||
}
|
||||
|
||||
func parseArgs(s string) *Args {
|
||||
// init FFmpeg arguments
|
||||
args := &Args{
|
||||
bin: defaults["bin"],
|
||||
global: defaults["global"],
|
||||
output: defaults["output"],
|
||||
}
|
||||
|
||||
var query url.Values
|
||||
if i := strings.IndexByte(s, '#'); i > 0 {
|
||||
query = parseQuery(s[i+1:])
|
||||
args.video = len(query["video"])
|
||||
args.audio = len(query["audio"])
|
||||
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"
|
||||
}
|
||||
args.input = inputTemplate("rtsp", s, query)
|
||||
} else if strings.HasPrefix(s, "device?") {
|
||||
var err error
|
||||
args.input, err = device.GetInput(s)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
} 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"] {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
args.AddCodec("-vn")
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
args.AddCodec("-an")
|
||||
}
|
||||
|
||||
if query["hardware"] != nil {
|
||||
MakeHardware(args, query["hardware"][0])
|
||||
}
|
||||
}
|
||||
|
||||
if args.codecs == nil {
|
||||
args.AddCodec("-c copy")
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
func parseQuery(s string) map[string][]string {
|
||||
query := map[string][]string{}
|
||||
for _, key := range strings.Split(s, "#") {
|
||||
var value string
|
||||
i := strings.IndexByte(key, '=')
|
||||
if i > 0 {
|
||||
key, value = key[:i], key[i+1:]
|
||||
}
|
||||
query[key] = append(query[key], value)
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
type Args struct {
|
||||
bin string // ffmpeg
|
||||
global string // -hide_banner -v error
|
||||
input string // -re -stream_loop -1 -i /media/bunny.mp4
|
||||
codecs []string // -c:v libx264 -g:v 30 -preset:v ultrafast -tune:v zerolatency
|
||||
filters []string // scale=1920:1080
|
||||
output string // -f rtsp {output}
|
||||
|
||||
video, audio int // count of video and audio params
|
||||
}
|
||||
|
||||
func (a *Args) AddCodec(codec string) {
|
||||
a.codecs = append(a.codecs, codec)
|
||||
}
|
||||
|
||||
func (a *Args) AddFilter(filter string) {
|
||||
a.filters = append(a.filters, filter)
|
||||
}
|
||||
|
||||
func (a *Args) InsertFilter(filter string) {
|
||||
a.filters = append([]string{filter}, a.filters...)
|
||||
}
|
||||
|
||||
func (a *Args) String() string {
|
||||
b := bytes.NewBuffer(make([]byte, 0, 512))
|
||||
|
||||
b.WriteString(a.bin)
|
||||
|
||||
if a.global != "" {
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(a.global)
|
||||
}
|
||||
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(a.input)
|
||||
|
||||
multimode := a.video > 1 || a.audio > 1
|
||||
var iv, ia int
|
||||
|
||||
for _, codec := range a.codecs {
|
||||
// support multiple video and/or audio codecs
|
||||
if multimode && len(codec) >= 5 {
|
||||
switch codec[:5] {
|
||||
case "-c:v ":
|
||||
codec = "-map 0:v:0? " + strings.ReplaceAll(codec, ":v ", ":v:"+strconv.Itoa(iv)+" ")
|
||||
iv++
|
||||
case "-c:a ":
|
||||
codec = "-map 0:a:0? " + strings.ReplaceAll(codec, ":a ", ":a:"+strconv.Itoa(ia)+" ")
|
||||
ia++
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(codec)
|
||||
}
|
||||
|
||||
if a.filters != nil {
|
||||
for i, filter := range a.filters {
|
||||
if i == 0 {
|
||||
b.WriteString(" -vf ")
|
||||
} else {
|
||||
b.WriteByte(',')
|
||||
}
|
||||
b.WriteString(filter)
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(a.output)
|
||||
|
||||
return b.String()
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseArgs(t *testing.T) {
|
||||
args := parseArgs("rtsp://example.com#video=h264#rotate=180")
|
||||
assert.Equal(t, "ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i rtsp://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -an -vf transpose=1,transpose=1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}", args.String())
|
||||
|
||||
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=vaapi")
|
||||
assert.Equal(t, "ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport 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 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}", args.String())
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
var cache = map[string]string{}
|
||||
|
||||
// MakeHardware converts software FFmpeg args to hardware args
|
||||
// empty engine for autoselect
|
||||
func MakeHardware(args *Args, engine string) {
|
||||
for i, codec := range args.codecs {
|
||||
if len(codec) < 12 {
|
||||
continue // skip short line (-c:v libx264...)
|
||||
}
|
||||
|
||||
// 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 and MJPEG
|
||||
if engine == "" && name == "h264" {
|
||||
if engine = cache[name]; engine == "" {
|
||||
engine = ProbeHardware(name)
|
||||
cache[name] = engine
|
||||
}
|
||||
}
|
||||
|
||||
switch engine {
|
||||
case EngineVAAPI:
|
||||
args.input = "-hwaccel vaapi -hwaccel_output_format vaapi " + args.input
|
||||
args.codecs[i] = defaults[name+"/"+engine]
|
||||
|
||||
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
|
||||
args.InsertFilter("format=vaapi|nv12,hwupload")
|
||||
|
||||
case EngineCUDA:
|
||||
args.input = "-hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 2 " + args.input
|
||||
args.codecs[i] = defaults[name+"/"+engine]
|
||||
|
||||
for i, filter := range args.filters {
|
||||
if strings.HasPrefix(filter, "scale=") {
|
||||
args.filters[i] = "scale_cuda=" + filter[6:]
|
||||
}
|
||||
}
|
||||
|
||||
case EngineDXVA2:
|
||||
args.input = "-hwaccel dxva2 -hwaccel_output_format dxva2_vld " + args.input
|
||||
args.codecs[i] = defaults[name+"/"+engine]
|
||||
|
||||
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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func run(arg ...string) bool {
|
||||
err := exec.Command(defaults["bin"], arg...).Run()
|
||||
log.Printf("%v %v", arg, err)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package ffmpeg
|
||||
|
||||
func ProbeHardware(name string) string {
|
||||
switch name {
|
||||
case "h264":
|
||||
if run(
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "h264_videotoolbox", "-f", "null", "-") {
|
||||
return EngineVideoToolbox
|
||||
}
|
||||
|
||||
case "h265":
|
||||
if run(
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "hevc_videotoolbox", "-f", "null", "-") {
|
||||
return EngineVideoToolbox
|
||||
}
|
||||
}
|
||||
|
||||
return EngineSoftware
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func ProbeHardware(name string) string {
|
||||
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
|
||||
switch name {
|
||||
case "h264":
|
||||
if run(
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "h264_v4l2m2m", "-f", "null", "-") {
|
||||
return EngineV4L2M2M
|
||||
}
|
||||
|
||||
case "h265":
|
||||
if run(
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "hevc_v4l2m2m", "-f", "null", "-") {
|
||||
return EngineV4L2M2M
|
||||
}
|
||||
}
|
||||
|
||||
return EngineSoftware
|
||||
}
|
||||
|
||||
switch name {
|
||||
case "h264":
|
||||
if run("-init_hw_device", "cuda",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "h264_nvenc", "-f", "null", "-") {
|
||||
return EngineCUDA
|
||||
}
|
||||
|
||||
if run("-init_hw_device", "vaapi",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-vf", "format=nv12,hwupload",
|
||||
"-c", "h264_vaapi", "-f", "null", "-") {
|
||||
return EngineVAAPI
|
||||
}
|
||||
|
||||
case "h265":
|
||||
if run("-init_hw_device", "cuda",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "hevc_nvenc", "-f", "null", "-") {
|
||||
return EngineCUDA
|
||||
}
|
||||
|
||||
if run("-init_hw_device", "vaapi",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-vf", "format=nv12,hwupload",
|
||||
"-c", "hevc_vaapi", "-f", "null", "-") {
|
||||
return EngineVAAPI
|
||||
}
|
||||
|
||||
case "mjpeg":
|
||||
if run("-init_hw_device", "vaapi",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-vf", "format=nv12,hwupload",
|
||||
"-c", "mjpeg_vaapi", "-f", "null", "-") {
|
||||
return EngineVAAPI
|
||||
}
|
||||
}
|
||||
|
||||
return EngineSoftware
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package ffmpeg
|
||||
|
||||
func ProbeHardware(name string) string {
|
||||
switch name {
|
||||
case "h264":
|
||||
if run("-init_hw_device", "cuda",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "h264_nvenc", "-f", "null", "-") {
|
||||
return EngineCUDA
|
||||
}
|
||||
|
||||
if run("-init_hw_device", "dxva2",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "h264_qsv", "-f", "null", "-") {
|
||||
return EngineDXVA2
|
||||
}
|
||||
|
||||
case "h265":
|
||||
if run("-init_hw_device", "cuda",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "hevc_nvenc", "-f", "null", "-") {
|
||||
return EngineCUDA
|
||||
}
|
||||
|
||||
if run("-init_hw_device", "dxva2",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "hevc_qsv", "-f", "null", "-") {
|
||||
return EngineDXVA2
|
||||
}
|
||||
|
||||
case "mjpeg":
|
||||
if run("-init_hw_device", "dxva2",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "mjpeg_qsv", "-f", "null", "-") {
|
||||
return EngineDXVA2
|
||||
}
|
||||
}
|
||||
|
||||
return EngineSoftware
|
||||
}
|
||||
Reference in New Issue
Block a user