Skip to content

Commit 4369b6c

Browse files
committed
Merge branch 'misc-bug-fixes'
2 parents 86d29b9 + 2b007ea commit 4369b6c

File tree

3 files changed

+105
-19
lines changed

3 files changed

+105
-19
lines changed

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

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2243,6 +2243,94 @@ async fn seek_to(editor_instance: WindowEditorInstance, frame_number: u32) -> Re
22432243
Ok(())
22442244
}
22452245

2246+
#[tauri::command]
2247+
#[specta::specta]
2248+
#[instrument(skip(editor_instance))]
2249+
async fn get_display_frame_for_cropping(
2250+
editor_instance: WindowEditorInstance,
2251+
fps: u32,
2252+
) -> Result<Vec<u8>, String> {
2253+
use cap_project::ClipOffsets;
2254+
use cap_rendering::{PixelFormat, cpu_yuv};
2255+
use image::{ImageEncoder, codecs::png::PngEncoder};
2256+
use std::io::Cursor;
2257+
2258+
let frame_number = editor_instance.state.lock().await.playhead_position;
2259+
let time_secs = frame_number as f64 / fps as f64;
2260+
2261+
let project = editor_instance.project_config.1.borrow().clone();
2262+
2263+
let (segment_time, segment) = project
2264+
.get_segment_time(time_secs)
2265+
.ok_or_else(|| "No segment found for current time".to_string())?;
2266+
2267+
let segment_medias = editor_instance
2268+
.segment_medias
2269+
.get(segment.recording_clip as usize)
2270+
.ok_or_else(|| "Segment media not found".to_string())?;
2271+
2272+
let clip_offsets = project
2273+
.clips
2274+
.iter()
2275+
.find(|v| v.index == segment.recording_clip)
2276+
.map(|v| v.offsets)
2277+
.unwrap_or(ClipOffsets::default());
2278+
2279+
let segment_frames = segment_medias
2280+
.decoders
2281+
.get_frames(segment_time as f32, false, clip_offsets)
2282+
.await
2283+
.ok_or_else(|| "Failed to get frame".to_string())?;
2284+
2285+
let screen_frame = segment_frames.screen_frame;
2286+
let width = screen_frame.width();
2287+
let height = screen_frame.height();
2288+
2289+
let rgba_data = match screen_frame.format() {
2290+
PixelFormat::Rgba => screen_frame.data().to_vec(),
2291+
PixelFormat::Nv12 => {
2292+
let y_plane = screen_frame.y_plane().ok_or("Missing Y plane")?;
2293+
let uv_plane = screen_frame.uv_plane().ok_or("Missing UV plane")?;
2294+
let mut rgba = vec![0u8; (width * height * 4) as usize];
2295+
cpu_yuv::nv12_to_rgba(
2296+
y_plane,
2297+
uv_plane,
2298+
width,
2299+
height,
2300+
screen_frame.y_stride(),
2301+
screen_frame.uv_stride(),
2302+
&mut rgba,
2303+
);
2304+
rgba
2305+
}
2306+
PixelFormat::Yuv420p => {
2307+
let y_plane = screen_frame.y_plane().ok_or("Missing Y plane")?;
2308+
let u_plane = screen_frame.u_plane().ok_or("Missing U plane")?;
2309+
let v_plane = screen_frame.v_plane().ok_or("Missing V plane")?;
2310+
let mut rgba = vec![0u8; (width * height * 4) as usize];
2311+
cpu_yuv::yuv420p_to_rgba(
2312+
y_plane,
2313+
u_plane,
2314+
v_plane,
2315+
width,
2316+
height,
2317+
screen_frame.y_stride(),
2318+
screen_frame.uv_stride(),
2319+
&mut rgba,
2320+
);
2321+
rgba
2322+
}
2323+
};
2324+
2325+
let mut png_data = Cursor::new(Vec::new());
2326+
let encoder = PngEncoder::new(&mut png_data);
2327+
encoder
2328+
.write_image(&rgba_data, width, height, image::ExtendedColorType::Rgba8)
2329+
.map_err(|e| format!("Failed to encode PNG: {e}"))?;
2330+
2331+
Ok(png_data.into_inner())
2332+
}
2333+
22462334
#[tauri::command]
22472335
#[specta::specta]
22482336
#[instrument(skip(editor_instance))]
@@ -2518,6 +2606,7 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
25182606
reset_microphone_permissions,
25192607
is_camera_window_open,
25202608
seek_to,
2609+
get_display_frame_for_cropping,
25212610
windows::position_traffic_lights,
25222611
windows::set_theme,
25232612
global_message_dialog,

apps/desktop/src/routes/editor/Editor.tsx

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,6 @@ function Inner() {
121121
setEditorState,
122122
previewResolutionBase,
123123
dialog,
124-
canvasControls,
125124
} = useEditorContext();
126125

127126
const isExportMode = () => {
@@ -494,11 +493,8 @@ function Dialogs() {
494493
})()}
495494
>
496495
{(dialog) => {
497-
const {
498-
setProject: setState,
499-
editorInstance,
500-
canvasControls,
501-
} = useEditorContext();
496+
const { setProject: setState, editorInstance } =
497+
useEditorContext();
502498
const display = editorInstance.recordings.segments[0].display;
503499

504500
let cropperRef: CropperRef | undefined;
@@ -509,20 +505,18 @@ function Dialogs() {
509505
string | null
510506
>(null);
511507

512-
const controls = canvasControls();
513-
if (controls) {
514-
controls
515-
.captureFrame()
516-
.then((blob) => {
517-
if (blob) {
518-
const url = URL.createObjectURL(blob);
519-
setFrameBlobUrl(url);
520-
}
521-
})
522-
.catch((error) => {
523-
console.warn("Frame capture failed:", error);
508+
commands
509+
.getDisplayFrameForCropping(FPS)
510+
.then((pngBytes) => {
511+
const blob = new Blob([new Uint8Array(pngBytes)], {
512+
type: "image/png",
524513
});
525-
}
514+
const url = URL.createObjectURL(blob);
515+
setFrameBlobUrl(url);
516+
})
517+
.catch((error: unknown) => {
518+
console.warn("Display frame fetch failed:", error);
519+
});
526520

527521
onCleanup(() => {
528522
const url = frameBlobUrl();

apps/desktop/src/utils/tauri.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,9 @@ async isCameraWindowOpen() : Promise<boolean> {
200200
async seekTo(frameNumber: number) : Promise<null> {
201201
return await TAURI_INVOKE("seek_to", { frameNumber });
202202
},
203+
async getDisplayFrameForCropping(fps: number) : Promise<number[]> {
204+
return await TAURI_INVOKE("get_display_frame_for_cropping", { fps });
205+
},
203206
async positionTrafficLights(controlsInset: [number, number] | null) : Promise<void> {
204207
await TAURI_INVOKE("position_traffic_lights", { controlsInset });
205208
},

0 commit comments

Comments
 (0)