Rework FFmpeg devices support

This commit is contained in:
Alexey Khit
2023-05-04 00:03:01 +03:00
parent 1746f55eda
commit 5387e88fe3
5 changed files with 139 additions and 152 deletions
+36 -29
View File
@@ -1,43 +1,43 @@
package device package device
import ( import (
"bytes" "github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/core"
"os/exec" "os/exec"
"regexp"
"strings" "strings"
) )
// https://trac.ffmpeg.org/wiki/Capture/Webcam // https://trac.ffmpeg.org/wiki/Capture/Webcam
const deviceInputPrefix = "-f avfoundation" const deviceInputPrefix = "-f avfoundation"
func deviceInputSuffix(videoIdx, audioIdx int) string { func deviceInputSuffix(video, audio string) string {
video := findMedia(core.KindVideo, videoIdx)
audio := findMedia(core.KindAudio, audioIdx)
switch { switch {
case video != nil && audio != nil: case video != "" && audio != "":
return `"` + video.ID + `:` + audio.ID + `"` return `"` + video + `:` + audio + `"`
case video != nil: case video != "":
return `"` + video.ID + `"` return `"` + video + `"`
case audio != nil: case audio != "":
return `"` + audio.ID + `"` return `":` + audio + `"`
} }
return "" return ""
} }
func loadMedias() { 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( cmd := exec.Command(
Bin, "-hide_banner", "-list_devices", "true", "-f", "avfoundation", "-i", "dummy", Bin, "-hide_banner", "-list_devices", "true", "-f", "avfoundation", "-i", "",
) )
b, _ := cmd.CombinedOutput()
var buf bytes.Buffer re := regexp.MustCompile(`\[\d+] (.+)`)
cmd.Stderr = &buf
_ = cmd.Run()
var kind string var kind string
for _, line := range strings.Split(string(b), "\n") {
lines := strings.Split(buf.String(), "\n")
process:
for _, line := range lines {
switch { switch {
case strings.HasSuffix(line, "video devices:"): case strings.HasSuffix(line, "video devices:"):
kind = core.KindVideo kind = core.KindVideo
@@ -45,17 +45,24 @@ process:
case strings.HasSuffix(line, "audio devices:"): case strings.HasSuffix(line, "audio devices:"):
kind = core.KindAudio kind = core.KindAudio
continue continue
case strings.HasPrefix(line, "dummy"):
break process
} }
// [AVFoundation indev @ 0x7fad54604380] [0] FaceTime HD Camera m := re.FindStringSubmatch(line)
name := line[42:] if m == nil {
media := loadMedia(kind, name) continue
medias = append(medias, media) }
name := m[1]
switch kind {
case core.KindVideo:
videos = append(videos, name)
case core.KindAudio:
audios = append(audios, name)
}
streams = append(streams, api.Stream{
Name: name, URL: "ffmpeg:device?" + kind + "=" + name,
})
} }
} }
func loadMedia(kind, name string) *core.Media {
return &core.Media{Kind: kind, ID: name}
}
+37 -27
View File
@@ -1,50 +1,60 @@
package device package device
import ( import (
"bytes" "github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/core"
"io/ioutil" "os"
"os/exec" "os/exec"
"regexp"
"strings" "strings"
) )
// https://trac.ffmpeg.org/wiki/Capture/Webcam // https://trac.ffmpeg.org/wiki/Capture/Webcam
const deviceInputPrefix = "-f v4l2" const deviceInputPrefix = "-f v4l2"
func deviceInputSuffix(videoIdx, audioIdx int) string { func deviceInputSuffix(video, audio string) string {
if video := findMedia(core.KindVideo, videoIdx); video != nil { if video != "" {
return video.ID return video
} }
return "" return ""
} }
func loadMedias() { func initDevices() {
files, err := ioutil.ReadDir("/dev") files, err := os.ReadDir("/dev")
if err != nil { if err != nil {
return return
} }
for _, file := range files { for _, file := range files {
log.Trace().Msg("[ffmpeg] " + file.Name()) if !strings.HasPrefix(file.Name(), core.KindVideo) {
if strings.HasPrefix(file.Name(), core.KindVideo) { continue
media := loadMedia(core.KindVideo, "/dev/"+file.Name()) }
if media != nil {
medias = append(medias, media) 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.Stream{
Name: i[3] + " | " + 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)
} }
} }
} }
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}
}
+26 -33
View File
@@ -1,57 +1,50 @@
package device package device
import ( import (
"bytes" "github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/core"
"os/exec" "os/exec"
"strings" "regexp"
) )
// https://trac.ffmpeg.org/wiki/DirectShow // https://trac.ffmpeg.org/wiki/DirectShow
const deviceInputPrefix = "-f dshow" const deviceInputPrefix = "-f dshow"
func deviceInputSuffix(videoIdx, audioIdx int) string { func deviceInputSuffix(video, audio string) string {
video := findMedia(core.KindVideo, videoIdx)
audio := findMedia(core.KindAudio, audioIdx)
switch { switch {
case video != nil && audio != nil: case video != "" && audio != "":
return `video="` + video.ID + `":audio=` + audio.ID + `"` return `video="` + video + `":audio=` + audio + `"`
case video != nil: case video != "":
return `video="` + video.ID + `"` return `video="` + video + `"`
case audio != nil: case audio != "":
return `audio="` + audio.ID + `"` return `audio="` + audio + `"`
} }
return "" return ""
} }
func loadMedias() { func initDevices() {
cmd := exec.Command( cmd := exec.Command(
Bin, "-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "", Bin, "-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "",
) )
b, _ := cmd.CombinedOutput()
var buf bytes.Buffer re := regexp.MustCompile(`"([^"]+)" \((video|audio)\)`)
cmd.Stderr = &buf for _, m := range re.FindAllStringSubmatch(string(b), -1) {
_ = cmd.Run() name := m[1]
kind := m[2]
lines := strings.Split(buf.String(), "\r\n") stream := api.Stream{
for _, line := range lines { Name: name, URL: "ffmpeg:device?" + kind + "=" + name,
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 switch kind {
// [dshow @ 00000181e8d028c0] "VMware Virtual USB Video Device" (video) case core.KindVideo:
name := line[28 : len(line)-9] videos = append(videos, name)
media := loadMedia(kind, name) stream.URL += "#video=h264#hardware"
medias = append(medias, media) case core.KindAudio:
audios = append(audios, name)
}
streams = append(streams, stream)
} }
} }
func loadMedia(kind, name string) *core.Media {
return &core.Media{Kind: kind, ID: name}
}
+27 -50
View File
@@ -2,29 +2,24 @@ package device
import ( import (
"github.com/AlexxIT/go2rtc/internal/api" "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/http"
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"sync"
) )
func Init() { func Init() {
log = app.GetLogger("exec") api.HandleFunc("api/ffmpeg/devices", apiDevices)
api.HandleFunc("api/devices", handle)
} }
func GetInput(src string) (string, error) { func GetInput(src string) (string, error) {
if medias == nil { runonce.Do(initDevices)
loadMedias()
}
input := deviceInputPrefix input := deviceInputPrefix
var videoIdx, audioIdx int var video, audio string
if i := strings.IndexByte(src, '?'); i > 0 { if i := strings.IndexByte(src, '?'); i > 0 {
query, err := url.ParseQuery(src[i+1:]) query, err := url.ParseQuery(src[i+1:])
if err != nil { if err != nil {
@@ -33,9 +28,9 @@ func GetInput(src string) (string, error) {
for key, value := range query { for key, value := range query {
switch key { switch key {
case "video": case "video":
videoIdx, _ = strconv.Atoi(value[0]) video = value[0]
case "audio": case "audio":
audioIdx, _ = strconv.Atoi(value[0]) audio = value[0]
case "framerate": case "framerate":
input += " -framerate " + value[0] input += " -framerate " + value[0]
case "resolution": case "resolution":
@@ -44,48 +39,30 @@ func GetInput(src string) (string, error) {
} }
} }
input += " -i " + deviceInputSuffix(videoIdx, audioIdx) if video != "" {
if i, err := strconv.Atoi(video); err == nil && i < len(videos) {
video = videos[i]
}
}
if audio != "" {
if i, err := strconv.Atoi(audio); err == nil && i < len(audios) {
audio = audios[i]
}
}
input += " -i " + deviceInputSuffix(video, audio)
return input, nil return input, nil
} }
var Bin string var Bin string
var log zerolog.Logger
var medias []*core.Media
func findMedia(kind string, index int) *core.Media { var videos, audios []string
for _, media := range medias { var streams []api.Stream
if media.Kind != kind { var runonce sync.Once
continue
} func apiDevices(w http.ResponseWriter, r *http.Request) {
if index == 0 { runonce.Do(initDevices)
return media
} api.ResponseStreams(w, streams)
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)
} }
+13 -13
View File
@@ -171,6 +171,19 @@
</script> </script>
<button id="devices">FFmpeg Devices (USB)</button>
<div class="module">
<table id="devices-table">
</table>
</div>
<script>
document.getElementById('devices').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block'
await getStreams('api/ffmpeg/devices', 'devices-table')
})
</script>
<button id="hass">Home Assistant</button> <button id="hass">Home Assistant</button>
<div class="module"> <div class="module">
<table id="hass-table"></table> <table id="hass-table"></table>
@@ -232,19 +245,6 @@
</script> </script>
<button id="devices">USB Devices</button>
<div class="module">
<table id="devices-table">
</table>
</div>
<script>
document.getElementById('devices').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block'
await getStreams('api/devices', 'devices-table')
})
</script>
<button id="webtorrent">WebTorrent Shares</button> <button id="webtorrent">WebTorrent Shares</button>
<div class="module"> <div class="module">
<table id="webtorrent-table"></table> <table id="webtorrent-table"></table>