@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 +63 -20
- package/lib/index.js +17 -6
- package/package.json +1 -1
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
|
-
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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.
|
|
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 ? "投屏中 (~
|
|
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
|
-
|
|
990
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
"version": "0.6.0-preview.5",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.22.0",
|
|
7
7
|
"engines": {
|