diff --git a/cmd/exec/exec.go b/cmd/exec/exec.go index 2c0c1ac4..72f6efc9 100644 --- a/cmd/exec/exec.go +++ b/cmd/exec/exec.go @@ -49,7 +49,7 @@ func Handle(url string) (streamer.Producer, error) { ) // remove `exec:` - args := strings.Split(url[5:], " ") + args := QuoteSplit(url[5:]) cmd := exec.Command(args[0], args[1:]...) if log.Trace().Enabled() { @@ -83,3 +83,39 @@ func Handle(url string) (streamer.Producer, error) { var log zerolog.Logger var waiters map[string]chan streamer.Producer + +func QuoteSplit(s string) []string { + var a []string + + for len(s) > 0 { + is := strings.IndexByte(s, ' ') + if is >= 0 { + // skip prefix and double spaces + if is == 0 { + // goto next symbol + s = s[1:] + continue + } + + // check if quote in word + if i := strings.IndexByte(s[:is], '"'); i >= 0 { + // search quote end + if is = strings.Index(s, `" `); is > 0 { + is += 1 + } else { + is = -1 + } + } + } + + if is >= 0 { + a = append(a, strings.ReplaceAll(s[:is], `"`, "")) + s = s[is+1:] + } else { + //add last word + a = append(a, s) + break + } + } + return a +} diff --git a/cmd/ffmpeg/README.md b/cmd/ffmpeg/README.md index c21b7e65..6c573035 100644 --- a/cmd/ffmpeg/README.md +++ b/cmd/ffmpeg/README.md @@ -1,6 +1,41 @@ +## 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_-_%D0%BE%D0%BF%D0%B8%D1%81%D0%B0%D0%BD%D0%B8%D0%B5_%D0%BA%D0%BB%D1%8E%D1%87%D0%B5%D0%B9_%D0%BA%D0%BE%D0%B4%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F +- https://codec.fandom.com/ru/wiki/X264_-_описание_ключей_кодирования - https://html5test.com/ +- https://trac.ffmpeg.org/wiki/Capture/Webcam +- https://trac.ffmpeg.org/wiki/DirectShow diff --git a/cmd/ffmpeg/device_darwin.go b/cmd/ffmpeg/device_darwin.go new file mode 100644 index 00000000..f6216e89 --- /dev/null +++ b/cmd/ffmpeg/device_darwin.go @@ -0,0 +1,63 @@ +package ffmpeg + +import ( + "bytes" + "github.com/AlexxIT/go2rtc/pkg/streamer" + "os/exec" + "strings" +) + +// https://trac.ffmpeg.org/wiki/Capture/Webcam +const deviceInputPrefix = "-f avfoundation" + +func deviceInputSuffix(videoIdx, audioIdx int) string { + video := findMedia(streamer.KindVideo, videoIdx) + audio := findMedia(streamer.KindAudio, audioIdx) + switch { + case video != nil && audio != nil: + return `"` + video.Title + `:` + audio.Title + `"` + case video != nil: + return `"` + video.Title + `"` + case audio != nil: + return `"` + audio.Title + `"` + } + return "" +} + +func loadMedias() { + cmd := exec.Command( + tpl["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 = streamer.KindVideo + continue + case strings.HasSuffix(line, "audio devices:"): + kind = streamer.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) *streamer.Media { + return &streamer.Media{ + Kind: kind, Title: name, + } +} diff --git a/cmd/ffmpeg/device_linux.go b/cmd/ffmpeg/device_linux.go new file mode 100644 index 00000000..c02407d9 --- /dev/null +++ b/cmd/ffmpeg/device_linux.go @@ -0,0 +1,36 @@ +package ffmpeg + +import ( + "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/rs/zerolog/log" + "io/ioutil" + "strings" +) + +// https://trac.ffmpeg.org/wiki/Capture/Webcam +const deviceInputPrefix = "-f v4l2" + +func deviceInputSuffix(videoIdx, audioIdx int) string { + video := findMedia(streamer.KindVideo, videoIdx) + return video.Title +} + +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(), streamer.KindVideo) { + media := loadMedia(streamer.KindVideo, "/dev/"+file.Name()) + medias = append(medias, media) + } + } +} + +func loadMedia(kind, name string) *streamer.Media { + return &streamer.Media{ + Kind: kind, Title: name, + } +} diff --git a/cmd/ffmpeg/device_windows.go b/cmd/ffmpeg/device_windows.go new file mode 100644 index 00000000..78b134af --- /dev/null +++ b/cmd/ffmpeg/device_windows.go @@ -0,0 +1,59 @@ +package ffmpeg + +import ( + "bytes" + "github.com/AlexxIT/go2rtc/pkg/streamer" + "os/exec" + "strings" +) + +// https://trac.ffmpeg.org/wiki/DirectShow +const deviceInputPrefix = "-f dshow" + +func deviceInputSuffix(videoIdx, audioIdx int) string { + video := findMedia(streamer.KindVideo, videoIdx) + audio := findMedia(streamer.KindAudio, audioIdx) + switch { + case video != nil && audio != nil: + return `video="` + video.Title + `":audio=` + audio.Title + `"` + case video != nil: + return `video="` + video.Title + `"` + case audio != nil: + return `audio="` + audio.Title + `"` + } + return "" +} + +func loadMedias() { + cmd := exec.Command( + tpl["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 = streamer.KindVideo + } else if strings.HasSuffix(line, "(audio)") { + kind = streamer.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) *streamer.Media { + return &streamer.Media{ + Kind: kind, Title: name, + } +} diff --git a/cmd/ffmpeg/devices.go b/cmd/ffmpeg/devices.go new file mode 100644 index 00000000..e622ff0e --- /dev/null +++ b/cmd/ffmpeg/devices.go @@ -0,0 +1,73 @@ +package ffmpeg + +import ( + "encoding/json" + "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/rs/zerolog/log" + "net/http" + "net/url" + "strconv" + "strings" +) + +func getDevice(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 medias []*streamer.Media + +func findMedia(kind string, index int) *streamer.Media { + for _, media := range medias { + if media.Kind != kind { + continue + } + if index == 0 { + return media + } + index-- + } + return nil +} + +func handleDevices(w http.ResponseWriter, r *http.Request) { + if medias == nil { + loadMedias() + } + + data, err := json.Marshal(medias) + if err != nil { + log.Error().Err(err).Msg("[api.ffmpeg]") + return + } + if _, err = w.Write(data); err != nil { + log.Error().Err(err).Msg("[api.ffmpeg]") + } +} diff --git a/cmd/ffmpeg/ffmpeg.go b/cmd/ffmpeg/ffmpeg.go index 8ff29378..167ba9a9 100644 --- a/cmd/ffmpeg/ffmpeg.go +++ b/cmd/ffmpeg/ffmpeg.go @@ -1,6 +1,7 @@ package ffmpeg import ( + "github.com/AlexxIT/go2rtc/cmd/api" "github.com/AlexxIT/go2rtc/cmd/app" "github.com/AlexxIT/go2rtc/cmd/exec" "github.com/AlexxIT/go2rtc/cmd/streams" @@ -20,9 +21,9 @@ func Init() { "bin": "ffmpeg", // inputs - "link": "-hide_banner -i {input}", - "rtsp": "-hide_banner -fflags nobuffer -flags low_delay -rtsp_transport tcp -i {input}", - "file": "-hide_banner -re -stream_loop -1 -i {input}", + "link": "-i {input}", + "rtsp": "-fflags nobuffer -flags low_delay -rtsp_transport tcp -i {input}", + "file": "-re -stream_loop -1 -i {input}", // output "out": "-rtsp_transport tcp -f rtsp {output}", @@ -31,7 +32,8 @@ func Init() { // `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1` // `-tune zerolatency` - for minimal latency // `-profile main -level 4.1` - most used streaming profile - "h264": "-codec:v libx264 -g 30 -preset superfast -tune zerolatency -profile main -level 4.1", + // `-pix_fmt yuv420p` - if input pix format 4:2:2 + "h264": "-codec:v libx264 -g 30 -preset superfast -tune zerolatency -profile main -level 4.1 -pix_fmt yuv420p", "h264/ultra": "-codec:v libx264 -g 30 -preset ultrafast -tune zerolatency", "h264/high": "-codec:v libx264 -g 30 -preset superfast -tune zerolatency", "h265": "-codec:v libx265 -g 30 -preset ultrafast -tune zerolatency", @@ -47,7 +49,7 @@ func Init() { app.LoadConfig(&cfg) - tpl := cfg.Mod + tpl = cfg.Mod streams.HandleFunc("ffmpeg", func(s string) (streamer.Producer, error) { s = s[7:] // remove `ffmpeg:` @@ -62,13 +64,15 @@ func Init() { switch { case strings.HasPrefix(s, "rtsp"): template = tpl["rtsp"] + case strings.HasPrefix(s, "device"): + template, _ = getDevice(s) case strings.Contains(s, "://"): template = tpl["link"] default: template = tpl["file"] } - s = "exec:" + tpl["bin"] + " " + + s = "exec:" + tpl["bin"] + " -hide_banner " + strings.Replace(template, "{input}", s, 1) if query != nil { @@ -109,8 +113,12 @@ func Init() { return exec.Handle(s) }) + + api.HandleFunc("/api/devices", handleDevices) } +var tpl map[string]string + func parseQuery(s string) map[string][]string { query := map[string][]string{} for _, key := range strings.Split(s, "#") { diff --git a/go.mod b/go.mod index a4091287..aa965dbd 100644 --- a/go.mod +++ b/go.mod @@ -38,4 +38,13 @@ require ( golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 // indirect ) -replace github.com/deepch/vdk v0.0.19 => github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e +replace ( + // windows support: https://github.com/brutella/dnssd/pull/35 + github.com/brutella/dnssd v1.2.2 => github.com/rblenkinsopp/dnssd v1.2.3-0.20220516082132-0923f3c787a1 + // RTP tlv8 fix + github.com/brutella/hap v0.0.17 => github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657 + // MSE update + github.com/deepch/vdk v0.0.19 => github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e + // AES_256_CM_HMAC_SHA1_80 support + github.com/pion/srtp/v2 v2.0.10 => github.com/AlexxIT/srtp/v2 v2.0.10-0.20220608200505-3191d4f19c10 +) diff --git a/go.sum b/go.sum index 457cad44..b790fc51 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/AlexxIT/srtp/v2 v2.0.10-0.20220608200505-3191d4f19c10 h1:4aKRthhmkYcStKuk1hcyvkeNJ/BDx5BTIvYmDO9ZJvg= +github.com/AlexxIT/srtp/v2 v2.0.10-0.20220608200505-3191d4f19c10/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4= github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e h1:NAgHHZB+JUN3/J4/yq1q1EAc8xwJ8bb/Qp0AcjkfzAA= github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e/go.mod h1:KqQ/KU3hOc4a62l/jPRH5Hiz5fhTq5cGCl8IqeCxWQI= github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= @@ -90,8 +92,6 @@ github.com/pion/sdp/v3 v3.0.5/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0 github.com/pion/srtp v1.5.1 h1:9Q3jAfslYZBt+C69SI/ZcONJh9049JUHZWYRRf5KEKw= github.com/pion/srtp v1.5.1/go.mod h1:B+QgX5xPeQTNc1CJStJPHzOlHK66ViMDWTT0HZTCkcA= github.com/pion/srtp/v2 v2.0.9/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4= -github.com/pion/srtp/v2 v2.0.10 h1:b8ZvEuI+mrL8hbr/f1YiJFB34UMrOac3R3N1yq2UN0w= -github.com/pion/srtp/v2 v2.0.10/go.mod h1:XEeSWaK9PfuMs7zxXyiN252AHPbH12NX5q/CFDWtUuA= github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg= github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE= diff --git a/pkg/streamer/media.go b/pkg/streamer/media.go index 529236ac..b440352b 100644 --- a/pkg/streamer/media.go +++ b/pkg/streamer/media.go @@ -46,12 +46,13 @@ func GetKind(name string) string { // - deepch/vdk/format/rtsp/sdp.Media // - pion/sdp.MediaDescription type Media struct { - Kind string // video, audio - Direction string - Codecs []*Codec + Kind string `json:"kind,omitempty"` // video or audio + Direction string `json:"direction,omitempty"` + Codecs []*Codec `json:"codecs,omitempty"` - MID string // TODO: fixme? - Control string // TODO: fixme? + MID string `json:"mid,omitempty"` // TODO: fixme? + Control string `json:"control,omitempty"` // TODO: fixme? + Title string `json:"title,omitempty"` // TODO: fixme? } func (m *Media) String() string { diff --git a/www/devices.html b/www/devices.html new file mode 100644 index 00000000..c01c792a --- /dev/null +++ b/www/devices.html @@ -0,0 +1,82 @@ + + + + + + + + go2rtc + + + + +
+ + add +
+ + + + + + + + + +
KindName
+ + + \ No newline at end of file diff --git a/www/index.html b/www/index.html index 8c16af01..c5f2bf3a 100644 --- a/www/index.html +++ b/www/index.html @@ -45,6 +45,7 @@
add + devices