Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ serde_json = "1.0"
dirs = "5.0"
base64 = "0.22"
getrandom = "0.2"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "io-util", "time", "sync", "signal"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "io-util", "time", "sync", "signal", "process"] }
tokio-tungstenite = { version = "0.24", features = ["rustls-tls-webpki-roots"] }
futures-util = "0.3"
url = "2"
Expand Down
281 changes: 122 additions & 159 deletions cli/src/native/actions.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use serde_json::{json, Value};
use std::env;
use std::io::Write;
use std::sync::atomic::AtomicU64;
use std::sync::Arc;
use tokio::sync::{broadcast, RwLock};
use tokio::sync::{broadcast, oneshot, RwLock};

use super::auth;
use super::browser::{BrowserManager, WaitUntil};
Expand Down Expand Up @@ -166,6 +167,31 @@ impl DaemonState {
}
}

/// Spawn a background task that polls screenshots and pipes them to ffmpeg.
async fn start_recording_task(
&mut self,
client: Arc<CdpClient>,
session_id: String,
) -> Result<(), String> {
let shared_count = Arc::new(AtomicU64::new(0));
let (cancel_tx, cancel_rx) = oneshot::channel();
let handle = recording::spawn_recording_task(
client,
session_id,
self.recording_state.output_path.clone(),
shared_count.clone(),
cancel_rx,
);
self.recording_state.capture_task = Some(handle);
self.recording_state.shared_frame_count = Some(shared_count);
self.recording_state.cancel_tx = Some(cancel_tx);
Ok(())
}

async fn stop_recording_task(&mut self) -> Result<(), String> {
recording::stop_recording_task(&mut self.recording_state).await
}

fn drain_cdp_events(
&mut self,
) -> (
Expand Down Expand Up @@ -346,21 +372,6 @@ impl DaemonState {
}
}
"Page.screencastFrame" => {
if self.recording_state.active {
if let Some(data) =
event.params.get("data").and_then(|v| v.as_str())
{
if let Ok(bytes) = base64::Engine::decode(
&base64::engine::general_purpose::STANDARD,
data,
) {
recording::recording_add_frame(
&mut self.recording_state,
&bytes,
);
}
}
}
if let Some(sid) =
event.params.get("sessionId").and_then(|v| v.as_i64())
{
Expand Down Expand Up @@ -2574,132 +2585,111 @@ async fn handle_recording_start(cmd: &Value, state: &mut DaemonState) -> Result<
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty());

let mgr = state.browser.as_mut().ok_or("Browser not launched")?;
let old_session_id = mgr.active_session_id()?.to_string();
let (client, new_session_id) = {
let mgr = state.browser.as_mut().ok_or("Browser not launched")?;
let old_session_id = mgr.active_session_id()?.to_string();

// Capture current URL if no URL specified
let nav_url = if let Some(u) = recording_url {
u.to_string()
} else {
mgr.get_url()
.await
.unwrap_or_else(|_| "about:blank".to_string())
};
// Capture current URL if no URL specified
let nav_url = if let Some(u) = recording_url {
u.to_string()
} else {
mgr.get_url()
.await
.unwrap_or_else(|_| "about:blank".to_string())
};

// Capture current cookies
let cookies_result = mgr
.client
.send_command_no_params("Network.getAllCookies", Some(&old_session_id))
.await
.ok();
// Capture current cookies
let cookies_result = mgr
.client
.send_command_no_params("Network.getAllCookies", Some(&old_session_id))
.await
.ok();

// Create new browser context
let ctx_result = mgr
.client
.send_command_no_params("Target.createBrowserContext", None)
.await?;
let context_id = ctx_result
.get("browserContextId")
.and_then(|v| v.as_str())
.ok_or("Failed to get browserContextId")?
.to_string();
// Create new browser context
let ctx_result = mgr
.client
.send_command_no_params("Target.createBrowserContext", None)
.await?;
let context_id = ctx_result
.get("browserContextId")
.and_then(|v| v.as_str())
.ok_or("Failed to get browserContextId")?
.to_string();

// Create page in new context
let create_result: CreateTargetResult = mgr
.client
.send_command_typed(
"Target.createTarget",
&json!({ "url": "about:blank", "browserContextId": context_id }),
None,
)
.await?;
// Create page in new context
let create_result: CreateTargetResult = mgr
.client
.send_command_typed(
"Target.createTarget",
&json!({ "url": "about:blank", "browserContextId": context_id }),
None,
)
.await?;

let attach_result: AttachToTargetResult = mgr
.client
.send_command_typed(
"Target.attachToTarget",
&AttachToTargetParams {
target_id: create_result.target_id.clone(),
flatten: true,
},
None,
)
.await?;
let attach_result: AttachToTargetResult = mgr
.client
.send_command_typed(
"Target.attachToTarget",
&AttachToTargetParams {
target_id: create_result.target_id.clone(),
flatten: true,
},
None,
)
.await?;

let new_session_id = attach_result.session_id.clone();
mgr.enable_domains_pub(&new_session_id).await?;
let new_session_id = attach_result.session_id.clone();
mgr.enable_domains_pub(&new_session_id).await?;

// Transfer cookies to new context
if let Some(ref cr) = cookies_result {
if let Some(cookie_arr) = cr.get("cookies").and_then(|v| v.as_array()) {
if !cookie_arr.is_empty() {
let _ = mgr
.client
.send_command(
"Network.setCookies",
Some(json!({ "cookies": cookie_arr })),
Some(&new_session_id),
)
.await;
// Transfer cookies to new context
if let Some(ref cr) = cookies_result {
if let Some(cookie_arr) = cr.get("cookies").and_then(|v| v.as_array()) {
if !cookie_arr.is_empty() {
let _ = mgr
.client
.send_command(
"Network.setCookies",
Some(json!({ "cookies": cookie_arr })),
Some(&new_session_id),
)
.await;
}
}
}
}

// Add page and switch to it
mgr.add_page(super::browser::PageInfo {
target_id: create_result.target_id,
session_id: new_session_id.clone(),
url: nav_url.clone(),
title: String::new(),
target_type: "page".to_string(),
});
// Add page and switch to it
mgr.add_page(super::browser::PageInfo {
target_id: create_result.target_id,
session_id: new_session_id.clone(),
url: nav_url.clone(),
title: String::new(),
target_type: "page".to_string(),
});

// Navigate to URL
if nav_url != "about:blank" {
let _ = mgr
.client
.send_command(
"Page.navigate",
Some(json!({ "url": nav_url })),
Some(&new_session_id),
)
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
}
// Navigate to URL
if nav_url != "about:blank" {
let _ = mgr
.client
.send_command(
"Page.navigate",
Some(json!({ "url": nav_url })),
Some(&new_session_id),
)
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
}

let result = recording::recording_start(&mut state.recording_state, path)?;
(mgr.client.clone(), new_session_id)
};

// Start screencast on new page
stream::start_screencast(&mgr.client, &new_session_id, "jpeg", 80, 1280, 720).await?;
state.screencasting = true;
let result = recording::recording_start(&mut state.recording_state, path)?;
state.start_recording_task(client, new_session_id).await?;

Ok(result)
}

async fn handle_recording_stop(state: &mut DaemonState) -> Result<Value, String> {
// Stop screencast
if state.screencasting {
if let Some(ref browser) = state.browser {
if let Ok(session_id) = browser.active_session_id() {
let _ = stream::stop_screencast(&browser.client, session_id).await;
}
}
state.screencasting = false;
}

// Drain remaining frames before stopping
let (ack_ids, _, _, _) = state.drain_cdp_events();
if !ack_ids.is_empty() {
if let Some(ref browser) = state.browser {
if let Ok(session_id) = browser.active_session_id() {
for ack_sid in ack_ids {
let _ =
stream::ack_screencast_frame(&browser.client, session_id, ack_sid).await;
}
}
}
}

state.stop_recording_task().await?;
recording::recording_stop(&mut state.recording_state)
}

Expand All @@ -2709,22 +2699,14 @@ async fn handle_recording_restart(cmd: &Value, state: &mut DaemonState) -> Resul
.and_then(|v| v.as_str())
.ok_or("Missing 'path' parameter")?;

// Stop screencast, restart recording, start screencast again
if state.screencasting {
if let Some(ref browser) = state.browser {
if let Ok(session_id) = browser.active_session_id() {
let _ = stream::stop_screencast(&browser.client, session_id).await;
}
}
state.screencasting = false;
}

let _ = state.stop_recording_task().await;
let result = recording::recording_restart(&mut state.recording_state, path)?;

if let Some(ref browser) = state.browser {
let session_id = browser.active_session_id()?.to_string();
stream::start_screencast(&browser.client, &session_id, "jpeg", 80, 1280, 720).await?;
state.screencasting = true;
state
.start_recording_task(browser.client.clone(), session_id)
.await?;
}

Ok(result)
Expand Down Expand Up @@ -4351,8 +4333,9 @@ async fn handle_video_start(cmd: &Value, state: &mut DaemonState) -> Result<Valu
let session_id = mgr.active_session_id()?.to_string();

recording::recording_start(&mut state.recording_state, path)?;
stream::start_screencast(&mgr.client, &session_id, "jpeg", 80, 1280, 720).await?;
state.screencasting = true;
state
.start_recording_task(mgr.client.clone(), session_id)
.await?;

Ok(json!({
"started": true,
Expand All @@ -4368,27 +4351,7 @@ async fn handle_video_stop(state: &mut DaemonState) -> Result<Value, String> {
}));
}

if state.screencasting {
if let Some(ref browser) = state.browser {
if let Ok(session_id) = browser.active_session_id() {
let _ = stream::stop_screencast(&browser.client, session_id).await;
}
}
state.screencasting = false;
}

let (ack_ids, _, _, _) = state.drain_cdp_events();
if !ack_ids.is_empty() {
if let Some(ref browser) = state.browser {
if let Ok(session_id) = browser.active_session_id() {
for ack_sid in ack_ids {
let _ =
stream::ack_screencast_frame(&browser.client, session_id, ack_sid).await;
}
}
}
}

state.stop_recording_task().await?;
recording::recording_stop(&mut state.recording_state)
}

Expand Down
Loading
Loading