Add AI chat to dashboard, refactor stream module, snapshot --urls, batch argument mode (#1160)
* chat * refactor * fixes * fixes * fixes * fixes * improvements * download chat * batch * fixes * fixes * fixes * fmt * fixes * fixes * fixes * fmt
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
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": meta.and_then(|m| m.get("deviceWidth")).and_then(|v| v.as_u64()).unwrap_or(1280),
|
||||
"deviceHeight": meta.and_then(|m| m.get("deviceHeight")).and_then(|v| v.as_u64()).unwrap_or(720),
|
||||
"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(())
|
||||
}
|
||||
Reference in New Issue
Block a user