playwright_rs/protocol/screencast.rs
1//! Live screencast frame streaming, optional disk recording, and
2//! action / chapter / HTML overlays.
3//!
4//! Available on every [`Page`] via
5//! [`screencast()`](crate::protocol::Page::screencast). Once started,
6//! the Playwright server streams JPEG frames as they're rendered,
7//! delivered to handlers registered with [`Screencast::on_frame`].
8//! Optionally records to disk via the [`Artifact`](crate::protocol::artifact::Artifact)
9//! save-on-stop pathway, and can overlay action labels, chapter cards,
10//! or arbitrary HTML on the streamed frames.
11//!
12//! The action / chapter / HTML overlay surfaces are useful for "agent
13//! receipts" — an LLM-driven flow can produce annotated video logs of
14//! what it did alongside the action log.
15//!
16//! # Disk recording vs the Video class
17//!
18//! [`Video`](crate::protocol::Video) and [`Screencast`] cover
19//! complementary lifecycles, both backed by the same underlying
20//! `Artifact` save mechanism:
21//!
22//! - **`Video`** — automatic, captures the entire page session from
23//! open to close. Enabled with `BrowserContextOptions::record_video`.
24//! Use when you want a continuous recording over the whole session.
25//! - **`Screencast::start({ path })`** — user-initiated, captures only
26//! during the start/stop window, saves to `path` on stop. Use when
27//! you want a recording that brackets a specific phase.
28//!
29//! # Example
30//!
31//! ```no_run
32//! use playwright_rs::{Playwright, ScreencastStartOptions};
33//!
34//! #[tokio::main]
35//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
36//! let pw = Playwright::launch().await?;
37//! let browser = pw.chromium().launch().await?;
38//! let page = browser.new_page().await?;
39//! let screencast = page.screencast();
40//!
41//! // Stream frames live
42//! screencast.on_frame(|frame| async move {
43//! println!("got {} byte frame", frame.data.len());
44//! Ok(())
45//! });
46//!
47//! screencast.start(ScreencastStartOptions::default()
48//! .path(std::path::PathBuf::from("/tmp/run.webm"))).await?;
49//!
50//! page.goto("https://example.com", None).await?;
51//! screencast.show_chapter(
52//! "Logged in",
53//! Default::default(),
54//! ).await?;
55//!
56//! screencast.stop().await?; // saves /tmp/run.webm
57//! browser.close().await?;
58//! Ok(())
59//! }
60//! ```
61//!
62//! See: <https://playwright.dev/docs/api/class-page#page-screencast>
63
64use crate::error::Result;
65use crate::protocol::page::Page;
66use crate::server::channel_owner::ChannelOwner;
67use std::path::PathBuf;
68
69/// A single frame emitted while a screencast is active. Wire format is
70/// JPEG; `data` holds the raw bytes ready to write to disk or pass to
71/// an image decoder.
72///
73/// `data` is a [`bytes::Bytes`] handle so the decoded JPEG is allocated
74/// exactly once per frame and cloning into each registered handler is
75/// a refcount bump rather than a memcpy. `Bytes` implements
76/// `Deref<Target = [u8]>`, so existing reads (`frame.data.len()`,
77/// `&frame.data[..]`, `tokio::fs::write(path, &frame.data)`) compile
78/// unchanged from the previous `Vec<u8>` shape.
79#[derive(Debug, Clone)]
80#[non_exhaustive]
81pub struct ScreencastFrame {
82 /// JPEG-encoded frame bytes.
83 pub data: bytes::Bytes,
84}
85
86/// Options for [`Screencast::start`].
87#[derive(Debug, Default, Clone)]
88#[non_exhaustive]
89pub struct ScreencastStartOptions {
90 /// Output frame size. When `None`, Playwright uses the page's
91 /// current viewport size.
92 pub size: Option<ScreencastSize>,
93 /// JPEG quality, `0..=100`. Server default is implementation-defined.
94 pub quality: Option<i32>,
95 /// When set, the screencast is also recorded to a file at this
96 /// path. The file is written on [`Screencast::stop`]. The recording
97 /// covers only the active start/stop window — for a continuous
98 /// "always-on" recording over the whole page session, use
99 /// `BrowserContextOptions::record_video` instead (the `Video`
100 /// class).
101 pub path: Option<PathBuf>,
102}
103
104impl ScreencastStartOptions {
105 /// Output video size.
106 pub fn size(mut self, size: ScreencastSize) -> Self {
107 self.size = Some(size);
108 self
109 }
110 /// Video quality (codec-specific).
111 pub fn quality(mut self, quality: i32) -> Self {
112 self.quality = Some(quality);
113 self
114 }
115 /// Output file path.
116 pub fn path(mut self, path: PathBuf) -> Self {
117 self.path = Some(path);
118 self
119 }
120}
121
122/// Pixel dimensions for a screencast frame.
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub struct ScreencastSize {
125 pub width: i32,
126 pub height: i32,
127}
128
129/// Position for the action-label overlay.
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131#[non_exhaustive]
132pub enum ActionPosition {
133 TopLeft,
134 Top,
135 TopRight,
136 BottomLeft,
137 Bottom,
138 BottomRight,
139}
140
141impl ActionPosition {
142 pub(crate) fn as_str(self) -> &'static str {
143 match self {
144 ActionPosition::TopLeft => "top-left",
145 ActionPosition::Top => "top",
146 ActionPosition::TopRight => "top-right",
147 ActionPosition::BottomLeft => "bottom-left",
148 ActionPosition::Bottom => "bottom",
149 ActionPosition::BottomRight => "bottom-right",
150 }
151 }
152}
153
154/// Options for [`Screencast::show_actions`].
155#[derive(Debug, Default, Clone)]
156#[non_exhaustive]
157pub struct ShowActionsOptions {
158 /// How long each action label stays on screen (milliseconds).
159 pub duration: Option<f64>,
160 /// Where the label appears.
161 pub position: Option<ActionPosition>,
162 /// Label font size, pixels.
163 pub font_size: Option<i32>,
164}
165
166impl ShowActionsOptions {
167 /// How long to show each action, in milliseconds.
168 pub fn duration(mut self, duration: f64) -> Self {
169 self.duration = Some(duration);
170 self
171 }
172 /// Where to render the action labels.
173 pub fn position(mut self, position: ActionPosition) -> Self {
174 self.position = Some(position);
175 self
176 }
177 /// Label font size in pixels.
178 pub fn font_size(mut self, font_size: i32) -> Self {
179 self.font_size = Some(font_size);
180 self
181 }
182}
183
184/// Options for [`Screencast::show_chapter`].
185#[derive(Debug, Default, Clone)]
186#[non_exhaustive]
187pub struct ChapterOptions {
188 /// Optional second line under the chapter title.
189 pub description: Option<String>,
190 /// How long the chapter card stays on screen (milliseconds).
191 pub duration: Option<f64>,
192}
193
194impl ChapterOptions {
195 /// Chapter description text.
196 pub fn description(mut self, description: impl Into<String>) -> Self {
197 self.description = Some(description.into());
198 self
199 }
200 /// Chapter duration, in milliseconds.
201 pub fn duration(mut self, duration: f64) -> Self {
202 self.duration = Some(duration);
203 self
204 }
205}
206
207/// Options for [`Screencast::show_overlay`].
208#[derive(Debug, Default, Clone)]
209#[non_exhaustive]
210pub struct ShowOverlayOptions {
211 /// How long the overlay stays on screen (milliseconds).
212 pub duration: Option<f64>,
213}
214
215impl ShowOverlayOptions {
216 /// How long to show the overlay, in milliseconds.
217 pub fn duration(mut self, duration: f64) -> Self {
218 self.duration = Some(duration);
219 self
220 }
221}
222
223/// Identifier for an active HTML overlay; pass to
224/// [`Screencast::remove_overlay`] to dismiss the overlay before its
225/// duration expires.
226#[derive(Debug, Clone, PartialEq, Eq)]
227pub struct OverlayId(pub String);
228
229/// Live frame-streaming entry point. Obtained from
230/// [`Page::screencast`](crate::protocol::Page::screencast).
231#[derive(Clone)]
232pub struct Screencast {
233 page: Page,
234}
235
236impl Screencast {
237 pub(crate) fn new(page: Page) -> Self {
238 Self { page }
239 }
240
241 /// Begin streaming. Frames arrive on handlers registered via
242 /// [`on_frame`](Self::on_frame); register them before calling
243 /// `start` so no frames are missed.
244 ///
245 /// If `options.path` is set, the screencast is also recorded to
246 /// disk; the file is written when [`stop`](Self::stop) is called.
247 #[tracing::instrument(level = "info", skip_all, fields(page_guid = %self.page.guid()))]
248 pub async fn start(&self, options: ScreencastStartOptions) -> Result<()> {
249 self.page.screencast_start(options).await
250 }
251
252 /// Stop the screencast. If `start` was called with a `path`, the
253 /// recorded file is written to that path before this call returns.
254 #[tracing::instrument(level = "info", skip_all, fields(page_guid = %self.page.guid()))]
255 pub async fn stop(&self) -> Result<()> {
256 self.page.screencast_stop().await
257 }
258
259 /// Register a handler for incoming frames. Multiple handlers may be
260 /// registered; they fire in order for each frame.
261 pub fn on_frame<F, Fut>(&self, handler: F)
262 where
263 F: Fn(ScreencastFrame) -> Fut + Send + Sync + 'static,
264 Fut: std::future::Future<Output = Result<()>> + Send + 'static,
265 {
266 self.page.screencast_on_frame(handler);
267 }
268
269 /// Overlay action labels on the streamed frames as actions occur.
270 /// Pair with [`hide_actions`](Self::hide_actions) to stop.
271 #[tracing::instrument(level = "debug", skip_all, fields(page_guid = %self.page.guid()))]
272 pub async fn show_actions(&self, options: ShowActionsOptions) -> Result<()> {
273 self.page.screencast_show_actions(options).await
274 }
275
276 /// Stop overlaying action labels. No-op if not currently shown.
277 #[tracing::instrument(level = "debug", skip_all, fields(page_guid = %self.page.guid()))]
278 pub async fn hide_actions(&self) -> Result<()> {
279 self.page.screencast_hide_actions().await
280 }
281
282 /// Show a chapter card with the given title (and optional
283 /// description). Useful for splitting a session into named phases
284 /// for an agent's video log.
285 #[tracing::instrument(level = "debug", skip_all, fields(page_guid = %self.page.guid(), title = %title))]
286 pub async fn show_chapter(&self, title: &str, options: ChapterOptions) -> Result<()> {
287 self.page.screencast_chapter(title, options).await
288 }
289
290 /// Render arbitrary HTML as an overlay. Returns an [`OverlayId`]
291 /// you can pass to [`remove_overlay`](Self::remove_overlay) to
292 /// dismiss it early; otherwise it dismisses itself after
293 /// `options.duration` (if set) or stays until removed.
294 #[tracing::instrument(level = "debug", skip_all, fields(page_guid = %self.page.guid()))]
295 pub async fn show_overlay(&self, html: &str, options: ShowOverlayOptions) -> Result<OverlayId> {
296 self.page.screencast_show_overlay(html, options).await
297 }
298
299 /// Remove an overlay previously created via
300 /// [`show_overlay`](Self::show_overlay). Idempotent.
301 #[tracing::instrument(level = "debug", skip_all, fields(page_guid = %self.page.guid()))]
302 pub async fn remove_overlay(&self, id: OverlayId) -> Result<()> {
303 self.page.screencast_remove_overlay(id).await
304 }
305
306 /// Toggle visibility of all currently-shown overlays without
307 /// removing them. Useful for hiding overlays during a section the
308 /// agent considers "noise" and re-showing them later.
309 #[tracing::instrument(level = "debug", skip_all, fields(page_guid = %self.page.guid(), visible))]
310 pub async fn set_overlay_visible(&self, visible: bool) -> Result<()> {
311 self.page.screencast_set_overlay_visible(visible).await
312 }
313}