Merge pull request #185 from openqj/feat-browser-stream-first-frame

fix: navigate 后立即推一帧,静态页开流不再等重绘
This commit is contained in:
openqj authored and GitHub committed 2026-10-30 13:22:48 +08:00
commit e9370d7291
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()