@windypro-rourou/dsh-logcat 0.6.0-preview.4 → 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,8 @@ 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);
228
230
  const [hostScreenCapable, setHostScreenCapable] = useState(false);
229
231
  const panelOpen = useSyncExternalStore(controller.subscribe, controller.getSnapshot).panelOpen;
230
232
 
@@ -241,10 +243,14 @@ window.__ModuleLoader__.load({
241
243
  const fileInputRef = useRef(null);
242
244
  const screenWrapRef = useRef(null);
243
245
  const screenImgRef = useRef(null);
246
+ const screenCanvasRef = useRef(null);
244
247
  const screenDragRef = useRef(null);
245
248
  const screenWantRef = useRef(false);
246
249
  const handleBinaryRef = useRef(null);
247
250
  const prevSerialRef = useRef(serial);
251
+ const screenDecodingRef = useRef(false);
252
+ const screenPendingRef = useRef(null);
253
+ const screenFpsTsRef = useRef([]);
248
254
 
249
255
  useEffect(() => { serialRef.current = serial; }, [serial]);
250
256
  useEffect(() => { pausedRef.current = paused; }, [paused]);
@@ -671,6 +677,11 @@ window.__ModuleLoader__.load({
671
677
  .catch(() => setWifiOut("网络错误"));
672
678
  };
673
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
+
674
685
  const loadProcesses = () => {
675
686
  fetch(API_BASE + "/processes?serial=" + encodeURIComponent(serial) + "&filter=" + encodeURIComponent(reFilter))
676
687
  .then((res) => res.json())
@@ -728,6 +739,32 @@ window.__ModuleLoader__.load({
728
739
  }, [tab, serial, panelOpen]);
729
740
 
730
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
+
731
768
  const handleBinary = (buffer) => {
732
769
  try {
733
770
  const view = new DataView(buffer);
@@ -735,12 +772,14 @@ window.__ModuleLoader__.load({
735
772
  const header = JSON.parse(new TextDecoder().decode(new Uint8Array(buffer, 4, hlen)));
736
773
  if (header.type !== "simage" || header.serial !== serialRef.current) return;
737
774
  const bytes = new Uint8Array(buffer, 4 + hlen);
738
- let binary = "";
739
- const CHUNK = 0x8000;
740
- for (let i = 0; i < bytes.length; i += CHUNK) {
741
- binary += String.fromCharCode.apply(null, bytes.subarray(i, i + CHUNK));
742
- }
743
- 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();
744
783
  } catch { /* malformed frame */ }
745
784
  };
746
785
  handleBinaryRef.current = handleBinary;
@@ -793,13 +832,16 @@ window.__ModuleLoader__.load({
793
832
  };
794
833
 
795
834
  const downloadScreen = () => {
796
- if (screenImg === null) return;
797
- const a = document.createElement("a");
798
- a.href = screenImg;
799
- a.download = "screen-" + (serial || "device") + "-" + new Date().toISOString().replace(/[:.]/g, "-") + ".png";
800
- document.body.appendChild(a);
801
- a.click();
802
- 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 */ }
803
845
  };
804
846
 
805
847
  return h("div", { className: "lc-panel" },
@@ -957,7 +999,9 @@ window.__ModuleLoader__.load({
957
999
  h("div", { className: "lc-re-col" },
958
1000
  h("div", { className: "lc-re-head" }, "进程 (" + rePids.length + ")"),
959
1001
  h("div", { className: "lc-re-list" },
960
- rePids.map((p) =>
1002
+ rePids.length === 0
1003
+ ? h("div", { className: "lc-re-row", title: "点击「进程」或等待自动加载" }, "(尚未加载 — 点击「进程」刷新)")
1004
+ : rePids.map((p) =>
961
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(""); } },
962
1006
  p.pid + " " + p.name)))),
963
1007
  h("div", { className: "lc-re-col2" },
@@ -978,7 +1022,7 @@ window.__ModuleLoader__.load({
978
1022
  ? h("div", { className: "lc-screen" },
979
1023
  h("div", { className: "lc-screen-bar" },
980
1024
  h("span", { className: "lc-dot " + (screenOn && hostScreenCapable ? "on" : "off") }),
981
- h("span", null, !hostScreenCapable ? "宿主版本过旧" : (screenOn ? "投屏中 (~1fps)" : "投屏未开启")),
1025
+ h("span", null, !hostScreenCapable ? "宿主版本过旧" : (screenOn ? "投屏中 (~" + screenFps + "fps)" : "投屏未开启")),
982
1026
  h("input", { placeholder: "输入文字后回车发送…", value: screenText, onChange: (e) => setScreenText(e.target.value), onKeyDown: (e) => { if (e.key === "Enter") sendScreenText(); }, style: { flex: 1, minWidth: 140 } }),
983
1027
  h("button", { type: "button", className: "lc-btn", onClick: downloadScreen, title: "下载当前帧 PNG" }, "下载"),
984
1028
  h("span", { className: "lc-screen-hint" }, "点击=点按 · 拖动=滑动"),
@@ -986,9 +1030,8 @@ window.__ModuleLoader__.load({
986
1030
  !hostScreenCapable
987
1031
  ? h("div", { className: "lc-empty" }, "⚠ 运行中的 GUI 宿主版本过旧(0.6.0-preview.1)——请重启 GUI(dsh web)后刷新,投屏功能才会生效")
988
1032
  : h("div", { className: "lc-screen-img", ref: screenWrapRef, onMouseDown: screenMouseDown, onMouseUp: screenMouseUp, onMouseLeave: screenMouseUp },
989
- screenImg !== null
990
- ? h("img", { ref: screenImgRef, src: screenImg, alt: "设备屏幕", draggable: false })
991
- : h("div", { className: "lc-empty" }, "等待画面…(设备连接后自动开始,1fps)"),
1033
+ h("canvas", { ref: screenCanvasRef, alt: "设备屏幕", style: { width: "1080px", height: "2408px" } }),
1034
+ !screenHasFrame ? h("div", { className: "lc-empty" }, "等待画面…(设备连接后自动开始)") : null,
992
1035
  ),
993
1036
  )
994
1037
  : eventsOn
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
  }
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.4",
4
+ "version": "0.6.0-preview.5",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.22.0",
7
7
  "engines": {