Skip to content

Commit

Permalink
Support multiple segments in recording meta (#177)
Browse files Browse the repository at this point in the history
* support multiple segments for video

* cargo fmt

* multi-segment metadata w/o cursors

* clippy

* multi-segment cursor recording

* fix types

* update ts

* put back zoom segments

* extra tests
  • Loading branch information
Brendonovich authored Nov 26, 2024
1 parent b039741 commit aae824c
Show file tree
Hide file tree
Showing 39 changed files with 1,488 additions and 881 deletions.
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 @@ -751,7 +751,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 @@ -1320,13 +1320,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

1 comment on commit aae824c

@vercel
Copy link

@vercel vercel bot commented on aae824c Nov 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.