Add support v4l2 source

This commit is contained in:
Alex X
2025-01-06 23:47:35 +03:00
parent df831833b1
commit d59139a2ab
11 changed files with 859 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
package main
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/mjpeg"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/internal/v4l2"
"github.com/AlexxIT/go2rtc/pkg/shell"
)
func main() {
app.Init()
streams.Init()
api.Init()
mjpeg.Init()
v4l2.Init()
shell.RunUntilSignal()
}
+7
View File
@@ -0,0 +1,7 @@
//go:build !linux
package v4l2
func Init() {
// not supported
}
+79
View File
@@ -0,0 +1,79 @@
package v4l2
import (
"encoding/binary"
"fmt"
"net/http"
"os"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/v4l2"
"github.com/AlexxIT/go2rtc/pkg/v4l2/device"
)
func Init() {
streams.HandleFunc("v4l2", func(source string) (core.Producer, error) {
return v4l2.Open(source)
})
api.HandleFunc("api/v4l2", apiV4L2)
}
func apiV4L2(w http.ResponseWriter, r *http.Request) {
files, err := os.ReadDir("/dev")
if err != nil {
return
}
var sources []*api.Source
for _, file := range files {
if !strings.HasPrefix(file.Name(), core.KindVideo) {
continue
}
path := "/dev/" + file.Name()
dev, err := device.Open(path)
if err != nil {
continue
}
formats, _ := dev.ListFormats()
for _, fourCC := range formats {
source := &api.Source{}
for _, format := range device.Formats {
if format.FourCC == fourCC {
source.Name = format.Name
source.URL = "v4l2:device?video=" + path + "&input_format=" + format.FFmpeg + "&video_size="
break
}
}
if source.Name != "" {
sizes, _ := dev.ListSizes(fourCC)
for i := 0; i < len(sizes); i += 2 {
size := fmt.Sprintf("%dx%d", sizes[i], sizes[i+1])
if i > 0 {
source.Info += " " + size
} else {
source.Info = size
source.URL += size
}
}
} else {
source.Name = string(binary.LittleEndian.AppendUint32(nil, fourCC))
}
sources = append(sources, source)
}
_ = dev.Close()
}
api.ResponseSources(w, sources)
}
+2
View File
@@ -31,6 +31,7 @@ import (
"github.com/AlexxIT/go2rtc/internal/srtp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/internal/tapo"
"github.com/AlexxIT/go2rtc/internal/v4l2"
"github.com/AlexxIT/go2rtc/internal/webrtc"
"github.com/AlexxIT/go2rtc/internal/webtorrent"
"github.com/AlexxIT/go2rtc/pkg/shell"
@@ -84,6 +85,7 @@ func main() {
expr.Init() // expr source
gopro.Init() // gopro source
doorbird.Init() // doorbird source
v4l2.Init() // v4l2 source
// 6. Helper modules
+244
View File
@@ -0,0 +1,244 @@
//go:build linux
package device
import (
"bytes"
"errors"
"fmt"
"syscall"
"unsafe"
)
type Device struct {
fd int
bufs [][]byte
}
func Open(path string) (*Device, error) {
fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CLOEXEC, 0)
if err != nil {
return nil, err
}
return &Device{fd: fd}, nil
}
const buffersCount = 2
type Capability struct {
Driver string
Card string
BusInfo string
Version string
}
func (d *Device) Capability() (*Capability, error) {
c := v4l2_capability{}
if err := ioctl(d.fd, VIDIOC_QUERYCAP, unsafe.Pointer(&c)); err != nil {
return nil, err
}
return &Capability{
Driver: str(c.driver[:]),
Card: str(c.card[:]),
BusInfo: str(c.bus_info[:]),
Version: fmt.Sprintf("%d.%d.%d", byte(c.version>>16), byte(c.version>>8), byte(c.version)),
}, nil
}
func (d *Device) ListFormats() ([]uint32, error) {
var items []uint32
for i := uint32(0); ; i++ {
fd := v4l2_fmtdesc{
index: i,
typ: V4L2_BUF_TYPE_VIDEO_CAPTURE,
}
if err := ioctl(d.fd, VIDIOC_ENUM_FMT, unsafe.Pointer(&fd)); err != nil {
if !errors.Is(err, syscall.EINVAL) {
return nil, err
}
break
}
items = append(items, fd.pixelformat)
}
return items, nil
}
func (d *Device) ListSizes(pixFmt uint32) ([]uint32, error) {
var items []uint32
for i := uint32(0); ; i++ {
fs := v4l2_frmsizeenum{
index: i,
pixel_format: pixFmt,
}
if err := ioctl(d.fd, VIDIOC_ENUM_FRAMESIZES, unsafe.Pointer(&fs)); err != nil {
if !errors.Is(err, syscall.EINVAL) {
return nil, err
}
break
}
if fs.typ != V4L2_FRMSIZE_TYPE_DISCRETE {
continue
}
items = append(items, fs.discrete.width, fs.discrete.height)
}
return items, nil
}
func (d *Device) ListFrameRates(pixFmt, width, height uint32) ([]uint32, error) {
var items []uint32
for i := uint32(0); ; i++ {
fi := v4l2_frmivalenum{
index: i,
pixel_format: pixFmt,
width: width,
height: height,
}
if err := ioctl(d.fd, VIDIOC_ENUM_FRAMEINTERVALS, unsafe.Pointer(&fi)); err != nil {
if !errors.Is(err, syscall.EINVAL) {
return nil, err
}
break
}
if fi.typ != V4L2_FRMIVAL_TYPE_DISCRETE || fi.discrete.numerator != 1 {
continue
}
items = append(items, fi.discrete.denominator)
}
return items, nil
}
func (d *Device) SetFormat(width, height, pixFmt uint32) error {
f := v4l2_format{
typ: V4L2_BUF_TYPE_VIDEO_CAPTURE,
fmt: v4l2_pix_format{
width: width,
height: height,
pixelformat: pixFmt,
field: V4L2_FIELD_NONE,
colorspace: V4L2_COLORSPACE_DEFAULT,
},
}
return ioctl(d.fd, VIDIOC_S_FMT, unsafe.Pointer(&f))
}
func (d *Device) SetParam(fps uint32) error {
p := v4l2_streamparm{
typ: V4L2_BUF_TYPE_VIDEO_CAPTURE,
capture: v4l2_captureparm{
timeperframe: v4l2_fract{numerator: 1, denominator: fps},
},
}
return ioctl(d.fd, VIDIOC_S_PARM, unsafe.Pointer(&p))
}
func (d *Device) StreamOn() (err error) {
rb := v4l2_requestbuffers{
count: buffersCount,
typ: V4L2_BUF_TYPE_VIDEO_CAPTURE,
memory: V4L2_MEMORY_MMAP,
}
if err = ioctl(d.fd, VIDIOC_REQBUFS, unsafe.Pointer(&rb)); err != nil {
return err
}
d.bufs = make([][]byte, buffersCount)
for i := uint32(0); i < buffersCount; i++ {
qb := v4l2_buffer{
index: i,
typ: V4L2_BUF_TYPE_VIDEO_CAPTURE,
memory: V4L2_MEMORY_MMAP,
}
if err = ioctl(d.fd, VIDIOC_QUERYBUF, unsafe.Pointer(&qb)); err != nil {
return err
}
if d.bufs[i], err = syscall.Mmap(
d.fd, int64(qb.offset), int(qb.length), syscall.PROT_READ, syscall.MAP_SHARED,
); nil != err {
return err
}
if err = ioctl(d.fd, VIDIOC_QBUF, unsafe.Pointer(&qb)); err != nil {
return err
}
}
typ := uint32(V4L2_BUF_TYPE_VIDEO_CAPTURE)
return ioctl(d.fd, VIDIOC_STREAMON, unsafe.Pointer(&typ))
}
func (d *Device) StreamOff() (err error) {
typ := uint32(V4L2_BUF_TYPE_VIDEO_CAPTURE)
if err = ioctl(d.fd, VIDIOC_STREAMOFF, unsafe.Pointer(&typ)); err != nil {
return err
}
for i := range d.bufs {
_ = syscall.Munmap(d.bufs[i])
}
rb := v4l2_requestbuffers{
count: 0,
typ: V4L2_BUF_TYPE_VIDEO_CAPTURE,
memory: V4L2_MEMORY_MMAP,
}
return ioctl(d.fd, VIDIOC_REQBUFS, unsafe.Pointer(&rb))
}
func (d *Device) Capture(cositedYUV bool) ([]byte, error) {
dec := v4l2_buffer{
typ: V4L2_BUF_TYPE_VIDEO_CAPTURE,
memory: V4L2_MEMORY_MMAP,
}
if err := ioctl(d.fd, VIDIOC_DQBUF, unsafe.Pointer(&dec)); err != nil {
return nil, err
}
buf := make([]byte, dec.bytesused)
if cositedYUV {
YUYV2YUV(buf, d.bufs[dec.index][:dec.bytesused])
} else {
copy(buf, d.bufs[dec.index][:dec.bytesused])
}
enc := v4l2_buffer{
typ: V4L2_BUF_TYPE_VIDEO_CAPTURE,
memory: V4L2_MEMORY_MMAP,
index: dec.index,
}
if err := ioctl(d.fd, VIDIOC_QBUF, unsafe.Pointer(&enc)); err != nil {
return nil, err
}
return buf, nil
}
func (d *Device) Close() error {
return syscall.Close(d.fd)
}
func ioctl(fd int, req uint, arg unsafe.Pointer) error {
_, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg))
if err != 0 {
return err
}
return nil
}
func str(b []byte) string {
if i := bytes.IndexByte(b, 0); i >= 0 {
return string(b[:i])
}
return string(b)
}
+40
View File
@@ -0,0 +1,40 @@
package device
const (
V4L2_PIX_FMT_YUYV = 'Y' | 'U'<<8 | 'Y'<<16 | 'V'<<24
V4L2_PIX_FMT_MJPEG = 'M' | 'J'<<8 | 'P'<<16 | 'G'<<24
)
type Format struct {
FourCC uint32
Name string
FFmpeg string
}
var Formats = []Format{
{V4L2_PIX_FMT_YUYV, "YUV 4:2:2", "yuyv422"},
{V4L2_PIX_FMT_MJPEG, "Motion-JPEG", "mjpeg"},
}
// YUYV2YUV convert [Y0 Cb Y1 Cr] to cosited [Y0Y1... Cb... Cr...]
func YUYV2YUV(dst, src []byte) {
n := len(src)
i0 := 0
iy := 0
iu := n / 2
iv := n / 4 * 3
for i0 < n {
dst[iy] = src[i0]
i0++
iy++
dst[iu] = src[i0]
i0++
iu++
dst[iy] = src[i0]
i0++
iy++
dst[iv] = src[i0]
i0++
iv++
}
}
+34
View File
@@ -0,0 +1,34 @@
package device
import (
"runtime"
"testing"
"unsafe"
"github.com/stretchr/testify/require"
)
func TestSize(t *testing.T) {
switch runtime.GOARCH {
case "amd64", "arm64":
require.Equal(t, 104, int(unsafe.Sizeof(v4l2_capability{})))
require.Equal(t, 208, int(unsafe.Sizeof(v4l2_format{})))
require.Equal(t, 204, int(unsafe.Sizeof(v4l2_streamparm{})))
require.Equal(t, 20, int(unsafe.Sizeof(v4l2_requestbuffers{})))
require.Equal(t, 88, int(unsafe.Sizeof(v4l2_buffer{})))
require.Equal(t, 16, int(unsafe.Sizeof(v4l2_timecode{})))
require.Equal(t, 64, int(unsafe.Sizeof(v4l2_fmtdesc{})))
require.Equal(t, 44, int(unsafe.Sizeof(v4l2_frmsizeenum{})))
require.Equal(t, 52, int(unsafe.Sizeof(v4l2_frmivalenum{})))
case "386", "arm":
require.Equal(t, 104, int(unsafe.Sizeof(v4l2_capability{})))
require.Equal(t, 204, int(unsafe.Sizeof(v4l2_format{})))
require.Equal(t, 204, int(unsafe.Sizeof(v4l2_streamparm{})))
require.Equal(t, 20, int(unsafe.Sizeof(v4l2_requestbuffers{})))
require.Equal(t, 68, int(unsafe.Sizeof(v4l2_buffer{})))
require.Equal(t, 16, int(unsafe.Sizeof(v4l2_timecode{})))
require.Equal(t, 64, int(unsafe.Sizeof(v4l2_fmtdesc{})))
require.Equal(t, 44, int(unsafe.Sizeof(v4l2_frmsizeenum{})))
require.Equal(t, 52, int(unsafe.Sizeof(v4l2_frmivalenum{})))
}
}
+152
View File
@@ -0,0 +1,152 @@
//go:build 386 || arm
package device
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/videodev2.h
const (
VIDIOC_QUERYCAP = 0x80685600
VIDIOC_ENUM_FMT = 0xc0405602
VIDIOC_G_FMT = 0xc0cc5604
VIDIOC_S_FMT = 0xc0cc5605
VIDIOC_REQBUFS = 0xc0145608
VIDIOC_QUERYBUF = 0xc0445609
VIDIOC_QBUF = 0xc044560f
VIDIOC_DQBUF = 0xc0445611
VIDIOC_STREAMON = 0x40045612
VIDIOC_STREAMOFF = 0x40045613
VIDIOC_G_PARM = 0xc0cc5615
VIDIOC_S_PARM = 0xc0cc5616
VIDIOC_ENUM_FRAMESIZES = 0xc02c564a
VIDIOC_ENUM_FRAMEINTERVALS = 0xc034564b
)
const (
V4L2_BUF_TYPE_VIDEO_CAPTURE = 1
V4L2_COLORSPACE_DEFAULT = 0
V4L2_FIELD_NONE = 1
V4L2_FRMIVAL_TYPE_DISCRETE = 1
V4L2_FRMSIZE_TYPE_DISCRETE = 1
V4L2_MEMORY_MMAP = 1
)
type v4l2_capability struct {
driver [16]byte
card [32]byte
bus_info [32]byte
version uint32
capabilities uint32
device_caps uint32
reserved [3]uint32
}
type v4l2_format struct {
typ uint32
fmt v4l2_pix_format
}
type v4l2_pix_format struct {
width uint32 // 0
height uint32 // 4
pixelformat uint32 // 8
field uint32 // 12
bytesperline uint32 // 16
sizeimage uint32 // 20
colorspace uint32 // 24
priv uint32 // 28
flags uint32 // 32
ycbcr_enc uint32 // 36
quantization uint32 // 40
xfer_func uint32 // 44
_ [152]byte // 48
}
type v4l2_streamparm struct {
typ uint32
capture v4l2_captureparm
}
type v4l2_captureparm struct {
capability uint32 // 0
capturemode uint32 // 4
timeperframe v4l2_fract // 8
extendedmode uint32 // 16
readbuffers uint32 // 20
_ [176]byte // 24
}
type v4l2_fract struct {
numerator uint32
denominator uint32
}
type v4l2_requestbuffers struct {
count uint32
typ uint32
memory uint32
capabilities uint32
flags uint8
reserved [3]uint8
}
type v4l2_buffer struct {
index uint32 // 0
typ uint32 // 4
bytesused uint32 // 8
flags uint32 // 12
field uint32 // 16
_ [8]byte // 20
timecode v4l2_timecode // 28
sequence uint32 // 44
memory uint32 // 48
offset uint32 // 52
length uint32 // 56
_ [8]byte // 60
}
type v4l2_timecode struct {
typ uint32
flags uint32
frames uint8
seconds uint8
minutes uint8
hours uint8
userbits [4]uint8
}
type v4l2_fmtdesc struct {
index uint32
typ uint32
flags uint32
description [32]byte
pixelformat uint32
mbus_code uint32
reserved [3]uint32
}
type v4l2_frmsizeenum struct {
index uint32 // 0
pixel_format uint32 // 4
typ uint32 // 8
discrete v4l2_frmsize_discrete // 12
_ [24]byte
}
type v4l2_frmsize_discrete struct {
width uint32
height uint32
}
type v4l2_frmivalenum struct {
index uint32
pixel_format uint32
width uint32
height uint32
typ uint32
discrete v4l2_fract
_ [24]byte
}
+153
View File
@@ -0,0 +1,153 @@
//go:build amd64 || arm64
package device
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/videodev2.h
const (
VIDIOC_QUERYCAP = 0x80685600
VIDIOC_ENUM_FMT = 0xc0405602
VIDIOC_G_FMT = 0xc0d05604
VIDIOC_S_FMT = 0xc0d05605
VIDIOC_REQBUFS = 0xc0145608
VIDIOC_QUERYBUF = 0xc0585609
VIDIOC_QBUF = 0xc058560f
VIDIOC_DQBUF = 0xc0585611
VIDIOC_STREAMON = 0x40045612
VIDIOC_STREAMOFF = 0x40045613
VIDIOC_G_PARM = 0xc0cc5615
VIDIOC_S_PARM = 0xc0cc5616
VIDIOC_ENUM_FRAMESIZES = 0xc02c564a
VIDIOC_ENUM_FRAMEINTERVALS = 0xc034564b
)
const (
V4L2_BUF_TYPE_VIDEO_CAPTURE = 1
V4L2_COLORSPACE_DEFAULT = 0
V4L2_FIELD_NONE = 1
V4L2_FRMIVAL_TYPE_DISCRETE = 1
V4L2_FRMSIZE_TYPE_DISCRETE = 1
V4L2_MEMORY_MMAP = 1
)
type v4l2_capability struct {
driver [16]byte
card [32]byte
bus_info [32]byte
version uint32
capabilities uint32
device_caps uint32
reserved [3]uint32
}
type v4l2_format struct {
typ uint64
fmt v4l2_pix_format
}
type v4l2_pix_format struct {
width uint32 // 0
height uint32 // 4
pixelformat uint32 // 8
field uint32 // 12
bytesperline uint32 // 16
sizeimage uint32 // 20
colorspace uint32 // 24
priv uint32 // 28
flags uint32 // 32
ycbcr_enc uint32 // 36
quantization uint32 // 40
xfer_func uint32 // 44
_ [152]byte // 48
}
type v4l2_streamparm struct {
typ uint32
capture v4l2_captureparm
}
type v4l2_captureparm struct {
capability uint32 // 0
capturemode uint32 // 4
timeperframe v4l2_fract // 8
extendedmode uint32 // 16
readbuffers uint32 // 20
_ [176]byte // 24
}
type v4l2_fract struct {
numerator uint32
denominator uint32
}
type v4l2_requestbuffers struct {
count uint32
typ uint32
memory uint32
capabilities uint32
flags uint8
reserved [3]uint8
}
type v4l2_buffer struct {
index uint32 // 0
typ uint32 // 4
bytesused uint32 // 8
flags uint32 // 12
field uint32 // 16
_ [20]byte // 20
timecode v4l2_timecode // 40
sequence uint32 // 56
memory uint32 // 60
offset uint32 // 64
_ [4]byte // 68
length uint32 // 72
_ [12]byte // 76
}
type v4l2_timecode struct {
typ uint32
flags uint32
frames uint8
seconds uint8
minutes uint8
hours uint8
userbits [4]uint8
}
type v4l2_fmtdesc struct {
index uint32
typ uint32
flags uint32
description [32]byte
pixelformat uint32
mbus_code uint32
reserved [3]uint32
}
type v4l2_frmsizeenum struct {
index uint32 // 0
pixel_format uint32 // 4
typ uint32 // 8
discrete v4l2_frmsize_discrete // 12
_ [24]byte
}
type v4l2_frmsize_discrete struct {
width uint32
height uint32
}
type v4l2_frmivalenum struct {
index uint32
pixel_format uint32
width uint32
height uint32
typ uint32
discrete v4l2_fract
_ [24]byte
}
+115
View File
@@ -0,0 +1,115 @@
//go:build linux
package v4l2
import (
"errors"
"net/url"
"strings"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/v4l2/device"
"github.com/pion/rtp"
)
type Producer struct {
core.Connection
dev *device.Device
}
func Open(rawURL string) (*Producer, error) {
// Example (ffmpeg source compatible):
// v4l2:device?video=/dev/video0&input_format=mjpeg&video_size=1280x720
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
query := u.Query()
dev, err := device.Open(query.Get("video"))
if err != nil {
return nil, err
}
codec := &core.Codec{
ClockRate: 90000,
PayloadType: core.PayloadTypeRAW,
}
var width, height, pixFmt uint32
if wh := strings.Split(query.Get("video_size"), "x"); len(wh) == 2 {
codec.FmtpLine = "width=" + wh[0] + ";height=" + wh[1]
width = uint32(core.Atoi(wh[0]))
height = uint32(core.Atoi(wh[1]))
}
switch query.Get("input_format") {
case "mjpeg":
codec.Name = core.CodecJPEG
pixFmt = device.V4L2_PIX_FMT_MJPEG
case "yuyv422":
if codec.FmtpLine == "" {
return nil, errors.New("v4l2: invalid video_size")
}
codec.Name = core.CodecRAW
codec.FmtpLine += ";colorspace=422"
pixFmt = device.V4L2_PIX_FMT_YUYV
default:
return nil, errors.New("v4l2: invalid input_format")
}
if err = dev.SetFormat(width, height, pixFmt); err != nil {
return nil, err
}
medias := []*core.Media{
{
Kind: core.KindVideo,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{codec},
},
}
return &Producer{
Connection: core.Connection{
ID: core.NewID(),
FormatName: "v4l2",
Medias: medias,
},
dev: dev,
}, nil
}
func (c *Producer) Start() error {
if err := c.dev.StreamOn(); err != nil {
return err
}
cositedYUV := c.Medias[0].Codecs[0].Name == core.CodecRAW
for {
buf, err := c.dev.Capture(cositedYUV)
if err != nil {
return err
}
c.Recv += len(buf)
if len(c.Receivers) == 0 {
continue
}
pkt := &rtp.Packet{
Header: rtp.Header{Timestamp: core.Now90000()},
Payload: buf,
}
c.Receivers[0].WriteRTP(pkt)
}
}
func (c *Producer) Stop() error {
_ = c.Connection.Stop()
return errors.Join(c.dev.StreamOff(), c.dev.Close())
}
+12
View File
@@ -292,6 +292,18 @@
</script>
<button id="v4l2">V4L2 (USB)</button>
<div class="module">
<table id="v4l2-table"></table>
</div>
<script>
document.getElementById('v4l2').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block';
await getSources('v4l2-table', 'api/v4l2');
});
</script>
<button id="webtorrent">WebTorrent Shares</button>
<div class="module">
<table id="webtorrent-table"></table>