Skip to main content

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}