This commit is contained in:
eric
2026-05-19 06:47:07 +00:00
parent e1d7251545
commit 78e1308063
63 changed files with 2527 additions and 186 deletions

View File

@@ -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) {