1use crate::error::Result;
65use crate::protocol::page::Page;
66use crate::server::channel_owner::ChannelOwner;
67use std::path::PathBuf;
68
69#[derive(Debug, Clone)]
80#[non_exhaustive]
81pub struct ScreencastFrame {
82 pub data: bytes::Bytes,
84 pub timestamp: Option<f64>,
87}
88
89#[derive(Debug, Default, Clone)]
91#[non_exhaustive]
92pub struct ScreencastStartOptions {
93 pub size: Option<ScreencastSize>,
96 pub quality: Option<u8>,
98 pub path: Option<PathBuf>,
105 pub fps: Option<u32>,
109}
110
111impl ScreencastStartOptions {
112 pub fn size(mut self, size: ScreencastSize) -> Self {
114 self.size = Some(size);
115 self
116 }
117 pub fn quality(mut self, quality: u8) -> Self {
119 self.quality = Some(quality);
120 self
121 }
122 pub fn path(mut self, path: PathBuf) -> Self {
124 self.path = Some(path);
125 self
126 }
127 pub fn fps(mut self, fps: u32) -> Self {
129 self.fps = Some(fps);
130 self
131 }
132
133 pub(crate) fn params(&self, send_frames: bool) -> serde_json::Value {
136 let mut params = serde_json::json!({});
137 if let Some(size) = self.size {
138 params["size"] = serde_json::json!({
139 "width": size.width,
140 "height": size.height,
141 });
142 }
143 if let Some(quality) = self.quality {
144 params["quality"] = serde_json::json!(quality);
145 }
146 if let Some(fps) = self.fps {
147 params["fps"] = serde_json::json!(fps);
148 }
149 params["sendFrames"] = serde_json::json!(send_frames);
150 params["record"] = serde_json::json!(self.path.is_some());
151 params
152 }
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub struct ScreencastSize {
158 pub width: u32,
159 pub height: u32,
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164#[non_exhaustive]
165pub enum ActionPosition {
166 TopLeft,
167 Top,
168 TopRight,
169 BottomLeft,
170 Bottom,
171 BottomRight,
172}
173
174impl serde::Serialize for ActionPosition {
175 fn serialize<S: serde::Serializer>(
176 &self,
177 serializer: S,
178 ) -> std::result::Result<S::Ok, S::Error> {
179 serializer.serialize_str(self.as_str())
180 }
181}
182
183impl ActionPosition {
184 pub(crate) fn as_str(self) -> &'static str {
185 match self {
186 ActionPosition::TopLeft => "top-left",
187 ActionPosition::Top => "top",
188 ActionPosition::TopRight => "top-right",
189 ActionPosition::BottomLeft => "bottom-left",
190 ActionPosition::Bottom => "bottom",
191 ActionPosition::BottomRight => "bottom-right",
192 }
193 }
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
198#[non_exhaustive]
199pub enum ActionCursor {
200 None,
202 Pointer,
204}
205
206impl serde::Serialize for ActionCursor {
207 fn serialize<S: serde::Serializer>(
208 &self,
209 serializer: S,
210 ) -> std::result::Result<S::Ok, S::Error> {
211 serializer.serialize_str(self.as_str())
212 }
213}
214
215impl ActionCursor {
216 pub(crate) fn as_str(self) -> &'static str {
217 match self {
218 ActionCursor::None => "none",
219 ActionCursor::Pointer => "pointer",
220 }
221 }
222}
223
224#[derive(Debug, Default, Clone, serde::Serialize)]
229#[non_exhaustive]
230pub struct ShowActionsStyle {
231 #[serde(skip_serializing_if = "Option::is_none")]
233 pub point: Option<String>,
234 #[serde(skip_serializing_if = "Option::is_none")]
236 pub highlight: Option<String>,
237 #[serde(skip_serializing_if = "Option::is_none")]
239 pub title: Option<String>,
240}
241
242impl ShowActionsStyle {
243 pub fn point(mut self, css: impl Into<String>) -> Self {
245 self.point = Some(css.into());
246 self
247 }
248 pub fn highlight(mut self, css: impl Into<String>) -> Self {
250 self.highlight = Some(css.into());
251 self
252 }
253 pub fn title(mut self, css: impl Into<String>) -> Self {
255 self.title = Some(css.into());
256 self
257 }
258}
259
260#[derive(Debug, Default, Clone, serde::Serialize)]
264#[serde(rename_all = "camelCase")]
265#[non_exhaustive]
266pub struct ShowActionsOptions {
267 #[serde(skip_serializing_if = "Option::is_none")]
269 pub duration: Option<f64>,
270 #[serde(skip_serializing_if = "Option::is_none")]
272 pub position: Option<ActionPosition>,
273 #[deprecated(
275 since = "0.20.0",
276 note = "use `style` with a `title` such as `font-size: 16px`"
277 )]
278 #[serde(skip_serializing_if = "Option::is_none")]
279 pub font_size: Option<i32>,
280 #[serde(skip_serializing_if = "Option::is_none")]
282 pub cursor: Option<ActionCursor>,
283 #[serde(skip_serializing_if = "Option::is_none")]
285 pub style: Option<ShowActionsStyle>,
286}
287
288impl ShowActionsOptions {
289 pub fn duration(mut self, duration: f64) -> Self {
291 self.duration = Some(duration);
292 self
293 }
294 pub fn position(mut self, position: ActionPosition) -> Self {
296 self.position = Some(position);
297 self
298 }
299 #[deprecated(
301 since = "0.20.0",
302 note = "use `style` with a `title` such as `font-size: 16px`"
303 )]
304 pub fn font_size(mut self, font_size: i32) -> Self {
305 #[allow(deprecated)]
306 {
307 self.font_size = Some(font_size);
308 }
309 self
310 }
311 pub fn style(mut self, style: ShowActionsStyle) -> Self {
313 self.style = Some(style);
314 self
315 }
316 pub fn cursor(mut self, cursor: ActionCursor) -> Self {
318 self.cursor = Some(cursor);
319 self
320 }
321}
322
323#[derive(Debug, Default, Clone)]
325#[non_exhaustive]
326pub struct ChapterOptions {
327 pub description: Option<String>,
329 pub duration: Option<f64>,
331}
332
333impl ChapterOptions {
334 pub fn description(mut self, description: impl Into<String>) -> Self {
336 self.description = Some(description.into());
337 self
338 }
339 pub fn duration(mut self, duration: f64) -> Self {
341 self.duration = Some(duration);
342 self
343 }
344}
345
346#[derive(Debug, Default, Clone)]
348#[non_exhaustive]
349pub struct ShowOverlayOptions {
350 pub duration: Option<f64>,
352}
353
354impl ShowOverlayOptions {
355 pub fn duration(mut self, duration: f64) -> Self {
357 self.duration = Some(duration);
358 self
359 }
360}
361
362#[derive(Debug, Clone, PartialEq, Eq)]
366pub struct OverlayId(pub String);
367
368#[derive(Clone)]
371pub struct Screencast {
372 page: Page,
373}
374
375impl Screencast {
376 pub(crate) fn new(page: Page) -> Self {
377 Self { page }
378 }
379
380 #[tracing::instrument(level = "info", skip_all, fields(page_guid = %self.page.guid()))]
387 pub async fn start(&self, options: ScreencastStartOptions) -> Result<()> {
388 self.page.screencast_start(options).await
389 }
390
391 #[tracing::instrument(level = "info", skip_all, fields(page_guid = %self.page.guid()))]
394 pub async fn stop(&self) -> Result<()> {
395 self.page.screencast_stop().await
396 }
397
398 pub fn on_frame<F, Fut>(&self, handler: F)
401 where
402 F: Fn(ScreencastFrame) -> Fut + Send + Sync + 'static,
403 Fut: std::future::Future<Output = Result<()>> + Send + 'static,
404 {
405 self.page.screencast_on_frame(handler);
406 }
407
408 #[tracing::instrument(level = "debug", skip_all, fields(page_guid = %self.page.guid()))]
411 pub async fn show_actions(&self, options: ShowActionsOptions) -> Result<()> {
412 self.page.screencast_show_actions(options).await
413 }
414
415 #[tracing::instrument(level = "debug", skip_all, fields(page_guid = %self.page.guid()))]
417 pub async fn hide_actions(&self) -> Result<()> {
418 self.page.screencast_hide_actions().await
419 }
420
421 #[tracing::instrument(level = "debug", skip_all, fields(page_guid = %self.page.guid(), title = %title))]
425 pub async fn show_chapter(&self, title: &str, options: ChapterOptions) -> Result<()> {
426 self.page.screencast_chapter(title, options).await
427 }
428
429 #[tracing::instrument(level = "debug", skip_all, fields(page_guid = %self.page.guid()))]
434 pub async fn show_overlay(&self, html: &str, options: ShowOverlayOptions) -> Result<OverlayId> {
435 self.page.screencast_show_overlay(html, options).await
436 }
437
438 #[tracing::instrument(level = "debug", skip_all, fields(page_guid = %self.page.guid()))]
441 pub async fn remove_overlay(&self, id: OverlayId) -> Result<()> {
442 self.page.screencast_remove_overlay(id).await
443 }
444
445 #[tracing::instrument(level = "debug", skip_all, fields(page_guid = %self.page.guid(), visible))]
449 pub async fn set_overlay_visible(&self, visible: bool) -> Result<()> {
450 self.page.screencast_set_overlay_visible(visible).await
451 }
452}
453
454#[cfg(test)]
455mod options_tests {
456 use super::{
457 ActionCursor, ActionPosition, ScreencastSize, ScreencastStartOptions, ShowActionsOptions,
458 ShowActionsStyle,
459 };
460 use crate::protocol_spec::{
461 PAGE, command_parameters, keys, mixin_properties, nested_properties,
462 };
463 use serde_json::json;
464
465 #[test]
466 fn show_actions_options_serialize_to_the_driver_spelling() {
467 let options = ShowActionsOptions::default()
468 .duration(500.0)
469 .position(ActionPosition::TopLeft)
470 .cursor(ActionCursor::Pointer)
471 .style(
472 ShowActionsStyle::default()
473 .point("width: 20px")
474 .highlight("outline: 2px solid #333")
475 .title("font-size: 16px"),
476 );
477 let sent = serde_json::to_value(&options).unwrap();
478 assert_eq!(
479 sent,
480 json!({
481 "duration": 500.0,
482 "position": "top-left",
483 "cursor": "pointer",
484 "style": {
485 "point": "width: 20px",
486 "highlight": "outline: 2px solid #333",
487 "title": "font-size: 16px"
488 }
489 })
490 );
491 assert!(keys(&sent).is_subset(&mixin_properties("ShowActionsOptions")));
492 assert!(keys(&sent["style"]).is_subset(&nested_properties("ShowActionsOptions", "style")));
493 assert_eq!(
494 serde_json::to_value(ShowActionsOptions::default()).unwrap(),
495 json!({})
496 );
497 }
498
499 #[test]
500 #[allow(deprecated)]
501 fn the_deprecated_font_size_is_still_sent() {
502 let sent = serde_json::to_value(ShowActionsOptions::default().font_size(14)).unwrap();
503 assert_eq!(sent, json!({"fontSize": 14}));
504 }
505
506 #[test]
507 fn start_params_carry_fps_and_what_the_page_needs() {
508 let options = ScreencastStartOptions::default()
509 .size(ScreencastSize {
510 width: 640,
511 height: 480,
512 })
513 .quality(80)
514 .fps(60)
515 .path("out.webm".into());
516 let sent = options.params(true);
517 assert_eq!(
518 sent,
519 json!({
520 "size": {"width": 640, "height": 480},
521 "quality": 80,
522 "fps": 60,
523 "sendFrames": true,
524 "record": true
525 })
526 );
527 assert!(keys(&sent).is_subset(&command_parameters(PAGE, "screencastStart")));
528 assert_eq!(
529 ScreencastStartOptions::default().params(false),
530 json!({"sendFrames": false, "record": false})
531 );
532 }
533
534 #[test]
535 fn start_builder_sets_quality_and_size() {
536 let o = ScreencastStartOptions::default()
537 .quality(80)
538 .size(ScreencastSize {
539 width: 1280,
540 height: 720,
541 });
542 assert_eq!(o.quality, Some(80));
543 assert_eq!(o.size.map(|s| (s.width, s.height)), Some((1280, 720)));
544 }
545
546 #[test]
547 fn action_cursor_as_str_maps_each_variant() {
548 assert_eq!(ActionCursor::None.as_str(), "none");
549 assert_eq!(ActionCursor::Pointer.as_str(), "pointer");
550 }
551
552 #[test]
553 fn action_position_as_str_maps_each_variant() {
554 assert_eq!(ActionPosition::TopLeft.as_str(), "top-left");
555 assert_eq!(ActionPosition::Top.as_str(), "top");
556 assert_eq!(ActionPosition::TopRight.as_str(), "top-right");
557 assert_eq!(ActionPosition::BottomLeft.as_str(), "bottom-left");
558 assert_eq!(ActionPosition::Bottom.as_str(), "bottom");
559 assert_eq!(ActionPosition::BottomRight.as_str(), "bottom-right");
560 }
561
562 #[test]
563 fn show_actions_builder_sets_cursor() {
564 let o = ShowActionsOptions::default().cursor(ActionCursor::Pointer);
565 assert_eq!(o.cursor, Some(ActionCursor::Pointer));
566 }
567}