playwright_rs/protocol/video.rs
1// Copyright 2026 Paul Adamson
2// Licensed under the Apache License, Version 2.0
3//
4// Video protocol object
5//
6// Represents a video recording associated with a page.
7// Video recording is enabled via BrowserContextOptions::record_video.
8
9use crate::error::{Error, Result};
10use crate::server::channel_owner::ChannelOwner;
11use std::path::Path;
12use std::sync::{Arc, Mutex};
13
14/// Video represents a video recording of a page.
15///
16/// Video recording is enabled by passing `record_video` to
17/// `Browser::new_context_with_options()`. Each page in the context receives
18/// its own `Video` object accessible via `page.video()`.
19///
20/// The underlying recording is backed by an `Artifact` whose GUID is provided
21/// in the `Page` initializer. Methods that access the file wait for the
22/// artifact to become ready before acting — in practice this happens almost
23/// immediately, but calling `path()` or `save_as()` before the page is closed
24/// may return an error if the artifact hasn't finished writing.
25///
26/// See: <https://playwright.dev/docs/api/class-video>
27#[derive(Clone)]
28pub struct Video {
29 /// Shared state: the artifact once the "video" event fires, or an error if
30 /// the page was closed without producing frames.
31 inner: Arc<VideoInner>,
32}
33
34struct VideoInner {
35 /// Mutex-protected artifact slot; populated by `set_artifact`.
36 artifact: Mutex<Option<Arc<dyn ChannelOwner>>>,
37 /// Notifier for waiters: incremented whenever `artifact` is set.
38 notify: tokio::sync::Notify,
39}
40
41impl Video {
42 /// Creates a new `Video` shell with no artifact resolved yet.
43 pub(crate) fn new() -> Self {
44 Self {
45 inner: Arc::new(VideoInner {
46 artifact: Mutex::new(None),
47 notify: tokio::sync::Notify::new(),
48 }),
49 }
50 }
51
52 /// Called once the artifact GUID has been resolved via the connection.
53 pub(crate) fn set_artifact(&self, artifact: Arc<dyn ChannelOwner>) {
54 let mut guard = self.inner.artifact.lock().unwrap();
55 *guard = Some(artifact);
56 drop(guard);
57 self.inner.notify.notify_waiters();
58 }
59
60 /// Waits for the artifact to become available, then returns its channel.
61 ///
62 /// Polls up to ~10 seconds before giving up, matching typical Playwright timeouts.
63 async fn wait_for_artifact_channel(&self) -> Result<crate::server::channel::Channel> {
64 const POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(50);
65 const TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
66
67 let deadline = tokio::time::Instant::now() + TIMEOUT;
68
69 loop {
70 // Check if already available
71 {
72 let guard = self.inner.artifact.lock().unwrap();
73 if let Some(artifact) = guard.as_ref() {
74 return Ok(artifact.channel().clone());
75 }
76 }
77
78 if tokio::time::Instant::now() >= deadline {
79 return Err(Error::ProtocolError(
80 "Video artifact not available after 10 seconds. \
81 Close the page before calling video methods to ensure the \
82 recording is finalised."
83 .to_string(),
84 ));
85 }
86
87 // Wait for notification or poll interval, whichever comes first
88 tokio::select! {
89 _ = self.inner.notify.notified() => {}
90 _ = tokio::time::sleep(POLL_INTERVAL) => {}
91 }
92 }
93 }
94
95 /// Returns the file system path of the video recording.
96 ///
97 /// The recording is guaranteed to be written to the filesystem after the
98 /// browser context closes. This method waits up to 10 seconds for the
99 /// recording to be ready.
100 ///
101 /// See: <https://playwright.dev/docs/api/class-video#video-path>
102 pub async fn path(&self) -> Result<std::path::PathBuf> {
103 #[derive(serde::Deserialize)]
104 #[serde(rename_all = "camelCase")]
105 struct PathResponse {
106 value: String,
107 }
108
109 let channel = self.wait_for_artifact_channel().await?;
110 let resp: PathResponse = channel
111 .send("pathAfterFinished", serde_json::json!({}))
112 .await?;
113 Ok(std::path::PathBuf::from(resp.value))
114 }
115
116 /// Saves the video recording to the specified path.
117 ///
118 /// This method can be called while recording is still in progress, or after
119 /// the page has been closed. It waits up to 10 seconds for the recording to
120 /// be ready.
121 ///
122 /// See: <https://playwright.dev/docs/api/class-video#video-save-as>
123 pub async fn save_as(&self, path: impl AsRef<Path>) -> Result<()> {
124 let path_str = path
125 .as_ref()
126 .to_str()
127 .ok_or_else(|| Error::InvalidArgument("path contains invalid UTF-8".to_string()))?;
128
129 let channel = self.wait_for_artifact_channel().await?;
130 channel
131 .send_no_result("saveAs", serde_json::json!({ "path": path_str }))
132 .await
133 }
134
135 /// Deletes the video file.
136 ///
137 /// This method waits up to 10 seconds for the recording to finish before deleting.
138 ///
139 /// See: <https://playwright.dev/docs/api/class-video#video-delete>
140 pub async fn delete(&self) -> Result<()> {
141 let channel = self.wait_for_artifact_channel().await?;
142 channel
143 .send_no_result("delete", serde_json::json!({}))
144 .await
145 }
146}
147
148impl std::fmt::Debug for Video {
149 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150 f.debug_struct("Video").finish()
151 }
152}