@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 +70 -23
- package/lib/index.js +18 -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,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
|
-
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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.
|
|
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 ? "投屏中 (~
|
|
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
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
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
|
|
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
|
}
|
|
@@ -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.
|
|
4
|
+
"version": "0.6.0-preview.5",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.22.0",
|
|
7
7
|
"engines": {
|