@windypro-rourou/dsh-logcat 0.6.0-preview.3 → 0.6.0-preview.5

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/lib/client.js CHANGED
@@ -136,8 +136,8 @@ window.__ModuleLoader__.load({
136
136
  .lc-screen { position: absolute; inset: 0; display: flex; flex-direction: column; }
137
137
  .lc-screen-bar { display: flex; gap: 6px; padding: 6px 12px; border-bottom: 1px solid light-dark(rgba(0,0,0,.08), rgba(255,255,255,.1)); flex: none; align-items: center; flex-wrap: wrap; font-size: 11px; }
138
138
  .lc-screen-bar input { background: light-dark(rgba(0,0,0,.05), rgba(255,255,255,.08)); border: 1px solid light-dark(rgba(0,0,0,.16), rgba(255,255,255,.2)); border-radius: 7px; color: inherit; font: inherit; font-size: 12px; padding: 4px 8px; }
139
- .lc-screen-img { flex: 1; min-height: 0; overflow: auto; display: flex; align-items: center; justify-content: center; cursor: crosshair; background: light-dark(rgba(0,0,0,.03), rgba(255,255,255,.03)); }
140
- .lc-screen-img img { max-width: 100%; max-height: 100%; object-fit: contain; user-select: none; }
139
+ .lc-screen-img { flex: 1; min-height: 0; overflow: auto; display: flex; align-items: center; justify-content: center; cursor: crosshair; background: light-dark(rgba(0,0,0,.03), rgba(255,255,255,.03)); position: relative; }
140
+ .lc-screen-img img, .lc-screen-img canvas { max-width: 100%; max-height: 100%; object-fit: contain; user-select: none; }
141
141
  .lc-screen-hint { color: light-dark(#6b7078, #9aa0a8); }
142
142
  `;
143
143
  //#endregion
@@ -225,6 +225,9 @@ window.__ModuleLoader__.load({
225
225
  const [screenImg, setScreenImg] = useState(null);
226
226
  const [screenText, setScreenText] = useState("");
227
227
  const [screenOn, setScreenOn] = useState(false);
228
+ const [screenFps, setScreenFps] = useState(0);
229
+ const [screenHasFrame, setScreenHasFrame] = useState(false);
230
+ const [hostScreenCapable, setHostScreenCapable] = useState(false);
228
231
  const panelOpen = useSyncExternalStore(controller.subscribe, controller.getSnapshot).panelOpen;
229
232
 
230
233
  const wsRef = useRef(null);
@@ -240,10 +243,14 @@ window.__ModuleLoader__.load({
240
243
  const fileInputRef = useRef(null);
241
244
  const screenWrapRef = useRef(null);
242
245
  const screenImgRef = useRef(null);
246
+ const screenCanvasRef = useRef(null);
243
247
  const screenDragRef = useRef(null);
244
248
  const screenWantRef = useRef(false);
245
249
  const handleBinaryRef = useRef(null);
246
250
  const prevSerialRef = useRef(serial);
251
+ const screenDecodingRef = useRef(false);
252
+ const screenPendingRef = useRef(null);
253
+ const screenFpsTsRef = useRef([]);
247
254
 
248
255
  useEffect(() => { serialRef.current = serial; }, [serial]);
249
256
  useEffect(() => { pausedRef.current = paused; }, [paused]);
@@ -404,6 +411,7 @@ window.__ModuleLoader__.load({
404
411
  setDevices(body.devices ?? []);
405
412
  setStreaming(body.streaming ?? []);
406
413
  setEventsStreaming(body.eventsStreaming ?? []);
414
+ setHostScreenCapable(body.screenCapable === true);
407
415
  setCurrentPackage(body.currentPackage ?? "");
408
416
  setCurrentVersion(body.currentVersion ?? "");
409
417
  setLatestVersion(body.latestVersion ?? "");
@@ -669,6 +677,11 @@ window.__ModuleLoader__.load({
669
677
  .catch(() => setWifiOut("网络错误"));
670
678
  };
671
679
 
680
+ // RE workbench: auto-load the process list when the tab opens.
681
+ useEffect(() => {
682
+ if (tab === "re" && serial !== "") loadProcesses();
683
+ }, [tab, serial]);
684
+
672
685
  const loadProcesses = () => {
673
686
  fetch(API_BASE + "/processes?serial=" + encodeURIComponent(serial) + "&filter=" + encodeURIComponent(reFilter))
674
687
  .then((res) => res.json())
@@ -726,6 +739,32 @@ window.__ModuleLoader__.load({
726
739
  }, [tab, serial, panelOpen]);
727
740
 
728
741
  // Binary WS frames: 4-byte JSON header length + header + PNG payload.
742
+ // Decoding goes through createImageBitmap into a canvas (no base64
743
+ // data-URL churn per frame); only the newest pending frame is kept.
744
+ const pumpScreenFrame = async () => {
745
+ if (screenDecodingRef.current) return;
746
+ screenDecodingRef.current = true;
747
+ try {
748
+ while (screenPendingRef.current !== null) {
749
+ const bytes = screenPendingRef.current;
750
+ screenPendingRef.current = null;
751
+ const canvas = screenCanvasRef.current;
752
+ if (canvas === null) break; // wait until the canvas mounts
753
+ try {
754
+ const bmp = await createImageBitmap(new Blob([bytes], { type: "image/png" }));
755
+ if (canvas.width !== bmp.width) canvas.width = bmp.width;
756
+ if (canvas.height !== bmp.height) canvas.height = bmp.height;
757
+ const c2d = canvas.getContext("2d");
758
+ c2d.drawImage(bmp, 0, 0);
759
+ bmp.close();
760
+ setScreenHasFrame(true);
761
+ } catch { /* undecodable frame — skip */ }
762
+ }
763
+ } finally {
764
+ screenDecodingRef.current = false;
765
+ }
766
+ };
767
+
729
768
  const handleBinary = (buffer) => {
730
769
  try {
731
770
  const view = new DataView(buffer);
@@ -733,12 +772,14 @@ window.__ModuleLoader__.load({
733
772
  const header = JSON.parse(new TextDecoder().decode(new Uint8Array(buffer, 4, hlen)));
734
773
  if (header.type !== "simage" || header.serial !== serialRef.current) return;
735
774
  const bytes = new Uint8Array(buffer, 4 + hlen);
736
- let binary = "";
737
- const CHUNK = 0x8000;
738
- for (let i = 0; i < bytes.length; i += CHUNK) {
739
- binary += String.fromCharCode.apply(null, bytes.subarray(i, i + CHUNK));
740
- }
741
- setScreenImg("data:image/png;base64," + btoa(binary));
775
+ // Sliding 1.5s window → rough fps readout.
776
+ const now = performance.now();
777
+ const ts = screenFpsTsRef.current;
778
+ ts.push(now);
779
+ while (ts.length > 0 && now - ts[0] > 1500) ts.shift();
780
+ setScreenFps(Math.min(ts.length, 10));
781
+ screenPendingRef.current = bytes;
782
+ void pumpScreenFrame();
742
783
  } catch { /* malformed frame */ }
743
784
  };
744
785
  handleBinaryRef.current = handleBinary;
@@ -791,13 +832,16 @@ window.__ModuleLoader__.load({
791
832
  };
792
833
 
793
834
  const downloadScreen = () => {
794
- if (screenImg === null) return;
795
- const a = document.createElement("a");
796
- a.href = screenImg;
797
- a.download = "screen-" + (serial || "device") + "-" + new Date().toISOString().replace(/[:.]/g, "-") + ".png";
798
- document.body.appendChild(a);
799
- a.click();
800
- a.remove();
835
+ const canvas = screenCanvasRef.current;
836
+ if (canvas === null || canvas.width <= 1) return;
837
+ try {
838
+ const a = document.createElement("a");
839
+ a.href = canvas.toDataURL("image/png");
840
+ a.download = "screen-" + (serial || "device") + "-" + new Date().toISOString().replace(/[:.]/g, "-") + ".png";
841
+ document.body.appendChild(a);
842
+ a.click();
843
+ a.remove();
844
+ } catch { /* empty canvas */ }
801
845
  };
802
846
 
803
847
  return h("div", { className: "lc-panel" },
@@ -955,7 +999,9 @@ window.__ModuleLoader__.load({
955
999
  h("div", { className: "lc-re-col" },
956
1000
  h("div", { className: "lc-re-head" }, "进程 (" + rePids.length + ")"),
957
1001
  h("div", { className: "lc-re-list" },
958
- rePids.map((p) =>
1002
+ rePids.length === 0
1003
+ ? h("div", { className: "lc-re-row", title: "点击「进程」或等待自动加载" }, "(尚未加载 — 点击「进程」刷新)")
1004
+ : rePids.map((p) =>
959
1005
  h("div", { key: p.pid, className: "lc-re-row", "data-on": rePid === p.pid ? "" : undefined, title: (p.user ?? "") + " " + p.name, onClick: () => { setRePid(p.pid); setReResults([]); setReDump(""); setReError(""); } },
960
1006
  p.pid + " " + p.name)))),
961
1007
  h("div", { className: "lc-re-col2" },
@@ -975,17 +1021,18 @@ window.__ModuleLoader__.load({
975
1021
  : tab === "screen"
976
1022
  ? h("div", { className: "lc-screen" },
977
1023
  h("div", { className: "lc-screen-bar" },
978
- h("span", { className: "lc-dot " + (screenOn ? "on" : "off") }),
979
- h("span", null, screenOn ? "投屏中 (~1fps)" : "投屏未开启"),
1024
+ h("span", { className: "lc-dot " + (screenOn && hostScreenCapable ? "on" : "off") }),
1025
+ h("span", null, !hostScreenCapable ? "宿主版本过旧" : (screenOn ? "投屏中 (~" + screenFps + "fps)" : "投屏未开启")),
980
1026
  h("input", { placeholder: "输入文字后回车发送…", value: screenText, onChange: (e) => setScreenText(e.target.value), onKeyDown: (e) => { if (e.key === "Enter") sendScreenText(); }, style: { flex: 1, minWidth: 140 } }),
981
1027
  h("button", { type: "button", className: "lc-btn", onClick: downloadScreen, title: "下载当前帧 PNG" }, "下载"),
982
1028
  h("span", { className: "lc-screen-hint" }, "点击=点按 · 拖动=滑动"),
983
1029
  ),
984
- h("div", { className: "lc-screen-img", ref: screenWrapRef, onMouseDown: screenMouseDown, onMouseUp: screenMouseUp, onMouseLeave: screenMouseUp },
985
- screenImg !== null
986
- ? h("img", { ref: screenImgRef, src: screenImg, alt: "设备屏幕", draggable: false })
987
- : h("div", { className: "lc-empty" }, "等待画面…(设备连接后自动开始,1fps)"),
988
- ),
1030
+ !hostScreenCapable
1031
+ ? h("div", { className: "lc-empty" }, "⚠ 运行中的 GUI 宿主版本过旧(0.6.0-preview.1)——请重启 GUI(dsh web)后刷新,投屏功能才会生效")
1032
+ : h("div", { className: "lc-screen-img", ref: screenWrapRef, onMouseDown: screenMouseDown, onMouseUp: screenMouseUp, onMouseLeave: screenMouseUp },
1033
+ h("canvas", { ref: screenCanvasRef, alt: "设备屏幕", style: { width: "1080px", height: "2408px" } }),
1034
+ !screenHasFrame ? h("div", { className: "lc-empty" }, "等待画面…(设备连接后自动开始)") : null,
1035
+ ),
989
1036
  )
990
1037
  : eventsOn
991
1038
  ? (filteredEvents.length === 0
package/lib/index.js CHANGED
@@ -687,19 +687,21 @@ export class AdbEngine {
687
687
  return picked.slice(-lines)
688
688
  }
689
689
 
690
- /** Start / stop the ~1fps screen-stream loop for a device (panel live view). */
690
+ /** Start / stop the screen-stream loop for a device (panel live view). */
691
691
  setScreen(serial, on) {
692
692
  const existing = this.screenLoops.get(serial)
693
693
  if (on) {
694
694
  if (existing === undefined && this.adb !== null && this.devices.get(serial)?.state === 'device') {
695
- const loop = { timer: null, busy: false }
695
+ const loop = { timer: null, busy: false, lastHash: null }
696
696
  this.screenLoops.set(serial, loop)
697
697
  const tick = () => {
698
698
  if (loop.busy || this.screenLoops.get(serial) !== loop) return
699
699
  loop.busy = true
700
- void this.captureScreenFrame(serial).finally(() => { loop.busy = false })
700
+ void this.captureScreenFrame(serial, loop).finally(() => { loop.busy = false })
701
701
  }
702
- loop.timer = setInterval(tick, 1000)
702
+ // 500ms cadence: screencap itself takes 300-700ms, so the busy flag
703
+ // throttles naturally; static screens are deduped by content hash.
704
+ loop.timer = setInterval(tick, 500)
703
705
  tick()
704
706
  }
705
707
  } else if (existing !== undefined) {
@@ -708,11 +710,20 @@ export class AdbEngine {
708
710
  }
709
711
  }
710
712
 
711
- /** Capture one frame and broadcast it as a binary WS frame. */
712
- async captureScreenFrame(serial) {
713
+ /** Capture one frame and broadcast it as a binary WS frame (deduped when the picture is unchanged). */
714
+ async captureScreenFrame(serial, loop) {
713
715
  try {
714
716
  const png = await runAdbBinary(this.adb, ['-s', serial, 'exec-out', 'screencap', '-p'], 8000)
715
717
  if (png.length < 100) return
718
+ // Cheap content hash (FNV-1a over the full buffer) — skip broadcasts of
719
+ // identical frames so idle screens cost nothing on the client.
720
+ let hash = 2166136261
721
+ for (let i = 0; i < png.length; i++) {
722
+ hash ^= png[i]
723
+ hash = Math.imul(hash, 16777619)
724
+ }
725
+ if (loop !== undefined && hash === loop.lastHash) return
726
+ if (loop !== undefined) loop.lastHash = hash
716
727
  this.broadcastBinary({ type: 'simage', serial, len: png.length, ts: Date.now() }, png)
717
728
  } catch { /* frame dropped */ }
718
729
  }
@@ -890,6 +901,7 @@ function makeRoutes(engine) {
890
901
  devices: engine.deviceList(),
891
902
  streaming: [...engine.streams.keys()],
892
903
  eventsStreaming: [...engine.eventsStreams.keys()],
904
+ screenCapable: true,
893
905
  currentPackage: engine.currentPackage,
894
906
  currentVersion: OWN_MANIFEST.version,
895
907
  latestVersion,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@windypro-rourou/dsh-logcat",
3
3
  "description": "Android Logcat viewer for the dsh web GUI: auto-connects to any adb device in debug mode, live logcat stream with level/keyword filters, pause/clear/export, plus agent tools (logcat_recent). Hot-pluggable — mounted via ~/.dsh/cordis.patch.yml + a profile node_modules copy, no dsh source changes.",
4
- "version": "0.6.0-preview.3",
4
+ "version": "0.6.0-preview.5",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.22.0",
7
7
  "engines": {