From db0757c8e2a1e6119b3b3ea347e963fc39afc908 Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Fri, 22 May 2026 21:04:05 +0700 Subject: [PATCH] refactor(editor): extract nvidia export opt-in hook --- src/components/video-editor/VideoEditor.tsx | 60 +++--------- .../useNvidiaCudaExportOptIn.test.ts | 68 +++++++++++++ .../video-editor/useNvidiaCudaExportOptIn.ts | 96 +++++++++++++++++++ 3 files changed, 175 insertions(+), 49 deletions(-) create mode 100644 src/components/video-editor/useNvidiaCudaExportOptIn.test.ts create mode 100644 src/components/video-editor/useNvidiaCudaExportOptIn.ts diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 6308efc6..3e53a29c 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -52,7 +52,6 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover import { Toaster } from "@/components/ui/sonner"; import { useI18n } from "@/contexts/I18nContext"; import { useShortcuts } from "@/contexts/ShortcutsContext"; -import { loadAppSetting, saveAppSetting } from "@/lib/appSettings"; import { calculateOutputDimensions, DEFAULT_MP4_CODEC, @@ -89,6 +88,7 @@ import { } from "@/utils/aspectRatioUtils"; import { ExtensionIcon } from "./ExtensionIcon"; import { calculateMp4ExportDimensions, calculateMp4SourceDimensions } from "./exportDimensions"; +import { useNvidiaCudaExportOptIn } from "./useNvidiaCudaExportOptIn"; const PhCursorFill = (props: { className?: string; weight?: "fill" | "regular" }) => ( @@ -109,8 +109,6 @@ const PhSettings = (props: { className?: string; weight?: "fill" | "regular" }) ); -const NVIDIA_CUDA_EXPORT_OPT_IN_SETTING_KEY = "recordly.export.experimentalNvidiaCuda"; - import type { SourceAudioTrackSettings } from "@/components/video-editor/audio/audioTypes"; import { extensionHost } from "@/lib/extensions"; import { useVideoEditorAudio } from "./audio/useVideoEditorAudio"; @@ -554,10 +552,16 @@ export default function VideoEditor() { const [exportPipelineModel, setExportPipelineModel] = useState( initialEditorPreferences.exportPipelineModel, ); - const [nvidiaCudaExportAvailable, setNvidiaCudaExportAvailable] = useState(false); - const [experimentalNvidiaCudaExport, setExperimentalNvidiaCudaExportState] = useState( - () => loadAppSetting(NVIDIA_CUDA_EXPORT_OPT_IN_SETTING_KEY) === true, - ); + const enableModernExportPipeline = useCallback(() => { + setExportPipelineModel("modern"); + }, []); + const { + nvidiaCudaExportAvailable, + experimentalNvidiaCudaExport, + setExperimentalNvidiaCudaExport, + } = useNvidiaCudaExportOptIn({ + onEnabled: enableModernExportPipeline, + }); const [mp4FrameRate, setMp4FrameRate] = useState( initialEditorPreferences.mp4FrameRate ?? DEFAULT_MP4_EXPORT_FRAME_RATE, ); @@ -628,48 +632,6 @@ export default function VideoEditor() { }); }, []); - useEffect(() => { - let cancelled = false; - - void (async () => { - try { - const result = await window.electronAPI?.getNativeExportCapabilities?.(); - if (cancelled) { - return; - } - - const available = result?.capabilities?.nvidiaCuda.available === true; - setNvidiaCudaExportAvailable(available); - if (!available) { - setExperimentalNvidiaCudaExportState(false); - } - } catch (error) { - if (cancelled) { - return; - } - console.warn("[export] Failed to load native export capabilities", error); - setNvidiaCudaExportAvailable(false); - setExperimentalNvidiaCudaExportState(false); - } - })(); - - return () => { - cancelled = true; - }; - }, []); - - const setExperimentalNvidiaCudaExport = useCallback( - (enabled: boolean) => { - const nextEnabled = Boolean(enabled && nvidiaCudaExportAvailable); - setExperimentalNvidiaCudaExportState(nextEnabled); - saveAppSetting(NVIDIA_CUDA_EXPORT_OPT_IN_SETTING_KEY, nextEnabled); - if (nextEnabled) { - setExportPipelineModel("modern"); - } - }, - [nvidiaCudaExportAvailable], - ); - useEffect(() => { autoSuggestedVideoPathRef.current = null; pendingFreshRecordingAutoSuggestTelemetryCountRef.current = 0; diff --git a/src/components/video-editor/useNvidiaCudaExportOptIn.test.ts b/src/components/video-editor/useNvidiaCudaExportOptIn.test.ts new file mode 100644 index 00000000..d63ba97c --- /dev/null +++ b/src/components/video-editor/useNvidiaCudaExportOptIn.test.ts @@ -0,0 +1,68 @@ +import { afterEach, describe, expect, it } from "vitest"; + +import { + isNvidiaCudaExportAvailable, + loadInitialNvidiaCudaExportOptIn, + NVIDIA_CUDA_EXPORT_OPT_IN_SETTING_KEY, + resolveNvidiaCudaExportOptIn, + saveNvidiaCudaExportOptIn, +} from "./useNvidiaCudaExportOptIn"; + +function stubElectronSettings(initialValues: Record = {}) { + const store = new Map(Object.entries(initialValues)); + + Object.defineProperty(globalThis, "electronAPI", { + configurable: true, + value: { + getAppSetting: (key: string) => (store.has(key) ? store.get(key) : null), + setAppSetting: (key: string, value: unknown) => { + store.set(key, value); + return true; + }, + } as Pick, + }); + + return store; +} + +describe("nvidiaCudaExportOptIn", () => { + afterEach(() => { + Reflect.deleteProperty(globalThis, "electronAPI"); + }); + + it("only treats explicit native CUDA availability as available", () => { + expect( + isNvidiaCudaExportAvailable({ + capabilities: { nvidiaCuda: { available: true } }, + }), + ).toBe(true); + expect( + isNvidiaCudaExportAvailable({ + capabilities: { nvidiaCuda: { available: false } }, + }), + ).toBe(false); + expect(isNvidiaCudaExportAvailable({ capabilities: {} })).toBe(false); + expect(isNvidiaCudaExportAvailable(null)).toBe(false); + }); + + it("requires both user request and runtime availability before enabling", () => { + expect(resolveNvidiaCudaExportOptIn(true, true)).toBe(true); + expect(resolveNvidiaCudaExportOptIn(true, false)).toBe(false); + expect(resolveNvidiaCudaExportOptIn(false, true)).toBe(false); + expect(resolveNvidiaCudaExportOptIn(false, false)).toBe(false); + }); + + it("loads and saves the local opt-in flag through app settings", () => { + const store = stubElectronSettings({ + [NVIDIA_CUDA_EXPORT_OPT_IN_SETTING_KEY]: true, + }); + + expect(loadInitialNvidiaCudaExportOptIn()).toBe(true); + expect(saveNvidiaCudaExportOptIn(false)).toBe(true); + expect(store.get(NVIDIA_CUDA_EXPORT_OPT_IN_SETTING_KEY)).toBe(false); + }); + + it("defaults to disabled when the app setting is unavailable", () => { + expect(loadInitialNvidiaCudaExportOptIn()).toBe(false); + }); +}); diff --git a/src/components/video-editor/useNvidiaCudaExportOptIn.ts b/src/components/video-editor/useNvidiaCudaExportOptIn.ts new file mode 100644 index 00000000..a674b4ae --- /dev/null +++ b/src/components/video-editor/useNvidiaCudaExportOptIn.ts @@ -0,0 +1,96 @@ +import { useCallback, useEffect, useState } from "react"; +import { loadAppSetting, saveAppSetting } from "@/lib/appSettings"; + +export const NVIDIA_CUDA_EXPORT_OPT_IN_SETTING_KEY = + "recordly.export.experimentalNvidiaCuda"; + +type NativeExportCapabilitiesResult = { + capabilities?: { + nvidiaCuda?: { + available?: boolean; + }; + }; +} | null; + +export function isNvidiaCudaExportAvailable( + result: NativeExportCapabilitiesResult | undefined, +) { + return result?.capabilities?.nvidiaCuda?.available === true; +} + +export function resolveNvidiaCudaExportOptIn( + requested: boolean, + nvidiaCudaExportAvailable: boolean, +) { + return Boolean(requested && nvidiaCudaExportAvailable); +} + +export function loadInitialNvidiaCudaExportOptIn() { + return loadAppSetting(NVIDIA_CUDA_EXPORT_OPT_IN_SETTING_KEY) === true; +} + +export function saveNvidiaCudaExportOptIn(enabled: boolean) { + return saveAppSetting(NVIDIA_CUDA_EXPORT_OPT_IN_SETTING_KEY, enabled); +} + +export function useNvidiaCudaExportOptIn({ + onEnabled, +}: { + onEnabled?: () => void; +} = {}) { + const [nvidiaCudaExportAvailable, setNvidiaCudaExportAvailable] = useState(false); + const [experimentalNvidiaCudaExport, setExperimentalNvidiaCudaExportState] = useState( + loadInitialNvidiaCudaExportOptIn, + ); + + useEffect(() => { + let cancelled = false; + + void (async () => { + try { + const result = await window.electronAPI?.getNativeExportCapabilities?.(); + if (cancelled) { + return; + } + + const available = isNvidiaCudaExportAvailable(result); + setNvidiaCudaExportAvailable(available); + if (!available) { + setExperimentalNvidiaCudaExportState(false); + } + } catch (error) { + if (cancelled) { + return; + } + console.warn("[export] Failed to load native export capabilities", error); + setNvidiaCudaExportAvailable(false); + setExperimentalNvidiaCudaExportState(false); + } + })(); + + return () => { + cancelled = true; + }; + }, []); + + const setExperimentalNvidiaCudaExport = useCallback( + (enabled: boolean) => { + const nextEnabled = resolveNvidiaCudaExportOptIn( + enabled, + nvidiaCudaExportAvailable, + ); + setExperimentalNvidiaCudaExportState(nextEnabled); + saveNvidiaCudaExportOptIn(nextEnabled); + if (nextEnabled) { + onEnabled?.(); + } + }, + [nvidiaCudaExportAvailable, onEnabled], + ); + + return { + nvidiaCudaExportAvailable, + experimentalNvidiaCudaExport, + setExperimentalNvidiaCudaExport, + }; +}