From 2d840a6c37905f6c324c231fc9719bb0bd1f9da6 Mon Sep 17 00:00:00 2001 From: host Date: Fri, 30 Oct 2026 13:14:38 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20navigate=20=E5=90=8E=E7=AB=8B=E5=8D=B3?= =?UTF-8?q?=E6=8E=A8=E4=B8=80=E5=B8=A7=EF=BC=8C=E9=9D=99=E6=80=81=E9=A1=B5?= =?UTF-8?q?=E5=BC=80=E6=B5=81=E4=B8=8D=E5=86=8D=E7=AD=89=E9=87=8D=E7=BB=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit screencast 帧只在重绘时来,页面可能在 startScreencast 之前 就已画好——静态页于是开流后一直无画面,直到下一次重绘。 navigate 后用 CDP captureScreenshot 抓一张当前视口塞进帧 队列,泵在下一轮就把桥帧文件写出来;泵跳过空 session 的 ack(eager 帧没有可确认的 screencast 帧)。失败静默, screencast 本身仍照常流动。 --- cloud/playwright_browser.py | 31 +++++++++++++++++++++++++++++-- cloud/tests/test_playwright.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/cloud/playwright_browser.py b/cloud/playwright_browser.py index 2d1a77e..03e00ab 100644 --- a/cloud/playwright_browser.py +++ b/cloud/playwright_browser.py @@ -120,6 +120,7 @@ class Browser: wait_until="domcontentloaded") self._current_url = self._page.url self._start_screencast() + self._eager_frame() except Exception as exc: # noqa: BLE001 raise BrowserError("navigate failed: %s" % exc) result = self._info() @@ -321,9 +322,35 @@ class Browser: self._cdp = None self._cdp_page = None + def _eager_frame(self): + """Push one current-viewport frame into the sink, best-effort. + + Screencast frames only arrive on repaint, and the page may + already have painted before startScreencast ran -- so a + static page would otherwise show nothing until its next + repaint. Capturing the viewport once right after navigate + guarantees the bridge file (and any open stream) has a + picture within milliseconds of the page loading. + """ + if self._cdp is None or self._frame_sink is None: + return + try: + result = self._cdp.send( + "Page.captureScreenshot", + {"format": "jpeg", "quality": 60}) + data = result.get("data") if isinstance(result, dict) \ + else None + if data: + # Empty session id: pump stores it but must not ack + # it (there is no screencast frame to acknowledge). + self._frame_sink.put(("", data)) + except Exception: # noqa: BLE001 - screencast still flows + pass + def pump_screencast(self): """Main-thread only: store the latest frame and ack every - queued frame (no ack = CDP stops sending after the first). + real screencast frame (no ack = CDP stops sending after the + first). Eager frames carry no session id and skip ack. Returns the latest frame data (base64 jpeg) or ''.""" if self._frame_sink is None: return "" @@ -335,7 +362,7 @@ class Browser: break if data: latest = data - if self._cdp is not None: + if self._cdp is not None and sid: try: self._cdp.send("Page.screencastFrameAck", {"sessionId": sid}) diff --git a/cloud/tests/test_playwright.py b/cloud/tests/test_playwright.py index 893865c..43867bd 100644 --- a/cloud/tests/test_playwright.py +++ b/cloud/tests/test_playwright.py @@ -59,12 +59,18 @@ class _FakeCDP: def __init__(self): self.sent = [] self.handlers = {} + self.fail_capture = False def on(self, event, handler): self.handlers[event] = handler def send(self, method, params=None): self.sent.append((method, params or {})) + if method == "Page.captureScreenshot": + if self.fail_capture: + raise RuntimeError("capture boom") + return {"data": "EAGERDATA"} + return None class _FakeContext: @@ -259,6 +265,33 @@ class BrowserTest(unittest.TestCase): self.assertIn(("Page.screencastFrameAck", {"sessionId": "s1"}), [(m, p) for m, p in self.page.cdp.sent]) + def test_navigate_pushes_eager_frame_without_ack(self): + from playwright_remote import SimpleQueue + sink = SimpleQueue() + self.browser.set_frame_sink(sink) + result = self.browser.navigate("https://example.com") + self.assertTrue(result["ok"]) + self.assertIn(("Page.captureScreenshot", + {"format": "jpeg", "quality": 60}), + [(m, p) for m, p in self.page.cdp.sent]) + frame = self.browser.pump_screencast() + self.assertEqual(frame, "EAGERDATA") + acks = [(m, p) for m, p in self.page.cdp.sent + if m == "Page.screencastFrameAck"] + self.assertEqual(acks, []) + + def test_navigate_survives_failed_eager_capture(self): + from playwright_remote import SimpleQueue + sink = SimpleQueue() + self.browser.set_frame_sink(sink) + self.page.cdp.fail_capture = True + try: + result = self.browser.navigate("https://example.com") + finally: + self.page.cdp.fail_capture = False + self.assertTrue(result["ok"]) + self.assertEqual(self.browser.pump_screencast(), "") + def test_live_shot_writes_fixed_file_and_status(self): self.browser.navigate("https://example.com") out = self.browser.live_shot()