diff --git a/Cargo.toml b/Cargo.toml
index f18ad9c2be..0b35f66d45 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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",
diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs
index d6ae97bfc3..115008412b 100644
--- a/apps/desktop/src-tauri/src/lib.rs
+++ b/apps/desktop/src-tauri/src/lib.rs
@@ -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,
diff --git a/apps/desktop/src-tauri/src/platform/mod.rs b/apps/desktop/src-tauri/src/platform/mod.rs
index 267ec61b9c..b409f600d7 100644
--- a/apps/desktop/src-tauri/src/platform/mod.rs
+++ b/apps/desktop/src-tauri/src/platform/mod.rs
@@ -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
+ }
+}
diff --git a/apps/desktop/src/routes/(window-chrome)/(main).tsx b/apps/desktop/src/routes/(window-chrome)/(main).tsx
index 5a98956cc4..dcfbe8a86e 100644
--- a/apps/desktop/src/routes/(window-chrome)/(main).tsx
+++ b/apps/desktop/src/routes/(window-chrome)/(main).tsx
@@ -1,10 +1,6 @@
import { Button } from "@cap/ui-solid";
import { useNavigate } from "@solidjs/router";
-import {
- createMutation,
- createQuery,
- useQueryClient,
-} from "@tanstack/solid-query";
+import { createMutation, createQuery } from "@tanstack/solid-query";
import { getVersion } from "@tauri-apps/api/app";
import { getCurrentWindow, LogicalSize } from "@tauri-apps/api/window";
import { cx } from "cva";
@@ -12,7 +8,6 @@ import {
type ComponentProps,
createEffect,
createResource,
- createSignal,
ErrorBoundary,
onCleanup,
onMount,
@@ -29,7 +24,6 @@ import {
createCurrentRecordingQuery,
createLicenseQuery,
createVideoDevicesQuery,
- getPermissions,
listAudioDevices,
listScreens,
listWindows,
@@ -39,7 +33,6 @@ import {
type CaptureDisplay,
type CaptureWindow,
commands,
- events,
type RecordingMode,
type ScreenCaptureTarget,
} from "~/utils/tauri";
@@ -172,10 +165,11 @@ function Page() {
cameraID: () =>
cameras.find((c) => {
const { cameraID } = rawOptions;
- if (!cameraID) return;
+ if (!cameraID) return null;
if ("ModelID" in cameraID && c.model_id === cameraID.ModelID) return c;
if ("DeviceID" in cameraID && c.device_id === cameraID.DeviceID)
return c;
+ return null;
}),
micName: () => mics.data?.find((name: any) => name === rawOptions.micName),
};
@@ -558,29 +552,9 @@ function Page() {
);
}
-function useRequestPermission() {
- const queryClient = useQueryClient();
-
- async function requestPermission(type: "camera" | "microphone") {
- try {
- if (type === "camera") {
- await commands.resetCameraPermissions();
- } else if (type === "microphone") {
- await commands.resetMicrophonePermissions();
- }
- await commands.requestPermission(type);
- await queryClient.refetchQueries(getPermissions);
- } catch (error) {
- console.error(`Failed to get ${type} permission:`, error);
- }
- }
-
- return requestPermission;
-}
-
import { createEventListener } from "@solid-primitives/event-listener";
import { makePersisted } from "@solid-primitives/storage";
-import { CheckMenuItem, Menu, PredefinedMenuItem } from "@tauri-apps/api/menu";
+import { CheckMenuItem, Menu } from "@tauri-apps/api/menu";
import {
getCurrentWebviewWindow,
WebviewWindow,
@@ -591,10 +565,12 @@ import * as updater from "@tauri-apps/plugin-updater";
import { Transition } from "solid-transition-group";
import { SignInButton } from "~/components/SignInButton";
import { authStore, generalSettingsStore } from "~/store";
-import { createTauriEventListener } from "~/utils/createEventListener";
import { handleRecordingResult } from "~/utils/recording";
import { apiClient } from "~/utils/web-api";
import { WindowChromeHeader } from "./Context";
+import { CameraSelectBase } from "./new-main/CameraSelect";
+import { MicrophoneSelectBase } from "./new-main/MicrophoneSelect";
+import { SystemAudioToggleRoot } from "./new-main/SystemAudio";
import {
RecordingOptionsProvider,
useRecordingOptions,
@@ -761,220 +737,50 @@ function AreaSelectButton(props: {
);
}
-const NO_CAMERA = "No Camera";
-
function CameraSelect(props: {
disabled?: boolean;
options: CameraInfo[];
value: CameraInfo | null;
onChange: (cameraInfo: CameraInfo | null) => void;
}) {
- const currentRecording = createCurrentRecordingQuery();
- const permissions = createQuery(() => getPermissions);
- const requestPermission = useRequestPermission();
-
- const permissionGranted = () =>
- permissions?.data?.camera === "granted" ||
- permissions?.data?.camera === "notNeeded";
-
- const onChange = (cameraInfo: CameraInfo | null) => {
- if (!cameraInfo && !permissionGranted()) return requestPermission("camera");
-
- props.onChange(cameraInfo);
-
- trackEvent("camera_selected", {
- camera_name: cameraInfo,
- enabled: !!cameraInfo,
- });
- };
-
return (
-
-
-
+
);
}
-const NO_MICROPHONE = "No Microphone";
-
function MicrophoneSelect(props: {
disabled?: boolean;
options: string[];
value: string | null;
onChange: (micName: string | null) => void;
}) {
- const DB_SCALE = 40;
-
- const permissions = createQuery(() => getPermissions);
- const currentRecording = createCurrentRecordingQuery();
-
- const [dbs, setDbs] = createSignal();
-
- const requestPermission = useRequestPermission();
-
- const permissionGranted = () =>
- permissions?.data?.microphone === "granted" ||
- permissions?.data?.microphone === "notNeeded";
-
- type Option = { name: string };
-
- const handleMicrophoneChange = async (item: Option | null) => {
- if (!props.options) return;
-
- props.onChange(item ? item.name : null);
- if (!item) setDbs();
-
- trackEvent("microphone_selected", {
- microphone_name: item?.name ?? null,
- enabled: !!item,
- });
- };
-
- createTauriEventListener(events.audioInputLevelChange, (dbs) => {
- if (!props.value) setDbs();
- else setDbs(dbs);
- });
-
- // visual audio level from 0 -> 1
- const audioLevel = () =>
- (1 - Math.max((dbs() ?? 0) + DB_SCALE, 0) / DB_SCALE) ** 0.5;
-
return (
-
-
-
+
);
}
function SystemAudio() {
- const { rawOptions, setOptions } = useRecordingOptions();
- const currentRecording = createCurrentRecordingQuery();
-
return (
-
+ PillComponent={InfoPill}
+ icon={
+
+
+
+ }
+ />
);
}
@@ -1038,39 +844,6 @@ function TargetSelect(props: {
);
}
-function TargetSelectInfoPill(props: {
- value: T | null;
- permissionGranted: boolean;
- requestPermission: () => void;
- onClick: (e: MouseEvent) => void;
-}) {
- return (
- {
- if (!props.permissionGranted || props.value === null) return;
-
- e.stopPropagation();
- }}
- onClick={(e) => {
- if (!props.permissionGranted) {
- props.requestPermission();
- e.stopPropagation();
- return;
- }
-
- props.onClick(e);
- }}
- >
- {!props.permissionGranted
- ? "Request Permission"
- : props.value !== null
- ? "On"
- : "Off"}
-
- );
-}
-
function InfoPill(
props: ComponentProps<"button"> & { variant: "blue" | "red" },
) {
diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/CameraSelect.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/CameraSelect.tsx
index 83a28b7fa8..ab95e90f45 100644
--- a/apps/desktop/src/routes/(window-chrome)/new-main/CameraSelect.tsx
+++ b/apps/desktop/src/routes/(window-chrome)/new-main/CameraSelect.tsx
@@ -1,8 +1,10 @@
import { createQuery } from "@tanstack/solid-query";
import { CheckMenuItem, Menu, PredefinedMenuItem } from "@tauri-apps/api/menu";
+import type { Component, ComponentProps } from "solid-js";
import { trackEvent } from "~/utils/analytics";
import { createCurrentRecordingQuery, getPermissions } from "~/utils/queries";
import type { CameraInfo } from "~/utils/tauri";
+import InfoPill from "./InfoPill";
import TargetSelectInfoPill from "./TargetSelectInfoPill";
import useRequestPermission from "./useRequestPermission";
@@ -13,6 +15,27 @@ export default function CameraSelect(props: {
options: CameraInfo[];
value: CameraInfo | null;
onChange: (camera: CameraInfo | null) => void;
+}) {
+ return (
+
+ );
+}
+
+export function CameraSelectBase(props: {
+ disabled?: boolean;
+ options: CameraInfo[];
+ value: CameraInfo | null;
+ onChange: (camera: CameraInfo | null) => void;
+ PillComponent: Component<
+ ComponentProps<"button"> & { variant: "blue" | "red" }
+ >;
+ class: string;
+ iconClass: string;
}) {
const currentRecording = createCurrentRecordingQuery();
const permissions = createQuery(() => getPermissions);
@@ -23,7 +46,7 @@ export default function CameraSelect(props: {
permissions?.data?.camera === "notNeeded";
const onChange = (cameraLabel: CameraInfo | null) => {
- if (!cameraLabel && permissions?.data?.camera !== "granted")
+ if (!cameraLabel && !permissionGranted())
return requestPermission("camera");
props.onChange(cameraLabel);
@@ -37,9 +60,14 @@ export default function CameraSelect(props: {
return (