cocos-farm/tools/open_mobile_h5_preview.py

196 lines
6.6 KiB
Python
Executable File

#!/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()