diff --git a/examples/go2rtc_mjpeg/main.go b/examples/go2rtc_mjpeg/main.go new file mode 100644 index 00000000..a3e08ff5 --- /dev/null +++ b/examples/go2rtc_mjpeg/main.go @@ -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() +} diff --git a/internal/v4l2/v4l2.go b/internal/v4l2/v4l2.go new file mode 100644 index 00000000..9cef99a5 --- /dev/null +++ b/internal/v4l2/v4l2.go @@ -0,0 +1,7 @@ +//go:build !linux + +package v4l2 + +func Init() { + // not supported +} diff --git a/internal/v4l2/v4l2_linux.go b/internal/v4l2/v4l2_linux.go new file mode 100644 index 00000000..4a54e1e1 --- /dev/null +++ b/internal/v4l2/v4l2_linux.go @@ -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) +} diff --git a/main.go b/main.go index b8d58b27..db8de9f4 100644 --- a/main.go +++ b/main.go @@ -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 diff --git a/pkg/v4l2/device/device.go b/pkg/v4l2/device/device.go new file mode 100644 index 00000000..4e4d6ade --- /dev/null +++ b/pkg/v4l2/device/device.go @@ -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) +} diff --git a/pkg/v4l2/device/formats.go b/pkg/v4l2/device/formats.go new file mode 100644 index 00000000..94d12504 --- /dev/null +++ b/pkg/v4l2/device/formats.go @@ -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++ + } +} diff --git a/pkg/v4l2/device/videodev2_test.go b/pkg/v4l2/device/videodev2_test.go new file mode 100644 index 00000000..2556feef --- /dev/null +++ b/pkg/v4l2/device/videodev2_test.go @@ -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{}))) + } +} diff --git a/pkg/v4l2/device/videodev2_x32.go b/pkg/v4l2/device/videodev2_x32.go new file mode 100644 index 00000000..4c4db26d --- /dev/null +++ b/pkg/v4l2/device/videodev2_x32.go @@ -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 +} diff --git a/pkg/v4l2/device/videodev2_x64.go b/pkg/v4l2/device/videodev2_x64.go new file mode 100644 index 00000000..97c3ab95 --- /dev/null +++ b/pkg/v4l2/device/videodev2_x64.go @@ -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 +} diff --git a/pkg/v4l2/producer.go b/pkg/v4l2/producer.go new file mode 100644 index 00000000..644c5ee5 --- /dev/null +++ b/pkg/v4l2/producer.go @@ -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()) +} diff --git a/www/add.html b/www/add.html index 4b40f431..49e954d3 100644 --- a/www/add.html +++ b/www/add.html @@ -292,6 +292,18 @@ + +