Rework FFmpeg devices support
This commit is contained in:
@@ -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}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user