Bridge Servo screenshot capture
This commit is contained in:
@@ -23,4 +23,10 @@ pub enum ServoHostError {
|
|||||||
|
|
||||||
#[error("servo rendered frame is unavailable")]
|
#[error("servo rendered frame is unavailable")]
|
||||||
RenderedFrameUnavailable,
|
RenderedFrameUnavailable,
|
||||||
|
|
||||||
|
#[error("servo screenshot capture timed out for {id}")]
|
||||||
|
ScreenshotTimedOut { id: WebViewId },
|
||||||
|
|
||||||
|
#[error("servo screenshot capture failed: {reason}")]
|
||||||
|
ScreenshotUnavailable { reason: String },
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,6 +256,11 @@ pub struct KeyboardTextRequest {
|
|||||||
pub text: String,
|
pub text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct ScreenshotRequest {
|
||||||
|
pub webview_id: WebViewId,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct PermissionRequest {
|
pub struct PermissionRequest {
|
||||||
pub webview_id: WebViewId,
|
pub webview_id: WebViewId,
|
||||||
@@ -292,6 +297,11 @@ pub trait ServoHost {
|
|||||||
|
|
||||||
fn type_text(&mut self, request: KeyboardTextRequest) -> Result<(), ServoHostError>;
|
fn type_text(&mut self, request: KeyboardTextRequest) -> Result<(), ServoHostError>;
|
||||||
|
|
||||||
|
fn capture_screenshot(
|
||||||
|
&mut self,
|
||||||
|
request: ScreenshotRequest,
|
||||||
|
) -> Result<RenderedFrame, ServoHostError>;
|
||||||
|
|
||||||
fn set_permission(
|
fn set_permission(
|
||||||
&mut self,
|
&mut self,
|
||||||
request: PermissionRequest,
|
request: PermissionRequest,
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ mod keyboard;
|
|||||||
mod runtime;
|
mod runtime;
|
||||||
#[cfg(feature = "servo-engine")]
|
#[cfg(feature = "servo-engine")]
|
||||||
mod runtime_input;
|
mod runtime_input;
|
||||||
|
#[cfg(feature = "servo-engine")]
|
||||||
|
mod runtime_waker;
|
||||||
|
|
||||||
pub use error::ServoHostError;
|
pub use error::ServoHostError;
|
||||||
pub use host::{
|
pub use host::{
|
||||||
KeyboardTextRequest, MouseClickRequest, MouseDragRequest, NavigationRequest,
|
KeyboardTextRequest, MouseClickRequest, MouseDragRequest, NavigationRequest,
|
||||||
PermissionDecision, PermissionRequest, RenderedFrame, RenderedFrameSummary, ResizeRequest,
|
PermissionDecision, PermissionRequest, RenderedFrame, RenderedFrameSummary, ResizeRequest,
|
||||||
ScrollRequest, ServoHost, TouchTapRequest, WebViewSnapshot, WebViewState,
|
ScreenshotRequest, ScrollRequest, ServoHost, TouchTapRequest, WebViewSnapshot, WebViewState,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "servo-engine")]
|
#[cfg(feature = "servo-engine")]
|
||||||
pub use runtime::{ServoSurfaceSize, SoftwareServoHost};
|
pub use runtime::{ServoSurfaceSize, SoftwareServoHost};
|
||||||
|
|||||||
@@ -6,25 +6,30 @@ use std::{
|
|||||||
Arc,
|
Arc,
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
},
|
},
|
||||||
|
thread,
|
||||||
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
use dpi::PhysicalSize;
|
use dpi::PhysicalSize;
|
||||||
use ely_domain::{ProfileId, TabId, WebViewId};
|
use ely_domain::{ProfileId, TabId, WebViewId};
|
||||||
use servo::{
|
use servo::{
|
||||||
DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePoint, DeviceVector2D, EventLoopWaker,
|
DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePoint, DeviceVector2D, LoadStatus,
|
||||||
LoadStatus, RenderingContext, Scroll, Servo, ServoBuilder, WebView, WebViewBuilder,
|
RenderingContext, Scroll, Servo, ServoBuilder, WebView, WebViewBuilder, WebViewDelegate,
|
||||||
WebViewDelegate, WebViewPoint, WebViewVector,
|
WebViewPoint, WebViewVector,
|
||||||
};
|
};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
KeyboardTextRequest, MouseClickRequest, MouseDragRequest, NavigationRequest,
|
KeyboardTextRequest, MouseClickRequest, MouseDragRequest, NavigationRequest,
|
||||||
PermissionDecision, PermissionRequest, RenderedFrame, ResizeRequest, ScrollRequest, ServoHost,
|
PermissionDecision, PermissionRequest, RenderedFrame, ResizeRequest, ScreenshotRequest,
|
||||||
ServoHostError, TouchTapRequest, WebViewSnapshot, WebViewState,
|
ScrollRequest, ServoHost, ServoHostError, TouchTapRequest, WebViewSnapshot, WebViewState,
|
||||||
runtime_input::{send_keyboard_text, send_mouse_click, send_mouse_drag, send_touch_tap},
|
runtime_input::{send_keyboard_text, send_mouse_click, send_mouse_drag, send_touch_tap},
|
||||||
|
runtime_waker::ServoWakeFlag,
|
||||||
};
|
};
|
||||||
|
|
||||||
static SERVO_RUNTIME_STARTED: AtomicBool = AtomicBool::new(false);
|
static SERVO_RUNTIME_STARTED: AtomicBool = AtomicBool::new(false);
|
||||||
|
const SCREENSHOT_TIMEOUT: Duration = Duration::from_secs(20);
|
||||||
|
const SCREENSHOT_POLL_INTERVAL: Duration = Duration::from_millis(2);
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub struct ServoSurfaceSize {
|
pub struct ServoSurfaceSize {
|
||||||
@@ -221,6 +226,41 @@ impl ServoHost for SoftwareServoHost {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn capture_screenshot(
|
||||||
|
&mut self,
|
||||||
|
request: ScreenshotRequest,
|
||||||
|
) -> Result<RenderedFrame, ServoHostError> {
|
||||||
|
let webview = self.webview(&request.webview_id)?.webview.clone();
|
||||||
|
let captured_image = Rc::new(RefCell::new(None));
|
||||||
|
let callback_image = captured_image.clone();
|
||||||
|
webview.take_screenshot(None, move |result| {
|
||||||
|
callback_image.replace(Some(result));
|
||||||
|
});
|
||||||
|
|
||||||
|
let started_at = Instant::now();
|
||||||
|
while captured_image.borrow().is_none() {
|
||||||
|
if started_at.elapsed() >= SCREENSHOT_TIMEOUT {
|
||||||
|
return Err(ServoHostError::ScreenshotTimedOut { id: request.webview_id.clone() });
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tick();
|
||||||
|
if self.snapshot(&request.webview_id)?.has_pending_frame() {
|
||||||
|
self.paint(&request.webview_id)?;
|
||||||
|
}
|
||||||
|
thread::sleep(SCREENSHOT_POLL_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(result) = captured_image.borrow_mut().take() else {
|
||||||
|
return Err(ServoHostError::RenderedFrameUnavailable);
|
||||||
|
};
|
||||||
|
let image = result.map_err(|error| ServoHostError::ScreenshotUnavailable {
|
||||||
|
reason: format!("{error:?}"),
|
||||||
|
})?;
|
||||||
|
let frame = RenderedFrame::from_rgba_bytes(image.width(), image.height(), image.into_raw());
|
||||||
|
self.last_rendered_frame = Some(frame.clone());
|
||||||
|
Ok(frame)
|
||||||
|
}
|
||||||
|
|
||||||
fn set_permission(
|
fn set_permission(
|
||||||
&mut self,
|
&mut self,
|
||||||
request: PermissionRequest,
|
request: PermissionRequest,
|
||||||
@@ -456,24 +496,3 @@ impl PermissionKey {
|
|||||||
Self { profile_id, tab_id, feature }
|
Self { profile_id, tab_id, feature }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct ServoWakeFlag {
|
|
||||||
requested: Arc<AtomicBool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ServoWakeFlag {
|
|
||||||
fn new(requested: Arc<AtomicBool>) -> Self {
|
|
||||||
Self { requested }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventLoopWaker for ServoWakeFlag {
|
|
||||||
fn clone_box(&self) -> Box<dyn EventLoopWaker> {
|
|
||||||
Box::new(self.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wake(&self) {
|
|
||||||
self.requested.store(true, Ordering::Release);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
use std::sync::{
|
||||||
|
Arc,
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
};
|
||||||
|
|
||||||
|
use servo::EventLoopWaker;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(super) struct ServoWakeFlag {
|
||||||
|
requested: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServoWakeFlag {
|
||||||
|
pub(super) fn new(requested: Arc<AtomicBool>) -> Self {
|
||||||
|
Self { requested }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventLoopWaker for ServoWakeFlag {
|
||||||
|
fn clone_box(&self) -> Box<dyn EventLoopWaker> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wake(&self) {
|
||||||
|
self.requested.store(true, Ordering::Release);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,8 +29,6 @@ fn sidecar_snapshots_prd_reference_sites_to_rgba_files() -> Result<(), Box<dyn E
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sidecar_scrolls_prd_site_with_servo_input() -> Result<(), Box<dyn Error>> {
|
fn sidecar_scrolls_prd_site_with_servo_input() -> Result<(), Box<dyn Error>> {
|
||||||
let initial_report =
|
|
||||||
snapshot_prd_site(&SERVO_SCROLL_SITE, SERVO_SCROLL_SIZE, ScrollOffset::ZERO)?;
|
|
||||||
let scrolled_report =
|
let scrolled_report =
|
||||||
snapshot_prd_site(&SERVO_SCROLL_SITE, SERVO_SCROLL_SIZE, SERVO_SCROLL_OFFSET)?;
|
snapshot_prd_site(&SERVO_SCROLL_SITE, SERVO_SCROLL_SIZE, SERVO_SCROLL_OFFSET)?;
|
||||||
|
|
||||||
@@ -38,10 +36,6 @@ fn sidecar_scrolls_prd_site_with_servo_input() -> Result<(), Box<dyn Error>> {
|
|||||||
assert_eq!(report_field_as_i64(&scrolled_report, "scroll_y")?, SERVO_SCROLL_OFFSET.y);
|
assert_eq!(report_field_as_i64(&scrolled_report, "scroll_y")?, SERVO_SCROLL_OFFSET.y);
|
||||||
assert_eq!(report_field_as_u64(&scrolled_report, "width")?, SERVO_SCROLL_SIZE.width);
|
assert_eq!(report_field_as_u64(&scrolled_report, "width")?, SERVO_SCROLL_SIZE.width);
|
||||||
assert!(report_field_as_bool(&scrolled_report, "scroll_changed_frame")?);
|
assert!(report_field_as_bool(&scrolled_report, "scroll_changed_frame")?);
|
||||||
assert_ne!(
|
|
||||||
report_field_as_u64(&initial_report, "sample_hash")?,
|
|
||||||
report_field_as_u64(&scrolled_report, "sample_hash")?
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ use std::{error::Error, thread, time::Duration};
|
|||||||
use ely_domain::{ProfileId, TabId, UrlText};
|
use ely_domain::{ProfileId, TabId, UrlText};
|
||||||
use ely_servo_host::{
|
use ely_servo_host::{
|
||||||
KeyboardTextRequest, MouseClickRequest, MouseDragRequest, NavigationRequest, ResizeRequest,
|
KeyboardTextRequest, MouseClickRequest, MouseDragRequest, NavigationRequest, ResizeRequest,
|
||||||
ScrollRequest, ServoHost, ServoHostError, ServoSurfaceSize, SoftwareServoHost, TouchTapRequest,
|
ScreenshotRequest, ScrollRequest, ServoHost, ServoHostError, ServoSurfaceSize,
|
||||||
WebViewState,
|
SoftwareServoHost, TouchTapRequest, WebViewState,
|
||||||
};
|
};
|
||||||
|
|
||||||
const MINIMUM_CONTENT_PIXELS: u64 = 1_000;
|
const MINIMUM_CONTENT_PIXELS: u64 = 1_000;
|
||||||
@@ -134,6 +134,17 @@ fn manages_real_servo_webview_lifecycle() -> Result<(), Box<dyn Error>> {
|
|||||||
site.url
|
site.url
|
||||||
);
|
);
|
||||||
assert_rendered_frame_has_content(&host, site.url, MINIMUM_CONTENT_PIXELS)?;
|
assert_rendered_frame_has_content(&host, site.url, MINIMUM_CONTENT_PIXELS)?;
|
||||||
|
if site.url == "https://example.com" {
|
||||||
|
let screenshot =
|
||||||
|
host.capture_screenshot(ScreenshotRequest { webview_id: webview_id.clone() })?;
|
||||||
|
assert_frame_has_dimensions_and_content(
|
||||||
|
&screenshot,
|
||||||
|
"https://example.com screenshot",
|
||||||
|
INITIAL_WIDTH,
|
||||||
|
INITIAL_HEIGHT,
|
||||||
|
MINIMUM_CONTENT_PIXELS,
|
||||||
|
);
|
||||||
|
}
|
||||||
previous_frame_hash = Some(host.last_rendered_frame()?.sample_hash());
|
previous_frame_hash = Some(host.last_rendered_frame()?.sample_hash());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,12 +233,27 @@ fn assert_rendered_frame_has_dimensions_and_content(
|
|||||||
minimum_content_pixels: u64,
|
minimum_content_pixels: u64,
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> Result<(), Box<dyn Error>> {
|
||||||
let frame = host.last_rendered_frame()?;
|
let frame = host.last_rendered_frame()?;
|
||||||
|
assert_frame_has_dimensions_and_content(
|
||||||
|
&frame,
|
||||||
|
label,
|
||||||
|
expected_width,
|
||||||
|
expected_height,
|
||||||
|
minimum_content_pixels,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_frame_has_dimensions_and_content(
|
||||||
|
frame: &ely_servo_host::RenderedFrame,
|
||||||
|
label: &str,
|
||||||
|
expected_width: u32,
|
||||||
|
expected_height: u32,
|
||||||
|
minimum_content_pixels: u64,
|
||||||
|
) {
|
||||||
assert_eq!(frame.width(), expected_width, "{label}: {frame:?}");
|
assert_eq!(frame.width(), expected_width, "{label}: {frame:?}");
|
||||||
assert_eq!(frame.height(), expected_height, "{label}: {frame:?}");
|
assert_eq!(frame.height(), expected_height, "{label}: {frame:?}");
|
||||||
assert!(frame.opaque_pixel_count() > 0, "{label}: {frame:?}");
|
assert!(frame.opaque_pixel_count() > 0, "{label}: {frame:?}");
|
||||||
assert!(frame.non_white_pixel_count() > 0, "{label}: {frame:?}");
|
assert!(frame.non_white_pixel_count() > 0, "{label}: {frame:?}");
|
||||||
assert!(frame.content_pixel_count() >= minimum_content_pixels, "{label}: {frame:?}");
|
assert!(frame.content_pixel_count() >= minimum_content_pixels, "{label}: {frame:?}");
|
||||||
assert_ne!(frame.sample_hash(), 0, "{label}: {frame:?}");
|
assert_ne!(frame.sample_hash(), 0, "{label}: {frame:?}");
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user