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
 # go4vl
+
 A Go library for the `Video for Linux 2`  (v4l2) user API.
 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.
 > It is *NOT* meant to be a portable/cross-platform capable package for real-time video processing.
 
 
 ## Features
 ## Features
+
 * Capture and control video data from your Go programs
 * 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
 * Exposes device enumeration and information
 * Provides device capture control
 * Provides device capture control
 * Access to video format information
 * 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
 * Go compiler/tools
-* Linux OS (32- or 64-bit)
 * Kernel minimum v5.10.x
 * Kernel minimum v5.10.x
 * A locally configured C compiler (i.e. gcc)
 * A locally configured C compiler (i.e. gcc)
 * Header files for V4L2 (i.e. /usr/include/linux/videodev2.h)
 * 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.
 The package should work with no problem on your 64-bit Linux OS.
 
 
 ## Getting started
 ## Getting started
+
+### System upgrade
+
 To avoid issues with old header files on your machine, upgrade your system to pull down the latest OS packages
 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
 ```shell
 sudo apt update
 sudo apt update
 sudo apt full-upgrade
 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
 ```bash
 go get github.com/vladimirvivien/go4vl/v4l2
 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() {
 func main() {
+	devName := "/dev/video0"
+	flag.StringVar(&devName, "d", devName, "device name (path)")
+	flag.Parse()
+
 	// open device
 	// 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 {
 	if err != nil {
 		log.Fatalf("failed to open device: %s", err)
 		log.Fatalf("failed to open device: %s", err)
 	}
 	}
 	defer device.Close()
 	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)
 		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
 	totalFrames := 10
 	count := 0
 	count := 0
-	for frame := range frameChan {
+	log.Printf("Capturing %d frames...", totalFrames)
+
+	for frame := range device.GetOutput() {
 		fileName := fmt.Sprintf("capture_%d.jpg", count)
 		fileName := fmt.Sprintf("capture_%d.jpg", count)
 		file, err := os.Create(fileName)
 		file, err := os.Create(fileName)
 		if err != nil {
 		if err != nil {
@@ -105,6 +96,7 @@ func main() {
 			log.Printf("failed to write file %s: %s", fileName, err)
 			log.Printf("failed to write file %s: %s", fileName, err)
 			continue
 			continue
 		}
 		}
+		log.Printf("Saved file: %s", fileName)
 		if err := file.Close(); err != nil {
 		if err := file.Close(); err != nil {
 			log.Printf("failed to close file %s: %s", fileName, err)
 			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.")
 	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
 ## 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.
 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"
 	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))
 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
 // 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)
 			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"
 	"testing"
 )
 )
 
 
-func TestList(t *testing.T){
+func TestList(t *testing.T) {
 	devices, err := GetAllDevicePaths()
 	devices, err := GetAllDevicePaths()
 	if err != nil {
 	if err != nil {
 		t.Error(err)
 		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"
 	"log"
 	"os"
 	"os"
 
 
+	"github.com/vladimirvivien/go4vl/device"
 	"github.com/vladimirvivien/go4vl/v4l2"
 	"github.com/vladimirvivien/go4vl/v4l2"
-	"github.com/vladimirvivien/go4vl/v4l2/device"
 )
 )
 
 
 func main() {
 func main() {
@@ -23,10 +23,15 @@ func main() {
 	}
 	}
 	defer device.Close()
 	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
 	// helper function to search for format descriptions
 	findPreferredFmt := func(fmts []v4l2.FormatDescription, pixEncoding v4l2.FourCCType) *v4l2.FormatDescription {
 	findPreferredFmt := func(fmts []v4l2.FormatDescription, pixEncoding v4l2.FourCCType) *v4l2.FormatDescription {
 		for _, desc := range fmts {
 		for _, desc := range fmts {
-			if desc.PixelFormat == pixEncoding{
+			if desc.PixelFormat == pixEncoding {
 				return &desc
 				return &desc
 			}
 			}
 		}
 		}
@@ -35,14 +40,14 @@ func main() {
 
 
 	// get supported format descriptions
 	// get supported format descriptions
 	fmtDescs, err := device.GetFormatDescriptions()
 	fmtDescs, err := device.GetFormatDescriptions()
-	if err != nil{
+	if err != nil {
 		log.Fatal("failed to get format desc:", err)
 		log.Fatal("failed to get format desc:", err)
 	}
 	}
 
 
 	// search for preferred formats
 	// search for preferred formats
 	preferredFmts := []v4l2.FourCCType{v4l2.PixelFmtMPEG, v4l2.PixelFmtMJPEG, v4l2.PixelFmtJPEG, v4l2.PixelFmtYUYV}
 	preferredFmts := []v4l2.FourCCType{v4l2.PixelFmtMPEG, v4l2.PixelFmtMJPEG, v4l2.PixelFmtJPEG, v4l2.PixelFmtYUYV}
 	var fmtDesc *v4l2.FormatDescription
 	var fmtDesc *v4l2.FormatDescription
-	for _, preferredFmt := range preferredFmts{
+	for _, preferredFmt := range preferredFmts {
 		fmtDesc = findPreferredFmt(fmtDescs, preferredFmt)
 		fmtDesc = findPreferredFmt(fmtDescs, preferredFmt)
 		if fmtDesc != nil {
 		if fmtDesc != nil {
 			break
 			break
@@ -54,29 +59,29 @@ func main() {
 		log.Fatalf("device does not support any of %#v", preferredFmts)
 		log.Fatalf("device does not support any of %#v", preferredFmts)
 	}
 	}
 	log.Printf("Found preferred fmt: %s", fmtDesc)
 	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)
 		log.Fatalf("failed to get framesize info: %s", err)
 	}
 	}
 
 
 	// select size 640x480 for format
 	// select size 640x480 for format
-	var frmSize v4l2.FrameSize
+	var frmSize v4l2.FrameSizeEnum
 	for _, size := range frameSizes {
 	for _, size := range frameSizes {
-		if size.Width == 640 && size.Height == 480 {
+		if size.Size.MinWidth == 640 && size.Size.MinHeight == 480 {
 			frmSize = size
 			frmSize = size
 			break
 			break
 		}
 		}
 	}
 	}
 
 
-	if frmSize.Width == 0 {
+	if frmSize.Size.MinWidth == 0 {
 		log.Fatalf("Size 640x480 not supported for fmt: %s", fmtDesc)
 		log.Fatalf("Size 640x480 not supported for fmt: %s", fmtDesc)
 	}
 	}
 
 
 	// configure device with preferred fmt
 	// configure device with preferred fmt
 
 
 	if err := device.SetPixFormat(v4l2.PixFormat{
 	if err := device.SetPixFormat(v4l2.PixFormat{
-		Width:       frmSize.Width,
-		Height:      frmSize.Height,
+		Width:       frmSize.Size.MinWidth,
+		Height:      frmSize.Size.MinHeight,
 		PixelFormat: fmtDesc.PixelFormat,
 		PixelFormat: fmtDesc.PixelFormat,
 		Field:       v4l2.FieldNone,
 		Field:       v4l2.FieldNone,
 	}); err != nil {
 	}); err != nil {
@@ -90,22 +95,16 @@ func main() {
 	log.Printf("Pixel format set to [%s]", pixFmt)
 	log.Printf("Pixel format set to [%s]", pixFmt)
 
 
 	// start stream
 	// 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())
 	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
 	// process frames from capture channel
 	totalFrames := 10
 	totalFrames := 10
 	count := 0
 	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)
 		fileName := fmt.Sprintf("capture_%d.jpg", count)
 		file, err := os.Create(fileName)
 		file, err := os.Create(fileName)
 		if err != nil {
 		if err != nil {
@@ -127,7 +126,7 @@ func main() {
 	}
 	}
 
 
 	cancel() // stop capture
 	cancel() // stop capture
-	if err := device.StopStream(); err != nil {
+	if err := device.Stop(); err != nil {
 		fmt.Println(err)
 		fmt.Println(err)
 		os.Exit(1)
 		os.Exit(1)
 	}
 	}

+ 3 - 3
examples/cgo_types/cgo_capture.go

@@ -78,7 +78,7 @@ func setFormat(fd uintptr, pixFmt PixFormat) error {
 	return nil
 	return nil
 }
 }
 
 
-func getFormat(fd uintptr) (PixFormat, error){
+func getFormat(fd uintptr) (PixFormat, error) {
 	var v4l2Fmt C.struct_v4l2_format
 	var v4l2Fmt C.struct_v4l2_format
 	v4l2Fmt._type = C.uint(BufTypeVideoCapture)
 	v4l2Fmt._type = C.uint(BufTypeVideoCapture)
 
 
@@ -88,7 +88,7 @@ func getFormat(fd uintptr) (PixFormat, error){
 	}
 	}
 
 
 	var pixFmt PixFormat
 	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
 	return pixFmt, nil
 
 
@@ -99,7 +99,7 @@ func getFormat(fd uintptr) (PixFormat, error){
 // Memory buffer types
 // Memory buffer types
 // https://elixir.bootlin.com/linux/v5.13-rc6/source/include/uapi/linux/videodev2.h#L188
 // https://elixir.bootlin.com/linux/v5.13-rc6/source/include/uapi/linux/videodev2.h#L188
 const (
 const (
-	StreamMemoryTypeMMAP    uint32 = C.V4L2_MEMORY_MMAP
+	StreamMemoryTypeMMAP uint32 = C.V4L2_MEMORY_MMAP
 )
 )
 
 
 // reqBuffers requests that the device allocates a `count`
 // 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"
 	"os"
 	"strings"
 	"strings"
 
 
+	device2 "github.com/vladimirvivien/go4vl/device"
 	"github.com/vladimirvivien/go4vl/v4l2"
 	"github.com/vladimirvivien/go4vl/v4l2"
-	"github.com/vladimirvivien/go4vl/v4l2/device"
 )
 )
 
 
 var template = "\t%-24s : %s\n"
 var template = "\t%-24s : %s\n"
@@ -21,13 +21,13 @@ func main() {
 	flag.Parse()
 	flag.Parse()
 
 
 	if devList {
 	if devList {
-		if err := listDevices(); err != nil{
+		if err := listDevices(); err != nil {
 			log.Fatal(err)
 			log.Fatal(err)
 		}
 		}
 		os.Exit(0)
 		os.Exit(0)
 	}
 	}
 
 
-	device, err := device.Open(devName)
+	device, err := device2.Open(devName)
 	if err != nil {
 	if err != nil {
 		log.Fatal(err)
 		log.Fatal(err)
 	}
 	}
@@ -49,40 +49,48 @@ func main() {
 		log.Fatal(err)
 		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 {
 func listDevices() error {
-	paths, err := device.GetAllDevicePaths()
+	paths, err := device2.GetAllDevicePaths()
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 	for _, path := range paths {
 	for _, path := range paths {
-		dev, err := device.Open(path)
+		dev, err := device2.Open(path)
 		if err != nil {
 		if err != nil {
 			log.Print(err)
 			log.Print(err)
 			continue
 			continue
 		}
 		}
 
 
 		var busInfo, card string
 		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
 			busInfo = cap.BusInfo
 			card = cap.Card
 			card = cap.Card
 		}
 		}
@@ -93,21 +101,17 @@ func listDevices() error {
 			continue
 			continue
 		}
 		}
 
 
-		fmt.Printf("Device [%s]: %s: %s\n", path, card, busInfo)
-
+		fmt.Printf("v4l2Device [%s]: %s: %s\n", path, card, busInfo)
 
 
 	}
 	}
 	return nil
 	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
 	// print driver info
-	fmt.Println("Device Info:")
+	fmt.Println("v4l2Device Info:")
 	fmt.Printf(template, "Driver name", caps.Driver)
 	fmt.Printf(template, "Driver name", caps.Driver)
 	fmt.Printf(template, "Card name", caps.Card)
 	fmt.Printf(template, "Card name", caps.Card)
 	fmt.Printf(template, "Bus info", caps.BusInfo)
 	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\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() {
 	for _, desc := range caps.GetDeviceCapDescriptions() {
 		fmt.Printf("\t\t%s\n", desc.Desc)
 		fmt.Printf("\t\t%s\n", desc.Desc)
 	}
 	}
@@ -127,7 +131,7 @@ func printDeviceDriverInfo(dev *device.Device) error {
 	return nil
 	return nil
 }
 }
 
 
-func printVideoInputInfo(dev *device.Device) error {
+func printVideoInputInfo(dev *device2.Device) error {
 	// first get current input
 	// first get current input
 	index, err := dev.GetVideoInputIndex()
 	index, err := dev.GetVideoInputIndex()
 	if err != nil {
 	if err != nil {
@@ -148,7 +152,7 @@ func printVideoInputInfo(dev *device.Device) error {
 	return nil
 	return nil
 }
 }
 
 
-func printFormatInfo(dev *device.Device) error {
+func printFormatInfo(dev *device2.Device) error {
 	pixFmt, err := dev.GetPixFormat()
 	pixFmt, err := dev.GetPixFormat()
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("video capture format: %w", err)
 		return fmt.Errorf("video capture format: %w", err)
@@ -190,28 +194,28 @@ func printFormatInfo(dev *device.Device) error {
 	return printFormatDesc(dev)
 	return printFormatDesc(dev)
 }
 }
 
 
-func printFormatDesc(dev *device.Device) error {
+func printFormatDesc(dev *device2.Device) error {
 	descs, err := dev.GetFormatDescriptions()
 	descs, err := dev.GetFormatDescriptions()
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("format desc: %w", err)
 		return fmt.Errorf("format desc: %w", err)
 	}
 	}
 	fmt.Println("Supported formats:")
 	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 {
 		if err != nil {
 			return fmt.Errorf("format desc: %w", err)
 			return fmt.Errorf("format desc: %w", err)
 		}
 		}
 		var sizeStr strings.Builder
 		var sizeStr strings.Builder
 		sizeStr.WriteString("Sizes: ")
 		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())
 		fmt.Printf(template, fmt.Sprintf("[%0d] %s", i, desc.Description), sizeStr.String())
 	}
 	}
 	return nil
 	return nil
 }
 }
 
 
-func printCropInfo(dev *device.Device) error {
+func printCropInfo(dev *device2.Device) error {
 	crop, err := dev.GetCropCapability()
 	crop, err := dev.GetCropCapability()
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("crop capability: %w", err)
 		return fmt.Errorf("crop capability: %w", err)
@@ -238,26 +242,50 @@ func printCropInfo(dev *device.Device) error {
 	return nil
 	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 {
 	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"
 	tpf := "not specified"
-	if params.Capability == v4l2.StreamParamTimePerFrame {
+	if params.Capture.Capability == v4l2.StreamParamTimePerFrame {
 		tpf = "time per frame"
 		tpf = "time per frame"
 	}
 	}
 	fmt.Printf(template, "Capability", tpf)
 	fmt.Printf(template, "Capability", tpf)
 
 
 	hiqual := "not specified"
 	hiqual := "not specified"
-	if params.CaptureMode == v4l2.StreamParamModeHighQuality {
+	if params.Capture.CaptureMode == v4l2.StreamParamModeHighQuality {
 		hiqual = "high quality"
 		hiqual = "high quality"
 	}
 	}
 	fmt.Printf(template, "Capture mode", hiqual)
 	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
 	return nil
-}
+}

+ 30 - 23
examples/format/devfmt.go

@@ -5,8 +5,8 @@ import (
 	"log"
 	"log"
 	"strings"
 	"strings"
 
 
+	device2 "github.com/vladimirvivien/go4vl/device"
 	"github.com/vladimirvivien/go4vl/v4l2"
 	"github.com/vladimirvivien/go4vl/v4l2"
-	"github.com/vladimirvivien/go4vl/v4l2/device"
 )
 )
 
 
 func main() {
 func main() {
@@ -21,18 +21,6 @@ func main() {
 	flag.StringVar(&format, "f", format, "pixel format")
 	flag.StringVar(&format, "f", format, "pixel format")
 	flag.Parse()
 	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
 	fmtEnc := v4l2.PixelFmtYUYV
 	switch strings.ToLower(format) {
 	switch strings.ToLower(format) {
 	case "mjpeg":
 	case "mjpeg":
@@ -43,18 +31,37 @@ func main() {
 		fmtEnc = v4l2.PixelFmtYUYV
 		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 {
 	if err != nil {
 		log.Fatalf("unable to get format: %s", err)
 		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"
 	"strings"
 	"time"
 	"time"
 
 
-	"github.com/vladimirvivien/go4vl/imgsupport"
+	"github.com/vladimirvivien/go4vl/device"
 	"github.com/vladimirvivien/go4vl/v4l2"
 	"github.com/vladimirvivien/go4vl/v4l2"
-	"github.com/vladimirvivien/go4vl/v4l2/device"
 )
 )
 
 
 var (
 var (
@@ -54,6 +53,11 @@ func serveVideoStream(w http.ResponseWriter, req *http.Request) {
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 
 
 	for frame := range frames {
 	for frame := range frames {
+		if len(frame) == 0 {
+			log.Print("skipping empty frame")
+			continue
+		}
+
 		// start boundary
 		// start boundary
 		io.WriteString(w, fmt.Sprintf("--%s\n", boundaryName))
 		io.WriteString(w, fmt.Sprintf("--%s\n", boundaryName))
 		io.WriteString(w, "Content-Type: image/jpeg\n")
 		io.WriteString(w, "Content-Type: image/jpeg\n")
@@ -63,23 +67,16 @@ func serveVideoStream(w http.ResponseWriter, req *http.Request) {
 		switch pixfmt {
 		switch pixfmt {
 		case v4l2.PixelFmtMJPEG:
 		case v4l2.PixelFmtMJPEG:
 			if _, err := w.Write(frame); err != nil {
 			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
 				return
 			}
 			}
+		default:
+			log.Printf("selected pixel format is not supported")
 		}
 		}
+
 		// close boundary
 		// close boundary
 		if _, err := io.WriteString(w, "\n"); err != nil {
 		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
 			return
 		}
 		}
 	}
 	}
@@ -88,6 +85,7 @@ func serveVideoStream(w http.ResponseWriter, req *http.Request) {
 func main() {
 func main() {
 	port := ":9090"
 	port := ":9090"
 	devName := "/dev/video0"
 	devName := "/dev/video0"
+	frameRate := int(fps)
 	defaultDev, err := device.Open(devName)
 	defaultDev, err := device.Open(devName)
 	skipDefault := false
 	skipDefault := false
 	if err != nil {
 	if err != nil {
@@ -118,23 +116,26 @@ func main() {
 	flag.IntVar(&height, "h", height, "capture height")
 	flag.IntVar(&height, "h", height, "capture height")
 	flag.StringVar(&format, "f", format, "pixel format")
 	flag.StringVar(&format, "f", format, "pixel format")
 	flag.StringVar(&port, "p", port, "webcam service port")
 	flag.StringVar(&port, "p", port, "webcam service port")
+	flag.IntVar(&frameRate, "r", frameRate, "frames per second (fps)")
 	flag.Parse()
 	flag.Parse()
 
 
 	// close device used for default info
 	// close device used for default info
 	if err := defaultDev.Close(); err != nil {
 	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
 	// 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 {
 	if err != nil {
 		log.Fatalf("failed to open device: %s", err)
 		log.Fatalf("failed to open device: %s", err)
 	}
 	}
 	defer device.Close()
 	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 [%s] opened\n", devName)
 	log.Printf("device info: %s", caps.String())
 	log.Printf("device info: %s", caps.String())
 
 
@@ -144,34 +145,22 @@ func main() {
 		log.Fatalf("unable to get format: %s", err)
 		log.Fatalf("unable to get format: %s", err)
 	}
 	}
 	log.Printf("Current format: %s", currFmt)
 	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
 	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
 	// start capture
 	ctx, cancel := context.WithCancel(context.TODO())
 	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)
 		log.Fatalf("stream capture: %s", err)
 	}
 	}
 	defer func() {
 	defer func() {
 		cancel()
 		cancel()
 		device.Close()
 		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.Printf("starting server on port %s", port)
 	log.Println("use url path /webcam")
 	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) {
 	switch strings.ToLower(fmtStr) {
 	case "mjpeg", "jpeg":
 	case "mjpeg", "jpeg":
-		pix.PixelFormat = v4l2.PixelFmtMJPEG
+		return v4l2.PixelFmtMJPEG
 	case "h264", "h.264":
 	case "h264", "h.264":
-		pix.PixelFormat = v4l2.PixelFmtH264
+		return v4l2.PixelFmtH264
 	case "yuyv":
 	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 (
 import (
 	"bytes"
 	"bytes"
+	"fmt"
 	"image"
 	"image"
 	"image/jpeg"
 	"image/jpeg"
 )
 )
@@ -9,6 +10,9 @@ import (
 // Yuyv2Jpeg attempts to convert the YUYV image using Go's built-in
 // Yuyv2Jpeg attempts to convert the YUYV image using Go's built-in
 // YCbCr encoder
 // YCbCr encoder
 func Yuyv2Jpeg(width, height int, frame []byte) ([]byte, error) {
 func Yuyv2Jpeg(width, height int, frame []byte) ([]byte, error) {
+	if true {
+		return nil, fmt.Errorf("unsupported")
+	}
 	//size := len(frame)
 	//size := len(frame)
 	ycbr := image.NewYCbCr(image.Rect(0, 0, width, height), image.YCbCrSubsampleRatio422)
 	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
 	Card string
 
 
 	// BusInfo is the name of the device bus
 	// BusInfo is the name of the device bus
-	BusInfo string 
+	BusInfo string
 
 
 	// Version is the kernel version
 	// Version is the kernel version
 	Version uint32
 	Version uint32
@@ -116,7 +116,7 @@ type Capability struct {
 	Capabilities uint32
 	Capabilities uint32
 
 
 	// DeviceCapabilities is the capability for this particular (opened) device or node
 	// DeviceCapabilities is the capability for this particular (opened) device or node
-	DeviceCapabilities uint32 
+	DeviceCapabilities uint32
 }
 }
 
 
 // GetCapability retrieves capability info for device
 // GetCapability retrieves capability info for device
@@ -135,44 +135,52 @@ func GetCapability(fd uintptr) (Capability, error) {
 	}, nil
 	}, 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
 // IsVideoCaptureSupported returns caps & CapVideoCapture
 func (c Capability) IsVideoCaptureSupported() bool {
 func (c Capability) IsVideoCaptureSupported() bool {
-	return (uint32(c.Capabilities) & CapVideoCapture) != 0
+	return c.Capabilities&CapVideoCapture != 0
 }
 }
 
 
 // IsVideoOutputSupported returns caps & CapVideoOutput
 // IsVideoOutputSupported returns caps & CapVideoOutput
 func (c Capability) IsVideoOutputSupported() bool {
 func (c Capability) IsVideoOutputSupported() bool {
-	return (uint32(c.Capabilities) & CapVideoOutput) != 0
+	return c.Capabilities&CapVideoOutput != 0
 }
 }
 
 
 // IsVideoOverlaySupported returns caps & CapVideoOverlay
 // IsVideoOverlaySupported returns caps & CapVideoOverlay
 func (c Capability) IsVideoOverlaySupported() bool {
 func (c Capability) IsVideoOverlaySupported() bool {
-	return (uint32(c.Capabilities) & CapVideoOverlay) != 0
+	return c.Capabilities&CapVideoOverlay != 0
 }
 }
 
 
 // IsVideoOutputOverlaySupported returns caps & CapVideoOutputOverlay
 // IsVideoOutputOverlaySupported returns caps & CapVideoOutputOverlay
 func (c Capability) IsVideoOutputOverlaySupported() bool {
 func (c Capability) IsVideoOutputOverlaySupported() bool {
-	return (uint32(c.Capabilities) & CapVideoOutputOverlay) != 0
+	return c.Capabilities&CapVideoOutputOverlay != 0
 }
 }
 
 
 // IsVideoCaptureMultiplanarSupported returns caps & CapVideoCaptureMPlane
 // IsVideoCaptureMultiplanarSupported returns caps & CapVideoCaptureMPlane
 func (c Capability) IsVideoCaptureMultiplanarSupported() bool {
 func (c Capability) IsVideoCaptureMultiplanarSupported() bool {
-	return (uint32(c.Capabilities) & CapVideoCaptureMPlane) != 0
+	return c.Capabilities&CapVideoCaptureMPlane != 0
 }
 }
 
 
 // IsVideoOutputMultiplanerSupported returns caps & CapVideoOutputMPlane
 // IsVideoOutputMultiplanerSupported returns caps & CapVideoOutputMPlane
 func (c Capability) IsVideoOutputMultiplanerSupported() bool {
 func (c Capability) IsVideoOutputMultiplanerSupported() bool {
-	return (uint32(c.Capabilities) & CapVideoOutputMPlane) != 0
+	return c.Capabilities&CapVideoOutputMPlane != 0
 }
 }
 
 
 // IsReadWriteSupported returns caps & CapReadWrite
 // IsReadWriteSupported returns caps & CapReadWrite
 func (c Capability) IsReadWriteSupported() bool {
 func (c Capability) IsReadWriteSupported() bool {
-	return (uint32(c.Capabilities) & CapReadWrite) != 0
+	return c.Capabilities&CapReadWrite != 0
 }
 }
 
 
 // IsStreamingSupported returns caps & CapStreaming
 // IsStreamingSupported returns caps & CapStreaming
 func (c Capability) IsStreamingSupported() bool {
 func (c Capability) IsStreamingSupported() bool {
-	return (uint32(c.Capabilities) & CapStreaming) != 0
+	return c.Capabilities&CapStreaming != 0
 }
 }
 
 
 // IsDeviceCapabilitiesProvided returns true if the device returns
 // IsDeviceCapabilitiesProvided returns true if the device returns
@@ -180,7 +188,7 @@ func (c Capability) IsStreamingSupported() bool {
 // See notes on VL42_CAP_DEVICE_CAPS:
 // 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
 // https://linuxtv.org/downloads/v4l-dvb-apis/userspace-api/v4l/vidioc-querycap.html?highlight=v4l2_cap_device_caps
 func (c Capability) IsDeviceCapabilitiesProvided() bool {
 func (c Capability) IsDeviceCapabilitiesProvided() bool {
-	return (uint32(c.Capabilities) & CapDeviceCapabilities) != 0
+	return c.Capabilities&CapDeviceCapabilities != 0
 }
 }
 
 
 // GetDriverCapDescriptions return textual descriptions of driver capabilities
 // 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"
 	"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)
 // CropCapability (v4l2_cropcap)
 // https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-cropcap.html#c.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
 // 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
 // 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
 // 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
 	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 {
 	if err := send(fd, C.VIDIOC_CROPCAP, uintptr(unsafe.Pointer(&cap))); err != nil {
 		return CropCapability{}, fmt.Errorf("crop capability: %w", err)
 		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 (
 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 {
 func parseErrorType(errno sys.Errno) error {
 	switch errno {
 	switch errno {
 	case sys.EBADF, sys.ENOMEM, sys.ENODEV, sys.EIO, sys.ENXIO, sys.EFAULT: // structural, terminal
 	case sys.EBADF, sys.ENOMEM, sys.ENODEV, sys.EIO, sys.ENXIO, sys.EFAULT: // structural, terminal
-	return ErrorSystem
+		return ErrorSystem
 	case sys.EINVAL: // bad argument
 	case sys.EINVAL: // bad argument
-	return ErrorBadArgument
+		return ErrorBadArgument
 	case sys.ENOTTY: // unsupported
 	case sys.ENOTTY: // unsupported
-	return ErrorUnsupported
+		return ErrorUnsupported
 	default:
 	default:
 		if errno.Timeout() {
 		if errno.Timeout() {
 			return ErrorTimeout
 			return ErrorTimeout

+ 1 - 1
v4l2/format.go

@@ -301,7 +301,7 @@ func GetPixFormat(fd uintptr) (PixFormat, error) {
 		Priv:         uint32(v4l2PixFmt.priv),
 		Priv:         uint32(v4l2PixFmt.priv),
 		Flags:        uint32(v4l2PixFmt.flags),
 		Flags:        uint32(v4l2PixFmt.flags),
 		YcbcrEnc:     *(*uint32)(unsafe.Pointer(&v4l2PixFmt.anon0[0])),
 		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),
 		Quantization: uint32(v4l2PixFmt.quantization),
 		XferFunc:     uint32(v4l2PixFmt.xfer_func),
 		XferFunc:     uint32(v4l2PixFmt.xfer_func),
 	}, nil
 	}, 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
 	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.
 // Use FrameSizeType to determine which sizes the driver support.
 // https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L829
 // 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
 // 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
 	PixelFormat FourCCType
+	Size        FrameSize
 }
 }
 
 
 // FrameSizeDiscrete (v4l2_frmsize_discrete)
 // FrameSizeDiscrete (v4l2_frmsize_discrete)
@@ -35,9 +35,10 @@ type FrameSizeDiscrete struct {
 	Height uint32 // height [pixel]
 	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]
 	MinWidth   uint32 // Minimum frame width [pixel]
 	MaxWidth   uint32 // Maximum frame width [pixel]
 	MaxWidth   uint32 // Maximum frame width [pixel]
 	StepWidth  uint32 // Frame width step size [pixel]
 	StepWidth  uint32 // Frame width step size [pixel]
@@ -46,40 +47,45 @@ type FrameSizeStepwise struct {
 	StepHeight uint32 // Frame height step size [pixel]
 	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:
 	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:
 	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
 	return frameSize
 }
 }
 
 
 // GetFormatFrameSize returns a supported device frame size for a specified encoding at index
 // 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
 	var frmSizeEnum C.struct_v4l2_frmsizeenum
 	frmSizeEnum.index = C.uint(index)
 	frmSizeEnum.index = C.uint(index)
 	frmSizeEnum.pixel_format = C.uint(encoding)
 	frmSizeEnum.pixel_format = C.uint(encoding)
 
 
 	if err := send(fd, C.VIDIOC_ENUM_FRAMESIZES, uintptr(unsafe.Pointer(&frmSizeEnum))); err != nil {
 	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
 	return getFrameSize(frmSizeEnum), nil
 }
 }
 
 
 // GetFormatFrameSizes returns all supported device frame sizes for a specified encoding
 // 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)
 	index := uint32(0)
 	for {
 	for {
 		var frmSizeEnum C.struct_v4l2_frmsizeenum
 		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.
 // 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
 // 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.
 // 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)
 	formats, err := GetAllFormatDescriptions(fd)
 	if len(formats) == 0 && err != nil {
 	if len(formats) == 0 && err != nil {
 		return nil, fmt.Errorf("frame sizes: %w", err)
 		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
 	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
 // 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
 // See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L1205
 type CaptureParam struct {
 type CaptureParam struct {
@@ -30,16 +39,53 @@ type CaptureParam struct {
 	_            [4]uint32
 	_            [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
 // 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 "C"
 
 
 import (
 import (
-	"errors"
 	"fmt"
 	"fmt"
-	"time"
 	"unsafe"
 	"unsafe"
 
 
 	sys "golang.org/x/sys/unix"
 	sys "golang.org/x/sys/unix"
@@ -26,16 +24,16 @@ const (
 	BufTypeOverlay      BufType = C.V4L2_BUF_TYPE_VIDEO_OVERLAY
 	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://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
 // https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L188
-type StreamType = uint32
+type IOType = uint32
 
 
 const (
 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
 // 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
 // https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L1037
 //
 //
 type Buffer struct {
 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
 // makeBuffer makes a Buffer value from C.struct_v4l2_buffer
 func makeBuffer(v4l2Buf C.struct_v4l2_buffer) Buffer {
 func makeBuffer(v4l2Buf C.struct_v4l2_buffer) Buffer {
 	return 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
 // StreamOn requests streaming to be turned on for
 // capture (or output) that uses memory map, user ptr, or DMA buffers.
 // 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
 // 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 fmt.Errorf("stream on: %w", err)
 	}
 	}
 	return nil
 	return nil
@@ -136,50 +134,50 @@ func StreamOn(fd uintptr) error {
 // StreamOff requests streaming to be turned off for
 // StreamOff requests streaming to be turned off for
 // capture (or output) that uses memory map, user ptr, or DMA buffers.
 // 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
 // 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 fmt.Errorf("stream off: %w", err)
 	}
 	}
 	return nil
 	return nil
 }
 }
 
 
 // InitBuffers sends buffer allocation request to initialize buffer IO
 // 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
 // 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
 	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)
 		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
 	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
 	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)
 	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 Buffer{}, fmt.Errorf("query buffer: %w", err)
 	}
 	}
 
 
 	return makeBuffer(v4l2Buf), nil
 	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)
 	data, err := sys.Mmap(int(fd), offset, len, sys.PROT_READ|sys.PROT_WRITE, sys.MAP_SHARED)
 	if err != nil {
 	if err != nil {
 		return nil, fmt.Errorf("map memory buffer: %w", err)
 		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
 	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 {
 	if err := sys.Munmap(buf); err != nil {
 		return fmt.Errorf("unmap memory buffer: %w", err)
 		return fmt.Errorf("unmap memory buffer: %w", err)
 	}
 	}
 	return nil
 	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)
 // 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
 // when using either memory map, user pointer, or DMA buffers. Buffer is returned with
 // additional information about the queued buffer.
 // additional information about the queued buffer.
 // https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-qbuf.html#vidioc-qbuf
 // 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
 	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)
 	v4l2Buf.index = C.uint(index)
 
 
 	if err := send(fd, C.VIDIOC_QBUF, uintptr(unsafe.Pointer(&v4l2Buf))); err != nil {
 	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
 // when using either memory map, user pointer, or DMA buffers. Buffer is returned with
 // additional information about the dequeued buffer.
 // additional information about the dequeued buffer.
 // https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-qbuf.html#vidioc-qbuf
 // 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
 	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 {
 	if err := send(fd, C.VIDIOC_DQBUF, uintptr(unsafe.Pointer(&v4l2Buf))); err != nil {
 		return Buffer{}, fmt.Errorf("buffer dequeue: %w", err)
 		return Buffer{}, fmt.Errorf("buffer dequeue: %w", err)
@@ -229,24 +263,18 @@ func DequeueBuffer(fd uintptr) (Buffer, error) {
 	return makeBuffer(v4l2Buf), nil
 	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
 package v4l2
 
 
 import (
 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 (
 var (
 	InputStatusNoPower  InputStatus = C.V4L2_IN_ST_NO_POWER
 	InputStatusNoPower  InputStatus = C.V4L2_IN_ST_NO_POWER
 	InputStatusNoSignal InputStatus = C.V4L2_IN_ST_NO_SIGNAL
 	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{
 var InputStatuses = map[InputStatus]string{