Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multiple segments in recording meta #177

Merged
merged 12 commits into from
Nov 26, 2024
6 changes: 3 additions & 3 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"type": "module",
"scripts": {
"dev": "dotenv -e ../../.env -- tauri dev",
"localdev": "vinxi dev --port 3001",
"localdev": "dotenv -e ../../.env -- vinxi dev --port 3001",
"build": "vinxi build",
"tauri": "tauri"
},
Expand Down Expand Up @@ -34,12 +34,12 @@
"@tauri-apps/plugin-dialog": "2.0.0-rc.1",
"@tauri-apps/plugin-fs": "2.0.0-rc.0",
"@tauri-apps/plugin-http": "^2.0.1",
"@tauri-apps/plugin-notification": "2.0.0-rc.0",
"@tauri-apps/plugin-os": "2.0.0-rc.1",
"@tauri-apps/plugin-process": "2.0.0-rc.0",
"@tauri-apps/plugin-shell": ">=2.0.0-rc.0",
"@tauri-apps/plugin-store": "2.1.0",
"@tauri-apps/plugin-updater": "2.0.0-rc.0",
"@tauri-apps/plugin-notification": "2.0.0-rc.0",
"@types/react-tooltip": "^4.2.4",
"cva": "npm:class-variance-authority@^0.7.0",
"effect": "^3.7.2",
Expand All @@ -59,7 +59,7 @@
"@iconify/json": "^2.2.239",
"@tauri-apps/cli": ">=2.0.0-rc.0",
"@types/dom-webcodecs": "^0.1.11",
"typescript": "^5.2.2",
"typescript": "^5.7.2",
"vite": "^5.4.3",
"vite-tsconfig-paths": "^5.0.1"
}
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src-tauri/src/auth.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

use serde::{Deserialize, Serialize};
use serde_json::json;
use specta::Type;
Expand Down
3 changes: 1 addition & 2 deletions apps/desktop/src-tauri/src/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,9 @@ pub async fn export_video(
.ok();
},
&editor_instance.project_path,
editor_instance.audio.clone(),
editor_instance.meta(),
editor_instance.render_constants.clone(),
editor_instance.cursor.clone(),
&editor_instance.segments,
)
.await
.map_err(|e| {
Expand Down
21 changes: 12 additions & 9 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ mod windows;

use audio::AppSounds;
use auth::{AuthStore, AuthenticationInvalid};
use cap_editor::{EditorInstance, FRAMES_WS_PATH};
use cap_editor::{EditorState, ProjectRecordings};
use cap_editor::EditorState;
use cap_editor::{EditorInstance, ProjectRecordings, FRAMES_WS_PATH};
use cap_media::feeds::{AudioInputFeed, AudioInputSamplesSender};
use cap_media::sources::CaptureScreen;
use cap_media::{
Expand Down Expand Up @@ -735,7 +735,7 @@ async fn create_editor_instance(
let project_config = editor_instance.project_config.1.borrow();
project_config.clone()
},
recordings: editor_instance.recordings,
recordings: editor_instance.recordings.clone(),
path: editor_instance.project_path.clone(),
pretty_name: meta.pretty_name,
})
Expand Down Expand Up @@ -1304,13 +1304,16 @@ async fn take_screenshot(app: AppHandle, _state: MutableState<'_, App>) -> Resul
project_path: recording_dir.clone(),
sharing: None,
pretty_name: screenshot_name,
display: Display {
path: screenshot_path.clone(),
content: cap_project::Content::SingleSegment {
segment: cap_project::SingleSegment {
display: Display {
path: screenshot_path.clone(),
},
camera: None,
audio: None,
cursor: None,
},
},
camera: None,
audio: None,
segments: vec![],
cursor: None,
}
.save_for_project();

Expand Down
3 changes: 0 additions & 3 deletions apps/desktop/src-tauri/src/platform/macos/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ use objc::{class, msg_send, sel, sel_impl};

pub mod delegates;

use specta::Type;
use tauri_specta::Event;

#[derive(Debug)]
pub struct Window {
pub window_number: u32,
Expand Down
169 changes: 104 additions & 65 deletions apps/desktop/src-tauri/src/recording.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::time::Instant;

use crate::{
audio::AppSounds,
auth::AuthStore,
Expand All @@ -12,11 +14,14 @@ use crate::{
RecordingStarted, RecordingStopped, UploadMode,
};
use cap_editor::ProjectRecordings;
use cap_flags::FLAGS;
use cap_media::feeds::CameraFeed;
use cap_media::sources::{AVFrameCapture, CaptureScreen, CaptureWindow, ScreenCaptureSource};
use cap_project::{ProjectConfiguration, TimelineConfiguration, TimelineSegment, ZoomSegment};
use cap_project::{
Content, ProjectConfiguration, TimelineConfiguration, TimelineSegment, ZoomSegment,
};
use cap_recording::CompletedRecording;
use cap_rendering::ZOOM_DURATION;
use std::time::Instant;
use tauri::{AppHandle, Manager};
use tauri_specta::Event;

Expand Down Expand Up @@ -120,22 +125,24 @@ pub async fn start_recording(app: AppHandle, state: MutableState<'_, App>) -> Re
#[tauri::command]
#[specta::specta]
pub async fn pause_recording(state: MutableState<'_, App>) -> Result<(), String> {
let state = state.write().await;
let mut state = state.write().await;

if let Some(recording) = state.current_recording.as_mut() {
recording.pause().await.map_err(|e| e.to_string())?;
}

// if let Some(recording) = &mut state.current_recording {
// recording.pause().await.map_err(|e| e.to_string())?;
// }
Ok(())
}

#[tauri::command]
#[specta::specta]
pub async fn resume_recording(state: MutableState<'_, App>) -> Result<(), String> {
let state = state.write().await;
let mut state = state.write().await;

if let Some(recording) = state.current_recording.as_mut() {
recording.resume().await.map_err(|e| e.to_string())?;
}

// if let Some(recording) = &mut state.current_recording {
// recording.resume().await.map_err(|e| e.to_string())?;
// }
Ok(())
}

Expand All @@ -152,7 +159,7 @@ pub async fn stop_recording(app: AppHandle, state: MutableState<'_, App>) -> Res
};

let now = Instant::now();
let recording = current_recording.stop().await.map_err(|e| e.to_string())?;
let completed_recording = current_recording.stop().await.map_err(|e| e.to_string())?;
println!("stopped recording in {:?}", now.elapsed());

if let Some(window) = CapWindowId::InProgressRecording.get(&app) {
Expand All @@ -163,25 +170,29 @@ pub async fn stop_recording(app: AppHandle, state: MutableState<'_, App>) -> Res
window.unminimize().ok();
}

let screenshots_dir = recording.recording_dir.join("screenshots");
let screenshots_dir = completed_recording.recording_dir.join("screenshots");
std::fs::create_dir_all(&screenshots_dir).ok();

let display_output_path = match &completed_recording.meta.content {
Content::SingleSegment { segment } => {
segment.path(&completed_recording.meta, &segment.display.path)
}
Content::MultipleSegments { inner } => {
inner.path(&completed_recording.meta, &inner.segments[0].display.path)
}
};

let display_screenshot = screenshots_dir.join("display.jpg");
let now = Instant::now();
create_screenshot(
recording.display_output_path.clone(),
display_screenshot.clone(),
None,
)
.await?;
create_screenshot(display_output_path, display_screenshot.clone(), None).await?;
println!("created screenshot in {:?}", now.elapsed());

// let thumbnail = screenshots_dir.join("thumbnail.png");
// let now = Instant::now();
// create_thumbnail(display_screenshot, thumbnail, (100, 100)).await?;
// println!("created thumbnail in {:?}", now.elapsed());

let recording_dir = recording.recording_dir.clone();
let recording_dir = completed_recording.recording_dir.clone();

ShowCapWindow::PrevRecordings.show(&app).ok();

Expand All @@ -197,75 +208,55 @@ pub async fn stop_recording(app: AppHandle, state: MutableState<'_, App>) -> Res
.emit(&app)
.ok();

let recordings = ProjectRecordings::new(&recording.meta);
let max_duration = recordings.duration();
let recordings = ProjectRecordings::new(&completed_recording.meta);

let config = {
let segments = {
let mut segments = vec![];
let mut passed_duration = 0.0;

for i in (0..recording.segments.len()).step_by(2) {
// multi-segment
// for segment in &completed_recording.segments {
// let start = passed_duration;
// passed_duration += segment.end - segment.start;
// segments.push(TimelineSegment {
// recording_segment: None,
// start,
// end: passed_duration.min(recordings.duration()),
// timescale: 1.0,
// });
// }

// single-segment
for i in (0..completed_recording.segments.len()).step_by(2) {
let start = passed_duration;
passed_duration += recording.segments[i + 1] - recording.segments[i];
passed_duration +=
completed_recording.segments[i + 1] - completed_recording.segments[i];
segments.push(TimelineSegment {
recording_segment: None,
start,
end: passed_duration.min(recordings.duration()),
timescale: 1.0,
});
}
segments
};

let zoom_segments = {
let mut segments = vec![];

const ZOOM_SEGMENT_AFTER_CLICK_PADDING: f64 = 1.5;

for click in &recording.cursor_data.clicks {
let time = click.process_time_ms / 1000.0;

if segments.last().is_none() {
segments.push(ZoomSegment {
start: (click.process_time_ms / 1000.0 - (ZOOM_DURATION + 0.2)).max(0.0),
end: click.process_time_ms / 1000.0 + ZOOM_SEGMENT_AFTER_CLICK_PADDING,
amount: 2.0,
});
} else {
let last_segment = segments.last_mut().unwrap();

if click.down {
if last_segment.end > time {
last_segment.end = (time + ZOOM_SEGMENT_AFTER_CLICK_PADDING)
.min(recordings.duration());
} else if time < max_duration - ZOOM_DURATION {
segments.push(ZoomSegment {
start: (time - ZOOM_DURATION).max(0.0),
end: time + ZOOM_SEGMENT_AFTER_CLICK_PADDING,
amount: 2.0,
});
}
} else {
last_segment.end =
(time + ZOOM_SEGMENT_AFTER_CLICK_PADDING).min(recordings.duration());
}
}
}

segments
};

ProjectConfiguration {
timeline: Some(TimelineConfiguration {
segments,
zoom_segments,
zoom_segments: generate_zoom_segments_from_clicks(
&completed_recording,
&recordings,
),
}),
..Default::default()
}
};

config
.write(&recording.recording_dir)
.write(&completed_recording.recording_dir)
.map_err(|e| e.to_string())?;

AppSounds::StopRecording.play();
Expand Down Expand Up @@ -299,7 +290,7 @@ pub async fn stop_recording(app: AppHandle, state: MutableState<'_, App>) -> Res

export_video(
app.clone(),
recording.id.clone(),
completed_recording.id.clone(),
config,
tauri::ipc::Channel::new(|_| Ok(())),
true,
Expand All @@ -310,7 +301,7 @@ pub async fn stop_recording(app: AppHandle, state: MutableState<'_, App>) -> Res
while retry_count < max_retries {
match upload_exported_video(
app.clone(),
recording.id.clone(),
completed_recording.id.clone(),
UploadMode::Initial {
pre_created_video: Some(pre_created_video.clone()),
},
Expand Down Expand Up @@ -344,11 +335,59 @@ pub async fn stop_recording(app: AppHandle, state: MutableState<'_, App>) -> Res
});
}
} else if settings.open_editor_after_recording {
open_editor(app.clone(), recording.id);
open_editor(app.clone(), completed_recording.id);
}
}

CurrentRecordingChanged.emit(&app).ok();

Ok(())
}

fn generate_zoom_segments_from_clicks(
recording: &CompletedRecording,
recordings: &ProjectRecordings,
) -> Vec<ZoomSegment> {
let mut segments = vec![];

if !FLAGS.zoom {
return vec![];
};

let max_duration = recordings.duration();

const ZOOM_SEGMENT_AFTER_CLICK_PADDING: f64 = 1.5;

// single-segment only
for click in &recording.cursor_data.clicks {
let time = click.process_time_ms / 1000.0;

if segments.last().is_none() {
segments.push(ZoomSegment {
start: (click.process_time_ms / 1000.0 - (ZOOM_DURATION + 0.2)).max(0.0),
end: click.process_time_ms / 1000.0 + ZOOM_SEGMENT_AFTER_CLICK_PADDING,
amount: 2.0,
});
} else {
let last_segment = segments.last_mut().unwrap();

if click.down {
if last_segment.end > time {
last_segment.end =
(time + ZOOM_SEGMENT_AFTER_CLICK_PADDING).min(recordings.duration());
} else if time < max_duration - ZOOM_DURATION {
segments.push(ZoomSegment {
start: (time - ZOOM_DURATION).max(0.0),
end: time + ZOOM_SEGMENT_AFTER_CLICK_PADDING,
amount: 2.0,
});
}
} else {
last_segment.end =
(time + ZOOM_SEGMENT_AFTER_CLICK_PADDING).min(recordings.duration());
}
}
}

segments
}
Loading
Loading