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.
Files changed (43) hide show
  1. package/dist/cli/src/auth.js +14 -3
  2. package/dist/cli/src/auth.js.map +1 -1
  3. package/dist/cli/src/commands/desktop-workspace.js +5 -6
  4. package/dist/cli/src/commands/desktop-workspace.js.map +1 -1
  5. package/dist/cli/src/commands/host.js +1 -1
  6. package/dist/cli/src/commands/host.js.map +1 -1
  7. package/dist/cli/tsconfig.tsbuildinfo +1 -1
  8. package/dist/host/src/drivers/acp/connection.d.ts +2 -0
  9. package/dist/host/src/drivers/acp/connection.js +2 -0
  10. package/dist/host/src/drivers/acp/connection.js.map +1 -1
  11. package/dist/host/src/drivers/acp/driver.js +5 -0
  12. package/dist/host/src/drivers/acp/driver.js.map +1 -1
  13. package/dist/host/src/drivers/claude/driver.js +19 -2
  14. package/dist/host/src/drivers/claude/driver.js.map +1 -1
  15. package/dist/host/src/drivers/codex/app-server.js +3 -2
  16. package/dist/host/src/drivers/codex/app-server.js.map +1 -1
  17. package/dist/host/src/drivers/codex/driver.js +3 -0
  18. package/dist/host/src/drivers/codex/driver.js.map +1 -1
  19. package/dist/host/src/drivers/codex/installation.d.ts +2 -0
  20. package/dist/host/src/drivers/codex/installation.js +14 -0
  21. package/dist/host/src/drivers/codex/installation.js.map +1 -1
  22. package/dist/host/src/index.d.ts +1 -0
  23. package/dist/host/src/index.js +1 -0
  24. package/dist/host/src/index.js.map +1 -1
  25. package/dist/host/src/local-transport.d.ts +3 -1
  26. package/dist/host/src/local-transport.js +13 -8
  27. package/dist/host/src/local-transport.js.map +1 -1
  28. package/dist/host/src/screen-loading.d.ts +3 -0
  29. package/dist/host/src/screen-loading.js +93 -0
  30. package/dist/host/src/screen-loading.js.map +1 -0
  31. package/dist/host/src/screen-viewer.d.ts +2 -0
  32. package/dist/host/src/screen-viewer.js +67 -27
  33. package/dist/host/src/screen-viewer.js.map +1 -1
  34. package/dist/host/src/screen.d.ts +4 -1
  35. package/dist/host/src/screen.js +45 -22
  36. package/dist/host/src/screen.js.map +1 -1
  37. package/dist/host/src/shell-env.d.ts +3 -1
  38. package/dist/host/src/shell-env.js +55 -20
  39. package/dist/host/src/shell-env.js.map +1 -1
  40. package/package.json +3 -3
  41. package/src/auth.ts +14 -3
  42. package/src/commands/desktop-workspace.ts +5 -6
  43. 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
- const targetChanged = () => ["display", "width", "maxFps"].some((name) => desired[name] !== original[name]);
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 say = (text) => { text = sessionError || text; note.textContent = text; note.classList.toggle("gone", !text); };
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 (state.paused) say("屏幕已暂停");
700
- else if (!state.inputReady) say("正在调整屏幕…");
701
- else if (previousError || note.textContent === "屏幕已暂停" || note.textContent === "正在调整屏幕…") say("");
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 (blocked || note.classList.contains("gone") || note.textContent.startsWith("正在")) say("屏幕连接已断开");
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) return say(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 (!live) patience = setTimeout(frameless, 4000);
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
- say("正在连接电脑屏幕…");
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
- if (blocked) say("正在连接电脑屏幕…");
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
- say(live ? "" : "正在连接电脑屏幕…");
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
- say("轻点屏幕,开始显示画面");
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
- say("正在连接电脑屏幕…");
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
- <div id="note">正在连接电脑屏幕…</div>
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoHvB,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0YrC,CAAC;AAEF,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAihFtB,CAAC;AAEF,MAAM,MAAM,GAAG,WAAW,YAAY,GAAG,IAAI,OAAO,CAAC;AAErD,MAAM,UAAU,UAAU,CAAC,WAA4B,QAAQ;IAC7D,OAAO;;;;SAIA,KAAK;uBACS,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ;;;;;;;;;;iHAUiD,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"}
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
- private token;
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
@@ -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
- token = "";
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: an old viewer URL stops working.
235
- this.token = randomBytes(24).toString("base64url");
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: this.token, displays: displays.map(({ index, name }) => ({ index, name })) };
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
- if (this.server)
259
- return Promise.resolve(this.server.address().port);
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
- const expected = Buffer.from(this.token);
301
- return this.token.length > 0 && given.length === expected.length && timingSafeEqual(given, expected);
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