Browse Source

Merge pull request #18 from vladimirvivien/list-devices

Major refactor to simplify API surface and consolidates C-backed types.
Vladimir Vivien 3 years ago
parent
commit
4f52a3b513

+ 43 - 53
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.

+ 412 - 0
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
+}

+ 51 - 0
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
+	}
+}

+ 2 - 0
device/doc.go

@@ -0,0 +1,2 @@
+// Package device provides a device abstraction that supports video streaming.
+package device

+ 3 - 3
v4l2/device/list.go → 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
-}
+	return result, nil
+}

+ 1 - 1
v4l2/device/list_test.go → 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)

+ 6 - 0
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

+ 58 - 0
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).

+ 64 - 0
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.")
+
+}

+ 119 - 0
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).

+ 21 - 22
examples/capture/capture.go → 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)
 	}

+ 3 - 3
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`

+ 40 - 0
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.

+ 76 - 48
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
-}
+}

+ 30 - 23
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)
-}
+	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)
+}

+ 165 - 0
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)

BIN
examples/webcam/screenshot.png


+ 32 - 47
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
-}
+	return v4l2.PixelFmtMPEG
+}

+ 4 - 0
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)
 

+ 19 - 11
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

+ 44 - 0
v4l2/control.go

@@ -0,0 +1,44 @@
+package v4l2
+
+//#include <linux/videodev2.h>
+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
+}

+ 97 - 0
v4l2/controls.go

@@ -0,0 +1,97 @@
+package v4l2
+
+//#include <linux/videodev2.h>
+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
+}

+ 15 - 0
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
+}

+ 135 - 0
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
+}

+ 34 - 0
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
+}

+ 94 - 0
v4l2/controls_vp8.go

@@ -0,0 +1,94 @@
+package v4l2
+
+//#include <linux/v4l2-controls.h>
+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
+}

+ 2 - 20
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)

+ 0 - 314
v4l2/device/device.go

@@ -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
-}

+ 26 - 0
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
+}

+ 9 - 8
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

+ 1 - 1
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

+ 86 - 0
v4l2/format_frameintervals.go

@@ -0,0 +1,86 @@
+package v4l2
+
+//#include <linux/videodev2.h>
+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)
+}

+ 32 - 26
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)

+ 55 - 9
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(&param))); 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(&param.Capture))
 	}
-	return *(*CaptureParam)(unsafe.Pointer(&param.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(&param.Output))
+	}
+
+	if err := send(fd, C.VIDIOC_S_PARM, uintptr(unsafe.Pointer(&v4l2Parm))); err != nil {
+		return fmt.Errorf("stream param: %w", err)
+	}
+
+	return nil
 }

+ 112 - 84
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
 }

+ 28 - 0
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
+}

+ 30 - 21
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())
-}
+//// 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
+//}

+ 30 - 0
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())
+}

+ 1 - 1
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{