This commit is contained in:
eric
2026-03-29 03:49:29 -05:00
parent 22061fb3c8
commit 7cab3f27b8
8 changed files with 99 additions and 17 deletions

View File

@@ -176,6 +176,8 @@ export function LiveAndroidStreamConsole({
const [connBusy, setConnBusy] = useState(false);
const [bookmarks, setBookmarks] = useState<string[]>([]);
const [scrcpyStream, setScrcpyStream] = useState(false);
/** 用户在本机序列号上点了「关闭真流」后,不再自动拉起,直到切换设备。 */
const [scrcpyUserStoppedForSerial, setScrcpyUserStoppedForSerial] = useState<string | null>(null);
const [scrcpyConnected, setScrcpyConnected] = useState(false);
const [scrcpyErr, setScrcpyErr] = useState<string | null>(null);
const [scrcpyInfo, setScrcpyInfo] = useState<{
@@ -288,6 +290,45 @@ export function LiveAndroidStreamConsole({
if (!scrcpyStream) setScrcpyErr(null);
}, [scrcpyStream]);
useEffect(() => {
setScrcpyUserStoppedForSerial(null);
}, [serial]);
useEffect(() => {
const row = devices.find((d) => d.serial === serial);
const userStopped = Boolean(serial && scrcpyUserStoppedForSerial === serial);
const canAuto =
adbAvailable &&
Boolean(serial) &&
row?.state === "device" &&
Boolean(scrcpyInfo?.server_jar) &&
webCodecsH264Supported() &&
!userStopped;
if (canAuto) {
setScrcpyStream(true);
setLiveMs(0);
return;
}
if (userStopped || !serial || !adbAvailable) {
setScrcpyStream(false);
return;
}
if (scrcpyInfo && !scrcpyInfo.server_jar) {
setScrcpyStream(false);
return;
}
if (!webCodecsH264Supported()) {
setScrcpyStream(false);
return;
}
if (row && row.state !== "device") {
setScrcpyStream(false);
return;
}
}, [serial, devices, adbAvailable, scrcpyInfo, scrcpyUserStoppedForSerial]);
useEffect(() => {
if (!scrcpyStream || !serial || !adbAvailable) {
wsRef.current?.close();
@@ -666,8 +707,8 @@ export function LiveAndroidStreamConsole({
</h3>
<p className="mt-1 max-w-xl text-[11px] leading-relaxed text-zinc-500">
{t(
"统一适配 USB / Redroid / 无线 adb可选官方 scrcpy-server WebSocket H.264 真流(与桌面 ",
"USB / Redroid / wireless adb: optional real H.264 from official scrcpy-server over WebSocket (same stack as ",
"统一适配 USB / Redroid / 无线 adb已授权设备默认走官方 scrcpy-server WebSocket H.264 真流(与桌面 ",
"USB / Redroid / wireless adb: authorized devices default to official scrcpy-server WebSocket H.264 (same stack as ",
)}
<a href={ESCRCPY_URL} target="_blank" rel="noreferrer" className="text-emerald-300/90 underline-offset-2 hover:underline">
escrcpy
@@ -676,7 +717,7 @@ export function LiveAndroidStreamConsole({
<a href={SCRCPY_URL} target="_blank" rel="noreferrer" className="text-emerald-300/90 underline-offset-2 hover:underline">
scrcpy
</a>
{t("截图轮询作备用。", "); polled PNG as fallback.")}
{t("无 jar 或关闭真流时用截图。", "); PNG screenshot when jar missing or stream off.")}
</p>
</div>
</div>
@@ -818,11 +859,13 @@ export function LiveAndroidStreamConsole({
onClick={() => {
setScrcpyStream((v) => {
const next = !v;
notify(
next
? t("已请求 Scrcpy 真流(WebSocket", "Scrcpy WebSocket stream starting")
: t("已关闭 Scrcpy 真流", "Scrcpy stream stopped"),
);
if (next) {
setScrcpyUserStoppedForSerial(null);
notify(t("已开启实时画面H.264 WebSocket", "Live H.264 WebSocket stream on"));
} else {
if (serial) setScrcpyUserStoppedForSerial(serial);
notify(t("已关闭实时画面(将使用截图)", "Live stream off — screenshot mode"));
}
return next;
});
}}
@@ -833,7 +876,7 @@ export function LiveAndroidStreamConsole({
}`}
>
<Radio className="h-3 w-3" />
{scrcpyStream ? t("关闭真流", "Stop WS") : t("Scrcpy 真流", "Scrcpy WS")}
{scrcpyStream ? t("关闭真流", "Stop WS") : t("实时画面", "Live H.264")}
</button>
</div>
{scrcpyInfo && !scrcpyInfo.server_jar ? (

View File

@@ -1,11 +1,12 @@
/**
* 将 HTTP API 根地址转为 WebSocket 根(用于 /android/scrcpy/ws
* next dev 下若未设置 NEXT_PUBLIC_API_URL浏览器会连到 :3000WebSocket 升级通常无法被中间件代理;
* 真流调试请设 NEXT_PUBLIC_API_URL 指向 uvicorn例如 http://127.0.0.1:8001
* next devHTTP 可走 middleware rewrite但 WebSocket 升级通常不会转发,故在常见 dev 端口直连 uvicorn与 middleware API_PROXY_TARGET 默认一致)。
*/
const DEFAULT_DEV_API_PORT = "8001";
export function httpApiBaseToWsBase(apiBase: string): string {
if (typeof window === "undefined") return "ws://127.0.0.1:8001";
if (typeof window === "undefined") return `ws://127.0.0.1:${DEFAULT_DEV_API_PORT}`;
const base = (apiBase || "").trim();
if (base) {
try {
@@ -16,6 +17,24 @@ export function httpApiBaseToWsBase(apiBase: string): string {
/* ignore */
}
}
const pub =
typeof process !== "undefined" && process.env.NEXT_PUBLIC_API_URL
? String(process.env.NEXT_PUBLIC_API_URL).trim()
: "";
if (pub) {
try {
const u = new URL(pub, window.location.href);
const proto = u.protocol === "https:" ? "wss:" : "ws:";
return `${proto}//${u.host}`;
} catch {
/* ignore */
}
}
const port = window.location.port;
if (port === "3000" || port === "3001") {
const h = window.location.hostname || "127.0.0.1";
return `ws://${h}:${DEFAULT_DEV_API_PORT}`;
}
const { protocol, host } = window.location;
const proto = protocol === "https:" ? "wss:" : "ws:";
return `${proto}//${host}`;

View File

@@ -29,6 +29,7 @@ const API_ROOTS = new Set([
"android",
"hub",
"youtube_pro_channels",
"tiktok_replay",
]);
export function middleware(request: NextRequest) {
@@ -68,5 +69,6 @@ export const config = {
"/android/:path*",
"/hub/:path*",
"/youtube_pro_channels",
"/tiktok_replay/:path*",
],
};