From eab8daf9ba4baa349fc1ab57be15cb0bf52bd759 Mon Sep 17 00:00:00 2001 From: Vladimir Vivien Date: Mon, 8 Nov 2021 20:35:18 -0500 Subject: [PATCH] Example using cgo; example device fmt; updating webcam example --- examples/format/devfmt.go | 59 +++++ examples/native_c_types/capture.go | 363 +++++++++++++++++++++++++++++ examples/webcam/webcam.go | 91 +++++++- imgsupport/converters.go | 24 +- v4l2/format.go | 32 ++- 5 files changed, 543 insertions(+), 26 deletions(-) create mode 100644 examples/format/devfmt.go create mode 100644 examples/native_c_types/capture.go diff --git a/examples/format/devfmt.go b/examples/format/devfmt.go new file mode 100644 index 0000000..c2e1fc4 --- /dev/null +++ b/examples/format/devfmt.go @@ -0,0 +1,59 @@ +package main + +import ( + "flag" + "log" + "strings" + + "github.com/vladimirvivien/go4vl/v4l2" +) + +func main() { + devName := "/dev/video0" + width := 640 + height := 480 + format := "yuyv" + + flag.StringVar(&devName, "d", devName, "device name (path)") + flag.IntVar(&width, "w", width, "capture width") + flag.IntVar(&height, "h", height, "capture height") + flag.StringVar(&format, "f", format, "pixel format") + flag.Parse() + + device, err := v4l2.Open(devName) + if err != nil { + log.Fatalf("failed to open device: %s", err) + } + defer device.Close() + + currFmt, err := device.GetPixFormat() + if err != nil { + log.Fatalf("unable to get format: %s", err) + } + log.Printf("Current format: %s", currFmt) + + fmtEnc := v4l2.PixelFmtYUYV + switch strings.ToLower(format) { + case "mjpeg": + fmtEnc = v4l2.PixelFmtMJPEG + case "h264", "h.264": + fmtEnc = v4l2.PixelFmtH264 + case "yuyv": + 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) + } + + currFmt, err = device.GetPixFormat() + if err != nil { + log.Fatalf("unable to get format: %s", err) + } + log.Printf("Updated format: %s", currFmt) +} \ No newline at end of file diff --git a/examples/native_c_types/capture.go b/examples/native_c_types/capture.go new file mode 100644 index 0000000..c4df9f6 --- /dev/null +++ b/examples/native_c_types/capture.go @@ -0,0 +1,363 @@ +package main + +/* +#include +*/ +import "C" + +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 + iocNone = 0 // no op + iocWrite = 1 // userland app is writing, kernel reading + iocRead = 2 // userland app is reading, kernel writing + + 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(iocRead, iocType, number, size) +} + +func ioEncW(iocType, number, size uintptr) uintptr { + return ioEnc(iocWrite, iocType, number, size) +} + +func ioEncRW(iocType, number, size uintptr) uintptr { + return ioEnc(iocRead|iocWrite, 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 uint32 = C.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 = C.V4L2_FIELD_ANY + FieldNone uint32 = C.V4L2_FIELD_NONE +) + +// buff stream types +// https://elixir.bootlin.com/linux/v5.13-rc6/source/include/uapi/linux/videodev2.h#L142 +const ( + BufTypeVideoCapture uint32 = C.V4L2_BUF_TYPE_VIDEO_CAPTURE + BufTypeVideoOutput uint32 = C.V4L2_BUF_TYPE_VIDEO_OUTPUT + BufTypeOverlay uint32 = C.V4L2_BUF_TYPE_VIDEO_OVERLAY +) + +// 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 { + var v4l2Fmt C.struct_v4l2_format + v4l2Fmt._type = C.uint(BufTypeVideoCapture) + *(*C.struct_v4l2_pix_format)(unsafe.Pointer(&v4l2Fmt.fmt[0])) = *(*C.struct_v4l2_pix_format)(unsafe.Pointer(&pixFmt)) + + // encode command to send + // vidiocSetFormat := ioEncRW('V', 5, uintptr(unsafe.Sizeof(v4l2Fmt))) + + // send command + if err := ioctl(fd, C.VIDIOC_S_FMT, uintptr(unsafe.Pointer(&v4l2Fmt))); err != nil { + return err + } + log.Printf("setting format to: %dx%d\n", pixFmt.Width, pixFmt.Height) + return nil +} + +func getFormat(fd uintptr) (PixFormat, error){ + var v4l2Fmt C.struct_v4l2_format + v4l2Fmt._type = C.uint(BufTypeVideoCapture) + + // send command + if err := ioctl(fd, C.VIDIOC_G_FMT, uintptr(unsafe.Pointer(&v4l2Fmt))); err != nil { + return PixFormat{}, err + } + + var pixFmt PixFormat + *(*C.struct_v4l2_pix_format)(unsafe.Pointer(&pixFmt))= *(*C.struct_v4l2_pix_format)(unsafe.Pointer(&v4l2Fmt.fmt[0])) + + return pixFmt, 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 = C.V4L2_MEMORY_MMAP +) + +// 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 { + var reqbuf C.struct_v4l2_requestbuffers + reqbuf.count = C.uint(count) + reqbuf._type = C.uint(BufTypeVideoCapture) + reqbuf.memory = C.uint(StreamMemoryTypeMMAP) + + if err := ioctl(fd, C.VIDIOC_REQBUFS, uintptr(unsafe.Pointer(&reqbuf))); err != nil { + return err + } + log.Printf("Request %d buffers OK\n", count) + return nil +} + +// ================================ Map device Memory =============================== + +// buffer service is embedded uion m +// in v4l2_buffer C type. +type BufferService struct { + Offset uint32 + UserPtr uintptr + Planes uintptr + FD int32 +} + +// 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) { + var v4l2Buf C.struct_v4l2_buffer + v4l2Buf._type = C.uint(BufTypeVideoCapture) + v4l2Buf.memory = C.uint(StreamMemoryTypeMMAP) + v4l2Buf.index = C.uint(idx) + + // send ioctl command + if err := ioctl(fd, C.VIDIOC_QUERYBUF, uintptr(unsafe.Pointer(&v4l2Buf))); err != nil { + return nil, err + } + + // grab m union and place it in type BufferService + bufSvc := *(*BufferService)(unsafe.Pointer(&v4l2Buf.m[0])) + + // map the memory and get []byte to access it + mbuf, err := sys.Mmap(int(fd), int64(bufSvc.Offset), int(v4l2Buf.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 := C.uint(BufTypeVideoCapture) + if err := ioctl(fd, C.VIDIOC_STREAMON, 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 { + var v4l2Buf C.struct_v4l2_buffer + v4l2Buf._type = C.uint(BufTypeVideoCapture) + v4l2Buf.memory = C.uint(StreamMemoryTypeMMAP) + v4l2Buf.index = C.uint(idx) + + if err := ioctl(fd, C.VIDIOC_QBUF, uintptr(unsafe.Pointer(&v4l2Buf))); 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) { + var v4l2Buf C.struct_v4l2_buffer + v4l2Buf._type = C.uint(BufTypeVideoCapture) + v4l2Buf.memory = C.uint(StreamMemoryTypeMMAP) + + if err := ioctl(fd, C.VIDIOC_DQBUF, uintptr(unsafe.Pointer(&v4l2Buf))); err != nil { + return 0, err + } + return uint32(v4l2Buf.bytesused), nil +} + +// =========================== Start device streaming ========================= + +// stopStreaming requests the device to stop the streaming process and release +// buffer resources. +func stopStreaming(fd uintptr) error { + bufType := C.uint(BufTypeVideoCapture) + + if err := ioctl(fd, C.VIDIOC_STREAMOFF, 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 + if err := setFormat(fd, PixFormat{ + Width: 640, + Height: 480, + PixelFormat: PixelFmtMJPEG, + Field: FieldNone, + }); err != nil { + log.Fatal(err) + } + + pixFmt, err := getFormat(fd) + if err != nil { + log.Fatal(err) + } + log.Printf("Format set: %dx%d pixel format %d [FmtMJPG]", pixFmt.Width, pixFmt.Height, pixFmt.PixelFormat) + + // 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) + } +} diff --git a/examples/webcam/webcam.go b/examples/webcam/webcam.go index 7341cf8..2c86636 100644 --- a/examples/webcam/webcam.go +++ b/examples/webcam/webcam.go @@ -8,14 +8,17 @@ import ( "io" "log" "net/http" + "strings" "time" + "github.com/vladimirvivien/go4vl/imgsupport" "github.com/vladimirvivien/go4vl/v4l2" ) var ( frames <-chan []byte fps uint32 = 30 + pixfmt v4l2.FourCCEncoding ) // servePage reads templated HTML @@ -56,9 +59,22 @@ func serveVideoStream(w http.ResponseWriter, req *http.Request) { 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 + switch pixfmt { + case v4l2.PixelFmtMJPEG: + if _, err := w.Write(frame); err != nil { + log.Printf("failed to write image: %s", err) + return + } + case v4l2.PixelFmtYUYV: + data, err := imgsupport.Yuyv2Jpeg(640, 480, frame) + if err != nil { + log.Printf("failed to convert yuyv to jpeg: %s", err) + continue + } + if _, err := w.Write(data); err != nil { + log.Printf("failed to write image: %s", err) + return + } } // close boundary if _, err := io.WriteString(w, "\n"); err != nil { @@ -71,10 +87,43 @@ func serveVideoStream(w http.ResponseWriter, req *http.Request) { func main() { port := ":9090" devName := "/dev/video0" + defaultDev, err := v4l2.Open(devName) + skipDefault := false + if err != nil { + skipDefault = true + } + + width := 640 + height := 480 + format := "yuyv" + if !skipDefault { + pix, err := defaultDev.GetPixFormat() + if err == nil { + width = int(pix.Width) + height = int(pix.Height) + switch pix.PixelFormat { + case v4l2.PixelFmtMJPEG: + format = "mjpeg" + case v4l2.PixelFmtH264: + format = "h264" + default: + format = "yuyv" + } + } + } + flag.StringVar(&devName, "d", devName, "device name (path)") + flag.IntVar(&width, "w", width, "capture width") + flag.IntVar(&height, "h", height, "capture height") + flag.StringVar(&format, "f", format, "pixel format") flag.StringVar(&port, "p", port, "webcam service port") flag.Parse() + // close device used for default info + if err := defaultDev.Close(); err != nil { + // default device failed to close + } + // open device and setup device device, err := v4l2.Open(devName) if err != nil { @@ -89,17 +138,23 @@ func main() { 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 { + currFmt, err := device.GetPixFormat() + if err != nil { + log.Fatalf("unable to get format: %s", err) + } + log.Printf("Current format: %s", currFmt) + if err := device.SetPixFormat(updateFormat(currFmt, format, width, height)); err != nil { log.Fatalf("failed to set format: %s", err) } + currFmt, err = device.GetPixFormat() + if err != nil { + log.Fatalf("unable to get format: %s", err) + } + pixfmt = currFmt.PixelFormat + log.Printf("Updated format: %s", currFmt) // Setup and start stream capture - if err := device.StartStream(15); err != nil { + if err := device.StartStream(2); err != nil { log.Fatalf("unable to start stream: %s", err) } @@ -127,3 +182,19 @@ func main() { log.Fatal(err) } } + +func updateFormat(pix v4l2.PixFormat, fmtStr string, w, h int) v4l2.PixFormat { + pix.Width = uint32(w) + pix.Height = uint32(h) + + switch strings.ToLower(fmtStr) { + case "mjpeg", "jpeg": + pix.PixelFormat = v4l2.PixelFmtMJPEG + case "h264", "h.264": + pix.PixelFormat = v4l2.PixelFmtH264 + case "yuyv": + pix.PixelFormat = v4l2.PixelFmtYUYV + } + + return pix +} \ No newline at end of file diff --git a/imgsupport/converters.go b/imgsupport/converters.go index 8de56a7..26f2834 100644 --- a/imgsupport/converters.go +++ b/imgsupport/converters.go @@ -6,18 +6,28 @@ import ( "image/jpeg" ) +// Yuyv2Jpeg attempts to convert the YUYV image using Go's built-in +// YCbCr encoder func Yuyv2Jpeg(width, height int, frame []byte) ([]byte, error) { - size := len(frame) + //size := len(frame) ycbr := image.NewYCbCr(image.Rect(0, 0, width, height), image.YCbCrSubsampleRatio422) - for i := 0; i < size; i += 4 { - y1, u, y2, v := frame[i], frame[i+1], frame[i+2], frame[i+3] - ycbr.Y[i] = y1 - ycbr.Y[i+1] = y2 - ycbr.Cb[i] = u - ycbr.Cr[i] = v + for i := range ycbr.Cb { + ii := i * 4 + ycbr.Y[i*2] = frame[ii] + ycbr.Y[i*2+1] = frame[ii+2] + ycbr.Cb[i] = frame[ii+1] + ycbr.Cr[i] = frame[ii+3] } + //for i := 0; i < size; i += 4 { + // y1, u, y2, v := frame[i], frame[i+1], frame[i+2], frame[i+3] + // ycbr.Y[i] = y1 + // ycbr.Y[i+1] = y2 + // ycbr.Cb[i] = u + // ycbr.Cr[i] = v + //} + var jpgBuf bytes.Buffer if err := jpeg.Encode(&jpgBuf, ycbr, nil); err != nil { return nil, err diff --git a/v4l2/format.go b/v4l2/format.go index 384c2b2..f58c385 100644 --- a/v4l2/format.go +++ b/v4l2/format.go @@ -267,9 +267,24 @@ type PixFormat struct { XferFunc XferFunctionType } +func (f PixFormat) String() string { + return fmt.Sprintf( + "%s [%dx%d]; field=%s; bytes per line=%d; size image=%d; colorspace=%s; YCbCr=%s; Quant=%s; XferFunc=%s", + PixelFormats[f.PixelFormat], + f.Width, f.Height, + Fields[f.Field], + f.BytesPerLine, + f.SizeImage, + Colorspaces[f.Colorspace], + YCbCrEncodings[f.YcbcrEnc], + Quantizations[f.Quantization], + XferFunctions[f.XferFunc], + ) +} + // v4l2Format (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 +// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L2331 // // field fmt is a union, thus it's constructed as an appropriately sized array: // @@ -292,21 +307,21 @@ type v4l2Format struct { } // getPixFormat returns the PixFormat by casting the pointer to the union type -func (f v4l2Format) getPixFormat() PixFormat { +func (f *v4l2Format) getPixFormat() PixFormat { pixfmt := (*PixFormat)(unsafe.Pointer(&f.fmt[0])) return *pixfmt } // setPixFormat sets the PixFormat by casting the pointer to the fmt union and set its value -func (f v4l2Format) setPixFormat(newPix PixFormat) { - *(*PixFormat)(unsafe.Pointer(&f.fmt[0])) = newPix +func (f *v4l2Format) setPixFormat(newPix PixFormat) { + f.fmt = *(*[200]byte)(unsafe.Pointer(&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 := v4l2Format{StreamType: BufTypeVideoCapture} - if err := Send(fd, VidiocGetFormat, uintptr(unsafe.Pointer(&format))); err != nil { + format := &v4l2Format{StreamType: BufTypeVideoCapture} + if err := Send(fd, VidiocGetFormat, uintptr(unsafe.Pointer(format))); err != nil { return PixFormat{}, fmt.Errorf("pix format failed: %w", err) } @@ -316,10 +331,9 @@ func GetPixFormat(fd uintptr) (PixFormat, error) { // 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 := v4l2Format{StreamType: BufTypeVideoCapture} + format := &v4l2Format{StreamType: BufTypeVideoCapture} format.setPixFormat(pixFmt) - - if err := Send(fd, VidiocSetFormat, uintptr(unsafe.Pointer(&format))); err != nil { + if err := Send(fd, VidiocSetFormat, uintptr(unsafe.Pointer(format))); err != nil { return fmt.Errorf("pix format failed: %w", err) } return nil