Skip to content

Commit

Permalink
Add support v4l2 source
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexxIT committed Jan 6, 2025
1 parent df83183 commit d59139a
Show file tree
Hide file tree
Showing 11 changed files with 859 additions and 0 deletions.
21 changes: 21 additions & 0 deletions examples/go2rtc_mjpeg/main.go
Original file line number Diff line number Diff line change
@@ -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 changes: 7 additions & 0 deletions internal/v4l2/v4l2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//go:build !linux

package v4l2

func Init() {
// not supported
}
79 changes: 79 additions & 0 deletions internal/v4l2/v4l2_linux.go
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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

Expand Down
244 changes: 244 additions & 0 deletions pkg/v4l2/device/device.go
Original file line number Diff line number Diff line change
@@ -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{}

Check failure on line 36 in pkg/v4l2/device/device.go

View workflow job for this annotation

GitHub Actions / Build binaries

undefined: v4l2_capability
if err := ioctl(d.fd, VIDIOC_QUERYCAP, unsafe.Pointer(&c)); err != nil {

Check failure on line 37 in pkg/v4l2/device/device.go

View workflow job for this annotation

GitHub Actions / Build binaries

undefined: VIDIOC_QUERYCAP
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{

Check failure on line 52 in pkg/v4l2/device/device.go

View workflow job for this annotation

GitHub Actions / Build binaries

undefined: v4l2_fmtdesc
index: i,
typ: V4L2_BUF_TYPE_VIDEO_CAPTURE,

Check failure on line 54 in pkg/v4l2/device/device.go

View workflow job for this annotation

GitHub Actions / Build binaries

undefined: V4L2_BUF_TYPE_VIDEO_CAPTURE
}
if err := ioctl(d.fd, VIDIOC_ENUM_FMT, unsafe.Pointer(&fd)); err != nil {

Check failure on line 56 in pkg/v4l2/device/device.go

View workflow job for this annotation

GitHub Actions / Build binaries

undefined: VIDIOC_ENUM_FMT
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{

Check failure on line 73 in pkg/v4l2/device/device.go

View workflow job for this annotation

GitHub Actions / Build binaries

undefined: v4l2_frmsizeenum
index: i,
pixel_format: pixFmt,
}
if err := ioctl(d.fd, VIDIOC_ENUM_FRAMESIZES, unsafe.Pointer(&fs)); err != nil {

Check failure on line 77 in pkg/v4l2/device/device.go

View workflow job for this annotation

GitHub Actions / Build binaries

undefined: VIDIOC_ENUM_FRAMESIZES
if !errors.Is(err, syscall.EINVAL) {
return nil, err
}
break
}

if fs.typ != V4L2_FRMSIZE_TYPE_DISCRETE {

Check failure on line 84 in pkg/v4l2/device/device.go

View workflow job for this annotation

GitHub Actions / Build binaries

undefined: 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{

Check failure on line 98 in pkg/v4l2/device/device.go

View workflow job for this annotation

GitHub Actions / Build binaries

undefined: v4l2_frmivalenum
index: i,
pixel_format: pixFmt,
width: width,
height: height,
}
if err := ioctl(d.fd, VIDIOC_ENUM_FRAMEINTERVALS, unsafe.Pointer(&fi)); err != nil {

Check failure on line 104 in pkg/v4l2/device/device.go

View workflow job for this annotation

GitHub Actions / Build binaries

undefined: VIDIOC_ENUM_FRAMEINTERVALS
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)
}
Loading

0 comments on commit d59139a

Please sign in to comment.