/** * Mobile Proxy Test — Bun Reverse Proxy Server * * Sits on port 3000 (what ngrok tunnels) and: * GET / -> serves our clean mobile viewer (view.html) * Everything else -> reverse-proxied to ws-scrcpy on port 8000 * * WebSocket upgrades are proxied transparently so the ws-scrcpy * player running inside our iframe can talk to the scrcpy server. */ const WS_SCRCPY_ORIGIN = "http://localhost:8000"; const PROXY_PORT = 3000; const viewHtml = await Bun.file( new URL("./view.html", import.meta.url).pathname, ).text(); const server = Bun.serve({ port: PROXY_PORT, hostname: "0.0.0.0", async fetch(req, server) { const url = new URL(req.url); // Serve our clean mobile viewer at root only // (the iframe loads ws-scrcpy's own page via /?scrcpy=1) if ( (url.pathname === "/" || url.pathname === "/index.html") && !url.searchParams.has("scrcpy") ) { return new Response(viewHtml, { headers: { "Content-Type": "text/html; charset=utf-8" }, }); } // Upgrade WebSocket connections — proxy to ws-scrcpy if (req.headers.get("upgrade")?.toLowerCase() === "websocket") { const targetUrl = `${WS_SCRCPY_ORIGIN.replace("http", "ws")}${url.pathname}${url.search}`; // Use Bun's WebSocket upgrade to establish a client-side WS to ws-scrcpy // and bridge the two connections const success = server.upgrade(req, { data: { targetUrl }, }); if (success) return undefined; return new Response("WebSocket upgrade failed", { status: 400 }); } // Proxy all other HTTP requests to ws-scrcpy const targetUrl = `${WS_SCRCPY_ORIGIN}${url.pathname}${url.search}`; try { const proxyRes = await fetch(targetUrl, { method: req.method, headers: req.headers, body: req.method !== "GET" && req.method !== "HEAD" ? req.body : undefined, }); // Clone response with CORS headers stripped / adjusted const headers = new Headers(proxyRes.headers); headers.delete("content-encoding"); // Bun handles this return new Response(proxyRes.body, { status: proxyRes.status, statusText: proxyRes.statusText, headers, }); } catch (err) { console.error(`Proxy error for ${url.pathname}:`, err); return new Response( "ws-scrcpy not reachable — is it running on port 8000?", { status: 502, }, ); } }, websocket: { async open(ws) { const { targetUrl } = ws.data as { targetUrl: string }; // Open a WebSocket connection to ws-scrcpy const upstream = new WebSocket(targetUrl); // Store upstream reference on ws data for cleanup (ws.data as any).upstream = upstream; upstream.binaryType = "arraybuffer"; upstream.onopen = () => { // Connection established, nothing extra needed }; upstream.onmessage = (event) => { try { if (event.data instanceof ArrayBuffer) { ws.sendBinary(new Uint8Array(event.data)); } else { ws.sendText(event.data); } } catch { // Client disconnected } }; upstream.onclose = () => { ws.close(); }; upstream.onerror = (err) => { console.error("Upstream WS error:", err); ws.close(); }; }, message(ws, message) { const upstream = (ws.data as any).upstream as WebSocket | undefined; if (!upstream || upstream.readyState !== WebSocket.OPEN) return; try { if (typeof message === "string") { upstream.send(message); } else { upstream.send(message); } } catch { // Upstream disconnected } }, close(ws) { const upstream = (ws.data as any).upstream as WebSocket | undefined; if (upstream && upstream.readyState === WebSocket.OPEN) { upstream.close(); } }, }, }); console.log(`\n Mobile Proxy running on http://localhost:${server.port}`); console.log(` Proxying to ws-scrcpy at ${WS_SCRCPY_ORIGIN}`); console.log( `\n Point ngrok at port ${server.port}, then open the ngrok URL on the user's phone.\n`, );