@ozaiya/openclaw-channel 0.10.44 → 0.10.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/gateway.js +182 -96
- package/dist/src/gateway.js.map +1 -1
- package/dist/src/sandboxScreenCdp.d.ts +10 -0
- package/dist/src/sandboxScreenCdp.js +129 -31
- package/dist/src/sandboxScreenCdp.js.map +1 -1
- package/dist/src/sandboxScreenDesktop.d.ts +100 -0
- package/dist/src/sandboxScreenDesktop.js +371 -0
- package/dist/src/sandboxScreenDesktop.js.map +1 -0
- package/dist/src/sandboxScreenH264.d.ts +40 -0
- package/dist/src/sandboxScreenH264.js +155 -59
- package/dist/src/sandboxScreenH264.js.map +1 -1
- package/dist/src/sandboxScreenRtc.d.ts +3 -0
- package/dist/src/sandboxScreenRtc.js +37 -3
- package/dist/src/sandboxScreenRtc.js.map +1 -1
- package/package.json +1 -1
package/dist/src/gateway.js
CHANGED
|
@@ -28,7 +28,8 @@ import * as desktop from "./desktopContainer.js";
|
|
|
28
28
|
import { createSandboxFileSession, handleSandboxFileMessage } from "./sandboxFileTransfer.js";
|
|
29
29
|
import { acquireCdpScreencast, releaseCdpScreencast } from "./sandboxScreenCdp.js";
|
|
30
30
|
import { createSandboxRtcSession } from "./sandboxScreenRtc.js";
|
|
31
|
-
import { probeFfmpeg, createH264Transcoder } from "./sandboxScreenH264.js";
|
|
31
|
+
import { probeFfmpeg, createH264Transcoder, createBitrateController } from "./sandboxScreenH264.js";
|
|
32
|
+
import { probeDesktopSource, createDesktopVideo, createDesktopInput } from "./sandboxScreenDesktop.js";
|
|
32
33
|
import { unsealScreenKey, encryptScreenPayload, decryptScreenPayload, E2E_INPUT_MARKER } from "./sandboxScreenE2E.js";
|
|
33
34
|
import { readSandboxIdentity } from "./sandboxIdentity.js";
|
|
34
35
|
import { WeChatMonitorSession, getMonitor, getAllMonitors } from "./wechatMonitor.js";
|
|
@@ -583,7 +584,7 @@ export async function startGatewayMode(options) {
|
|
|
583
584
|
// DataChannel (P2P, or TURN via coturn) and move frames + input onto it —
|
|
584
585
|
// skipping the double-proxied relay, which is the dominant latency. The
|
|
585
586
|
// relay stays connected as signaling pipe + automatic fallback.
|
|
586
|
-
const { channelId, botId, supportsRtc, supportsE2E, iceServers, requestVnc } = payload.params;
|
|
587
|
+
const { channelId, botId, supportsRtc, supportsE2E, iceServers, requestVnc, requestDesktop } = payload.params;
|
|
587
588
|
// Full-desktop noVNC tier (the bot's screenMode='vnc' → server &vnc=1 → requestVnc). Instead
|
|
588
589
|
// of the CDP screencast, raw-pipe the sandbox-browser's websockify (x11vnc on Xvfb :1)
|
|
589
590
|
// straight to the relay — exactly like desktop-screen-open. The viewer runs noVNC/RFB and
|
|
@@ -695,7 +696,20 @@ export async function startGatewayMode(options) {
|
|
|
695
696
|
let userQuality = null;
|
|
696
697
|
let userBitrateKbps = null;
|
|
697
698
|
let userMinIntervalMs = null;
|
|
698
|
-
|
|
699
|
+
// Desktop source (viewer asked with &desktop=1 — screenMode='vnc' bots):
|
|
700
|
+
// the whole X display + real X11 input, served by the browser
|
|
701
|
+
// container's control server. Compose mode only (host-mode sandboxes
|
|
702
|
+
// don't run it). Unavailable ⇒ the page (CDP) source, and the viewer is
|
|
703
|
+
// told so it can fall back to its noVNC pipe.
|
|
704
|
+
const controlHost = process.env.NOVNC_HOST || '';
|
|
705
|
+
const controlPort = Number(process.env.RECORD_CONTROL_PORT || '8091');
|
|
706
|
+
const desktopInfo = requestDesktop && desktop.isInContainer && controlHost
|
|
707
|
+
? await probeDesktopSource(controlHost, controlPort)
|
|
708
|
+
: null;
|
|
709
|
+
const useDesktop = !!desktopInfo;
|
|
710
|
+
if (requestDesktop && !useDesktop)
|
|
711
|
+
log?.warn(`[gateway] sandbox-screen: desktop source unavailable channelId=${channelId} — page source`);
|
|
712
|
+
log?.info(`[gateway] sandbox-screen-open channelId=${channelId} botId=${botId} (${useDesktop ? `desktop ${desktopInfo.width}x${desktopInfo.height}` : 'cdp'}${supportsRtc ? '+rtc' : ''}${expectE2E ? '+e2e' : ''})`);
|
|
699
713
|
// Resolve the browser container's CDP host:port (the socat-proxied DevTools
|
|
700
714
|
// endpoint). In-container: NOVNC_HOST + CDP port. On host: the mapped 9222.
|
|
701
715
|
let cdpHost = process.env.NOVNC_HOST || 'localhost';
|
|
@@ -735,7 +749,8 @@ export async function startGatewayMode(options) {
|
|
|
735
749
|
if (!desktop.isInContainer && browserContainerId) {
|
|
736
750
|
ffmpegCandidates.push(['docker', 'exec', '-i', browserContainerId, 'ffmpeg']);
|
|
737
751
|
}
|
|
738
|
-
|
|
752
|
+
// (The desktop source encodes inside the browser container — no local ffmpeg needed.)
|
|
753
|
+
const ffmpegCmd = useDesktop ? null : await probeFfmpeg(ffmpegCandidates, dockerEnvLocal);
|
|
739
754
|
if (ffmpegCmd)
|
|
740
755
|
log?.info(`[gateway] sandbox-screen: H.264 encoder available (${ffmpegCmd[0]})`);
|
|
741
756
|
// Open the relay WS back to the server.
|
|
@@ -751,7 +766,7 @@ export async function startGatewayMode(options) {
|
|
|
751
766
|
ack({ ok: false, error: `Failed to connect relay WS: ${String(err)}` });
|
|
752
767
|
return;
|
|
753
768
|
}
|
|
754
|
-
const entry = { relayWs, cdpKey: { host: cdpHost, port: cdpPort } };
|
|
769
|
+
const entry = { relayWs, cdpKey: useDesktop ? undefined : { host: cdpHost, port: cdpPort } };
|
|
755
770
|
screenConnections.set(channelId, entry);
|
|
756
771
|
let relayOpen = false;
|
|
757
772
|
const earlyFrames = [];
|
|
@@ -770,20 +785,20 @@ export async function startGatewayMode(options) {
|
|
|
770
785
|
if (!pt || pt.length < 1)
|
|
771
786
|
return;
|
|
772
787
|
if (pt[0] === 0)
|
|
773
|
-
entry.
|
|
788
|
+
entry.input?.handleBinaryInput(pt.subarray(1));
|
|
774
789
|
else {
|
|
775
790
|
try {
|
|
776
791
|
const m = JSON.parse(pt.subarray(1).toString());
|
|
777
792
|
if (m.type === 'input')
|
|
778
|
-
entry.
|
|
793
|
+
entry.input?.handleInput(m);
|
|
779
794
|
else if (m.type === 'viewport-size')
|
|
780
|
-
entry.
|
|
795
|
+
entry.input?.resize(Number(m.width), Number(m.height));
|
|
781
796
|
}
|
|
782
797
|
catch { /* ignore */ }
|
|
783
798
|
}
|
|
784
799
|
return;
|
|
785
800
|
}
|
|
786
|
-
entry.
|
|
801
|
+
entry.input?.handleBinaryInput(buf);
|
|
787
802
|
return;
|
|
788
803
|
}
|
|
789
804
|
try {
|
|
@@ -805,9 +820,9 @@ export async function startGatewayMode(options) {
|
|
|
805
820
|
return;
|
|
806
821
|
}
|
|
807
822
|
if (msg.type === 'input')
|
|
808
|
-
entry.
|
|
823
|
+
entry.input?.handleInput(msg);
|
|
809
824
|
else if (msg.type === 'viewport-size')
|
|
810
|
-
entry.
|
|
825
|
+
entry.input?.resize(Number(msg.width), Number(msg.height));
|
|
811
826
|
// The viewer pings every second for its latency indicator — answer on
|
|
812
827
|
// the active transport (silently dropped before, which left the
|
|
813
828
|
// latency stat permanently empty on the bot view).
|
|
@@ -827,13 +842,17 @@ export async function startGatewayMode(options) {
|
|
|
827
842
|
if (isFinite(mbps) && mbps > 0) {
|
|
828
843
|
userBitrateKbps = Math.max(300, Math.min(40000, Math.round(mbps * 1000)));
|
|
829
844
|
userQuality = Math.max(40, Math.min(95, Math.round(45 + Math.log2(mbps) * 10)));
|
|
830
|
-
|
|
845
|
+
// The pick is the adaptation ceiling; it jumps there and
|
|
846
|
+
// backs off from it only on loss.
|
|
847
|
+
bitrate.setCeiling(userBitrateKbps);
|
|
831
848
|
}
|
|
832
849
|
}
|
|
833
850
|
else if (msg.setting === 'frameRate') {
|
|
834
851
|
const f = Number(msg.value);
|
|
835
|
-
if (isFinite(f) && f > 0)
|
|
852
|
+
if (isFinite(f) && f > 0) {
|
|
836
853
|
userMinIntervalMs = Math.round(1000 / Math.max(1, Math.min(30, f)));
|
|
854
|
+
desktopVideo?.setFps(f);
|
|
855
|
+
}
|
|
837
856
|
}
|
|
838
857
|
}
|
|
839
858
|
}
|
|
@@ -847,6 +866,17 @@ export async function startGatewayMode(options) {
|
|
|
847
866
|
t?.close();
|
|
848
867
|
}
|
|
849
868
|
catch { /* ignore */ } };
|
|
869
|
+
// Desktop source halves (useDesktop only): the x11grab stream and the XTest input.
|
|
870
|
+
let desktopVideo = null;
|
|
871
|
+
let desktopInput = null;
|
|
872
|
+
// Media-track bitrate follows the viewer's RTCP loss reports + PLIs,
|
|
873
|
+
// between a floor and the quality pick (or a default ceiling).
|
|
874
|
+
const bitrate = createBitrateController({
|
|
875
|
+
initialKbps: 1200,
|
|
876
|
+
maxKbps: 2500,
|
|
877
|
+
apply: (kbps) => { transcoder?.updateBitrate(kbps); desktopVideo?.updateBitrate(kbps); },
|
|
878
|
+
log: (m) => log?.info(m),
|
|
879
|
+
});
|
|
850
880
|
if (supportsRtc) {
|
|
851
881
|
// The container NAT is symmetric on Docker-Desktop-Mac, so a missing
|
|
852
882
|
// TURN entry here means the direct tier can't ever connect — make
|
|
@@ -870,16 +900,25 @@ export async function startGatewayMode(options) {
|
|
|
870
900
|
onData: (data) => handleViewerData(data, typeof data !== 'string'),
|
|
871
901
|
onActive: () => log?.info(`[gateway] sandbox-screen: P2P DataChannel active channelId=${channelId} — frames direct, relay on standby`),
|
|
872
902
|
onClosed: () => log?.info(`[gateway] sandbox-screen: P2P closed channelId=${channelId} — frames back on relay`),
|
|
873
|
-
enableVideo: !!ffmpegCmd,
|
|
903
|
+
enableVideo: !!ffmpegCmd || useDesktop,
|
|
874
904
|
onMediaStateChange: (ready) => {
|
|
905
|
+
if (ready)
|
|
906
|
+
bitrate.settle();
|
|
907
|
+
if (useDesktop) {
|
|
908
|
+
// Same x11grab stream, re-dialed as H.264 for the media track
|
|
909
|
+
// or JPEG for the DataChannel/relay.
|
|
910
|
+
log?.info(`[gateway] sandbox-screen: desktop ${ready ? 'H.264 media track up' : 'media track down — JPEG'} channelId=${channelId}`);
|
|
911
|
+
desktopVideo?.setMode(ready ? 'h264' : 'jpeg');
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
875
914
|
if (ready && ffmpegCmd && !transcoder && !transcoderDead) {
|
|
876
915
|
log?.info(`[gateway] sandbox-screen: H.264 media track up channelId=${channelId} — starting encoder`);
|
|
877
916
|
transcoder = createH264Transcoder({
|
|
878
917
|
cmdPrefix: ffmpegCmd,
|
|
879
918
|
env: dockerEnvLocal,
|
|
880
919
|
fps: 25,
|
|
881
|
-
//
|
|
882
|
-
bitrateKbps:
|
|
920
|
+
// Current adaptive target (includes a quality pick made before the track came up).
|
|
921
|
+
bitrateKbps: bitrate.current(),
|
|
883
922
|
onAccessUnit: (au) => { rtc?.sendVideoFrame(au); },
|
|
884
923
|
onExit: () => { transcoder = null; transcoderDead = true; log?.warn(`[gateway] sandbox-screen: encoder gone channelId=${channelId} — JPEG fallback`); },
|
|
885
924
|
log: { info: (m) => log?.info(m), warn: (m) => log?.warn(m) },
|
|
@@ -891,7 +930,8 @@ export async function startGatewayMode(options) {
|
|
|
891
930
|
},
|
|
892
931
|
// Viewer PLI → force the encoder to emit a keyframe now (debounced
|
|
893
932
|
// inside the transcoder). No transcoder ⇒ no-op (GOP recovers).
|
|
894
|
-
onKeyframeRequest: () => transcoder?.forceKeyframe(),
|
|
933
|
+
onKeyframeRequest: () => { bitrate.onPli(); transcoder?.forceKeyframe(); desktopVideo?.forceKeyframe(); },
|
|
934
|
+
onLossReport: (fraction) => bitrate.onLoss(fraction),
|
|
895
935
|
log: { info: (m) => log?.info(m), warn: (m) => log?.warn(m) },
|
|
896
936
|
});
|
|
897
937
|
}
|
|
@@ -900,10 +940,22 @@ export async function startGatewayMode(options) {
|
|
|
900
940
|
return;
|
|
901
941
|
screenConnections.delete(channelId);
|
|
902
942
|
stopTranscoder();
|
|
943
|
+
try {
|
|
944
|
+
desktopVideo?.close();
|
|
945
|
+
}
|
|
946
|
+
catch { /* ignore */ }
|
|
947
|
+
try {
|
|
948
|
+
desktopInput?.close();
|
|
949
|
+
}
|
|
950
|
+
catch { /* ignore */ }
|
|
951
|
+
desktopVideo = null;
|
|
952
|
+
desktopInput = null;
|
|
903
953
|
// The CDP session is SHARED across viewers of this browser — release
|
|
904
954
|
// our slot; it closes only when the last viewer leaves.
|
|
905
|
-
|
|
955
|
+
if (!useDesktop)
|
|
956
|
+
releaseCdpScreencast(cdpHost, cdpPort, channelId);
|
|
906
957
|
entry.cdp = undefined;
|
|
958
|
+
entry.input = undefined;
|
|
907
959
|
try {
|
|
908
960
|
rtc?.close();
|
|
909
961
|
}
|
|
@@ -955,88 +1007,118 @@ export async function startGatewayMode(options) {
|
|
|
955
1007
|
earlyFrames.push(data);
|
|
956
1008
|
}
|
|
957
1009
|
};
|
|
958
|
-
//
|
|
959
|
-
//
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
log: { info: (m) => log?.info(m), warn: (m) => log?.warn(m) },
|
|
966
|
-
}, channelId, {
|
|
967
|
-
// Transport-adaptive tuning. Relay tier: ~18fps / q40 — capture is only
|
|
968
|
-
// ~38ms, so the bottleneck is per-frame transfer over the (Cloudflare-
|
|
969
|
-
// proxied) relay; smaller frames transfer faster = lower latency. Direct
|
|
970
|
-
// tier (P2P/TURN DataChannel): the proxy hop is gone, so spend the
|
|
971
|
-
// headroom on fidelity + smoothness (~25fps / q60). H.264 tier: the JPEG
|
|
972
|
-
// is only encoder input (bandwidth decouples from its size), so feed it
|
|
973
|
-
// high quality at the encoder's frame rate. The server drops frames a
|
|
974
|
-
// slow viewer can't take (dropFramesOnBackpressure) and the screencast
|
|
975
|
-
// self-throttles on the active transport's buffer, so these stay
|
|
976
|
-
// ceilings. (Shared session: max quality / min interval across viewers.)
|
|
977
|
-
getQuality: () => (userQuality ?? (transcoder && rtc?.isMediaReady() ? 80 : rtc?.isActive() ? 60 : 40)),
|
|
978
|
-
getMinIntervalMs: () => (userMinIntervalMs ?? (transcoder && rtc?.isMediaReady() ? 40 : rtc?.isActive() ? 40 : 55)),
|
|
979
|
-
onFrame: (jpeg) => {
|
|
980
|
-
// H.264 tier: the poll feeds the encoder; the WebRTC media track is
|
|
981
|
-
// already DTLS-E2E, so no app-layer crypto there.
|
|
982
|
-
if (transcoder && rtc?.isMediaReady()) {
|
|
983
|
-
transcoder.feed(jpeg);
|
|
1010
|
+
// A JPEG frame for the DataChannel/relay tiers. With E2E negotiated,
|
|
1011
|
+
// encrypt with the viewer's key; until it arrives, WITHHOLD (never leak a
|
|
1012
|
+
// plaintext frame to the server). Frames are stateless, so the first one
|
|
1013
|
+
// just shows a beat later.
|
|
1014
|
+
const sendJpegFrame = (jpeg) => {
|
|
1015
|
+
if (expectE2E) {
|
|
1016
|
+
if (!e2eKey)
|
|
984
1017
|
return;
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1018
|
+
sendToViewer(encryptScreenPayload(e2eKey, jpeg));
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
sendToViewer(jpeg);
|
|
1022
|
+
};
|
|
1023
|
+
if (useDesktop) {
|
|
1024
|
+
desktopVideo = createDesktopVideo({
|
|
1025
|
+
host: controlHost,
|
|
1026
|
+
port: controlPort,
|
|
1027
|
+
mode: 'jpeg', // the media track (if any) comes up later
|
|
1028
|
+
fps: 25,
|
|
1029
|
+
bitrateKbps: bitrate.current(),
|
|
1030
|
+
onAccessUnit: (au) => { rtc?.sendVideoFrame(au); },
|
|
1031
|
+
onJpeg: sendJpegFrame,
|
|
1032
|
+
onExit: () => {
|
|
1033
|
+
// The display can't be streamed after all — tell the viewer (it
|
|
1034
|
+
// falls back to noVNC) and end this session.
|
|
1035
|
+
try {
|
|
1036
|
+
if (relayWs.readyState === 1)
|
|
1037
|
+
relayWs.send(JSON.stringify({ type: 'screen-source', source: 'page' }));
|
|
1038
|
+
}
|
|
1039
|
+
catch { /* ignore */ }
|
|
1040
|
+
cleanupScreen('desktop-stream-failed');
|
|
1041
|
+
},
|
|
1042
|
+
log: { info: (m) => log?.info(m), warn: (m) => log?.warn(m) },
|
|
1043
|
+
});
|
|
1044
|
+
desktopInput = createDesktopInput({ host: controlHost, port: controlPort, log: { info: (m) => log?.info(m), warn: (m) => log?.warn(m) } });
|
|
1045
|
+
entry.input = desktopInput;
|
|
1046
|
+
}
|
|
1047
|
+
else {
|
|
1048
|
+
// Attach to the (shared, per-browser) CDP screencast — one capture loop
|
|
1049
|
+
// no matter how many viewers are watching this sandbox.
|
|
1050
|
+
acquireCdpScreencast({
|
|
1051
|
+
host: cdpHost,
|
|
1052
|
+
port: cdpPort,
|
|
1053
|
+
minIntervalMs: 55,
|
|
1054
|
+
quality: 40,
|
|
1055
|
+
log: { info: (m) => log?.info(m), warn: (m) => log?.warn(m) },
|
|
1056
|
+
}, channelId, {
|
|
1057
|
+
// Transport-adaptive tuning. Relay tier: ~18fps / q40 — capture is only
|
|
1058
|
+
// ~38ms, so the bottleneck is per-frame transfer over the (Cloudflare-
|
|
1059
|
+
// proxied) relay; smaller frames transfer faster = lower latency. Direct
|
|
1060
|
+
// tier (P2P/TURN DataChannel): the proxy hop is gone, so spend the
|
|
1061
|
+
// headroom on fidelity + smoothness (~25fps / q60). H.264 tier: the JPEG
|
|
1062
|
+
// is only encoder input (bandwidth decouples from its size), so feed it
|
|
1063
|
+
// high quality at the encoder's frame rate. The server drops frames a
|
|
1064
|
+
// slow viewer can't take (dropFramesOnBackpressure) and the screencast
|
|
1065
|
+
// self-throttles on the active transport's buffer, so these stay
|
|
1066
|
+
// ceilings. (Shared session: max quality / min interval across viewers.)
|
|
1067
|
+
getQuality: () => (userQuality ?? (transcoder && rtc?.isMediaReady() ? 80 : rtc?.isActive() ? 60 : 40)),
|
|
1068
|
+
getMinIntervalMs: () => (userMinIntervalMs ?? (transcoder && rtc?.isMediaReady() ? 40 : rtc?.isActive() ? 40 : 55)),
|
|
1069
|
+
onFrame: (jpeg) => {
|
|
1070
|
+
// H.264 tier: the poll feeds the encoder; the WebRTC media track is
|
|
1071
|
+
// already DTLS-E2E, so no app-layer crypto there.
|
|
1072
|
+
if (transcoder && rtc?.isMediaReady()) {
|
|
1073
|
+
transcoder.feed(jpeg);
|
|
992
1074
|
return;
|
|
993
|
-
|
|
1075
|
+
}
|
|
1076
|
+
sendJpegFrame(jpeg);
|
|
1077
|
+
},
|
|
1078
|
+
// Frames carry no pointer (headless screenshots have no OS cursor), so
|
|
1079
|
+
// stream the page's pointer position — the viewer draws the arrow. The
|
|
1080
|
+
// shape (CSS cursor under the pointer) gives the hover affordance.
|
|
1081
|
+
onCursor: (x, y, click, shape) => {
|
|
1082
|
+
const msg = { type: 'cursor', x, y };
|
|
1083
|
+
if (click)
|
|
1084
|
+
msg.click = true;
|
|
1085
|
+
if (shape)
|
|
1086
|
+
msg.shape = shape;
|
|
1087
|
+
sendToViewer(JSON.stringify(msg));
|
|
1088
|
+
},
|
|
1089
|
+
// Text copied inside the sandbox page → viewer clipboard. Clipboard is
|
|
1090
|
+
// sensitive (passwords), so encrypt it under E2E — sent as a base64
|
|
1091
|
+
// `enc` field (text, so the server's frame-drop never touches it). When
|
|
1092
|
+
// E2E is expected but the key hasn't landed, drop rather than leak.
|
|
1093
|
+
onClipboard: (text) => {
|
|
1094
|
+
if (expectE2E) {
|
|
1095
|
+
if (!e2eKey)
|
|
1096
|
+
return;
|
|
1097
|
+
sendToViewer(JSON.stringify({ type: 'clipboard', enc: encryptScreenPayload(e2eKey, Buffer.from(text, 'utf-8')).toString('base64') }));
|
|
1098
|
+
}
|
|
1099
|
+
else {
|
|
1100
|
+
sendToViewer(JSON.stringify({ type: 'clipboard', text }));
|
|
1101
|
+
}
|
|
1102
|
+
},
|
|
1103
|
+
getBackpressure: () => {
|
|
1104
|
+
if (transcoder && rtc?.isMediaReady())
|
|
1105
|
+
return 0; // encoder drops internally; RTP self-paces
|
|
1106
|
+
return rtc?.isActive() ? rtc.getBufferedAmount() : (relayOpen ? relayWs.bufferedAmount : 0);
|
|
1107
|
+
},
|
|
1108
|
+
}).then((session) => {
|
|
1109
|
+
if (!session) {
|
|
1110
|
+
cleanupScreen('cdp-init-failed');
|
|
994
1111
|
return;
|
|
995
1112
|
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
if (shape)
|
|
1006
|
-
msg.shape = shape;
|
|
1007
|
-
sendToViewer(JSON.stringify(msg));
|
|
1008
|
-
},
|
|
1009
|
-
// Text copied inside the sandbox page → viewer clipboard. Clipboard is
|
|
1010
|
-
// sensitive (passwords), so encrypt it under E2E — sent as a base64
|
|
1011
|
-
// `enc` field (text, so the server's frame-drop never touches it). When
|
|
1012
|
-
// E2E is expected but the key hasn't landed, drop rather than leak.
|
|
1013
|
-
onClipboard: (text) => {
|
|
1014
|
-
if (expectE2E) {
|
|
1015
|
-
if (!e2eKey)
|
|
1016
|
-
return;
|
|
1017
|
-
sendToViewer(JSON.stringify({ type: 'clipboard', enc: encryptScreenPayload(e2eKey, Buffer.from(text, 'utf-8')).toString('base64') }));
|
|
1018
|
-
}
|
|
1019
|
-
else {
|
|
1020
|
-
sendToViewer(JSON.stringify({ type: 'clipboard', text }));
|
|
1021
|
-
}
|
|
1022
|
-
},
|
|
1023
|
-
getBackpressure: () => {
|
|
1024
|
-
if (transcoder && rtc?.isMediaReady())
|
|
1025
|
-
return 0; // encoder drops internally; RTP self-paces
|
|
1026
|
-
return rtc?.isActive() ? rtc.getBufferedAmount() : (relayOpen ? relayWs.bufferedAmount : 0);
|
|
1027
|
-
},
|
|
1028
|
-
}).then((session) => {
|
|
1029
|
-
if (!session) {
|
|
1030
|
-
cleanupScreen('cdp-init-failed');
|
|
1031
|
-
return;
|
|
1032
|
-
}
|
|
1033
|
-
if (!screenConnections.has(channelId)) {
|
|
1034
|
-
releaseCdpScreencast(cdpHost, cdpPort, channelId);
|
|
1035
|
-
return;
|
|
1036
|
-
} // closed during init
|
|
1037
|
-
entry.cdp = session;
|
|
1038
|
-
log?.info(`[gateway] sandbox-screen: CDP screencast attached channelId=${channelId} (${cdpHost}:${cdpPort})`);
|
|
1039
|
-
}).catch((err) => { log?.warn(`[gateway] sandbox-screen: CDP init error ${String(err)}`); cleanupScreen('cdp-error'); });
|
|
1113
|
+
if (!screenConnections.has(channelId)) {
|
|
1114
|
+
releaseCdpScreencast(cdpHost, cdpPort, channelId);
|
|
1115
|
+
return;
|
|
1116
|
+
} // closed during init
|
|
1117
|
+
entry.cdp = session;
|
|
1118
|
+
entry.input = session;
|
|
1119
|
+
log?.info(`[gateway] sandbox-screen: CDP screencast attached channelId=${channelId} (${cdpHost}:${cdpPort})`);
|
|
1120
|
+
}).catch((err) => { log?.warn(`[gateway] sandbox-screen: CDP init error ${String(err)}`); cleanupScreen('cdp-error'); });
|
|
1121
|
+
}
|
|
1040
1122
|
relayWs.on("open", () => {
|
|
1041
1123
|
try {
|
|
1042
1124
|
relayWs._socket?.setNoDelay?.(true);
|
|
@@ -1044,6 +1126,10 @@ export async function startGatewayMode(options) {
|
|
|
1044
1126
|
catch { /* ignore */ }
|
|
1045
1127
|
log?.info(`[gateway] sandbox-screen: relay WS connected channelId=${channelId}`);
|
|
1046
1128
|
relayOpen = true;
|
|
1129
|
+
// Which source this session streams: 'desktop' (whole X display, X11
|
|
1130
|
+
// input) or 'page' (CDP). A viewer that asked for the desktop and
|
|
1131
|
+
// hears 'page' falls back to its noVNC pipe.
|
|
1132
|
+
relayWs.send(JSON.stringify({ type: 'screen-source', source: useDesktop ? 'desktop' : 'page' }));
|
|
1047
1133
|
for (const f of earlyFrames) {
|
|
1048
1134
|
if (relayWs.readyState === 1)
|
|
1049
1135
|
relayWs.send(f);
|