Vladimir Vivien преди 4 години
родител
ревизия
77e428df1e

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+.DS_Store
+.idea
+.vscode

+ 76 - 0
examples/capture/capture.go

@@ -0,0 +1,76 @@
+package main
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"log"
+	"os"
+
+	"github.com/vladimirvivien/go4vl/v4l2"
+)
+
+func main() {
+	devName := "/dev/video0"
+	flag.StringVar(&devName, "d", devName, "device name (path)")
+	flag.Parse()
+
+	// open device
+	device, err := v4l2.Open(devName)
+	if err != nil {
+		log.Fatalf("failed to open device: %s", err)
+	}
+	defer device.Close()
+
+	// configuration
+	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 stream
+	if err := device.StartStream(10); err != nil {
+		log.Fatalf("failed to start stream: %s", err)
+	}
+
+	ctx, cancel := context.WithCancel(context.TODO())
+	frameChan, err := device.Capture(ctx, 15)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// process frames from capture channel
+	totalFrames := 10
+	count := 0
+	for frame := range frameChan {
+		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
+		}
+		if err := file.Close(); err != nil {
+			log.Printf("failed to close file %s: %s", fileName, err)
+		}
+		count++
+		if count >= totalFrames {
+			break
+		}
+	}
+
+	cancel() // stop capture
+	if err := device.StopStream(); err != nil {
+		fmt.Println(err)
+		os.Exit(1)
+	}
+	fmt.Println("Done.")
+
+}

+ 83 - 0
examples/device_info/devinfo.go

@@ -0,0 +1,83 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"log"
+
+	"github.com/vladimirvivien/go4vl/v4l2"
+)
+
+func deviceCap(device *v4l2.Device) error {
+	caps, err := device.GetCapability()
+	if err != nil {
+		return err
+	}
+
+	log.Printf("%#v", caps.String())
+	return nil
+}
+
+func setDefaultCrop(device *v4l2.Device) error {
+	cap, err := device.GetCropCapability()
+	if err != nil {
+		return err
+	}
+	log.Printf("device crop capability: %s", cap.String())
+	err = device.SetCropRect(cap.DefaultRect())
+	if err != nil {
+		log.Printf("setcrop unsupported: %s", err)
+	}
+	return nil
+}
+
+func getPixelFormat(device *v4l2.Device) error {
+	format, err := device.GetPixFormat()
+	if err != nil {
+		return fmt.Errorf("default format: %w", err)
+	}
+	log.Println("got default format")
+	log.Printf("pixformat %#v", format)
+	return nil
+}
+
+func setPixelFormat(device *v4l2.Device) error {
+	err := device.SetPixFormat(v4l2.PixFormat{
+		Width:       320,
+		Height:      240,
+		PixelFormat: v4l2.PixelFmtYUYV,
+		Field:       v4l2.FieldNone,
+	})
+	if err != nil {
+		return fmt.Errorf("failed to set format: %w", err)
+	}
+	log.Println("pixel format set")
+	return nil
+}
+
+func main() {
+	var devName string
+	flag.StringVar(&devName, "d", "/dev/video0", "device name (path)")
+	flag.Parse()
+	device, err := v4l2.Open(devName)
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer device.Close()
+
+	if err := deviceCap(device); err != nil {
+		log.Fatal(err)
+	}
+
+	if err := setDefaultCrop(device); err != nil {
+		log.Fatal(err)
+	}
+
+	if err := getPixelFormat(device); err != nil {
+		log.Fatal(err)
+	}
+
+	if err := setPixelFormat(device); err != nil {
+		log.Fatal(err)
+	}
+}

+ 5 - 0
examples/raw_v4l2/README.md

@@ -0,0 +1,5 @@
+# Raw V4L2
+The example in this directory shows all of the moving pieces that make
+the V4L2 API works using Go.  It shows all of the steps, in detail, that
+are required to communicate with a device driver to configure, initiate,
+and capture images.

+ 392 - 0
examples/raw_v4l2/raw_capture.go

@@ -0,0 +1,392 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"log"
+	"os"
+	"time"
+	"unsafe"
+
+	sys "golang.org/x/sys/unix"
+)
+
+// ========================= V4L2 command encoding =====================
+// https://elixir.bootlin.com/linux/v5.13-rc6/source/include/uapi/asm-generic/ioctl.h
+
+const (
+	//ioctl command layout
+	iocOpNone  = 0
+	iocOpWrite = 1
+	iocOpRead  = 2
+
+	iocTypeBits   = 8
+	iocNumberBits = 8
+	iocSizeBits   = 14
+	iocOpBits     = 2
+
+	numberPos = 0
+	typePos   = numberPos + iocNumberBits
+	sizePos   = typePos + iocTypeBits
+	opPos     = sizePos + iocSizeBits
+)
+
+// ioctl command encoding funcs
+func ioEnc(iocMode, iocType, number, size uintptr) uintptr {
+	return (iocMode << opPos) |
+		(iocType << typePos) |
+		(number << numberPos) |
+		(size << sizePos)
+}
+
+func ioEncR(iocType, number, size uintptr) uintptr {
+	return ioEnc(iocOpRead, iocType, number, size)
+}
+
+func ioEncW(iocType, number, size uintptr) uintptr {
+	return ioEnc(iocOpWrite, iocType, number, size)
+}
+
+func ioEncRW(iocType, number, size uintptr) uintptr {
+	return ioEnc(iocOpRead|iocOpWrite, iocType, number, size)
+}
+
+// four character pixel format encoding
+func fourcc(a, b, c, d uint32) uint32 {
+	return (a | b<<8) | c<<16 | d<<24
+}
+
+// wrapper for ioctl system call
+func ioctl(fd, req, arg uintptr) (err error) {
+	if _, _, errno := sys.Syscall(sys.SYS_IOCTL, fd, req, arg); errno != 0 {
+		err = errno
+		return
+	}
+	return nil
+}
+
+// ========================= Pixel Format =========================
+// https://elixir.bootlin.com/linux/v5.13-rc6/source/include/uapi/linux/videodev2.h#L682
+
+var (
+	PixelFmtMJPEG = fourcc('M', 'J', 'P', 'G') // V4L2_PIX_FMT_MJPEG
+)
+
+// Pix format field types
+// https://elixir.bootlin.com/linux/v5.13-rc6/source/include/uapi/linux/videodev2.h#L89
+const (
+	FieldAny  uint32 = iota // V4L2_FIELD_ANY
+	FieldNone               // V4L2_FIELD_NONE
+)
+
+// buff stream types
+// https://elixir.bootlin.com/linux/v5.13-rc6/source/include/uapi/linux/videodev2.h#L142
+const (
+	BufTypeVideoCapture uint32 = iota + 1 // V4L2_BUF_TYPE_VIDEO_CAPTURE = 1
+	BufTypeVideoOutput                    // V4L2_BUF_TYPE_VIDEO_OUTPUT  = 2
+	BufTypeOverlay                        // V4L2_BUF_TYPE_VIDEO_OVERLAY = 3
+)
+
+// Format represents C type v4l2_format
+// https://elixir.bootlin.com/linux/v5.13-rc6/source/include/uapi/linux/videodev2.h#L2324
+type Format struct {
+	StreamType uint32
+	fmt        [200]byte // max uinion size
+}
+
+// PixFormat represents v4l2_pix_format
+// https://elixir.bootlin.com/linux/v5.13-rc6/source/include/uapi/linux/videodev2.h#L496
+type PixFormat struct {
+	Width        uint32
+	Height       uint32
+	PixelFormat  uint32
+	Field        uint32
+	BytesPerLine uint32
+	SizeImage    uint32
+	Colorspace   uint32
+	Priv         uint32
+	Flags        uint32
+	YcbcrEnc     uint32
+	Quantization uint32
+	XferFunc     uint32
+}
+
+// setsFormat sets pixel format of device
+func setFormat(fd uintptr, pixFmt PixFormat) error {
+	format := Format{StreamType: BufTypeVideoCapture}
+
+	// a bit of C union type magic with unsafe.Pointer
+	*(*PixFormat)(unsafe.Pointer(&format.fmt[0])) = pixFmt
+
+	// encode command to send
+	vidiocSetFormat := ioEncRW('V', 5, uintptr(unsafe.Sizeof(Format{})))
+
+	// send command
+	if err := ioctl(fd, vidiocSetFormat, uintptr(unsafe.Pointer(&format))); err != nil {
+		return err
+	}
+	return nil
+}
+
+// =========================== Buffers and Streaming ========================== //
+
+// Memory buffer types
+// https://elixir.bootlin.com/linux/v5.13-rc6/source/include/uapi/linux/videodev2.h#L188
+const (
+	StreamMemoryTypeMMAP    uint32 = iota + 1 // V4L2_MEMORY_MMAP             = 1,
+	StreamMemoryTypeUserPtr                   // V4L2_MEMORY_USERPTR          = 2,
+	StreamMemoryTypeOverlay                   // V4L2_MEMORY_OVERLAY          = 3,
+	StreamMemoryTypeDMABuf                    // V4L2_MEMORY_DMABUF           = 4,
+)
+
+// RequestBuffers represents C type
+// https://elixir.bootlin.com/linux/v5.13-rc6/source/include/uapi/linux/videodev2.h#L951
+type RequestBuffers struct {
+	Count        uint32
+	StreamType   uint32
+	Memory       uint32
+	Capabilities uint32
+	Reserved     [1]uint32
+}
+
+// reqBuffers requests that the device allocates a `count`
+// number of internal buffers before they can be mapped into
+// the application's address space. The driver will return
+// the actual number of buffers allocated in the RequestBuffers
+// struct.
+func reqBuffers(fd uintptr, count uint32) error {
+	reqbuf := RequestBuffers{
+		StreamType: BufTypeVideoCapture,
+		Count:      count,
+		Memory:     StreamMemoryTypeMMAP,
+	}
+	vidiocReqBufs := ioEncRW('V', 8, uintptr(unsafe.Sizeof(RequestBuffers{})))
+	if err := ioctl(fd, vidiocReqBufs, uintptr(unsafe.Pointer(&reqbuf))); err != nil {
+		return err
+	}
+	return nil
+}
+
+// ================================ Map device Memory ===============================
+
+// BufferInfo represents C type v4l2_buffer
+// https://elixir.bootlin.com/linux/v5.13-rc6/source/include/uapi/linux/videodev2.h#L1037
+type BufferInfo struct {
+	Index      uint32
+	StreamType uint32
+	BytesUsed  uint32
+	Flags      uint32
+	Field      uint32
+	Timestamp  sys.Timeval
+	Timecode   Timecode
+	Sequence   uint32
+	Memory     uint32
+	m          [unsafe.Sizeof(&BufferService{})]byte // union m, cast to BufferService
+	Length     uint32
+	Reserved2  uint32
+	RequestFD  int32
+}
+
+// buffer service is embedded uion m
+// in v4l2_buffer C type.
+type BufferService struct {
+	Offset  uint32
+	UserPtr uintptr
+	Planes  uintptr
+	FD      int32
+}
+
+// Timecode represents C type v4l2_timecode
+// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L875
+type Timecode struct {
+	Type     uint32
+	Flags    uint32
+	Frames   uint8
+	Seconds  uint8
+	Minutes  uint8
+	Hours    uint8
+	Userbits [4]uint8
+}
+
+// mamapBuffer first queries the status of the device buffer at idx
+// by retrieving BufferInfo which returns the length of the buffer and
+// the current offset of the allocated buffers.  That information is
+// used to map the device's buffer unto the application's address space.
+func mmapBuffer(fd uintptr, idx uint32) ([]byte, error) {
+	buf := BufferInfo{
+		StreamType: BufTypeVideoCapture,
+		Memory:     StreamMemoryTypeMMAP,
+		Index:      idx,
+	}
+
+	// send ioctl command
+	vidiocQueryBuf := ioEncRW('V', 9, uintptr(unsafe.Sizeof(BufferInfo{}))) // VIDIOC_QUERYBUF
+	if err := ioctl(fd, vidiocQueryBuf, uintptr(unsafe.Pointer(&buf))); err != nil {
+		return nil, err
+	}
+
+	// grab m union and place it in type BufferService
+	bufSvc := *(*BufferService)(unsafe.Pointer(&buf.m[0]))
+
+	// map the memory and get []byte to access it
+	mbuf, err := sys.Mmap(int(fd), int64(bufSvc.Offset), int(buf.Length), sys.PROT_READ|sys.PROT_WRITE, sys.MAP_SHARED)
+	if err != nil {
+		return nil, err
+	}
+
+	return mbuf, nil
+}
+
+// =========================== Start device streaming =========================
+
+// startStreaming requests the device to start the capture process and start
+// filling device buffers.
+func startStreaming(fd uintptr) error {
+	bufType := BufTypeVideoCapture
+	vidiocStreamOn := ioEncW('V', 18, uintptr(unsafe.Sizeof(int32(0)))) // VIDIOC_STREAMON
+	if err := ioctl(fd, vidiocStreamOn, uintptr(unsafe.Pointer(&bufType))); err != nil {
+		return err
+	}
+	return nil
+}
+
+// ======================== Queue/Dequeue device buffer =======================
+
+// queueBuffer requests that an emptty buffer is enqueued into the device's
+// incoming queue at the specified index (so that it can be filled later).
+func queueBuffer(fd uintptr, idx uint32) error {
+	buf := BufferInfo{
+		StreamType: BufTypeVideoCapture,
+		Memory:     StreamMemoryTypeMMAP,
+		Index:      idx,
+	}
+	vidiocQueueBuf := ioEncRW('V', 15, uintptr(unsafe.Sizeof(BufferInfo{}))) // VIDIOC_QBUF
+	if err := ioctl(fd, vidiocQueueBuf, uintptr(unsafe.Pointer(&buf))); err != nil {
+		return err
+	}
+	return nil
+}
+
+// dequeueBuffer is called to dequeue a filled buffer from the devices buffer queue.
+// Once a device buffer is dequeued, it is mapped and is ready to be read by the application.
+func dequeueBuffer(fd uintptr) (uint32, error) {
+	buf := BufferInfo{
+		StreamType: BufTypeVideoCapture,
+		Memory:     StreamMemoryTypeMMAP,
+	}
+	vidiocDequeueBuf := ioEncRW('V', 17, uintptr(unsafe.Sizeof(BufferInfo{}))) // VIDIOC_DQBUF
+	if err := ioctl(fd, vidiocDequeueBuf, uintptr(unsafe.Pointer(&buf))); err != nil {
+		return 0, err
+	}
+	return buf.BytesUsed, nil
+}
+
+// =========================== Start device streaming =========================
+
+// stopStreaming requests the device to stop the streaming process and release
+// buffer resources.
+func stopStreaming(fd uintptr) error {
+	bufType := BufTypeVideoCapture
+	vidiocStreamOff := ioEncW('V', 19, uintptr(unsafe.Sizeof(int32(0)))) // VIDIOC_STREAMOFF
+	if err := ioctl(fd, vidiocStreamOff, uintptr(unsafe.Pointer(&bufType))); err != nil {
+		return err
+	}
+	return nil
+}
+
+// use sys.Select to wait for the device to become read-ready.
+func waitForDeviceReady(fd uintptr) error {
+	timeval := sys.NsecToTimeval((2 * time.Second).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 fmt.Errorf("wait for device ready: timeout")
+		default:
+			return nil
+		}
+	}
+}
+
+func main() {
+	var devName string
+	flag.StringVar(&devName, "d", "/dev/video0", "device name (path)")
+	flag.Parse()
+
+	// open device
+	devFile, err := os.OpenFile(devName, sys.O_RDWR|sys.O_NONBLOCK, 0)
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer devFile.Close()
+	fd := devFile.Fd()
+
+	// Set the format
+	log.Println("setting format to 640x480 MJPEG")
+	if err := setFormat(fd, PixFormat{
+		Width:       640,
+		Height:      480,
+		PixelFormat: PixelFmtMJPEG,
+		Field:       FieldNone,
+	}); err != nil {
+		log.Fatal(err)
+	}
+
+	// request device to setup 3 buffers
+	if err := reqBuffers(fd, 3); err != nil {
+		log.Fatal(err)
+	}
+
+	// map a device buffer to a local byte slice
+	// here we use the latest buffer
+	data, err := mmapBuffer(fd, 2)
+	if err != nil {
+		log.Fatalf("unable to map device buffer: %s", err)
+	}
+
+	// now, queue an initial device buffer at the selected index
+	//  to be filled with data prior to starting the device stream
+	if err := queueBuffer(fd, 2); err != nil {
+		log.Fatalf("failed to queue initial buffer: %s", err)
+	}
+
+	// now, ask the device to start the stream
+	if err := startStreaming(fd); err != nil {
+		log.Fatalf("failed to start streaming: %s", err)
+	}
+
+	// now wait for the device to be ready for read operation,
+	// this means the mapped buffer is ready to be consumed
+	if err := waitForDeviceReady(fd); err != nil {
+		log.Fatalf("failed during device read-wait: %s", err)
+	}
+
+	// deqeue the device buffer so that the local mapped byte slice
+	// is filled.
+	bufSize, err := dequeueBuffer(fd)
+	if err != nil {
+		log.Fatalf("failed during device read-wait: %s", err)
+	}
+
+	// save mapped buffer bytes to file
+	jpgFile, err := os.Create("capture.jpg")
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer jpgFile.Close()
+	if _, err := jpgFile.Write(data[:bufSize]); err != nil {
+		log.Fatalf("failed to save file: %s", err)
+	}
+
+	// release streaming resources
+	if err := stopStreaming(fd); err != nil {
+		log.Fatalf("failed to stop stream: %s", err)
+	}
+}

BIN
examples/webcam/gologo.png


+ 55 - 0
examples/webcam/gologo.svg

@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 24.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 205.4 76.7" style="enable-background:new 0 0 205.4 76.7;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:#00ACD7;}
+</style>
+<g>
+	<g>
+		<g>
+			<g>
+				<path class="st0" d="M15.5,23.2c-0.4,0-0.5-0.2-0.3-0.5l2.1-2.7c0.2-0.3,0.7-0.5,1.1-0.5h35.7c0.4,0,0.5,0.3,0.3,0.6l-1.7,2.6
+					c-0.2,0.3-0.7,0.6-1,0.6L15.5,23.2z"/>
+			</g>
+		</g>
+	</g>
+	<g>
+		<g>
+			<g>
+				<path class="st0" d="M0.4,32.4c-0.4,0-0.5-0.2-0.3-0.5l2.1-2.7c0.2-0.3,0.7-0.5,1.1-0.5h45.6c0.4,0,0.6,0.3,0.5,0.6l-0.8,2.4
+					c-0.1,0.4-0.5,0.6-0.9,0.6L0.4,32.4z"/>
+			</g>
+		</g>
+	</g>
+	<g>
+		<g>
+			<g>
+				<path class="st0" d="M24.6,41.6c-0.4,0-0.5-0.3-0.3-0.6l1.4-2.5c0.2-0.3,0.6-0.6,1-0.6h20c0.4,0,0.6,0.3,0.6,0.7L47.1,41
+					c0,0.4-0.4,0.7-0.7,0.7L24.6,41.6z"/>
+			</g>
+		</g>
+	</g>
+	<g>
+		<g id="CXHf1q_3_">
+			<g>
+				<g>
+					<path class="st0" d="M128.4,21.4c-6.3,1.6-10.6,2.8-16.8,4.4c-1.5,0.4-1.6,0.5-2.9-1c-1.5-1.7-2.6-2.8-4.7-3.8
+						c-6.3-3.1-12.4-2.2-18.1,1.5c-6.8,4.4-10.3,10.9-10.2,19c0.1,8,5.6,14.6,13.5,15.7c6.8,0.9,12.5-1.5,17-6.6
+						c0.9-1.1,1.7-2.3,2.7-3.7c-3.6,0-8.1,0-19.3,0c-2.1,0-2.6-1.3-1.9-3c1.3-3.1,3.7-8.3,5.1-10.9c0.3-0.6,1-1.6,2.5-1.6
+						c5.1,0,23.9,0,36.4,0c-0.2,2.7-0.2,5.4-0.6,8.1c-1.1,7.2-3.8,13.8-8.2,19.6c-7.2,9.5-16.6,15.4-28.5,17
+						c-9.8,1.3-18.9-0.6-26.9-6.6c-7.4-5.6-11.6-13-12.7-22.2c-1.3-10.9,1.9-20.7,8.5-29.3c7.1-9.3,16.5-15.2,28-17.3
+						c9.4-1.7,18.4-0.6,26.5,4.9c5.3,3.5,9.1,8.3,11.6,14.1C130,20.6,129.6,21.1,128.4,21.4z"/>
+				</g>
+				<g>
+					<path class="st0" d="M161.5,76.7c-9.1-0.2-17.4-2.8-24.4-8.8c-5.9-5.1-9.6-11.6-10.8-19.3c-1.8-11.3,1.3-21.3,8.1-30.2
+						c7.3-9.6,16.1-14.6,28-16.7c10.2-1.8,19.8-0.8,28.5,5.1c7.9,5.4,12.8,12.7,14.1,22.3c1.7,13.5-2.2,24.5-11.5,33.9
+						c-6.6,6.7-14.7,10.9-24,12.8C166.8,76.3,164.1,76.4,161.5,76.7z M185.3,36.3c-0.1-1.3-0.1-2.3-0.3-3.3
+						c-1.8-9.9-10.9-15.5-20.4-13.3c-9.3,2.1-15.3,8-17.5,17.4c-1.8,7.8,2,15.7,9.2,18.9c5.5,2.4,11,2.1,16.3-0.6
+						C180.5,51.3,184.8,44.9,185.3,36.3z"/>
+				</g>
+			</g>
+		</g>
+	</g>
+</g>
+</svg>

+ 129 - 0
examples/webcam/webcam.go

@@ -0,0 +1,129 @@
+package main
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"html/template"
+	"io"
+	"log"
+	"net/http"
+	"time"
+
+	"github.com/vladimirvivien/go4vl/v4l2"
+)
+
+var (
+	frames <-chan []byte
+	fps    uint32 = 30
+)
+
+// servePage reads templated HTML
+func servePage(w http.ResponseWriter, r *http.Request) {
+	// Start HTTP response
+	w.Header().Add("Content-Type", "text/html")
+	pd := map[string]string{
+		"fps":        fmt.Sprintf("%d fps", fps),
+		"streamPath": fmt.Sprintf("/stream?%d", time.Now().UnixNano()),
+	}
+	t, err := template.ParseFiles("webcam.html")
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	// execute and return the template
+	w.WriteHeader(http.StatusOK)
+	err = t.Execute(w, pd)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+	}
+}
+
+// start http service
+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)))
+
+		// write frame
+		if _, err := w.Write(frame); err != nil {
+			log.Printf("failed to write image: %s", err)
+			return
+		}
+		// close boundary
+		if _, err := io.WriteString(w, "\n"); err != nil {
+			log.Printf("failed to write bounday: %s", err)
+			return
+		}
+	}
+}
+
+func main() {
+	port := ":9090"
+	devName := "/dev/video0"
+	flag.StringVar(&devName, "d", devName, "device name (path)")
+	flag.StringVar(&port, "p", port, "webcam service port")
+	flag.Parse()
+
+	// open device and setup device
+	device, err := v4l2.Open(devName)
+	if err != nil {
+		log.Fatalf("failed to open device: %s", err)
+	}
+	defer device.Close()
+	caps, err := device.GetCapability()
+	if err != nil {
+		log.Println("failed to get device capabilities:", err)
+	}
+	log.Printf("device [%s] opened\n", devName)
+	log.Printf("device info: %s", caps.String())
+
+	// set device format
+	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)
+	}
+
+	// Setup and start stream capture
+	if err := device.StartStream(15); err != nil {
+		log.Fatalf("unable to start stream: %s", err)
+	}
+
+	// start capture
+	ctx, cancel := context.WithCancel(context.TODO())
+	f, err := device.Capture(ctx, fps)
+	if err != nil {
+		log.Fatalf("stream capture: %s", err)
+	}
+	defer func() {
+		cancel()
+		device.Close()
+	}()
+	frames = f // make frames available.
+	log.Println("device capture started, frames available")
+
+	log.Printf("starting server on port %s", port)
+	log.Println("use url path /webcam")
+
+	// setup http service
+	http.HandleFunc("/webcam", servePage)        // returns an html page
+	http.HandleFunc("/stream", serveVideoStream) // returns video feed
+	http.Handle("/", http.FileServer(http.Dir(".")))
+	if err := http.ListenAndServe(port, nil); err != nil {
+		log.Fatal(err)
+	}
+}

+ 49 - 0
examples/webcam/webcam.html

@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0"/>
+    <title>Go WebCam</title>
+
+    <!-- CSS  -->
+    <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
+    <link href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css" type="text/css" rel="stylesheet" media="screen,projection"/>
+</head>
+<body>
+
+<div class="section no-pad-bot" id="index-banner">
+    <div class="container">
+        <div class="row center">
+            <img src="/gologo.png" height="75"/>
+        </div>
+        <br><br>
+        <h1 class="header center blue-text">WebCam</h1>
+        <div class="row center">
+            <h3 class="header col s12 light">Realtime video capture using the Go programming language</h3>
+        </div>
+
+        <div class="center">
+            <div class="col s12 m7">
+                <div class="card">
+                    <div class="card-image">
+                        <img id="img" src="{{.streamPath}}"/>
+                        <span class="card-title">Go WebCam (at {{.fps}})</span>
+                    </div>
+                    <div class="card-content">
+                        <p class="flow-text">Images are captured using the Video for Linux API (V4L2) and are streamed as JPEG using
+                            multipart/x-mixed-replace header with boundaries.</p>
+                    </div>
+                    <div class="card-action">
+                        <a href="#"></a>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+</div>
+
+<!--  Scripts-->
+<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
+
+</body>
+</html>

+ 5 - 0
go.mod

@@ -0,0 +1,5 @@
+module github.com/vladimirvivien/go4vl
+
+go 1.16
+
+require golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c

+ 2 - 0
go.sum

@@ -0,0 +1,2 @@
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

+ 97 - 0
v4l2/capability.go

@@ -0,0 +1,97 @@
+package v4l2
+
+import (
+	"fmt"
+	"unsafe"
+)
+
+// V4l2 video capability constants
+// see https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L451
+
+const (
+	CapVideoCapture       = 0x00000001 // V4L2_CAP_VIDEO_CAPTURE
+	CapVideoOutput        = 0x00000002 // V4L2_CAP_VIDEO_OUTPUT
+	CapVideoOverlay       = 0x00000004 // V4L2_CAP_VIDEO_OVERLAY
+	CapVideoOutputOverlay = 0x00000200 // V4L2_CAP_VIDEO_OUTPUT_OVERLAY
+	CapReadWrite          = 0x01000000 // V4L2_CAP_READWRITE
+	CapAsyncIO            = 0x02000000 // V4L2_CAP_ASYNCIO
+	CapStreaming          = 0x04000000 // V4L2_CAP_STREAMING
+)
+
+// v4l2Capabiolity type for device (see v4l2_capability
+// https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-querycap.html#c.V4L.v4l2_capability
+// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L440
+type v4l2Capability struct {
+	driver       [16]uint8
+	card         [32]uint8
+	busInfo      [32]uint8
+	version      uint32
+	capabilities uint32
+	deviceCaps   uint32
+	reserved     [3]uint32
+}
+
+// Capability represents capabilities retrieved for the device.
+// Use methods on this type to access capabilities.
+// See https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-querycap.html#c.V4L.v4l2_capability
+type Capability struct {
+	v4l2Cap v4l2Capability
+}
+
+// GetCapability retrieves capability info for device
+func GetCapability(fd uintptr) (Capability, error) {
+	v4l2Cap := v4l2Capability{}
+	if err := Send(fd, vidiocQueryCap, uintptr(unsafe.Pointer(&v4l2Cap))); err != nil {
+		return Capability{}, fmt.Errorf("capability: %w", err)
+	}
+	return Capability{v4l2Cap: v4l2Cap}, nil
+}
+
+// GetCapabilities returns the capability mask as a union of
+// all exported capabilities for the physical device (opened or not).
+// Use this method to access capabilities.
+func (c Capability) GetCapabilities() uint32 {
+	return c.v4l2Cap.capabilities
+}
+
+// GetDeviceCaps returns the capability mask for the open device.
+// This is a subset of capabilities returned by GetCapabilities.
+func (c Capability) GetDeviceCaps() uint32 {
+	return c.v4l2Cap.deviceCaps
+}
+
+func (c Capability) IsVideoCaptureSupported() bool {
+	return (c.v4l2Cap.capabilities & CapVideoCapture) != 0
+}
+
+func (c Capability) IsVideoOutputSupported() bool {
+	return (c.v4l2Cap.capabilities & CapVideoOutput) != 0
+}
+
+func (c Capability) IsReadWriteSupported() bool {
+	return (c.v4l2Cap.capabilities & CapReadWrite) != 0
+}
+
+func (c Capability) IsStreamingSupported() bool {
+	return (c.v4l2Cap.capabilities & CapStreaming) != 0
+}
+
+func (c Capability) DriverName() string {
+	return GoString(c.v4l2Cap.driver[:])
+}
+
+func (c Capability) CardName() string {
+	return GoString(c.v4l2Cap.card[:])
+}
+
+func (c Capability) BusInfo() string {
+	return GoString(c.v4l2Cap.busInfo[:])
+}
+
+func (c Capability) GetVersion() uint32 {
+	return c.v4l2Cap.version
+}
+
+func (c Capability) String() string {
+	return fmt.Sprintf("driver: %s; card: %s; bus info: %s", c.DriverName(), c.CardName(), c.BusInfo())
+}

+ 20 - 0
v4l2/commands.go

@@ -0,0 +1,20 @@
+package v4l2
+
+import "unsafe"
+
+// v4l2 ioctl commands
+// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L2510
+// https://www.kernel.org/doc/html/v4.14/media/uapi/v4l/user-func.html
+var (
+	vidiocQueryCap   = encodeRead('V', 0, uintptr(unsafe.Sizeof(v4l2Capability{})))       // VIDIOC_QUERYCAP
+	vidiocGetFormat  = encodeReadWrite('V', 4, uintptr(unsafe.Sizeof(Format{})))          // VIDIOC_G_FMT
+	vidiocSetFormat  = encodeReadWrite('V', 5, uintptr(unsafe.Sizeof(Format{})))          // VIDIOC_S_FMT
+	vidiocReqBufs    = encodeReadWrite('V', 8, uintptr(unsafe.Sizeof(RequestBuffers{})))  // VIDIOC_REQBUFS
+	vidiocQueryBuf   = encodeReadWrite('V', 9, uintptr(unsafe.Sizeof(BufferInfo{})))      // VIDIOC_QUERYBUF
+	vidiocQueueBuf   = encodeReadWrite('V', 15, uintptr(unsafe.Sizeof(BufferInfo{})))     // VIDIOC_QBUF
+	vidiocDequeueBuf = encodeReadWrite('V', 17, uintptr(unsafe.Sizeof(BufferInfo{})))     // VIDIOC_DQBUF
+	vidiocStreamOn   = encodeWrite('V', 18, uintptr(unsafe.Sizeof(int32(0))))             // VIDIOC_STREAMON
+	vidiocStreamOff  = encodeWrite('V', 19, uintptr(unsafe.Sizeof(int32(0))))             // VIDIOC_STREAMOFF
+	vidiocCropCap    = encodeReadWrite('V', 58, uintptr(unsafe.Sizeof(CropCapability{}))) // VIDIOC_CROPCAP
+	vidiocSetCrop    = encodeWrite('V', 60, uintptr(unsafe.Sizeof(Crop{})))               // VIDIOC_S_CROP
+)

+ 86 - 0
v4l2/crop.go

@@ -0,0 +1,86 @@
+package v4l2
+
+import (
+	"errors"
+	"fmt"
+	"unsafe"
+)
+
+// Rect (v4l2_rect)
+// https://www.kernel.org/doc/html/v4.14/media/uapi/v4l/dev-overlay.html?highlight=v4l2_rect#c.v4l2_rect
+// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L412
+type Rect struct {
+	Left   int32
+	Top    int32
+	Width  uint32
+	Height uint32
+}
+
+// Fract (v4l2_fract)
+// https://www.kernel.org/doc/html/v4.14/media/uapi/v4l/vidioc-enumstd.html#c.v4l2_fract
+// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L419
+type Fract struct {
+	Numerator   uint32
+	Denominator uint32
+}
+
+// CropCapability (v4l2_cropcap)
+// https://www.kernel.org/doc/html/v4.14/media/uapi/v4l/vidioc-cropcap.html#c.v4l2_cropcap
+// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L1221
+type CropCapability struct {
+	StreamType  uint32
+	Bounds      Rect
+	DefaultRect Rect
+	PixelAspect Fract
+}
+
+// Crop (v4l2_crop)
+// https://www.kernel.org/doc/html/v4.14/media/uapi/v4l/vidioc-g-crop.html#c.v4l2_crop
+// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L1228
+type Crop struct {
+	StreamType uint32
+	Rect       Rect
+}
+
+// GetCropCapability  retrieves cropping info for specified device
+// See https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-cropcap.html#ioctl-vidioc-cropcap
+func GetCropCapability(fd uintptr) (CropCapability, error) {
+	cropCap := CropCapability{}
+	cropCap.StreamType = BufTypeVideoCapture
+	if err := Send(fd, vidiocCropCap, uintptr(unsafe.Pointer(&cropCap))); err != nil {
+		return CropCapability{}, fmt.Errorf("crop capability: %w", err)
+	}
+	return cropCap, nil
+}
+
+// SetCropRect sets the cropping dimension for specified device
+// See https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-g-crop.html#ioctl-vidioc-g-crop-vidioc-s-crop
+func SetCropRect(fd uintptr, r Rect) error {
+	crop := Crop{Rect: r, StreamType: BufTypeVideoCapture}
+	if err := Send(fd, vidiocSetCrop, uintptr(unsafe.Pointer(&crop))); err != nil {
+		switch {
+		case errors.Is(err, ErrorUnsupported):
+			return fmt.Errorf("setcrop: unsupported: %w", err)
+		default:
+			return fmt.Errorf("setcrop failed: %w", err)
+		}
+	}
+	return nil
+}
+
+func (c CropCapability) String() string {
+	return fmt.Sprintf("default:{top=%d, left=%d, width=%d,height=%d};  bounds:{top=%d, left=%d, width=%d,height=%d}; pixel-aspect{%d:%d}",
+		c.DefaultRect.Top,
+		c.DefaultRect.Left,
+		c.DefaultRect.Width,
+		c.DefaultRect.Height,
+
+		c.Bounds.Top,
+		c.Bounds.Left,
+		c.Bounds.Width,
+		c.Bounds.Height,
+
+		c.PixelAspect.Numerator,
+		c.PixelAspect.Denominator,
+	)
+}

+ 259 - 0
v4l2/device.go

@@ -0,0 +1,259 @@
+package v4l2
+
+import (
+	"context"
+	"fmt"
+	"os"
+	sys "syscall"
+	"time"
+)
+
+type Device struct {
+	path         string
+	file         *os.File
+	fd           uintptr
+	cap          *Capability
+	cropCap      *CropCapability
+	pixFormat    PixFormat
+	buffers      [][]byte
+	requestedBuf 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()
+}
+
+// GetCapability retrieves device capability info and
+// caches it for future capability check.
+func (d *Device) GetCapability() (*Capability, error) {
+	if d.cap != nil {
+		return d.cap, nil
+	}
+	cap, err := 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() (CropCapability, error) {
+	if d.cropCap != nil {
+		return *d.cropCap, nil
+	}
+	if err := d.assertVideoCaptureSupport(); err != nil {
+		return CropCapability{}, fmt.Errorf("device: %w", err)
+	}
+
+	cropCap, err := GetCropCapability(d.fd)
+	if err != nil {
+		return 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 Rect) error {
+	if err := d.assertVideoCaptureSupport(); err != nil {
+		return fmt.Errorf("device: %w", err)
+	}
+	if err := 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() (PixFormat, error) {
+	if err := d.assertVideoCaptureSupport(); err != nil {
+		return PixFormat{}, fmt.Errorf("device: %w", err)
+	}
+	pixFmt, err := GetPixFormat(d.fd)
+	if err != nil {
+		return PixFormat{}, fmt.Errorf("device: %w", err)
+	}
+	return pixFmt, nil
+}
+
+// SetPixFormat sets the pixel format for the associated device.
+func (d *Device) SetPixFormat(pixFmt PixFormat) error {
+	if err := d.assertVideoCaptureSupport(); err != nil {
+		return fmt.Errorf("device: %w", err)
+	}
+
+	if err := SetPixFormat(d.fd, pixFmt); err != nil {
+		return fmt.Errorf("device: %w", err)
+	}
+	return nil
+}
+
+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 := AllocateBuffers(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++ {
+		bufInfo, err := GetBufferInfo(d.fd, uint32(i))
+		if err != nil {
+			return fmt.Errorf("device: start stream: %w", err)
+		}
+
+		offset := bufInfo.GetService().Offset
+		length := bufInfo.Length
+		mappedBuf, err := 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 := QueueBuffer(d.fd, uint32(i))
+		if err != nil {
+			return fmt.Errorf("device: start stream: %w", err)
+		}
+	}
+
+	// turn on device stream
+	if err := 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 := WaitForDeviceRead(d.fd, 2*time.Second); err != nil {
+					panic(fmt.Errorf("device: capture: %w", err).Error())
+				}
+
+				// dequeue the device buf
+				bufInfo, err := 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 := 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 := UnmapMemoryBuffer(d.buffers[i]); err != nil {
+			return fmt.Errorf("device: stop stream: %w", err)
+		}
+	}
+	if err := 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
+}

+ 68 - 0
v4l2/encoding.go

@@ -0,0 +1,68 @@
+package v4l2
+
+import "bytes"
+
+// ioctl command API encoding:
+// ioctl command encoding uses 32 bits total:
+// - command in lower 16 bits
+// - size of the parameter structure in the lower 14 bits of the upper 16 bits.
+// - The highest 2 bits are reserved for indicating the ``access mode''.
+// https://elixir.bootlin.com/linux/v5.13-rc6/source/include/uapi/asm-generic/ioctl.h
+
+const (
+	// ioctl op direction:
+	// Write: userland is writing and kernel is reading.
+	// Read:  userland is reading and kernel is writing.
+	iocOpNone  = 0
+	iocOpWrite = 1
+	iocOpRead  = 2
+
+	// ioctl command bit sizes
+	iocTypeBits   = 8
+	iocNumberBits = 8
+	iocSizeBits   = 14
+	iocOpBits     = 2
+
+	// ioctl bit layout positions
+	numberPos = 0
+	typePos   = numberPos + iocNumberBits
+	sizePos   = typePos + iocTypeBits
+	opPos     = sizePos + iocSizeBits
+)
+
+// encodes V42L API command
+func encode(iocMode, iocType, number, size uintptr) uintptr {
+	return (iocMode << opPos) | (iocType << typePos) | (number << numberPos) | (size << sizePos)
+}
+
+// encodeRead encodes ioctl read command
+func encodeRead(iocType, number, size uintptr) uintptr {
+	return encode(iocOpRead, iocType, number, size)
+}
+
+// encodeWrite encodes ioctl write command
+func encodeWrite(iocType, number, size uintptr) uintptr {
+	return encode(iocOpWrite, iocType, number, size)
+}
+
+// encodeReadWrite encodes ioctl command for read or write
+func encodeReadWrite(iocType, number, size uintptr) uintptr {
+	return encode(iocOpRead|iocOpWrite, iocType, number, size)
+}
+
+// fourcc implements the four character code encoding found
+// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L81
+// #define v4l2_fourcc(a, b, c, d)\
+// 	 ((__u32)(a) | ((__u32)(b) << 8) | ((__u32)(c) << 16) | ((__u32)(d) << 24))
+func fourcc(a, b, c, d uint32) uint32 {
+	return (a | b<<8) | c<<16 | d<<24
+}
+
+// GoString encodes C null-terminated string to Go string
+func GoString(s []byte) string {
+	null := bytes.Index(s, []byte{0})
+	if null < 0 {
+		return ""
+	}
+	return string(s[:null])
+}

+ 167 - 0
v4l2/format.go

@@ -0,0 +1,167 @@
+package v4l2
+
+import (
+	"errors"
+	"fmt"
+	"unsafe"
+)
+
+// Some Predefined pixel format definitions
+// https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/pixfmt.html
+// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L518
+var (
+	PixFmtRGB24   = fourcc('R', 'G', 'B', '3') // V4L2_PIX_FMT_RGB24
+	PixFmtGrey    = fourcc('G', 'R', 'E', 'Y') // V4L2_PIX_FMT_GREY
+	PixelFmtYUYV  = fourcc('Y', 'U', 'Y', 'V') // V4L2_PIX_FMT_YUYV
+	PixelFmtYYUV  = fourcc('Y', 'Y', 'U', 'V') // V4L2_PIX_FMT_YYUV
+	PixelFmtYVYU  = fourcc('Y', 'V', 'Y', 'U') // V4L2_PIX_FMT_YVYU
+	PixelFmtUYVY  = fourcc('U', 'Y', 'V', 'Y') // V4L2_PIX_FMT_UYVY
+	PixelFmtVYUY  = fourcc('V', 'Y', 'U', 'Y') // V4L2_PIX_FMT_VYUY
+	PixelFmtMJPEG = fourcc('M', 'J', 'P', 'G') // V4L2_PIX_FMT_MJPEG
+	PixelFmtJPEG  = fourcc('J', 'P', 'E', 'G') // V4L2_PIX_FMT_JPEG
+	PixelFmtMPEG  = fourcc('M', 'P', 'E', 'G') // V4L2_PIX_FMT_MPEG
+	PixelFmtH264  = fourcc('H', '2', '6', '4') // V4L2_PIX_FMT_H264
+	PixelFmtMPEG4 = fourcc('M', 'P', 'G', '4') // V4L2_PIX_FMT_MPEG4
+)
+
+// YcbcrEncoding (v4l2_ycbcr_encoding)
+// https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/colorspaces-defs.html?highlight=v4l2_ycbcr_encoding
+// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L300
+type YcbcrEncoding = uint32
+
+const (
+	YcbcrEncDefault YcbcrEncoding = iota // V4L2_YCBCR_ENC_DEFAULT
+	YcbcrEnc601                          // V4L2_YCBCR_ENC_601
+	YcbcrEnc709                          // V4L2_YCBCR_ENC_709
+	YcbcrEncXV601                        // V4L2_YCBCR_ENC_XV601
+	YcbcrEncXV709                        // V4L2_YCBCR_ENC_XV709
+)
+
+// Quantization (v4l2_quantization)
+// https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/colorspaces-defs.html?highlight=v4l2_quantization#c.V4L.v4l2_quantization
+// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L372
+type Quantization = uint32
+
+const (
+	QuantizationDefault   Quantization = iota // V4L2_QUANTIZATION_DEFAULT
+	QuantizationFullRange                     // V4L2_QUANTIZATION_FULL_RANGE
+	QuantizationLimRange                      // V4L2_QUANTIZATION_LIM_RANGE
+)
+
+// XferFunction (v4l2_xfer_func)
+// https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/colorspaces-defs.html?highlight=v4l2_xfer_func#c.V4L.v4l2_xfer_func
+// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L259
+type XferFunction = uint32
+
+const (
+	ferFuncDefault    XferFunction = iota // V4L2_XFER_FUNC_DEFAULT = 0
+	XferFunc709                           // V4L2_XFER_FUNC_709         = 1,
+	ferFuncSRGB                           // V4L2_XFER_FUNC_SRGB        = 2,
+	XferFuncOpRGB                         // V4L2_XFER_FUNC_OPRGB       = 3,
+	XferFuncSmpte240M                     // V4L2_XFER_FUNC_SMPTE240M   = 4,
+	XferFuncNone                          // V4L2_XFER_FUNC_NONE        = 5,
+	XferFuncDciP3                         // V4L2_XFER_FUNC_DCI_P3      = 6,
+	XferFuncSmpte2084                     // V4L2_XFER_FUNC_SMPTE2084   = 7,
+)
+
+// Field (v4l2_field)
+// https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/field-order.html?highlight=v4l2_field#c.v4l2_field
+// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L88
+type Field = uint32
+
+const (
+	FieldAny          Field = iota // V4L2_FIELD_ANY
+	FieldNone                      // V4L2_FIELD_NONE
+	FieldTop                       // V4L2_FIELD_TOP
+	FieldBottom                    // V4L2_FIELD_BOTTOM
+	FieldInterlaced                // V4L2_FIELD_INTERLACED
+	FieldSeqTb                     // V4L2_FIELD_SEQ_TB
+	FieldSeqBt                     // V4L2_FIELD_SEQ_BT
+	FieldAlternate                 // V4L2_FIELD_ALTERNATE
+	FieldInterlacedTb              // V4L2_FIELD_INTERLACED_TB
+	FieldInterlacedBt              // V4L2_FIELD_INTERLACED_BT
+)
+
+// PixFormat (v4l2_pix_format)
+// https://www.kernel.org/doc/html/v4.9/media/uapi/v4l/pixfmt-002.html?highlight=v4l2_pix_format
+// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L496
+type PixFormat struct {
+	Width        uint32
+	Height       uint32
+	PixelFormat  uint32
+	Field        uint32
+	BytesPerLine uint32
+	SizeImage    uint32
+	Colorspace   uint32
+	Priv         uint32
+	Flags        uint32
+	YcbcrEnc     YcbcrEncoding
+	Quantization Quantization
+	XferFunc     XferFunction
+}
+
+// Format (v4l2_format)
+// https://www.kernel.org/doc/html/v4.9/media/uapi/v4l/vidioc-g-fmt.html?highlight=v4l2_format
+// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L2303
+//
+// field fmt is a union, thus it's constructed as an appropriately sized array:
+//
+// struct v4l2_format {
+// 	__u32	 type;
+// 	union {
+// 		struct v4l2_pix_format		    pix;
+// 		struct v4l2_pix_format_mplane	pix_mp;
+// 		struct v4l2_window		        win;
+// 		struct v4l2_vbi_format		    vbi;
+// 		struct v4l2_sliced_vbi_format	sliced;
+// 		struct v4l2_sdr_format	 	    sdr;
+// 		struct v4l2_meta_format		    meta;
+// 		__u8	raw_data[200];   /* user-defined */
+// 	} fmt;
+// };
+type Format struct {
+	StreamType uint32
+	fmt        [200]byte
+}
+
+func (f Format) GetPixFormat() PixFormat {
+	pixfmt := (*PixFormat)(unsafe.Pointer(&f.fmt[0]))
+	return *pixfmt
+}
+
+func (f Format) SetPixFormat(newPix PixFormat) {
+	*(*PixFormat)(unsafe.Pointer(&f.fmt[0])) = newPix
+}
+
+// GetPixFormat retrieves pixel information for the specified driver
+// See https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-g-fmt.html#ioctl-vidioc-g-fmt-vidioc-s-fmt-vidioc-try-fmt
+func GetPixFormat(fd uintptr) (PixFormat, error){
+	format := Format{StreamType: BufTypeVideoCapture}
+	if err := Send(fd, vidiocGetFormat, uintptr(unsafe.Pointer(&format))); err != nil {
+		switch {
+		case errors.Is(err, ErrorUnsupported):
+			return PixFormat{}, fmt.Errorf("pix format: unsupported: %w", err)
+		default:
+			return PixFormat{}, fmt.Errorf("pix format failed: %w", err)
+		}
+	}
+
+	return format.GetPixFormat(), nil
+}
+
+// SetPixFormat sets the pixel format information for the specified driver
+// See https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-g-fmt.html#ioctl-vidioc-g-fmt-vidioc-s-fmt-vidioc-try-fmt
+func SetPixFormat(fd uintptr, pixFmt PixFormat) error {
+	format := Format{StreamType: BufTypeVideoCapture}
+	format.SetPixFormat(pixFmt)
+
+	if err := Send(fd, vidiocSetFormat, uintptr(unsafe.Pointer(&format))); err != nil {
+		switch {
+		case errors.Is(err, ErrorUnsupported):
+			return fmt.Errorf("pix format: unsupported operation: %w", err)
+		default:
+			return fmt.Errorf("pix format failed: %w", err)
+		}
+	}
+	return nil
+}

+ 23 - 0
v4l2/ioctl.go

@@ -0,0 +1,23 @@
+package v4l2
+
+import (
+	sys "syscall"
+)
+
+// Send sends raw command to driver (via ioctl syscall)
+func Send(fd, req, arg uintptr) error {
+	return ioctl(fd, req, arg)
+}
+
+func ioctl(fd, req, arg uintptr) (err error) {
+	if _, _, errno := sys.Syscall(sys.SYS_IOCTL, fd, req, arg); errno != 0 {
+		switch errno {
+		case sys.EINVAL:
+			err = ErrorUnsupported
+		default:
+			err = errno
+		}
+		return
+	}
+	return nil
+}

+ 307 - 0
v4l2/streaming.go

@@ -0,0 +1,307 @@
+package v4l2
+
+import (
+	"errors"
+	"fmt"
+	"time"
+	"unsafe"
+
+	sys "golang.org/x/sys/unix"
+)
+
+// Streaming with Buffers
+// See https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/buffer.html
+
+// BufType (v4l2_buf_type)
+// https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/buffer.html?highlight=v4l2_buf_type#c.V4L.v4l2_buf_type
+// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L141
+type BufType = uint32
+
+const (
+	BufTypeVideoCapture BufType = iota + 1 // V4L2_BUF_TYPE_VIDEO_CAPTURE = 1
+	BufTypeVideoOutput                     // V4L2_BUF_TYPE_VIDEO_OUTPUT  = 2
+	BufTypeOverlay                         // V4L2_BUF_TYPE_VIDEO_OVERLAY = 3
+)
+
+// StreamMemoryType (v4l2_memory)
+// https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/mmap.html?highlight=v4l2_memory_mmap
+// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L188
+type StreamMemoryType = uint32
+
+const (
+	StreamMemoryTypeMMAP    StreamMemoryType = iota + 1 // V4L2_MEMORY_MMAP             = 1,
+	StreamMemoryTypeUserPtr                             // V4L2_MEMORY_USERPTR          = 2,
+	StreamMemoryTypeOverlay                             // V4L2_MEMORY_OVERLAY          = 3,
+	StreamMemoryTypeDMABuf                              // V4L2_MEMORY_DMABUF           = 4,
+)
+
+// TODO implement vl42_create_buffers
+
+// RequestBuffers (v4l2_requestbuffers)
+// This type is used to allocate buffer/io resources when initializing streaming io for
+// memory mapped, user pointer, or DMA buffer access.
+// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L949
+// https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-reqbufs.html?highlight=v4l2_requestbuffers#c.V4L.v4l2_requestbuffers
+type RequestBuffers struct {
+	Count        uint32
+	StreamType   uint32
+	Memory       uint32
+	Capabilities uint32
+	Reserved     [1]uint32
+}
+
+// BufferInfo (v4l2_buffer)
+// This type is used to send buffers management info between application and driver
+// after streaming IO has been initialized.
+// https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/buffer.html#c.V4L.v4l2_buffer
+// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L1037
+//
+// BufferInfo represents type v4l2_buffer which contains unions as shown below.
+// Remember, the union is represented as an arry of bytes sized as the largest
+// member in bytes.
+// struct v4l2_buffer {
+// 	__u32			index;
+// 	__u32			type;
+// 	__u32			bytesused;
+// 	__u32			flags;
+// 	__u32			field;
+// 	struct timeval		timestamp;
+// 	struct v4l2_timecode	timecode;
+// 	__u32			sequence;
+// 	__u32			memory;
+// 	union {
+// 		__u32           offset;
+// 		unsigned long   userptr;
+// 		struct v4l2_plane *planes;
+// 		__s32		fd;
+// 	} m;
+// 	__u32			length;
+// 	__u32			reserved2;
+// 	union {
+// 		__s32		request_fd;
+// 		__u32		reserved;
+// 	};
+// };
+type BufferInfo struct {
+	Index      uint32
+	StreamType uint32
+	BytesUsed  uint32
+	Flags      uint32
+	Field      uint32
+	Timestamp  sys.Timeval
+	Timecode   Timecode
+	Sequence   uint32
+	Memory     uint32
+	m          [unsafe.Sizeof(&BufferService{})]byte // union m, cast to BufferService
+	Length     uint32
+	Reserved2  uint32
+	RequestFD  int32
+}
+
+func (b BufferInfo) GetService() BufferService {
+	m := (*BufferService)(unsafe.Pointer(&b.m[0]))
+	return *m
+}
+
+// BufferService represents Union of several values in type Buffer
+// that are used to service the stream depending on the type of streaming
+// selected (MMap, User pointer, planar, file descriptor for DMA)
+type BufferService struct {
+	Offset  uint32
+	UserPtr uintptr
+	Planes  *PlaneInfo
+	FD      int32
+}
+
+// PlaneInfo (v4l2_plane)
+// Represents a plane in a multi-planar buffers
+// https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/buffer.html#c.V4L.v4l2_plane
+// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L990
+//
+// PlaneInfo includes a uinion of types as shown below:
+// struct v4l2_plane {
+// 	__u32			bytesused;
+// 	__u32			length;
+// 	union {
+// 		__u32		mem_offset;
+// 		unsigned long	userptr;
+// 		__s32		fd;
+// 	} m;
+// 	__u32			data_offset;
+// 	__u32			reserved[11];
+// };
+type PlaneInfo struct {
+	BytesUsed  uint32
+	Length     uint32
+	m          [unsafe.Sizeof(uintptr(0))]byte // union m, cast to BufferPlaneService
+	DataOffset uint32
+	Reserved   [11]uint32
+}
+
+func (p PlaneInfo) GetService() PlaneService {
+	m := (*PlaneService)(unsafe.Pointer(&p.m[0]))
+	return *m
+}
+
+// PlaneService representes the combination of type
+// of type of memory stream that can be serviced for the
+// associated plane.
+type PlaneService struct {
+	MemOffset uint32
+	UserPtr   uintptr
+	FD        int32
+}
+
+// StreamOn requests streaming to be turned on for
+// capture (or output) that uses memory map, user ptr, or DMA buffers.
+// https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-streamon.html
+func StreamOn(fd uintptr) error {
+	bufType := BufTypeVideoCapture
+	if err := Send(fd, vidiocStreamOn, uintptr(unsafe.Pointer(&bufType))); err != nil {
+		switch {
+		case errors.Is(err, ErrorUnsupported):
+			return fmt.Errorf("stream on: unsupported: %w", err)
+		default:
+			return fmt.Errorf("stream on: %w", err)
+		}
+	}
+	return nil
+}
+
+// StreamOff requests streaming to be turned off for
+// capture (or output) that uses memory map, user ptr, or DMA buffers.
+// https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-streamon.html
+func StreamOff(fd uintptr) error {
+	bufType := BufTypeVideoCapture
+	if err := Send(fd, vidiocStreamOff, uintptr(unsafe.Pointer(&bufType))); err != nil {
+		switch {
+		case errors.Is(err, ErrorUnsupported):
+			return fmt.Errorf("stream off: unsupported: %w", err)
+		default:
+			return fmt.Errorf("stream off: %w", err)
+		}
+	}
+	return nil
+}
+
+// AllocateBuffers sends buffer allocation request to underlying driver
+// for video capture when using either mem map, user pointer, or DMA buffers.
+func AllocateBuffers(fd uintptr, buffSize uint32) (RequestBuffers, error) {
+	req := RequestBuffers{
+		Count:      buffSize,
+		StreamType: BufTypeVideoCapture,
+		Memory:     StreamMemoryTypeMMAP,
+	}
+
+	if err := Send(fd, vidiocReqBufs, uintptr(unsafe.Pointer(&req))); err != nil {
+		switch {
+		case errors.Is(err, ErrorUnsupported):
+			return RequestBuffers{}, fmt.Errorf("request buffers: unsupported: %w", err)
+		default:
+			return RequestBuffers{}, fmt.Errorf("request buffers: %w", err)
+		}
+	}
+	if req.Count < 2 {
+		return RequestBuffers{}, errors.New("request buffers: insufficient memory on device")
+	}
+
+	return req, nil
+}
+
+// GetBuffersInfo retrieves information for allocated buffers at provided index.
+// This call should take place after buffers are allocated (for mmap for instance).
+func GetBufferInfo(fd uintptr, index uint32) (BufferInfo, error) {
+	buf := BufferInfo{
+		StreamType: BufTypeVideoCapture,
+		Memory:     StreamMemoryTypeMMAP,
+		Index:      index,
+	}
+
+	if err := Send(fd, vidiocQueryBuf, uintptr(unsafe.Pointer(&buf))); err != nil {
+		switch {
+		case errors.Is(err, ErrorUnsupported):
+			return BufferInfo{}, fmt.Errorf("buffer info: unsupported: %w", err)
+		default:
+			return BufferInfo{}, fmt.Errorf("buffer info: %w", err)
+		}
+	}
+
+	return buf, 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) {
+	return sys.Mmap(int(fd), offset, len, sys.PROT_READ|sys.PROT_WRITE, sys.MAP_SHARED)
+}
+
+// UnmapMemoryBuffer removes the buffer that was previously mapped.
+func UnmapMemoryBuffer(buf []byte) error {
+	return sys.Munmap(buf)
+}
+
+// QueueBuffer enqueues a buffer in the device driver (empty for capturing, filled for video output)
+// when using either memory map, user pointer, or DMA buffers. BufferInfo is returned with
+// additional information about the queued buffer.
+// https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-qbuf.html#vidioc-qbuf
+func QueueBuffer(fd uintptr, index uint32) (BufferInfo, error) {
+	buf := BufferInfo{
+		StreamType: BufTypeVideoCapture,
+		Memory:     StreamMemoryTypeMMAP,
+		Index:      index,
+	}
+
+	if err := Send(fd, vidiocQueueBuf, uintptr(unsafe.Pointer(&buf))); err != nil {
+		switch {
+		case errors.Is(err, ErrorUnsupported):
+			return BufferInfo{}, fmt.Errorf("buffer: unsupported: %w", err)
+		default:
+			return BufferInfo{}, fmt.Errorf("buffer: %w", err)
+		}
+	}
+
+	return buf, nil
+}
+
+// DequeueBuffer dequeues a buffer in the device driver, marking it as consumed by the application,
+// when using either memory map, user pointer, or DMA buffers. BufferInfo is returned with
+// additional information about the dequeued buffer.
+// https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-qbuf.html#vidioc-qbuf
+func DequeueBuffer(fd uintptr) (BufferInfo, error) {
+	buf := BufferInfo{
+		StreamType: BufTypeVideoCapture,
+		Memory:     StreamMemoryTypeMMAP,
+	}
+
+	if err := Send(fd, vidiocDequeueBuf, uintptr(unsafe.Pointer(&buf))); err != nil {
+		switch {
+		case errors.Is(err, ErrorUnsupported):
+			return BufferInfo{}, fmt.Errorf("buffer: unsupported: %w", err)
+		default:
+			return BufferInfo{}, fmt.Errorf("buffer: %w", err)
+		}
+	}
+
+	return buf, 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
+		}
+	}
+}

+ 37 - 0
v4l2/timing.go

@@ -0,0 +1,37 @@
+package v4l2
+
+// TimecodeType
+// https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/buffer.html?highlight=v4l2_timecode#timecode-type
+// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L886
+type TimecodeType = uint32
+
+const (
+	TimecodeType24FPS TimecodeType = iota + 1 // V4L2_TC_TYPE_24FPS
+	TimecodeType25FPS                         // V4L2_TC_TYPE_25FPS
+	TimecodeType30FPS                         // V4L2_TC_TYPE_30FPS
+	TimecodeType50FPS                         // V4L2_TC_TYPE_50FPS
+	TimecodeType60FPS                         // V4L2_TC_TYPE_60FPS
+)
+
+// TimecodeFlag
+// https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/buffer.html?highlight=v4l2_timecode#timecode-flags
+// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L892
+type TimecodeFlag = uint32
+
+const (
+	TimecodeFlagDropFrame  TimecodeFlag = 0x0001 // V4L2_TC_FLAG_DROPFRAME	0x0001
+	TimecodeFlagColorFrame TimecodeFlag = 0x0002 // V4L2_TC_FLAG_COLORFRAME	0x0002
+)
+
+// Timecode (v4l2_timecode)
+// https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/buffer.html?highlight=v4l2_timecode#c.V4L.v4l2_timecode
+// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L875
+type Timecode struct {
+	Type     TimecodeType
+	Flags    TimecodeFlag
+	Frames   uint8
+	Seconds  uint8
+	Minutes  uint8
+	Hours    uint8
+	Userbits [4]uint8
+}

+ 7 - 0
v4l2/types.go

@@ -0,0 +1,7 @@
+package v4l2
+
+import "errors"
+
+var (
+	ErrorUnsupported = errors.New("unsupported feature")
+)