diff --git a/README.md b/README.md index c1dfe7d..e18695d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ +[![Go Report Card](https://goreportcard.com/badge/github.com/vladimirvivien/go4vl)](https://goreportcard.com/report/github.com/vladimirvivien/go4vl) + # go4vl + A Go library for the `Video for Linux 2` (v4l2) user API. ---- @@ -10,91 +13,79 @@ It hides all the complexities of working with V4L2 and provides idiomatic Go typ > It is *NOT* meant to be a portable/cross-platform capable package for real-time video processing. ## Features + * Capture and control video data from your Go programs -* Idiomatic Go API for device access and video capture -* Use cgo-generated types for correct data representation in Go -* Use familiar types such as channels to stream video data +* Idiomatic Go types such as channels to access and stream video data * Exposes device enumeration and information * Provides device capture control * Access to video format information -* Streaming support using memory map (other methods coming later) +* Streaming users zero-copy IO using memory mapped buffers -### Not working/supported yet -* Inherent support for video output -* Only support MMap memory stream (user pointers, DMA not working) -* Device control not implemented yet +## Compilation Requirements -## Prerequisites * Go compiler/tools -* Linux OS (32- or 64-bit) * Kernel minimum v5.10.x * A locally configured C compiler (i.e. gcc) * Header files for V4L2 (i.e. /usr/include/linux/videodev2.h) -All examples have been tested using a Rasperry PI 3, running 32-bit Raspberry PI OS. +All examples have been tested using a Raspberry PI 3, running 32-bit Raspberry PI OS. The package should work with no problem on your 64-bit Linux OS. ## Getting started + +### System upgrade + To avoid issues with old header files on your machine, upgrade your system to pull down the latest OS packages -with something similar to the following (follow directions for your system to properly upgrade): +with something similar to the following (follow directions for your system for proper upgrade): ```shell sudo apt update sudo apt full-upgrade ``` -To include `go4vl` in your own code, pull the package +### Using the go4vl package + +To include `go4vl` in your own code, `go get` the package: ```bash go get github.com/vladimirvivien/go4vl/v4l2 ``` -## Examples -The following is a simple example that captures video data from an attached camera device to -and saves them as JPEG files. The example assumes the attached device supports JPEG (MJPEG) output format inherently. +## Video capture example -```go -package main +The following is a simple example that captures video data from an attached camera device to +and saves the captured frames as JPEG files. -import ( - ... - "github.com/vladimirvivien/go4vl/v4l2" -) +The example assumes the attached device supports JPEG (MJPEG) output format inherently. +```go func main() { + devName := "/dev/video0" + flag.StringVar(&devName, "d", devName, "device name (path)") + flag.Parse() + // open device - device, err := v4l2.Open("/dev/video0") + device, err := device.Open( + devName, + device.WithPixFormat(v4l2.PixFormat{PixelFormat: v4l2.PixelFmtMPEG, Width: 640, Height: 480}), + ) if err != nil { log.Fatalf("failed to open device: %s", err) } defer device.Close() - // configure device with preferred fmt - if err := device.SetPixFormat(v4l2.PixFormat{ - Width: 640, - Height: 480, - PixelFormat: v4l2.PixelFmtMJPEG, - Field: v4l2.FieldNone, - }); err != nil { - log.Fatalf("failed to set format: %s", err) - } - - // start a device stream with 3 video buffers - if err := device.StartStream(3); err != nil { + // start stream with cancellable context + ctx, stop := context.WithCancel(context.TODO()) + if err := device.Start(ctx); err != nil { log.Fatalf("failed to start stream: %s", err) } - ctx, cancel := context.WithCancel(context.TODO()) - // capture video data at 15 fps - frameChan, err := device.Capture(ctx, 15) - if err != nil { - log.Fatal(err) - } - - // grab 10 frames from frame channel and save them as files + // process frames from capture channel totalFrames := 10 count := 0 - for frame := range frameChan { + log.Printf("Capturing %d frames...", totalFrames) + + for frame := range device.GetOutput() { fileName := fmt.Sprintf("capture_%d.jpg", count) file, err := os.Create(fileName) if err != nil { @@ -105,6 +96,7 @@ func main() { log.Printf("failed to write file %s: %s", fileName, err) continue } + log.Printf("Saved file: %s", fileName) if err := file.Close(); err != nil { log.Printf("failed to close file %s: %s", fileName, err) } @@ -114,20 +106,18 @@ func main() { } } - cancel() // stop capture - if err := device.StopStream(); err != nil { - log.Fatal(err) - } + stop() // stop capture fmt.Println("Done.") } ``` -### Other examples -The [./examples](./examples) directory contains additional examples including: +> Read a detail walk-through about this example [here](./examples/capture0/README.md). -* [device_info](./examples/device_info) - queries and prints devince information -* [webcam](./examples/webcam) - uses the v4l2 package to create a simple webcam that streams images from an attached camera accessible via a web page. +### Other examples +The [./examples](./examples/README.md) directory contains additional examples including: +* [device_info](./examples/device_info/README.md) - queries and prints video device information +* [webcam](./examples/webcam/README.md) - uses the v4l2 package to create a simple webcam that streams images from an attached camera accessible via a web page. ## Roadmap -There is no defined roadmap. The main goal is to port as much functionlities as possible so that +The main goal is to port as many functionalities as possible so that adopters can use Go to create cool video-based tools on platforms such as the Raspberry Pi. \ No newline at end of file diff --git a/device/device.go b/device/device.go new file mode 100644 index 0000000..bf07dc6 --- /dev/null +++ b/device/device.go @@ -0,0 +1,412 @@ +package device + +import ( + "context" + "fmt" + "os" + "reflect" + sys "syscall" + + "github.com/vladimirvivien/go4vl/v4l2" +) + +type Device struct { + path string + file *os.File + fd uintptr + config config + bufType v4l2.BufType + cap v4l2.Capability + cropCap v4l2.CropCapability + buffers [][]byte + requestedBuf v4l2.RequestBuffers + streaming bool + output chan []byte +} + +// Open creates opens the underlying device at specified path for streaming. +// It returns a *Device or an error if unable to open device. +func Open(path string, options ...Option) (*Device, error) { + dev := &Device{path: path, config: config{}} + // apply options + if len(options) > 0 { + for _, o := range options { + o(&dev.config) + } + } + + file, err := os.OpenFile(path, sys.O_RDWR|sys.O_NONBLOCK, 0644) + //file, err := os.OpenFile(path, sys.O_RDWR, 0644) + if err != nil { + return nil, fmt.Errorf("device open: %w", err) + } + dev.file = file + dev.fd = file.Fd() + + // get capability + cap, err := v4l2.GetCapability(file.Fd()) + if err != nil { + if err := file.Close(); err != nil { + return nil, fmt.Errorf("device %s: closing after failure: %s", path, err) + } + return nil, fmt.Errorf("device open: %s: %w", path, err) + } + dev.cap = cap + + // set preferred device buffer size + if reflect.ValueOf(dev.config.bufSize).IsZero() { + dev.config.bufSize = 2 + } + + // only supports streaming IO model right now + if !dev.cap.IsStreamingSupported() { + return nil, fmt.Errorf("device open: only streaming IO is supported") + } + + switch { + case cap.IsVideoCaptureSupported(): + // setup capture parameters and chan for captured data + dev.bufType = v4l2.BufTypeVideoCapture + dev.output = make(chan []byte, dev.config.bufSize) + case cap.IsVideoOutputSupported(): + dev.bufType = v4l2.BufTypeVideoOutput + default: + if err := file.Close(); err != nil { + return nil, fmt.Errorf("device open: %s: closing after failure: %s", path, err) + } + return nil, fmt.Errorf("device open: %s: %w", path, v4l2.ErrorUnsupportedFeature) + } + + if !reflect.ValueOf(dev.config.bufType).IsZero() && dev.config.bufType != dev.bufType { + return nil, fmt.Errorf("device open: does not support buffer stream type") + } + + // ensures IOType is set + if reflect.ValueOf(dev.config.ioType).IsZero() { + dev.config.ioType = v4l2.IOTypeMMAP + } + + // set pix format + if !reflect.ValueOf(dev.config.pixFormat).IsZero() { + if err := dev.SetPixFormat(dev.config.pixFormat); err != nil { + fmt.Errorf("device open: %s: set format: %w", path, err) + } + } else { + if dev.config.pixFormat, err = v4l2.GetPixFormat(dev.fd); err != nil { + fmt.Errorf("device open: %s: get pix format: %w", path, err) + } + } + + // set fps + if !reflect.ValueOf(dev.config.fps).IsZero() { + if err := dev.SetFrameRate(dev.config.fps); err != nil { + fmt.Errorf("device open: %s: set fps: %w", path, err) + } + } else { + if dev.config.fps, err = dev.GetFrameRate(); err != nil { + fmt.Errorf("device open: %s: get fps: %w", path, err) + } + } + + return dev, nil +} + +// Close closes the underlying device associated with `d` . +func (d *Device) Close() error { + if d.streaming { + if err := d.Stop(); err != nil { + return err + } + } + + return d.file.Close() +} + +// Name returns the device name (or path) +func (d *Device) Name() string { + return d.path +} + +// Fd returns the file descriptor value for the device +func (d *Device) Fd() uintptr { + return d.fd +} + +// Buffers returns the internal mapped buffers. This method should be +// called after streaming has been started otherwise it may return nil. +func (d *Device) Buffers() [][]byte { + return d.buffers +} + +// Capability returns device capability info. +func (d *Device) Capability() v4l2.Capability { + return d.cap +} + +// BufferType this is a convenience method that returns the device mode (i.e. Capture, Output, etc) +// Use method Capability for detail about the device. +func (d *Device) BufferType() v4l2.BufType { + return d.bufType +} + +// BufferCount returns configured number of buffers to be used during streaming. +// If called after streaming start, this value could be updated by the driver. +func (d *Device) BufferCount() v4l2.BufType { + return d.config.bufSize +} + +// MemIOType returns the device memory input/output type (i.e. Memory mapped, DMA, user pointer, etc) +func (d *Device) MemIOType() v4l2.IOType { + return d.config.ioType +} + +// GetOutput returns the channel that outputs streamed data that is +// captured from the underlying device driver. +func (d *Device) GetOutput() <-chan []byte { + return d.output +} + +// SetInput sets up an input channel for data this sent for output to the +// underlying device driver. +func (d *Device) SetInput(in <-chan []byte) { + +} + +// GetCropCapability returns cropping info for device +func (d *Device) GetCropCapability() (v4l2.CropCapability, error) { + if !d.cap.IsVideoCaptureSupported() { + return v4l2.CropCapability{}, v4l2.ErrorUnsupportedFeature + } + return d.cropCap, nil +} + +// SetCropRect crops the video dimension for the device +func (d *Device) SetCropRect(r v4l2.Rect) error { + if !d.cap.IsVideoCaptureSupported() { + return v4l2.ErrorUnsupportedFeature + } + if err := v4l2.SetCropRect(d.fd, r); err != nil { + return fmt.Errorf("device: %w", err) + } + return nil +} + +// GetPixFormat retrieves pixel format info for device +func (d *Device) GetPixFormat() (v4l2.PixFormat, error) { + if !d.cap.IsVideoCaptureSupported() { + return v4l2.PixFormat{}, v4l2.ErrorUnsupportedFeature + } + + if reflect.ValueOf(d.config.pixFormat).IsZero() { + pixFmt, err := v4l2.GetPixFormat(d.fd) + if err != nil { + return v4l2.PixFormat{}, fmt.Errorf("device: %w", err) + } + d.config.pixFormat = pixFmt + } + + return d.config.pixFormat, nil +} + +// SetPixFormat sets the pixel format for the associated device. +func (d *Device) SetPixFormat(pixFmt v4l2.PixFormat) error { + if !d.cap.IsVideoCaptureSupported() { + return v4l2.ErrorUnsupportedFeature + } + + if err := v4l2.SetPixFormat(d.fd, pixFmt); err != nil { + return fmt.Errorf("device: %w", err) + } + d.config.pixFormat = pixFmt + return nil +} + +// GetFormatDescription returns a format description for the device at specified format index +func (d *Device) GetFormatDescription(idx uint32) (v4l2.FormatDescription, error) { + if !d.cap.IsVideoCaptureSupported() { + return v4l2.FormatDescription{}, v4l2.ErrorUnsupportedFeature + } + + return v4l2.GetFormatDescription(d.fd, idx) +} + +// GetFormatDescriptions returns all possible format descriptions for device +func (d *Device) GetFormatDescriptions() ([]v4l2.FormatDescription, error) { + if !d.cap.IsVideoCaptureSupported() { + return nil, v4l2.ErrorUnsupportedFeature + } + + return v4l2.GetAllFormatDescriptions(d.fd) +} + +// GetVideoInputIndex returns current video input index for device +func (d *Device) GetVideoInputIndex() (int32, error) { + if !d.cap.IsVideoCaptureSupported() { + return 0, v4l2.ErrorUnsupportedFeature + } + + return v4l2.GetCurrentVideoInputIndex(d.fd) +} + +// GetVideoInputInfo returns video input info for device +func (d *Device) GetVideoInputInfo(index uint32) (v4l2.InputInfo, error) { + if !d.cap.IsVideoCaptureSupported() { + return v4l2.InputInfo{}, v4l2.ErrorUnsupportedFeature + } + + return v4l2.GetVideoInputInfo(d.fd, index) +} + +// GetStreamParam returns streaming parameter information for device +func (d *Device) GetStreamParam() (v4l2.StreamParam, error) { + if !d.cap.IsVideoCaptureSupported() && d.cap.IsVideoOutputSupported() { + return v4l2.StreamParam{}, v4l2.ErrorUnsupportedFeature + } + return v4l2.GetStreamParam(d.fd, d.bufType) +} + +// SetStreamParam saves stream parameters for device +func (d *Device) SetStreamParam(param v4l2.StreamParam) error { + if !d.cap.IsVideoCaptureSupported() && d.cap.IsVideoOutputSupported() { + return v4l2.ErrorUnsupportedFeature + } + return v4l2.SetStreamParam(d.fd, d.bufType, param) +} + +// SetFrameRate sets the FPS rate value of the device +func (d *Device) SetFrameRate(fps uint32) error { + if !d.cap.IsStreamingSupported() { + return fmt.Errorf("set frame rate: %w", v4l2.ErrorUnsupportedFeature) + } + + var param v4l2.StreamParam + switch { + case d.cap.IsVideoCaptureSupported(): + param.Capture = v4l2.CaptureParam{TimePerFrame: v4l2.Fract{Numerator: 1, Denominator: fps}} + case d.cap.IsVideoOutputSupported(): + param.Output = v4l2.OutputParam{TimePerFrame: v4l2.Fract{Numerator: 1, Denominator: fps}} + default: + return v4l2.ErrorUnsupportedFeature + } + if err := d.SetStreamParam(param); err != nil { + return fmt.Errorf("device: set fps: %w", err) + } + d.config.fps = fps + return nil +} + +// GetFrameRate returns the FPS value for the device +func (d *Device) GetFrameRate() (uint32, error) { + if reflect.ValueOf(d.config.fps).IsZero() { + param, err := d.GetStreamParam() + if err != nil { + return 0, fmt.Errorf("device: frame rate: %w", err) + } + switch { + case d.cap.IsVideoCaptureSupported(): + d.config.fps = param.Capture.TimePerFrame.Denominator + case d.cap.IsVideoOutputSupported(): + d.config.fps = param.Output.TimePerFrame.Denominator + default: + return 0, v4l2.ErrorUnsupportedFeature + } + } + + return d.config.fps, nil +} + +// GetMediaInfo returns info for a device that supports the Media API +func (d *Device) GetMediaInfo() (v4l2.MediaDeviceInfo, error) { + return v4l2.GetMediaDeviceInfo(d.fd) +} + +func (d *Device) Start(ctx context.Context) error { + if ctx.Err() != nil { + return ctx.Err() + } + + if !d.cap.IsStreamingSupported() { + return fmt.Errorf("device: start stream: %s", v4l2.ErrorUnsupportedFeature) + } + + if d.streaming { + return fmt.Errorf("device: stream already started") + } + + // allocate device buffers + bufReq, err := v4l2.InitBuffers(d) + if err != nil { + return fmt.Errorf("device: init buffers: %w", err) + } + d.config.bufSize = bufReq.Count // update with granted buf size + d.requestedBuf = bufReq + + // for each allocated device buf, map into local space + if d.buffers, err = v4l2.MapMemoryBuffers(d); err != nil { + return fmt.Errorf("device: make mapped buffers: %s", err) + } + + // Initial enqueue of buffers for capture + for i := 0; i < int(d.config.bufSize); i++ { + _, err := v4l2.QueueBuffer(d.fd, d.config.ioType, d.bufType, uint32(i)) + if err != nil { + return fmt.Errorf("device: initial buffer queueing: %w", err) + } + } + + if err := d.startStreamLoop(ctx); err != nil { + return fmt.Errorf("device: start stream loop: %s", err) + } + + d.streaming = true + + return nil +} + +func (d *Device) Stop() error { + if !d.streaming { + return nil + } + if err := v4l2.UnmapMemoryBuffers(d); err != nil { + return fmt.Errorf("device: stop: %w", err) + } + if err := v4l2.StreamOff(d); err != nil { + return fmt.Errorf("device: stop: %w", err) + } + d.streaming = false + return nil +} + +func (d *Device) startStreamLoop(ctx context.Context) error { + if err := v4l2.StreamOn(d); err != nil { + return fmt.Errorf("stream loop: stream on: %w", err) + } + + go func() { + defer close(d.output) + + fd := d.Fd() + ioMemType := d.MemIOType() + bufType := d.BufferType() + + for { + select { + // handle stream capture (read from driver) + case <-v4l2.WaitForRead(d): + //TODO add better error-handling, for now just panic + buff, err := v4l2.CaptureBuffer(fd, ioMemType, bufType) + if err != nil { + panic(fmt.Errorf("stream loop: capture buffer: %s", err).Error()) + } + + d.output <- d.Buffers()[buff.Index][:buff.BytesUsed] + + case <-ctx.Done(): + d.Stop() + return + } + } + }() + + return nil +} diff --git a/device/device_config.go b/device/device_config.go new file mode 100644 index 0000000..2f05bb1 --- /dev/null +++ b/device/device_config.go @@ -0,0 +1,51 @@ +package device + +import ( + "github.com/vladimirvivien/go4vl/v4l2" +) + +type config struct { + ioType v4l2.IOType + pixFormat v4l2.PixFormat + bufSize uint32 + fps uint32 + bufType uint32 +} + +type Option func(*config) + +func WithIOType(ioType v4l2.IOType) Option { + return func(o *config) { + o.ioType = ioType + } +} + +func WithPixFormat(pixFmt v4l2.PixFormat) Option { + return func(o *config) { + o.pixFormat = pixFmt + } +} + +func WithBufferSize(size uint32) Option { + return func(o *config) { + o.bufSize = size + } +} + +func WithFPS(fps uint32) Option { + return func(o *config) { + o.fps = fps + } +} + +func WithVideoCaptureEnabled() Option { + return func(o *config) { + o.bufType = v4l2.BufTypeVideoCapture + } +} + +func WithVideoOutputEnabled() Option { + return func(o *config) { + o.bufType = v4l2.BufTypeVideoOutput + } +} diff --git a/device/doc.go b/device/doc.go new file mode 100644 index 0000000..eb92125 --- /dev/null +++ b/device/doc.go @@ -0,0 +1,2 @@ +// Package device provides a device abstraction that supports video streaming. +package device diff --git a/v4l2/device/list.go b/device/list.go similarity index 90% rename from v4l2/device/list.go rename to device/list.go index 0c2a6a5..97824f2 100644 --- a/v4l2/device/list.go +++ b/device/list.go @@ -10,7 +10,7 @@ var ( root = "/dev" ) -// devPattern is device directory name pattern on Linux +// devPattern is device directory name pattern on Linux (i.e. video0, video10, vbi0, etc) var devPattern = regexp.MustCompile(fmt.Sprintf(`%s/(video|radio|vbi|swradio|v4l-subdev|v4l-touch|media)[0-9]+`, root)) // IsDevice tests whether the path matches a V4L device name and is a device file @@ -52,5 +52,5 @@ func GetAllDevicePaths() ([]string, error) { result = append(result, dev) } } - return result, nil -} \ No newline at end of file + return result, nil +} diff --git a/v4l2/device/list_test.go b/device/list_test.go similarity index 82% rename from v4l2/device/list_test.go rename to device/list_test.go index 5a6035d..5f480bd 100644 --- a/v4l2/device/list_test.go +++ b/device/list_test.go @@ -4,7 +4,7 @@ import ( "testing" ) -func TestList(t *testing.T){ +func TestList(t *testing.T) { devices, err := GetAllDevicePaths() if err != nil { t.Error(err) diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..5242406 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,6 @@ +# Examples + +* [capture0](./capture0) - simple capture example with hardcoded pixel format +* [capture1](./capture1) - capture example with preferred format search +* [device_info](./device_info) - uses go4vl to retrieve device and format info +* [webcam](./webcam) - use go4vl to build a working webcam example \ No newline at end of file diff --git a/examples/capture0/README.md b/examples/capture0/README.md new file mode 100644 index 0000000..ef19369 --- /dev/null +++ b/examples/capture0/README.md @@ -0,0 +1,58 @@ +# Capture example + +This example shows how to use the `go4vl` API to create a simple program that captures a video frames from an attached input (camera) device. + +Firstly, the source code opens a device, `devName`, with a hard-coded pixel format (MPEG) and size. If the device does not support +the specified format, the open operation will fail, returning an error. + +```go +func main() { + devName := "/dev/video0" + // open device + device, err := device.Open( + devName, + device.WithPixFormat(v4l2.PixFormat{PixelFormat: v4l2.PixelFmtMPEG, Width: 640, Height: 480}), + ) +... +} +``` + +Next, the source code calls the `device.Start` method to start the input (capture) process. + +```go +func main() { +... + // start stream + ctx, stop := context.WithCancel(context.TODO()) + if err := device.Start(ctx); err != nil { + log.Fatalf("failed to start stream: %s", err) + } +... + +} +``` + +Once the device starts, the code sets up a loop capture incoming video frame buffer from the input device and save each +frame to a local file. + +```go +func main() { +... + for frame := range device.GetOutput() { + fileName := fmt.Sprintf("capture_%d.jpg", count) + file, err := os.Create(fileName) + ... + if _, err := file.Write(frame); err != nil { + log.Printf("failed to write file %s: %s", fileName, err) + continue + } + + if err := file.Close(); err != nil { + log.Printf("failed to close file %s: %s", fileName, err) + } + } + +} +``` + +> See the full source code [here](./capture0.go). \ No newline at end of file diff --git a/examples/capture0/capture0.go b/examples/capture0/capture0.go new file mode 100644 index 0000000..89ab9e4 --- /dev/null +++ b/examples/capture0/capture0.go @@ -0,0 +1,64 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log" + "os" + + "github.com/vladimirvivien/go4vl/device" + "github.com/vladimirvivien/go4vl/v4l2" +) + +func main() { + devName := "/dev/video0" + flag.StringVar(&devName, "d", devName, "device name (path)") + flag.Parse() + + // open device + device, err := device.Open( + devName, + device.WithPixFormat(v4l2.PixFormat{PixelFormat: v4l2.PixelFmtMPEG, Width: 640, Height: 480}), + ) + if err != nil { + log.Fatalf("failed to open device: %s", err) + } + defer device.Close() + + // start stream + ctx, stop := context.WithCancel(context.TODO()) + if err := device.Start(ctx); err != nil { + log.Fatalf("failed to start stream: %s", err) + } + + // process frames from capture channel + totalFrames := 10 + count := 0 + log.Printf("Capturing %d frames...", totalFrames) + + for frame := range device.GetOutput() { + fileName := fmt.Sprintf("capture_%d.jpg", count) + file, err := os.Create(fileName) + if err != nil { + log.Printf("failed to create file %s: %s", fileName, err) + continue + } + if _, err := file.Write(frame); err != nil { + log.Printf("failed to write file %s: %s", fileName, err) + continue + } + log.Printf("Saved file: %s", fileName) + if err := file.Close(); err != nil { + log.Printf("failed to close file %s: %s", fileName, err) + } + count++ + if count >= totalFrames { + break + } + } + + stop() // stop capture + fmt.Println("Done.") + +} diff --git a/examples/capture1/README.md b/examples/capture1/README.md new file mode 100644 index 0000000..55e39f0 --- /dev/null +++ b/examples/capture1/README.md @@ -0,0 +1,119 @@ +# Capture example + +In this capture example, the source code uses a more advanced approach (compared to [capture0/capture0.go](../capture0/capture0.go)) where it +leverages the go4vl device description API to ensure that the device supports the selected preferred format and size. + +First, the source code opens the device with `device.Open` function call. Unlike in the [previous example](../capture0/capture0.go), the call to +`Open` omits the pixel format option. + +```go +func main() { + devName := "/dev/video0" + device, err := device.Open(devName) + if err != nil { + log.Fatalf("failed to open device: %s", err) + } + defer device.Close() +} +``` + +Next, the source code defines a function that is used to search formats supported by the device. + +```go +func main() { +... + findPreferredFmt := func(fmts []v4l2.FormatDescription, pixEncoding v4l2.FourCCType) *v4l2.FormatDescription { + for _, desc := range fmts { + if desc.PixelFormat == pixEncoding{ + return &desc + } + } + return nil + } +} +``` + +Next, the code enumerates the formats supported by the device, `device.GetFormatDescriptions`, and used the search function +to test whether the device support one of several preferred formats. + +```go +func main() { +... + fmtDescs, err := device.GetFormatDescriptions() + if err != nil{ + log.Fatal("failed to get format desc:", err) + } + + // search for preferred formats + preferredFmts := []v4l2.FourCCType{v4l2.PixelFmtMPEG, v4l2.PixelFmtMJPEG, v4l2.PixelFmtJPEG, v4l2.PixelFmtYUYV} + var fmtDesc *v4l2.FormatDescription + for _, preferredFmt := range preferredFmts{ + fmtDesc = findPreferredFmt(fmtDescs, preferredFmt) + if fmtDesc != nil { + break + } + } +} +``` + +Next, if one of the preferred formats is found, then it is assigned to `fmtDesc`. The next step is to search the device +for an appropriate supported dimension (640x480) for the selected format which is stored in `frmSize`. + +```go +func main() { +... + frameSizes, err := v4l2.GetFormatFrameSizes(device.Fd(), fmtDesc.PixelFormat) + + // select size 640x480 for format + var frmSize v4l2.FrameSizeEnum + for _, size := range frameSizes { + if size.Size.MinWidth == 640 && size.Size.MinHeight == 480 { + frmSize = size + break + } + } +} +``` + +At this point, the device can be assigned the selected pixel format and its associated size. + +```go +func main() { +... + if err := device.SetPixFormat(v4l2.PixFormat{ + Width: frmSize.Size.MinWidth, + Height: frmSize.Size.MinHeight, + PixelFormat: fmtDesc.PixelFormat, + Field: v4l2.FieldNone, + }); err != nil { + log.Fatalf("failed to set format: %s", err) + } +} +``` + +Finally, the device can be started and the streaming buffers can be captured: + +```go +fun main() { +... + if err := device.Start(ctx); err != nil { + log.Fatalf("failed to stream: %s", err) + } + + for frame := range device.GetOutput() { + fileName := fmt.Sprintf("capture_%d.jpg", count) + file, err := os.Create(fileName) + if err != nil { + log.Printf("failed to create file %s: %s", fileName, err) + continue + } + if _, err := file.Write(frame); err != nil { + log.Printf("failed to write file %s: %s", fileName, err) + continue + } + ... + } +} +``` + +> See source code [here](./capture1.go). \ No newline at end of file diff --git a/examples/capture/capture.go b/examples/capture1/capture1.go similarity index 77% rename from examples/capture/capture.go rename to examples/capture1/capture1.go index 5579eb1..b290fb6 100644 --- a/examples/capture/capture.go +++ b/examples/capture1/capture1.go @@ -7,8 +7,8 @@ import ( "log" "os" + "github.com/vladimirvivien/go4vl/device" "github.com/vladimirvivien/go4vl/v4l2" - "github.com/vladimirvivien/go4vl/v4l2/device" ) func main() { @@ -23,10 +23,15 @@ func main() { } defer device.Close() + fps, err := device.GetFrameRate() + if err != nil { + log.Fatalf("failed to get framerate: %s", err) + } + // helper function to search for format descriptions findPreferredFmt := func(fmts []v4l2.FormatDescription, pixEncoding v4l2.FourCCType) *v4l2.FormatDescription { for _, desc := range fmts { - if desc.PixelFormat == pixEncoding{ + if desc.PixelFormat == pixEncoding { return &desc } } @@ -35,14 +40,14 @@ func main() { // get supported format descriptions fmtDescs, err := device.GetFormatDescriptions() - if err != nil{ + if err != nil { log.Fatal("failed to get format desc:", err) } // search for preferred formats preferredFmts := []v4l2.FourCCType{v4l2.PixelFmtMPEG, v4l2.PixelFmtMJPEG, v4l2.PixelFmtJPEG, v4l2.PixelFmtYUYV} var fmtDesc *v4l2.FormatDescription - for _, preferredFmt := range preferredFmts{ + for _, preferredFmt := range preferredFmts { fmtDesc = findPreferredFmt(fmtDescs, preferredFmt) if fmtDesc != nil { break @@ -54,29 +59,29 @@ func main() { log.Fatalf("device does not support any of %#v", preferredFmts) } log.Printf("Found preferred fmt: %s", fmtDesc) - frameSizes, err := v4l2.GetFormatFrameSizes(device.GetFileDescriptor(), fmtDesc.PixelFormat) - if err!=nil{ + frameSizes, err := v4l2.GetFormatFrameSizes(device.Fd(), fmtDesc.PixelFormat) + if err != nil { log.Fatalf("failed to get framesize info: %s", err) } // select size 640x480 for format - var frmSize v4l2.FrameSize + var frmSize v4l2.FrameSizeEnum for _, size := range frameSizes { - if size.Width == 640 && size.Height == 480 { + if size.Size.MinWidth == 640 && size.Size.MinHeight == 480 { frmSize = size break } } - if frmSize.Width == 0 { + if frmSize.Size.MinWidth == 0 { log.Fatalf("Size 640x480 not supported for fmt: %s", fmtDesc) } // configure device with preferred fmt if err := device.SetPixFormat(v4l2.PixFormat{ - Width: frmSize.Width, - Height: frmSize.Height, + Width: frmSize.Size.MinWidth, + Height: frmSize.Size.MinHeight, PixelFormat: fmtDesc.PixelFormat, Field: v4l2.FieldNone, }); err != nil { @@ -90,22 +95,16 @@ func main() { log.Printf("Pixel format set to [%s]", pixFmt) // start stream - log.Println("Start capturing...") - if err := device.StartStream(3); err != nil { - log.Fatalf("failed to start stream: %s", err) - } - ctx, cancel := context.WithCancel(context.TODO()) - frameChan, err := device.Capture(ctx, 15) - if err != nil { - log.Fatal(err) + if err := device.Start(ctx); err != nil { + log.Fatalf("failed to stream: %s", err) } // process frames from capture channel totalFrames := 10 count := 0 - log.Println("Streaming frames from device...") - for frame := range frameChan { + log.Printf("Capturing %d frames at %d fps...", totalFrames, fps) + for frame := range device.GetOutput() { fileName := fmt.Sprintf("capture_%d.jpg", count) file, err := os.Create(fileName) if err != nil { @@ -127,7 +126,7 @@ func main() { } cancel() // stop capture - if err := device.StopStream(); err != nil { + if err := device.Stop(); err != nil { fmt.Println(err) os.Exit(1) } diff --git a/examples/cgo_types/cgo_capture.go b/examples/cgo_types/cgo_capture.go index 63be537..819b20d 100644 --- a/examples/cgo_types/cgo_capture.go +++ b/examples/cgo_types/cgo_capture.go @@ -78,7 +78,7 @@ func setFormat(fd uintptr, pixFmt PixFormat) error { return nil } -func getFormat(fd uintptr) (PixFormat, error){ +func getFormat(fd uintptr) (PixFormat, error) { var v4l2Fmt C.struct_v4l2_format v4l2Fmt._type = C.uint(BufTypeVideoCapture) @@ -88,7 +88,7 @@ func getFormat(fd uintptr) (PixFormat, error){ } var pixFmt PixFormat - *(*C.struct_v4l2_pix_format)(unsafe.Pointer(&pixFmt))= *(*C.struct_v4l2_pix_format)(unsafe.Pointer(&v4l2Fmt.fmt[0])) + *(*C.struct_v4l2_pix_format)(unsafe.Pointer(&pixFmt)) = *(*C.struct_v4l2_pix_format)(unsafe.Pointer(&v4l2Fmt.fmt[0])) return pixFmt, nil @@ -99,7 +99,7 @@ func getFormat(fd uintptr) (PixFormat, error){ // Memory buffer types // https://elixir.bootlin.com/linux/v5.13-rc6/source/include/uapi/linux/videodev2.h#L188 const ( - StreamMemoryTypeMMAP uint32 = C.V4L2_MEMORY_MMAP + StreamMemoryTypeMMAP uint32 = C.V4L2_MEMORY_MMAP ) // reqBuffers requests that the device allocates a `count` diff --git a/examples/device_info/README.md b/examples/device_info/README.md new file mode 100644 index 0000000..cc488a8 --- /dev/null +++ b/examples/device_info/README.md @@ -0,0 +1,40 @@ +# Device info example + +The example in this directory showcases `go4vl` support for device information. For instance, the following function +prints driver information + +```go +func main() { + devName := '/dev/video0' + device, err := device2.Open(devName) + if err := printDeviceDriverInfo(device); err != nil { + log.Fatal(err) + } +} + +func printDeviceDriverInfo(dev *device.Device) error { + caps := dev.Capability() + + // print driver info + fmt.Println("v4l2Device Info:") + fmt.Printf(template, "Driver name", caps.Driver) + fmt.Printf(template, "Card name", caps.Card) + fmt.Printf(template, "Bus info", caps.BusInfo) + + fmt.Printf(template, "Driver version", caps.GetVersionInfo()) + + fmt.Printf("\t%-16s : %0x\n", "Driver capabilities", caps.Capabilities) + for _, desc := range caps.GetDriverCapDescriptions() { + fmt.Printf("\t\t%s\n", desc.Desc) + } + + fmt.Printf("\t%-16s : %0x\n", "v4l2Device capabilities", caps.Capabilities) + for _, desc := range caps.GetDeviceCapDescriptions() { + fmt.Printf("\t\t%s\n", desc.Desc) + } + + return nil +} +``` + +> See the [complete example](./devinfo.go) and all available device information from go4vl. \ No newline at end of file diff --git a/examples/device_info/devinfo.go b/examples/device_info/devinfo.go index cde9f23..0d0cd5f 100644 --- a/examples/device_info/devinfo.go +++ b/examples/device_info/devinfo.go @@ -7,8 +7,8 @@ import ( "os" "strings" + device2 "github.com/vladimirvivien/go4vl/device" "github.com/vladimirvivien/go4vl/v4l2" - "github.com/vladimirvivien/go4vl/v4l2/device" ) var template = "\t%-24s : %s\n" @@ -21,13 +21,13 @@ func main() { flag.Parse() if devList { - if err := listDevices(); err != nil{ + if err := listDevices(); err != nil { log.Fatal(err) } os.Exit(0) } - device, err := device.Open(devName) + device, err := device2.Open(devName) if err != nil { log.Fatal(err) } @@ -49,40 +49,48 @@ func main() { log.Fatal(err) } - if err := printCaptureParam(device); err != nil { - log.Fatal(err) + if device.Capability().IsVideoCaptureSupported() { + if err := printCaptureParam(device); err != nil { + log.Fatal(err) + } + } + + if device.Capability().IsVideoOutputSupported() { + if err := printOutputParam(device); err != nil { + log.Fatal(err) + } } + } func listDevices() error { - paths, err := device.GetAllDevicePaths() + paths, err := device2.GetAllDevicePaths() if err != nil { return err } for _, path := range paths { - dev, err := device.Open(path) + dev, err := device2.Open(path) if err != nil { log.Print(err) continue } var busInfo, card string - cap, err := dev.GetCapability() - if err != nil { - // is a media device? - if mdi, err := dev.GetMediaInfo(); err == nil { - if mdi.BusInfo != "" { - busInfo = mdi.BusInfo - }else{ - busInfo = "platform: " + mdi.Driver - } - if mdi.Model != "" { - card = mdi.Model - }else{ - card = mdi.Driver - } + cap := dev.Capability() + + // is a media device? + if mdi, err := dev.GetMediaInfo(); err == nil { + if mdi.BusInfo != "" { + busInfo = mdi.BusInfo + } else { + busInfo = "platform: " + mdi.Driver + } + if mdi.Model != "" { + card = mdi.Model + } else { + card = mdi.Driver } - }else{ + } else { busInfo = cap.BusInfo card = cap.Card } @@ -93,21 +101,17 @@ func listDevices() error { continue } - fmt.Printf("Device [%s]: %s: %s\n", path, card, busInfo) - + fmt.Printf("v4l2Device [%s]: %s: %s\n", path, card, busInfo) } return nil } -func printDeviceDriverInfo(dev *device.Device) error { - caps, err := dev.GetCapability() - if err != nil { - return fmt.Errorf("driver info: %w", err) - } +func printDeviceDriverInfo(dev *device2.Device) error { + caps := dev.Capability() // print driver info - fmt.Println("Device Info:") + fmt.Println("v4l2Device Info:") fmt.Printf(template, "Driver name", caps.Driver) fmt.Printf(template, "Card name", caps.Card) fmt.Printf(template, "Bus info", caps.BusInfo) @@ -119,7 +123,7 @@ func printDeviceDriverInfo(dev *device.Device) error { fmt.Printf("\t\t%s\n", desc.Desc) } - fmt.Printf("\t%-16s : %0x\n", "Device capabilities", caps.Capabilities) + fmt.Printf("\t%-16s : %0x\n", "v4l2Device capabilities", caps.Capabilities) for _, desc := range caps.GetDeviceCapDescriptions() { fmt.Printf("\t\t%s\n", desc.Desc) } @@ -127,7 +131,7 @@ func printDeviceDriverInfo(dev *device.Device) error { return nil } -func printVideoInputInfo(dev *device.Device) error { +func printVideoInputInfo(dev *device2.Device) error { // first get current input index, err := dev.GetVideoInputIndex() if err != nil { @@ -148,7 +152,7 @@ func printVideoInputInfo(dev *device.Device) error { return nil } -func printFormatInfo(dev *device.Device) error { +func printFormatInfo(dev *device2.Device) error { pixFmt, err := dev.GetPixFormat() if err != nil { return fmt.Errorf("video capture format: %w", err) @@ -190,28 +194,28 @@ func printFormatInfo(dev *device.Device) error { return printFormatDesc(dev) } -func printFormatDesc(dev *device.Device) error { +func printFormatDesc(dev *device2.Device) error { descs, err := dev.GetFormatDescriptions() if err != nil { return fmt.Errorf("format desc: %w", err) } fmt.Println("Supported formats:") - for i, desc := range descs{ - frmSizes, err := v4l2.GetFormatFrameSizes(dev.GetFileDescriptor(), desc.PixelFormat) + for i, desc := range descs { + frmSizes, err := v4l2.GetFormatFrameSizes(dev.Fd(), desc.PixelFormat) if err != nil { return fmt.Errorf("format desc: %w", err) } var sizeStr strings.Builder sizeStr.WriteString("Sizes: ") - for _, size := range frmSizes{ - sizeStr.WriteString(fmt.Sprintf("[%dx%d] ", size.Width, size.Height)) + for _, size := range frmSizes { + sizeStr.WriteString(fmt.Sprintf("[%dx%d] ", size.Size.MinWidth, size.Size.MinHeight)) } fmt.Printf(template, fmt.Sprintf("[%0d] %s", i, desc.Description), sizeStr.String()) } return nil } -func printCropInfo(dev *device.Device) error { +func printCropInfo(dev *device2.Device) error { crop, err := dev.GetCropCapability() if err != nil { return fmt.Errorf("crop capability: %w", err) @@ -238,26 +242,50 @@ func printCropInfo(dev *device.Device) error { return nil } -func printCaptureParam(dev *device.Device) error { - params, err := dev.GetCaptureParam() +func printCaptureParam(dev *device2.Device) error { + params, err := dev.GetStreamParam() if err != nil { - return fmt.Errorf("streaming capture param: %w", err) + return fmt.Errorf("stream capture param: %w", err) } - fmt.Println("Streaming parameters for video capture:") + fmt.Println("Stream capture parameters:") tpf := "not specified" - if params.Capability == v4l2.StreamParamTimePerFrame { + if params.Capture.Capability == v4l2.StreamParamTimePerFrame { tpf = "time per frame" } fmt.Printf(template, "Capability", tpf) hiqual := "not specified" - if params.CaptureMode == v4l2.StreamParamModeHighQuality { + if params.Capture.CaptureMode == v4l2.StreamParamModeHighQuality { hiqual = "high quality" } fmt.Printf(template, "Capture mode", hiqual) - fmt.Printf(template, "Frames per second", fmt.Sprintf("%d/%d", params.TimePerFrame.Denominator, params.TimePerFrame.Numerator)) - fmt.Printf(template, "Read buffers", fmt.Sprintf("%d",params.ReadBuffers)) + fmt.Printf(template, "Frames per second", fmt.Sprintf("%d/%d", params.Capture.TimePerFrame.Denominator, params.Capture.TimePerFrame.Numerator)) + fmt.Printf(template, "Read buffers", fmt.Sprintf("%d", params.Capture.ReadBuffers)) + return nil +} + +func printOutputParam(dev *device2.Device) error { + params, err := dev.GetStreamParam() + if err != nil { + return fmt.Errorf("stream output param: %w", err) + } + fmt.Println("Stream output parameters:") + + tpf := "not specified" + if params.Output.Capability == v4l2.StreamParamTimePerFrame { + tpf = "time per frame" + } + fmt.Printf(template, "Capability", tpf) + + hiqual := "not specified" + if params.Output.CaptureMode == v4l2.StreamParamModeHighQuality { + hiqual = "high quality" + } + fmt.Printf(template, "Output mode", hiqual) + + fmt.Printf(template, "Frames per second", fmt.Sprintf("%d/%d", params.Output.TimePerFrame.Denominator, params.Output.TimePerFrame.Numerator)) + fmt.Printf(template, "Write buffers", fmt.Sprintf("%d", params.Output.WriteBuffers)) return nil -} \ No newline at end of file +} diff --git a/examples/format/devfmt.go b/examples/format/devfmt.go index 47d41c9..f383a5b 100644 --- a/examples/format/devfmt.go +++ b/examples/format/devfmt.go @@ -5,8 +5,8 @@ import ( "log" "strings" + device2 "github.com/vladimirvivien/go4vl/device" "github.com/vladimirvivien/go4vl/v4l2" - "github.com/vladimirvivien/go4vl/v4l2/device" ) func main() { @@ -21,18 +21,6 @@ func main() { flag.StringVar(&format, "f", format, "pixel format") flag.Parse() - device, err := device.Open(devName) - if err != nil { - log.Fatalf("failed to open device: %s", err) - } - defer device.Close() - - currFmt, err := device.GetPixFormat() - if err != nil { - log.Fatalf("unable to get format: %s", err) - } - log.Printf("Current format: %s", currFmt) - fmtEnc := v4l2.PixelFmtYUYV switch strings.ToLower(format) { case "mjpeg": @@ -43,18 +31,37 @@ func main() { fmtEnc = v4l2.PixelFmtYUYV } - if err := device.SetPixFormat(v4l2.PixFormat{ - Width: uint32(width), - Height: uint32(height), - PixelFormat: fmtEnc, - Field: v4l2.FieldNone, - }); err != nil { - log.Fatalf("failed to set format: %s", err) + device, err := device2.Open( + devName, + device2.WithPixFormat(v4l2.PixFormat{Width: uint32(width), Height: uint32(height), PixelFormat: fmtEnc, Field: v4l2.FieldNone}), + device2.WithFPS(15), + ) + if err != nil { + log.Fatalf("failed to open device: %s", err) } + defer device.Close() - currFmt, err = device.GetPixFormat() + currFmt, err := device.GetPixFormat() if err != nil { log.Fatalf("unable to get format: %s", err) } - log.Printf("Updated format: %s", currFmt) -} \ No newline at end of file + log.Printf("Current format: %s", currFmt) + + // FPS + fps, err := device.GetFrameRate() + if err != nil { + log.Fatalf("failed to get fps: %s", err) + } + log.Printf("current frame rate: %d fps", fps) + // update fps + if fps < 30 { + if err := device.SetFrameRate(30); err != nil { + log.Fatalf("failed to set frame rate: %s", err) + } + } + fps, err = device.GetFrameRate() + if err != nil { + log.Fatalf("failed to get fps: %s", err) + } + log.Printf("updated frame rate: %d fps", fps) +} diff --git a/examples/webcam/README.md b/examples/webcam/README.md new file mode 100644 index 0000000..771edef --- /dev/null +++ b/examples/webcam/README.md @@ -0,0 +1,165 @@ +# Webcam example + +The webcam examples shows how the `go4vl` API can be used to create a webcam that streams incoming video frames from an attached camera to a web page. The code sets up a web server that returns a web page with an image element that continuously stream the captured video from the camera. + +## Running the example +Keep in mind that this code can only run on systems with the Linux operating system. +Before you can build and run the code, you must satisfy the following prerequisites. + +### Pre-requisites + +* Go compiler/tools +* Linux OS (32- or 64-bit) +* Kernel minimum v5.10.x or higher +* A locally configured C compiler (i.e. gcc) +* Header files for V4L2 (i.e. /usr/include/linux/videodev2.h) +* A video camera (with support for Video for Linux API) + +If you are running a system that has not been upgraded in a while, ensure to issue the following commands: + +``` +sudo apt update +sudo apt full-upgrade +``` + +This example has been tested using a Raspberry Pi 3 running 32-bit Linux, with kernel version 5.14, with cheap USB video camera attached. + +### Build and run + +From within this directory, build with the following command: + +``` +go build -o webcam webcam.go +``` + +Once built, you can start the webcam with the following command (and output as shown): + +``` + ./webcam + +2022/05/21 09:04:31 device [/dev/video0] opened +2022/05/21 09:04:31 device info: driver: uvcvideo; card: HDM Webcam USB: HDM Webcam USB; bus info: usb-3f980000.usb-1.5 +2022/05/21 09:04:31 Current format: Motion-JPEG [1920x1080]; field=any; bytes per line=0; size image=0; colorspace=Default; YCbCr=Default; Quant=Default; XferFunc=Default +2022/05/21 09:04:31 device capture started, frames available +2022/05/21 09:04:31 starting server on port :9090 +2022/05/21 09:04:31 use url path /webcam +``` + +Next, point your browser to your machine's address and shown port (i.e. `http://198.162.100.20:9090`). +You should see a webpage with the streaming video (see below.) + +![](./screenshot.png) + +The webcam program offers several CLI arguments that you can use to configure the webcam: + +``` +./webcam --help +Usage of ./webcam: + -d string + device name (path) (default "/dev/video0") + -f string + pixel format (default "mjpeg") + -h int + capture height (default 1080) + -p string + webcam service port (default ":9090") + -r int + frames per second (fps) (default 30) + -w int + capture width (default 1920) +``` + +## The source code +The following code walkthrough illustrates how simple it is to create programs that can stream video using the `go4vl` project. + +Firstly, the `main` function opens the video device with a set of specified configurations (from CLI flags): + +```go +var frames <-chan []byte + +func main() { + port := ":9090" + devName := "/dev/video0" + frameRate := 30 + + // create device + device, err := device.Open(devName, + device.WithIOType(v4l2.IOTypeMMAP), + device.WithPixFormat(v4l2.PixFormat{PixelFormat: getFormatType(format), Width: uint32(width), Height: uint32(height)}), + device.WithFPS(uint32(frameRate)), + ) +} +``` + +Next, start the device and make the device stream available via package variable `frames`: + +```go +var frames <-chan []byte + +func main() { +... + ctx, cancel := context.WithCancel(context.TODO()) + if err := device.Start(ctx); err != nil { + log.Fatalf("stream capture: %s", err) + } + defer func() { + cancel() + device.Close() + }() + + frames = device.GetOutput() + +} +``` + +The last major step is to start an HTTP server to serve the video buffers, as images, and the page for the webcam: + +```go +var frames <-chan []byte + +func main() { +... + + // setup http service + http.HandleFunc("/webcam", servePage) // returns an html page + http.HandleFunc("/stream", serveVideoStream) // returns video feed + if err := http.ListenAndServe(port, nil); err != nil { + log.Fatal(err) + } +} +``` + +The video captured from the camera is served at endpoint `/stream` (see source above) which is serviced by HTTP handler +function `serveVideoStream`. The function uses a content type of `multipart/x-mixed-replace`, with a separate boundary for +each image buffer, that is rendered on the browser as a video stream. + +```go +func serveVideoStream(w http.ResponseWriter, req *http.Request) { + // Start HTTP Response + const boundaryName = "Yt08gcU534c0p4Jqj0p0" + + // send multi-part header + w.Header().Set("Content-Type", fmt.Sprintf("multipart/x-mixed-replace; boundary=%s", boundaryName)) + w.WriteHeader(http.StatusOK) + + for frame := range frames { + // start boundary + io.WriteString(w, fmt.Sprintf("--%s\n", boundaryName)) + io.WriteString(w, "Content-Type: image/jpeg\n") + io.WriteString(w, fmt.Sprintf("Content-Length: %d\n\n", len(frame))) + + if _, err := w.Write(frame); err != nil { + log.Printf("failed to write mjpeg image: %s", err) + return + } + + // close boundary + if _, err := io.WriteString(w, "\n"); err != nil { + log.Printf("failed to write boundary: %s", err) + return + } + } +} +``` + +> See the full source code [here](./webcam.go) \ No newline at end of file diff --git a/examples/webcam/screenshot.png b/examples/webcam/screenshot.png new file mode 100644 index 0000000..8dab9e1 Binary files /dev/null and b/examples/webcam/screenshot.png differ diff --git a/examples/webcam/webcam.go b/examples/webcam/webcam.go index b7afb30..2e1d413 100644 --- a/examples/webcam/webcam.go +++ b/examples/webcam/webcam.go @@ -11,9 +11,8 @@ import ( "strings" "time" - "github.com/vladimirvivien/go4vl/imgsupport" + "github.com/vladimirvivien/go4vl/device" "github.com/vladimirvivien/go4vl/v4l2" - "github.com/vladimirvivien/go4vl/v4l2/device" ) var ( @@ -54,6 +53,11 @@ func serveVideoStream(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusOK) for frame := range frames { + if len(frame) == 0 { + log.Print("skipping empty frame") + continue + } + // start boundary io.WriteString(w, fmt.Sprintf("--%s\n", boundaryName)) io.WriteString(w, "Content-Type: image/jpeg\n") @@ -63,23 +67,16 @@ func serveVideoStream(w http.ResponseWriter, req *http.Request) { switch pixfmt { case v4l2.PixelFmtMJPEG: if _, err := w.Write(frame); err != nil { - log.Printf("failed to write image: %s", err) - return - } - case v4l2.PixelFmtYUYV: - data, err := imgsupport.Yuyv2Jpeg(640, 480, frame) - if err != nil { - log.Printf("failed to convert yuyv to jpeg: %s", err) - continue - } - if _, err := w.Write(data); err != nil { - log.Printf("failed to write image: %s", err) + log.Printf("failed to write mjpeg image: %s", err) return } + default: + log.Printf("selected pixel format is not supported") } + // close boundary if _, err := io.WriteString(w, "\n"); err != nil { - log.Printf("failed to write bounday: %s", err) + log.Printf("failed to write boundary: %s", err) return } } @@ -88,6 +85,7 @@ func serveVideoStream(w http.ResponseWriter, req *http.Request) { func main() { port := ":9090" devName := "/dev/video0" + frameRate := int(fps) defaultDev, err := device.Open(devName) skipDefault := false if err != nil { @@ -118,23 +116,26 @@ func main() { flag.IntVar(&height, "h", height, "capture height") flag.StringVar(&format, "f", format, "pixel format") flag.StringVar(&port, "p", port, "webcam service port") + flag.IntVar(&frameRate, "r", frameRate, "frames per second (fps)") flag.Parse() // close device used for default info if err := defaultDev.Close(); err != nil { - // default device failed to close + log.Fatalf("failed to close default device: %s", err) } // open device and setup device - device, err := device.Open(devName) + device, err := device.Open(devName, + device.WithIOType(v4l2.IOTypeMMAP), + device.WithPixFormat(v4l2.PixFormat{PixelFormat: getFormatType(format), Width: uint32(width), Height: uint32(height)}), + device.WithFPS(uint32(frameRate)), + ) + if err != nil { log.Fatalf("failed to open device: %s", err) } defer device.Close() - caps, err := device.GetCapability() - if err != nil { - log.Println("failed to get device capabilities:", err) - } + caps := device.Capability() log.Printf("device [%s] opened\n", devName) log.Printf("device info: %s", caps.String()) @@ -144,34 +145,22 @@ func main() { log.Fatalf("unable to get format: %s", err) } log.Printf("Current format: %s", currFmt) - if err := device.SetPixFormat(updateFormat(currFmt, format, width, height)); err != nil { - log.Fatalf("failed to set format: %s", err) - } - currFmt, err = device.GetPixFormat() - if err != nil { - log.Fatalf("unable to get format: %s", err) - } pixfmt = currFmt.PixelFormat - log.Printf("Updated format: %s", currFmt) - - // Setup and start stream capture - if err := device.StartStream(2); err != nil { - log.Fatalf("unable to start stream: %s", err) - } // start capture ctx, cancel := context.WithCancel(context.TODO()) - f, err := device.Capture(ctx, fps) - if err != nil { + if err := device.Start(ctx); err != nil { log.Fatalf("stream capture: %s", err) } defer func() { cancel() device.Close() }() - frames = f // make frames available. - log.Println("device capture started, frames available") + // video stream + frames = device.GetOutput() + + log.Println("device capture started, frames available") log.Printf("starting server on port %s", port) log.Println("use url path /webcam") @@ -184,18 +173,14 @@ func main() { } } -func updateFormat(pix v4l2.PixFormat, fmtStr string, w, h int) v4l2.PixFormat { - pix.Width = uint32(w) - pix.Height = uint32(h) - +func getFormatType(fmtStr string) v4l2.FourCCType { switch strings.ToLower(fmtStr) { case "mjpeg", "jpeg": - pix.PixelFormat = v4l2.PixelFmtMJPEG + return v4l2.PixelFmtMJPEG case "h264", "h.264": - pix.PixelFormat = v4l2.PixelFmtH264 + return v4l2.PixelFmtH264 case "yuyv": - pix.PixelFormat = v4l2.PixelFmtYUYV + return v4l2.PixelFmtYUYV } - - return pix -} \ No newline at end of file + return v4l2.PixelFmtMPEG +} diff --git a/imgsupport/converters.go b/imgsupport/converters.go index 26f2834..c90a28c 100644 --- a/imgsupport/converters.go +++ b/imgsupport/converters.go @@ -2,6 +2,7 @@ package imgsupport import ( "bytes" + "fmt" "image" "image/jpeg" ) @@ -9,6 +10,9 @@ import ( // Yuyv2Jpeg attempts to convert the YUYV image using Go's built-in // YCbCr encoder func Yuyv2Jpeg(width, height int, frame []byte) ([]byte, error) { + if true { + return nil, fmt.Errorf("unsupported") + } //size := len(frame) ycbr := image.NewYCbCr(image.Rect(0, 0, width, height), image.YCbCrSubsampleRatio422) diff --git a/v4l2/capability.go b/v4l2/capability.go index 021d31e..e6ec0a1 100644 --- a/v4l2/capability.go +++ b/v4l2/capability.go @@ -107,7 +107,7 @@ type Capability struct { Card string // BusInfo is the name of the device bus - BusInfo string + BusInfo string // Version is the kernel version Version uint32 @@ -116,7 +116,7 @@ type Capability struct { Capabilities uint32 // DeviceCapabilities is the capability for this particular (opened) device or node - DeviceCapabilities uint32 + DeviceCapabilities uint32 } // GetCapability retrieves capability info for device @@ -135,44 +135,52 @@ func GetCapability(fd uintptr) (Capability, error) { }, nil } +// GetCapabilities returns device capabilities if supported +func (c Capability) GetCapabilities() uint32 { + if c.IsDeviceCapabilitiesProvided() { + return c.DeviceCapabilities + } + return c.Capabilities +} + // IsVideoCaptureSupported returns caps & CapVideoCapture func (c Capability) IsVideoCaptureSupported() bool { - return (uint32(c.Capabilities) & CapVideoCapture) != 0 + return c.Capabilities&CapVideoCapture != 0 } // IsVideoOutputSupported returns caps & CapVideoOutput func (c Capability) IsVideoOutputSupported() bool { - return (uint32(c.Capabilities) & CapVideoOutput) != 0 + return c.Capabilities&CapVideoOutput != 0 } // IsVideoOverlaySupported returns caps & CapVideoOverlay func (c Capability) IsVideoOverlaySupported() bool { - return (uint32(c.Capabilities) & CapVideoOverlay) != 0 + return c.Capabilities&CapVideoOverlay != 0 } // IsVideoOutputOverlaySupported returns caps & CapVideoOutputOverlay func (c Capability) IsVideoOutputOverlaySupported() bool { - return (uint32(c.Capabilities) & CapVideoOutputOverlay) != 0 + return c.Capabilities&CapVideoOutputOverlay != 0 } // IsVideoCaptureMultiplanarSupported returns caps & CapVideoCaptureMPlane func (c Capability) IsVideoCaptureMultiplanarSupported() bool { - return (uint32(c.Capabilities) & CapVideoCaptureMPlane) != 0 + return c.Capabilities&CapVideoCaptureMPlane != 0 } // IsVideoOutputMultiplanerSupported returns caps & CapVideoOutputMPlane func (c Capability) IsVideoOutputMultiplanerSupported() bool { - return (uint32(c.Capabilities) & CapVideoOutputMPlane) != 0 + return c.Capabilities&CapVideoOutputMPlane != 0 } // IsReadWriteSupported returns caps & CapReadWrite func (c Capability) IsReadWriteSupported() bool { - return (uint32(c.Capabilities) & CapReadWrite) != 0 + return c.Capabilities&CapReadWrite != 0 } // IsStreamingSupported returns caps & CapStreaming func (c Capability) IsStreamingSupported() bool { - return (uint32(c.Capabilities) & CapStreaming) != 0 + return c.Capabilities&CapStreaming != 0 } // IsDeviceCapabilitiesProvided returns true if the device returns @@ -180,7 +188,7 @@ func (c Capability) IsStreamingSupported() bool { // See notes on VL42_CAP_DEVICE_CAPS: // https://linuxtv.org/downloads/v4l-dvb-apis/userspace-api/v4l/vidioc-querycap.html?highlight=v4l2_cap_device_caps func (c Capability) IsDeviceCapabilitiesProvided() bool { - return (uint32(c.Capabilities) & CapDeviceCapabilities) != 0 + return c.Capabilities&CapDeviceCapabilities != 0 } // GetDriverCapDescriptions return textual descriptions of driver capabilities diff --git a/v4l2/control.go b/v4l2/control.go new file mode 100644 index 0000000..9ba5b2e --- /dev/null +++ b/v4l2/control.go @@ -0,0 +1,44 @@ +package v4l2 + +//#include +import "C" +import ( + "fmt" + "unsafe" +) + +// Control (v4l2_control) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L1725 +// See https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-g-ctrl.html +type Control struct { + ID uint32 + Value uint32 +} + +// GetControl returns control value for specified ID +func GetControl(fd uintptr, id uint32) (Control, error) { + var ctrl C.struct_v4l2_control + ctrl.id = C.uint(id) + + if err := send(fd, C.VIDIOC_G_CTRL, uintptr(unsafe.Pointer(&ctrl))); err != nil { + return Control{}, fmt.Errorf("get control: id %d: %w", id, err) + } + + return Control{ + ID: uint32(ctrl.id), + Value: uint32(ctrl.value), + }, nil +} + +// SetControl applies control value for specified ID +func SetControl(fd uintptr, id, value uint32) error { + var ctrl C.struct_v4l2_control + ctrl.id = C.uint(id) + ctrl.value = C.int(value) + + if err := send(fd, C.VIDIOC_G_CTRL, uintptr(unsafe.Pointer(&ctrl))); err != nil { + return fmt.Errorf("set control: id %d: value: %d: %w", id, value, err) + } + + return nil +} diff --git a/v4l2/controls.go b/v4l2/controls.go new file mode 100644 index 0000000..b91581a --- /dev/null +++ b/v4l2/controls.go @@ -0,0 +1,97 @@ +package v4l2 + +//#include +import "C" +import ( + "fmt" + "unsafe" +) + +// TODO - Implementation of extended controls (v4l2_ext_control) is paused for now, +// so that efforts can be focused on other parts of the API. This can resumed +// later when type v4l2_ext_control and v4l2_ext_controls are better understood. + +// ExtControl (v4l2_ext_control) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L1730 +// See https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-g-ext-ctrls.html +type ExtControl struct { + ID uint32 + Size uint32 + Ctrl ExtControlUnion +} + +type ExtControlUnion struct { + Value int32 + Value64 int64 + String string + PU8 uint8 + PU16 uint16 + PU32 uint32 + PArea Area + PH264SPS ControlH264SPS + PH264PPS ControlH264PPS + PH264ScalingMatrix ControlH264ScalingMatrix + H264PredWeights ControlH264PredictionWeights + PH264SliceParams ControlH264SliceParams + PH264DecodeParams ControlH264DecodeParams + PFWHTParams ControlFWHTParams + PVP8Frame ControlVP8Frame + PMPEG2Sequence ControlMPEG2Sequence + PMPEG2Picture ControlMPEG2Picture + PMPEG2Quantization ControlMPEG2Quantization + _ uintptr +} + +// ExtControls (v4l2_ext_controls) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L1757 +// See https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-g-ext-ctrls.html +type ExtControls struct { + Which uint32 + Count uint32 + ErrorIndex uint32 + Controls []ExtControl +} + +// GetExtControls retrieve one or more controls +func GetExtControls(fd uintptr, controls []ExtControl) (ExtControls, error) { + if true { + // TODO remove when supported + return ExtControls{}, fmt.Errorf("unsupported") + } + + var ctrls C.struct_v4l2_ext_controls + ctrls.count = C.uint(len(controls)) + + // prepare control requests + var Cctrls []C.struct_v4l2_ext_control + for _, control := range controls { + var Cctrl C.struct_v4l2_ext_control + Cctrl.id = C.uint(control.ID) + Cctrl.size = C.uint(control.Size) + *(*ExtControlUnion)(unsafe.Pointer(&Cctrl.anon0[0])) = control.Ctrl + Cctrls = append(Cctrls, Cctrl) + } + ctrls.controls = (*C.struct_v4l2_ext_control)(unsafe.Pointer(&ctrls.controls)) + + if err := send(fd, C.VIDIOC_G_EXT_CTRLS, uintptr(unsafe.Pointer(&ctrls))); err != nil { + return ExtControls{}, fmt.Errorf("get ext controls: %w", err) + } + + // gather returned controls + retCtrls := ExtControls{ + Count: uint32(ctrls.count), + ErrorIndex: uint32(ctrls.error_idx), + } + // extract controls array + Cctrls = *(*[]C.struct_v4l2_ext_control)(unsafe.Pointer(&ctrls.controls)) + for _, Cctrl := range Cctrls { + extCtrl := ExtControl{ + ID: uint32(Cctrl.id), + Size: uint32(Cctrl.size), + Ctrl: *(*ExtControlUnion)(unsafe.Pointer(&Cctrl.anon0[0])), + } + retCtrls.Controls = append(retCtrls.Controls, extCtrl) + } + + return retCtrls, nil +} diff --git a/v4l2/controls_fwht.go b/v4l2/controls_fwht.go new file mode 100644 index 0000000..479b722 --- /dev/null +++ b/v4l2/controls_fwht.go @@ -0,0 +1,15 @@ +package v4l2 + +// ControlFWHTParams (v4l2_ctrl_fwht_params) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1659 +type ControlFWHTParams struct { + BackwardRefTimestamp uint64 + Version uint32 + Width uint32 + Height uint32 + Flags uint32 + Colorspace ColorspaceType + XFerFunc XferFunctionType + YCbCrEncoding YCbCrEncodingType + Quantization QuantizationType +} diff --git a/v4l2/controls_h264.go b/v4l2/controls_h264.go new file mode 100644 index 0000000..25ffb04 --- /dev/null +++ b/v4l2/controls_h264.go @@ -0,0 +1,135 @@ +package v4l2 + +import "C" + +// TODO - Need to figure out how to import the proper header files for H264 support +const ( + H264NumDPBEntries uint32 = 16 // C.V4L2_H264_NUM_DPB_ENTRIES + H264RefListLength uint32 = 32 // C.V4L2_H264_REF_LIST_LEN +) + +// ControlH264SPS (v4l2_ctrl_h264_sps) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1308 +type ControlH264SPS struct { + ProfileIDC uint8 + ConstraintSetFlags uint8 + LevelIDC uint8 + SequenceParameterSetID uint8 + ChromaFormatIDC uint8 + BitDepthLumaMinus8 uint8 + BitDepthChromaMinus8 uint8 + Log2MaxFrameNumMinus4 uint8 + PicOrderCntType uint8 + Log2MaxPicOrderCntLsbMinus4 uint8 + MaxNumRefFrames uint8 + NumRefFramesInPicOrderCntCycle uint8 + OffsetForRefFrame [255]int32 + OffsetForNonRefPic int32 + OffsetForTopToBottomField int32 + PicWidthInMbsMinus1 uint16 + PicHeightInMapUnitsMinus1 uint16 + Falgs uint32 +} + +// ControlH264PPS (v4l2_ctrl_h264_pps) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1364 +type ControlH264PPS struct { + PicParameterSetID uint8 + SeqParameterSetID uint8 + NumSliceGroupsMinus1 uint8 + NumRefIndexL0DefaultActiveMinus1 uint8 + NumRefIndexL1DefaultActiveMinus1 uint8 + WeightedBipredIDC uint8 + PicInitQPMinus26 int8 + PicInitQSMinus26 int8 + ChromaQPIndexOffset int8 + SecondChromaQPIndexOffset int8 + Flags uint16 +} + +// ControlH264ScalingMatrix (v4l2_ctrl_h264_scaling_matrix) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1396 +type ControlH264ScalingMatrix struct { + ScalingList4x4 [6][16]uint8 + ScalingList8x8 [6][64]uint8 +} + +type H264WeightFators struct { + LumaWeight [32]int16 + LumaOffset [32]int16 + ChromaWeight [32][2]int16 + ChromaOffset [32][2]int16 +} + +// ControlH264PredictionWeights (4l2_ctrl_h264_pred_weights) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1426 +type ControlH264PredictionWeights struct { + LumaLog2WeightDenom uint16 + ChromaLog2WeightDenom uint16 + WeightFactors [2]H264WeightFators +} + +// H264Reference (v4l2_h264_reference) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1452 +type H264Reference struct { + Fields uint8 + Index uint8 +} + +// ControlH264SliceParams (v4l2_ctrl_h264_slice_params) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1499 +type ControlH264SliceParams struct { + HeaderBitSize uint32 + FirstMBInSlice uint32 + SliceType uint8 + ColorPlaneID uint8 + RedundantPicCnt uint8 + CabacInitIDC uint8 + SliceQPDelta int8 + SliceQSDelta int8 + DisableDeblockingFilterIDC uint8 + SliceAlphaC0OffsetDiv2 int8 + SliceBetaOffsetDiv2 int8 + NumRefIdxL0ActiveMinus1 uint8 + NumRefIdxL1ActiveMinus1 uint8 + + _ uint8 // reserved for padding + + RefPicList0 [H264RefListLength]H264Reference + RefPicList1 [H264RefListLength]H264Reference + + Flags uint32 +} + +// H264DPBEntry (v4l2_h264_dpb_entry) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1544 +type H264DPBEntry struct { + ReferenceTS uint64 + PicNum uint32 + FrameNum uint16 + Fields uint8 + _ [8]uint8 // reserved (padding field) + TopFieldOrder int32 + BottomFieldOrderCnt int32 + Flags uint32 +} + +// ControlH264DecodeParams (v4l2_ctrl_h264_decode_params) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1581 +type ControlH264DecodeParams struct { + DPB [H264NumDPBEntries]H264DPBEntry + NalRefIDC uint16 + FrameNum uint16 + TopFieldOrderCnt int32 + BottomFieldOrderCnt int32 + IDRPicID uint16 + PicOrderCntLSB uint16 + DeltaPicOrderCntBottom int32 + DeltaPicOrderCnt0 int32 + DeltaPicOrderCnt1 int32 + DecRefPicMarkingBitSize uint32 + PicOrderCntBitSize uint32 + SliceGroupChangeCycle uint32 + _ uint32 // reserved (padding) + Flags uint32 +} diff --git a/v4l2/controls_mpeg2.go b/v4l2/controls_mpeg2.go new file mode 100644 index 0000000..931a179 --- /dev/null +++ b/v4l2/controls_mpeg2.go @@ -0,0 +1,34 @@ +package v4l2 + +// ControlMPEG2Sequence (v4l2_ctrl_mpeg2_sequence) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1892 +type ControlMPEG2Sequence struct { + HorizontalSize uint16 + VerticalSize uint16 + VBVBufferSize uint32 + ProfileAndLevelIndication uint16 + ChromaFormat uint8 + Flags uint8 +} + +// ControlMPEG2Picture (v4l2_ctrl_mpeg2_picture) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1939 +type ControlMPEG2Picture struct { + BackwardRefTimestamp uint64 + ForwardRefTimestamp uint64 + Flags uint32 + FCode [2][2]uint8 + PictureCodingType uint8 + PictureStructure uint8 + IntraDCPrecision uint8 + _ [5]uint8 // padding +} + +// ControlMPEG2Quantization (v4l2_ctrl_mpeg2_quantisation) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1972 +type ControlMPEG2Quantization struct { + IntraQuantizerMatrix [64]uint8 + NonIntraQuantizerMatrix [64]uint8 + ChromaIntraQuantizerMatrix [64]uint8 + ChromaNonIntraQuantizerMatrix [64]uint8 +} diff --git a/v4l2/controls_vp8.go b/v4l2/controls_vp8.go new file mode 100644 index 0000000..d0e11e1 --- /dev/null +++ b/v4l2/controls_vp8.go @@ -0,0 +1,94 @@ +package v4l2 + +//#include +import "C" + +const ( + VP8CoefficientProbabilityCount uint32 = 11 // C.V4L2_VP8_COEFF_PROB_CNT + VP8MVProbabilityCount uint32 = 19 // C.V4L2_VP8_MV_PROB_CNT +) + +// VP8Segment (v4l2_vp8_segment) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1692 +type VP8Segment struct { + QuantUpdate [4]int8 + LoopFilterUpdate [4]int8 + SegmentProbabilities [3]uint8 + _ uint8 // padding + Flags uint32 +} + +// VP8LoopFilter (v4l2_vp8_loop_filter) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1719 +type VP8LoopFilter struct { + ReferenceFrameDelta int8 + MBModeDelta int8 + SharpnessLevel uint8 + Level uint8 + _ uint16 // padding + Flags uint32 +} + +// VP8Quantization (v4l2_vp8_quantization) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1744 +type VP8Quantization struct { + YACQIndex uint8 + YDCDelta int8 + Y2DCDelta int8 + Y2ACDelta int8 + UVDCDelta int8 + UVACDelta int8 + _ uint16 +} + +// VP8Entropy (v4l2_vp8_entropy) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1771 +type VP8Entropy struct { + CoefficientProbabilities [4][8][3][VP8CoefficientProbabilityCount]uint8 + YModeProbabilities uint8 + UVModeProbabilities uint8 + MVProbabilities [2][VP8MVProbabilityCount]uint8 + _ [3]uint8 // padding +} + +// VP8EntropyCoderState (v4l2_vp8_entropy_coder_state) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1790 +type VP8EntropyCoderState struct { + Range uint8 + Value uint8 + BitCount uint8 + _ uint8 // padding +} + +// ControlVP8Frame (v4l2_ctrl_vp8_frame) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1836 +type ControlVP8Frame struct { + Segment VP8Segment + LoopFilter VP8LoopFilter + Quantization VP8Quantization + Entropy VP8Entropy + EntropyCoderState VP8EntropyCoderState + + Width uint16 + Height uint16 + + HorizontalScale uint8 + VerticalScale uint8 + + Version uint8 + ProbSkipFalse uint8 + PropIntra uint8 + PropLast uint8 + ProbGF uint8 + NumDCTParts uint8 + + FirstPartSize uint32 + FirstPartHeader uint32 + DCTPartSize uint32 + + LastFrameTimestamp uint64 + GoldenFrameTimestamp uint64 + AltFrameTimestamp uint64 + + Flags uint64 +} diff --git a/v4l2/crop.go b/v4l2/crop.go index f750629..2456472 100644 --- a/v4l2/crop.go +++ b/v4l2/crop.go @@ -8,24 +8,6 @@ import ( "unsafe" ) -// Rect (v4l2_rect) -// https://www.kernel.org/doc/html/v4.14/media/uapi/v4l/dev-overlay.html?highlight=v4l2_rect#c.v4l2_rect -// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L412 -type Rect struct { - Left int32 - Top int32 - Width uint32 - Height uint32 -} - -// Fract (v4l2_fract) -// https://www.kernel.org/doc/html/v4.14/media/uapi/v4l/vidioc-enumstd.html#c.v4l2_fract -// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L419 -type Fract struct { - Numerator uint32 - Denominator uint32 -} - // CropCapability (v4l2_cropcap) // https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-cropcap.html#c.v4l2_cropcap // https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L1221 @@ -39,9 +21,9 @@ type CropCapability struct { // GetCropCapability retrieves cropping info for specified device // See https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-cropcap.html#ioctl-vidioc-cropcap -func GetCropCapability(fd uintptr) (CropCapability, error) { +func GetCropCapability(fd uintptr, bufType BufType) (CropCapability, error) { var cap C.struct_v4l2_cropcap - cap._type = C.uint(BufTypeVideoCapture) + cap._type = C.uint(bufType) if err := send(fd, C.VIDIOC_CROPCAP, uintptr(unsafe.Pointer(&cap))); err != nil { return CropCapability{}, fmt.Errorf("crop capability: %w", err) diff --git a/v4l2/device/device.go b/v4l2/device/device.go deleted file mode 100644 index b552f25..0000000 --- a/v4l2/device/device.go +++ /dev/null @@ -1,314 +0,0 @@ -package device - -import ( - "context" - "fmt" - "os" - sys "syscall" - "time" - - "github.com/vladimirvivien/go4vl/v4l2" -) - -type Device struct { - path string - file *os.File - fd uintptr - cap *v4l2.Capability - cropCap *v4l2.CropCapability - pixFormat v4l2.PixFormat - buffers [][]byte - requestedBuf v4l2.RequestBuffers - streaming bool -} - -// Open creates opens the underlying device at specified path -// and returns a *Device or an error if unable to open device. -func Open(path string) (*Device, error) { - file, err := os.OpenFile(path, sys.O_RDWR|sys.O_NONBLOCK, 0666) - if err != nil { - return nil, fmt.Errorf("device open: %w", err) - } - return &Device{path: path, file: file, fd: file.Fd()}, nil -} - -// Close closes the underlying device associated with `d` . -func (d *Device) Close() error { - if d.streaming{ - if err := d.StopStream(); err != nil{ - return err - } - } - - return d.file.Close() -} - -// GetFileDescriptor returns the file descriptor value for the device -func (d *Device) GetFileDescriptor() uintptr { - return d.fd -} - -// GetCapability retrieves device capability info and -// caches it for future capability check. -func (d *Device) GetCapability() (*v4l2.Capability, error) { - if d.cap != nil { - return d.cap, nil - } - cap, err := v4l2.GetCapability(d.fd) - if err != nil { - return nil, fmt.Errorf("device: %w", err) - } - d.cap = &cap - return d.cap, nil -} - -// GetCropCapability returns cropping info for device `d` -// and caches it for future capability check. -func (d *Device) GetCropCapability() (v4l2.CropCapability, error) { - if d.cropCap != nil { - return *d.cropCap, nil - } - if err := d.assertVideoCaptureSupport(); err != nil { - return v4l2.CropCapability{}, fmt.Errorf("device: %w", err) - } - - cropCap, err := v4l2.GetCropCapability(d.fd) - if err != nil { - return v4l2.CropCapability{}, fmt.Errorf("device: %w", err) - } - d.cropCap = &cropCap - return cropCap, nil -} - -// SetCropRect crops the video dimension for the device -func (d *Device) SetCropRect(r v4l2.Rect) error { - if err := d.assertVideoCaptureSupport(); err != nil { - return fmt.Errorf("device: %w", err) - } - if err := v4l2.SetCropRect(d.fd, r); err != nil { - return fmt.Errorf("device: %w", err) - } - return nil -} - -// GetPixFormat retrieves pixel format info for device -func (d *Device) GetPixFormat() (v4l2.PixFormat, error) { - if err := d.assertVideoCaptureSupport(); err != nil { - return v4l2.PixFormat{}, fmt.Errorf("device: %w", err) - } - pixFmt, err := v4l2.GetPixFormat(d.fd) - if err != nil { - return v4l2.PixFormat{}, fmt.Errorf("device: %w", err) - } - return pixFmt, nil -} - -// SetPixFormat sets the pixel format for the associated device. -func (d *Device) SetPixFormat(pixFmt v4l2.PixFormat) error { - if err := d.assertVideoCaptureSupport(); err != nil { - return fmt.Errorf("device: %w", err) - } - - if err := v4l2.SetPixFormat(d.fd, pixFmt); err != nil { - return fmt.Errorf("device: %w", err) - } - return nil -} - -// GetFormatDescription returns a format description for the device at specified format index -func (d *Device) GetFormatDescription(idx uint32) (v4l2.FormatDescription, error) { - if err := d.assertVideoCaptureSupport(); err != nil { - return v4l2.FormatDescription{}, fmt.Errorf("device: %w", err) - } - - return v4l2.GetFormatDescription(d.fd, idx) -} - - -// GetFormatDescriptions returns all possible format descriptions for device -func (d *Device) GetFormatDescriptions() ([]v4l2.FormatDescription, error) { - if err := d.assertVideoCaptureSupport(); err != nil { - return nil, fmt.Errorf("device: %w", err) - } - - return v4l2.GetAllFormatDescriptions(d.fd) -} - -// GetVideoInputIndex returns current video input index for device -func (d *Device) GetVideoInputIndex()(int32, error) { - if err := d.assertVideoCaptureSupport(); err != nil { - return 0, fmt.Errorf("device: %w", err) - } - return v4l2.GetCurrentVideoInputIndex(d.fd) -} - -// GetVideoInputInfo returns video input info for device -func (d *Device) GetVideoInputInfo(index uint32) (v4l2.InputInfo, error) { - if err := d.assertVideoCaptureSupport(); err != nil { - return v4l2.InputInfo{}, fmt.Errorf("device: %w", err) - } - return v4l2.GetVideoInputInfo(d.fd, index) -} - -// GetCaptureParam returns streaming capture parameter information -func (d *Device) GetCaptureParam() (v4l2.CaptureParam, error) { - if err := d.assertVideoCaptureSupport(); err != nil { - return v4l2.CaptureParam{}, fmt.Errorf("device: %w", err) - } - return v4l2.GetStreamCaptureParam(d.fd) -} - -// GetMediaInfo returns info for a device that supports the Media API -func (d *Device) GetMediaInfo() (v4l2.MediaDeviceInfo, error) { - return v4l2.GetMediaDeviceInfo(d.fd) -} - -func (d *Device) StartStream(buffSize uint32) error { - if d.streaming { - return nil - } - if err := d.assertVideoStreamSupport(); err != nil { - return fmt.Errorf("device: %w", err) - } - - // allocate device buffers - bufReq, err := v4l2.InitBuffers(d.fd, buffSize) - if err != nil { - return fmt.Errorf("device: start stream: %w", err) - } - d.requestedBuf = bufReq - - // for each device buff allocated, prepare local mapped buffer - bufCount := int(d.requestedBuf.Count) - d.buffers = make([][]byte, d.requestedBuf.Count) - for i := 0; i < bufCount; i++ { - buffer, err := v4l2.GetBuffer(d.fd, uint32(i)) - if err != nil { - return fmt.Errorf("device start stream: %w", err) - } - - offset := buffer.Info.Offset - length := buffer.Length - mappedBuf, err := v4l2.MapMemoryBuffer(d.fd, int64(offset), int(length)) - if err != nil { - return fmt.Errorf("device start stream: %w", err) - } - d.buffers[i] = mappedBuf - } - - // Initial enqueue of buffers for capture - for i := 0; i < bufCount; i++ { - _, err := v4l2.QueueBuffer(d.fd, uint32(i)) - if err != nil { - return fmt.Errorf("device start stream: %w", err) - } - } - - // turn on device stream - if err := v4l2.StreamOn(d.fd); err != nil { - return fmt.Errorf("device start stream: %w", err) - } - - d.streaming = true - - return nil -} - -// Capture captures video buffer from device and emit -// each buffer on channel. -func (d *Device) Capture(ctx context.Context, fps uint32) (<-chan []byte, error) { - if !d.streaming { - return nil, fmt.Errorf("device: capture: streaming not started") - } - if ctx == nil { - return nil, fmt.Errorf("device: context nil") - } - - bufCount := int(d.requestedBuf.Count) - dataChan := make(chan []byte, bufCount) - - if fps == 0 { - fps = 10 - } - - // delay duration based on frame per second - fpsDelay := time.Duration((float64(1) / float64(fps)) * float64(time.Second)) - - go func() { - defer close(dataChan) - - // capture forever or until signaled to stop - for { - // capture bufCount frames - for i := 0; i < bufCount; i++ { - //TODO add better error-handling during capture, for now just panic - if err := v4l2.WaitForDeviceRead(d.fd, 2*time.Second); err != nil { - panic(fmt.Errorf("device: capture: %w", err).Error()) - } - - // dequeue the device buf - bufInfo, err := v4l2.DequeueBuffer(d.fd) - if err != nil { - panic(fmt.Errorf("device: capture: %w", err).Error()) - } - - // assert dequeued buffer is in proper range - if !(int(bufInfo.Index) < bufCount) { - panic(fmt.Errorf("device: capture: unexpected device buffer index: %d", bufInfo.Index).Error()) - } - - select { - case dataChan <- d.buffers[bufInfo.Index][:bufInfo.BytesUsed]: - case <-ctx.Done(): - return - } - // enqueu used buffer, prepare for next read - if _, err := v4l2.QueueBuffer(d.fd, bufInfo.Index); err != nil { - panic(fmt.Errorf("device capture: %w", err).Error()) - } - - time.Sleep(fpsDelay) - } - } - }() - - return dataChan, nil -} - -func (d *Device) StopStream() error{ - d.streaming = false - for i := 0; i < len(d.buffers); i++ { - if err := v4l2.UnmapMemoryBuffer(d.buffers[i]); err != nil { - return fmt.Errorf("device: stop stream: %w", err) - } - } - if err := v4l2.StreamOff(d.fd); err != nil { - return fmt.Errorf("device: stop stream: %w", err) - } - return nil -} - -func (d *Device) assertVideoCaptureSupport() error { - cap, err := d.GetCapability() - if err != nil { - return fmt.Errorf("device capability: %w", err) - } - if !cap.IsVideoCaptureSupported() { - return fmt.Errorf("device capability: video capture not supported") - } - return nil -} - -func (d *Device) assertVideoStreamSupport() error { - cap, err := d.GetCapability() - if err != nil { - return fmt.Errorf("device capability: %w", err) - } - if !cap.IsVideoCaptureSupported() { - return fmt.Errorf("device capability: video capture not supported") - } - if !cap.IsStreamingSupported() { - return fmt.Errorf("device capability: streaming not supported") - } - return nil -} diff --git a/v4l2/dimension.go b/v4l2/dimension.go new file mode 100644 index 0000000..ebd47d2 --- /dev/null +++ b/v4l2/dimension.go @@ -0,0 +1,26 @@ +package v4l2 + +// Area (v4l2_area) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L424 +type Area struct { + Width uint32 + Height uint32 +} + +// Fract (v4l2_fract) +// https://www.kernel.org/doc/html/v4.14/media/uapi/v4l/vidioc-enumstd.html#c.v4l2_fract +// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L419 +type Fract struct { + Numerator uint32 + Denominator uint32 +} + +// Rect (v4l2_rect) +// https://www.kernel.org/doc/html/v4.14/media/uapi/v4l/dev-overlay.html?highlight=v4l2_rect#c.v4l2_rect +// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L412 +type Rect struct { + Left int32 + Top int32 + Width uint32 + Height uint32 +} diff --git a/v4l2/errors.go b/v4l2/errors.go index ab100af..7b454a1 100644 --- a/v4l2/errors.go +++ b/v4l2/errors.go @@ -6,21 +6,22 @@ import ( ) var ( - ErrorSystem = errors.New("system error") - ErrorBadArgument = errors.New("bad argument error") - ErrorTemporary = errors.New("temporary error") - ErrorTimeout = errors.New("timeout error") - ErrorUnsupported = errors.New("unsupported error") + ErrorSystem = errors.New("system error") + ErrorBadArgument = errors.New("bad argument error") + ErrorTemporary = errors.New("temporary error") + ErrorTimeout = errors.New("timeout error") + ErrorUnsupported = errors.New("unsupported error") + ErrorUnsupportedFeature = errors.New("feature unsupported error") ) func parseErrorType(errno sys.Errno) error { switch errno { case sys.EBADF, sys.ENOMEM, sys.ENODEV, sys.EIO, sys.ENXIO, sys.EFAULT: // structural, terminal - return ErrorSystem + return ErrorSystem case sys.EINVAL: // bad argument - return ErrorBadArgument + return ErrorBadArgument case sys.ENOTTY: // unsupported - return ErrorUnsupported + return ErrorUnsupported default: if errno.Timeout() { return ErrorTimeout diff --git a/v4l2/format.go b/v4l2/format.go index dab36bf..84f655c 100644 --- a/v4l2/format.go +++ b/v4l2/format.go @@ -301,7 +301,7 @@ func GetPixFormat(fd uintptr) (PixFormat, error) { Priv: uint32(v4l2PixFmt.priv), Flags: uint32(v4l2PixFmt.flags), YcbcrEnc: *(*uint32)(unsafe.Pointer(&v4l2PixFmt.anon0[0])), - HSVEnc: *(*uint32)(unsafe.Pointer(uintptr(unsafe.Pointer(&v4l2PixFmt.anon0[0])) + unsafe.Sizeof(&v4l2PixFmt.anon0[0]))), + HSVEnc: *(*uint32)(unsafe.Pointer(uintptr(unsafe.Pointer(&v4l2PixFmt.anon0[0])) + unsafe.Sizeof(C.uint(0)))), Quantization: uint32(v4l2PixFmt.quantization), XferFunc: uint32(v4l2PixFmt.xfer_func), }, nil diff --git a/v4l2/format_frameintervals.go b/v4l2/format_frameintervals.go new file mode 100644 index 0000000..573e53f --- /dev/null +++ b/v4l2/format_frameintervals.go @@ -0,0 +1,86 @@ +package v4l2 + +//#include +import "C" +import ( + "fmt" + "unsafe" +) + +// FrameIntervalType (v4l2_frmivaltypes) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L845 +type FrameIntervalType = uint32 + +const ( + FrameIntervalTypeDiscrete FrameIntervalType = C.V4L2_FRMIVAL_TYPE_DISCRETE + FrameIntervalTypeContinuous FrameIntervalType = C.V4L2_FRMIVAL_TYPE_CONTINUOUS + FrameIntervalTypeStepwise FrameIntervalType = C.V4L2_FRMIVAL_TYPE_STEPWISE +) + +// FrameIntervalEnum is used to store v4l2_frmivalenum values. +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L857 +// See https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-enum-frameintervals.html +type FrameIntervalEnum struct { + Index uint32 + PixelFormat FourCCType + Width uint32 + Height uint32 + Type FrameIntervalType + Interval FrameInterval +} + +// FrameInterval stores all frame interval values regardless of its type. This type maps to v4l2_frmival_stepwise. +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L851 +// See https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-enum-frameintervals.html +type FrameInterval struct { + Min Fract + Max Fract + Step Fract +} + +// getFrameInterval retrieves the supported frame interval info from following union based on the type: + +// union { +// struct v4l2_fract discrete; +// struct v4l2_frmival_stepwise stepwise; +// } + +// See https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-enum-frameintervals.html +func getFrameInterval(interval C.struct_v4l2_frmivalenum) (FrameIntervalEnum, error) { + frmInterval := FrameIntervalEnum{ + Index: uint32(interval.index), + Type: FrameIntervalType(interval._type), + PixelFormat: FourCCType(interval.pixel_format), + Width: uint32(interval.width), + Height: uint32(interval.height), + } + intervalType := uint32(interval._type) + switch intervalType { + case FrameIntervalTypeDiscrete: + fiDiscrete := *(*Fract)(unsafe.Pointer(&interval.anon0[0])) + frmInterval.Interval.Min = fiDiscrete + frmInterval.Interval.Max = fiDiscrete + frmInterval.Interval.Step.Numerator = 1 + frmInterval.Interval.Step.Denominator = 1 + case FrameIntervalTypeStepwise, FrameIntervalTypeContinuous: + // Calculate pointer to stepwise member of union + frmInterval.Interval = *(*FrameInterval)(unsafe.Pointer(uintptr(unsafe.Pointer(&interval.anon0[0])) + unsafe.Sizeof(Fract{}))) + default: + return FrameIntervalEnum{}, fmt.Errorf("unsupported frame interval type: %d", intervalType) + } + return frmInterval, nil +} + +// GetFormatFrameInterval returns a supported device frame interval for a specified encoding at index and format +func GetFormatFrameInterval(fd uintptr, index uint32, encoding FourCCType, width, height uint32) (FrameIntervalEnum, error) { + var interval C.struct_v4l2_frmivalenum + interval.index = C.uint(index) + interval.pixel_format = C.uint(encoding) + interval.width = C.uint(width) + interval.height = C.uint(height) + + if err := send(fd, C.VIDIOC_ENUM_FRAMEINTERVALS, uintptr(unsafe.Pointer(&interval))); err != nil { + return FrameIntervalEnum{}, fmt.Errorf("frame interval: index %d: %w", index, err) + } + return getFrameInterval(interval) +} diff --git a/v4l2/format_framesizes.go b/v4l2/format_framesizes.go index 45124ee..ddcd7f0 100644 --- a/v4l2/format_framesizes.go +++ b/v4l2/format_framesizes.go @@ -17,15 +17,15 @@ const ( FrameSizeTypeStepwise FrameSizeType = C.V4L2_FRMSIZE_TYPE_STEPWISE ) -// FrameSize uses v4l2_frmsizeenum to get supporeted frame size for the driver based for the pixel format. +// FrameSizeEnum uses v4l2_frmsizeenum to get supporeted frame size for the driver based for the pixel format. // Use FrameSizeType to determine which sizes the driver support. // https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L829 // https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-enum-framesizes.html -type FrameSize struct { - FrameSizeType - FrameSizeDiscrete - FrameSizeStepwise +type FrameSizeEnum struct { + Index uint32 + Type FrameSizeType PixelFormat FourCCType + Size FrameSize } // FrameSizeDiscrete (v4l2_frmsize_discrete) @@ -35,9 +35,10 @@ type FrameSizeDiscrete struct { Height uint32 // height [pixel] } -// FrameSizeStepwise (v4l2_frmsize_stepwise) -// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L820 -type FrameSizeStepwise struct { +// FrameSize stores all possible frame size information regardless of its type. It is mapped to v4l2_frmsize_stepwise. +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L820 +// See https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-enum-framesizes.html +type FrameSize struct { MinWidth uint32 // Minimum frame width [pixel] MaxWidth uint32 // Maximum frame width [pixel] StepWidth uint32 // Frame width step size [pixel] @@ -46,40 +47,45 @@ type FrameSizeStepwise struct { StepHeight uint32 // Frame height step size [pixel] } -// getFrameSize retrieves the supported frame size based on the type -func getFrameSize(frmSizeEnum C.struct_v4l2_frmsizeenum) FrameSize { - frameSize := FrameSize{FrameSizeType: FrameSizeType(frmSizeEnum._type), PixelFormat: FourCCType(frmSizeEnum.pixel_format)} - switch frameSize.FrameSizeType { +// getFrameSize retrieves the supported frame size info from following union based on the type: + +// union { +// struct v4l2_frmsize_discrete discrete; +// struct v4l2_frmsize_stepwise stepwise; +// } + +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L829 +func getFrameSize(frmSizeEnum C.struct_v4l2_frmsizeenum) FrameSizeEnum { + frameSize := FrameSizeEnum{Type: FrameSizeType(frmSizeEnum._type), PixelFormat: FourCCType(frmSizeEnum.pixel_format)} + switch frameSize.Type { case FrameSizeTypeDiscrete: - fsDiscrete := (*FrameSizeDiscrete)(unsafe.Pointer(&frmSizeEnum.anon0[0])) - frameSize.FrameSizeDiscrete = *fsDiscrete - frameSize.FrameSizeStepwise.MinWidth = frameSize.FrameSizeDiscrete.Width - frameSize.FrameSizeStepwise.MinHeight = frameSize.FrameSizeDiscrete.Height - frameSize.FrameSizeStepwise.MaxWidth = frameSize.FrameSizeDiscrete.Width - frameSize.FrameSizeStepwise.MaxHeight = frameSize.FrameSizeDiscrete.Height + fsDiscrete := *(*FrameSizeDiscrete)(unsafe.Pointer(&frmSizeEnum.anon0[0])) + frameSize.Size.MinWidth = fsDiscrete.Width + frameSize.Size.MinHeight = fsDiscrete.Height + frameSize.Size.MaxWidth = fsDiscrete.Width + frameSize.Size.MaxHeight = fsDiscrete.Height case FrameSizeTypeStepwise, FrameSizeTypeContinuous: - fsStepwise := (*FrameSizeStepwise)(unsafe.Pointer(&frmSizeEnum.anon0[0])) - frameSize.FrameSizeStepwise = *fsStepwise - frameSize.FrameSizeDiscrete.Width = frameSize.FrameSizeStepwise.MaxWidth - frameSize.FrameSizeDiscrete.Height = frameSize.FrameSizeStepwise.MaxHeight + // Calculate pointer to access stepwise member + frameSize.Size = *(*FrameSize)(unsafe.Pointer(uintptr(unsafe.Pointer(&frmSizeEnum.anon0[0])) + unsafe.Sizeof(FrameSizeDiscrete{}))) + default: } return frameSize } // GetFormatFrameSize returns a supported device frame size for a specified encoding at index -func GetFormatFrameSize(fd uintptr, index uint32, encoding FourCCType) (FrameSize, error) { +func GetFormatFrameSize(fd uintptr, index uint32, encoding FourCCType) (FrameSizeEnum, error) { var frmSizeEnum C.struct_v4l2_frmsizeenum frmSizeEnum.index = C.uint(index) frmSizeEnum.pixel_format = C.uint(encoding) if err := send(fd, C.VIDIOC_ENUM_FRAMESIZES, uintptr(unsafe.Pointer(&frmSizeEnum))); err != nil { - return FrameSize{}, fmt.Errorf("frame size: index %d: %w", index, err) + return FrameSizeEnum{}, fmt.Errorf("frame size: index %d: %w", index, err) } return getFrameSize(frmSizeEnum), nil } // GetFormatFrameSizes returns all supported device frame sizes for a specified encoding -func GetFormatFrameSizes(fd uintptr, encoding FourCCType) (result []FrameSize, err error) { +func GetFormatFrameSizes(fd uintptr, encoding FourCCType) (result []FrameSizeEnum, err error) { index := uint32(0) for { var frmSizeEnum C.struct_v4l2_frmsizeenum @@ -107,7 +113,7 @@ func GetFormatFrameSizes(fd uintptr, encoding FourCCType) (result []FrameSize, e // GetAllFormatFrameSizes returns all supported frame sizes for all supported formats. // It iterates from format at index 0 until it encounters and error and then stops. For // each supported format, it retrieves all supported frame sizes. -func GetAllFormatFrameSizes(fd uintptr) (result []FrameSize, err error) { +func GetAllFormatFrameSizes(fd uintptr) (result []FrameSizeEnum, err error) { formats, err := GetAllFormatDescriptions(fd) if len(formats) == 0 && err != nil { return nil, fmt.Errorf("frame sizes: %w", err) diff --git a/v4l2/stream_param.go b/v4l2/stream_param.go index 1dd24ce..0491722 100644 --- a/v4l2/stream_param.go +++ b/v4l2/stream_param.go @@ -18,7 +18,16 @@ const ( StreamParamTimePerFrame StreamParamFlag = C.V4L2_CAP_TIMEPERFRAME ) -// CaptureParam (v4l2_captureparam) +// StreamParam (v4l2_streamparam) +// https://linuxtv.org/downloads/v4l-dvb-apis/userspace-api/v4l/vidioc-g-parm.html#c.V4L.v4l2_streamparm +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L2362 +type StreamParam struct { + Type IOType + Capture CaptureParam + Output OutputParam +} + +// CaptureParam (v4l2_captureparm) // https://linuxtv.org/downloads/v4l-dvb-apis/userspace-api/v4l/vidioc-g-parm.html#c.V4L.v4l2_captureparm // See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L1205 type CaptureParam struct { @@ -30,16 +39,53 @@ type CaptureParam struct { _ [4]uint32 } -// GetStreamCaptureParam returns streaming capture parameter for the driver (v4l2_streamparm). +// OutputParam (v4l2_outputparm) +// https://linuxtv.org/downloads/v4l-dvb-apis/userspace-api/v4l/vidioc-g-parm.html#c.V4L.v4l2_outputparm +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L1228 +type OutputParam struct { + Capability StreamParamFlag + CaptureMode StreamParamFlag + TimePerFrame Fract + ExtendedMode uint32 + WriteBuffers uint32 + _ [4]uint32 +} + +// GetStreamParam returns streaming parameters for the driver (v4l2_streamparm). // https://linuxtv.org/downloads/v4l-dvb-apis/userspace-api/v4l/vidioc-g-parm.html -// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L2347 +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L2362 +func GetStreamParam(fd uintptr, bufType BufType) (StreamParam, error) { + var v4l2Param C.struct_v4l2_streamparm + v4l2Param._type = C.uint(bufType) -func GetStreamCaptureParam(fd uintptr) (CaptureParam, error) { - var param C.struct_v4l2_streamparm - param._type = C.uint(BufTypeVideoCapture) + if err := send(fd, C.VIDIOC_G_PARM, uintptr(unsafe.Pointer(&v4l2Param))); err != nil { + return StreamParam{}, fmt.Errorf("stream param: %w", err) + } + + capture := *(*CaptureParam)(unsafe.Pointer(&v4l2Param.parm[0])) + output := *(*OutputParam)(unsafe.Pointer(uintptr(unsafe.Pointer(&v4l2Param.parm[0])) + unsafe.Sizeof(C.struct_v4l2_captureparm{}))) + + return StreamParam{ + Type: BufTypeVideoCapture, + Capture: capture, + Output: output, + }, nil +} - if err := send(fd, C.VIDIOC_G_PARM, uintptr(unsafe.Pointer(¶m))); err != nil { - return CaptureParam{}, fmt.Errorf("stream param: %w", err) +func SetStreamParam(fd uintptr, bufType BufType, param StreamParam) error { + var v4l2Parm C.struct_v4l2_streamparm + v4l2Parm._type = C.uint(bufType) + if bufType == BufTypeVideoCapture { + *(*C.struct_v4l2_captureparm)(unsafe.Pointer(&v4l2Parm.parm[0])) = *(*C.struct_v4l2_captureparm)(unsafe.Pointer(¶m.Capture)) } - return *(*CaptureParam)(unsafe.Pointer(¶m.parm[0])), nil + if bufType == BufTypeVideoOutput { + *(*C.struct_v4l2_outputparm)(unsafe.Pointer(uintptr(unsafe.Pointer(&v4l2Parm.parm[0])) + unsafe.Sizeof(v4l2Parm.parm[0]))) = + *(*C.struct_v4l2_outputparm)(unsafe.Pointer(¶m.Output)) + } + + if err := send(fd, C.VIDIOC_S_PARM, uintptr(unsafe.Pointer(&v4l2Parm))); err != nil { + return fmt.Errorf("stream param: %w", err) + } + + return nil } diff --git a/v4l2/streaming.go b/v4l2/streaming.go index fc7dfe5..573a404 100644 --- a/v4l2/streaming.go +++ b/v4l2/streaming.go @@ -4,9 +4,7 @@ package v4l2 import "C" import ( - "errors" "fmt" - "time" "unsafe" sys "golang.org/x/sys/unix" @@ -26,16 +24,16 @@ const ( BufTypeOverlay BufType = C.V4L2_BUF_TYPE_VIDEO_OVERLAY ) -// StreamType (v4l2_memory) +// IOType (v4l2_memory) // https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/mmap.html?highlight=v4l2_memory_mmap // https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L188 -type StreamType = uint32 +type IOType = uint32 const ( - StreamTypeMMAP StreamType = C.V4L2_MEMORY_MMAP - StreamTypeUserPtr StreamType = C.V4L2_MEMORY_USERPTR - StreamTypeOverlay StreamType = C.V4L2_MEMORY_OVERLAY - StreamTypeDMABuf StreamType = C.V4L2_MEMORY_DMABUF + IOTypeMMAP IOType = C.V4L2_MEMORY_MMAP + IOTypeUserPtr IOType = C.V4L2_MEMORY_USERPTR + IOTypeOverlay IOType = C.V4L2_MEMORY_OVERLAY + IOTypeDMABuf IOType = C.V4L2_MEMORY_DMABUF ) // TODO implement vl42_create_buffers @@ -58,37 +56,37 @@ type RequestBuffers struct { // https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L1037 // type Buffer struct { - Index uint32 - StreamType uint32 - BytesUsed uint32 - Flags uint32 - Field uint32 - Timestamp sys.Timeval - Timecode Timecode - Sequence uint32 - Memory uint32 - Info BufferInfo // union m - Length uint32 - Reserved2 uint32 - RequestFD int32 + Index uint32 + Type uint32 + BytesUsed uint32 + Flags uint32 + Field uint32 + Timestamp sys.Timeval + Timecode Timecode + Sequence uint32 + Memory uint32 + Info BufferInfo // union m + Length uint32 + Reserved2 uint32 + RequestFD int32 } // makeBuffer makes a Buffer value from C.struct_v4l2_buffer func makeBuffer(v4l2Buf C.struct_v4l2_buffer) Buffer { return Buffer{ - Index: uint32(v4l2Buf.index), - StreamType: uint32(v4l2Buf._type), - BytesUsed: uint32(v4l2Buf.bytesused), - Flags: uint32(v4l2Buf.flags), - Field: uint32(v4l2Buf.field), - Timestamp: *(*sys.Timeval)(unsafe.Pointer(&v4l2Buf.timestamp)), - Timecode: *(*Timecode)(unsafe.Pointer(&v4l2Buf.timecode)), - Sequence: uint32(v4l2Buf.sequence), - Memory: uint32(v4l2Buf.memory), - Info: *(*BufferInfo)(unsafe.Pointer(&v4l2Buf.m[0])), - Length: uint32(v4l2Buf.length), - Reserved2: uint32(v4l2Buf.reserved2), - RequestFD: *(*int32)(unsafe.Pointer(&v4l2Buf.anon0[0])), + Index: uint32(v4l2Buf.index), + Type: uint32(v4l2Buf._type), + BytesUsed: uint32(v4l2Buf.bytesused), + Flags: uint32(v4l2Buf.flags), + Field: uint32(v4l2Buf.field), + Timestamp: *(*sys.Timeval)(unsafe.Pointer(&v4l2Buf.timestamp)), + Timecode: *(*Timecode)(unsafe.Pointer(&v4l2Buf.timecode)), + Sequence: uint32(v4l2Buf.sequence), + Memory: uint32(v4l2Buf.memory), + Info: *(*BufferInfo)(unsafe.Pointer(&v4l2Buf.m[0])), + Length: uint32(v4l2Buf.length), + Reserved2: uint32(v4l2Buf.reserved2), + RequestFD: *(*int32)(unsafe.Pointer(&v4l2Buf.anon0[0])), } } @@ -125,9 +123,9 @@ type PlaneInfo struct { // StreamOn requests streaming to be turned on for // capture (or output) that uses memory map, user ptr, or DMA buffers. // https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-streamon.html -func StreamOn(fd uintptr) error { - bufType := BufTypeVideoCapture - if err := send(fd, C.VIDIOC_STREAMON, uintptr(unsafe.Pointer(&bufType))); err != nil { +func StreamOn(dev StreamingDevice) error { + bufType := dev.BufferType() + if err := send(dev.Fd(), C.VIDIOC_STREAMON, uintptr(unsafe.Pointer(&bufType))); err != nil { return fmt.Errorf("stream on: %w", err) } return nil @@ -136,50 +134,50 @@ func StreamOn(fd uintptr) error { // StreamOff requests streaming to be turned off for // capture (or output) that uses memory map, user ptr, or DMA buffers. // https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-streamon.html -func StreamOff(fd uintptr) error { - bufType := BufTypeVideoCapture - if err := send(fd, C.VIDIOC_STREAMOFF, uintptr(unsafe.Pointer(&bufType))); err != nil { +func StreamOff(dev StreamingDevice) error { + bufType := dev.BufferType() + if err := send(dev.Fd(), C.VIDIOC_STREAMOFF, uintptr(unsafe.Pointer(&bufType))); err != nil { return fmt.Errorf("stream off: %w", err) } return nil } // InitBuffers sends buffer allocation request to initialize buffer IO -// for video capture when using either mem map, user pointer, or DMA buffers. +// for video capture or video output when using either mem map, user pointer, or DMA buffers. // See https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-reqbufs.html#vidioc-reqbufs -func InitBuffers(fd uintptr, buffSize uint32) (RequestBuffers, error) { +func InitBuffers(dev StreamingDevice) (RequestBuffers, error) { + if dev.MemIOType() != IOTypeMMAP && dev.MemIOType() != IOTypeDMABuf { + return RequestBuffers{}, fmt.Errorf("request buffers: %w", ErrorUnsupported) + } var req C.struct_v4l2_requestbuffers - req.count = C.uint(buffSize) - req._type = C.uint(BufTypeVideoCapture) - req.memory = C.uint(StreamTypeMMAP) + req.count = C.uint(dev.BufferCount()) + req._type = C.uint(dev.BufferType()) + req.memory = C.uint(dev.MemIOType()) - if err := send(fd, C.VIDIOC_REQBUFS, uintptr(unsafe.Pointer(&req))); err != nil { + if err := send(dev.Fd(), C.VIDIOC_REQBUFS, uintptr(unsafe.Pointer(&req))); err != nil { return RequestBuffers{}, fmt.Errorf("request buffers: %w", err) } - if req.count < 2 { - return RequestBuffers{}, errors.New("request buffers: insufficient memory on device") - } return *(*RequestBuffers)(unsafe.Pointer(&req)), nil } -// GetBuffer retrieves bunffer info for allocated buffers at provided index. -// This call should take place after buffers are allocated (for mmap for instance). -func GetBuffer(fd uintptr, index uint32) (Buffer, error) { +// GetBuffer retrieves buffer info for allocated buffers at provided index. +// This call should take place after buffers are allocated with RequestBuffers (for mmap for instance). +func GetBuffer(dev StreamingDevice, index uint32) (Buffer, error) { var v4l2Buf C.struct_v4l2_buffer - v4l2Buf._type = C.uint(BufTypeVideoCapture) - v4l2Buf.memory = C.uint(StreamTypeMMAP) + v4l2Buf._type = C.uint(dev.BufferType()) + v4l2Buf.memory = C.uint(dev.MemIOType()) v4l2Buf.index = C.uint(index) - if err := send(fd, C.VIDIOC_QUERYBUF, uintptr(unsafe.Pointer(&v4l2Buf))); err != nil { + if err := send(dev.Fd(), C.VIDIOC_QUERYBUF, uintptr(unsafe.Pointer(&v4l2Buf))); err != nil { return Buffer{}, fmt.Errorf("query buffer: %w", err) } return makeBuffer(v4l2Buf), nil } -// MapMemoryBuffer creates a local buffer mapped to the address space of the device specified by fd. -func MapMemoryBuffer(fd uintptr, offset int64, len int) ([]byte, error) { +// mapMemoryBuffer creates a local buffer mapped to the address space of the device specified by fd. +func mapMemoryBuffer(fd uintptr, offset int64, len int) ([]byte, error) { data, err := sys.Mmap(int(fd), offset, len, sys.PROT_READ|sys.PROT_WRITE, sys.MAP_SHARED) if err != nil { return nil, fmt.Errorf("map memory buffer: %w", err) @@ -187,22 +185,58 @@ func MapMemoryBuffer(fd uintptr, offset int64, len int) ([]byte, error) { return data, nil } -// UnmapMemoryBuffer removes the buffer that was previously mapped. -func UnmapMemoryBuffer(buf []byte) error { +// MapMemoryBuffers creates mapped memory buffers for specified buffer count of device. +func MapMemoryBuffers(dev StreamingDevice) ([][]byte, error) { + bufCount := int(dev.BufferCount()) + buffers := make([][]byte, bufCount) + for i := 0; i < bufCount; i++ { + buffer, err := GetBuffer(dev, uint32(i)) + if err != nil { + return nil, fmt.Errorf("mapped buffers: %w", err) + } + + // TODO check buffer flags for errors etc + + offset := buffer.Info.Offset + length := buffer.Length + mappedBuf, err := mapMemoryBuffer(dev.Fd(), int64(offset), int(length)) + if err != nil { + return nil, fmt.Errorf("mapped buffers: %w", err) + } + buffers[i] = mappedBuf + } + return buffers, nil +} + +// unmapMemoryBuffer removes the buffer that was previously mapped. +func unmapMemoryBuffer(buf []byte) error { if err := sys.Munmap(buf); err != nil { return fmt.Errorf("unmap memory buffer: %w", err) } return nil } +// UnmapMemoryBuffers unmaps all mapped memory buffer for device +func UnmapMemoryBuffers(dev StreamingDevice) error { + if dev.Buffers() == nil { + return fmt.Errorf("unmap buffers: uninitialized buffers") + } + for i := 0; i < len(dev.Buffers()); i++ { + if err := unmapMemoryBuffer(dev.Buffers()[i]); err != nil { + return fmt.Errorf("unmap buffers: %w", err) + } + } + return nil +} + // QueueBuffer enqueues a buffer in the device driver (as empty for capturing, or filled for video output) // when using either memory map, user pointer, or DMA buffers. Buffer is returned with // additional information about the queued buffer. // https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-qbuf.html#vidioc-qbuf -func QueueBuffer(fd uintptr, index uint32) (Buffer, error) { +func QueueBuffer(fd uintptr, ioType IOType, bufType BufType, index uint32) (Buffer, error) { var v4l2Buf C.struct_v4l2_buffer - v4l2Buf._type = C.uint(BufTypeVideoCapture) - v4l2Buf.memory = C.uint(StreamTypeMMAP) + v4l2Buf._type = C.uint(bufType) + v4l2Buf.memory = C.uint(ioType) v4l2Buf.index = C.uint(index) if err := send(fd, C.VIDIOC_QBUF, uintptr(unsafe.Pointer(&v4l2Buf))); err != nil { @@ -216,10 +250,10 @@ func QueueBuffer(fd uintptr, index uint32) (Buffer, error) { // when using either memory map, user pointer, or DMA buffers. Buffer is returned with // additional information about the dequeued buffer. // https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-qbuf.html#vidioc-qbuf -func DequeueBuffer(fd uintptr) (Buffer, error) { +func DequeueBuffer(fd uintptr, ioType IOType, bufType BufType) (Buffer, error) { var v4l2Buf C.struct_v4l2_buffer - v4l2Buf._type = C.uint(BufTypeVideoCapture) - v4l2Buf.memory = C.uint(StreamTypeMMAP) + v4l2Buf._type = C.uint(bufType) + v4l2Buf.memory = C.uint(ioType) if err := send(fd, C.VIDIOC_DQBUF, uintptr(unsafe.Pointer(&v4l2Buf))); err != nil { return Buffer{}, fmt.Errorf("buffer dequeue: %w", err) @@ -229,24 +263,18 @@ func DequeueBuffer(fd uintptr) (Buffer, error) { return makeBuffer(v4l2Buf), nil } -// WaitForDeviceRead blocks until the specified device is -// ready to be read or has timedout. -func WaitForDeviceRead(fd uintptr, timeout time.Duration) error { - timeval := sys.NsecToTimeval(timeout.Nanoseconds()) - var fdsRead sys.FdSet - fdsRead.Set(int(fd)) - for { - n, err := sys.Select(int(fd+1), &fdsRead, nil, nil, &timeval) - switch n { - case -1: - if err == sys.EINTR { - continue - } - return err - case 0: - return errors.New("wait for device ready: timeout") - default: - return nil - } +// CaptureBuffer captures a frame buffer from the device +func CaptureBuffer(fd uintptr, ioType IOType, bufType BufType) (Buffer, error) { + bufInfo, err := DequeueBuffer(fd, ioType, bufType) + if err != nil { + return Buffer{}, fmt.Errorf("capture frame: dequeue: %w", err) } + + // requeue/clear used buffer, prepare for next read + if _, err := QueueBuffer(fd, ioType, bufType, bufInfo.Index); err != nil { + return Buffer{}, fmt.Errorf("capture frame: queue: %w", err) + } + + // return captured buffer + return bufInfo, nil } diff --git a/v4l2/streaming_loop.go b/v4l2/streaming_loop.go new file mode 100644 index 0000000..691f615 --- /dev/null +++ b/v4l2/streaming_loop.go @@ -0,0 +1,28 @@ +package v4l2 + +import ( + sys "golang.org/x/sys/unix" +) + +func WaitForRead(dev Device) <-chan struct{} { + sigChan := make(chan struct{}) + + fd := dev.Fd() + + go func() { + defer close(sigChan) + var fdsRead sys.FdSet + fdsRead.Set(int(fd)) + for { + n, err := sys.Select(int(fd+1), &fdsRead, nil, nil, nil) + if n == -1 { + if err == sys.EINTR { + continue + } + } + sigChan <- struct{}{} + } + }() + + return sigChan +} diff --git a/v4l2/types.go b/v4l2/types.go index 10fcd3e..4beb440 100644 --- a/v4l2/types.go +++ b/v4l2/types.go @@ -1,30 +1,39 @@ package v4l2 import ( - "fmt" + "context" ) -type VersionInfo struct { - value uint32 +// Device is the base interface for a v4l2 device +type Device interface { + Name() string + Fd() uintptr + Capability() Capability + MemIOType() IOType + GetOutput() <-chan []byte + SetInput(<-chan []byte) + Close() error } -func (v VersionInfo) Major() uint32{ - return v.value >> 16 +// StreamingDevice represents device that supports streaming IO +// via mapped buffer sharing. +type StreamingDevice interface { + Device + Buffers() [][]byte + BufferType() BufType + BufferCount() uint32 + Start(context.Context) error + Stop() error } -func (v VersionInfo) Minor() uint32{ - return (v.value>>8)&0xff -} - -func (v VersionInfo) Patch() uint32{ - return v.value&0xff -} - -// Value returns the raw numeric version value -func (v VersionInfo) Value() uint32 { - return v.value -} - -func (v VersionInfo) String() string { - return fmt.Sprintf("v%d.%d.%d", v.Major(), v.Minor(), v.Patch()) -} \ No newline at end of file +//// CaptureDevice represents a device that captures video from an underlying device +//type CaptureDevice interface { +// StreamingDevice +// StartCapture(context.Context) (<-chan []byte, error) +//} +// +//// OutputDevice represents a device that can output video data to an underlying device driver +//type OutputDevice interface { +// StreamingDevice +// StartOutput(context.Context, chan<- []byte) error +//} diff --git a/v4l2/version.go b/v4l2/version.go new file mode 100644 index 0000000..5fa7a79 --- /dev/null +++ b/v4l2/version.go @@ -0,0 +1,30 @@ +package v4l2 + +import ( + "fmt" +) + +type VersionInfo struct { + value uint32 +} + +func (v VersionInfo) Major() uint32 { + return v.value >> 16 +} + +func (v VersionInfo) Minor() uint32 { + return (v.value >> 8) & 0xff +} + +func (v VersionInfo) Patch() uint32 { + return v.value & 0xff +} + +// Value returns the raw numeric version value +func (v VersionInfo) Value() uint32 { + return v.value +} + +func (v VersionInfo) String() string { + return fmt.Sprintf("v%d.%d.%d", v.Major(), v.Minor(), v.Patch()) +} diff --git a/v4l2/video_info.go b/v4l2/video_info.go index 0aaab64..098ffc7 100644 --- a/v4l2/video_info.go +++ b/v4l2/video_info.go @@ -17,7 +17,7 @@ type InputStatus = uint32 var ( InputStatusNoPower InputStatus = C.V4L2_IN_ST_NO_POWER InputStatusNoSignal InputStatus = C.V4L2_IN_ST_NO_SIGNAL - InputStatusNoColor InputStatus = C.V4L2_IN_ST_NO_COLOR + InputStatusNoColor InputStatus = C.V4L2_IN_ST_NO_COLOR ) var InputStatuses = map[InputStatus]string{