diff --git a/Overlord-Client/cmd/agent/capture/webcam_windows.go b/Overlord-Client/cmd/agent/capture/webcam_windows.go index 47a7c84..2123b4c 100644 --- a/Overlord-Client/cmd/agent/capture/webcam_windows.go +++ b/Overlord-Client/cmd/agent/capture/webcam_windows.go @@ -96,6 +96,23 @@ var ( webcamActive *webcamState ) +func activeWebcamFrameFPS(requested int, captureFPS float64) int { + actual := int(captureFPS + 0.5) + if requested < 1 { + requested = actual + } + if actual > 0 && requested > actual { + requested = actual + } + if requested < 1 { + requested = 30 + } + if requested > 120 { + requested = 120 + } + return requested +} + func NowWebcam(ctx context.Context, env *rt.Env) error { if ctx.Err() != nil { return nil @@ -122,6 +139,8 @@ func NowWebcam(ctx context.Context, env *rt.Env) error { return nil } + frameFPS := activeWebcamFrameFPS(env.WebcamFPS, state.captureFPS) + quality := env.WebcamQuality codec := env.WebcamCodec outFormat := format @@ -158,7 +177,7 @@ func NowWebcam(ctx context.Context, env *rt.Env) error { Type: "frame", Header: wire.FrameHeader{ Monitor: deviceIndex, - FPS: 0, + FPS: frameFPS, Format: outFormat, Webcam: true, }, @@ -168,14 +187,7 @@ func NowWebcam(ctx context.Context, env *rt.Env) error { return nil } if outFormat == "h264" && webrtcpub.IsActive(webrtcpub.KindWebcam) { - fps := env.WebcamFPS - if fps <= 0 { - fps = 30 - } - dur := time.Second / time.Duration(fps) - if dur <= 0 { - dur = 33 * time.Millisecond - } + dur := time.Second / time.Duration(frameFPS) if werr := webrtcpub.WriteH264(webrtcpub.KindWebcam, frameBytes, dur); werr != nil { log.Printf("webrtc: write webcam h264 failed: %v", werr) } diff --git a/Overlord-Client/cmd/agent/capture/webcam_windows_test.go b/Overlord-Client/cmd/agent/capture/webcam_windows_test.go new file mode 100644 index 0000000..ff958dd --- /dev/null +++ b/Overlord-Client/cmd/agent/capture/webcam_windows_test.go @@ -0,0 +1,73 @@ +//go:build windows && cgo + +package capture + +import ( + "context" + "os" + "testing" + "time" + + rt "overlord-client/cmd/agent/runtime" + "overlord-client/cmd/agent/wire" + + "github.com/vmihailenco/msgpack/v5" +) + +func TestActiveWebcamFrameFPS(t *testing.T) { + tests := []struct { + name string + requested int + captureFPS float64 + want int + }{ + {name: "requested rate", requested: 15, captureFPS: 30, want: 15}, + {name: "camera limit", requested: 60, captureFPS: 30, want: 30}, + {name: "camera default", captureFPS: 29.97, want: 30}, + {name: "safe default", want: 30}, + {name: "protocol limit", requested: 240, captureFPS: 240, want: 120}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := activeWebcamFrameFPS(test.requested, test.captureFPS); got != test.want { + t.Fatalf("activeWebcamFrameFPS(%d, %.2f) = %d, want %d", test.requested, test.captureFPS, got, test.want) + } + }) + } +} + +func TestWebcamCaptureSmoke(t *testing.T) { + if os.Getenv("OVERLORD_WEBCAM_SMOKE") != "1" { + t.Skip("set OVERLORD_WEBCAM_SMOKE=1 to exercise an attached camera") + } + + writer := &recordingWriter{} + env := &rt.Env{ + Conn: writer, + WebcamDeviceIndex: 0, + WebcamFPS: 30, + WebcamQuality: 90, + WebcamCodec: "jpeg", + } + t.Cleanup(CleanupWebcam) + + deadline := time.Now().Add(8 * time.Second) + for time.Now().Before(deadline) { + if err := NowWebcam(context.Background(), env); err != nil { + t.Fatalf("capture webcam frame: %v", err) + } + if len(writer.msgs) > 0 { + var frame wire.Frame + if err := msgpack.Unmarshal(writer.msgs[0], &frame); err != nil { + t.Fatalf("decode webcam frame: %v", err) + } + if !frame.Header.Webcam || frame.Header.FPS <= 0 || len(frame.Data) == 0 { + t.Fatalf("invalid live webcam frame: header=%+v bytes=%d", frame.Header, len(frame.Data)) + } + t.Logf("captured webcam format=%s fps=%d bytes=%d", frame.Header.Format, frame.Header.FPS, len(frame.Data)) + return + } + time.Sleep(25 * time.Millisecond) + } + t.Fatal("timed out waiting for a live webcam frame") +}