587 lines
14 KiB
Go
587 lines
14 KiB
Go
package webcam
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
"image/jpeg"
|
|
"image/png"
|
|
"os"
|
|
"sort"
|
|
"syscall"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"github.com/creack/goselect"
|
|
)
|
|
|
|
type Webcam struct {
|
|
fd uintptr
|
|
file *os.File
|
|
path string
|
|
timeout time.Duration
|
|
buffers [][]byte
|
|
}
|
|
|
|
func Open(path string, timeout time.Duration) (*Webcam, error) {
|
|
f, err := os.OpenFile(path, syscall.O_RDWR|syscall.O_NONBLOCK, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
w := new(Webcam)
|
|
w.fd = f.Fd()
|
|
w.file = f
|
|
w.path = path
|
|
w.timeout = timeout
|
|
|
|
v, s, err := w.checkCapabilities()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if v {
|
|
return nil, errors.New("Not a video capture device")
|
|
}
|
|
if s {
|
|
return nil, errors.New("Device does not support the streaming I/O method")
|
|
}
|
|
return w, nil
|
|
}
|
|
|
|
type requestbuffers struct {
|
|
count uint32
|
|
t uint32
|
|
memory uint32
|
|
reserved [2]uint32
|
|
}
|
|
|
|
type buffer struct {
|
|
index uint32
|
|
t uint32
|
|
bytesused uint32
|
|
flags uint32
|
|
field uint32
|
|
timestamp syscall.Timeval
|
|
timecode struct {
|
|
t uint32
|
|
flags uint32
|
|
frames uint8
|
|
seconds uint8
|
|
minutes uint8
|
|
hours uint8
|
|
userbits [4]uint8
|
|
}
|
|
sequence uint32
|
|
memory uint32
|
|
/*
|
|
union {
|
|
__u32 offset;
|
|
unsigned long userptr;
|
|
struct v4l2_plane *planes;
|
|
__s32 fd;
|
|
} m;
|
|
*/
|
|
offset int64 // m
|
|
length uint32
|
|
reserved2 uint32
|
|
reserved uint32
|
|
}
|
|
|
|
type format struct {
|
|
t uint32
|
|
pix pix
|
|
raw_data [155]byte // placeholder
|
|
}
|
|
type pix struct {
|
|
empty uint32 // placeholder
|
|
width uint32
|
|
height uint32
|
|
pixelformat uint32
|
|
field uint32
|
|
bytesperline uint32
|
|
sizeimage uint32
|
|
colorspace uint32
|
|
priv uint32
|
|
flags uint32
|
|
ycbcr_enc uint32
|
|
quantization uint32
|
|
// commented out for 'empty' placeholder
|
|
//xfer_func uint32
|
|
}
|
|
|
|
type capability struct {
|
|
driver [16]byte
|
|
card [32]byte
|
|
bus_info [32]byte
|
|
version uint32
|
|
capabilities uint32
|
|
device_caps uint32
|
|
reserved [3]uint32
|
|
}
|
|
|
|
type fmtdesc struct {
|
|
index uint32
|
|
t uint32
|
|
flags uint32
|
|
description [32]byte
|
|
pixelformat uint32
|
|
reserved [4]uint32
|
|
}
|
|
|
|
type frmsizeenum struct {
|
|
index uint32
|
|
pixel_format uint32
|
|
t uint32
|
|
/*
|
|
struct v4l2_frmsize_stepwise {
|
|
__u32 min_width
|
|
__u32 max_width
|
|
__u32 step_width
|
|
__u32 min_height
|
|
__u32 max_height
|
|
__u32 step_height
|
|
};
|
|
struct v4l2_frmsize_discrete {
|
|
__u32 width
|
|
__u32 height
|
|
};
|
|
*/
|
|
frmsize [6]uint32
|
|
reserved [2]uint32
|
|
}
|
|
|
|
type queryctrl struct {
|
|
id uint32
|
|
_type uint32
|
|
name [32]uint8
|
|
minimum int32
|
|
maximum int32
|
|
step int32
|
|
default_value int32
|
|
flags uint32
|
|
reserved [2]uint32
|
|
}
|
|
|
|
type querymenu struct {
|
|
id uint32
|
|
index uint32
|
|
name [32]uint8
|
|
reserved uint32
|
|
}
|
|
|
|
type control struct {
|
|
id uint32
|
|
value int32
|
|
}
|
|
|
|
const (
|
|
_IOC_READ = 2
|
|
_IOC_WRITE = 1
|
|
|
|
_IOC_NRBITS = 8
|
|
_IOC_TYPEBITS = 8
|
|
_IOC_SIZEBITS = 14
|
|
_IOC_NRSHIFT = 0
|
|
_IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS
|
|
_IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS
|
|
_IOC_DIRSHIFT = _IOC_SIZESHIFT + _IOC_SIZEBITS
|
|
|
|
_V4L2_BUF_TYPE_VIDEO_CAPTURE = 1
|
|
_V4L2_MEMORY_MMAP = 1
|
|
_V4L2_FIELD_ANY = 0
|
|
|
|
_V4L2_CAP_STREAMING = 0x04000000
|
|
_V4L2_CAP_VIDEO_CAPTURE = 0x00000001
|
|
|
|
_V4L2_FRMSIZE_TYPE_DISCRETE = 1
|
|
_V4L2_FRMSIZE_TYPE_CONTINUOUS = 2
|
|
_V4L2_FRMSIZE_TYPE_STEPWISE = 3
|
|
|
|
_V4L2_CTRL_FLAG_NEXT_CTRL = 0x80000000
|
|
_V4L2_CTRL_FLAG_DISABLED = 0x0001
|
|
_V4L2_CTRL_TYPE_MENU = 3
|
|
_V4L2_CTRL_CLASS_USER = 0x00980000
|
|
_V4L2_CID_BASE = _V4L2_CTRL_CLASS_USER | 0x900
|
|
_V4L2_CID_USER_BASE = _V4L2_CID_BASE
|
|
_V4L2_CID_USER_CLASS = _V4L2_CTRL_CLASS_USER | 1
|
|
|
|
// Controls
|
|
V4L2_CID_BRIGHTNESS = _V4L2_CID_BASE + 0
|
|
V4L2_CID_CONTRAST = _V4L2_CID_BASE + 1
|
|
V4L2_CID_SATURATION = _V4L2_CID_BASE + 2
|
|
V4L2_CID_HUE = _V4L2_CID_BASE + 3
|
|
V4L2_CID_AUDIO_VOLUME = _V4L2_CID_BASE + 5
|
|
V4L2_CID_AUDIO_BALANCE = _V4L2_CID_BASE + 6
|
|
V4L2_CID_AUDIO_BASS = _V4L2_CID_BASE + 7
|
|
V4L2_CID_AUDIO_TREBLE = _V4L2_CID_BASE + 8
|
|
V4L2_CID_AUDIO_MUTE = _V4L2_CID_BASE + 9
|
|
V4L2_CID_AUDIO_LOUDNESS = _V4L2_CID_BASE + 10
|
|
V4L2_CID_AUTO_WHITE_BALANCE = _V4L2_CID_BASE + 12
|
|
V4L2_CID_DO_WHITE_BALANCE = _V4L2_CID_BASE + 13
|
|
V4L2_CID_RED_BALANCE = _V4L2_CID_BASE + 14
|
|
V4L2_CID_BLUE_BALANCE = _V4L2_CID_BASE + 15
|
|
V4L2_CID_GAMMA = _V4L2_CID_BASE + 16
|
|
V4L2_CID_EXPOSURE = _V4L2_CID_BASE + 17
|
|
V4L2_CID_AUTOGAIN = _V4L2_CID_BASE + 18
|
|
V4L2_CID_GAIN = _V4L2_CID_BASE + 19
|
|
V4L2_CID_HFLIP = _V4L2_CID_BASE + 20
|
|
V4L2_CID_VFLIP = _V4L2_CID_BASE + 21
|
|
V4L2_CID_POWER_LINE_FREQUENCY = _V4L2_CID_BASE + 24
|
|
V4L2_CID_HUE_AUTO = _V4L2_CID_BASE + 25
|
|
V4L2_CID_WHITE_BALANCE_TEMPERATURE = _V4L2_CID_BASE + 26
|
|
V4L2_CID_SHARPNESS = _V4L2_CID_BASE + 27
|
|
V4L2_CID_BACKLIGHT_COMPENSATION = _V4L2_CID_BASE + 28
|
|
V4L2_CID_CHROMA_AGC = _V4L2_CID_BASE + 29
|
|
V4L2_CID_COLOR_KILLER = _V4L2_CID_BASE + 30
|
|
V4L2_CID_COLORFX = _V4L2_CID_BASE + 31
|
|
V4L2_CID_AUTOBRIGHTNESS = _V4L2_CID_BASE + 32
|
|
V4L2_CID_BAND_STOP_FILTER = _V4L2_CID_BASE + 33
|
|
V4L2_CID_ROTATE = _V4L2_CID_BASE + 34
|
|
V4L2_CID_BG_COLOR = _V4L2_CID_BASE + 35
|
|
V4L2_CID_CHROMA_GAIN = _V4L2_CID_BASE + 36
|
|
V4L2_CID_ILLUMINATORS_1 = _V4L2_CID_BASE + 37
|
|
V4L2_CID_ILLUMINATORS_2 = _V4L2_CID_BASE + 38
|
|
V4L2_CID_MIN_BUFFERS_FOR_CAPTURE = _V4L2_CID_BASE + 39
|
|
V4L2_CID_MIN_BUFFERS_FOR_OUTPUT = _V4L2_CID_BASE + 40
|
|
V4L2_CID_ALPHA_COMPONENT = _V4L2_CID_BASE + 41
|
|
V4L2_CID_COLORFX_CBCR = _V4L2_CID_BASE + 42
|
|
)
|
|
|
|
var (
|
|
_VIDIOC_QUERYCAP = _IOR('V', 0, unsafe.Sizeof(capability{}))
|
|
_VIDIOC_ENUM_FMT = _IOWR('V', 2, unsafe.Sizeof(fmtdesc{}))
|
|
_VIDIOC_S_FMT = _IOWR('V', 5, unsafe.Sizeof(format{}))
|
|
_VIDIOC_REQBUFS = _IOWR('V', 8, unsafe.Sizeof(requestbuffers{}))
|
|
_VIDIOC_QUERYBUF = _IOWR('V', 9, unsafe.Sizeof(buffer{}))
|
|
_VIDIOC_QBUF = _IOWR('V', 15, unsafe.Sizeof(buffer{}))
|
|
_VIDIOC_DQBUF = _IOWR('V', 17, unsafe.Sizeof(buffer{}))
|
|
_VIDIOC_STREAMON = _IOW('V', 18, unsafe.Sizeof(int32(0)))
|
|
_VIDIOC_ENUM_FRAMESIZES = _IOWR('V', 74, unsafe.Sizeof(frmsizeenum{}))
|
|
_VIDIOC_QUERYCTRL = _IOWR('V', 36, unsafe.Sizeof(queryctrl{}))
|
|
_VIDIOC_QUERYMENU = _IOWR('V', 37, unsafe.Sizeof(querymenu{}))
|
|
_VIDIOC_G_CTRL = _IOWR('V', 27, unsafe.Sizeof(control{}))
|
|
_VIDIOC_S_CTRL = _IOWR('V', 28, unsafe.Sizeof(control{}))
|
|
)
|
|
|
|
func _IOC(dir, t, nr, size uintptr) uintptr {
|
|
return dir<<_IOC_DIRSHIFT |
|
|
t<<_IOC_TYPESHIFT |
|
|
nr<<_IOC_NRSHIFT |
|
|
size<<_IOC_SIZESHIFT
|
|
}
|
|
|
|
func _IOWR(t rune, i, size uintptr) uintptr {
|
|
return _IOC(_IOC_READ|_IOC_WRITE, uintptr(t), i, size)
|
|
}
|
|
|
|
func _IOW(t rune, i, size uintptr) uintptr {
|
|
return _IOC(_IOC_WRITE, uintptr(t), i, size)
|
|
}
|
|
|
|
func _IOR(t rune, i, size uintptr) uintptr {
|
|
return _IOC(_IOC_READ, uintptr(t), i, size)
|
|
}
|
|
|
|
func ioctl(fd, cmd, ptr uintptr) error {
|
|
_, _, ep := syscall.Syscall(syscall.SYS_IOCTL, fd, cmd, ptr)
|
|
if ep != 0 {
|
|
return syscall.Errno(ep)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func u2s(a [32]uint8) string {
|
|
buf := make([]byte, 32)
|
|
for i, u := range a {
|
|
if u == 0 {
|
|
break
|
|
}
|
|
buf[i] = byte(u)
|
|
}
|
|
return string(buf)
|
|
}
|
|
|
|
func (w *Webcam) checkCapabilities() (video, stream bool, err error) {
|
|
c := new(capability)
|
|
err = ioctl(w.fd, _VIDIOC_QUERYCAP, uintptr(unsafe.Pointer(c)))
|
|
video = (c.capabilities & _V4L2_CAP_VIDEO_CAPTURE) == 0
|
|
stream = (c.capabilities & _V4L2_CAP_STREAMING) == 0
|
|
return
|
|
}
|
|
|
|
func (w *Webcam) enumMenu(id uint32, min, max int32) (items []string) {
|
|
var qm querymenu
|
|
qm.id = id
|
|
for qm.index = uint32(min); qm.index <= uint32(max); qm.index++ {
|
|
if ioctl(w.fd, _VIDIOC_QUERYMENU, uintptr(unsafe.Pointer(&qm))) == nil {
|
|
items = append(items, u2s(qm.name))
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (w *Webcam) GetControls() (m map[string][]string) {
|
|
m = make(map[string][]string)
|
|
var qc queryctrl
|
|
qc.id = _V4L2_CTRL_FLAG_NEXT_CTRL
|
|
for ioctl(w.fd, _VIDIOC_QUERYCTRL, uintptr(unsafe.Pointer(&qc))) == nil {
|
|
if (qc.flags & _V4L2_CTRL_FLAG_DISABLED) == 0 {
|
|
items := []string{}
|
|
if qc._type == _V4L2_CTRL_TYPE_MENU {
|
|
items = w.enumMenu(qc.id, qc.minimum, qc.maximum)
|
|
}
|
|
m[u2s(qc.name)] = items
|
|
}
|
|
qc.id |= _V4L2_CTRL_FLAG_NEXT_CTRL
|
|
}
|
|
return
|
|
}
|
|
|
|
func (w *Webcam) SetControl(ctrl uint32, value int32) error {
|
|
var qc queryctrl
|
|
qc.id = ctrl
|
|
err := ioctl(w.fd, _VIDIOC_QUERYCTRL, uintptr(unsafe.Pointer(&qc)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if (qc.flags & _V4L2_CTRL_FLAG_DISABLED) != 0 {
|
|
return errors.New("unsupported")
|
|
}
|
|
var c control
|
|
c.id = ctrl
|
|
c.value = value
|
|
return ioctl(w.fd, _VIDIOC_S_CTRL, uintptr(unsafe.Pointer(&c)))
|
|
|
|
}
|
|
func (w *Webcam) GetPixelFormats() (m map[uint32]string) {
|
|
m = make(map[uint32]string)
|
|
for i := uint32(0); ; i++ {
|
|
vfd := fmtdesc{
|
|
index: i,
|
|
t: _V4L2_BUF_TYPE_VIDEO_CAPTURE,
|
|
}
|
|
err := ioctl(w.fd, _VIDIOC_ENUM_FMT, uintptr(unsafe.Pointer(&vfd)))
|
|
if err != nil {
|
|
return
|
|
}
|
|
var s string
|
|
for _, b := range vfd.description {
|
|
s = s + string(b)
|
|
}
|
|
m[vfd.pixelformat] = s
|
|
}
|
|
return
|
|
}
|
|
|
|
type FrameSize struct {
|
|
MinWidth uint32
|
|
MaxWidth uint32
|
|
StepWidth uint32
|
|
MinHeight uint32
|
|
MaxHeight uint32
|
|
StepHeight uint32
|
|
}
|
|
|
|
func (s FrameSize) String() string {
|
|
if s.StepWidth == 0 && s.StepHeight == 0 {
|
|
return fmt.Sprintf("%dx%d", s.MaxWidth, s.MaxHeight)
|
|
} else {
|
|
return fmt.Sprintf("[%d-%d;%d]x[%d-%d;%d]", s.MinWidth, s.MaxWidth, s.StepWidth, s.MinHeight, s.MaxHeight, s.StepHeight)
|
|
}
|
|
}
|
|
|
|
type FrameSizes []FrameSize
|
|
|
|
func (fs FrameSizes) Len() int { return len(fs) }
|
|
func (fs FrameSizes) Less(i, j int) bool {
|
|
ls := fs[i].MaxWidth * fs[i].MaxHeight
|
|
rs := fs[j].MaxWidth * fs[j].MaxHeight
|
|
return ls < rs
|
|
}
|
|
func (fs FrameSizes) Swap(i, j int) { fs[i], fs[j] = fs[j], fs[i] }
|
|
|
|
func (w *Webcam) GetFrameSizes(code uint32) (fs FrameSizes) {
|
|
var s FrameSize
|
|
for i := uint32(0); ; i++ {
|
|
vfse := frmsizeenum{
|
|
index: i,
|
|
pixel_format: code,
|
|
}
|
|
if err := ioctl(w.fd, _VIDIOC_ENUM_FRAMESIZES, uintptr(unsafe.Pointer(&vfse))); err != nil {
|
|
break
|
|
}
|
|
switch vfse.t {
|
|
case _V4L2_FRMSIZE_TYPE_DISCRETE:
|
|
s.MinWidth = vfse.frmsize[0]
|
|
s.MaxWidth = vfse.frmsize[0]
|
|
s.StepWidth = 0
|
|
s.MinHeight = vfse.frmsize[1]
|
|
s.MaxHeight = vfse.frmsize[1]
|
|
s.StepHeight = 0
|
|
case _V4L2_FRMSIZE_TYPE_CONTINUOUS, _V4L2_FRMSIZE_TYPE_STEPWISE:
|
|
s.MinWidth = vfse.frmsize[0]
|
|
s.MaxWidth = vfse.frmsize[1]
|
|
s.StepWidth = vfse.frmsize[2]
|
|
s.MinHeight = vfse.frmsize[3]
|
|
s.MaxHeight = vfse.frmsize[4]
|
|
s.StepHeight = vfse.frmsize[5]
|
|
}
|
|
fs = append(fs, s)
|
|
}
|
|
sort.Sort(fs)
|
|
return
|
|
}
|
|
|
|
func (cam *Webcam) SetImageFormat(w, h, formatcode uint32) (uint32, uint32, uint32, error) {
|
|
f := format{
|
|
t: _V4L2_BUF_TYPE_VIDEO_CAPTURE,
|
|
pix: pix{
|
|
width: w,
|
|
height: h,
|
|
pixelformat: formatcode,
|
|
field: _V4L2_FIELD_ANY,
|
|
},
|
|
}
|
|
err := ioctl(cam.fd, _VIDIOC_S_FMT, uintptr(unsafe.Pointer(&f)))
|
|
return f.pix.pixelformat, f.pix.width, f.pix.height, err
|
|
}
|
|
|
|
func (w *Webcam) mmapRequestBuffers(bufcount uint32) (int, error) {
|
|
req := requestbuffers{
|
|
count: bufcount,
|
|
t: _V4L2_BUF_TYPE_VIDEO_CAPTURE,
|
|
memory: _V4L2_MEMORY_MMAP,
|
|
}
|
|
err := ioctl(w.fd, _VIDIOC_REQBUFS, uintptr(unsafe.Pointer(&req)))
|
|
return int(req.count), err
|
|
}
|
|
|
|
func (w *Webcam) mmapQueryBuffer(index uint32) ([]byte, error) {
|
|
buf := buffer{
|
|
t: _V4L2_BUF_TYPE_VIDEO_CAPTURE,
|
|
memory: _V4L2_MEMORY_MMAP,
|
|
index: index,
|
|
}
|
|
err := ioctl(w.fd, _VIDIOC_QUERYBUF, uintptr(unsafe.Pointer(&buf)))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return syscall.Mmap(int(w.fd), buf.offset, int(buf.length), syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED)
|
|
}
|
|
|
|
func (w *Webcam) mmapDequeueBuffer() (uint32, uint32, error) {
|
|
buf := buffer{
|
|
t: _V4L2_BUF_TYPE_VIDEO_CAPTURE,
|
|
memory: _V4L2_MEMORY_MMAP,
|
|
}
|
|
err := ioctl(w.fd, _VIDIOC_DQBUF, uintptr(unsafe.Pointer(&buf)))
|
|
return buf.index, buf.bytesused, err
|
|
}
|
|
|
|
func (w *Webcam) mmapEnqueueBuffer(index uint32) error {
|
|
buf := buffer{
|
|
t: _V4L2_BUF_TYPE_VIDEO_CAPTURE,
|
|
memory: _V4L2_MEMORY_MMAP,
|
|
index: index,
|
|
}
|
|
return ioctl(w.fd, _VIDIOC_QBUF, uintptr(unsafe.Pointer(&buf)))
|
|
}
|
|
|
|
func (w *Webcam) mmapReleaseBuffers() error {
|
|
for _, buf := range w.buffers {
|
|
if err := syscall.Munmap(buf); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (w *Webcam) StartStreaming() error {
|
|
n, err := w.mmapRequestBuffers(uint32(256))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
w.buffers = make([][]byte, n)
|
|
for i := range w.buffers {
|
|
buf, err := w.mmapQueryBuffer(uint32(i))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.buffers[i] = buf
|
|
err = w.mmapEnqueueBuffer(uint32(i))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
t := _V4L2_BUF_TYPE_VIDEO_CAPTURE
|
|
return ioctl(w.fd, _VIDIOC_STREAMON, uintptr(unsafe.Pointer(&t)))
|
|
}
|
|
|
|
type TimeoutErr string
|
|
|
|
func (t TimeoutErr) Error() string {
|
|
return string(t)
|
|
}
|
|
|
|
func (w *Webcam) WaitForFrame() (err error) {
|
|
fds := new(goselect.FDSet)
|
|
for {
|
|
fds.Zero()
|
|
fds.Set(w.fd)
|
|
err = goselect.Select(int(w.fd)+1, fds, nil, nil, w.timeout)
|
|
if err == nil {
|
|
return
|
|
}
|
|
e, ok := err.(syscall.Errno)
|
|
if ok {
|
|
if e.Timeout() {
|
|
return TimeoutErr("timeout")
|
|
}
|
|
if e.Temporary() {
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (w *Webcam) ReadFrame() ([]byte, error) {
|
|
i, l, err := w.mmapDequeueBuffer()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
buf := make([]byte, l)
|
|
copy(buf, w.buffers[int(i)])
|
|
|
|
err = w.mmapEnqueueBuffer(i)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return buf, nil
|
|
}
|
|
|
|
func (w *Webcam) ReadJPEG() (image.Image, error) {
|
|
buf, err := w.ReadFrame()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return jpeg.Decode(bytes.NewBuffer(buf))
|
|
}
|
|
|
|
func (w *Webcam) ReadPNG() (image.Image, error) {
|
|
buf, err := w.ReadFrame()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return png.Decode(bytes.NewBuffer(buf))
|
|
}
|
|
|
|
func (w *Webcam) Close() error {
|
|
if err := w.mmapReleaseBuffers(); err != nil {
|
|
return err
|
|
}
|
|
return w.file.Close()
|
|
}
|