ss
This commit is contained in:
@@ -39,6 +39,8 @@ export type DeviceRow = {
|
||||
type TFn = (zh: string, en: string) => string;
|
||||
|
||||
const BOOKMARKS_KEY = "live-hub-adb-bookmarks-v1";
|
||||
const SCREENSHOT_FALLBACK_INTERVAL_MS = 450;
|
||||
const SCRCPY_FIRST_FRAME_TIMEOUT_MS = 5200;
|
||||
|
||||
function clientPointToCanvasPixel(
|
||||
canvas: HTMLCanvasElement,
|
||||
@@ -195,6 +197,9 @@ export function LiveAndroidStreamConsole({
|
||||
const decoderRef = useRef<ScrcpyH264Decoder | null>(null);
|
||||
const mirrorFocusRef = useRef<HTMLDivElement | null>(null);
|
||||
const shotFailRef = useRef(0);
|
||||
const scrcpyExpectedCloseRef = useRef(false);
|
||||
const scrcpyFirstFrameTimerRef = useRef<number | null>(null);
|
||||
const scrcpyGotFrameRef = useRef(false);
|
||||
|
||||
const activeTransport = devices.find((d) => d.serial === serial)?.transport;
|
||||
const useFastShot = liveMs > 0;
|
||||
@@ -207,6 +212,13 @@ export function LiveAndroidStreamConsole({
|
||||
setFrameTs((n) => n + 1);
|
||||
}, []);
|
||||
|
||||
const clearScrcpyFirstFrameTimer = useCallback(() => {
|
||||
if (scrcpyFirstFrameTimerRef.current != null) {
|
||||
window.clearTimeout(scrcpyFirstFrameTimerRef.current);
|
||||
scrcpyFirstFrameTimerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const onVis = () => setPageVisible(document.visibilityState === "visible");
|
||||
onVis();
|
||||
@@ -304,6 +316,7 @@ export function LiveAndroidStreamConsole({
|
||||
|
||||
useEffect(() => {
|
||||
setScrcpyUserStoppedForSerial(null);
|
||||
scrcpyExpectedCloseRef.current = false;
|
||||
}, [serial]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -343,6 +356,9 @@ export function LiveAndroidStreamConsole({
|
||||
|
||||
useEffect(() => {
|
||||
if (!scrcpyStream || !serial || !adbAvailable) {
|
||||
clearScrcpyFirstFrameTimer();
|
||||
scrcpyExpectedCloseRef.current = true;
|
||||
scrcpyGotFrameRef.current = false;
|
||||
wsRef.current?.close();
|
||||
wsRef.current = null;
|
||||
decoderRef.current?.reset();
|
||||
@@ -358,9 +374,29 @@ export function LiveAndroidStreamConsole({
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const dec = new ScrcpyH264Decoder(canvas, (msg) => setScrcpyErr(msg));
|
||||
const fallbackToScreenshot = (message: string) => {
|
||||
clearScrcpyFirstFrameTimer();
|
||||
scrcpyExpectedCloseRef.current = true;
|
||||
wsRef.current?.close();
|
||||
setScrcpyErr(message);
|
||||
setScrcpyStream(false);
|
||||
setLiveMs(SCREENSHOT_FALLBACK_INTERVAL_MS);
|
||||
setScrcpyUserStoppedForSerial(serial);
|
||||
window.setTimeout(() => bumpFrame(), 120);
|
||||
};
|
||||
|
||||
const dec = new ScrcpyH264Decoder(
|
||||
canvas,
|
||||
(msg) => fallbackToScreenshot(`${t("实时画面解码失败,已切回截图模式", "Live stream decode failed, using screenshot mode")}: ${msg}`),
|
||||
() => {
|
||||
scrcpyGotFrameRef.current = true;
|
||||
clearScrcpyFirstFrameTimer();
|
||||
},
|
||||
);
|
||||
decoderRef.current = dec;
|
||||
|
||||
scrcpyExpectedCloseRef.current = false;
|
||||
scrcpyGotFrameRef.current = false;
|
||||
const wsUrl = androidScrcpyWsUrl(apiBase, serial);
|
||||
const ws = new WebSocket(wsUrl);
|
||||
wsRef.current = ws;
|
||||
@@ -369,6 +405,11 @@ export function LiveAndroidStreamConsole({
|
||||
ws.onopen = () => {
|
||||
setScrcpyConnected(true);
|
||||
setScrcpyErr(null);
|
||||
clearScrcpyFirstFrameTimer();
|
||||
scrcpyFirstFrameTimerRef.current = window.setTimeout(() => {
|
||||
if (scrcpyGotFrameRef.current) return;
|
||||
fallbackToScreenshot(t("实时画面无首帧,已切回截图模式", "No first live frame, using screenshot mode"));
|
||||
}, SCRCPY_FIRST_FRAME_TIMEOUT_MS);
|
||||
};
|
||||
ws.onmessage = (ev) => {
|
||||
if (typeof ev.data === "string") {
|
||||
@@ -389,15 +430,22 @@ export function LiveAndroidStreamConsole({
|
||||
setScrcpyConnected(false);
|
||||
decoderRef.current?.reset();
|
||||
decoderRef.current = null;
|
||||
clearScrcpyFirstFrameTimer();
|
||||
if (scrcpyStream && !scrcpyExpectedCloseRef.current) {
|
||||
fallbackToScreenshot(t("实时画面连接失败,已切回截图模式", "Live stream failed, using screenshot mode"));
|
||||
}
|
||||
};
|
||||
|
||||
return () => {
|
||||
clearScrcpyFirstFrameTimer();
|
||||
scrcpyExpectedCloseRef.current = true;
|
||||
scrcpyGotFrameRef.current = false;
|
||||
ws.close();
|
||||
wsRef.current = null;
|
||||
dec.reset();
|
||||
if (decoderRef.current === dec) decoderRef.current = null;
|
||||
};
|
||||
}, [scrcpyStream, serial, adbAvailable, apiBase, t]);
|
||||
}, [scrcpyStream, serial, adbAvailable, apiBase, bumpFrame, clearScrcpyFirstFrameTimer, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (serial && adbAvailable) {
|
||||
|
||||
@@ -118,9 +118,17 @@ type HubDashboard = {
|
||||
tx_bytes?: number;
|
||||
rx_packets?: number;
|
||||
tx_packets?: number;
|
||||
rx_bps?: number;
|
||||
tx_bps?: number;
|
||||
sample_interval_seconds?: number;
|
||||
speed_mbps?: number | null;
|
||||
operstate?: string;
|
||||
}>;
|
||||
total_rx_bytes?: number;
|
||||
total_tx_bytes?: number;
|
||||
total_rx_bps?: number;
|
||||
total_tx_bps?: number;
|
||||
sample_interval_seconds?: number;
|
||||
};
|
||||
live_events?: {
|
||||
pm2_tail?: string[];
|
||||
@@ -221,6 +229,11 @@ function formatBytes(value?: number) {
|
||||
return `${n.toFixed(n >= 10 || i === 0 ? 0 : 1)} ${u[i]}`;
|
||||
}
|
||||
|
||||
function formatRate(value?: number) {
|
||||
const text = formatBytes(value);
|
||||
return text === "—" ? text : `${text}/s`;
|
||||
}
|
||||
|
||||
function statusPill(status: string) {
|
||||
if (status === "online" || status === "running")
|
||||
return "bg-emerald-500/15 text-emerald-300 ring-1 ring-emerald-500/30";
|
||||
@@ -1181,11 +1194,26 @@ export default function LiveControlApp() {
|
||||
<div className="rounded-2xl border border-cyan-500/15 bg-gradient-to-br from-cyan-500/[0.07] to-transparent p-5">
|
||||
<h3 className="flex items-center gap-2 text-sm font-semibold text-white">
|
||||
<Wifi className="h-4 w-4 text-cyan-300" />
|
||||
{t("网络流量(累计)", "Network I/O (cumulative)")}
|
||||
{t("网络带宽 / 流量", "Network bandwidth / traffic")}
|
||||
</h3>
|
||||
<p className="mt-1 text-[11px] text-zinc-500">
|
||||
{t("自开机以来各接口收发字节;非实时速率。", "Per-interface RX/TX since boot (not a live bitrate).")}
|
||||
{t(
|
||||
"实时速率由总览自动刷新间隔计算;累计值来自系统网卡计数器。",
|
||||
"Live rates are calculated from dashboard refresh deltas; totals come from kernel counters.",
|
||||
)}
|
||||
</p>
|
||||
<div className="mt-4 grid gap-3 sm:grid-cols-2">
|
||||
<div className="rounded-xl border border-white/[0.06] bg-black/25 px-4 py-3 font-mono text-[11px]">
|
||||
<p className="text-[10px] uppercase tracking-[0.08em] text-cyan-300/70">{t("实时下行", "Live down")}</p>
|
||||
<p className="mt-1 text-lg font-semibold text-cyan-100">{formatRate(dash.network.total_rx_bps)}</p>
|
||||
<p className="mt-1 text-zinc-500">{t("累计", "Total")}: {formatBytes(dash.network.total_rx_bytes)}</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-white/[0.06] bg-black/25 px-4 py-3 font-mono text-[11px]">
|
||||
<p className="text-[10px] uppercase tracking-[0.08em] text-cyan-300/70">{t("实时上行", "Live up")}</p>
|
||||
<p className="mt-1 text-lg font-semibold text-cyan-100">{formatRate(dash.network.total_tx_bps)}</p>
|
||||
<p className="mt-1 text-zinc-500">{t("累计", "Total")}: {formatBytes(dash.network.total_tx_bytes)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{(dash.network.interfaces || []).slice(0, 6).map((iface) => (
|
||||
<div
|
||||
@@ -1198,11 +1226,13 @@ export default function LiveControlApp() {
|
||||
{iface.speed_mbps ? `${iface.speed_mbps} Mbps` : t("未知速率", "unknown speed")}
|
||||
</p>
|
||||
<p className="mt-2 text-zinc-500">
|
||||
↓ {formatBytes(iface.rx_bytes)} <span className="text-zinc-600">·</span>{" "}
|
||||
↓ {formatRate(iface.rx_bps)} <span className="text-zinc-600">·</span>{" "}
|
||||
{formatBytes(iface.rx_bytes)} <span className="text-zinc-600">·</span>{" "}
|
||||
{iface.rx_packets?.toLocaleString() ?? "—"} pkt
|
||||
</p>
|
||||
<p className="mt-0.5 text-zinc-500">
|
||||
↑ {formatBytes(iface.tx_bytes)} <span className="text-zinc-600">·</span>{" "}
|
||||
↑ {formatRate(iface.tx_bps)} <span className="text-zinc-600">·</span>{" "}
|
||||
{formatBytes(iface.tx_bytes)} <span className="text-zinc-600">·</span>{" "}
|
||||
{iface.tx_packets?.toLocaleString() ?? "—"} pkt
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user