@linkshell/host 0.4.43 → 0.4.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.
@@ -421,6 +421,132 @@ function cleanShortcuts(list) {
421
421
  }
422
422
  return out;
423
423
  }
424
+
425
+ /** A configuration belongs to this socket; only its latest acknowledgement may restore input. */
426
+ function createScreenSession(initial, effects) {
427
+ let desired = { active: true, display: 0, width: "1920" }, hidden = false;
428
+ let capabilities = null, opened = false, closed = false, scheduled = false;
429
+ let requestId = 0, pending = null, sentKey = "", error = "", inputReady = true, generation;
430
+ const clean = (next) => {
431
+ const out = {};
432
+ if (!next || typeof next !== "object") return out;
433
+ if (typeof next.active === "boolean") out.active = next.active;
434
+ if (Number.isSafeInteger(next.display) && next.display >= 0) out.display = next.display;
435
+ if (next.width === "native") out.width = "native";
436
+ else if ((typeof next.width === "string" || typeof next.width === "number") && Number.isInteger(Number(next.width)) && Number(next.width) >= 640 && Number(next.width) <= 3840) out.width = String(Number(next.width));
437
+ if ([30, 60, 120].includes(next.maxFps)) out.maxFps = next.maxFps;
438
+ return out;
439
+ };
440
+ desired = Object.assign(desired, clean(initial));
441
+ const original = { ...desired };
442
+ const state = () => ({ display: desired.display, width: desired.width, ...(desired.maxFps ? { maxFps: desired.maxFps } : {}), paused: !desired.active || hidden });
443
+ const key = () => JSON.stringify(state());
444
+ const targetChanged = () => ["display", "width", "maxFps"].some((name) => desired[name] !== original[name]);
445
+ const changed = () => effects.changed?.({ ...desired, paused: state().paused, inputReady, error });
446
+ const suspend = () => { effects.release(); inputReady = false; };
447
+ const pump = () => {
448
+ scheduled = false;
449
+ if (closed || !opened || !capabilities) return;
450
+ const next = state(), nextKey = key();
451
+ if (capabilities.configure) {
452
+ if (nextKey === sentKey) return;
453
+ suspend();
454
+ sentKey = nextKey;
455
+ pending = { requestId: ++requestId, key: nextKey, ...next };
456
+ effects.send({ t: "screen.configure", requestId, ...next });
457
+ } else {
458
+ if (targetChanged() && !next.paused) {
459
+ if (nextKey !== sentKey) effects.reload({ display: desired.display, width: desired.width, maxFps: desired.maxFps });
460
+ inputReady = false;
461
+ } else inputReady = !next.paused;
462
+ sentKey = nextKey;
463
+ }
464
+ changed();
465
+ };
466
+ const schedule = () => {
467
+ if (!scheduled && !closed) { scheduled = true; effects.defer(pump); }
468
+ };
469
+ return {
470
+ desired: () => ({ ...desired }),
471
+ active: () => !closed && !state().paused,
472
+ canInput: () => !closed && !state().paused && inputReady,
473
+ generation: () => generation,
474
+ supported: () => capabilities?.configure === true,
475
+ known: () => capabilities !== null,
476
+ open() {
477
+ if (closed) return;
478
+ opened = true;
479
+ if (state().paused) { suspend(); effects.send({ t: "hidden" }); }
480
+ schedule();
481
+ },
482
+ update(next) {
483
+ if (closed) return;
484
+ const before = key();
485
+ desired = Object.assign({}, desired, clean(next));
486
+ if (key() === before && !error) return;
487
+ suspend();
488
+ // A pause can reach the native receiver before this microtask runs. Even a
489
+ // quick return to the same target needs a fresh apply and input generation.
490
+ pending = null;
491
+ sentKey = "";
492
+ error = "";
493
+ if (!capabilities?.configure && opened && state().paused !== JSON.parse(before).paused) effects.send({ t: state().paused ? "hidden" : "shown" });
494
+ changed();
495
+ schedule();
496
+ },
497
+ visibility(nextHidden) {
498
+ if (closed || hidden === nextHidden) return;
499
+ hidden = nextHidden;
500
+ suspend();
501
+ pending = null;
502
+ sentKey = "";
503
+ error = "";
504
+ if (!capabilities?.configure && opened) effects.send({ t: state().paused ? "hidden" : "shown" });
505
+ changed();
506
+ schedule();
507
+ },
508
+ receive(message) {
509
+ if (closed || !message || typeof message !== "object") return;
510
+ if (message.t === "capabilities") {
511
+ capabilities = { ...message, configure: message.version === 1 && message.configure === true, pause: message.version === 1 && message.pause === true };
512
+ sentKey = "";
513
+ pending = null;
514
+ if (capabilities.configure || state().paused || targetChanged()) suspend();
515
+ else inputReady = true;
516
+ effects.report(capabilities);
517
+ changed();
518
+ schedule();
519
+ return;
520
+ }
521
+ if (!pending || message.requestId !== pending.requestId || pending.key !== key()) return;
522
+ if (message.t !== "configured" && message.t !== "error") return;
523
+ if (message.t === "configured" && (message.display !== pending.display || message.paused !== pending.paused || !Number.isSafeInteger(message.generation) || message.generation < 0)) return;
524
+ inputReady = message.t === "configured" && !message.paused;
525
+ if (message.t === "configured") generation = message.generation;
526
+ error = message.t === "error" ? message.message || "屏幕配置失败,请重试" : "";
527
+ pending = null;
528
+ if (error) sentKey = "";
529
+ effects.report(message);
530
+ changed();
531
+ },
532
+ close() {
533
+ if (closed) return;
534
+ suspend();
535
+ closed = true;
536
+ pending = null;
537
+ changed();
538
+ },
539
+ reconnect() {
540
+ if (closed) return;
541
+ suspend();
542
+ capabilities = pending = null;
543
+ generation = undefined;
544
+ sentKey = "";
545
+ error = "";
546
+ changed();
547
+ },
548
+ };
549
+ }
424
550
  `;
425
551
  const BODY = String.raw `
426
552
  const $ = (id) => document.getElementById(id);
@@ -430,6 +556,11 @@ const sheet = $("sheet"), actions = $("actions"), maker = $("maker"), naming = $
430
556
  const query = new URLSearchParams(location.search);
431
557
  const app = window.ReactNativeWebView;
432
558
  const tellApp = (message) => { if (app) app.postMessage(JSON.stringify(message)); };
559
+ const tellSession = (session) => {
560
+ const message = { type: "screenSession", session };
561
+ tellApp(message);
562
+ if (typeof window.__linkshellSession === "function") window.__linkshellSession(message);
563
+ };
433
564
  const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
434
565
  const coarse = matchMedia("(pointer: coarse)").matches;
435
566
  const android = /Android/i.test(navigator.userAgent);
@@ -442,7 +573,7 @@ const HINTS = {
442
573
  };
443
574
  let stored = null;
444
575
  try { stored = localStorage.getItem("linkshell.screen.mode"); } catch {}
445
- let mode = MODES.includes(query.get("mode")) ? query.get("mode") : MODES.includes(stored) ? stored : "view";
576
+ let mode = MODES.includes(query.get("mode")) ? query.get("mode") : MODES.includes(window.__linkshellChrome?.mode) ? window.__linkshellChrome.mode : MODES.includes(stored) ? stored : "view";
446
577
 
447
578
  // What the app around the page has done to the phone's screen. A page in a plain browser has none of it.
448
579
  // "clear" is the side of a phone lying down that has no camera in it.
@@ -464,7 +595,8 @@ const mods = new Set();
464
595
 
465
596
  // ---- The picture ----------------------------------------------------------
466
597
 
467
- const say = (text) => { note.textContent = text; note.classList.toggle("gone", !text); };
598
+ let sessionError = "";
599
+ const say = (text) => { text = sessionError || text; note.textContent = text; note.classList.toggle("gone", !text); };
468
600
  const round = (value, digits) => (value == null || !isFinite(value) ? null : +value.toFixed(digits === undefined ? 1 : digits));
469
601
  const hex = (n) => n.toString(16).padStart(2, "0");
470
602
  function sps(unit) {
@@ -499,12 +631,35 @@ const asked = new URLSearchParams(location.search);
499
631
  asked.delete("video");
500
632
  if (wantVideo) asked.set("video", "1");
501
633
  // How wide the video track may be (the socket's picture has its own ladder). The app says it in the address;
502
- // a plain browser remembers its own. A new choice loads the page again with it: the track is offered anew.
634
+ // a plain browser remembers its own. A capable host applies a new choice to this same track.
503
635
  const WIDTHS = ["1280", "1920", "2560", "native"];
504
636
  let keptWidth = null;
505
637
  try { keptWidth = localStorage.getItem("linkshell.screen.width"); } catch {}
506
- const width = WIDTHS.includes(query.get("width")) ? query.get("width") : WIDTHS.includes(keptWidth) ? keptWidth : "1920";
638
+ let width = WIDTHS.includes(query.get("width")) ? query.get("width") : WIDTHS.includes(keptWidth) ? keptWidth : "1920";
639
+ const initialSession = typeof window.__linkshellSession === "object" && window.__linkshellSession ? window.__linkshellSession : {};
640
+ const session = createScreenSession({ active: true, display: Number(query.get("display")) || 0, width, maxFps: Number(query.get("maxFps")) || undefined, ...initialSession }, {
641
+ send: (message) => send(message),
642
+ defer: (action) => Promise.resolve().then(action),
643
+ release: () => releaseInput(),
644
+ report: tellSession,
645
+ reload: (state) => reloadSession(state),
646
+ changed: (state) => {
647
+ width = state.width;
648
+ const previousError = sessionError;
649
+ sessionError = state.error;
650
+ if (sessionError) say(sessionError);
651
+ else if (state.paused) say("屏幕已暂停");
652
+ else if (!state.inputReady) say("正在调整屏幕…");
653
+ else if (previousError || note.textContent === "屏幕已暂停" || note.textContent === "正在调整屏幕…") say("");
654
+ if (state.inputReady && mode !== "view") askControl();
655
+ refreshWidths();
656
+ refresh();
657
+ },
658
+ });
659
+ width = session.desired().width;
507
660
  asked.set("width", width);
661
+ asked.set("display", String(session.desired().display));
662
+ if (session.desired().maxFps) asked.set("maxFps", String(session.desired().maxFps));
508
663
  // A page that is a srcdoc (the gateway's web client puts it in one) has no address of its own to load again:
509
664
  // it asks the page around it to, where that page says it can, and offers no choice where it can't.
510
665
  const framed = document.URL === "about:srcdoc";
@@ -515,10 +670,34 @@ const ws = nativePicture
515
670
  : new WebSocket((location.protocol === "https:" ? "wss://" : "ws://") + location.host + "/stream?" + asked);
516
671
  ws.binaryType = "arraybuffer";
517
672
  const send = (message) => { if (ws.readyState === 1) ws.send(JSON.stringify(message)); };
518
- ws.onopen = () => { if (mode !== "view") askControl(); if (document.hidden) send({ t: "hidden" }); };
673
+ let capabilityTimer = 0, socketClosed = false;
674
+ function waitForCapabilities() {
675
+ clearTimeout(capabilityTimer);
676
+ capabilityTimer = setTimeout(() => {
677
+ if (!session.known()) session.receive({ t: "capabilities", version: 1, configure: false, pause: false, retainMs: 0 });
678
+ }, 10000);
679
+ }
680
+ ws.onopen = () => { session.open(); waitForCapabilities(); if (mode !== "view") askControl(); };
519
681
  // Out of sight, a page answers late (its timers slow to one a second) and shows nothing: the host sends no
520
682
  // picture down the socket meanwhile, and starts again at a keyframe.
521
- document.addEventListener("visibilitychange", () => send({ t: document.hidden ? "hidden" : "shown" }));
683
+ document.addEventListener("visibilitychange", () => session.visibility(document.hidden));
684
+ window.linkshellSession = (state) => session.update(state);
685
+
686
+ function reloadSession(state) {
687
+ if (nativePicture) return;
688
+ if (reloadVia) return reloadVia({ ...state, mode });
689
+ const next = new URLSearchParams(location.search);
690
+ for (const name of ["display", "width", "maxFps"]) if (state[name] !== undefined) next.set(name, String(state[name]));
691
+ next.set("mode", mode);
692
+ location.replace(location.pathname + "?" + next);
693
+ }
694
+
695
+ function refreshWidths() {
696
+ for (const tile of $("widths").querySelectorAll(".tile")) {
697
+ tile.classList.toggle("on", tile.dataset.width === width);
698
+ tile.setAttribute("aria-pressed", String(tile.dataset.width === width));
699
+ }
700
+ }
522
701
  ws.onmessage = (event) => {
523
702
  if (typeof event.data === "string") return heard(JSON.parse(event.data));
524
703
  if (!("VideoDecoder" in window)) return;
@@ -559,14 +738,23 @@ ws.onmessage = (event) => {
559
738
  decoder.decode(new EncodedVideoChunk({ type: key ? "key" : "delta", timestamp: seq, data: unit }));
560
739
  };
561
740
  ws.onclose = () => {
741
+ if (socketClosed) return;
742
+ socketClosed = true;
743
+ clearTimeout(capabilityTimer);
744
+ session.close();
562
745
  // A video track is this socket's: without the one there is no finding the other again, and no hands.
563
746
  drop();
564
747
  if (requestRelay()) return;
748
+ tellApp({ type: "screenClosed" });
565
749
  if (blocked || note.classList.contains("gone") || note.textContent.startsWith("正在")) say("屏幕连接已断开");
566
750
  blocked = false;
567
751
  };
568
752
 
569
753
  function heard(message) {
754
+ if (message.session) {
755
+ if (message.session.t === "capabilities") clearTimeout(capabilityTimer);
756
+ return session.receive(message.session);
757
+ }
570
758
  if (message.error) return say(message.error);
571
759
  if (message.rtc) return signal(message.rtc);
572
760
  if (message.pong) return meter && meter.pong(message.pong);
@@ -635,7 +823,7 @@ function signal(message) {
635
823
  if (!mine) return;
636
824
  if (message.t === "offer") {
637
825
  tried = true;
638
- patience = setTimeout(() => giveUp("not connected within " + patienceSeconds + " s of the offer"), patienceSeconds * 1000);
826
+ patience = setTimeout(() => connectionPatience(mine), patienceSeconds * 1000);
639
827
  if (failAfter) setTimeout(() => { if (rtc === mine) giveUp("asked to (failAfter)"); }, failAfter * 1000);
640
828
  // The answer is the browser's own, as it made it: the sender's playout-delay header has to survive in it.
641
829
  mine.described = mine.pc.setRemoteDescription({ type: "offer", sdp: message.sdp })
@@ -702,16 +890,23 @@ function watch(mine) {
702
890
  }
703
891
 
704
892
  function away(mine, since) {
893
+ if (rtc !== mine) return;
705
894
  // A page that was put aside (another app, another tab) could not hear the path come back: once it is
706
895
  // looked at again it is given the time again.
707
896
  const now = performance.now();
708
- if (document.hidden || now - since > 6000) mine.away = setTimeout(() => away(mine, now), 4000);
897
+ if (!session.active() || now - since > 6000) mine.away = setTimeout(() => away(mine, now), 4000);
709
898
  else giveUp("disconnected for more than 4 s");
710
899
  }
711
900
 
901
+ function connectionPatience(mine) {
902
+ if (rtc !== mine) return;
903
+ if (!session.active()) patience = setTimeout(() => connectionPatience(mine), patienceSeconds * 1000);
904
+ else giveUp("not connected within " + patienceSeconds + " s of the offer");
905
+ }
906
+
712
907
  function frameless() {
713
908
  // A page nobody is looking at is shown nothing, and one waiting for a tap has not begun: neither is the path's doing.
714
- if (document.hidden || blocked) patience = setTimeout(frameless, 4000);
909
+ if (!session.active() || blocked) patience = setTimeout(frameless, 4000);
715
910
  else giveUp("connected, but no frame within 4 s");
716
911
  }
717
912
 
@@ -859,6 +1054,7 @@ async function presentTrack(track) {
859
1054
  if (mine.stopped || !mine.latest) return;
860
1055
  const frame = mine.latest;
861
1056
  mine.latest = null;
1057
+ if (!session.active()) { frame.close(); return; }
862
1058
  try {
863
1059
  // The canvas carries the frame's own pixels; the layout gives it its place on the page.
864
1060
  if (gpu.width !== frame.displayWidth || gpu.height !== frame.displayHeight) {
@@ -914,6 +1110,7 @@ async function presentTrack(track) {
914
1110
  if (value) value.close();
915
1111
  break;
916
1112
  }
1113
+ if (!session.active()) { value.close(); continue; }
917
1114
  if (mine.latest) {
918
1115
  mine.latest.close();
919
1116
  mine.dropped += 1;
@@ -1270,7 +1467,7 @@ function expandControls(open) {
1270
1467
  }
1271
1468
  function armControlsIdle() {
1272
1469
  clearTimeout(controls.timer);
1273
- if (!controlsMayCollapse() || controls.target === 0) return;
1470
+ if (!session.active() || !controlsMayCollapse() || controls.target === 0) return;
1274
1471
  controls.timer = setTimeout(() => { if (controlsMayCollapse()) expandControls(false); }, 3000);
1275
1472
  }
1276
1473
  function layoutControls(safe, anchor) {
@@ -1310,7 +1507,7 @@ reducedMotion.addEventListener("change", () => {
1310
1507
  });
1311
1508
  orb.addEventListener("pointerdown", (event) => {
1312
1509
  event.preventDefault(); event.stopPropagation();
1313
- if (!controls.orb) return;
1510
+ if (!session.active() || !controls.orb) return;
1314
1511
  try { orb.setPointerCapture(event.pointerId); } catch {}
1315
1512
  clearTimeout(controls.timer);
1316
1513
  controls.drag = { id: event.pointerId, x: event.clientX, y: event.clientY, origin: { ...controls.orb }, moved: false };
@@ -1542,12 +1739,12 @@ function panBy(dx, dy) {
1542
1739
 
1543
1740
  // ---- Sending --------------------------------------------------------------
1544
1741
 
1545
- const controlling = () => mode !== "view" && control.available && control.trusted;
1742
+ const controlling = () => session.canInput() && mode !== "view" && control.available && control.trusted;
1546
1743
 
1547
1744
  function askControl() {
1548
- if (control.asked) return;
1745
+ if (control.asked || !session.canInput()) return;
1549
1746
  control.asked = true;
1550
- send({ t: "control" });
1747
+ send({ t: "control", ...(session.generation() !== undefined ? { screenGeneration: session.generation() } : {}) });
1551
1748
  }
1552
1749
 
1553
1750
  // Where an event travels: down the socket, in order, until a video's two channels to the app are open; then
@@ -1562,6 +1759,8 @@ function askControl() {
1562
1759
  // after something sent later.
1563
1760
  let wired = false, held = 0, sure = true, sent = 0, rest = 0, movedAt = -Infinity;
1564
1761
  const trail = [];
1762
+ const heldButtons = new Map();
1763
+ let inputEpoch = 0;
1565
1764
 
1566
1765
  function lanes() {
1567
1766
  if (nativePicture) { wired = true; return; }
@@ -1572,15 +1771,23 @@ function lanes() {
1572
1771
  }
1573
1772
 
1574
1773
  function post(message, lane) {
1774
+ if (!session.canInput()) return;
1775
+ if (session.generation() !== undefined) message.screenGeneration = session.generation();
1776
+ transmitInput(message, lane);
1777
+ }
1778
+
1779
+ function transmitInput(message, lane) {
1575
1780
  if (nativePicture) return nativePost({ kind: "input", message, replaceable: lane === "pointer" });
1576
1781
  if (!wired) return send(message);
1577
1782
  message.i = ++sent;
1578
- rtc.channels[lane].send(JSON.stringify(message));
1783
+ const channel = rtc && rtc.channels[lane];
1784
+ if (channel && channel.readyState === "open") channel.send(JSON.stringify(message));
1579
1785
  }
1580
1786
 
1581
1787
  let moveWaiting = false;
1582
1788
  /** Says where the pointer is: in order, when something is about to happen there. */
1583
1789
  function tell(placed) {
1790
+ if (!session.canInput()) { moveWaiting = false; return; }
1584
1791
  lanes();
1585
1792
  moveWaiting = false;
1586
1793
  const now = performance.now(), lane = laneOf("move", placed, held > 0, false);
@@ -1597,6 +1804,7 @@ function tell(placed) {
1597
1804
  const flush = () => { if (moveWaiting) tell(false); };
1598
1805
  // A pointer position a frame: as often as the picture could show it.
1599
1806
  function moved() {
1807
+ if (!session.canInput()) return;
1600
1808
  if (!moveWaiting) requestAnimationFrame(flush);
1601
1809
  moveWaiting = true;
1602
1810
  movedAt = performance.now();
@@ -1605,13 +1813,64 @@ function moved() {
1605
1813
  }
1606
1814
  // Everything else goes at once, after the position it happens at.
1607
1815
  function act(message) {
1816
+ if (!session.canInput()) return;
1608
1817
  lanes();
1609
1818
  const placed = moveWaiting || !sure;
1610
1819
  if (placed) tell(true);
1611
- if (message.t === "down") held += 1;
1820
+ if (message.t === "down") { heldButtons.set(message.b, { ...message }); held = heldButtons.size; }
1612
1821
  if (message.t === "scroll" && placed && gesture) gesture.tied = true;
1613
1822
  post(message, laneOf(message.t, placed, held > 0, !!gesture && !!gesture.tied));
1614
- if (message.t === "up") held = Math.max(0, held - 1);
1823
+ if (message.t === "up") { heldButtons.delete(message.b); held = heldButtons.size; }
1824
+ }
1825
+
1826
+ /** Input queued for the old display must never be replayed when the retained page is shown again. */
1827
+ function releaseInput() {
1828
+ inputEpoch += 1;
1829
+ control.asked = false;
1830
+ lanes();
1831
+ for (const button of heldButtons.values()) {
1832
+ const released = { ...button, t: "up" };
1833
+ if (session.generation() !== undefined) released.screenGeneration = session.generation();
1834
+ transmitInput(released, "input");
1835
+ }
1836
+ heldButtons.clear();
1837
+ held = 0;
1838
+ sure = true;
1839
+ moveWaiting = false;
1840
+ clearTimeout(rest);
1841
+ clearTimeout(fade);
1842
+ clearTimeout(settling);
1843
+ clearTimeout(toastTimer);
1844
+ clearTimeout(disarming);
1845
+ clearInterval(statusTimer);
1846
+ clearTimeout(controls.timer);
1847
+ if (gesture) clearTimeout(gesture.hold);
1848
+ for (const id of touches.keys()) { try { stage.releasePointerCapture(id); } catch {} }
1849
+ touches.clear();
1850
+ gesture = lastTap = mouseHeld = null;
1851
+ mouseClicks = { t: 0, n: 0, x: 0, y: 0, b: 0 };
1852
+ movedAt = -Infinity;
1853
+ trail.length = 0;
1854
+ mods.clear();
1855
+ composing = writing = false;
1856
+ composed = "";
1857
+ leftover = wanted = null;
1858
+ askedAt = -Infinity;
1859
+ keysOpen = false;
1860
+ if (document.activeElement && document.activeElement !== document.body) document.activeElement.blur();
1861
+ resetTyping();
1862
+ shortcut.value = "";
1863
+ disarm();
1864
+ pointer.classList.remove("pressed", "on");
1865
+ toast.classList.remove("on");
1866
+ if (controls.drag) { try { orb.releasePointerCapture(controls.drag.id); } catch {} }
1867
+ controls.drag = null;
1868
+ for (const element of pressedControls) element.style.scale = "";
1869
+ pressedControls.clear();
1870
+ finishPresentation();
1871
+ drawKeys();
1872
+ clearTimeout(controls.timer);
1873
+ clearTimeout(settling);
1615
1874
  }
1616
1875
 
1617
1876
  let fade = 0;
@@ -1687,7 +1946,7 @@ function endOne() {
1687
1946
  }
1688
1947
 
1689
1948
  stage.addEventListener("pointerdown", (event) => {
1690
- if (presentation.active) { event.preventDefault(); return; }
1949
+ if (!session.active() || presentation.active) { event.preventDefault(); return; }
1691
1950
  if (!immersiveControls() || controls.target > 0) wake();
1692
1951
  // A touch that closes the menu, or the shortcuts, does nothing else.
1693
1952
  const closing = [closeMenu(), closeSheet()].some(Boolean);
@@ -1720,7 +1979,7 @@ stage.addEventListener("pointerdown", (event) => {
1720
1979
  });
1721
1980
 
1722
1981
  stage.addEventListener("pointermove", (event) => {
1723
- if (presentation.active) return;
1982
+ if (!session.active() || presentation.active) return;
1724
1983
  if (event.pointerType === "mouse") return mouseMove(event);
1725
1984
  const touch = touches.get(event.pointerId);
1726
1985
  if (!touch || !gesture) return;
@@ -1852,7 +2111,7 @@ function mouseUp(event) {
1852
2111
  }
1853
2112
  stage.addEventListener("wheel", (event) => {
1854
2113
  event.preventDefault();
1855
- if (presentation.active) return;
2114
+ if (!session.active() || presentation.active) return;
1856
2115
  if (controlling() && !event.ctrlKey) return act({ t: "scroll", dx: -event.deltaX, dy: -event.deltaY });
1857
2116
  zoomTo(zoom * Math.exp(-event.deltaY / 300), { x: event.clientX, y: event.clientY });
1858
2117
  }, { passive: false });
@@ -1990,6 +2249,7 @@ const keyboardShown = () => keyboardIsUp({
1990
2249
 
1991
2250
  /** Brings the phone's keyboard up for a field. Within a touch: a browser gives a keyboard to nothing else. */
1992
2251
  function ask(field, again) {
2252
+ if (!session.canInput()) return;
1993
2253
  // A keyboard goes to the view the keys go to, and inside an app that is not always the page: the app
1994
2254
  // sees to it, and has the page ask once more (linkshellKeyboard) if no keyboard has come.
1995
2255
  if (!again) tellApp({ type: "keyboard" });
@@ -2055,7 +2315,8 @@ document.addEventListener("visibilitychange", () => { if (!document.hidden) sett
2055
2315
 
2056
2316
  /** A control that works on touch-up and never takes the focus (the keyboard stays as it is). */
2057
2317
  function tappable(element, action) {
2058
- element.addEventListener("pointerdown", (event) => { event.preventDefault(); event.stopPropagation(); wake(); pressFeedback(element, true); });
2318
+ let epoch = -1;
2319
+ element.addEventListener("pointerdown", (event) => { event.preventDefault(); event.stopPropagation(); if (!session.active()) return; epoch = inputEpoch; wake(); pressFeedback(element, true); });
2059
2320
  element.addEventListener("pointercancel", () => pressFeedback(element, false));
2060
2321
  element.addEventListener("pointerleave", () => pressFeedback(element, false));
2061
2322
  element.addEventListener("touchstart", (event) => event.preventDefault(), { passive: false });
@@ -2064,6 +2325,8 @@ function tappable(element, action) {
2064
2325
  event.preventDefault();
2065
2326
  event.stopPropagation();
2066
2327
  pressFeedback(element, false);
2328
+ if (!session.active() || epoch !== inputEpoch) return;
2329
+ epoch = -1;
2067
2330
  const box = element.getBoundingClientRect(), cx = box.left + box.width / 2, cy = box.top + box.height / 2;
2068
2331
  if (Math.abs(event.clientX - cx) <= element.offsetWidth / 2 && Math.abs(event.clientY - cy) <= element.offsetHeight / 2) action();
2069
2332
  });
@@ -2072,7 +2335,7 @@ function tappable(element, action) {
2072
2335
  /** The same inside something that scrolls: the touch is left to the browser, and counts if it stayed where it landed. */
2073
2336
  function pressable(element, action) {
2074
2337
  let from = null;
2075
- element.addEventListener("pointerdown", (event) => { event.stopPropagation(); wake(); pressFeedback(element, true); from = { id: event.pointerId, x: event.clientX, y: event.clientY }; });
2338
+ element.addEventListener("pointerdown", (event) => { event.stopPropagation(); if (!session.active()) return; wake(); pressFeedback(element, true); from = { id: event.pointerId, x: event.clientX, y: event.clientY, epoch: inputEpoch }; });
2076
2339
  element.addEventListener("pointercancel", () => { from = null; pressFeedback(element, false); });
2077
2340
  element.addEventListener("pointerleave", () => pressFeedback(element, false));
2078
2341
  // The mouse's events a browser makes up after a touch would land on whatever the action has left under the
@@ -2084,7 +2347,7 @@ function pressable(element, action) {
2084
2347
  from = null;
2085
2348
  pressFeedback(element, false);
2086
2349
  event.stopPropagation();
2087
- if (was && was.id === event.pointerId && Math.hypot(event.clientX - was.x, event.clientY - was.y) < SLOP) action();
2350
+ if (session.active() && was && was.epoch === inputEpoch && was.id === event.pointerId && Math.hypot(event.clientX - was.x, event.clientY - was.y) < SLOP) action();
2088
2351
  });
2089
2352
  }
2090
2353
 
@@ -2103,6 +2366,8 @@ function hint(text, ms) {
2103
2366
 
2104
2367
  /** Says why a controlling mode is not controlling, or how it works once it is. */
2105
2368
  function explain(was) {
2369
+ if (!session.active()) return;
2370
+ if (!session.canInput()) return hint("正在调整屏幕…", 0);
2106
2371
  if (!control.known) return hint("正在准备控制电脑…", 0);
2107
2372
  if (!control.available) return hint(control.reason || "这台电脑暂时不能被控制");
2108
2373
  if (!control.trusted) {
@@ -2237,7 +2502,7 @@ async function status() {
2237
2502
  layout();
2238
2503
  };
2239
2504
  // The choice is the track's: the socket's picture follows its own ladder.
2240
- const choosing = live && canChooseWidth;
2505
+ const choosing = live && (canChooseWidth || session.supported());
2241
2506
  if ($("widths").classList.contains("gone") === choosing) {
2242
2507
  $("widths").classList.toggle("gone", !choosing);
2243
2508
  layout();
@@ -2294,12 +2559,7 @@ for (const tile of $("widths").querySelectorAll(".tile")) {
2294
2559
  if (chosen === width) return;
2295
2560
  try { localStorage.setItem("linkshell.screen.width", chosen); } catch {}
2296
2561
  tellApp({ type: "width", width: chosen });
2297
- if (nativePicture) return;
2298
- if (reloadVia) return reloadVia({ width: chosen, mode });
2299
- const next = new URLSearchParams(location.search);
2300
- next.set("width", chosen);
2301
- next.set("mode", mode);
2302
- location.replace(location.pathname + "?" + next);
2562
+ window.linkshellSession({ width: chosen });
2303
2563
  });
2304
2564
  }
2305
2565
  pressable($("full"), () => {
@@ -2761,6 +3021,7 @@ if (!nativePicture && query.get("measure")) meter = (() => {
2761
3021
  resetTyping();
2762
3022
  refresh();
2763
3023
  wake();
3024
+ session.visibility(document.hidden);
2764
3025
  if (nativePicture) {
2765
3026
  window.linkshellNative = {
2766
3027
  picture: (size) => {
@@ -2776,6 +3037,8 @@ if (nativePicture) {
2776
3037
  stats: (stats) => { nativeStats = stats; },
2777
3038
  connecting: () => {
2778
3039
  nativeStats = null;
3040
+ session.reconnect();
3041
+ waitForCapabilities();
2779
3042
  Object.assign(control, { asked: false, known: false, trusted: false });
2780
3043
  say("正在连接电脑屏幕…");
2781
3044
  if (mode !== "view") askControl();
@@ -1 +1 @@
1
- {"version":3,"file":"screen-viewer.js","sourceRoot":"","sources":["../../../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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4QrC,CAAC;AAEF,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAo0EtB,CAAC;AAEF,MAAM,MAAM,GAAG,WAAW,YAAY,GAAG,IAAI,OAAO,CAAC;AAErD,MAAM,UAAU,UAAU;IACxB,OAAO;;;;SAIA,KAAK;;;;;;;;;;;iHAWmG,KAAK,CAAC,KAAK;;;;;;;;;;;;;;;kEAe1D,KAAK,CAAC,QAAQ;iEACf,KAAK,CAAC,KAAK;+DACb,KAAK,CAAC,GAAG;gEACR,KAAK,CAAC,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6CAmD7B,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;UACxD,MAAM;eACD,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"screen-viewer.js","sourceRoot":"","sources":["../../../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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA68EtB,CAAC;AAEF,MAAM,MAAM,GAAG,WAAW,YAAY,GAAG,IAAI,OAAO,CAAC;AAErD,MAAM,UAAU,UAAU;IACxB,OAAO;;;;SAIA,KAAK;;;;;;;;;;;iHAWmG,KAAK,CAAC,KAAK;;;;;;;;;;;;;;;kEAe1D,KAAK,CAAC,QAAQ;iEACf,KAAK,CAAC,KAAK;+DACb,KAAK,CAAC,GAAG;gEACR,KAAK,CAAC,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6CAmD7B,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;UACxD,MAAM;eACD,CAAC;AAChB,CAAC"}
@@ -1,4 +1,5 @@
1
1
  import { type ChildProcess } from "node:child_process";
2
+ import { type StreamProfile } from "./input.js";
2
3
  export interface Display {
3
4
  index: number;
4
5
  name: string;
@@ -12,7 +13,7 @@ export interface ScreenAccess {
12
13
  problem?: string;
13
14
  }
14
15
  /** ffmpeg's capture of an X display (Linux), for one rung of the ladder (see `screen-pacer.ts`): 0 is the best picture. */
15
- export declare function captureArgs(level?: number): string[];
16
+ export declare function captureArgs(level?: number, profile?: StreamProfile): string[];
16
17
  /** Splits an Annex-B stream at access-unit delimiters (NAL type 9) into whole frames. */
17
18
  export declare class AccessUnitSplitter {
18
19
  private buffer;
@@ -68,6 +69,7 @@ export declare class ScreenShare {
68
69
  /** The viewer the app is sending the screen to as a video track. One at a time, like captures. */
69
70
  private video?;
70
71
  private readonly controls;
72
+ private readonly viewers;
71
73
  constructor(log: (message: string) => void,
72
74
  /** The servers both ends ask for their public address, to reach each other directly. */
73
75
  iceServers?: () => string[]);