Răsfoiți Sursa

Code and example updates (new examples, new features, ect)

This patch adds many updtes to the code and examples, including

- New updates to the webcam example GUI
- Streaming loop code update to copy frames to corruption by device during capture
- Update webcam to include face detection feature
- New examples including snapshot and simplecam
- Updates to the example documentation
- And much more
Vladimir Vivien 3 ani în urmă
părinte
comite
a93d5b26b7

+ 14 - 7
device/device.go

@@ -341,9 +341,7 @@ func (d *Device) Start(ctx context.Context) error {
 	if err != nil {
 		return fmt.Errorf("device: requested buffer type not be supported: %w", err)
 	}
-	if bufReq.Count < 2 {
-		return fmt.Errorf("device: %s: issuficient buffer memory", d.path)
-	}
+
 	d.config.bufSize = bufReq.Count
 	d.requestedBuf = bufReq
 
@@ -395,10 +393,10 @@ func (d *Device) startStreamLoop(ctx context.Context) error {
 		defer close(d.output)
 
 		fd := d.Fd()
+		var frame []byte
 		ioMemType := d.MemIOType()
 		bufType := d.BufferType()
 		waitForRead := v4l2.WaitForRead(d)
-
 		for {
 			select {
 			// handle stream capture (read from driver)
@@ -411,12 +409,21 @@ func (d *Device) startStreamLoop(ctx context.Context) error {
 					panic(fmt.Sprintf("device: stream loop dequeue: %s", err))
 				}
 
+				// copy mapped buffer (copying avoids polluted data from subsequent dequeue ops)
+				if buff.Flags&v4l2.BufFlagMapped != 0 && buff.Flags&v4l2.BufFlagError == 0 {
+					frame = make([]byte, buff.BytesUsed)
+					if n := copy(frame, d.buffers[buff.Index][:buff.BytesUsed]); n == 0 {
+						d.output <- []byte{}
+					}
+					d.output <- frame
+					frame = nil
+				} else {
+					d.output <- []byte{}
+				}
+
 				if _, err := v4l2.QueueBuffer(fd, ioMemType, bufType, buff.Index); err != nil {
 					panic(fmt.Sprintf("device: stream loop queue: %s: buff: %#v", err, buff))
 				}
-
-				d.output <- d.Buffers()[buff.Index][:buff.BytesUsed]
-
 			case <-ctx.Done():
 				d.Stop()
 				return

+ 3 - 1
examples/capture0/README.md

@@ -11,7 +11,9 @@ func main() {
 	// open device
 	device, err := device.Open(
 		devName,
-		device.WithPixFormat(v4l2.PixFormat{PixelFormat: v4l2.PixelFmtMPEG, Width: 640, Height: 480}),
+		device.WithPixFormat(
+			v4l2.PixFormat{PixelFormat: v4l2.PixelFmtMPEG, Width: 640, Height: 480},
+		),
 	)
 ...
 }

+ 3 - 1
examples/capture0/capture0.go

@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"log"
 	"os"
+	"time"
 
 	"github.com/vladimirvivien/go4vl/device"
 	"github.com/vladimirvivien/go4vl/v4l2"
@@ -19,7 +20,7 @@ func main() {
 	// open device
 	device, err := device.Open(
 		devName,
-		device.WithPixFormat(v4l2.PixFormat{PixelFormat: v4l2.PixelFmtMJPEG, Width: 640, Height: 480, Field: v4l2.FieldInterlaced}),
+		device.WithPixFormat(v4l2.PixFormat{PixelFormat: v4l2.PixelFmtMJPEG, Width: 640, Height: 480}),
 	)
 	if err != nil {
 		log.Fatalf("failed to open device: %s", err)
@@ -56,6 +57,7 @@ func main() {
 		if count >= totalFrames {
 			break
 		}
+		time.Sleep(1 * time.Second)
 	}
 
 	stop() // stop capture

+ 41 - 8
examples/capture1/capture1.go

@@ -1,9 +1,12 @@
 package main
 
 import (
+	"bytes"
 	"context"
 	"flag"
 	"fmt"
+	"image"
+	"image/jpeg"
 	"log"
 	"os"
 
@@ -13,7 +16,13 @@ import (
 
 func main() {
 	devName := "/dev/video0"
+	totalFrames := 3
+	width := 640
+	height := 480
 	flag.StringVar(&devName, "d", devName, "device name (path)")
+	flag.IntVar(&totalFrames, "c", totalFrames, "number of frames to caputure")
+	flag.IntVar(&width, "w", width, "picture width")
+	flag.IntVar(&height, "h", height, "picture height")
 	flag.Parse()
 
 	// open device
@@ -59,6 +68,7 @@ func main() {
 		log.Fatalf("device does not support any of %#v", preferredFmts)
 	}
 	log.Printf("Found preferred fmt: %s", fmtDesc)
+
 	frameSizes, err := v4l2.GetFormatFrameSizes(device.Fd(), fmtDesc.PixelFormat)
 	if err != nil {
 		log.Fatalf("failed to get framesize info: %s", err)
@@ -67,16 +77,18 @@ func main() {
 	// select size 640x480 for format
 	var frmSize v4l2.FrameSizeEnum
 	for _, size := range frameSizes {
-		if size.Size.MinWidth == 640 && size.Size.MinHeight == 480 {
+		if size.Size.MinWidth == uint32(width) && size.Size.MinHeight == uint32(height) {
 			frmSize = size
 			break
 		}
 	}
 
 	if frmSize.Size.MinWidth == 0 {
-		log.Fatalf("Size 640x480 not supported for fmt: %s", fmtDesc)
+		log.Fatalf("Size %dx%d not supported for fmt: %s", width, height, fmtDesc)
 	}
 
+	log.Printf("Found preferred size: %#v", frmSize)
+
 	// configure device with preferred fmt
 
 	if err := device.SetPixFormat(v4l2.PixFormat{
@@ -101,28 +113,49 @@ func main() {
 	}
 
 	// process frames from capture channel
-	totalFrames := 10
 	count := 0
-	log.Printf("Capturing %d frames at %d fps...", totalFrames, fps)
+	log.Printf("Capturing %d frames (buffers: %d, %d fps)...", totalFrames, device.BufferCount(), fps)
 	for frame := range device.GetOutput() {
+		if count >= totalFrames {
+			break
+		}
+		count++
+
+		if len(frame) == 0 {
+			log.Println("received frame size 0")
+			continue
+		}
+
+		log.Printf("captured %d bytes", len(frame))
+		img, fmtName, err := image.Decode(bytes.NewReader(frame))
+		if err != nil {
+			log.Printf("failed to decode jpeg: %s", err)
+			continue
+		}
+		log.Printf("decoded image format: %s", fmtName)
+
+		var imgBuf bytes.Buffer
+		if err := jpeg.Encode(&imgBuf, img, nil); err != nil {
+			log.Printf("failed to encode jpeg: %s", err)
+			continue
+		}
+
 		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)
+			file.Close()
 			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
-		}
 	}
 
 	cancel() // stop capture

+ 1 - 1
examples/ccapture/README.md

@@ -1,6 +1,6 @@
 # V4L2 video capture example in C
 
-This an example in C showing a minimally required steps to capture video using V4L2. This is can be used to run tests on devices and compare results with the Go4VL code.
+This an example in C showing the minimally required steps to capture video using the V4L2 framework. This can be used as a test tool to compare results between C and the Go4VL Go code.
 
 ## Build and run
 On a Linux machine, run the following:

+ 9 - 0
examples/fileserv/README.md

@@ -0,0 +1,9 @@
+# fileserv
+
+A simple file server that can be used to view generated images in a remote environment.
+
+## Run
+
+```
+go run fileserv.go ":port"
+```

+ 24 - 0
examples/fileserv/fileserv.go

@@ -0,0 +1,24 @@
+package main
+
+import (
+	"log"
+	"net/http"
+	"os"
+)
+
+var (
+	port = ":5050"
+)
+
+func main() {
+	if len(os.Args) > 2 {
+		port = os.Args[1]
+	}
+
+	// serve examples dir
+	log.Printf("serving files on port %s", port)
+	http.Handle("/", http.FileServer(http.Dir("../")))
+	if err := http.ListenAndServe(port, nil); err != nil {
+		log.Fatal(err)
+	}
+}

+ 4 - 0
examples/simplecam/README.md

@@ -0,0 +1,4 @@
+# camserv
+
+This is a simple example shows how easy it is to use go4vl to 
+create a simple web application to stream camera images.

+ 64 - 0
examples/simplecam/simplecam.go

@@ -0,0 +1,64 @@
+package main
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"log"
+	"mime/multipart"
+	"net/http"
+	"net/textproto"
+
+	"github.com/vladimirvivien/go4vl/device"
+	"github.com/vladimirvivien/go4vl/v4l2"
+)
+
+var (
+	frames <-chan []byte
+)
+
+func imageServ(w http.ResponseWriter, req *http.Request) {
+	mimeWriter := multipart.NewWriter(w)
+	w.Header().Set("Content-Type", fmt.Sprintf("multipart/x-mixed-replace; boundary=%s", mimeWriter.Boundary()))
+	partHeader := make(textproto.MIMEHeader)
+	partHeader.Add("Content-Type", "image/jpeg")
+
+	var frame []byte
+	for frame = range frames {
+		partWriter, err := mimeWriter.CreatePart(partHeader)
+		if err != nil {
+			log.Printf("failed to create multi-part writer: %s", err)
+			return
+		}
+
+		if _, err := partWriter.Write(frame); err != nil {
+			log.Printf("failed to write image: %s", err)
+		}
+	}
+}
+
+func main() {
+	port := ":9090"
+	devName := "/dev/video0"
+	flag.StringVar(&devName, "d", devName, "device name (path)")
+	flag.StringVar(&port, "p", port, "webcam service port")
+
+	camera, err := device.Open(
+		devName,
+		device.WithPixFormat(v4l2.PixFormat{PixelFormat: v4l2.PixelFmtMJPEG, Width: 640, Height: 480}),
+	)
+	if err != nil {
+		log.Fatalf("failed to open device: %s", err)
+	}
+	defer camera.Close()
+
+	if err := camera.Start(context.TODO()); err != nil {
+		log.Fatalf("camera start: %s", err)
+	}
+
+	frames = camera.GetOutput()
+
+	log.Printf("Serving images: [%s/stream]", port)
+	http.HandleFunc("/stream", imageServ)
+	log.Fatal(http.ListenAndServe(port, nil))
+}

+ 33 - 0
examples/snapshot/snap.go

@@ -0,0 +1,33 @@
+package main
+
+import (
+	"context"
+	"log"
+	"os"
+
+	"github.com/vladimirvivien/go4vl/device"
+)
+
+func main() {
+	dev, err := device.Open("/dev/video0", device.WithBufferSize(1))
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer dev.Close()
+
+	if err := dev.Start(context.TODO()); err != nil {
+		log.Fatal(err)
+	}
+
+	frame := <-dev.GetOutput()
+
+	file, err := os.Create("pic.jpg")
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer file.Close()
+
+	if _, err := file.Write(frame); err != nil {
+		log.Fatal(err)
+	}
+}

+ 9 - 0
examples/webcam/build.sh

@@ -0,0 +1,9 @@
+#! /bin/bash
+# Run the following once to pull correct dependencies
+go get github.com/vladimirvivien/go4vl@latest
+go get github.com/esimov/pigo/core@latest
+go get github.com/fogleman/gg@8febc0f526adecda6f8ae80f3869b7cd77e52984
+
+go mod tidy
+
+go build .

BIN
examples/webcam/facefinder.model


+ 14 - 0
examples/webcam/go.mod

@@ -0,0 +1,14 @@
+module github.com/vladimirvivien/go4vl/exampels/webcam
+
+go 1.19
+
+require (
+	github.com/esimov/pigo v1.4.5
+	github.com/fogleman/gg v1.3.1-0.20210928143535-8febc0f526ad
+)
+
+require (
+	github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
+	golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 // indirect
+	golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
+)

+ 224 - 46
examples/webcam/webcam.go

@@ -1,34 +1,68 @@
 package main
 
 import (
+	"bytes"
 	"context"
+	"encoding/json"
 	"flag"
 	"fmt"
 	"html/template"
+	"image"
+	"image/color"
 	"io"
 	"log"
+	"mime/multipart"
 	"net/http"
+	"net/textproto"
+	"os"
+	"strconv"
 	"strings"
 	"time"
 
+	pigo "github.com/esimov/pigo/core"
+	"github.com/fogleman/gg"
 	"github.com/vladimirvivien/go4vl/device"
 	"github.com/vladimirvivien/go4vl/v4l2"
 )
 
 var (
-	frames <-chan []byte
-	fps    uint32 = 30
-	pixfmt v4l2.FourCCType
+	camera      *device.Device
+	frames      <-chan []byte
+	fps         uint32 = 30
+	pixfmt      v4l2.FourCCType
+	width       = 640
+	height      = 480
+	streamInfo  string
+	faceEnabled bool
+	faceFinder  *pigo.Pigo
 )
 
+type PageData struct {
+	StreamInfo     string
+	StreamPath     string
+	ImgWidth       int
+	ImgHeight      int
+	ControlPath    string
+	FaceDetectPath string
+	FaceEnabled    bool
+}
+
 // servePage reads templated HTML
 func servePage(w http.ResponseWriter, r *http.Request) {
+	pd := PageData{
+		StreamInfo:     streamInfo,
+		StreamPath:     fmt.Sprintf("/stream?%d", time.Now().UnixNano()),
+		ImgWidth:       width,
+		ImgHeight:      height,
+		ControlPath:    "/control",
+		FaceDetectPath: "/face",
+	}
+	if faceFinder != nil {
+		pd.FaceEnabled = true
+	}
+
 	// 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)
@@ -45,12 +79,10 @@ func servePage(w http.ResponseWriter, r *http.Request) {
 
 // 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)
+	mimeWriter := multipart.NewWriter(w)
+	w.Header().Set("Content-Type", fmt.Sprintf("multipart/x-mixed-replace; boundary=%s", mimeWriter.Boundary()))
+	partHeader := make(textproto.MIMEHeader)
+	partHeader.Add("Content-Type", "image/jpeg")
 
 	var frame []byte
 	for frame = range frames {
@@ -59,42 +91,163 @@ func serveVideoStream(w http.ResponseWriter, req *http.Request) {
 			continue
 		}
 
-		// 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
-		switch pixfmt {
-		case v4l2.PixelFmtMJPEG:
-			if _, err := w.Write(frame); err != nil {
-				log.Printf("failed to write mjpeg image: %s", err)
-				return
+		partWriter, err := mimeWriter.CreatePart(partHeader)
+		if err != nil {
+			log.Printf("failed to create multi-part writer: %s", err)
+			return
+		}
+
+		if faceEnabled {
+			if err := runFaceDetect(partWriter, frame); err != nil {
+				log.Printf("face detection failed: %s", err)
+				continue
+			}
+		} else {
+			if _, err := partWriter.Write(frame); err != nil {
+				log.Printf("failed to write image: %s", err)
 			}
-		default:
-			log.Printf("selected pixel format is not supported")
 		}
 
-		// close boundary
-		if _, err := io.WriteString(w, "\n"); err != nil {
-			log.Printf("failed to write boundary: %s", err)
+	}
+}
+
+type faceDetectRequest struct {
+	Mode string
+}
+
+func faceDetectControl(w http.ResponseWriter, req *http.Request) {
+	var face faceDetectRequest
+	err := json.NewDecoder(req.Body).Decode(&face)
+	if err != nil {
+		log.Printf("failed to decode control: %s", err)
+		return
+	}
+	log.Printf("Face mode = %s", face.Mode)
+	switch face.Mode {
+	case "true", "on", "enable":
+		if faceFinder == nil {
+			faceEnabled = false
+			log.Println("face detection not enabled, re-run webcam with -face flag")
+			return
+		}
+		faceEnabled = true
+	case "off", "disabled":
+		faceEnabled = false
+	}
+}
+
+func initFaceDetect() error {
+	model, err := os.ReadFile("./facefinder.model")
+	if err != nil {
+		return fmt.Errorf("failed to load face finder model: %s", err)
+	}
+	p := pigo.NewPigo()
+	faceFinder, err = p.Unpack(model)
+	if err != nil {
+		faceFinder = nil
+		return fmt.Errorf("failed to initialize face classifier: %s", err)
+	}
+	return nil
+}
+
+func runFaceDetect(w io.Writer, frame []byte) error {
+	img, _, err := image.Decode(bytes.NewReader(frame))
+	if err != nil {
+		return err
+	}
+
+	src := img.(*image.YCbCr)
+	bounds := img.Bounds()
+	params := pigo.CascadeParams{
+		MinSize:     100,
+		MaxSize:     600,
+		ShiftFactor: 0.15,
+		ScaleFactor: 1.1,
+		ImageParams: pigo.ImageParams{
+			Pixels: src.Y,
+			Rows:   bounds.Dy(),
+			Cols:   bounds.Dx(),
+			Dim:    bounds.Dx(),
+		},
+	}
+
+	dets := faceFinder.RunCascade(params, 0.0)
+	dets = faceFinder.ClusterDetections(dets, 0)
+
+	drawer := gg.NewContext(bounds.Max.X, bounds.Max.Y)
+	drawer.DrawImage(img, 0, 0)
+
+	for _, det := range dets {
+		if det.Q >= 5.0 {
+			drawer.DrawRectangle(
+				float64(det.Col-det.Scale/2),
+				float64(det.Row-det.Scale/2),
+				float64(det.Scale),
+				float64(det.Scale),
+			)
+
+			drawer.SetLineWidth(3.0)
+			drawer.SetStrokeStyle(gg.NewSolidPattern(color.RGBA{R: 255, G: 0, B: 0, A: 255}))
+			drawer.Stroke()
+		}
+	}
+
+	return drawer.EncodeJPG(w, nil)
+}
+
+type controlRequest struct {
+	Name  string
+	Value string
+}
+
+func controlVideo(w http.ResponseWriter, req *http.Request) {
+	var ctrl controlRequest
+	err := json.NewDecoder(req.Body).Decode(&ctrl)
+	if err != nil {
+		log.Printf("failed to decode control: %s", err)
+		return
+	}
+
+	val, err := strconv.Atoi(ctrl.Value)
+	if err != nil {
+		log.Printf("failed to set brightness: %s", err)
+		return
+	}
+
+	switch ctrl.Name {
+	case "brightness":
+		if err := camera.SetControlBrightness(int32(val)); err != nil {
+			log.Printf("failed to set brightness: %s", err)
+			return
+		}
+	case "contrast":
+		if err := camera.SetControlContrast(int32(val)); err != nil {
+			log.Printf("failed to set contrast: %s", err)
+			return
+		}
+	case "saturation":
+		if err := camera.SetControlSaturation(int32(val)); err != nil {
+			log.Printf("failed to set saturation: %s", err)
 			return
 		}
 	}
+
+	log.Printf("applied control %#v", ctrl)
+
 }
 
 func main() {
 	port := ":9090"
 	devName := "/dev/video0"
 	frameRate := int(fps)
+	buffSize := 4
 	defaultDev, err := device.Open(devName)
 	skipDefault := false
+	face := false
 	if err != nil {
 		skipDefault = true
 	}
 
-	width := 640
-	height := 480
 	format := "yuyv"
 	if !skipDefault {
 		pix, err := defaultDev.GetPixFormat()
@@ -111,6 +264,10 @@ func main() {
 			}
 		}
 	}
+	// close device used for default info
+	if err := defaultDev.Close(); err != nil {
+		log.Fatalf("failed to close default device: %s", err)
+	}
 
 	flag.StringVar(&devName, "d", devName, "device name (path)")
 	flag.IntVar(&width, "w", width, "capture width")
@@ -118,57 +275,72 @@ func main() {
 	flag.StringVar(&format, "f", format, "pixel format")
 	flag.StringVar(&port, "p", port, "webcam service port")
 	flag.IntVar(&frameRate, "r", frameRate, "frames per second (fps)")
+	flag.IntVar(&buffSize, "b", buffSize, "device buffer size")
+	flag.BoolVar(&face, "face", face, "turns on face detection mode")
 	flag.Parse()
 
-	// close device used for default info
-	if err := defaultDev.Close(); err != nil {
-		log.Fatalf("failed to close default device: %s", err)
+	// if face enabled, force fmt, buff size, and frame rate to low.
+	if face {
+		if err := initFaceDetect(); err != nil {
+			log.Printf("failed to initialize face detection: %s", err)
+		}
+		format = "mjpeg"
+		buffSize = 1
+		frameRate = 5
 	}
 
-	// open device and setup device
-	device, err := device.Open(devName,
+	// open camera and setup camera
+	camera, err = device.Open(devName,
 		device.WithIOType(v4l2.IOTypeMMAP),
-		device.WithPixFormat(v4l2.PixFormat{PixelFormat: getFormatType(format), Width: uint32(width), Height: uint32(height)}),
+		device.WithPixFormat(v4l2.PixFormat{PixelFormat: getFormatType(format), Width: uint32(width), Height: uint32(height), Field: v4l2.FieldAny}),
 		device.WithFPS(uint32(frameRate)),
+		device.WithBufferSize(uint32(buffSize)),
 	)
 
 	if err != nil {
 		log.Fatalf("failed to open device: %s", err)
 	}
-	defer device.Close()
-	caps := device.Capability()
+	defer camera.Close()
+
+	caps := camera.Capability()
 	log.Printf("device [%s] opened\n", devName)
 	log.Printf("device info: %s", caps.String())
 
 	// set device format
-	currFmt, err := device.GetPixFormat()
+	currFmt, err := camera.GetPixFormat()
 	if err != nil {
 		log.Fatalf("unable to get format: %s", err)
 	}
 	log.Printf("Current format: %s", currFmt)
 	pixfmt = currFmt.PixelFormat
+	streamInfo = fmt.Sprintf("%s - %s [%dx%d] %d fps",
+		caps.Card,
+		v4l2.PixelFormats[currFmt.PixelFormat],
+		currFmt.Width, currFmt.Height, frameRate,
+	)
 
 	// start capture
 	ctx, cancel := context.WithCancel(context.TODO())
-	if err := device.Start(ctx); err != nil {
+	if err := camera.Start(ctx); err != nil {
 		log.Fatalf("stream capture: %s", err)
 	}
 	defer func() {
 		cancel()
-		device.Close()
+		camera.Close()
 	}()
 
 	// video stream
-	frames = device.GetOutput()
+	frames = camera.GetOutput()
 
-	log.Println("device capture started, frames available")
+	log.Printf("device capture started (buffer size set %d)", camera.BufferCount())
 	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(".")))
+	http.HandleFunc("/control", controlVideo)    // applies video controls
+	http.HandleFunc("/face", faceDetectControl)  // controls face detection
 	if err := http.ListenAndServe(port, nil); err != nil {
 		log.Fatal(err)
 	}
@@ -176,12 +348,18 @@ func main() {
 
 func getFormatType(fmtStr string) v4l2.FourCCType {
 	switch strings.ToLower(fmtStr) {
-	case "mjpeg", "jpeg":
+	case "jpeg":
+		return v4l2.PixelFmtJPEG
+	case "mpeg":
+		return v4l2.PixelFmtMPEG
+	case "mjpeg":
 		return v4l2.PixelFmtMJPEG
 	case "h264", "h.264":
 		return v4l2.PixelFmtH264
 	case "yuyv":
 		return v4l2.PixelFmtYUYV
+	case "rgb":
+		return v4l2.PixelFmtRGB24
 	}
 	return v4l2.PixelFmtMPEG
 }

+ 78 - 23
examples/webcam/webcam.html

@@ -3,7 +3,7 @@
 <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>
+    <title>📸 GoCam</title>
 
     <!-- CSS  -->
     <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
@@ -11,39 +11,94 @@
 </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 class="row red accent-2" style="padding: 0px; border:0px; margin: 0px;">
+        <div class="col s12 m12 l12">
+            <h1 class="center-align white-text">📸 GoCam</h1>
         </div>
+    </div>
+    <div class="row" style="padding: 0px; border:0px; margin: 0px;">
+        <div class="col s12 m4 l3 blue-grey darken-3"> 
+            <div style="height:100vh ;">
+                <form class="container" action="#">
+                    <div class="card blue-grey darken-1">
+                        <div class="card-content white-text hoverable">
+                            <span class="card-title">Brightness</span>
+                            <p class="range-field" style="margin:0px;">
+                               <input type="range" id="brightness" min="-64" max="64" onclick="setCtrl('brightness', this.value)" />
+                            </p>
+                        </div>
+                    </div>
+                    <div class="card blue-grey darken-1 hoverable">
+                        <div class="card-content white-text">
+                            <span class="card-title">Contrast</span>
+                            <p class="range-field">
+                               <input type="range" id="contrast" min="0" max="64"  onclick="setCtrl('contrast', this.value)"/>
+                            </p>
+                        </div>
+                    </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 class="card blue-grey darken-1 hoverable">
+                        <div class="card-content white-text">
+                            <span class="card-title">Saturation</span>
+                            <p class="range-field">
+                               <input type="range" id="saturation" min="0" max="128"  onclick="setCtrl('saturation', this.value)"/>
+                            </p>
+                        </div>
                     </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>
+
+                    {{if .FaceEnabled}}
+                    <div class="card blue-grey darken-1 hoverable">
+                        <div class="card-content white-text">
+                            <span class="card-title">Face Detect</span>
+                            <div class="switch">
+                                <label>
+                                    Off
+                                    <input type="checkbox" onclick="setFace(this.value)">
+                                    <span class="lever"></span>
+                                    On
+                                </label>
+                            </div>
+                        </div>
                     </div>
-                    <div class="card-action">
-                        <a href="#"></a>
+                    {{end}}
+                  </form>
+            </div>
+        </div>
+        <div class="col s12 m8 l9">
+            <div class="container center-align">
+                <div class="card">
+                    <div class="card-image">
+                        <img id="img" src="{{.StreamPath}}"/>
+                        <span class="card-title">GoCam ({{.StreamInfo}})</span>
                     </div>
                 </div>
             </div>
         </div>
-
-</div>
+    </div>
 
 <!--  Scripts-->
 <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
 
+<script>
+    let faceDetect = "off"
+    function setCtrl(ctrl, val) {
+        let xhttp = new XMLHttpRequest();
+        xhttp.open("POST", "{{.ControlPath}}", true);
+        xhttp.setRequestHeader("Content-Type", "application/json");
+        var data = JSON.stringify({ "name": ctrl, "value": val });
+        xhttp.send(data)
+    }
+
+    function setFace(val) {
+        faceDetect = (faceDetect === "off") ? "on" : "off"
+
+        let xhttp = new XMLHttpRequest();
+        xhttp.open("POST", "{{.FaceDetectPath}}", true);
+        xhttp.setRequestHeader("Content-Type", "application/json");
+        var data = JSON.stringify({ "mode": faceDetect });
+        xhttp.send(data)  
+    }
+</script>
 </body>
 </html>

+ 4 - 4
v4l2/format.go

@@ -15,8 +15,8 @@ type FourCCType = uint32
 // 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   FourCCType = C.V4L2_PIX_FMT_RGB24
-	PixFmtGrey    FourCCType = C.V4L2_PIX_FMT_GREY
+	PixelFmtRGB24 FourCCType = C.V4L2_PIX_FMT_RGB24
+	PixelFmtGrey  FourCCType = C.V4L2_PIX_FMT_GREY
 	PixelFmtYUYV  FourCCType = C.V4L2_PIX_FMT_YUYV
 	PixelFmtYYUV  FourCCType = C.V4L2_PIX_FMT_YYUV
 	PixelFmtYVYU  FourCCType = C.V4L2_PIX_FMT_YVYU
@@ -31,8 +31,8 @@ var (
 
 // PixelFormats provides a map of FourCCType encoding description
 var PixelFormats = map[FourCCType]string{
-	PixFmtRGB24:   "24-bit RGB 8-8-8",
-	PixFmtGrey:    "8-bit Greyscale",
+	PixelFmtRGB24: "24-bit RGB 8-8-8",
+	PixelFmtGrey:  "8-bit Greyscale",
 	PixelFmtYUYV:  "YUYV 4:2:2",
 	PixelFmtMJPEG: "Motion-JPEG",
 	PixelFmtJPEG:  "JFIF JPEG",

+ 28 - 1
v4l2/streaming.go

@@ -36,6 +36,33 @@ const (
 	IOTypeDMABuf  IOType = C.V4L2_MEMORY_DMABUF
 )
 
+type BufFlag = uint32
+
+const (
+	BufFlagMapped              BufFlag = C.V4L2_BUF_FLAG_MAPPED
+	BufFlagQueued              BufFlag = C.V4L2_BUF_FLAG_QUEUED
+	BufFlagDone                BufFlag = C.V4L2_BUF_FLAG_DONE
+	BufFlagKeyFrame            BufFlag = C.V4L2_BUF_FLAG_KEYFRAME
+	BufFlagPFrame              BufFlag = C.V4L2_BUF_FLAG_PFRAME
+	BufFlagBFrame              BufFlag = C.V4L2_BUF_FLAG_BFRAME
+	BufFlagError               BufFlag = C.V4L2_BUF_FLAG_ERROR
+	BufFlagInRequest           BufFlag = C.V4L2_BUF_FLAG_IN_REQUEST
+	BufFlagTimeCode            BufFlag = C.V4L2_BUF_FLAG_TIMECODE
+	BufFlagM2MHoldCaptureBuf   BufFlag = C.V4L2_BUF_FLAG_M2M_HOLD_CAPTURE_BUF
+	BufFlagPrepared            BufFlag = C.V4L2_BUF_FLAG_PREPARED
+	BufFlagNoCacheInvalidate   BufFlag = C.V4L2_BUF_FLAG_NO_CACHE_INVALIDATE
+	BufFlagNoCacheClean        BufFlag = C.V4L2_BUF_FLAG_NO_CACHE_CLEAN
+	BufFlagTimestampMask       BufFlag = C.V4L2_BUF_FLAG_TIMESTAMP_MASK
+	BufFlagTimestampUnknown    BufFlag = C.V4L2_BUF_FLAG_TIMESTAMP_UNKNOWN
+	BufFlagTimestampMonotonic  BufFlag = C.V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC
+	BufFlagTimestampCopy       BufFlag = C.V4L2_BUF_FLAG_TIMESTAMP_COPY
+	BufFlagTimestampSourceMask BufFlag = C.V4L2_BUF_FLAG_TSTAMP_SRC_MASK
+	BufFlagTimestampSourceEOF  BufFlag = C.V4L2_BUF_FLAG_TSTAMP_SRC_EOF
+	BufFlagTimestampSourceSOE  BufFlag = C.V4L2_BUF_FLAG_TSTAMP_SRC_SOE
+	BufFlagLast                BufFlag = C.V4L2_BUF_FLAG_LAST
+	BufFlagRequestFD           BufFlag = C.V4L2_BUF_FLAG_REQUEST_FD
+)
+
 // TODO implement vl42_create_buffers
 
 // RequestBuffers (v4l2_requestbuffers) is used to request buffer allocation initializing
@@ -274,7 +301,7 @@ func DequeueBuffer(fd uintptr, ioType IOType, bufType BufType) (Buffer, error) {
 
 	err := send(fd, C.VIDIOC_DQBUF, uintptr(unsafe.Pointer(&v4l2Buf)))
 	if err != nil {
-		return Buffer{}, fmt.Errorf("buffer dequeue: EGAIN: %w", err)
+		return Buffer{}, fmt.Errorf("buffer dequeue: %w", err)
 	}
 
 	return makeBuffer(v4l2Buf), nil