Skip to content

Commit d2f8ed3

Browse files
committed
feat: Implement resolution picker for Instant Mode
1 parent 5355d72 commit d2f8ed3

File tree

9 files changed

+161
-21
lines changed

9 files changed

+161
-21
lines changed

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,27 @@ pub enum PostDeletionBehaviour {
3131
ReopenRecordingWindow,
3232
}
3333

34+
#[derive(Default, Serialize, Deserialize, Type, Debug, Clone, Copy, PartialEq, Eq)]
35+
#[serde(rename_all = "camelCase")]
36+
pub enum InstantModeResolution {
37+
#[default]
38+
Fhd1080,
39+
Hd720,
40+
Qhd1440,
41+
Uhd2160,
42+
}
43+
44+
impl InstantModeResolution {
45+
pub fn target_height(self) -> u32 {
46+
match self {
47+
Self::Hd720 => 720,
48+
Self::Fhd1080 => 1080,
49+
Self::Qhd1440 => 1440,
50+
Self::Uhd2160 => 2160,
51+
}
52+
}
53+
}
54+
3455
impl MainWindowRecordingStartBehaviour {
3556
pub fn perform(&self, window: &tauri::WebviewWindow) -> tauri::Result<()> {
3657
match self {
@@ -120,6 +141,8 @@ pub struct GeneralSettingsStore {
120141
pub excluded_windows: Vec<WindowExclusion>,
121142
#[serde(default)]
122143
pub delete_instant_recordings_after_upload: bool,
144+
#[serde(default)]
145+
pub instant_mode_resolution: InstantModeResolution,
123146
}
124147

125148
fn default_enable_native_camera_preview() -> bool {
@@ -180,6 +203,7 @@ impl Default for GeneralSettingsStore {
180203
post_deletion_behaviour: PostDeletionBehaviour::DoNothing,
181204
excluded_windows: default_excluded_windows(),
182205
delete_instant_recordings_after_upload: false,
206+
instant_mode_resolution: InstantModeResolution::default(),
183207
}
184208
}
185209
}

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ use crate::{
4646
auth::AuthStore,
4747
create_screenshot,
4848
general_settings::{
49-
self, GeneralSettingsStore, PostDeletionBehaviour, PostStudioRecordingBehaviour,
49+
self, GeneralSettingsStore, InstantModeResolution, PostDeletionBehaviour,
50+
PostStudioRecordingBehaviour,
5051
},
5152
open_external_link,
5253
presets::PresetsStore,
@@ -537,7 +538,15 @@ pub async fn start_recording(
537538
recording_dir.clone(),
538539
inputs.capture_target.clone(),
539540
)
540-
.with_system_audio(inputs.capture_system_audio);
541+
.with_system_audio(inputs.capture_system_audio)
542+
.with_output_height(
543+
general_settings
544+
.as_ref()
545+
.map(|settings| settings.instant_mode_resolution.target_height())
546+
.unwrap_or_else(|| {
547+
InstantModeResolution::default().target_height()
548+
}),
549+
);
541550

542551
#[cfg(target_os = "macos")]
543552
{

apps/desktop/src/routes/(window-chrome)/settings/general.tsx

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
commands,
3030
events,
3131
type GeneralSettingsStore,
32+
type InstantModeResolution,
3233
type MainWindowRecordingStartBehaviour,
3334
type PostDeletionBehaviour,
3435
type PostStudioRecordingBehaviour,
@@ -61,7 +62,11 @@ const getWindowOptionLabel = (window: CaptureWindow) => {
6162
return parts.join(" • ");
6263
};
6364

64-
const createDefaultGeneralSettings = (): GeneralSettingsStore => ({
65+
type ExtendedGeneralSettingsStore = GeneralSettingsStore & {
66+
instantModeResolution?: InstantModeResolution;
67+
};
68+
69+
const createDefaultGeneralSettings = (): ExtendedGeneralSettingsStore => ({
6570
uploadIndividualFiles: false,
6671
hideDockIcon: false,
6772
autoCreateShareableLink: false,
@@ -71,11 +76,12 @@ const createDefaultGeneralSettings = (): GeneralSettingsStore => ({
7176
autoZoomOnClicks: false,
7277
custom_cursor_capture2: true,
7378
excludedWindows: [],
79+
instantModeResolution: "fhd1080",
7480
});
7581

7682
const deriveInitialSettings = (
7783
store: GeneralSettingsStore | null,
78-
): GeneralSettingsStore => {
84+
): ExtendedGeneralSettingsStore => {
7985
const defaults = createDefaultGeneralSettings();
8086
if (!store) return defaults;
8187

@@ -85,6 +91,17 @@ const deriveInitialSettings = (
8591
};
8692
};
8793

94+
const INSTANT_MODE_RESOLUTION_OPTIONS = [
95+
{ value: "hd720", label: "720p", height: 720 },
96+
{ value: "fhd1080", label: "1080p", height: 1080 },
97+
{ value: "qhd1440", label: "1440p", height: 1440 },
98+
{ value: "uhd2160", label: "4K", height: 2160 },
99+
] satisfies {
100+
value: InstantModeResolution;
101+
label: string;
102+
height: number;
103+
}[];
104+
88105
export default function GeneralSettings() {
89106
const [store] = createResource(() => generalSettingsStore.get());
90107

@@ -167,7 +184,7 @@ function AppearanceSection(props: {
167184
}
168185

169186
function Inner(props: { initialStore: GeneralSettingsStore | null }) {
170-
const [settings, setSettings] = createStore<GeneralSettingsStore>(
187+
const [settings, setSettings] = createStore<ExtendedGeneralSettingsStore>(
171188
deriveInitialSettings(props.initialStore),
172189
);
173190

@@ -250,6 +267,9 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) {
250267
return data.filter(isWindowAvailable);
251268
});
252269

270+
const instantResolutionDescription =
271+
"Choose the resolution for Instant Mode recordings.";
272+
253273
const refreshAvailableWindows = async (): Promise<CaptureWindow[]> => {
254274
try {
255275
const refreshed = (await refetchWindows()) ?? windows() ?? [];
@@ -304,6 +324,7 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) {
304324
| MainWindowRecordingStartBehaviour
305325
| PostStudioRecordingBehaviour
306326
| PostDeletionBehaviour
327+
| InstantModeResolution
307328
| number,
308329
>(props: {
309330
label: string;
@@ -417,6 +438,24 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) {
417438
)}
418439

419440
<SettingGroup title="Recording">
441+
<SelectSettingItem
442+
label="Instant mode resolution"
443+
description={instantResolutionDescription}
444+
value={
445+
settings.instantModeResolution ??
446+
("fhd1080" as InstantModeResolution)
447+
}
448+
onChange={(value) =>
449+
handleChange(
450+
"instantModeResolution",
451+
value as InstantModeResolution,
452+
)
453+
}
454+
options={INSTANT_MODE_RESOLUTION_OPTIONS.map((option) => ({
455+
text: option.label,
456+
value: option.value,
457+
}))}
458+
/>
420459
<SelectSettingItem
421460
label="Recording countdown"
422461
description="Countdown before recording starts"

apps/desktop/src/utils/tauri.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ export type ExportSettings = ({ format: "Mp4" } & Mp4ExportSettings) | ({ format
393393
export type FileType = "recording" | "screenshot"
394394
export type Flags = { captions: boolean }
395395
export type FramesRendered = { renderedCount: number; totalFrames: number; type: "FramesRendered" }
396-
export type GeneralSettingsStore = { instanceId?: string; uploadIndividualFiles?: boolean; hideDockIcon?: boolean; hapticsEnabled?: boolean; autoCreateShareableLink?: boolean; enableNotifications?: boolean; disableAutoOpenLinks?: boolean; hasCompletedStartup?: boolean; theme?: AppTheme; commercialLicense?: CommercialLicense | null; lastVersion?: string | null; windowTransparency?: boolean; postStudioRecordingBehaviour?: PostStudioRecordingBehaviour; mainWindowRecordingStartBehaviour?: MainWindowRecordingStartBehaviour; custom_cursor_capture2?: boolean; serverUrl?: string; recordingCountdown?: number | null; enableNativeCameraPreview: boolean; autoZoomOnClicks?: boolean; enableNewRecordingFlow: boolean; postDeletionBehaviour?: PostDeletionBehaviour; excludedWindows?: WindowExclusion[]; deleteInstantRecordingsAfterUpload?: boolean }
396+
export type GeneralSettingsStore = { instanceId?: string; uploadIndividualFiles?: boolean; hideDockIcon?: boolean; hapticsEnabled?: boolean; autoCreateShareableLink?: boolean; enableNotifications?: boolean; disableAutoOpenLinks?: boolean; hasCompletedStartup?: boolean; theme?: AppTheme; commercialLicense?: CommercialLicense | null; lastVersion?: string | null; windowTransparency?: boolean; postStudioRecordingBehaviour?: PostStudioRecordingBehaviour; mainWindowRecordingStartBehaviour?: MainWindowRecordingStartBehaviour; custom_cursor_capture2?: boolean; serverUrl?: string; recordingCountdown?: number | null; enableNativeCameraPreview: boolean; autoZoomOnClicks?: boolean; enableNewRecordingFlow: boolean; postDeletionBehaviour?: PostDeletionBehaviour; excludedWindows?: WindowExclusion[]; deleteInstantRecordingsAfterUpload?: boolean; instantModeResolution?: InstantModeResolution }
397397
export type GifExportSettings = { fps: number; resolution_base: XY<number>; quality: GifQuality | null }
398398
export type GifQuality = {
399399
/**
@@ -410,6 +410,7 @@ export type Hotkey = { code: string; meta: boolean; ctrl: boolean; alt: boolean;
410410
export type HotkeyAction = "startStudioRecording" | "startInstantRecording" | "stopRecording" | "restartRecording" | "openRecordingPicker" | "openRecordingPickerDisplay" | "openRecordingPickerWindow" | "openRecordingPickerArea" | "other"
411411
export type HotkeysConfiguration = { show: boolean }
412412
export type HotkeysStore = { hotkeys: { [key in HotkeyAction]: Hotkey } }
413+
export type InstantModeResolution = "fhd1080" | "hd720" | "qhd1440" | "uhd2160"
413414
export type InstantRecordingMeta = { recording: boolean } | { error: string } | { fps: number; sample_rate: number | null }
414415
export type JsonValue<T> = [T]
415416
export type LogicalBounds = { position: LogicalPosition; size: LogicalSize }

crates/recording/src/capture_pipeline.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ use anyhow::anyhow;
88
use cap_timestamp::Timestamps;
99
use scap_targets::bounds::LogicalBounds;
1010
use std::{path::PathBuf, sync::Arc};
11+
#[cfg(windows)]
12+
use windows::Graphics::SizeInt32;
1113

1214
pub trait MakeCapturePipeline: ScreenCaptureFormat + std::fmt::Debug + 'static {
1315
async fn make_studio_mode_pipeline(
@@ -23,6 +25,7 @@ pub trait MakeCapturePipeline: ScreenCaptureFormat + std::fmt::Debug + 'static {
2325
system_audio: Option<screen_capture::SystemAudioSourceConfig>,
2426
mic_feed: Option<Arc<MicrophoneFeedLock>>,
2527
output_path: PathBuf,
28+
scaled_output: Option<(u32, u32)>,
2629
) -> anyhow::Result<OutputPipeline>
2730
where
2831
Self: Sized;
@@ -49,6 +52,7 @@ impl MakeCapturePipeline for screen_capture::CMSampleBufferCapture {
4952
system_audio: Option<screen_capture::SystemAudioSourceConfig>,
5053
mic_feed: Option<Arc<MicrophoneFeedLock>>,
5154
output_path: PathBuf,
55+
scaled_output: Option<(u32, u32)>,
5256
) -> anyhow::Result<OutputPipeline> {
5357
let mut output = OutputPipeline::builder(output_path.clone())
5458
.with_video::<screen_capture::VideoSource>(screen_capture);
@@ -63,7 +67,7 @@ impl MakeCapturePipeline for screen_capture::CMSampleBufferCapture {
6367

6468
output
6569
.build::<AVFoundationMp4Muxer>(AVFoundationMp4MuxerConfig {
66-
output_height: Some(1080),
70+
output_height: scaled_output.map(|(_, height)| height),
6771
})
6872
.await
6973
}
@@ -86,6 +90,7 @@ impl MakeCapturePipeline for screen_capture::Direct3DCapture {
8690
d3d_device,
8791
bitrate_multiplier: 0.1f32,
8892
frame_rate: 30u32,
93+
output_size: None,
8994
})
9095
.await
9196
}
@@ -95,6 +100,7 @@ impl MakeCapturePipeline for screen_capture::Direct3DCapture {
95100
system_audio: Option<screen_capture::SystemAudioSourceConfig>,
96101
mic_feed: Option<Arc<MicrophoneFeedLock>>,
97102
output_path: PathBuf,
103+
scaled_output: Option<(u32, u32)>,
98104
) -> anyhow::Result<OutputPipeline> {
99105
let d3d_device = screen_capture.d3d_device.clone();
100106
let mut output_builder = OutputPipeline::builder(output_path.clone())
@@ -115,6 +121,10 @@ impl MakeCapturePipeline for screen_capture::Direct3DCapture {
115121
bitrate_multiplier: 0.08f32,
116122
frame_rate: 30u32,
117123
d3d_device,
124+
output_size: scaled_output.map(|(w, h)| SizeInt32 {
125+
Width: w as i32,
126+
Height: h as i32,
127+
}),
118128
})
119129
.await
120130
}

0 commit comments

Comments
 (0)