's'
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
HOST=0.0.0.0
|
HOST=0.0.0.0
|
||||||
|
|
||||||
# Web 控制台(FastAPI + 静态页)端口;与同机 SRS / Clash / Neko 等冲突时请改成未占用端口,例如 8101、9200
|
# Web 控制台(FastAPI + 静态页)端口;与同机 SRS / Clash / Neko 等冲突时请改成未占用端口,例如 8101、9200
|
||||||
|
# 若 Nginx/Caddy 反代出现 502,请核对 proxy_pass 端口与这里一致(Docker Compose 默认常为 8101,PM2 ecosystem 默认 8001)
|
||||||
PORT=8001
|
PORT=8001
|
||||||
|
|
||||||
# 开发模式时 Next 端口(一般无需改)
|
# 开发模式时 Next 端口(一般无需改)
|
||||||
|
|||||||
@@ -93,6 +93,16 @@ def ensure_console_built(*, skip: bool) -> bool:
|
|||||||
if _npm(["install"], cwd=CONSOLE) != 0:
|
if _npm(["install"], cwd=CONSOLE) != 0:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if _npm(["run", "build"], cwd=CONSOLE) != 0:
|
if _npm(["run", "build"], cwd=CONSOLE) != 0:
|
||||||
|
if OUT_INDEX.is_file():
|
||||||
|
print(
|
||||||
|
"[launch] npm run build 失败,但检测到已有 web-console/out/index.html,"
|
||||||
|
"将继续启动 API(静态页可能为旧构建;请修复构建后重启)。",
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
print(
|
||||||
|
"[launch] npm run build 失败且缺少 web-console/out/index.html;"
|
||||||
|
"无法提供控制台。请在 web-console 目录执行 npm run build 或检查 Node/内存。",
|
||||||
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
print("[launch] 前端构建完成。")
|
print("[launch] 前端构建完成。")
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -78,7 +78,10 @@ async def hub_dashboard() -> dict[str, Any]:
|
|||||||
else:
|
else:
|
||||||
devices = []
|
devices = []
|
||||||
stack = stack_summary()
|
stack = stack_summary()
|
||||||
probes = await stack_stream_probes()
|
try:
|
||||||
|
probes = await stack_stream_probes()
|
||||||
|
except Exception:
|
||||||
|
probes = {"srs": {}, "neko": {}, "error": "probe_failed"}
|
||||||
|
|
||||||
live_status: dict[str, Any] = {
|
live_status: dict[str, Any] = {
|
||||||
"processes": [],
|
"processes": [],
|
||||||
|
|||||||
@@ -147,8 +147,8 @@ async def open_scrcpy_h264_stream(
|
|||||||
serial: str,
|
serial: str,
|
||||||
*,
|
*,
|
||||||
max_size: int = 1920,
|
max_size: int = 1920,
|
||||||
connect_attempts: int = 12,
|
connect_attempts: int = 22,
|
||||||
connect_delay: float = 0.22,
|
connect_delay: float = 0.28,
|
||||||
) -> tuple[int, str, asyncio.StreamReader, asyncio.StreamWriter]:
|
) -> tuple[int, str, asyncio.StreamReader, asyncio.StreamWriter]:
|
||||||
"""建立 forward + 启动 server,并返回 (local_port, scid_hex, reader, writer)。"""
|
"""建立 forward + 启动 server,并返回 (local_port, scid_hex, reader, writer)。"""
|
||||||
_validate_serial(serial)
|
_validate_serial(serial)
|
||||||
|
|||||||
@@ -176,6 +176,8 @@ export function LiveAndroidStreamConsole({
|
|||||||
const [connBusy, setConnBusy] = useState(false);
|
const [connBusy, setConnBusy] = useState(false);
|
||||||
const [bookmarks, setBookmarks] = useState<string[]>([]);
|
const [bookmarks, setBookmarks] = useState<string[]>([]);
|
||||||
const [scrcpyStream, setScrcpyStream] = useState(false);
|
const [scrcpyStream, setScrcpyStream] = useState(false);
|
||||||
|
/** 用户在本机序列号上点了「关闭真流」后,不再自动拉起,直到切换设备。 */
|
||||||
|
const [scrcpyUserStoppedForSerial, setScrcpyUserStoppedForSerial] = useState<string | null>(null);
|
||||||
const [scrcpyConnected, setScrcpyConnected] = useState(false);
|
const [scrcpyConnected, setScrcpyConnected] = useState(false);
|
||||||
const [scrcpyErr, setScrcpyErr] = useState<string | null>(null);
|
const [scrcpyErr, setScrcpyErr] = useState<string | null>(null);
|
||||||
const [scrcpyInfo, setScrcpyInfo] = useState<{
|
const [scrcpyInfo, setScrcpyInfo] = useState<{
|
||||||
@@ -288,6 +290,45 @@ export function LiveAndroidStreamConsole({
|
|||||||
if (!scrcpyStream) setScrcpyErr(null);
|
if (!scrcpyStream) setScrcpyErr(null);
|
||||||
}, [scrcpyStream]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (!scrcpyStream || !serial || !adbAvailable) {
|
if (!scrcpyStream || !serial || !adbAvailable) {
|
||||||
wsRef.current?.close();
|
wsRef.current?.close();
|
||||||
@@ -666,8 +707,8 @@ export function LiveAndroidStreamConsole({
|
|||||||
</h3>
|
</h3>
|
||||||
<p className="mt-1 max-w-xl text-[11px] leading-relaxed text-zinc-500">
|
<p className="mt-1 max-w-xl text-[11px] leading-relaxed text-zinc-500">
|
||||||
{t(
|
{t(
|
||||||
"统一适配 USB / Redroid / 无线 adb:可选官方 scrcpy-server 经 WebSocket 的 H.264 真流(与桌面 ",
|
"统一适配 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 / 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">
|
<a href={ESCRCPY_URL} target="_blank" rel="noreferrer" className="text-emerald-300/90 underline-offset-2 hover:underline">
|
||||||
escrcpy
|
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">
|
<a href={SCRCPY_URL} target="_blank" rel="noreferrer" className="text-emerald-300/90 underline-offset-2 hover:underline">
|
||||||
scrcpy
|
scrcpy
|
||||||
</a>
|
</a>
|
||||||
{t(");截图轮询作备用。", "); polled PNG as fallback.")}
|
{t(");无 jar 或关闭真流时用截图。", "); PNG screenshot when jar missing or stream off.")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -818,11 +859,13 @@ export function LiveAndroidStreamConsole({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setScrcpyStream((v) => {
|
setScrcpyStream((v) => {
|
||||||
const next = !v;
|
const next = !v;
|
||||||
notify(
|
if (next) {
|
||||||
next
|
setScrcpyUserStoppedForSerial(null);
|
||||||
? t("已请求 Scrcpy 真流(WebSocket)", "Scrcpy WebSocket stream starting")
|
notify(t("已开启实时画面(H.264 WebSocket)", "Live H.264 WebSocket stream on"));
|
||||||
: t("已关闭 Scrcpy 真流", "Scrcpy stream stopped"),
|
} else {
|
||||||
);
|
if (serial) setScrcpyUserStoppedForSerial(serial);
|
||||||
|
notify(t("已关闭实时画面(将使用截图)", "Live stream off — screenshot mode"));
|
||||||
|
}
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -833,7 +876,7 @@ export function LiveAndroidStreamConsole({
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Radio className="h-3 w-3" />
|
<Radio className="h-3 w-3" />
|
||||||
{scrcpyStream ? t("关闭真流", "Stop WS") : t("Scrcpy 真流", "Scrcpy WS")}
|
{scrcpyStream ? t("关闭真流", "Stop WS") : t("实时画面", "Live H.264")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{scrcpyInfo && !scrcpyInfo.server_jar ? (
|
{scrcpyInfo && !scrcpyInfo.server_jar ? (
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* 将 HTTP API 根地址转为 WebSocket 根(用于 /android/scrcpy/ws)。
|
* 将 HTTP API 根地址转为 WebSocket 根(用于 /android/scrcpy/ws)。
|
||||||
* next dev 下若未设置 NEXT_PUBLIC_API_URL,浏览器会连到 :3000,WebSocket 升级通常无法被中间件代理;
|
* next dev:HTTP 可走 middleware rewrite,但 WebSocket 升级通常不会转发,故在常见 dev 端口直连 uvicorn(与 middleware API_PROXY_TARGET 默认一致)。
|
||||||
* 真流调试请设 NEXT_PUBLIC_API_URL 指向 uvicorn(例如 http://127.0.0.1:8001)。
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const DEFAULT_DEV_API_PORT = "8001";
|
||||||
|
|
||||||
export function httpApiBaseToWsBase(apiBase: string): string {
|
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();
|
const base = (apiBase || "").trim();
|
||||||
if (base) {
|
if (base) {
|
||||||
try {
|
try {
|
||||||
@@ -16,6 +17,24 @@ export function httpApiBaseToWsBase(apiBase: string): string {
|
|||||||
/* ignore */
|
/* 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 { protocol, host } = window.location;
|
||||||
const proto = protocol === "https:" ? "wss:" : "ws:";
|
const proto = protocol === "https:" ? "wss:" : "ws:";
|
||||||
return `${proto}//${host}`;
|
return `${proto}//${host}`;
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const API_ROOTS = new Set([
|
|||||||
"android",
|
"android",
|
||||||
"hub",
|
"hub",
|
||||||
"youtube_pro_channels",
|
"youtube_pro_channels",
|
||||||
|
"tiktok_replay",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function middleware(request: NextRequest) {
|
export function middleware(request: NextRequest) {
|
||||||
@@ -68,5 +69,6 @@ export const config = {
|
|||||||
"/android/:path*",
|
"/android/:path*",
|
||||||
"/hub/:path*",
|
"/hub/:path*",
|
||||||
"/youtube_pro_channels",
|
"/youtube_pro_channels",
|
||||||
|
"/tiktok_replay/:path*",
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
8
web.py
8
web.py
@@ -336,7 +336,11 @@ def script_for_pm2(pm2: str) -> Optional[str]:
|
|||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
await process_backend.refresh_mode()
|
try:
|
||||||
|
await process_backend.refresh_mode()
|
||||||
|
except Exception:
|
||||||
|
# PM2/Shell 异常时不阻塞整站启动(否则反代持续 502)
|
||||||
|
pass
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
@@ -364,7 +368,7 @@ async def alias_youtube_pro_channels_post(body: YoutubeProChannelsBody):
|
|||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"],
|
allow_origins=["*"],
|
||||||
allow_methods=["GET", "POST"],
|
allow_methods=["GET", "POST", "OPTIONS"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user