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//! ```ignore
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 {
48//! path: Some(std::path::PathBuf::from("/tmp/run.webm")),
49//! ..Default::default()
50//! }).await?;
51//!
52//! page.goto("https://example.com", None).await?;
53//! screencast.show_chapter(
54//! "Logged in",
55//! Default::default(),
56//! ).await?;
57//!
58//! screencast.stop().await?; // saves /tmp/run.webm
59//! browser.close().await?;
60//! Ok(())
61//! }
62//! ```
63//!
64//! See: <https://playwright.dev/docs/api/class-page#page-screencast>
65
66use crate::error::Result;
67use crate::protocol::page::Page;
68use crate::server::channel_owner::ChannelOwner;
69use std::path::PathBuf;
70
71/// A single frame emitted while a screencast is active. Wire format is
72/// JPEG; `data` holds the raw bytes ready to write to disk or pass to
73/// an image decoder.
74///
75/// `data` is a [`bytes::Bytes`] handle so the decoded JPEG is allocated
76/// exactly once per frame and cloning into each registered handler is
77/// a refcount bump rather than a memcpy. `Bytes` implements
78/// `Deref<Target = [u8]>`, so existing reads (`frame.data.len()`,
79/// `&frame.data[..]`, `tokio::fs::write(path, &frame.data)`) compile
80/// unchanged from the previous `Vec<u8>` shape.
81#[derive(Debug, Clone)]
82pub struct ScreencastFrame {
83 /// JPEG-encoded frame bytes.
84 pub data: bytes::Bytes,
85}
86
87/// Options for [`Screencast::start`].
88#[derive(Debug, Default, Clone)]
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
104/// Pixel dimensions for a screencast frame.
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub struct ScreencastSize {
107 pub width: i32,
108 pub height: i32,
109}
110
111/// Position for the action-label overlay.
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum ActionPosition {
114 TopLeft,
115 Top,
116 TopRight,
117 BottomLeft,
118 Bottom,
119 BottomRight,
120}
121
122impl ActionPosition {
123 pub(crate) fn as_str(self) -> &'static str {
124 match self {
125 ActionPosition::TopLeft => "top-left",
126 ActionPosition::Top => "top",
127 ActionPosition::TopRight => "top-right",
128 ActionPosition::BottomLeft => "bottom-left",
129 ActionPosition::Bottom => "bottom",
130 ActionPosition::BottomRight => "bottom-right",
131 }
132 }
133}
134
135/// Options for [`Screencast::show_actions`].
136#[derive(Debug, Default, Clone)]
137pub struct ShowActionsOptions {
138 /// How long each action label stays on screen (milliseconds).
139 pub duration: Option<f64>,
140 /// Where the label appears.
141 pub position: Option<ActionPosition>,
142 /// Label font size, pixels.
143 pub font_size: Option<i32>,
144}
145
146/// Options for [`Screencast::show_chapter`].
147#[derive(Debug, Default, Clone)]
148pub struct ChapterOptions {
149 /// Optional second line under the chapter title.
150 pub description: Option<String>,
151 /// How long the chapter card stays on screen (milliseconds).
152 pub duration: Option<f64>,
153}
154
155/// Options for [`Screencast::show_overlay`].
156#[derive(Debug, Default, Clone)]
157pub struct ShowOverlayOptions {
158 /// How long the overlay stays on screen (milliseconds).
159 pub duration: Option<f64>,
160}
161
162/// Identifier for an active HTML overlay; pass to
163/// [`Screencast::remove_overlay`] to dismiss the overlay before its
164/// duration expires.
165#[derive(Debug, Clone, PartialEq, Eq)]
166pub struct OverlayId(pub String);
167
168/// Live frame-streaming entry point. Obtained from
169/// [`Page::screencast`](crate::protocol::Page::screencast).
170#[derive(Clone)]
171pub struct Screencast {
172 page: Page,
173}
174
175impl Screencast {
176 pub(crate) fn new(page: Page) -> Self {
177 Self { page }
178 }
179
180 /// Begin streaming. Frames arrive on handlers registered via
181 /// [`on_frame`](Self::on_frame); register them before calling
182 /// `start` so no frames are missed.
183 ///
184 /// If `options.path` is set, the screencast is also recorded to
185 /// disk; the file is written when [`stop`](Self::stop) is called.
186 #[tracing::instrument(level = "info", skip_all, fields(page_guid = %self.page.guid()))]
187 pub async fn start(&self, options: ScreencastStartOptions) -> Result<()> {
188 self.page.screencast_start(options).await
189 }
190
191 /// Stop the screencast. If `start` was called with a `path`, the
192 /// recorded file is written to that path before this call returns.
193 #[tracing::instrument(level = "info", skip_all, fields(page_guid = %self.page.guid()))]
194 pub async fn stop(&self) -> Result<()> {
195 self.page.screencast_stop().await
196 }
197
198 /// Register a handler for incoming frames. Multiple handlers may be
199 /// registered; they fire in order for each frame.
200 pub fn on_frame<F, Fut>(&self, handler: F)
201 where
202 F: Fn(ScreencastFrame) -> Fut + Send + Sync + 'static,
203 Fut: std::future::Future<Output = Result<()>> + Send + 'static,
204 {
205 self.page.screencast_on_frame(handler);
206 }
207
208 /// Overlay action labels on the streamed frames as actions occur.
209 /// Pair with [`hide_actions`](Self::hide_actions) to stop.
210 #[tracing::instrument(level = "debug", skip_all, fields(page_guid = %self.page.guid()))]
211 pub async fn show_actions(&self, options: ShowActionsOptions) -> Result<()> {
212 self.page.screencast_show_actions(options).await
213 }
214
215 /// Stop overlaying action labels. No-op if not currently shown.
216 #[tracing::instrument(level = "debug", skip_all, fields(page_guid = %self.page.guid()))]
217 pub async fn hide_actions(&self) -> Result<()> {
218 self.page.screencast_hide_actions().await
219 }
220
221 /// Show a chapter card with the given title (and optional
222 /// description). Useful for splitting a session into named phases
223 /// for an agent's video log.
224 #[tracing::instrument(level = "debug", skip_all, fields(page_guid = %self.page.guid(), title = %title))]
225 pub async fn show_chapter(&self, title: &str, options: ChapterOptions) -> Result<()> {
226 self.page.screencast_chapter(title, options).await
227 }
228
229 /// Render arbitrary HTML as an overlay. Returns an [`OverlayId`]
230 /// you can pass to [`remove_overlay`](Self::remove_overlay) to
231 /// dismiss it early; otherwise it dismisses itself after
232 /// `options.duration` (if set) or stays until removed.
233 #[tracing::instrument(level = "debug", skip_all, fields(page_guid = %self.page.guid()))]
234 pub async fn show_overlay(&self, html: &str, options: ShowOverlayOptions) -> Result<OverlayId> {
235 self.page.screencast_show_overlay(html, options).await
236 }
237
238 /// Remove an overlay previously created via
239 /// [`show_overlay`](Self::show_overlay). Idempotent.
240 #[tracing::instrument(level = "debug", skip_all, fields(page_guid = %self.page.guid()))]
241 pub async fn remove_overlay(&self, id: OverlayId) -> Result<()> {
242 self.page.screencast_remove_overlay(id).await
243 }
244
245 /// Toggle visibility of all currently-shown overlays without
246 /// removing them. Useful for hiding overlays during a section the
247 /// agent considers "noise" and re-showing them later.
248 #[tracing::instrument(level = "debug", skip_all, fields(page_guid = %self.page.guid(), visible))]
249 pub async fn set_overlay_visible(&self, visible: bool) -> Result<()> {
250 self.page.screencast_set_overlay_visible(visible).await
251 }
252}