Files
chrome-use/cli/src/native/stream/cdp_loop.rs
T
2164e71c30 fix: use custom viewport dimensions in streaming frame metadata and image resolution (#1033)
* fix: use custom viewport dimensions in streaming frame metadata

  CDP's Page.screencastFrame metadata returns physical device dimensions
  instead of the emulated viewport, causing frame messages to report
  incorrect deviceWidth/deviceHeight when a custom viewport is set.

  Use the viewport dimensions captured at screencast start instead of
  the CDP metadata values, since the screencast image is already captured
  at the configured viewport size.

  Closes #1031

* fix: resize browser content area on viewport change for correct
  screencast dimensions

  Emulation.setDeviceMetricsOverride only changes the CSS viewport, but
  screencast captures the actual browser content area. This caused frame
  images to have incorrect dimensions (e.g., 1000x451 instead of
  1000x1000)
  when a custom viewport was set.

  - Call Browser.setContentsSize after setDeviceMetricsOverride so the
    content area matches the emulated viewport
  - Restart active screencast when viewport dimensions change so
    maxWidth/maxHeight parameters are updated
  - Skip redundant screencast restarts when dimensions are unchanged
  - Extend E2E test to verify actual JPEG image dimensions, not just
    metadata

* fix: pass viewport dimensions to --window-size at launch and log setContentsSize failures

- Add viewport_size to LaunchOptions so --window-size matches the
  configured viewport from the start, reducing reliance on the
  experimental Browser.setContentsSize CDP call at runtime
- Log Browser.setContentsSize failures instead of silently ignoring
  them with let _ =

* fix: remove duplicate viewport change detection block (dead code from merge)

* fix: use log::debug! instead of eprintln! for setContentsSize failure

* revert: use eprintln! instead of log crate for setContentsSize failure

The daemon's stderr pipe is closed after startup, so log crate
subscribers cannot output during normal operation. eprintln! is
visible during startup and in tests, matching the existing convention.

---------

Co-authored-by: hyunjinee <leehj0110@kakao.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 01:28:36 -05:00

326 lines
16 KiB
Rust

use serde_json::{json, Value};
use std::sync::Arc;
use tokio::sync::{broadcast, watch, Mutex, RwLock};
use crate::native::cdp::client::CdpClient;
use crate::native::network;
use super::timestamp_ms;
/// Background task that subscribes to CDP events and broadcasts screencast frames in real-time.
/// Also handles auto-start/stop of screencast based on WebSocket client count.
#[allow(clippy::too_many_arguments)]
pub(super) async fn cdp_event_loop(
frame_tx: broadcast::Sender<String>,
client_slot: Arc<RwLock<Option<Arc<CdpClient>>>>,
client_notify: Arc<tokio::sync::Notify>,
screencasting: Arc<Mutex<bool>>,
client_count: Arc<Mutex<usize>>,
cdp_session_id: Arc<RwLock<Option<String>>>,
viewport_width: Arc<Mutex<u32>>,
viewport_height: Arc<Mutex<u32>>,
last_frame: Arc<RwLock<Option<String>>>,
last_tabs: Arc<RwLock<Vec<Value>>>,
last_engine: Arc<RwLock<String>>,
recording: Arc<Mutex<bool>>,
mut shutdown_rx: watch::Receiver<bool>,
) {
loop {
tokio::select! {
changed = shutdown_rx.changed() => {
if changed.is_err() || *shutdown_rx.borrow() {
let session_id = cdp_session_id.read().await.clone();
if *screencasting.lock().await {
if let Some(ref client) = *client_slot.read().await {
let _ = client
.send_command_no_params("Page.stopScreencast", session_id.as_deref())
.await;
}
let mut sc = screencasting.lock().await;
*sc = false;
}
return;
}
}
_ = client_notify.notified() => {}
}
let count = *client_count.lock().await;
let guard = client_slot.read().await;
if count > 0 {
if let Some(ref client) = *guard {
let mut event_rx = client.subscribe();
let client_arc = Arc::clone(client);
drop(guard);
let session_id = cdp_session_id.read().await.clone();
let vw = *viewport_width.lock().await;
let vh = *viewport_height.lock().await;
let eng = last_engine.read().await.clone();
let supports_screencast = eng == "chrome";
if supports_screencast {
let _ = client_arc
.send_command(
"Page.startScreencast",
Some(json!({
"format": "jpeg",
"quality": 80,
"maxWidth": vw,
"maxHeight": vh,
"everyNthFrame": 1,
})),
session_id.as_deref(),
)
.await;
}
{
let mut sc = screencasting.lock().await;
*sc = supports_screencast;
}
let rec = *recording.lock().await;
let status = json!({
"type": "status",
"connected": true,
"screencasting": supports_screencast,
"viewportWidth": vw,
"viewportHeight": vh,
"engine": eng,
"recording": rec,
});
let _ = frame_tx.send(status.to_string());
loop {
tokio::select! {
changed = shutdown_rx.changed() => {
if changed.is_err() || *shutdown_rx.borrow() {
if supports_screencast {
let session_id = cdp_session_id.read().await.clone();
let _ = client_arc
.send_command_no_params("Page.stopScreencast", session_id.as_deref())
.await;
}
let mut sc = screencasting.lock().await;
*sc = false;
return;
}
}
event = event_rx.recv() => {
match event {
Ok(evt) => {
if evt.method == "Page.frameNavigated" {
if let Some(frame) = evt.params.get("frame") {
let is_main = frame
.get("parentId")
.and_then(|v| v.as_str())
.is_none_or(|s| s.is_empty());
if is_main {
if let Some(url) = frame.get("url").and_then(|v| v.as_str()) {
{
let mut tabs = last_tabs.write().await;
for tab in tabs.iter_mut() {
if tab.get("active").and_then(|v| v.as_bool()).unwrap_or(false) {
tab.as_object_mut().map(|o| o.insert("url".to_string(), json!(url)));
}
}
}
let msg = json!({
"type": "url",
"url": url,
"timestamp": timestamp_ms(),
});
let _ = frame_tx.send(msg.to_string());
}
}
}
} else if evt.method == "Page.screencastFrame" {
if let Some(sid) = evt.params.get("sessionId").and_then(|v| v.as_i64()) {
let _ = client_arc.send_command(
"Page.screencastFrameAck",
Some(json!({ "sessionId": sid })),
evt.session_id.as_deref(),
).await;
}
if let Some(data) = evt.params.get("data").and_then(|v| v.as_str()) {
let meta = evt.params.get("metadata");
let msg = json!({
"type": "frame",
"data": data,
"metadata": {
"offsetTop": meta.and_then(|m| m.get("offsetTop")).and_then(|v| v.as_f64()).unwrap_or(0.0),
"pageScaleFactor": meta.and_then(|m| m.get("pageScaleFactor")).and_then(|v| v.as_f64()).unwrap_or(1.0),
"deviceWidth": vw,
"deviceHeight": vh,
"scrollOffsetX": meta.and_then(|m| m.get("scrollOffsetX")).and_then(|v| v.as_f64()).unwrap_or(0.0),
"scrollOffsetY": meta.and_then(|m| m.get("scrollOffsetY")).and_then(|v| v.as_f64()).unwrap_or(0.0),
"timestamp": meta.and_then(|m| m.get("timestamp")).and_then(|v| v.as_u64()).unwrap_or(0),
}
});
let msg_str = msg.to_string();
{
let mut lf = last_frame.write().await;
*lf = Some(msg_str.clone());
}
let _ = frame_tx.send(msg_str);
}
} else if evt.method == "Runtime.consoleAPICalled" {
let level = evt.params.get("type")
.and_then(|v| v.as_str())
.unwrap_or("log");
let raw_args = evt.params.get("args")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let text = network::format_console_args(&raw_args);
if !text.is_empty() {
let mut msg = json!({
"type": "console",
"level": level,
"text": text,
"timestamp": timestamp_ms(),
});
if !raw_args.is_empty() {
msg.as_object_mut().unwrap().insert(
"args".to_string(),
Value::Array(raw_args),
);
}
let _ = frame_tx.send(msg.to_string());
}
} else if evt.method == "Runtime.exceptionThrown" {
let text = evt.params.get("exceptionDetails")
.and_then(|d| {
d.get("exception")
.and_then(|e| e.get("description").and_then(|v| v.as_str()))
.or_else(|| d.get("text").and_then(|v| v.as_str()))
})
.unwrap_or("Unknown error");
let line = evt.params.get("exceptionDetails")
.and_then(|d| d.get("lineNumber").and_then(|v| v.as_i64()));
let column = evt.params.get("exceptionDetails")
.and_then(|d| d.get("columnNumber").and_then(|v| v.as_i64()));
let msg = json!({
"type": "page_error",
"text": text,
"line": line,
"column": column,
"timestamp": timestamp_ms(),
});
let _ = frame_tx.send(msg.to_string());
}
}
Err(broadcast::error::RecvError::Lagged(_)) => continue,
Err(broadcast::error::RecvError::Closed) => break,
}
}
_ = client_notify.notified() => {
let count = *client_count.lock().await;
let new_session_id = cdp_session_id.read().await.clone();
if count == 0 {
if supports_screencast {
let _ = client_arc
.send_command_no_params("Page.stopScreencast", session_id.as_deref())
.await;
}
let mut sc = screencasting.lock().await;
*sc = false;
break;
}
let client_changed = {
let guard = client_slot.read().await;
let same = guard
.as_ref()
.is_some_and(|c| Arc::ptr_eq(c, &client_arc));
!same
};
let session_changed = new_session_id != session_id;
let new_vw = *viewport_width.lock().await;
let new_vh = *viewport_height.lock().await;
let viewport_changed = new_vw != vw || new_vh != vh;
if client_changed || session_changed || viewport_changed {
if supports_screencast {
let _ = client_arc
.send_command_no_params("Page.stopScreencast", session_id.as_deref())
.await;
}
let mut sc = screencasting.lock().await;
*sc = false;
client_notify.notify_one();
break;
}
}
}
}
} else {
drop(guard);
}
} else {
let was_screencasting = *screencasting.lock().await;
if was_screencasting {
if let Some(ref client) = *guard {
let session_id = cdp_session_id.read().await.clone();
let _ = client
.send_command_no_params("Page.stopScreencast", session_id.as_deref())
.await;
}
let mut sc = screencasting.lock().await;
*sc = false;
}
drop(guard);
}
}
}
pub async fn start_screencast(
client: &CdpClient,
session_id: &str,
format: &str,
quality: i32,
max_width: i32,
max_height: i32,
) -> Result<(), String> {
client
.send_command(
"Page.startScreencast",
Some(json!({
"format": format,
"quality": quality,
"maxWidth": max_width,
"maxHeight": max_height,
"everyNthFrame": 1,
})),
Some(session_id),
)
.await?;
Ok(())
}
pub async fn stop_screencast(client: &CdpClient, session_id: &str) -> Result<(), String> {
client
.send_command_no_params("Page.stopScreencast", Some(session_id))
.await?;
Ok(())
}
pub async fn ack_screencast_frame(
client: &CdpClient,
session_id: &str,
screencast_session_id: i64,
) -> Result<(), String> {
client
.send_command(
"Page.screencastFrameAck",
Some(json!({ "sessionId": screencast_session_id })),
Some(session_id),
)
.await?;
Ok(())
}