linkshell-cli 0.10.50 → 0.10.52
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/cli/src/auth.js +14 -3
- package/dist/cli/src/auth.js.map +1 -1
- package/dist/cli/src/commands/desktop-workspace.js +5 -6
- package/dist/cli/src/commands/desktop-workspace.js.map +1 -1
- package/dist/cli/src/commands/host.js +1 -1
- package/dist/cli/src/commands/host.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/host/src/drivers/acp/connection.d.ts +2 -0
- package/dist/host/src/drivers/acp/connection.js +2 -0
- package/dist/host/src/drivers/acp/connection.js.map +1 -1
- package/dist/host/src/drivers/acp/driver.js +5 -0
- package/dist/host/src/drivers/acp/driver.js.map +1 -1
- package/dist/host/src/drivers/claude/driver.js +19 -2
- package/dist/host/src/drivers/claude/driver.js.map +1 -1
- package/dist/host/src/drivers/codex/app-server.js +3 -2
- package/dist/host/src/drivers/codex/app-server.js.map +1 -1
- package/dist/host/src/drivers/codex/driver.js +3 -0
- package/dist/host/src/drivers/codex/driver.js.map +1 -1
- package/dist/host/src/drivers/codex/installation.d.ts +2 -0
- package/dist/host/src/drivers/codex/installation.js +14 -0
- package/dist/host/src/drivers/codex/installation.js.map +1 -1
- package/dist/host/src/index.d.ts +1 -0
- package/dist/host/src/index.js +1 -0
- package/dist/host/src/index.js.map +1 -1
- package/dist/host/src/local-transport.d.ts +3 -1
- package/dist/host/src/local-transport.js +13 -8
- package/dist/host/src/local-transport.js.map +1 -1
- package/dist/host/src/screen-loading.d.ts +3 -0
- package/dist/host/src/screen-loading.js +93 -0
- package/dist/host/src/screen-loading.js.map +1 -0
- package/dist/host/src/screen-viewer.d.ts +2 -0
- package/dist/host/src/screen-viewer.js +67 -27
- package/dist/host/src/screen-viewer.js.map +1 -1
- package/dist/host/src/screen.d.ts +4 -1
- package/dist/host/src/screen.js +45 -22
- package/dist/host/src/screen.js.map +1 -1
- package/dist/host/src/shell-env.d.ts +3 -1
- package/dist/host/src/shell-env.js +55 -20
- package/dist/host/src/shell-env.js.map +1 -1
- package/package.json +3 -3
- package/src/auth.ts +14 -3
- package/src/commands/desktop-workspace.ts +5 -6
- package/src/commands/host.ts +1 -1
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
// device can reach each other, it is a video track from the app, with the hands and the pointer on
|
|
18
18
|
// data channels beside it; the page draws the pointer, which that picture does not have in it.
|
|
19
19
|
// Otherwise it is H.264 down the page's socket, decoded here onto a canvas, as it always was.
|
|
20
|
+
import { SCREEN_LOADING_HTML, SCREEN_LOADING_SCRIPT, SCREEN_LOADING_STYLE } from "./screen-loading.js";
|
|
21
|
+
/** Final for a viewer: falling back to another path would only take the screen back. The app matches it too. */
|
|
22
|
+
export const SCREEN_TAKEN = "屏幕画面被另一台设备接手了";
|
|
20
23
|
const icon = (body) => `<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">${body}</svg>`;
|
|
21
24
|
const ICONS = {
|
|
22
25
|
view: icon('<path d="M2.5 12s3.5-6.5 9.5-6.5 9.5 6.5 9.5 6.5-3.5 6.5-9.5 6.5S2.5 12 2.5 12Z"/><circle cx="12" cy="12" r="2.8"/>'),
|
|
@@ -52,8 +55,6 @@ const STYLE = String.raw `
|
|
|
52
55
|
#pointer.pressed { background: rgba(255,255,255,0.4); }
|
|
53
56
|
#pointer.own { border: 0; border-radius: 0; box-shadow: none; background: transparent center / 100% 100% no-repeat; }
|
|
54
57
|
#stage.own { cursor: none; }
|
|
55
|
-
#note { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; padding: 32px;
|
|
56
|
-
color: rgba(255,255,255,0.72); text-align: center; pointer-events: none; }
|
|
57
58
|
.glass { background: rgba(38,38,42,0.74); -webkit-backdrop-filter: blur(22px) saturate(1.6); backdrop-filter: blur(22px) saturate(1.6);
|
|
58
59
|
border: 0.5px solid rgba(255,255,255,0.16); box-shadow: 0 8px 28px rgba(0,0,0,0.38); }
|
|
59
60
|
#control-shell { position: absolute; pointer-events: none; z-index: 1; will-change: width, height, transform, border-radius, opacity; }
|
|
@@ -441,7 +442,9 @@ function createScreenSession(initial, effects) {
|
|
|
441
442
|
const original = { ...desired };
|
|
442
443
|
const state = () => ({ display: desired.display, width: desired.width, ...(desired.maxFps ? { maxFps: desired.maxFps } : {}), paused: !desired.active || hidden });
|
|
443
444
|
const key = () => JSON.stringify(state());
|
|
444
|
-
|
|
445
|
+
// A lower frame-rate ceiling (a 60 Hz phone, Low Power) needs no new connection from an app that can't
|
|
446
|
+
// reconfigure: the receiver just shows fewer frames. Reloading for it left the native picture "adjusting".
|
|
447
|
+
const targetChanged = () => ["display", "width"].some((name) => desired[name] !== original[name]);
|
|
445
448
|
const changed = () => effects.changed?.({ ...desired, paused: state().paused, inputReady, error });
|
|
446
449
|
const suspend = () => { effects.release(); inputReady = false; };
|
|
447
450
|
const pump = () => {
|
|
@@ -597,8 +600,22 @@ const mods = new Set();
|
|
|
597
600
|
|
|
598
601
|
// ---- The picture ----------------------------------------------------------
|
|
599
602
|
|
|
600
|
-
let sessionError = "";
|
|
601
|
-
const
|
|
603
|
+
let sessionError = "", sessionPhase = "", hasPicture = false;
|
|
604
|
+
const loading = createScreenLoading(note, () => {
|
|
605
|
+
if (app && chrome.canRetryScreen) tellApp({ type: "screenRetry" });
|
|
606
|
+
else if (typeof window.__linkshellReload === "function") window.__linkshellReload({});
|
|
607
|
+
else location.reload();
|
|
608
|
+
}, (phase) => window.__linkshellScreenState?.(phase));
|
|
609
|
+
addEventListener("pagehide", () => loading.dispose(), { once: true });
|
|
610
|
+
const say = (text) => {
|
|
611
|
+
if (!text) hasPicture = true;
|
|
612
|
+
if (sessionError) loading.set("error", sessionError);
|
|
613
|
+
else if (text) loading.set("error", text);
|
|
614
|
+
else if (sessionPhase) loading.set(sessionPhase);
|
|
615
|
+
else loading.set("ready");
|
|
616
|
+
};
|
|
617
|
+
// Transport updates must not hide a configuration failure or a paused retained session.
|
|
618
|
+
const showProgress = (phase) => { if (!sessionError && !sessionPhase) loading.set(phase); };
|
|
602
619
|
const round = (value, digits) => (value == null || !isFinite(value) ? null : +value.toFixed(digits === undefined ? 1 : digits));
|
|
603
620
|
const hex = (n) => n.toString(16).padStart(2, "0");
|
|
604
621
|
function sps(unit) {
|
|
@@ -694,11 +711,13 @@ const session = createScreenSession({ active: true, display: Number(query.get("d
|
|
|
694
711
|
changed: (state) => {
|
|
695
712
|
width = state.width;
|
|
696
713
|
const previousError = sessionError;
|
|
714
|
+
const previousPhase = sessionPhase;
|
|
697
715
|
sessionError = state.error;
|
|
716
|
+
sessionPhase = state.paused ? "paused" : !state.inputReady ? "adjusting" : "";
|
|
698
717
|
if (sessionError) say(sessionError);
|
|
699
|
-
else if (
|
|
700
|
-
|
|
701
|
-
else if (previousError ||
|
|
718
|
+
else if (sessionPhase) loading.set(sessionPhase);
|
|
719
|
+
// Configuration acknowledgement enables input; only received media proves the first picture exists.
|
|
720
|
+
else if (previousError || previousPhase) loading.set(hasPicture ? "ready" : "receiving");
|
|
702
721
|
if (state.inputReady && mode !== "view") askControl();
|
|
703
722
|
refreshWidths();
|
|
704
723
|
refresh();
|
|
@@ -718,14 +737,14 @@ const ws = nativePicture
|
|
|
718
737
|
: new WebSocket((location.protocol === "https:" ? "wss://" : "ws://") + location.host + "/stream?" + asked);
|
|
719
738
|
ws.binaryType = "arraybuffer";
|
|
720
739
|
const send = (message) => { if (ws.readyState === 1) ws.send(JSON.stringify(message)); };
|
|
721
|
-
let capabilityTimer = 0, socketClosed = false;
|
|
740
|
+
let capabilityTimer = 0, socketClosed = false, taken = false;
|
|
722
741
|
function waitForCapabilities() {
|
|
723
742
|
clearTimeout(capabilityTimer);
|
|
724
743
|
capabilityTimer = setTimeout(() => {
|
|
725
744
|
if (!session.known()) session.receive({ t: "capabilities", version: 1, configure: false, pause: false, retainMs: 0 });
|
|
726
745
|
}, 10000);
|
|
727
746
|
}
|
|
728
|
-
ws.onopen = () => { session.open(); waitForCapabilities(); if (mode !== "view") askControl(); };
|
|
747
|
+
ws.onopen = () => { if (loading.phase === "connecting") showProgress("receiving"); session.open(); waitForCapabilities(); if (mode !== "view") askControl(); };
|
|
729
748
|
// Out of sight, a page answers late (its timers slow to one a second) and shows nothing: the host sends no
|
|
730
749
|
// picture down the socket meanwhile, and starts again at a keyframe.
|
|
731
750
|
document.addEventListener("visibilitychange", () => session.visibility(document.hidden));
|
|
@@ -801,9 +820,9 @@ ws.onclose = () => {
|
|
|
801
820
|
session.close();
|
|
802
821
|
// A video track is this socket's: without the one there is no finding the other again, and no hands.
|
|
803
822
|
drop();
|
|
804
|
-
if (requestRelay()) return;
|
|
805
|
-
tellApp({ type: "screenClosed" });
|
|
806
|
-
if (
|
|
823
|
+
if (!taken && requestRelay()) return;
|
|
824
|
+
tellApp({ type: "screenClosed", ...(taken ? { reason: "taken" } : {}) });
|
|
825
|
+
if (loading.phase !== "error") say("屏幕连接已断开,请确认电脑在线后重新连接。");
|
|
807
826
|
blocked = false;
|
|
808
827
|
};
|
|
809
828
|
|
|
@@ -812,7 +831,10 @@ function heard(message) {
|
|
|
812
831
|
if (message.session.t === "capabilities") clearTimeout(capabilityTimer);
|
|
813
832
|
return session.receive(message.session);
|
|
814
833
|
}
|
|
815
|
-
if (message.error)
|
|
834
|
+
if (message.error) {
|
|
835
|
+
if (message.error === SCREEN_TAKEN) taken = true;
|
|
836
|
+
return say(message.error);
|
|
837
|
+
}
|
|
816
838
|
if (message.rtc) return signal(message.rtc);
|
|
817
839
|
if (message.pong) return meter && meter.pong(message.pong);
|
|
818
840
|
if (message.restart) {
|
|
@@ -936,15 +958,17 @@ function watch(mine) {
|
|
|
936
958
|
const state = mine.pc.connectionState || mine.pc.iceConnectionState;
|
|
937
959
|
if (state === "failed" || mine.pc.iceConnectionState === "failed") return giveUp(mine.up ? "the connection failed" : "no path between the two (ICE failed)");
|
|
938
960
|
if (state === "disconnected") {
|
|
961
|
+
showProgress("recovering");
|
|
939
962
|
// A path that drops for a moment comes back by itself; one gone for longer is given up.
|
|
940
963
|
if (mine.up && !mine.away) mine.away = setTimeout(() => away(mine, performance.now()), 4000);
|
|
941
964
|
} else if (state === "connected" || state === "completed") {
|
|
965
|
+
if (loading.phase !== "ready") showProgress("receiving");
|
|
942
966
|
clearTimeout(mine.away);
|
|
943
967
|
mine.away = 0;
|
|
944
968
|
if (mine.up) return;
|
|
945
969
|
mine.up = true;
|
|
946
970
|
clearTimeout(patience);
|
|
947
|
-
if (
|
|
971
|
+
if (loading.phase !== "ready") patience = setTimeout(frameless, 4000);
|
|
948
972
|
}
|
|
949
973
|
}
|
|
950
974
|
|
|
@@ -973,6 +997,7 @@ function frameless() {
|
|
|
973
997
|
function giveUp(reason) {
|
|
974
998
|
if (gaveUp) return;
|
|
975
999
|
gaveUp = true;
|
|
1000
|
+
showProgress("recovering");
|
|
976
1001
|
const pc = rtc && rtc.pc;
|
|
977
1002
|
const told = (detail) => {
|
|
978
1003
|
drop();
|
|
@@ -989,7 +1014,7 @@ function requestRelay() {
|
|
|
989
1014
|
if (!relayRequested) {
|
|
990
1015
|
relayRequested = true;
|
|
991
1016
|
tellApp({ type: "screenFallback" });
|
|
992
|
-
|
|
1017
|
+
showProgress("recovering");
|
|
993
1018
|
}
|
|
994
1019
|
return true;
|
|
995
1020
|
}
|
|
@@ -1010,7 +1035,7 @@ function fallBack() {
|
|
|
1010
1035
|
gaveUp = true;
|
|
1011
1036
|
drop();
|
|
1012
1037
|
if (requestRelay()) return;
|
|
1013
|
-
|
|
1038
|
+
showProgress("recovering");
|
|
1014
1039
|
blocked = false;
|
|
1015
1040
|
if (!windowsHost && !("VideoDecoder" in window)) return unseeable();
|
|
1016
1041
|
if (tried) hint(live ? "直连断开了:已改用兼容方式传画面,延迟会高一些" : "没能和电脑直连:已改用兼容方式传画面,延迟会高一些", 5000);
|
|
@@ -1024,12 +1049,12 @@ function play() {
|
|
|
1024
1049
|
started.then(() => {
|
|
1025
1050
|
if (!blocked) return;
|
|
1026
1051
|
blocked = false;
|
|
1027
|
-
|
|
1052
|
+
showProgress("receiving");
|
|
1028
1053
|
}, (error) => {
|
|
1029
1054
|
// A new stream replacing the one being started rejects the first call too; that is not a refusal.
|
|
1030
1055
|
if (error.name === "AbortError" || !rtc) return;
|
|
1031
1056
|
blocked = true;
|
|
1032
|
-
|
|
1057
|
+
if (!sessionError && !sessionPhase) loading.set("blocked", "浏览器需要你的确认,轻点屏幕即可开始观看。");
|
|
1033
1058
|
});
|
|
1034
1059
|
}
|
|
1035
1060
|
// What a browser takes as someone's say-so to start a video.
|
|
@@ -1041,19 +1066,34 @@ function sized() {
|
|
|
1041
1066
|
// While this page presents the frames itself, the size that counts is the frame's own: the
|
|
1042
1067
|
// <video> is kept running as the fallback but is not what is on screen.
|
|
1043
1068
|
if (presenter) {
|
|
1044
|
-
clearTimeout(patience);
|
|
1069
|
+
if (presenter.shown) clearTimeout(patience);
|
|
1045
1070
|
if (!live) show(true);
|
|
1046
1071
|
return;
|
|
1047
1072
|
}
|
|
1048
1073
|
content.w = video.videoWidth;
|
|
1049
1074
|
content.h = video.videoHeight;
|
|
1050
|
-
if (!blocked) say("");
|
|
1051
1075
|
if (live) return layout();
|
|
1052
|
-
clearTimeout(patience);
|
|
1053
1076
|
show(true);
|
|
1054
1077
|
}
|
|
1055
1078
|
video.addEventListener("resize", sized);
|
|
1056
1079
|
video.addEventListener("loadedmetadata", sized);
|
|
1080
|
+
// Metadata and a connected socket do not mean a frame is visible yet.
|
|
1081
|
+
function videoShown() {
|
|
1082
|
+
if (!rtc || blocked || video.readyState < 2 || presenter) return;
|
|
1083
|
+
clearTimeout(patience);
|
|
1084
|
+
sized();
|
|
1085
|
+
say("");
|
|
1086
|
+
}
|
|
1087
|
+
if ("requestVideoFrameCallback" in video) {
|
|
1088
|
+
const nextFrame = () => {
|
|
1089
|
+
if (loading.phase !== "ready") videoShown();
|
|
1090
|
+
video.requestVideoFrameCallback(nextFrame);
|
|
1091
|
+
};
|
|
1092
|
+
video.requestVideoFrameCallback(nextFrame);
|
|
1093
|
+
} else {
|
|
1094
|
+
video.addEventListener("playing", videoShown);
|
|
1095
|
+
video.addEventListener("timeupdate", videoShown);
|
|
1096
|
+
}
|
|
1057
1097
|
|
|
1058
1098
|
// ---- Presenting the track's frames this page's own way --------------------
|
|
1059
1099
|
|
|
@@ -1129,6 +1169,7 @@ async function presentTrack(track) {
|
|
|
1129
1169
|
draws.end();
|
|
1130
1170
|
device.queue.submit([pass.finish()]);
|
|
1131
1171
|
mine.shown += 1;
|
|
1172
|
+
say("");
|
|
1132
1173
|
if (mine.w !== frame.displayWidth || mine.h !== frame.displayHeight) {
|
|
1133
1174
|
mine.w = frame.displayWidth;
|
|
1134
1175
|
mine.h = frame.displayHeight;
|
|
@@ -1138,7 +1179,6 @@ async function presentTrack(track) {
|
|
|
1138
1179
|
// the <video> beside it was allowed to start playing.
|
|
1139
1180
|
clearTimeout(patience);
|
|
1140
1181
|
blocked = false;
|
|
1141
|
-
say("");
|
|
1142
1182
|
if (!live) show(true);
|
|
1143
1183
|
else layout();
|
|
1144
1184
|
}
|
|
@@ -3108,7 +3148,7 @@ if (nativePicture) {
|
|
|
3108
3148
|
session.reconnect();
|
|
3109
3149
|
waitForCapabilities();
|
|
3110
3150
|
Object.assign(control, { asked: false, known: false, trusted: false });
|
|
3111
|
-
|
|
3151
|
+
showProgress("connecting");
|
|
3112
3152
|
if (mode !== "view") askControl();
|
|
3113
3153
|
},
|
|
3114
3154
|
};
|
|
@@ -3126,16 +3166,16 @@ if (nativePicture) {
|
|
|
3126
3166
|
}
|
|
3127
3167
|
tellApp({ type: "ready", mode });
|
|
3128
3168
|
`;
|
|
3129
|
-
const SCRIPT = `(() => {${VIEWER_LOGIC}${BODY}})();`;
|
|
3169
|
+
const SCRIPT = `(() => {const SCREEN_TAKEN = ${JSON.stringify(SCREEN_TAKEN)};${VIEWER_LOGIC}${SCREEN_LOADING_SCRIPT}${BODY}})();`;
|
|
3130
3170
|
export function viewerPage(platform = "darwin") {
|
|
3131
3171
|
return `<!doctype html>
|
|
3132
3172
|
<html><head><meta charset="utf-8">
|
|
3133
3173
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
|
|
3134
3174
|
<title>屏幕</title>
|
|
3135
|
-
<style>${STYLE}</style></head>
|
|
3175
|
+
<style>${STYLE}${SCREEN_LOADING_STYLE}</style></head>
|
|
3136
3176
|
<body data-platform="${platform === "win32" ? "win32" : "darwin"}">
|
|
3137
3177
|
<div id="stage"><div id="picture"><canvas id="screen"></canvas><canvas id="gpu" class="gone"></canvas><video id="video" class="gone" autoplay playsinline muted></video><div id="pointer"></div></div></div>
|
|
3138
|
-
|
|
3178
|
+
${SCREEN_LOADING_HTML}
|
|
3139
3179
|
<div id="toast" class="glass"></div>
|
|
3140
3180
|
<div id="menu" class="glass gone">
|
|
3141
3181
|
<div class="choice" role="button" aria-label="只看,不操作电脑" data-mode="view"><i></i><div><b>只看</b><span>不操作电脑</span></div></div>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screen-viewer.js","sourceRoot":"","sources":["../../../../host/src/screen-viewer.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,2EAA2E;AAC3E,6EAA6E;AAC7E,mEAAmE;AACnE,EAAE;AACF,4DAA4D;AAC5D,yEAAyE;AACzE,2CAA2C;AAC3C,6EAA6E;AAC7E,+EAA+E;AAC/E,2CAA2C;AAC3C,qGAAqG;AACrG,+EAA+E;AAC/E,6FAA6F;AAC7F,EAAE;AACF,oGAAoG;AACpG,mGAAmG;AACnG,+FAA+F;AAC/F,8FAA8F;AAE9F,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,EAAE,CAC5B,uJAAuJ,IAAI,QAAQ,CAAC;AAEtK,MAAM,KAAK,GAAG;IACZ,IAAI,EAAE,IAAI,CAAC,qHAAqH,CAAC;IACjI,QAAQ,EAAE,IAAI,CAAC,6DAA6D,CAAC;IAC7E,KAAK,EAAE,IAAI,CAAC,oGAAoG,CAAC;IACjH,QAAQ,EAAE,IAAI,CAAC,iJAAiJ,CAAC;IACjK,GAAG,EAAE,IAAI,CAAC,0EAA0E,CAAC;IACrF,MAAM,EAAE,IAAI,CAAC,oDAAoD,CAAC;IAClE,MAAM,EAAE,IAAI,CAAC,oDAAoD,CAAC;IAClE,IAAI,EAAE,IAAI,CAAC,4BAA4B,CAAC;IACxC,KAAK,EAAE,IAAI,CAAC,qDAAqD,CAAC;IAClE,OAAO,EAAE,IAAI,CAAC,sEAAsE,CAAC;IACrF,IAAI,EAAE,IAAI,CAAC,iEAAiE,CAAC;IAC7E,KAAK,EAAE,IAAI,CAAC,4CAA4C,CAAC;CAC1D,CAAC;AAEF,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA
|
|
1
|
+
{"version":3,"file":"screen-viewer.js","sourceRoot":"","sources":["../../../../host/src/screen-viewer.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,2EAA2E;AAC3E,6EAA6E;AAC7E,mEAAmE;AACnE,EAAE;AACF,4DAA4D;AAC5D,yEAAyE;AACzE,2CAA2C;AAC3C,6EAA6E;AAC7E,+EAA+E;AAC/E,2CAA2C;AAC3C,qGAAqG;AACrG,+EAA+E;AAC/E,6FAA6F;AAC7F,EAAE;AACF,oGAAoG;AACpG,mGAAmG;AACnG,+FAA+F;AAC/F,8FAA8F;AAE9F,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAEvG,gHAAgH;AAChH,MAAM,CAAC,MAAM,YAAY,GAAG,eAAe,CAAC;AAE5C,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,EAAE,CAC5B,uJAAuJ,IAAI,QAAQ,CAAC;AAEtK,MAAM,KAAK,GAAG;IACZ,IAAI,EAAE,IAAI,CAAC,qHAAqH,CAAC;IACjI,QAAQ,EAAE,IAAI,CAAC,6DAA6D,CAAC;IAC7E,KAAK,EAAE,IAAI,CAAC,oGAAoG,CAAC;IACjH,QAAQ,EAAE,IAAI,CAAC,iJAAiJ,CAAC;IACjK,GAAG,EAAE,IAAI,CAAC,0EAA0E,CAAC;IACrF,MAAM,EAAE,IAAI,CAAC,oDAAoD,CAAC;IAClE,MAAM,EAAE,IAAI,CAAC,oDAAoD,CAAC;IAClE,IAAI,EAAE,IAAI,CAAC,4BAA4B,CAAC;IACxC,KAAK,EAAE,IAAI,CAAC,qDAAqD,CAAC;IAClE,OAAO,EAAE,IAAI,CAAC,sEAAsE,CAAC;IACrF,IAAI,EAAE,IAAI,CAAC,iEAAiE,CAAC;IAC7E,KAAK,EAAE,IAAI,CAAC,4CAA4C,CAAC;CAC1D,CAAC;AAEF,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkHvB,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4YrC,CAAC;AAEF,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsjFtB,CAAC;AAEF,MAAM,MAAM,GAAG,gCAAgC,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,IAAI,YAAY,GAAG,qBAAqB,GAAG,IAAI,OAAO,CAAC;AAElI,MAAM,UAAU,UAAU,CAAC,WAA4B,QAAQ;IAC7D,OAAO;;;;SAIA,KAAK,GAAG,oBAAoB;uBACd,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ;;EAE9D,mBAAmB;;;;;;;;iHAQ4F,KAAK,CAAC,KAAK;;;;;;;;;;;;;;;kEAe1D,KAAK,CAAC,QAAQ;iEACf,KAAK,CAAC,KAAK;+DACb,KAAK,CAAC,GAAG;gEACR,KAAK,CAAC,IAAI;;;;;;;;;;;;;;;4CAe9B,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,aAAa,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ;;4CAEtF,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,aAAa,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;;;;;;;;;;;;;;;;;;;;;;;oCAuBnG,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG;oCAClC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG;;;;;;;;;;6CAUzB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;UACxD,MAAM;eACD,CAAC;AAChB,CAAC"}
|
|
@@ -56,7 +56,9 @@ export declare class ScreenShare {
|
|
|
56
56
|
/** The servers both ends ask for their public address, to reach each other directly. */
|
|
57
57
|
private readonly iceServers;
|
|
58
58
|
private server?;
|
|
59
|
-
|
|
59
|
+
/** Two starts at once (the phone and a desktop, a retry) must share one bind, not read an unbound address. */
|
|
60
|
+
private listening?;
|
|
61
|
+
private tokens;
|
|
60
62
|
private displays;
|
|
61
63
|
private readonly captures;
|
|
62
64
|
/**
|
|
@@ -80,6 +82,7 @@ export declare class ScreenShare {
|
|
|
80
82
|
}>;
|
|
81
83
|
private listDisplays;
|
|
82
84
|
private listen;
|
|
85
|
+
private bind;
|
|
83
86
|
private authorized;
|
|
84
87
|
/**
|
|
85
88
|
* Whether the screen can be watched and controlled. On a Mac that is the
|
package/dist/host/src/screen.js
CHANGED
|
@@ -7,7 +7,7 @@ import { WebSocketServer } from "ws";
|
|
|
7
7
|
import { RpcError } from "@linkshell/wire";
|
|
8
8
|
import { closeInputApp, inputApp, InputControl, macAppSupported, NO_APP, videoSignal } from "./input.js";
|
|
9
9
|
import { LADDER, Pacer, RELAY_LEVEL, rung } from "./screen-pacer.js";
|
|
10
|
-
import { viewerPage } from "./screen-viewer.js";
|
|
10
|
+
import { SCREEN_TAKEN, viewerPage } from "./screen-viewer.js";
|
|
11
11
|
import { screenMeasurementLog } from "./screen-metrics.js";
|
|
12
12
|
import { captureWindows, configureWindowsCapture, WindowsFrameSplitter, windowsDisplays, windowsHelper, windowsProfile } from "./windows-screen.js";
|
|
13
13
|
import { WindowsVideoSession, windowsMedia } from "./windows-video.js";
|
|
@@ -201,12 +201,15 @@ export async function resolvedCandidate(sdp, resolve = mdns) {
|
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
const mdns = async (name) => (await lookup(name, { family: 4 })).address;
|
|
204
|
+
const RECENT_TOKENS = 4;
|
|
204
205
|
/** The loopback viewer server; started on first use, one capture per watching device. */
|
|
205
206
|
export class ScreenShare {
|
|
206
207
|
log;
|
|
207
208
|
iceServers;
|
|
208
209
|
server;
|
|
209
|
-
|
|
210
|
+
/** Two starts at once (the phone and a desktop, a retry) must share one bind, not read an unbound address. */
|
|
211
|
+
listening;
|
|
212
|
+
tokens = [];
|
|
210
213
|
displays = [];
|
|
211
214
|
captures = new Set();
|
|
212
215
|
/**
|
|
@@ -231,10 +234,12 @@ export class ScreenShare {
|
|
|
231
234
|
const displays = (this.displays = await this.listDisplays());
|
|
232
235
|
if (!displays.length)
|
|
233
236
|
throw RpcError.app("not_supported", "没有找到可以捕获的屏幕");
|
|
234
|
-
// A fresh token per start
|
|
235
|
-
|
|
237
|
+
// A fresh token per start; only the last few keep working, so devices starting
|
|
238
|
+
// at the same time (the phone and a desktop) don't lock each other out.
|
|
239
|
+
const token = randomBytes(24).toString("base64url");
|
|
240
|
+
this.tokens = [token, ...this.tokens].slice(0, RECENT_TOKENS);
|
|
236
241
|
const port = await this.listen();
|
|
237
|
-
return { port, token
|
|
242
|
+
return { port, token, displays: displays.map(({ index, name }) => ({ index, name })) };
|
|
238
243
|
}
|
|
239
244
|
async listDisplays() {
|
|
240
245
|
if (process.platform === "win32")
|
|
@@ -255,8 +260,15 @@ export class ScreenShare {
|
|
|
255
260
|
return [{ index: 0, name: "屏幕", screen: 0, ...(dimensions ? { w: Number(dimensions[1]), h: Number(dimensions[2]) } : {}) }];
|
|
256
261
|
}
|
|
257
262
|
listen() {
|
|
258
|
-
|
|
259
|
-
|
|
263
|
+
this.listening ??= this.bind().catch((error) => {
|
|
264
|
+
this.server?.close();
|
|
265
|
+
this.server = undefined;
|
|
266
|
+
this.listening = undefined;
|
|
267
|
+
throw error;
|
|
268
|
+
});
|
|
269
|
+
return this.listening;
|
|
270
|
+
}
|
|
271
|
+
bind() {
|
|
260
272
|
const server = createServer((request, response) => {
|
|
261
273
|
const url = new URL(request.url ?? "/", "http://localhost");
|
|
262
274
|
if (!this.authorized(url)) {
|
|
@@ -297,8 +309,10 @@ export class ScreenShare {
|
|
|
297
309
|
}
|
|
298
310
|
authorized(url) {
|
|
299
311
|
const given = Buffer.from(url.searchParams.get("token") ?? "");
|
|
300
|
-
|
|
301
|
-
|
|
312
|
+
return this.tokens.some((token) => {
|
|
313
|
+
const expected = Buffer.from(token);
|
|
314
|
+
return given.length === expected.length && timingSafeEqual(given, expected);
|
|
315
|
+
});
|
|
302
316
|
}
|
|
303
317
|
/**
|
|
304
318
|
* Whether the screen can be watched and controlled. On a Mac that is the
|
|
@@ -451,7 +465,7 @@ export class ScreenShare {
|
|
|
451
465
|
previous.end();
|
|
452
466
|
// Another device's picture ends here; a viewer changing its own just carries on.
|
|
453
467
|
if (previous.on !== on)
|
|
454
|
-
previous.on.exit(
|
|
468
|
+
previous.on.exit(SCREEN_TAKEN);
|
|
455
469
|
await captureReleased(previous);
|
|
456
470
|
}
|
|
457
471
|
if (!wanted())
|
|
@@ -477,12 +491,13 @@ export class ScreenShare {
|
|
|
477
491
|
await windowsDisplays();
|
|
478
492
|
if (viewer.closed())
|
|
479
493
|
return "refused";
|
|
494
|
+
viewer.claim();
|
|
480
495
|
this.video?.taken();
|
|
481
496
|
const holder = this.holder;
|
|
482
497
|
if (holder) {
|
|
483
498
|
this.holder = undefined;
|
|
484
499
|
holder.end();
|
|
485
|
-
holder.on.exit(
|
|
500
|
+
holder.on.exit(SCREEN_TAKEN);
|
|
486
501
|
await captureReleased(holder);
|
|
487
502
|
}
|
|
488
503
|
if (viewer.closed())
|
|
@@ -496,7 +511,7 @@ export class ScreenShare {
|
|
|
496
511
|
if (this.video === mine)
|
|
497
512
|
this.video = undefined;
|
|
498
513
|
session.end();
|
|
499
|
-
viewer.refuse(
|
|
514
|
+
viewer.refuse(SCREEN_TAKEN);
|
|
500
515
|
} };
|
|
501
516
|
this.video = mine;
|
|
502
517
|
const cancelled = setInterval(() => { if (viewer.closed())
|
|
@@ -538,11 +553,12 @@ export class ScreenShare {
|
|
|
538
553
|
if (viewer.closed())
|
|
539
554
|
return "refused";
|
|
540
555
|
// The latest viewer has the screen, whichever way the one before was getting it.
|
|
556
|
+
viewer.claim();
|
|
541
557
|
this.video?.taken();
|
|
542
558
|
const holder = this.holder;
|
|
543
559
|
if (holder) {
|
|
544
560
|
holder.end();
|
|
545
|
-
holder.on.exit(
|
|
561
|
+
holder.on.exit(SCREEN_TAKEN);
|
|
546
562
|
await captureReleased(holder);
|
|
547
563
|
if (this.holder === holder)
|
|
548
564
|
this.holder = undefined;
|
|
@@ -556,7 +572,7 @@ export class ScreenShare {
|
|
|
556
572
|
if (this.video === mine)
|
|
557
573
|
this.video = undefined;
|
|
558
574
|
app.endVideo(id);
|
|
559
|
-
viewer.refuse(
|
|
575
|
+
viewer.refuse(SCREEN_TAKEN);
|
|
560
576
|
},
|
|
561
577
|
};
|
|
562
578
|
this.video = mine;
|
|
@@ -599,13 +615,6 @@ export class ScreenShare {
|
|
|
599
615
|
return { id, reconfigure: status.videoReconfigure === true, configure: configuration => app.configureVideo(id, configuration), signal: message => app.signal(id, message), end: () => app.endVideo(id) };
|
|
600
616
|
}
|
|
601
617
|
stream(ws, display, relayed, wantsVideo, width, maxFps, diagnostics = false, codec) {
|
|
602
|
-
// Paused viewers still own a session. A new authenticated viewer takes that ownership too.
|
|
603
|
-
for (const [previous, release] of this.viewers) {
|
|
604
|
-
if (previous.readyState === previous.OPEN)
|
|
605
|
-
previous.send(JSON.stringify({ error: "屏幕画面被另一台设备接手了" }));
|
|
606
|
-
release();
|
|
607
|
-
previous.close();
|
|
608
|
-
}
|
|
609
618
|
let shown = this.displays.find((entry) => entry.index === display) ?? this.displays[0];
|
|
610
619
|
let desired = { display: shown?.index ?? 0, width: width ?? 1920, maxFps: maxFps === 30 || maxFps === 60 || maxFps === 120 ? maxFps : undefined, paused: false };
|
|
611
620
|
let closed = false;
|
|
@@ -938,10 +947,23 @@ export class ScreenShare {
|
|
|
938
947
|
}
|
|
939
948
|
void drain();
|
|
940
949
|
};
|
|
950
|
+
// Paused viewers still own a session; the latest viewer takes it over, but only once it is
|
|
951
|
+
// about to take the capture or the video: one that fails before that mustn't end a healthy one.
|
|
952
|
+
const claim = () => {
|
|
953
|
+
for (const [previous, release] of this.viewers) {
|
|
954
|
+
if (previous === ws)
|
|
955
|
+
continue;
|
|
956
|
+
if (previous.readyState === previous.OPEN)
|
|
957
|
+
previous.send(JSON.stringify({ error: SCREEN_TAKEN }));
|
|
958
|
+
release();
|
|
959
|
+
previous.close();
|
|
960
|
+
}
|
|
961
|
+
};
|
|
941
962
|
const startFallback = () => {
|
|
942
963
|
endVideo();
|
|
943
964
|
if (!live())
|
|
944
965
|
return;
|
|
966
|
+
claim();
|
|
945
967
|
tell({ rtc: { t: "off" } });
|
|
946
968
|
canConfigure = !!shown?.w && !!shown?.h;
|
|
947
969
|
initialized = true;
|
|
@@ -1055,7 +1077,7 @@ export class ScreenShare {
|
|
|
1055
1077
|
if (wantsVideo) {
|
|
1056
1078
|
let lostWhileOpening = false;
|
|
1057
1079
|
void this.offerVideo(shown?.screen ?? 0, width, maxFps, diagnostics, codec, {
|
|
1058
|
-
closed: () => !live(), tell,
|
|
1080
|
+
closed: () => !live(), claim, tell,
|
|
1059
1081
|
refuse: (message) => { tell({ error: message }); dispose(); ws.close(); },
|
|
1060
1082
|
lost: () => { if (video)
|
|
1061
1083
|
fallBack();
|
|
@@ -1118,6 +1140,7 @@ export class ScreenShare {
|
|
|
1118
1140
|
closeInputApp();
|
|
1119
1141
|
this.server?.close();
|
|
1120
1142
|
this.server = undefined;
|
|
1143
|
+
this.listening = undefined;
|
|
1121
1144
|
}
|
|
1122
1145
|
}
|
|
1123
1146
|
//# sourceMappingURL=screen.js.map
|