Skip to content

Commit 940aa52

Browse files
Merge pull request #1732 from CapSoftware/ultra-recording
Studio recording quality setting + screen capture auto-restart
2 parents 64e5f01 + cb830b9 commit 940aa52

13 files changed

Lines changed: 940 additions & 35 deletions

File tree

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ pub enum EditorPreviewQuality {
4242
Full,
4343
}
4444

45+
#[derive(Default, Serialize, Deserialize, Type, Debug, Clone, Copy, PartialEq, Eq)]
46+
#[serde(rename_all = "camelCase")]
47+
pub enum StudioRecordingQuality {
48+
#[default]
49+
Balanced,
50+
Ultra,
51+
}
52+
4553
impl MainWindowRecordingStartBehaviour {
4654
pub fn perform(&self, window: &tauri::WebviewWindow) -> tauri::Result<()> {
4755
match self {
@@ -144,6 +152,8 @@ pub struct GeneralSettingsStore {
144152
#[serde(default)]
145153
pub editor_preview_quality: EditorPreviewQuality,
146154
#[serde(default)]
155+
pub studio_recording_quality: StudioRecordingQuality,
156+
#[serde(default)]
147157
pub main_window_position: Option<WindowPosition>,
148158
#[serde(default)]
149159
pub camera_window_position: Option<WindowPosition>,
@@ -228,6 +238,7 @@ impl Default for GeneralSettingsStore {
228238
max_fps: 60,
229239
transcription_hints: default_transcription_hints(),
230240
editor_preview_quality: EditorPreviewQuality::Half,
241+
studio_recording_quality: StudioRecordingQuality::Balanced,
231242
main_window_position: None,
232243
camera_window_position: None,
233244
camera_window_positions_by_monitor_name: BTreeMap::new(),

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -919,6 +919,20 @@ pub async fn start_recording(
919919
)
920920
.with_max_fps(
921921
general_settings.as_ref().map(|s| s.max_fps).unwrap_or(60),
922+
)
923+
.with_quality(
924+
match general_settings
925+
.as_ref()
926+
.map(|s| s.studio_recording_quality)
927+
.unwrap_or_default()
928+
{
929+
crate::general_settings::StudioRecordingQuality::Balanced => {
930+
cap_recording::StudioQuality::Balanced
931+
}
932+
crate::general_settings::StudioRecordingQuality::Ultra => {
933+
cap_recording::StudioQuality::Ultra
934+
}
935+
},
922936
);
923937

924938
#[cfg(target_os = "macos")]

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
type MainWindowRecordingStartBehaviour,
3939
type PostDeletionBehaviour,
4040
type PostStudioRecordingBehaviour,
41+
type StudioRecordingQuality,
4142
type WindowExclusion,
4243
} from "~/utils/tauri";
4344
import IconLucidePlus from "~icons/lucide/plus";
@@ -328,6 +329,7 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) {
328329
| MainWindowRecordingStartBehaviour
329330
| PostStudioRecordingBehaviour
330331
| PostDeletionBehaviour
332+
| StudioRecordingQuality
331333
| number,
332334
>(props: {
333335
label: string;
@@ -423,6 +425,16 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) {
423425
)}
424426

425427
<SettingGroup title="Recording">
428+
<SelectSettingItem
429+
label="Studio mode quality"
430+
description="Balanced uses less storage and CPU. Ultra records at higher bitrate for maximum quality."
431+
value={settings.studioRecordingQuality ?? "balanced"}
432+
onChange={(value) => handleChange("studioRecordingQuality", value)}
433+
options={[
434+
{ text: "Balanced", value: "balanced" as StudioRecordingQuality },
435+
{ text: "Ultra", value: "ultra" as StudioRecordingQuality },
436+
]}
437+
/>
426438
<SelectSettingItem
427439
label="Instant mode max resolution"
428440
description="Choose the maximum resolution for Instant Mode recordings."

apps/desktop/src/utils/tauri.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,7 @@ export type ExportSettings = ({ format: "Mp4" } & Mp4ExportSettings) | ({ format
486486
export type FileType = "recording" | "screenshot"
487487
export type Flags = { captions: boolean }
488488
export type FramesRendered = { renderedCount: number; totalFrames: number; type: "FramesRendered" }
489-
export type GeneralSettingsStore = { instanceId?: string; uploadIndividualFiles?: boolean; hideDockIcon?: 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; captureKeyboardEvents?: boolean; postDeletionBehaviour?: PostDeletionBehaviour; excludedWindows?: WindowExclusion[]; deleteInstantRecordingsAfterUpload?: boolean; instantModeMaxResolution?: number; defaultProjectNameTemplate?: string | null; crashRecoveryRecording?: boolean; maxFps?: number; transcriptionHints?: string[]; editorPreviewQuality?: EditorPreviewQuality; mainWindowPosition?: WindowPosition | null; cameraWindowPosition?: WindowPosition | null; cameraWindowPositionsByMonitorName?: { [key in string]: WindowPosition }; hasCompletedOnboarding?: boolean }
489+
export type GeneralSettingsStore = { instanceId?: string; uploadIndividualFiles?: boolean; hideDockIcon?: 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; captureKeyboardEvents?: boolean; postDeletionBehaviour?: PostDeletionBehaviour; excludedWindows?: WindowExclusion[]; deleteInstantRecordingsAfterUpload?: boolean; instantModeMaxResolution?: number; defaultProjectNameTemplate?: string | null; crashRecoveryRecording?: boolean; maxFps?: number; transcriptionHints?: string[]; editorPreviewQuality?: EditorPreviewQuality; studioRecordingQuality?: StudioRecordingQuality; mainWindowPosition?: WindowPosition | null; cameraWindowPosition?: WindowPosition | null; cameraWindowPositionsByMonitorName?: { [key in string]: WindowPosition }; hasCompletedOnboarding?: boolean }
490490
export type GifExportSettings = { fps: number; resolution_base: XY<number>; quality: GifQuality | null }
491491
export type GifQuality = {
492492
/**
@@ -581,6 +581,7 @@ export type SingleSegment = { display: VideoMeta; camera?: VideoMeta | null; aud
581581
export type StartRecordingInputs = { capture_target: ScreenCaptureTarget; capture_system_audio?: boolean; mode: RecordingMode; organization_id?: string | null }
582582
export type StereoMode = "stereo" | "monoL" | "monoR"
583583
export type StudioRecordingMeta = { segment: SingleSegment } | { inner: MultipleSegments }
584+
export type StudioRecordingQuality = "balanced" | "ultra"
584585
export type StudioRecordingStatus = { status: "InProgress" } | { status: "NeedsRemux" } | { status: "Failed"; error: string } | { status: "Complete" }
585586
export type SystemDiagnostics = { macosVersion: MacOSVersionInfo | null; availableEncoders: string[]; screenCaptureSupported: boolean; metalSupported: boolean; gpuName: string | null }
586587
export type TargetUnderCursor = { display_id: DisplayId | null; window: WindowUnderCursor | null }

crates/enc-avfoundation/src/mp4.rs

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,30 @@ impl MP4Encoder {
119119
audio_config: Option<AudioInfo>,
120120
output_height: Option<u32>,
121121
) -> Result<Self, InitError> {
122-
Self::init_with_options(output, video_config, audio_config, output_height, false)
122+
Self::init_with_options(
123+
output,
124+
video_config,
125+
audio_config,
126+
output_height,
127+
false,
128+
false,
129+
)
130+
}
131+
132+
pub fn init_ultra(
133+
output: PathBuf,
134+
video_config: VideoInfo,
135+
audio_config: Option<AudioInfo>,
136+
output_height: Option<u32>,
137+
) -> Result<Self, InitError> {
138+
Self::init_with_options(
139+
output,
140+
video_config,
141+
audio_config,
142+
output_height,
143+
false,
144+
true,
145+
)
123146
}
124147

125148
pub fn init_instant_mode(
@@ -128,7 +151,14 @@ impl MP4Encoder {
128151
audio_config: Option<AudioInfo>,
129152
output_height: Option<u32>,
130153
) -> Result<Self, InitError> {
131-
Self::init_with_options(output, video_config, audio_config, output_height, true)
154+
Self::init_with_options(
155+
output,
156+
video_config,
157+
audio_config,
158+
output_height,
159+
true,
160+
false,
161+
)
132162
}
133163

134164
fn init_with_options(
@@ -137,6 +167,7 @@ impl MP4Encoder {
137167
audio_config: Option<AudioInfo>,
138168
output_height: Option<u32>,
139169
instant_mode: bool,
170+
ultra_quality: bool,
140171
) -> Result<Self, InitError> {
141172
info!(
142173
width = video_config.width,
@@ -227,18 +258,22 @@ impl MP4Encoder {
227258

228259
let bitrate = if instant_mode {
229260
get_instant_mode_bitrate(output_width as f32, output_height as f32, fps)
261+
} else if ultra_quality {
262+
get_ultra_bitrate(output_width as f32, output_height as f32, fps)
230263
} else {
231264
get_average_bitrate(output_width as f32, output_height as f32, fps)
232265
};
233266

234-
debug!(instant_mode, "recording bitrate: {bitrate}");
267+
debug!(instant_mode, ultra_quality, "recording bitrate: {bitrate}");
235268

236269
let keyframe_interval = if instant_mode {
237270
fps as i32
238271
} else {
239272
(fps * 2.0) as i32
240273
};
241274

275+
let allow_frame_reordering = ultra_quality && !instant_mode;
276+
242277
output_settings.insert(
243278
av::video_settings_keys::compression_props(),
244279
ns::Dictionary::with_keys_values(
@@ -250,7 +285,7 @@ impl MP4Encoder {
250285
],
251286
&[
252287
ns::Number::with_f32(bitrate).as_id_ref(),
253-
ns::Number::with_bool(false).as_id_ref(),
288+
ns::Number::with_bool(allow_frame_reordering).as_id_ref(),
254289
ns::Number::with_f32(fps).as_id_ref(),
255290
ns::Number::with_i32(keyframe_interval).as_id_ref(),
256291
],
@@ -935,10 +970,19 @@ fn timescale_value_to_duration(value: i64, timescale: i32) -> Duration {
935970
Duration::from_nanos(nanos)
936971
}
937972

973+
const MIN_STUDIO_BITRATE: f32 = 8_000_000.0;
974+
const MAX_ULTRA_BITRATE: f32 = 120_000_000.0;
975+
938976
fn get_average_bitrate(width: f32, height: f32, fps: f32) -> f32 {
939-
5_000_000.0
940-
+ width * height / (1920.0 * 1080.0) * 2_000_000.0
941-
+ fps.min(60.0) / 30.0 * 5_000_000.0
977+
let pixels = width * height;
978+
let fps_factor = fps.min(60.0) / 30.0;
979+
(pixels * fps_factor * 5.0).max(MIN_STUDIO_BITRATE)
980+
}
981+
982+
fn get_ultra_bitrate(width: f32, height: f32, fps: f32) -> f32 {
983+
let pixels = width * height;
984+
let fps_factor = fps.min(60.0) / 30.0;
985+
(pixels * fps_factor * 10.0).clamp(MIN_STUDIO_BITRATE, MAX_ULTRA_BITRATE)
942986
}
943987

944988
fn get_instant_mode_bitrate(width: f32, height: f32, fps: f32) -> f32 {

crates/enc-ffmpeg/src/video/h264.rs

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ pub enum H264EncoderError {
5353

5454
impl H264EncoderBuilder {
5555
pub const QUALITY_BPP: f32 = 0.3;
56+
pub const ULTRA_BPP: f32 = 1.0;
5657
pub const INSTANT_MODE_BPP: f32 = 0.15;
5758

5859
pub fn new(input_config: VideoInfo) -> Self {
@@ -808,17 +809,12 @@ fn get_codec_and_options(
808809
options.set("trellis", "0");
809810
}
810811
} else {
811-
options.set(
812-
"preset",
813-
match preset {
814-
H264Preset::Slow => "slow",
815-
H264Preset::Medium => "medium",
816-
H264Preset::Ultrafast | H264Preset::HighThroughput => "ultrafast",
817-
},
818-
);
819-
if matches!(preset, H264Preset::Ultrafast | H264Preset::HighThroughput) {
820-
options.set("tune", "zerolatency");
821-
}
812+
let realtime_preset = match preset {
813+
H264Preset::Slow | H264Preset::Medium => "veryfast",
814+
H264Preset::Ultrafast | H264Preset::HighThroughput => "ultrafast",
815+
};
816+
options.set("preset", realtime_preset);
817+
options.set("tune", "zerolatency");
822818
}
823819
options.set("vsync", "1");
824820
options.set("g", &keyframe_interval_str);

0 commit comments

Comments
 (0)