Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ tracing = "0.1.41"
futures = "0.3.31"

cidre = { git = "https://github.com/CapSoftware/cidre", rev = "bf84b67079a8", features = [
"macos_13_0",
"macos_12_7",
"cv",
"cf",
"core_audio",
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1873,6 +1873,7 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
show_window,
write_clipboard_string,
platform::perform_haptic_feedback,
platform::is_system_audio_capture_supported,
list_fails,
set_fail,
update_auth_plan,
Expand Down
20 changes: 20 additions & 0 deletions apps/desktop/src-tauri/src/platform/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,23 @@ pub fn perform_haptic_feedback(
#[cfg(not(target_os = "macos"))]
Err("Haptics are only supported on macOS.".into())
}

/// Check if system audio capture is supported on the current platform and OS version.
/// On macOS, system audio capture requires macOS 13.0 or later.
/// On Windows/Linux, this may have different requirements.
#[tauri::command]
#[specta::specta]
#[instrument]
pub fn is_system_audio_capture_supported() -> bool {
#[cfg(target_os = "macos")]
{
scap_screencapturekit::is_system_audio_supported()
}

#[cfg(not(target_os = "macos"))]
{
// On Windows/Linux, we assume system audio capture is available
// This can be refined later based on platform-specific requirements
true
}
}
23 changes: 20 additions & 3 deletions apps/desktop/src/routes/(window-chrome)/(main).tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
createLicenseQuery,
createVideoDevicesQuery,
getPermissions,
isSystemAudioSupported,
listAudioDevices,
listScreens,
listWindows,
Expand Down Expand Up @@ -952,15 +953,25 @@ function MicrophoneSelect(props: {
function SystemAudio() {
const { rawOptions, setOptions } = useRecordingOptions();
const currentRecording = createCurrentRecordingQuery();
const systemAudioSupported = createQuery(() => isSystemAudioSupported);

return (
const isDisabled = () =>
!!currentRecording.data || systemAudioSupported.data === false;
const tooltipMessage = () => {
if (systemAudioSupported.data === false) {
return "System audio capture requires macOS 13.0 or later";
}
return undefined;
};

const button = (
<button
type="button"
onClick={() => {
if (!rawOptions) return;
if (!rawOptions || isDisabled()) return;
setOptions({ captureSystemAudio: !rawOptions.captureSystemAudio });
}}
disabled={!!currentRecording.data}
disabled={isDisabled()}
class="relative flex flex-row items-center h-[2rem] px-[0.375rem] gap-[0.375rem] border rounded-lg border-gray-3 w-full disabled:text-gray-11 transition-colors KSelect overflow-hidden z-10"
>
<div class="size-[1.25rem] flex items-center justify-center">
Expand All @@ -976,6 +987,12 @@ function SystemAudio() {
</InfoPill>
</button>
);

return tooltipMessage() ? (
<Tooltip content={tooltipMessage()!}>{button}</Tooltip>
) : (
button
);
}

function TargetSelect<T extends { id: string; name: string }>(props: {
Expand Down
29 changes: 25 additions & 4 deletions apps/desktop/src/routes/(window-chrome)/new-main/SystemAudio.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
import { createCurrentRecordingQuery } from "~/utils/queries";
import { createQuery } from "@tanstack/solid-query";
import Tooltip from "~/components/Tooltip";
import {
createCurrentRecordingQuery,
isSystemAudioSupported,
} from "~/utils/queries";
import { useRecordingOptions } from "../OptionsContext";
import InfoPill from "./InfoPill";

export default function SystemAudio() {
const { rawOptions, setOptions } = useRecordingOptions();
const currentRecording = createCurrentRecordingQuery();
const systemAudioSupported = createQuery(() => isSystemAudioSupported);

return (
const isDisabled = () =>
!!currentRecording.data || systemAudioSupported.data === false;
const tooltipMessage = () => {
if (systemAudioSupported.data === false) {
return "System audio capture requires macOS 13.0 or later";
}
return undefined;
};

const button = (
<button
onClick={() => {
if (!rawOptions) return;
if (!rawOptions || isDisabled()) return;
setOptions({ captureSystemAudio: !rawOptions.captureSystemAudio });
}}
disabled={!!currentRecording.data}
disabled={isDisabled()}
class="flex flex-row gap-2 items-center px-2 w-full h-9 rounded-lg transition-colors curosr-default disabled:opacity-70 bg-gray-3 disabled:text-gray-11 KSelect"
>
<IconPhMonitorBold class="text-gray-10 size-4" />
Expand All @@ -26,4 +41,10 @@ export default function SystemAudio() {
</InfoPill>
</button>
);

return tooltipMessage() ? (
<Tooltip content={tooltipMessage()!}>{button}</Tooltip>
) : (
button
);
}
6 changes: 6 additions & 0 deletions apps/desktop/src/utils/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ export const getPermissions = queryOptions({
refetchInterval: 1000,
});

export const isSystemAudioSupported = queryOptions({
queryKey: ["systemAudioSupported"] as const,
queryFn: () => commands.isSystemAudioCaptureSupported(),
staleTime: Number.POSITIVE_INFINITY, // This won't change during runtime
});

export function createOptionsQuery() {
const PERSIST_KEY = "recording-options-query-2";
const [_state, _setState] = createStore<{
Expand Down
19 changes: 12 additions & 7 deletions apps/desktop/src/utils/tauri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,14 @@ async writeClipboardString(text: string) : Promise<null> {
async performHapticFeedback(pattern: HapticPattern | null, time: HapticPerformanceTime | null) : Promise<null> {
return await TAURI_INVOKE("perform_haptic_feedback", { pattern, time });
},
/**
* Check if system audio capture is supported on the current platform and OS version.
* On macOS, system audio capture requires macOS 13.0 or later.
* On Windows/Linux, this may have different requirements.
*/
async isSystemAudioCaptureSupported() : Promise<boolean> {
return await TAURI_INVOKE("is_system_audio_capture_supported");
},
async listFails() : Promise<{ [key in string]: boolean }> {
return await TAURI_INVOKE("list_fails");
},
Expand Down Expand Up @@ -301,7 +309,6 @@ recordingOptionsChanged: RecordingOptionsChanged,
recordingStarted: RecordingStarted,
recordingStopped: RecordingStopped,
renderFrameEvent: RenderFrameEvent,
requestNewScreenshot: RequestNewScreenshot,
requestOpenRecordingPicker: RequestOpenRecordingPicker,
requestOpenSettings: RequestOpenSettings,
requestScreenCapturePrewarm: RequestScreenCapturePrewarm,
Expand All @@ -323,7 +330,6 @@ recordingOptionsChanged: "recording-options-changed",
recordingStarted: "recording-started",
recordingStopped: "recording-stopped",
renderFrameEvent: "render-frame-event",
requestNewScreenshot: "request-new-screenshot",
requestOpenRecordingPicker: "request-open-recording-picker",
requestOpenSettings: "request-open-settings",
requestScreenCapturePrewarm: "request-screen-capture-prewarm",
Expand All @@ -343,7 +349,7 @@ export type AspectRatio = "wide" | "vertical" | "square" | "classic" | "tall"
export type Audio = { duration: number; sample_rate: number; channels: number; start_time: number }
export type AudioConfiguration = { mute: boolean; improve: boolean; micVolumeDb?: number; micStereoMode?: StereoMode; systemVolumeDb?: number }
export type AudioInputLevelChange = number
export type AudioMeta = { path: string;
export type AudioMeta = { path: string;
/**
* unix time of the first frame
*/
Expand Down Expand Up @@ -395,11 +401,11 @@ export type Flags = { captions: boolean }
export type FramesRendered = { renderedCount: number; totalFrames: number; type: "FramesRendered" }
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; instantModeMaxResolution?: number }
export type GifExportSettings = { fps: number; resolution_base: XY<number>; quality: GifQuality | null }
export type GifQuality = {
export type GifQuality = {
/**
* Encoding quality from 1-100 (default: 90)
*/
quality: number | null;
quality: number | null;
/**
* Whether to prioritize speed over quality (default: false)
*/
Expand Down Expand Up @@ -449,7 +455,6 @@ export type RecordingStarted = null
export type RecordingStopped = null
export type RecordingTargetMode = "display" | "window" | "area"
export type RenderFrameEvent = { frame_number: number; fps: number; resolution_base: XY<number> }
export type RequestNewScreenshot = null
export type RequestOpenRecordingPicker = { target_mode: RecordingTargetMode | null }
export type RequestOpenSettings = { page: string }
export type RequestScreenCapturePrewarm = { force?: boolean }
Expand Down Expand Up @@ -477,7 +482,7 @@ export type UploadProgress = { progress: number }
export type UploadProgressEvent = { video_id: string; uploaded: string; total: string }
export type UploadResult = { Success: string } | "NotAuthenticated" | "PlanCheckFailed" | "UpgradeRequired"
export type Video = { duration: number; width: number; height: number; fps: number; start_time: number }
export type VideoMeta = { path: string; fps?: number;
export type VideoMeta = { path: string; fps?: number;
/**
* unix time of the first frame
*/
Expand Down
11 changes: 8 additions & 3 deletions crates/audio/src/bin/macos-audio-capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ mod macos {
use std::{sync::mpsc::Sender, time::Duration};

use cidre::{
cm, define_obj_type, ns, objc,
api, cm, define_obj_type, ns, objc,
sc::{
self,
stream::{Output, OutputImpl},
Expand Down Expand Up @@ -63,8 +63,13 @@ mod macos {

pub async fn main() {
let mut cfg = sc::StreamCfg::new();
cfg.set_captures_audio(true);
cfg.set_excludes_current_process_audio(false);

if api::macos_available("13.0") {
unsafe {
cfg.set_captures_audio(true);
cfg.set_excludes_current_process_audio(false);
}
}

let content = sc::ShareableContent::current().await.expect("content");
let display = &content.displays().get(0).unwrap();
Expand Down
11 changes: 7 additions & 4 deletions crates/camera-avfoundation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ use std::{
use tracing::warn;

pub fn list_video_devices() -> arc::R<ns::Array<av::CaptureDevice>> {
let mut device_types = vec![
av::CaptureDeviceType::built_in_wide_angle_camera(),
av::CaptureDeviceType::desk_view_camera(),
];
let mut device_types = vec![av::CaptureDeviceType::built_in_wide_angle_camera()];

if api::macos_available("13.0")
&& let Some(typ) = unsafe { av::CaptureDeviceType::desk_view_camera() }
{
device_types.push(typ);
}

if api::macos_available("14.0") {
if let Some(typ) = unsafe { av::CaptureDeviceType::external() } {
Expand Down
2 changes: 1 addition & 1 deletion crates/scap-screencapturekit/src/capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ impl CapturerBuilder {

let queue = dispatch::Queue::serial_with_ar_pool();

if self.config.captures_audio() {
if crate::is_system_audio_supported() && unsafe { self.config.captures_audio() } {
stream
.add_stream_output(callbacks.as_ref(), sc::OutputType::Audio, Some(&queue))
.map_err(|e| e.retained())?;
Expand Down
7 changes: 6 additions & 1 deletion crates/scap-screencapturekit/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,13 @@ impl StreamCfgBuilder {
});
}

// Only supported on macOS 13.0+
pub fn set_captures_audio(&mut self, captures_audio: bool) {
self.0.set_captures_audio(captures_audio);
if crate::is_system_audio_supported() {
unsafe {
self.0.set_captures_audio(captures_audio);
}
}
}

/// Logical width of the capture area
Expand Down
6 changes: 6 additions & 0 deletions crates/scap-screencapturekit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,9 @@ mod permission;
pub use capture::{AudioFrame, Capturer, CapturerBuilder, Frame, VideoFrame};
pub use config::StreamCfgBuilder;
pub use permission::{has_permission, request_permission};

/// Check if system audio capture is supported on the current macOS version.
/// System audio capture via ScreenCaptureKit requires macOS 13.0 or later.
pub fn is_system_audio_supported() -> bool {
cidre::api::macos_available("13.0")
}