196 lines
6.6 KiB
Python
Executable File
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()
|