#!/usr/bin/env python3 """Open the Cocos H5 build with fixed mobile browser emulation. This is for manual preview only. It makes the browser page use the same logical inputs as a real phone H5 page: CSS viewport, DPR, mobile mode, touch, and user agent. """ from __future__ import annotations import argparse import json import shutil import socket import subprocess import tempfile import time import urllib.request from pathlib import Path ROOT = Path(__file__).resolve().parents[1] DEFAULT_URL = "http://127.0.0.1:8787/index.html" DEFAULT_BUILD_DIR = ROOT / "CocosFarm/build/web-mobile" EDGE = Path("/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge") CHROME = Path("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome") IPHONE_14_PRO_MAX_UA = ( "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) " "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 " "Mobile/15E148 Safari/604.1" ) class CdpClient: def __init__(self, ws_url: str) -> None: try: import websocket # type: ignore except ImportError as exc: raise SystemExit("Missing websocket-client package for mobile preview.") from exc self.ws = websocket.create_connection(ws_url, timeout=30) self.next_id = 1 def close(self) -> None: self.ws.close() def call(self, method: str, params: dict | None = None, timeout: float = 30.0) -> dict: msg_id = self.next_id self.next_id += 1 self.ws.send(json.dumps({"id": msg_id, "method": method, "params": params or {}})) deadline = time.time() + timeout while time.time() < deadline: data = json.loads(self.ws.recv()) if data.get("id") == msg_id: if "error" in data: raise RuntimeError(f"CDP {method} failed: {data['error']}") return data.get("result", {}) raise TimeoutError(f"Timed out waiting for CDP response: {method}") def find_free_port() -> int: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.bind(("127.0.0.1", 0)) return int(sock.getsockname()[1]) def wait_for_target_ws(port: int) -> str: deadline = time.time() + 20 last_error: Exception | None = None while time.time() < deadline: try: with urllib.request.urlopen(f"http://127.0.0.1:{port}/json/list", timeout=1) as response: targets = json.loads(response.read().decode("utf-8")) for target in targets: ws_url = target.get("webSocketDebuggerUrl") if target.get("type") == "page" and ws_url: return str(ws_url) except Exception as exc: last_error = exc time.sleep(0.2) raise RuntimeError(f"Browser CDP target was not ready: {last_error}") def browser_path() -> Path: if EDGE.exists(): return EDGE if CHROME.exists(): return CHROME found = shutil.which("Microsoft Edge") or shutil.which("Google Chrome") or shutil.which("chrome") if found: return Path(found) raise SystemExit("Could not find Edge or Chrome for mobile preview.") def start_static_server(port: int, directory: Path) -> subprocess.Popen: if not directory.exists(): raise SystemExit(f"Build directory does not exist: {directory}") return subprocess.Popen( ["python3", "-m", "http.server", str(port), "--bind", "127.0.0.1", "--directory", str(directory)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, text=True, ) def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--url", default=DEFAULT_URL) parser.add_argument("--width", type=int, default=430, help="Phone CSS viewport width.") parser.add_argument("--height", type=int, default=932, help="Phone CSS viewport height.") parser.add_argument("--dpr", type=float, default=3.0, help="Phone devicePixelRatio.") parser.add_argument("--user-agent", default=IPHONE_14_PRO_MAX_UA) parser.add_argument("--serve-build", action="store_true", help="Serve CocosFarm/build/web-mobile on port 8787.") parser.add_argument("--server-port", type=int, default=8787) args = parser.parse_args() server_proc: subprocess.Popen | None = None if args.serve_build: server_proc = start_static_server(args.server_port, DEFAULT_BUILD_DIR) time.sleep(0.5) remote_port = find_free_port() user_data = tempfile.TemporaryDirectory(prefix="farm-mobile-preview-") browser = browser_path() proc = subprocess.Popen( [ str(browser), f"--remote-debugging-port={remote_port}", "--remote-allow-origins=*", f"--user-data-dir={user_data.name}", f"--window-size={args.width},{args.height}", "--no-first-run", "--no-default-browser-check", "--hide-scrollbars", "--mute-audio", "about:blank", ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, text=True, ) client: CdpClient | None = None try: client = CdpClient(wait_for_target_ws(remote_port)) client.call("Page.enable") client.call("Runtime.enable") client.call( "Emulation.setDeviceMetricsOverride", { "width": args.width, "height": args.height, "deviceScaleFactor": args.dpr, "mobile": True, }, ) client.call("Emulation.setTouchEmulationEnabled", {"enabled": True, "maxTouchPoints": 1}) client.call( "Emulation.setUserAgentOverride", { "userAgent": args.user_agent, "platform": "iPhone", "mobile": True, }, ) client.call("Page.navigate", {"url": args.url}) print( json.dumps( { "status": "opened", "url": args.url, "browser": str(browser), "cssViewport": {"width": args.width, "height": args.height}, "devicePixelRatio": args.dpr, "mobile": True, "touch": True, "note": "Keep this process running while previewing. Press Ctrl-C to close.", }, ensure_ascii=False, indent=2, ) ) proc.wait() except KeyboardInterrupt: pass finally: if client is not None: client.close() proc.terminate() if server_proc is not None: server_proc.terminate() if __name__ == "__main__": main()