-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Expand file tree
/
Copy pathscreenshot_editor.rs
More file actions
477 lines (418 loc) · 18.4 KB
/
screenshot_editor.rs
File metadata and controls
477 lines (418 loc) · 18.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
use crate::PendingScreenshots;
use crate::frame_ws::{WSFrame, create_watch_frame_ws};
use crate::gpu_context;
use crate::windows::{CapWindowId, ScreenshotEditorWindowIds};
use cap_project::{
ProjectConfiguration, RecordingMeta, RecordingMetaInner, SingleSegment, StudioRecordingMeta,
VideoMeta,
};
use cap_rendering::{
DecodedFrame, DecodedSegmentFrames, FrameRenderer, ProjectUniforms, RenderVideoConstants,
RendererLayers,
};
use image::{GenericImageView, RgbImage, buffer::ConvertBuffer};
use relative_path::RelativePathBuf;
use serde::Serialize;
use specta::Type;
use std::str::FromStr;
use std::{collections::HashMap, ops::Deref, path::PathBuf, sync::Arc};
use tauri::{
Manager, Runtime, Window,
ipc::{CommandArg, InvokeError},
};
use tokio::sync::{RwLock, watch};
use tokio_util::sync::CancellationToken;
const MAX_DIMENSION: u32 = 16_384;
pub struct ScreenshotEditorInstance {
pub ws_port: u16,
pub ws_shutdown_token: CancellationToken,
pub config_tx: watch::Sender<ProjectConfiguration>,
pub path: PathBuf,
pub pretty_name: String,
}
impl ScreenshotEditorInstance {
pub async fn dispose(&self) {
self.ws_shutdown_token.cancel();
}
}
#[derive(Clone)]
pub struct ScreenshotEditorInstances(Arc<RwLock<HashMap<String, Arc<ScreenshotEditorInstance>>>>);
pub struct WindowScreenshotEditorInstance(pub Arc<ScreenshotEditorInstance>);
impl specta::function::FunctionArg for WindowScreenshotEditorInstance {
fn to_datatype(_: &mut specta::TypeMap) -> Option<specta::DataType> {
None
}
}
impl Deref for WindowScreenshotEditorInstance {
type Target = Arc<ScreenshotEditorInstance>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'de, R: Runtime> CommandArg<'de, R> for WindowScreenshotEditorInstance {
fn from_command(command: tauri::ipc::CommandItem<'de, R>) -> Result<Self, InvokeError> {
let window = Window::from_command(command)?;
let instances = window.state::<ScreenshotEditorInstances>();
let instance = futures::executor::block_on(instances.0.read());
if let Some(instance) = instance.get(window.label()).cloned() {
Ok(Self(instance))
} else {
Err(InvokeError::from(format!(
"no ScreenshotEditor instance for window '{}'",
window.label(),
)))
}
}
}
impl ScreenshotEditorInstances {
pub async fn get_or_create(
window: &Window,
path: PathBuf,
) -> Result<Arc<ScreenshotEditorInstance>, String> {
let instances = match window.try_state::<ScreenshotEditorInstances>() {
Some(s) => (*s).clone(),
None => {
let instances = Self(Arc::new(RwLock::new(HashMap::new())));
window.manage(instances.clone());
instances
}
};
let mut instances = instances.0.write().await;
use std::collections::hash_map::Entry;
match instances.entry(window.label().to_string()) {
Entry::Vacant(entry) => {
let (frame_tx, frame_rx) = watch::channel(None);
let (ws_port, ws_shutdown_token) = create_watch_frame_ws(frame_rx).await;
let (data, width, height) = {
let key = path
.parent()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default();
let pending = window.try_state::<PendingScreenshots>();
let pending_frame = pending.and_then(|p| p.remove(&key));
if let Some(frame) = pending_frame {
let width = frame.width;
let height = frame.height;
if width > MAX_DIMENSION || height > MAX_DIMENSION {
return Err(format!(
"Image dimensions exceed maximum: {width}x{height}"
));
}
let expected_len = width
.checked_mul(height)
.and_then(|p| p.checked_mul(3))
.ok_or_else(|| {
format!("Image dimensions overflow: {width}x{height}")
})?;
let expected_len = usize::try_from(expected_len)
.map_err(|_| format!("Image size too large: {width}x{height}"))?;
let data = frame.data;
if data.len() != expected_len {
return Err(format!(
"Image data length mismatch: expected {expected_len} bytes for {width}x{height} frame, got {}",
data.len()
));
}
let rgb_img = RgbImage::from_raw(width, height, data).ok_or_else(|| {
format!("Invalid RGB data for {width}x{height} frame")
})?;
let rgba_img: image::RgbaImage = rgb_img.convert();
(rgba_img.into_raw(), width, height)
} else {
let image_path = if path.is_dir() {
let original = path.join("original.png");
if original.exists() {
original
} else {
std::fs::read_dir(&path)
.ok()
.and_then(|dir| {
dir.flatten()
.find(|e| {
e.path().extension().and_then(|s| s.to_str())
== Some("png")
})
.map(|e| e.path())
})
.ok_or_else(|| {
format!("No PNG file found in directory: {:?}", path)
})?
}
} else {
path.clone()
};
let img = image::open(&image_path)
.map_err(|e| format!("Failed to open image: {e}"))?;
let (w, h) = img.dimensions();
if w > MAX_DIMENSION || h > MAX_DIMENSION {
return Err(format!("Image dimensions exceed maximum: {w}x{h}"));
}
w.checked_mul(h)
.and_then(|p| p.checked_mul(4))
.ok_or_else(|| format!("Image dimensions overflow: {w}x{h}"))?;
(img.to_rgba8().into_raw(), w, h)
}
};
let cap_dir = if path.extension().and_then(|s| s.to_str()) == Some("cap") {
Some(path.clone())
} else if let Some(parent) = path.parent() {
if parent.extension().and_then(|s| s.to_str()) == Some("cap") {
Some(parent.to_path_buf())
} else {
None
}
} else {
None
};
let (recording_meta, loaded_config) = if let Some(cap_dir) = &cap_dir {
let meta = RecordingMeta::load_for_project(cap_dir).ok();
let config = ProjectConfiguration::load(cap_dir).ok();
(meta, config)
} else {
(None, None)
};
let recording_meta = if let Some(meta) = recording_meta {
meta
} else {
// Create dummy meta
let filename = path
.file_name()
.ok_or_else(|| "Invalid path".to_string())?
.to_string_lossy();
let relative_path = RelativePathBuf::from(filename.as_ref());
let video_meta = VideoMeta {
path: relative_path.clone(),
fps: 30,
start_time: Some(0.0),
};
let segment = SingleSegment {
display: video_meta.clone(),
camera: None,
audio: None,
cursor: None,
};
let studio_meta = StudioRecordingMeta::SingleSegment { segment };
RecordingMeta {
platform: None,
project_path: path.parent().unwrap().to_path_buf(),
pretty_name: "Screenshot".to_string(),
sharing: None,
inner: RecordingMetaInner::Studio(studio_meta.clone()),
upload: None,
}
};
let (instance, adapter, device, queue) =
if let Some(shared) = gpu_context::get_shared_gpu().await {
(
shared.instance.clone(),
shared.adapter.clone(),
shared.device.clone(),
shared.queue.clone(),
)
} else {
let instance =
Arc::new(wgpu::Instance::new(&wgpu::InstanceDescriptor::default()));
let adapter = Arc::new(
instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: false,
compatible_surface: None,
})
.await
.map_err(|_| "No GPU adapter found".to_string())?,
);
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("cap-rendering-device"),
required_features: wgpu::Features::empty(),
..Default::default()
})
.await
.map_err(|e| e.to_string())?;
(instance, adapter, Arc::new(device), Arc::new(queue))
};
let options = cap_rendering::RenderOptions {
screen_size: cap_project::XY::new(width, height),
camera_size: None,
};
// We need to extract the studio meta from the recording meta
let studio_meta = match &recording_meta.inner {
RecordingMetaInner::Studio(meta) => meta.clone(),
_ => return Err("Invalid recording meta for screenshot".to_string()),
};
let constants = RenderVideoConstants {
_instance: (*instance).clone(),
_adapter: (*adapter).clone(),
queue: (*queue).clone(),
device: (*device).clone(),
options,
meta: studio_meta,
recording_meta: recording_meta.clone(),
background_textures: Arc::new(tokio::sync::RwLock::new(HashMap::new())),
};
let (config_tx, mut config_rx) = watch::channel(loaded_config.unwrap_or_default());
let render_shutdown_token = ws_shutdown_token.clone();
let instance = Arc::new(ScreenshotEditorInstance {
ws_port,
ws_shutdown_token,
config_tx,
path: path.clone(),
pretty_name: recording_meta.pretty_name.clone(),
});
// Spawn render loop
let decoded_frame = DecodedFrame::new(data, width, height);
tokio::spawn(async move {
let mut frame_renderer = FrameRenderer::new(&constants);
let mut layers = RendererLayers::new(&constants.device, &constants.queue);
let shutdown_token = render_shutdown_token;
// Initial render
let mut current_config = config_rx.borrow().clone();
loop {
if shutdown_token.is_cancelled() {
break;
}
let segment_frames = DecodedSegmentFrames {
screen_frame: DecodedFrame::new(
decoded_frame.data().to_vec(),
decoded_frame.width(),
decoded_frame.height(),
),
camera_frame: None,
segment_time: 0.0,
recording_time: 0.0,
};
let (base_w, base_h) =
ProjectUniforms::get_base_size(&constants.options, ¤t_config);
let uniforms = ProjectUniforms::new(
&constants,
¤t_config,
0,
30,
cap_project::XY::new(base_w, base_h),
&cap_project::CursorEvents::default(),
&segment_frames,
);
let rendered_frame = frame_renderer
.render(
segment_frames,
uniforms,
&cap_project::CursorEvents::default(),
&mut layers,
)
.await;
match rendered_frame {
Ok(frame) => {
let _ = frame_tx.send(Some(WSFrame {
data: frame.data,
width: frame.width,
height: frame.height,
stride: frame.padded_bytes_per_row,
}));
}
Err(e) => {
eprintln!("Failed to render frame: {e}");
}
}
tokio::select! {
res = config_rx.changed() => {
if res.is_err() {
break;
}
current_config = config_rx.borrow().clone();
}
_ = shutdown_token.cancelled() => {
break;
}
}
}
let _ = frame_tx.send(None);
});
entry.insert(instance.clone());
Ok(instance)
}
Entry::Occupied(entry) => {
let instance = entry.get().clone();
// Force a re-render for the new client by sending the current config again
let config = instance.config_tx.borrow().clone();
let _ = instance.config_tx.send(config);
Ok(instance)
}
}
}
pub async fn remove(window: Window) {
let instances = match window.try_state::<ScreenshotEditorInstances>() {
Some(s) => (*s).clone(),
None => return,
};
let mut instances = instances.0.write().await;
if let Some(instance) = instances.remove(window.label()) {
instance.dispose().await;
}
}
}
#[derive(Serialize, Type, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SerializedScreenshotEditorInstance {
pub frames_socket_url: String,
pub path: PathBuf,
pub config: Option<ProjectConfiguration>,
pub pretty_name: String,
}
#[tauri::command]
#[specta::specta]
pub async fn create_screenshot_editor_instance(
window: Window,
) -> Result<SerializedScreenshotEditorInstance, String> {
let CapWindowId::ScreenshotEditor { id } =
CapWindowId::from_str(window.label()).map_err(|e| e.to_string())?
else {
return Err("Invalid window".to_string());
};
let path = {
let window_ids = ScreenshotEditorWindowIds::get(window.app_handle());
let window_ids = window_ids.ids.lock().unwrap();
let Some((path, _)) = window_ids.iter().find(|(_, _id)| *_id == id) else {
return Err("Screenshot editor instance not found".to_string());
};
path.clone()
};
let instance = ScreenshotEditorInstances::get_or_create(&window, path).await?;
let config = instance.config_tx.borrow().clone();
Ok(SerializedScreenshotEditorInstance {
frames_socket_url: format!("ws://localhost:{}", instance.ws_port),
path: instance.path.clone(),
config: Some(config),
pretty_name: instance.pretty_name.clone(),
})
}
#[tauri::command]
#[specta::specta]
pub async fn update_screenshot_config(
instance: WindowScreenshotEditorInstance,
config: ProjectConfiguration,
save: bool,
) -> Result<(), String> {
config.validate().map_err(|error| error.to_string())?;
let _ = instance.config_tx.send(config.clone());
if !save {
return Ok(());
}
let Some(parent) = instance.path.parent() else {
return Ok(());
};
if parent.extension().and_then(|s| s.to_str()) == Some("cap") {
let path = parent.to_path_buf();
if let Err(e) = config.write(&path) {
eprintln!("Failed to save screenshot config: {}", e);
} else {
println!("Saved screenshot config to {:?}", path);
}
} else {
println!(
"Not saving config: parent {:?} is not a .cap directory",
parent
);
}
Ok(())
}