Skip to content

Commit 3cb22ad

Browse files
Merge pull request #1395 from CapSoftware/screenshots
2 parents 2330c53 + 48f7aeb commit 3cb22ad

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+8401
-341
lines changed

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"Bash(pnpm typecheck:*)",
55
"Bash(pnpm lint:*)",
66
"Bash(pnpm build:*)",
7-
"Bash(cargo check:*)"
7+
"Bash(cargo check:*)",
8+
"Bash(cargo fmt:*)"
89
],
910
"deny": [],
1011
"ask": []

Cargo.lock

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/cli/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ impl Export {
172172
// print!("\rrendered frame {f}");
173173

174174
stdout.flush().unwrap();
175+
true
175176
})
176177
.await
177178
.map_err(|v| format!("Exporter error: {v}"))?;

apps/desktop/src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "cap-desktop"
3-
version = "0.3.84"
3+
version = "0.4.0"
44
description = "Beautiful screen recordings, owned by you."
55
authors = ["you"]
66
edition = "2024"

apps/desktop/src-tauri/src/camera_legacy.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ use tokio_util::sync::CancellationToken;
55
use crate::frame_ws::{WSFrame, create_frame_ws};
66

77
pub async fn create_camera_preview_ws() -> (Sender<FFmpegVideoFrame>, u16, CancellationToken) {
8-
let (camera_tx, mut _camera_rx) = flume::bounded::<FFmpegVideoFrame>(4);
9-
let (_camera_tx, camera_rx) = flume::bounded::<WSFrame>(4);
8+
let (camera_tx, camera_rx) = flume::bounded::<FFmpegVideoFrame>(4);
9+
let (frame_tx, _) = tokio::sync::broadcast::channel::<WSFrame>(4);
10+
let frame_tx_clone = frame_tx.clone();
1011
std::thread::spawn(move || {
1112
use ffmpeg::format::Pixel;
1213

1314
let mut converter: Option<(Pixel, ffmpeg::software::scaling::Context)> = None;
1415

15-
while let Ok(raw_frame) = _camera_rx.recv() {
16+
while let Ok(raw_frame) = camera_rx.recv() {
1617
let mut frame = raw_frame.inner;
1718

1819
if frame.format() != Pixel::RGBA || frame.width() > 1280 || frame.height() > 720 {
@@ -55,7 +56,7 @@ pub async fn create_camera_preview_ws() -> (Sender<FFmpegVideoFrame>, u16, Cance
5556
frame = new_frame;
5657
}
5758

58-
_camera_tx
59+
frame_tx_clone
5960
.send(WSFrame {
6061
data: frame.data(0).to_vec(),
6162
width: frame.width(),
@@ -66,7 +67,7 @@ pub async fn create_camera_preview_ws() -> (Sender<FFmpegVideoFrame>, u16, Cance
6667
}
6768
});
6869
// _shutdown needs to be kept alive to keep the camera ws running
69-
let (camera_ws_port, _shutdown) = create_frame_ws(camera_rx.clone()).await;
70+
let (camera_ws_port, _shutdown) = create_frame_ws(frame_tx).await;
7071

7172
(camera_tx, camera_ws_port, _shutdown)
7273
}

apps/desktop/src-tauri/src/editor_window.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::{collections::HashMap, ops::Deref, path::PathBuf, sync::Arc};
22
use tauri::{Manager, Runtime, Window, ipc::CommandArg};
3-
use tokio::sync::RwLock;
3+
use tokio::sync::{RwLock, broadcast};
44
use tokio_util::sync::CancellationToken;
55

66
use crate::{
@@ -88,9 +88,9 @@ impl EditorInstances {
8888

8989
match instances.entry(window.label().to_string()) {
9090
Entry::Vacant(entry) => {
91-
let (frame_tx, frame_rx) = flume::bounded(4);
91+
let (frame_tx, _) = broadcast::channel(4);
9292

93-
let (ws_port, ws_shutdown_token) = create_frame_ws(frame_rx).await;
93+
let (ws_port, ws_shutdown_token) = create_frame_ws(frame_tx.clone()).await;
9494
let instance = create_editor_instance_impl(
9595
window.app_handle(),
9696
path,

apps/desktop/src-tauri/src/export.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,21 +50,25 @@ pub async fn export_video(
5050
settings
5151
.export(exporter_base, move |frame_index| {
5252
// Ensure progress never exceeds total frames
53-
let _ = progress.send(FramesRendered {
54-
rendered_count: (frame_index + 1).min(total_frames),
55-
total_frames,
56-
});
53+
progress
54+
.send(FramesRendered {
55+
rendered_count: (frame_index + 1).min(total_frames),
56+
total_frames,
57+
})
58+
.is_ok()
5759
})
5860
.await
5961
}
6062
ExportSettings::Gif(settings) => {
6163
settings
6264
.export(exporter_base, move |frame_index| {
6365
// Ensure progress never exceeds total frames
64-
let _ = progress.send(FramesRendered {
65-
rendered_count: (frame_index + 1).min(total_frames),
66-
total_frames,
67-
});
66+
progress
67+
.send(FramesRendered {
68+
rendered_count: (frame_index + 1).min(total_frames),
69+
total_frames,
70+
})
71+
.is_ok()
6872
})
6973
.await
7074
}

apps/desktop/src-tauri/src/frame_ws.rs

Lines changed: 136 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
use std::sync::Arc;
2-
3-
use flume::Receiver;
1+
use tokio::sync::{broadcast, watch};
42
use tokio_util::sync::CancellationToken;
53

4+
#[derive(Clone)]
65
pub struct WSFrame {
76
pub data: Vec<u8>,
87
pub width: u32,
98
pub height: u32,
109
pub stride: u32,
1110
}
1211

13-
pub async fn create_frame_ws(frame_rx: Receiver<WSFrame>) -> (u16, CancellationToken) {
12+
pub async fn create_watch_frame_ws(
13+
frame_rx: watch::Receiver<Option<WSFrame>>,
14+
) -> (u16, CancellationToken) {
1415
use axum::{
1516
extract::{
1617
State,
@@ -19,9 +20,8 @@ pub async fn create_frame_ws(frame_rx: Receiver<WSFrame>) -> (u16, CancellationT
1920
response::IntoResponse,
2021
routing::get,
2122
};
22-
use tokio::sync::Mutex;
2323

24-
type RouterState = Arc<Mutex<Receiver<WSFrame>>>;
24+
type RouterState = watch::Receiver<Option<WSFrame>>;
2525

2626
#[axum::debug_handler]
2727
async fn ws_handler(
@@ -31,19 +31,136 @@ pub async fn create_frame_ws(frame_rx: Receiver<WSFrame>) -> (u16, CancellationT
3131
ws.on_upgrade(move |socket| handle_socket(socket, state))
3232
}
3333

34-
async fn handle_socket(mut socket: WebSocket, state: RouterState) {
35-
let camera_rx = state.lock().await;
34+
async fn handle_socket(mut socket: WebSocket, mut camera_rx: RouterState) {
3635
println!("socket connection established");
3736
tracing::info!("Socket connection established");
3837
let now = std::time::Instant::now();
3938

39+
// Send the current frame immediately upon connection (if one exists)
40+
// This ensures the client doesn't wait for the next config change to see the image
41+
{
42+
let frame_opt = camera_rx.borrow().clone();
43+
if let Some(mut frame) = frame_opt {
44+
frame.data.extend_from_slice(&frame.stride.to_le_bytes());
45+
frame.data.extend_from_slice(&frame.height.to_le_bytes());
46+
frame.data.extend_from_slice(&frame.width.to_le_bytes());
47+
48+
if let Err(e) = socket.send(Message::Binary(frame.data)).await {
49+
tracing::error!("Failed to send initial frame to socket: {:?}", e);
50+
return;
51+
}
52+
}
53+
}
54+
4055
loop {
4156
tokio::select! {
42-
_ = socket.recv() => {
43-
tracing::info!("Received message from socket");
44-
break;
57+
msg = socket.recv() => {
58+
match msg {
59+
Some(Ok(Message::Close(_))) | None => {
60+
tracing::info!("WebSocket closed");
61+
break;
62+
}
63+
Some(Ok(_)) => {
64+
tracing::info!("Received message from socket (ignoring)");
65+
}
66+
Some(Err(e)) => {
67+
tracing::error!("WebSocket error: {:?}", e);
68+
break;
69+
}
70+
}
4571
},
46-
incoming_frame = camera_rx.recv_async() => {
72+
res = camera_rx.changed() => {
73+
if res.is_err() {
74+
tracing::error!("Camera channel closed");
75+
break;
76+
}
77+
let frame_opt = camera_rx.borrow().clone();
78+
if let Some(mut frame) = frame_opt {
79+
frame.data.extend_from_slice(&frame.stride.to_le_bytes());
80+
frame.data.extend_from_slice(&frame.height.to_le_bytes());
81+
frame.data.extend_from_slice(&frame.width.to_le_bytes());
82+
83+
if let Err(e) = socket.send(Message::Binary(frame.data)).await {
84+
tracing::error!("Failed to send frame to socket: {:?}", e);
85+
break;
86+
}
87+
}
88+
}
89+
}
90+
}
91+
92+
let elapsed = now.elapsed();
93+
println!("Websocket closing after {elapsed:.2?}");
94+
tracing::info!("Websocket closing after {elapsed:.2?}");
95+
}
96+
97+
let router = axum::Router::new()
98+
.route("/", get(ws_handler))
99+
.with_state(frame_rx);
100+
101+
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
102+
let port = listener.local_addr().unwrap().port();
103+
tracing::info!("WebSocket server listening on port {}", port);
104+
105+
let cancel_token = CancellationToken::new();
106+
let cancel_token_child = cancel_token.child_token();
107+
tokio::spawn(async move {
108+
let server = axum::serve(listener, router.into_make_service());
109+
tokio::select! {
110+
_ = server => {},
111+
_ = cancel_token.cancelled() => {
112+
println!("WebSocket server shutting down");
113+
}
114+
}
115+
});
116+
117+
(port, cancel_token_child)
118+
}
119+
120+
pub async fn create_frame_ws(frame_tx: broadcast::Sender<WSFrame>) -> (u16, CancellationToken) {
121+
use axum::{
122+
extract::{
123+
State,
124+
ws::{Message, WebSocket, WebSocketUpgrade},
125+
},
126+
response::IntoResponse,
127+
routing::get,
128+
};
129+
130+
type RouterState = broadcast::Sender<WSFrame>;
131+
132+
#[axum::debug_handler]
133+
async fn ws_handler(
134+
ws: WebSocketUpgrade,
135+
State(state): State<RouterState>,
136+
) -> impl IntoResponse {
137+
let rx = state.subscribe();
138+
ws.on_upgrade(move |socket| handle_socket(socket, rx))
139+
}
140+
141+
async fn handle_socket(mut socket: WebSocket, mut camera_rx: broadcast::Receiver<WSFrame>) {
142+
println!("socket connection established");
143+
tracing::info!("Socket connection established");
144+
let now = std::time::Instant::now();
145+
146+
loop {
147+
tokio::select! {
148+
msg = socket.recv() => {
149+
match msg {
150+
Some(Ok(Message::Close(_))) | None => {
151+
tracing::info!("WebSocket closed");
152+
break;
153+
}
154+
Some(Ok(_)) => {
155+
tracing::info!("Received message from socket (ignoring)");
156+
}
157+
Some(Err(e)) => {
158+
tracing::error!("WebSocket error: {:?}", e);
159+
break;
160+
}
161+
}
162+
},
163+
incoming_frame = camera_rx.recv() => {
47164
match incoming_frame {
48165
Ok(mut frame) => {
49166
frame.data.extend_from_slice(&frame.stride.to_le_bytes());
@@ -55,13 +172,16 @@ pub async fn create_frame_ws(frame_rx: Receiver<WSFrame>) -> (u16, CancellationT
55172
break;
56173
}
57174
}
58-
Err(e) => {
175+
Err(broadcast::error::RecvError::Closed) => {
59176
tracing::error!(
60-
"Connection has been lost! Shutting down websocket server: {:?}",
61-
e
177+
"Connection has been lost! Shutting down websocket server"
62178
);
63179
break;
64180
}
181+
Err(broadcast::error::RecvError::Lagged(skipped)) => {
182+
tracing::warn!("Missed {skipped} frames on websocket receiver");
183+
continue;
184+
}
65185
}
66186
}
67187
}
@@ -74,7 +194,7 @@ pub async fn create_frame_ws(frame_rx: Receiver<WSFrame>) -> (u16, CancellationT
74194

75195
let router = axum::Router::new()
76196
.route("/", get(ws_handler))
77-
.with_state(Arc::new(Mutex::new(frame_rx)));
197+
.with_state(frame_tx);
78198

79199
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
80200
let port = listener.local_addr().unwrap().port();

0 commit comments

Comments
 (0)