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, client_slot: Arc>>>, client_notify: Arc, screencasting: Arc>, client_count: Arc>, cdp_session_id: Arc>>, viewport_width: Arc>, viewport_height: Arc>, last_frame: Arc>>, last_tabs: Arc>>, last_engine: Arc>, recording: Arc>, mut shutdown_rx: watch::Receiver, ) { 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(()) }