Skip to content

Commit

Permalink
multi-segment cursor recording
Browse files Browse the repository at this point in the history
  • Loading branch information
Brendonovich committed Nov 26, 2024
1 parent 661dfa8 commit 9e2c292
Show file tree
Hide file tree
Showing 14 changed files with 325 additions and 219 deletions.
4 changes: 2 additions & 2 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"name": "@cap/desktop",
"type": "module",
"scripts": {
"dev": "dotenv -e ../../.env -- tauri dev",
"localdev": "vinxi dev --port 3001",
"dev": "tauri dev",
"localdev": "dotenv -e ../../.env -- vinxi dev --port 3001",
"build": "vinxi build",
"tauri": "tauri"
},
Expand Down
2 changes: 1 addition & 1 deletion 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, ProjectRecordings, FRAMES_WS_PATH};
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
2 changes: 0 additions & 2 deletions apps/desktop/src-tauri/src/platform/macos/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +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 {
Expand Down
30 changes: 21 additions & 9 deletions apps/desktop/src-tauri/src/recording.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@ 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::{
Content, ProjectConfiguration, TimelineConfiguration, TimelineSegment,
};
use cap_project::{Content, ProjectConfiguration, TimelineConfiguration, TimelineSegment};
use std::time::Instant;
use tauri::{AppHandle, Manager};
use tauri_specta::Event;
Expand Down Expand Up @@ -123,9 +122,9 @@ pub async fn start_recording(app: AppHandle, state: MutableState<'_, App>) -> Re
pub async fn pause_recording(state: MutableState<'_, App>) -> Result<(), String> {
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) = state.current_recording.as_mut() {
// recording.pause().await.map_err(|e| e.to_string())?;
// }

Ok(())
}
Expand All @@ -135,9 +134,9 @@ pub async fn pause_recording(state: MutableState<'_, App>) -> Result<(), String>
pub async fn resume_recording(state: MutableState<'_, App>) -> Result<(), String> {
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) = state.current_recording.as_mut() {
// recording.resume().await.map_err(|e| e.to_string())?;
// }

Ok(())
}
Expand Down Expand Up @@ -210,6 +209,18 @@ pub async fn stop_recording(app: AppHandle, state: MutableState<'_, App>) -> Res
let mut segments = vec![];
let mut passed_duration = 0.0;

// multi-segment
// for segment in 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,
// });
// }

for i in (0..recording.segments.len()).step_by(2) {
let start = passed_duration;
passed_duration += recording.segments[i + 1] - recording.segments[i];
Expand All @@ -220,6 +231,7 @@ pub async fn stop_recording(app: AppHandle, state: MutableState<'_, App>) -> Res
timescale: 1.0,
});
}

segments
};

Expand Down
18 changes: 3 additions & 15 deletions apps/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,6 @@ const queryClient = new QueryClient({
export default function App() {
const darkMode = themeStore.isDarkMode;

onMount(async () => {
await themeStore.initialize();

const matches = useCurrentMatches();

onMount(() => {
for (const match of matches()) {
if (match.route.info?.AUTO_SHOW_WINDOW === false) return;
}

getCurrentWindow().show();
});
});

return (
<div class={darkMode() ? "dark" : ""}>
<ErrorBoundary
Expand All @@ -85,7 +71,9 @@ export default function App() {
if (match.route.info?.AUTO_SHOW_WINDOW === false) return;
}

getCurrentWindow().show();
themeStore.initialize().then(() => {
getCurrentWindow().show();
});
});

return <Suspense>{props.children}</Suspense>;
Expand Down
6 changes: 3 additions & 3 deletions apps/desktop/src/utils/tauri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,8 @@ export type HotkeyAction = "startRecording" | "stopRecording" | "restartRecordin
export type HotkeysConfiguration = { show: boolean }
export type HotkeysStore = { hotkeys: { [key in HotkeyAction]: Hotkey } }
export type JsonValue<T> = [T]
export type MultipleSegment = { display: Display; camera?: CameraMeta | null; audio?: AudioMeta | null; cursor: string | null }
export type MultipleSegments = { segments: MultipleSegment[] }
export type MultipleSegment = { display: Display; camera?: CameraMeta | null; audio?: AudioMeta | null; cursor?: string | null }
export type MultipleSegments = { segments: MultipleSegment[]; cursors: { [key in string]: string } }
export type NewNotification = { title: string; body: string; is_error: boolean }
export type NewRecordingAdded = { path: string }
export type NewScreenshotAdded = { path: string }
Expand Down Expand Up @@ -272,7 +272,7 @@ export type SegmentRecordings = { display: Video; camera: Video | null; audio: A
export type SerializedEditorInstance = { framesSocketUrl: string; recordingDuration: number; savedProjectConfig: ProjectConfiguration; recordings: ProjectRecordings; path: string; prettyName: string }
export type SharingMeta = { id: string; link: string }
export type ShowCapWindow = "Setup" | "Main" | { Settings: { page: string | null } } | { Editor: { project_id: string } } | "PrevRecordings" | "WindowCaptureOccluder" | { Camera: { ws_port: number } } | { InProgressRecording: { position: [number, number] | null } } | "Upgrade"
export type SingleSegment = { display: Display; camera?: CameraMeta | null; audio?: AudioMeta | null; cursor: string | null }
export type SingleSegment = { display: Display; camera?: CameraMeta | null; audio?: AudioMeta | null; cursor?: string | null }
export type TimelineConfiguration = { segments: TimelineSegment[]; zoomSegments?: ZoomSegment[] }
export type TimelineSegment = { recordingSegment: number | null; timescale: number; start: number; end: number }
export type UploadMode = { Initial: { pre_created_video: PreCreatedVideo | null } } | "Reupload"
Expand Down
6 changes: 3 additions & 3 deletions crates/flags/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ pub struct Flags {
}

pub const FLAGS: Flags = Flags {
record_mouse: false, //cfg!(debug_assertions),
record_mouse: false, // cfg!(debug_assertions),
split: false,
pause_resume: cfg!(debug_assertions),
zoom: false, //cfg!(debug_assertions),
pause_resume: false, // cfg!(debug_assertions),
zoom: false, // cfg!(debug_assertions),
};
2 changes: 1 addition & 1 deletion crates/media/src/sources/screen_capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ impl<T> ScreenCaptureSource<T> {

Options {
fps: self.fps,
show_cursor: !FLAGS.zoom,
show_cursor: !FLAGS.record_mouse,
show_highlight: true,
excluded_targets: Some(excluded_targets),
output_type: if cfg!(windows) {
Expand Down
13 changes: 9 additions & 4 deletions crates/project/src/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use either::Either;
use serde::{Deserialize, Serialize};
use specta::Type;
use std::{
collections::HashMap,
fs::File,
path::{Path, PathBuf},
};
Expand Down Expand Up @@ -84,10 +85,11 @@ pub enum Content {
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub struct SingleSegment {
pub display: Display,
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub camera: Option<CameraMeta>,
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub audio: Option<AudioMeta>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<PathBuf>,
}

Expand Down Expand Up @@ -148,6 +150,8 @@ impl SingleSegment {
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub struct MultipleSegments {
pub segments: Vec<MultipleSegment>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub cursors: HashMap<String, PathBuf>,
}

impl MultipleSegments {
Expand Down Expand Up @@ -196,10 +200,11 @@ impl MultipleSegments {
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub struct MultipleSegment {
pub display: Display,
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub camera: Option<CameraMeta>,
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub audio: Option<AudioMeta>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<PathBuf>,
}

Expand Down
54 changes: 31 additions & 23 deletions crates/recording/src/actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use cap_media::{
sources::{AudioInputSource, CameraSource, ScreenCaptureSource, ScreenCaptureTarget},
MediaError,
};
use cap_project::{CursorClickEvent, CursorMoveEvent, RecordingMeta};
use cap_project::RecordingMeta;
use either::Either;
use std::{
path::PathBuf,
Expand All @@ -17,7 +17,10 @@ use std::{
use thiserror::Error;
use tokio::sync::{oneshot, Mutex};

use crate::{cursor::spawn_cursor_recorder, RecordingOptions};
use crate::{
cursor::{spawn_cursor_recorder, CursorActor},
RecordingOptions,
};

pub enum ActorControlMessage {
Stop(oneshot::Sender<Result<CompletedRecording, RecordingError>>),
Expand All @@ -30,8 +33,7 @@ pub struct Actor {
pipeline: RecordingPipeline,
start_time: f64,
stop_signal: Arc<AtomicBool>,
cursor_moves: oneshot::Receiver<Vec<CursorMoveEvent>>,
cursor_clicks: oneshot::Receiver<Vec<CursorClickEvent>>,
cursor: Option<CursorActor>,
}

pub struct ActorHandle {
Expand Down Expand Up @@ -106,22 +108,14 @@ pub async fn spawn_recording_actor(
let stop_signal = Arc::new(AtomicBool::new(false));

// Initialize default values for cursor channels
let (cursor_moves, cursor_clicks) = if FLAGS.record_mouse {
let cursor = FLAGS.record_mouse.then(|| {
spawn_cursor_recorder(
stop_signal.clone(),
screen_source.get_bounds(),
content_dir,
cursors_dir,
Default::default(),
0,
)
} else {
// Create dummy channels that will never receive data
let (move_tx, move_rx) = oneshot::channel();
let (click_tx, click_rx) = oneshot::channel();
// Send empty vectors immediately
move_tx.send(vec![]).unwrap();
click_tx.send(vec![]).unwrap();
(move_rx, click_rx)
};
});

tokio::spawn({
let options = options.clone();
Expand All @@ -131,8 +125,7 @@ pub async fn spawn_recording_actor(
options,
pipeline,
start_time,
cursor_moves,
cursor_clicks,
cursor,
stop_signal,
id,
};
Expand Down Expand Up @@ -198,7 +191,7 @@ async fn stop_recording(mut actor: Actor) -> Result<CompletedRecording, Recordin
.map(|path| AudioMeta {
path: path.strip_prefix(&actor.recording_dir).unwrap().to_owned(),
}),
cursor: Some(PathBuf::from("cursor.json")),
cursor: Some(PathBuf::from("content/cursor.json")),
},
},
};
Expand All @@ -209,12 +202,27 @@ async fn stop_recording(mut actor: Actor) -> Result<CompletedRecording, Recordin
.stop_signal
.store(true, std::sync::atomic::Ordering::Relaxed);

let cursor_data = cap_project::CursorData {
clicks: actor.cursor_clicks.await.unwrap_or_default(),
moves: actor.cursor_moves.await.unwrap_or_default(),
cursor_images: CursorImages::default(), // This will be populated during recording
let cursor_data = if let Some(cursor) = actor.cursor {
let resp = cursor.stop().await;
cap_project::CursorData {
clicks: resp.clicks,
moves: resp.moves,
cursor_images: CursorImages(
resp.cursors
.into_values()
.map(|(filename, id)| (id.to_string(), filename))
.collect(),
),
}
} else {
Default::default()
};

std::fs::write(
actor.recording_dir.join("content/cursor.json"),
serde_json::to_string_pretty(&cursor_data)?,
)?;

meta.save_for_project()
.map_err(Either::either_into::<RecordingError>)?;

Expand Down
Loading

0 comments on commit 9e2c292

Please sign in to comment.