fix: navigate 后立即推一帧,静态页开流不再等重绘

screencast 帧只在重绘时来,页面可能在 startScreencast 之前
就已画好——静态页于是开流后一直无画面,直到下一次重绘。
navigate 后用 CDP captureScreenshot 抓一张当前视口塞进帧
队列,泵在下一轮就把桥帧文件写出来;泵跳过空 session 的
ack(eager 帧没有可确认的 screencast 帧)。失败静默,
screencast 本身仍照常流动。
This commit is contained in:
host committed 2026-10-30 13:14:38 +08:00
1 parent b844624c96
commit 2d840a6c37
2 files changed
+62 -2

No files matched your search

+29 -2
View File
@@ -120,6 +120,7 @@ class Browser:
wait_until="domcontentloaded") wait_until="domcontentloaded")
self._current_url = self._page.url self._current_url = self._page.url
self._start_screencast() self._start_screencast()
self._eager_frame()
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
raise BrowserError("navigate failed: %s" % exc) raise BrowserError("navigate failed: %s" % exc)
result = self._info() result = self._info()
@@ -321,9 +322,35 @@ class Browser:
self._cdp = None self._cdp = None
self._cdp_page = 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): def pump_screencast(self):
"""Main-thread only: store the latest frame and ack every """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 ''.""" Returns the latest frame data (base64 jpeg) or ''."""
if self._frame_sink is None: if self._frame_sink is None:
return "" return ""
@@ -335,7 +362,7 @@ class Browser:
break break
if data: if data:
latest = data latest = data
if self._cdp is not None: if self._cdp is not None and sid:
try: try:
self._cdp.send("Page.screencastFrameAck", self._cdp.send("Page.screencastFrameAck",
{"sessionId": sid}) {"sessionId": sid})
+33
View File
@@ -59,12 +59,18 @@ class _FakeCDP:
def __init__(self): def __init__(self):
self.sent = [] self.sent = []
self.handlers = {} self.handlers = {}
self.fail_capture = False
def on(self, event, handler): def on(self, event, handler):
self.handlers[event] = handler self.handlers[event] = handler
def send(self, method, params=None): def send(self, method, params=None):
self.sent.append((method, params or {})) 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: class _FakeContext:
@@ -259,6 +265,33 @@ class BrowserTest(unittest.TestCase):
self.assertIn(("Page.screencastFrameAck", {"sessionId": "s1"}), self.assertIn(("Page.screencastFrameAck", {"sessionId": "s1"}),
[(m, p) for m, p in self.page.cdp.sent]) [(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): def test_live_shot_writes_fixed_file_and_status(self):
self.browser.navigate("https://example.com") self.browser.navigate("https://example.com")
out = self.browser.live_shot() out = self.browser.live_shot()