use crate::error::Result;
use crate::protocol::page::Page;
use crate::server::channel_owner::ChannelOwner;
use std::path::PathBuf;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ScreencastFrame {
pub data: bytes::Bytes,
pub timestamp: Option<f64>,
}
#[derive(Debug, Default, Clone)]
#[non_exhaustive]
pub struct ScreencastStartOptions {
pub size: Option<ScreencastSize>,
pub quality: Option<u8>,
pub path: Option<PathBuf>,
pub fps: Option<u32>,
}
impl ScreencastStartOptions {
pub fn size(mut self, size: ScreencastSize) -> Self {
self.size = Some(size);
self
}
pub fn quality(mut self, quality: u8) -> Self {
self.quality = Some(quality);
self
}
pub fn path(mut self, path: PathBuf) -> Self {
self.path = Some(path);
self
}
pub fn fps(mut self, fps: u32) -> Self {
self.fps = Some(fps);
self
}
pub(crate) fn params(&self, send_frames: bool) -> serde_json::Value {
let mut params = serde_json::json!({});
if let Some(size) = self.size {
params["size"] = serde_json::json!({
"width": size.width,
"height": size.height,
});
}
if let Some(quality) = self.quality {
params["quality"] = serde_json::json!(quality);
}
if let Some(fps) = self.fps {
params["fps"] = serde_json::json!(fps);
}
params["sendFrames"] = serde_json::json!(send_frames);
params["record"] = serde_json::json!(self.path.is_some());
params
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScreencastSize {
pub width: u32,
pub height: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ActionPosition {
TopLeft,
Top,
TopRight,
BottomLeft,
Bottom,
BottomRight,
}
impl serde::Serialize for ActionPosition {
fn serialize<S: serde::Serializer>(
&self,
serializer: S,
) -> std::result::Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl ActionPosition {
pub(crate) fn as_str(self) -> &'static str {
match self {
ActionPosition::TopLeft => "top-left",
ActionPosition::Top => "top",
ActionPosition::TopRight => "top-right",
ActionPosition::BottomLeft => "bottom-left",
ActionPosition::Bottom => "bottom",
ActionPosition::BottomRight => "bottom-right",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ActionCursor {
None,
Pointer,
}
impl serde::Serialize for ActionCursor {
fn serialize<S: serde::Serializer>(
&self,
serializer: S,
) -> std::result::Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl ActionCursor {
pub(crate) fn as_str(self) -> &'static str {
match self {
ActionCursor::None => "none",
ActionCursor::Pointer => "pointer",
}
}
}
#[derive(Debug, Default, Clone, serde::Serialize)]
#[non_exhaustive]
pub struct ShowActionsStyle {
#[serde(skip_serializing_if = "Option::is_none")]
pub point: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub highlight: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
}
impl ShowActionsStyle {
pub fn point(mut self, css: impl Into<String>) -> Self {
self.point = Some(css.into());
self
}
pub fn highlight(mut self, css: impl Into<String>) -> Self {
self.highlight = Some(css.into());
self
}
pub fn title(mut self, css: impl Into<String>) -> Self {
self.title = Some(css.into());
self
}
}
#[derive(Debug, Default, Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ShowActionsOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub position: Option<ActionPosition>,
#[deprecated(
since = "0.20.0",
note = "use `style` with a `title` such as `font-size: 16px`"
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub font_size: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cursor: Option<ActionCursor>,
#[serde(skip_serializing_if = "Option::is_none")]
pub style: Option<ShowActionsStyle>,
}
impl ShowActionsOptions {
pub fn duration(mut self, duration: f64) -> Self {
self.duration = Some(duration);
self
}
pub fn position(mut self, position: ActionPosition) -> Self {
self.position = Some(position);
self
}
#[deprecated(
since = "0.20.0",
note = "use `style` with a `title` such as `font-size: 16px`"
)]
pub fn font_size(mut self, font_size: i32) -> Self {
#[allow(deprecated)]
{
self.font_size = Some(font_size);
}
self
}
pub fn style(mut self, style: ShowActionsStyle) -> Self {
self.style = Some(style);
self
}
pub fn cursor(mut self, cursor: ActionCursor) -> Self {
self.cursor = Some(cursor);
self
}
}
#[derive(Debug, Default, Clone)]
#[non_exhaustive]
pub struct ChapterOptions {
pub description: Option<String>,
pub duration: Option<f64>,
}
impl ChapterOptions {
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn duration(mut self, duration: f64) -> Self {
self.duration = Some(duration);
self
}
}
#[derive(Debug, Default, Clone)]
#[non_exhaustive]
pub struct ShowOverlayOptions {
pub duration: Option<f64>,
}
impl ShowOverlayOptions {
pub fn duration(mut self, duration: f64) -> Self {
self.duration = Some(duration);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OverlayId(pub String);
#[derive(Clone)]
pub struct Screencast {
page: Page,
}
impl Screencast {
pub(crate) fn new(page: Page) -> Self {
Self { page }
}
#[tracing::instrument(level = "info", skip_all, fields(page_guid = %self.page.guid()))]
pub async fn start(&self, options: ScreencastStartOptions) -> Result<()> {
self.page.screencast_start(options).await
}
#[tracing::instrument(level = "info", skip_all, fields(page_guid = %self.page.guid()))]
pub async fn stop(&self) -> Result<()> {
self.page.screencast_stop().await
}
pub fn on_frame<F, Fut>(&self, handler: F)
where
F: Fn(ScreencastFrame) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<()>> + Send + 'static,
{
self.page.screencast_on_frame(handler);
}
#[tracing::instrument(level = "debug", skip_all, fields(page_guid = %self.page.guid()))]
pub async fn show_actions(&self, options: ShowActionsOptions) -> Result<()> {
self.page.screencast_show_actions(options).await
}
#[tracing::instrument(level = "debug", skip_all, fields(page_guid = %self.page.guid()))]
pub async fn hide_actions(&self) -> Result<()> {
self.page.screencast_hide_actions().await
}
#[tracing::instrument(level = "debug", skip_all, fields(page_guid = %self.page.guid(), title = %title))]
pub async fn show_chapter(&self, title: &str, options: ChapterOptions) -> Result<()> {
self.page.screencast_chapter(title, options).await
}
#[tracing::instrument(level = "debug", skip_all, fields(page_guid = %self.page.guid()))]
pub async fn show_overlay(&self, html: &str, options: ShowOverlayOptions) -> Result<OverlayId> {
self.page.screencast_show_overlay(html, options).await
}
#[tracing::instrument(level = "debug", skip_all, fields(page_guid = %self.page.guid()))]
pub async fn remove_overlay(&self, id: OverlayId) -> Result<()> {
self.page.screencast_remove_overlay(id).await
}
#[tracing::instrument(level = "debug", skip_all, fields(page_guid = %self.page.guid(), visible))]
pub async fn set_overlay_visible(&self, visible: bool) -> Result<()> {
self.page.screencast_set_overlay_visible(visible).await
}
}
#[cfg(test)]
mod options_tests {
use super::{
ActionCursor, ActionPosition, ScreencastSize, ScreencastStartOptions, ShowActionsOptions,
ShowActionsStyle,
};
use crate::protocol_spec::{
PAGE, command_parameters, keys, mixin_properties, nested_properties,
};
use serde_json::json;
#[test]
fn show_actions_options_serialize_to_the_driver_spelling() {
let options = ShowActionsOptions::default()
.duration(500.0)
.position(ActionPosition::TopLeft)
.cursor(ActionCursor::Pointer)
.style(
ShowActionsStyle::default()
.point("width: 20px")
.highlight("outline: 2px solid #333")
.title("font-size: 16px"),
);
let sent = serde_json::to_value(&options).unwrap();
assert_eq!(
sent,
json!({
"duration": 500.0,
"position": "top-left",
"cursor": "pointer",
"style": {
"point": "width: 20px",
"highlight": "outline: 2px solid #333",
"title": "font-size: 16px"
}
})
);
assert!(keys(&sent).is_subset(&mixin_properties("ShowActionsOptions")));
assert!(keys(&sent["style"]).is_subset(&nested_properties("ShowActionsOptions", "style")));
assert_eq!(
serde_json::to_value(ShowActionsOptions::default()).unwrap(),
json!({})
);
}
#[test]
#[allow(deprecated)]
fn the_deprecated_font_size_is_still_sent() {
let sent = serde_json::to_value(ShowActionsOptions::default().font_size(14)).unwrap();
assert_eq!(sent, json!({"fontSize": 14}));
}
#[test]
fn start_params_carry_fps_and_what_the_page_needs() {
let options = ScreencastStartOptions::default()
.size(ScreencastSize {
width: 640,
height: 480,
})
.quality(80)
.fps(60)
.path("out.webm".into());
let sent = options.params(true);
assert_eq!(
sent,
json!({
"size": {"width": 640, "height": 480},
"quality": 80,
"fps": 60,
"sendFrames": true,
"record": true
})
);
assert!(keys(&sent).is_subset(&command_parameters(PAGE, "screencastStart")));
assert_eq!(
ScreencastStartOptions::default().params(false),
json!({"sendFrames": false, "record": false})
);
}
#[test]
fn start_builder_sets_quality_and_size() {
let o = ScreencastStartOptions::default()
.quality(80)
.size(ScreencastSize {
width: 1280,
height: 720,
});
assert_eq!(o.quality, Some(80));
assert_eq!(o.size.map(|s| (s.width, s.height)), Some((1280, 720)));
}
#[test]
fn action_cursor_as_str_maps_each_variant() {
assert_eq!(ActionCursor::None.as_str(), "none");
assert_eq!(ActionCursor::Pointer.as_str(), "pointer");
}
#[test]
fn action_position_as_str_maps_each_variant() {
assert_eq!(ActionPosition::TopLeft.as_str(), "top-left");
assert_eq!(ActionPosition::Top.as_str(), "top");
assert_eq!(ActionPosition::TopRight.as_str(), "top-right");
assert_eq!(ActionPosition::BottomLeft.as_str(), "bottom-left");
assert_eq!(ActionPosition::Bottom.as_str(), "bottom");
assert_eq!(ActionPosition::BottomRight.as_str(), "bottom-right");
}
#[test]
fn show_actions_builder_sets_cursor() {
let o = ShowActionsOptions::default().cursor(ActionCursor::Pointer);
assert_eq!(o.cursor, Some(ActionCursor::Pointer));
}
}