Skip to content

Commit

Permalink
Adds support local USB cameras
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexxIT committed Aug 26, 2022
1 parent c019dc5 commit 64247fc
Show file tree
Hide file tree
Showing 12 changed files with 419 additions and 16 deletions.
38 changes: 37 additions & 1 deletion cmd/exec/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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
}
37 changes: 36 additions & 1 deletion cmd/ffmpeg/README.md
Original file line number Diff line number Diff line change
@@ -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
63 changes: 63 additions & 0 deletions cmd/ffmpeg/device_darwin.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
36 changes: 36 additions & 0 deletions cmd/ffmpeg/device_linux.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
59 changes: 59 additions & 0 deletions cmd/ffmpeg/device_windows.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
73 changes: 73 additions & 0 deletions cmd/ffmpeg/devices.go
Original file line number Diff line number Diff line change
@@ -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]")
}
}
20 changes: 14 additions & 6 deletions cmd/ffmpeg/ffmpeg.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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}",
Expand All @@ -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",
Expand All @@ -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:`
Expand All @@ -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 {
Expand Down Expand Up @@ -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, "#") {
Expand Down
Loading

0 comments on commit 64247fc

Please sign in to comment.