mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-26 07:45:34 +00:00
Add NVIDIA CUDA export compositor
This commit is contained in:
@@ -37,6 +37,7 @@ vite.config.d.ts
|
||||
electron/native/wgc-capture/build/
|
||||
electron/native/cursor-monitor/build/
|
||||
electron/native/gpu-export-probe/build/
|
||||
electron/native/nvidia-cuda-compositor/build/
|
||||
electron/native/bin/*/whisper-*
|
||||
electron/native/bin/*/whisper-runtime.json
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ vi.mock("../ffmpeg/binary", () => ({
|
||||
|
||||
import {
|
||||
buildNativeVideoAudioMuxArgs,
|
||||
getNvidiaCudaAudioExportSkipReason,
|
||||
normalizeNativeStaticLayoutBackground,
|
||||
parseFfmpegDurationSeconds,
|
||||
parseFfmpegFrameRate,
|
||||
@@ -21,8 +22,29 @@ import {
|
||||
parseNvidiaCudaExportSummary,
|
||||
parseWindowsGpuExportProgressLine,
|
||||
parseWindowsGpuExportSummary,
|
||||
validateNvidiaCudaExportSummary,
|
||||
} from "./native-video";
|
||||
|
||||
function withNvidiaCudaAudioOverride<T>(value: string | undefined, callback: () => T) {
|
||||
const envName = "RECORDLY_NVIDIA_CUDA_ALLOW_AUDIO_EXPORT";
|
||||
const originalValue = process.env[envName];
|
||||
if (value === undefined) {
|
||||
delete process.env[envName];
|
||||
} else {
|
||||
process.env[envName] = value;
|
||||
}
|
||||
|
||||
try {
|
||||
return callback();
|
||||
} finally {
|
||||
if (originalValue === undefined) {
|
||||
delete process.env[envName];
|
||||
} else {
|
||||
process.env[envName] = originalValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("normalizeNativeStaticLayoutBackground", () => {
|
||||
it("falls back to a solid background when the configured image file is missing", async () => {
|
||||
const normalized = await normalizeNativeStaticLayoutBackground({
|
||||
@@ -46,6 +68,35 @@ describe("normalizeNativeStaticLayoutBackground", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getNvidiaCudaAudioExportSkipReason", () => {
|
||||
it("allows video-only CUDA exports by default", () => {
|
||||
withNvidiaCudaAudioOverride(undefined, () => {
|
||||
expect(getNvidiaCudaAudioExportSkipReason(undefined)).toBeNull();
|
||||
expect(getNvidiaCudaAudioExportSkipReason("none")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("guards CUDA for audio exports unless explicitly overridden", () => {
|
||||
withNvidiaCudaAudioOverride(undefined, () => {
|
||||
expect(getNvidiaCudaAudioExportSkipReason("copy-source")).toBe(
|
||||
"audio-mode:copy-source",
|
||||
);
|
||||
expect(getNvidiaCudaAudioExportSkipReason("trim-source")).toBe(
|
||||
"audio-mode:trim-source",
|
||||
);
|
||||
expect(getNvidiaCudaAudioExportSkipReason("edited-track")).toBe(
|
||||
"audio-mode:edited-track",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("allows CUDA audio exports only for explicit lab overrides", () => {
|
||||
withNvidiaCudaAudioOverride("1", () => {
|
||||
expect(getNvidiaCudaAudioExportSkipReason("copy-source")).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildNativeVideoAudioMuxArgs", () => {
|
||||
it("stream-copies source audio and preserves the requested video duration", () => {
|
||||
const args = buildNativeVideoAudioMuxArgs("video.mp4", "source.mp4", "out.mp4", {
|
||||
@@ -151,6 +202,87 @@ describe("parseNvidiaCudaExportSummary", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateNvidiaCudaExportSummary", () => {
|
||||
it("accepts CUDA output when frames and stream durations match the export target", () => {
|
||||
const issues = validateNvidiaCudaExportSummary(
|
||||
{
|
||||
success: true,
|
||||
targetFrames: 300,
|
||||
durationSec: 10,
|
||||
nativeSummary: { success: true, frames: 300 },
|
||||
outputVideo: { duration: "9.999900", nb_frames: "300" },
|
||||
outputAudio: { duration: "10.005000" },
|
||||
},
|
||||
{ durationSec: 10, targetFrames: 300 },
|
||||
);
|
||||
|
||||
expect(issues).toEqual([]);
|
||||
});
|
||||
|
||||
it("rejects CUDA output that reports too few frames or a short video stream", () => {
|
||||
const issues = validateNvidiaCudaExportSummary(
|
||||
{
|
||||
success: true,
|
||||
targetFrames: 300,
|
||||
durationSec: 10,
|
||||
nativeSummary: { success: true, frames: 144 },
|
||||
outputVideo: { duration: "4.799952", nb_frames: "144" },
|
||||
outputAudio: { duration: "10.005000" },
|
||||
},
|
||||
{ durationSec: 10, targetFrames: 300 },
|
||||
);
|
||||
|
||||
expect(issues).toEqual([
|
||||
"native frames 144 below expected minimum 285",
|
||||
"output video frames 144 below expected minimum 285",
|
||||
"output video duration 4.800s differs from expected 10.000s",
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects audio CUDA output unless the helper reports timestamp-aligned frame selection", () => {
|
||||
const issues = validateNvidiaCudaExportSummary(
|
||||
{
|
||||
success: true,
|
||||
targetFrames: 300,
|
||||
durationSec: 10,
|
||||
nativeSummary: {
|
||||
success: true,
|
||||
frames: 300,
|
||||
selectionStage: "decoder-policy-mapped-callback",
|
||||
},
|
||||
outputVideo: { duration: "9.999900", nb_frames: "300" },
|
||||
outputAudio: { duration: "10.005000" },
|
||||
},
|
||||
{ durationSec: 10, targetFrames: 300, requiresTimelineSync: true },
|
||||
);
|
||||
|
||||
expect(issues).toEqual([
|
||||
"CUDA timeline mode is not timestamp-aligned for audio export",
|
||||
]);
|
||||
});
|
||||
|
||||
it("accepts audio CUDA output when the helper reports PTS-aligned selection", () => {
|
||||
const issues = validateNvidiaCudaExportSummary(
|
||||
{
|
||||
success: true,
|
||||
targetFrames: 300,
|
||||
durationSec: 10,
|
||||
nativeSummary: {
|
||||
success: true,
|
||||
frames: 300,
|
||||
sourceTimestampMode: "pts",
|
||||
selectionStage: "timestamp-mapped-callback",
|
||||
},
|
||||
outputVideo: { duration: "9.999900", nb_frames: "300" },
|
||||
outputAudio: { duration: "10.005000" },
|
||||
},
|
||||
{ durationSec: 10, targetFrames: 300, requiresTimelineSync: true },
|
||||
);
|
||||
|
||||
expect(issues).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseWindowsGpuExportProgressLine", () => {
|
||||
it("parses bounded helper progress lines", () => {
|
||||
expect(
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
NativeStaticLayoutBackend,
|
||||
NativeStaticLayoutExportArgsConfig,
|
||||
NativeVideoAudioMuxMetrics,
|
||||
NativeVideoExportAudioMode,
|
||||
NativeVideoExportFinishOptions,
|
||||
} from "../nativeVideoExport";
|
||||
import {
|
||||
@@ -40,6 +41,7 @@ const getNowMs = () => performance.now();
|
||||
const formatFfmpegSeconds = (milliseconds: number) => (milliseconds / 1000).toFixed(3);
|
||||
const MISSING_NATIVE_STATIC_BACKGROUND_COLOR = "#ffffff";
|
||||
const NATIVE_EXPORT_HIGH_PRIORITY = os.constants.priority.PRIORITY_HIGH;
|
||||
const NVIDIA_CUDA_ALLOW_AUDIO_EXPORT_ENV = "RECORDLY_NVIDIA_CUDA_ALLOW_AUDIO_EXPORT";
|
||||
|
||||
export type NativeVideoExportSession = {
|
||||
ffmpegProcess: ChildProcessByStdio<Writable, null, Readable>;
|
||||
@@ -186,18 +188,24 @@ export interface NvidiaCudaExportSummary {
|
||||
bitrateMbps?: number;
|
||||
durationSec?: number;
|
||||
targetFrames?: number;
|
||||
sourcePtsFrames?: number;
|
||||
sourcePtsSource?: string;
|
||||
timingsMs?: {
|
||||
demux?: number;
|
||||
backgroundConvert?: number;
|
||||
cursorAtlas?: number;
|
||||
webcamConvert?: number;
|
||||
webcamDemux?: number;
|
||||
sourcePtsProbe?: number;
|
||||
nativeEncode?: number;
|
||||
mux?: number;
|
||||
endToEnd?: number;
|
||||
};
|
||||
nativeSummary?: {
|
||||
success?: boolean;
|
||||
selectionStage?: string;
|
||||
sourceTimestampMode?: string;
|
||||
timelineMode?: string;
|
||||
frames?: number;
|
||||
totalMs?: number;
|
||||
fps?: number;
|
||||
@@ -332,6 +340,111 @@ export function parseNvidiaCudaExportSummary(stdout: string): NvidiaCudaExportSu
|
||||
}
|
||||
}
|
||||
|
||||
function getFiniteNumber(value: unknown) {
|
||||
const numberValue = typeof value === "string" ? Number(value) : value;
|
||||
return typeof numberValue === "number" && Number.isFinite(numberValue)
|
||||
? numberValue
|
||||
: null;
|
||||
}
|
||||
|
||||
function getNvidiaCudaOutputStreamNumber(stream: unknown, property: string) {
|
||||
if (!stream || typeof stream !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getFiniteNumber((stream as Record<string, unknown>)[property]);
|
||||
}
|
||||
|
||||
export function validateNvidiaCudaExportSummary(
|
||||
summary: NvidiaCudaExportSummary,
|
||||
expected: {
|
||||
durationSec: number;
|
||||
targetFrames: number;
|
||||
requiresTimelineSync?: boolean;
|
||||
},
|
||||
) {
|
||||
const issues: string[] = [];
|
||||
const expectedFrames = Math.max(1, Math.round(expected.targetFrames));
|
||||
const minimumFrames = Math.max(1, Math.floor(expectedFrames * 0.95));
|
||||
const expectedDurationSec = Math.max(0, expected.durationSec);
|
||||
const durationToleranceSec = Math.min(
|
||||
2,
|
||||
Math.max(0.5, expectedDurationSec * 0.02),
|
||||
);
|
||||
const nativeFrames = getFiniteNumber(summary.nativeSummary?.frames);
|
||||
const outputVideoFrames = getNvidiaCudaOutputStreamNumber(
|
||||
summary.outputVideo,
|
||||
"nb_frames",
|
||||
);
|
||||
const outputVideoDurationSec = getNvidiaCudaOutputStreamNumber(
|
||||
summary.outputVideo,
|
||||
"duration",
|
||||
);
|
||||
const outputAudioDurationSec = getNvidiaCudaOutputStreamNumber(
|
||||
summary.outputAudio,
|
||||
"duration",
|
||||
);
|
||||
|
||||
if (!summary.outputVideo) {
|
||||
issues.push("missing output video probe");
|
||||
}
|
||||
if (
|
||||
expected.requiresTimelineSync &&
|
||||
!isNvidiaCudaTimestampAlignedSummary(summary)
|
||||
) {
|
||||
issues.push(
|
||||
"CUDA timeline mode is not timestamp-aligned for audio export",
|
||||
);
|
||||
}
|
||||
if (nativeFrames === null) {
|
||||
issues.push("missing native frame count");
|
||||
} else if (nativeFrames < minimumFrames) {
|
||||
issues.push(
|
||||
`native frames ${nativeFrames} below expected minimum ${minimumFrames}`,
|
||||
);
|
||||
}
|
||||
if (outputVideoFrames !== null && outputVideoFrames < minimumFrames) {
|
||||
issues.push(
|
||||
`output video frames ${outputVideoFrames} below expected minimum ${minimumFrames}`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
outputVideoDurationSec !== null &&
|
||||
Math.abs(outputVideoDurationSec - expectedDurationSec) > durationToleranceSec
|
||||
) {
|
||||
issues.push(
|
||||
`output video duration ${outputVideoDurationSec.toFixed(
|
||||
3,
|
||||
)}s differs from expected ${expectedDurationSec.toFixed(3)}s`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
outputAudioDurationSec !== null &&
|
||||
Math.abs(outputAudioDurationSec - expectedDurationSec) > durationToleranceSec
|
||||
) {
|
||||
issues.push(
|
||||
`output audio duration ${outputAudioDurationSec.toFixed(
|
||||
3,
|
||||
)}s differs from expected ${expectedDurationSec.toFixed(3)}s`,
|
||||
);
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
function isNvidiaCudaTimestampAlignedSummary(summary: NvidiaCudaExportSummary) {
|
||||
const nativeSummary = summary.nativeSummary;
|
||||
const timelineFields = [
|
||||
nativeSummary?.sourceTimestampMode,
|
||||
nativeSummary?.timelineMode,
|
||||
nativeSummary?.selectionStage,
|
||||
];
|
||||
return timelineFields.some((value) => {
|
||||
const normalized = String(value ?? "").toLowerCase();
|
||||
return normalized.includes("pts") || normalized.includes("timestamp");
|
||||
});
|
||||
}
|
||||
|
||||
function shouldPersistNvidiaCudaExportDiagnostics() {
|
||||
return (
|
||||
process.env.RECORDLY_NVIDIA_CUDA_EXPORT_DIAGNOSTICS === "1" ||
|
||||
@@ -1048,12 +1161,34 @@ async function resolveExperimentalWindowsGpuExporterPath() {
|
||||
return null;
|
||||
}
|
||||
|
||||
function isExperimentalNvidiaCudaExportEnabled(options: NativeStaticLayoutExportOptions) {
|
||||
return Boolean(
|
||||
process.platform === "win32" &&
|
||||
process.env.RECORDLY_EXPERIMENTAL_NVIDIA_CUDA_EXPORT === "1" &&
|
||||
options.experimentalWindowsGpuCompositor,
|
||||
);
|
||||
export function getNvidiaCudaAudioExportSkipReason(
|
||||
audioMode: NativeVideoExportAudioMode | undefined,
|
||||
) {
|
||||
const resolvedAudioMode = audioMode ?? "none";
|
||||
if (resolvedAudioMode === "none") {
|
||||
return null;
|
||||
}
|
||||
if (process.env[NVIDIA_CUDA_ALLOW_AUDIO_EXPORT_ENV] === "1") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `audio-mode:${resolvedAudioMode}`;
|
||||
}
|
||||
|
||||
function getExperimentalNvidiaCudaExportSkipReason(
|
||||
options: NativeStaticLayoutExportOptions,
|
||||
) {
|
||||
if (process.platform !== "win32") {
|
||||
return "not-windows";
|
||||
}
|
||||
if (process.env.RECORDLY_EXPERIMENTAL_NVIDIA_CUDA_EXPORT !== "1") {
|
||||
return "env-disabled";
|
||||
}
|
||||
if (!options.experimentalWindowsGpuCompositor) {
|
||||
return "windows-gpu-compositor-disabled";
|
||||
}
|
||||
|
||||
return getNvidiaCudaAudioExportSkipReason(options.audioOptions?.audioMode);
|
||||
}
|
||||
|
||||
async function resolveExperimentalNvidiaCudaExportScriptPath() {
|
||||
@@ -1067,6 +1202,28 @@ async function resolveExperimentalNvidiaCudaExportScriptPath() {
|
||||
candidates.push(configuredPath);
|
||||
}
|
||||
candidates.push(
|
||||
path.join(
|
||||
process.cwd(),
|
||||
"electron",
|
||||
"native",
|
||||
"nvidia-cuda-compositor",
|
||||
"run-mp4-pipeline.mjs",
|
||||
),
|
||||
path.join(
|
||||
app.getAppPath(),
|
||||
"electron",
|
||||
"native",
|
||||
"nvidia-cuda-compositor",
|
||||
"run-mp4-pipeline.mjs",
|
||||
),
|
||||
path.join(
|
||||
process.resourcesPath,
|
||||
"app.asar.unpacked",
|
||||
"electron",
|
||||
"native",
|
||||
"nvidia-cuda-compositor",
|
||||
"run-mp4-pipeline.mjs",
|
||||
),
|
||||
path.join(process.cwd(), ".tmp", "nvdec-nvenc-probe", "run-mp4-pipeline.mjs"),
|
||||
path.join(app.getAppPath(), ".tmp", "nvdec-nvenc-probe", "run-mp4-pipeline.mjs"),
|
||||
);
|
||||
@@ -1664,6 +1821,7 @@ async function runExperimentalNvidiaCudaStaticLayoutExport(
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
let stderrLineBuffer = "";
|
||||
let lastProgressPercentage = 0;
|
||||
let settled = false;
|
||||
const timeout = setTimeout(() => {
|
||||
if (settled) return;
|
||||
@@ -1693,8 +1851,13 @@ async function runExperimentalNvidiaCudaStaticLayoutExport(
|
||||
: elapsedMs > 0 && progress.currentFrame > 0
|
||||
? (progress.currentFrame * 1000) / elapsedMs
|
||||
: undefined;
|
||||
lastProgressPercentage = Math.max(
|
||||
lastProgressPercentage,
|
||||
progress.percentage,
|
||||
);
|
||||
onProgress?.({
|
||||
...progress,
|
||||
percentage: lastProgressPercentage,
|
||||
sessionId: options.sessionId,
|
||||
backend: "nvidia-cuda-compositor",
|
||||
elapsedMs,
|
||||
@@ -2030,7 +2193,23 @@ export async function exportNativeStaticLayoutVideo(
|
||||
}
|
||||
}
|
||||
let experimentalNvidiaCudaOptions = experimentalGpuOptions;
|
||||
let shouldTryNvidiaCuda = isExperimentalNvidiaCudaExportEnabled(options);
|
||||
const nvidiaCudaSkipReason =
|
||||
getExperimentalNvidiaCudaExportSkipReason(options);
|
||||
let shouldTryNvidiaCuda = nvidiaCudaSkipReason === null;
|
||||
if (
|
||||
!shouldTryNvidiaCuda &&
|
||||
process.env.RECORDLY_EXPERIMENTAL_NVIDIA_CUDA_EXPORT === "1" &&
|
||||
nvidiaCudaSkipReason !== "env-disabled"
|
||||
) {
|
||||
console.warn(
|
||||
"[native-static-layout-export] Skipping NVIDIA CUDA compositor; falling back to Windows GPU compositor",
|
||||
{
|
||||
reason: nvidiaCudaSkipReason,
|
||||
audioMode: options.audioOptions?.audioMode ?? "none",
|
||||
overrideEnv: NVIDIA_CUDA_ALLOW_AUDIO_EXPORT_ENV,
|
||||
},
|
||||
);
|
||||
}
|
||||
if (shouldTryNvidiaCuda && options.cursorTelemetry?.length) {
|
||||
const cursorTelemetryPath = await prepareNvidiaCudaCursorTelemetry(
|
||||
options,
|
||||
@@ -2064,6 +2243,21 @@ export async function exportNativeStaticLayoutVideo(
|
||||
session,
|
||||
onProgress,
|
||||
);
|
||||
const cudaValidationIssues = validateNvidiaCudaExportSummary(
|
||||
cudaResult.summary,
|
||||
{
|
||||
durationSec: options.durationSec,
|
||||
targetFrames: Math.ceil(options.durationSec * options.frameRate),
|
||||
requiresTimelineSync:
|
||||
(experimentalNvidiaCudaOptions.audioOptions?.audioMode ??
|
||||
"none") !== "none",
|
||||
},
|
||||
);
|
||||
if (cudaValidationIssues.length > 0) {
|
||||
throw new Error(
|
||||
`Experimental NVIDIA CUDA compositor produced an invalid output: ${cudaValidationIssues.join("; ")}`,
|
||||
);
|
||||
}
|
||||
console.info(
|
||||
"[native-static-layout-export] NVIDIA CUDA compositor completed",
|
||||
{
|
||||
@@ -2092,9 +2286,9 @@ export async function exportNativeStaticLayoutVideo(
|
||||
webcamOverlay: cudaResult.summary.nativeSummary?.webcamOverlay,
|
||||
cursorAtlas: cudaResult.summary.nativeSummary?.cursorAtlas,
|
||||
zoomOverlay: cudaResult.summary.nativeSummary?.zoomOverlay,
|
||||
zoomSamples: cudaResult.summary.nativeSummary?.zoomSamples,
|
||||
},
|
||||
);
|
||||
zoomSamples: cudaResult.summary.nativeSummary?.zoomSamples,
|
||||
},
|
||||
);
|
||||
const outputStat = await fs.stat(videoOnlyPath);
|
||||
metrics.chunkCount = 1;
|
||||
metrics.chunkDurationSec = options.durationSec;
|
||||
|
||||
@@ -20,8 +20,8 @@ import { startNativeCursorMonitor, stopNativeCursorMonitor } from "../cursor/mon
|
||||
import {
|
||||
clamp,
|
||||
pauseCursorCapture,
|
||||
resumeCursorCapture,
|
||||
resetCursorCaptureClock,
|
||||
resumeCursorCapture,
|
||||
sampleCursorPoint,
|
||||
snapshotCursorTelemetryForPersistence,
|
||||
startCursorSampling,
|
||||
@@ -1082,6 +1082,18 @@ export function registerRecordingHandlers(
|
||||
try {
|
||||
return await finalizeStoredVideo(videoPath);
|
||||
} catch {
|
||||
try {
|
||||
await validateRecordedVideo(videoPath);
|
||||
return {
|
||||
success: false,
|
||||
path: videoPath,
|
||||
message: "Failed to mux native Windows recording",
|
||||
error: String(error),
|
||||
};
|
||||
} catch {
|
||||
// The fallback path is not safely playable; surface the original mux error.
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to mux native Windows recording",
|
||||
|
||||
@@ -23,6 +23,13 @@
|
||||
"sourceDir": "electron/native/gpu-export-probe",
|
||||
"sourceFingerprint": "75bf080c4a5cbbcb1d42fb088a1545130c613e42d9e7681cc23f9151d1c8072b",
|
||||
"updatedAt": "2026-05-03T15:39:47.938Z"
|
||||
},
|
||||
"recordly-nvidia-cuda-compositor": {
|
||||
"binaryName": "recordly-nvidia-cuda-compositor.exe",
|
||||
"binarySha256": "f274039c46445a3b8e57c9faa6307238d6facc04d5c88f7bb84c43563feda649",
|
||||
"sourceDir": "electron/native/nvidia-cuda-compositor",
|
||||
"sourceFingerprint": "bc6f657ff3226f8c3bcdc89354de4118d0ef0f3ac1b5fde6d496bda9b77b65c6",
|
||||
"updatedAt": "2026-05-04T02:54:12.432Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,47 @@
|
||||
cmake_minimum_required(VERSION 3.24)
|
||||
|
||||
project(recordly_nvidia_cuda_compositor LANGUAGES CXX CUDA)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CUDA_STANDARD 17)
|
||||
set(CMAKE_CUDA_STANDARD_REQUIRED ON)
|
||||
|
||||
set(RECORDLY_NVIDIA_VIDEO_CODEC_SDK_ROOT
|
||||
"${CMAKE_CURRENT_LIST_DIR}/../../../.tmp/video-sdk-samples"
|
||||
CACHE PATH
|
||||
"Path to the NVIDIA Video Codec SDK samples checkout"
|
||||
)
|
||||
set(NVIDIA_SAMPLES_DIR "${RECORDLY_NVIDIA_VIDEO_CODEC_SDK_ROOT}/Samples")
|
||||
set(NVCODEC_DIR "${NVIDIA_SAMPLES_DIR}/NvCodec")
|
||||
|
||||
if(NOT EXISTS "${NVCODEC_DIR}")
|
||||
message(FATAL_ERROR "NVIDIA Video Codec SDK NvCodec directory not found: ${NVCODEC_DIR}. Set RECORDLY_NVIDIA_VIDEO_CODEC_SDK_ROOT.")
|
||||
endif()
|
||||
|
||||
add_executable(recordly-nvidia-cuda-compositor
|
||||
src/main.cu
|
||||
"${NVCODEC_DIR}/NvDecoder/NvDecoder.cpp"
|
||||
"${NVCODEC_DIR}/NvEncoder/NvEncoder.cpp"
|
||||
"${NVCODEC_DIR}/NvEncoder/NvEncoderCuda.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(recordly-nvidia-cuda-compositor PRIVATE
|
||||
"${NVIDIA_SAMPLES_DIR}"
|
||||
"${NVCODEC_DIR}"
|
||||
)
|
||||
|
||||
target_compile_definitions(recordly-nvidia-cuda-compositor PRIVATE
|
||||
NOMINMAX
|
||||
WIN32_LEAN_AND_MEAN
|
||||
)
|
||||
|
||||
find_package(CUDAToolkit REQUIRED)
|
||||
target_link_libraries(recordly-nvidia-cuda-compositor PRIVATE
|
||||
CUDA::cuda_driver
|
||||
"${NVCODEC_DIR}/Lib/x64/nvcuvid.lib"
|
||||
)
|
||||
|
||||
set_target_properties(recordly-nvidia-cuda-compositor PROPERTIES
|
||||
CUDA_SEPARABLE_COMPILATION OFF
|
||||
)
|
||||
@@ -0,0 +1,144 @@
|
||||
const { app, BrowserWindow, ipcMain } = require("electron");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const drawHeight = 256;
|
||||
const padding = 2;
|
||||
const cursorTypes = [
|
||||
"arrow",
|
||||
"text",
|
||||
"pointer",
|
||||
"crosshair",
|
||||
"open-hand",
|
||||
"closed-hand",
|
||||
"resize-ew",
|
||||
"resize-ns",
|
||||
"not-allowed",
|
||||
];
|
||||
const tahoeAssets = {
|
||||
arrow: ["pointer-1__14-6.svg", 0.14, 0.06],
|
||||
text: ["ibeam-1__50-44.svg", 0.5, 0.44],
|
||||
pointer: ["pointinghand-1__40-10.svg", 0.4, 0.1],
|
||||
crosshair: ["crosshair-1__50-50.svg", 0.5, 0.5],
|
||||
"open-hand": ["openhand-1__55-57.svg", 0.55, 0.57],
|
||||
"closed-hand": ["closedhand-1__50-46.svg", 0.5, 0.46],
|
||||
"resize-ew": ["resizeeastwest-1__50-50.svg", 0.5, 0.5],
|
||||
"resize-ns": ["resizenorthsouth-1__50-49.svg", 0.5, 0.49],
|
||||
"not-allowed": ["notallowed-1__23-0.svg", 0.23, 0],
|
||||
};
|
||||
|
||||
function arg(name, fallback = "") {
|
||||
const index = process.argv.indexOf(name);
|
||||
return index >= 0 && index + 1 < process.argv.length ? process.argv[index + 1] : fallback;
|
||||
}
|
||||
|
||||
const repoRoot = arg("--repo-root");
|
||||
const atlasRgbaPath = arg("--output-rgba");
|
||||
const atlasMetadataPath = arg("--output-metadata");
|
||||
|
||||
if (!repoRoot || !atlasRgbaPath || !atlasMetadataPath) {
|
||||
console.error("Usage: electron render-tahoe-cursor-atlas.cjs --repo-root <repo> --output-rgba <raw> --output-metadata <tsv>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const assets = cursorTypes.map((type, index) => {
|
||||
const [fileName, anchorX, anchorY] = tahoeAssets[type];
|
||||
return {
|
||||
type,
|
||||
index,
|
||||
filePath: path.join(repoRoot, "src", "assets", "cursors", "tahoe", fileName),
|
||||
anchorX,
|
||||
anchorY,
|
||||
};
|
||||
});
|
||||
|
||||
app.disableHardwareAcceleration();
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
const window = new BrowserWindow({
|
||||
show: false,
|
||||
width: 1,
|
||||
height: 1,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
backgroundThrottling: false,
|
||||
},
|
||||
});
|
||||
|
||||
ipcMain.once("atlas-ready", (_event, result) => {
|
||||
console.log(JSON.stringify(result));
|
||||
app.quit();
|
||||
});
|
||||
|
||||
const html = `
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<script>
|
||||
const { ipcRenderer } = require("electron");
|
||||
const fs = require("node:fs");
|
||||
const assets = ${JSON.stringify(assets)};
|
||||
const atlasRgbaPath = ${JSON.stringify(atlasRgbaPath)};
|
||||
const atlasMetadataPath = ${JSON.stringify(atlasMetadataPath)};
|
||||
const drawHeight = ${drawHeight};
|
||||
const padding = ${padding};
|
||||
|
||||
function loadImage(asset) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.onload = () => resolve({ asset, image });
|
||||
image.onerror = () => reject(new Error("Failed to load cursor SVG: " + asset.filePath));
|
||||
const svg = fs.readFileSync(asset.filePath, "utf8");
|
||||
image.src = "data:image/svg+xml;base64," + Buffer.from(svg, "utf8").toString("base64");
|
||||
});
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const loaded = await Promise.all(assets.map(loadImage));
|
||||
const packed = loaded.map(({ asset, image }) => {
|
||||
const aspectRatio = image.naturalHeight > 0 ? image.naturalWidth / image.naturalHeight : 1;
|
||||
return {
|
||||
asset,
|
||||
image,
|
||||
width: Math.max(1, Math.round(drawHeight * aspectRatio)),
|
||||
height: drawHeight,
|
||||
aspectRatio,
|
||||
};
|
||||
});
|
||||
const atlasWidth = packed.reduce((sum, item) => sum + item.width + padding, padding);
|
||||
const atlasHeight = drawHeight + padding * 2;
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = atlasWidth;
|
||||
canvas.height = atlasHeight;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.clearRect(0, 0, atlasWidth, atlasHeight);
|
||||
|
||||
let x = padding;
|
||||
const rows = [];
|
||||
for (const item of packed) {
|
||||
const y = padding;
|
||||
ctx.drawImage(item.image, x, y, item.width, item.height);
|
||||
rows.push([
|
||||
item.asset.index,
|
||||
x,
|
||||
y,
|
||||
item.width,
|
||||
item.height,
|
||||
item.asset.anchorX,
|
||||
item.asset.anchorY,
|
||||
item.aspectRatio,
|
||||
].join("\\t"));
|
||||
x += item.width + padding;
|
||||
}
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, atlasWidth, atlasHeight).data;
|
||||
fs.writeFileSync(atlasRgbaPath, Buffer.from(imageData.buffer));
|
||||
fs.writeFileSync(atlasMetadataPath, rows.join("\\n") + "\\n");
|
||||
ipcRenderer.send("atlas-ready", { width: atlasWidth, height: atlasHeight, entries: rows.length });
|
||||
})().catch((error) => {
|
||||
ipcRenderer.send("atlas-ready", { error: error.message });
|
||||
});
|
||||
</script>`;
|
||||
|
||||
await window.loadURL("data:text/html;charset=utf-8," + encodeURIComponent(html));
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -25,8 +25,9 @@
|
||||
"rebuild:native": "node ./node_modules/@electron/rebuild/lib/cli.js --force --only uiohook-napi",
|
||||
"build:native-helpers": "node scripts/build-native-helpers.mjs",
|
||||
"build:whisper-runtime": "node scripts/build-whisper-runtime.mjs",
|
||||
"build:platform-native-helpers": "npm run build:native-helpers && npm run build:windows-capture && npm run build:windows-gpu-export && npm run build:cursor-monitor && npm run build:whisper-runtime",
|
||||
"build:platform-native-helpers": "npm run build:native-helpers && npm run build:windows-capture && npm run build:windows-gpu-export && npm run build:nvidia-cuda-compositor && npm run build:cursor-monitor && npm run build:whisper-runtime",
|
||||
"build:windows-gpu-export": "node scripts/build-windows-gpu-export.mjs",
|
||||
"build:nvidia-cuda-compositor": "node scripts/build-nvidia-cuda-compositor.mjs",
|
||||
"build:windows-capture": "node scripts/build-windows-capture.mjs",
|
||||
"build:cursor-monitor": "node scripts/build-cursor-monitor.mjs",
|
||||
"build:mac": "npm run build:platform-native-helpers && tsc && vite build --config vite.config.ts && electron-builder --mac",
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import { copyFileSync, existsSync, mkdirSync, rmSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import {
|
||||
formatNativeHelperManifestWarning,
|
||||
updateNativeHelperManifest,
|
||||
verifyNativeHelperManifest,
|
||||
} from "./native-helper-manifest.mjs";
|
||||
|
||||
const projectRoot = process.cwd();
|
||||
const sourceDir = path.join(projectRoot, "electron", "native", "nvidia-cuda-compositor");
|
||||
const buildDir = path.join(sourceDir, "build");
|
||||
const bundledDir = path.join(
|
||||
projectRoot,
|
||||
"electron",
|
||||
"native",
|
||||
"bin",
|
||||
process.arch === "arm64" ? "win32-arm64" : "win32-x64",
|
||||
);
|
||||
const bundledExePath = path.join(bundledDir, "recordly-nvidia-cuda-compositor.exe");
|
||||
const helperId = "recordly-nvidia-cuda-compositor";
|
||||
const generatorArch = process.arch === "arm64" ? "ARM64" : "x64";
|
||||
const videoCodecSdkRoot =
|
||||
process.env.RECORDLY_NVIDIA_VIDEO_CODEC_SDK_ROOT?.trim() ||
|
||||
path.join(projectRoot, ".tmp", "video-sdk-samples");
|
||||
|
||||
if (process.platform !== "win32") {
|
||||
console.log("[build-nvidia-cuda-compositor] Skipping NVIDIA CUDA compositor build.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!existsSync(path.join(sourceDir, "CMakeLists.txt"))) {
|
||||
console.error("[build-nvidia-cuda-compositor] CMakeLists.txt not found at", sourceDir);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function fallbackToBundledHelperOrExit(reason) {
|
||||
if (existsSync(bundledExePath)) {
|
||||
const verification = verifyNativeHelperManifest({
|
||||
projectRoot,
|
||||
helperId,
|
||||
sourceDir,
|
||||
binaryPath: bundledExePath,
|
||||
binaryName: "recordly-nvidia-cuda-compositor.exe",
|
||||
});
|
||||
if (!verification.ok) {
|
||||
console.warn(
|
||||
formatNativeHelperManifestWarning("build-nvidia-cuda-compositor", verification),
|
||||
);
|
||||
}
|
||||
console.log(`[build-nvidia-cuda-compositor] ${reason}`);
|
||||
console.log(`[build-nvidia-cuda-compositor] Using bundled helper: ${bundledExePath}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.error(`[build-nvidia-cuda-compositor] ${reason}`);
|
||||
console.error(
|
||||
"[build-nvidia-cuda-compositor] No bundled helper is available; install CUDA Toolkit + NVIDIA Video Codec SDK or provide a staged helper.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function findCmake() {
|
||||
try {
|
||||
execSync("cmake --version", { stdio: "pipe" });
|
||||
return "cmake";
|
||||
} catch {
|
||||
// Continue probing common Windows install locations.
|
||||
}
|
||||
|
||||
const standaloneCmakePaths = [
|
||||
path.join("C:", "Program Files", "CMake", "bin", "cmake.exe"),
|
||||
path.join("C:", "Program Files (x86)", "CMake", "bin", "cmake.exe"),
|
||||
];
|
||||
for (const cmakePath of standaloneCmakePaths) {
|
||||
if (existsSync(cmakePath)) {
|
||||
return `"${cmakePath}"`;
|
||||
}
|
||||
}
|
||||
|
||||
const vsRoots = [
|
||||
path.join("C:", "Program Files", "Microsoft Visual Studio"),
|
||||
path.join("C:", "Program Files (x86)", "Microsoft Visual Studio"),
|
||||
];
|
||||
const vsEditions = ["Preview", "Community", "Professional", "Enterprise", "BuildTools"];
|
||||
const vsVersions = ["2022", "2019"];
|
||||
for (const root of vsRoots) {
|
||||
for (const version of vsVersions) {
|
||||
for (const edition of vsEditions) {
|
||||
const cmakePath = path.join(
|
||||
root,
|
||||
version,
|
||||
edition,
|
||||
"Common7",
|
||||
"IDE",
|
||||
"CommonExtensions",
|
||||
"Microsoft",
|
||||
"CMake",
|
||||
"CMake",
|
||||
"bin",
|
||||
"cmake.exe",
|
||||
);
|
||||
if (existsSync(cmakePath)) {
|
||||
return `"${cmakePath}"`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!existsSync(path.join(videoCodecSdkRoot, "Samples", "NvCodec"))) {
|
||||
fallbackToBundledHelperOrExit(
|
||||
`NVIDIA Video Codec SDK samples not found at ${videoCodecSdkRoot}. Set RECORDLY_NVIDIA_VIDEO_CODEC_SDK_ROOT to build from source.`,
|
||||
);
|
||||
}
|
||||
|
||||
const cmake = findCmake();
|
||||
if (!cmake) {
|
||||
fallbackToBundledHelperOrExit(
|
||||
"CMake not found. Install Visual Studio with C++ CMake tools or standalone CMake.",
|
||||
);
|
||||
}
|
||||
|
||||
mkdirSync(buildDir, { recursive: true });
|
||||
|
||||
function clearCmakeCache() {
|
||||
rmSync(path.join(buildDir, "CMakeCache.txt"), { force: true });
|
||||
rmSync(path.join(buildDir, "CMakeFiles"), { recursive: true, force: true });
|
||||
}
|
||||
|
||||
console.log("[build-nvidia-cuda-compositor] Configuring CMake...");
|
||||
try {
|
||||
clearCmakeCache();
|
||||
execSync(
|
||||
`${cmake} .. -G "Visual Studio 17 2022" -A ${generatorArch} -DRECORDLY_NVIDIA_VIDEO_CODEC_SDK_ROOT="${videoCodecSdkRoot}"`,
|
||||
{
|
||||
cwd: buildDir,
|
||||
stdio: "inherit",
|
||||
timeout: 120000,
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
console.log("[build-nvidia-cuda-compositor] VS 2022 generator not found, trying VS 2019...");
|
||||
try {
|
||||
clearCmakeCache();
|
||||
execSync(
|
||||
`${cmake} .. -G "Visual Studio 16 2019" -A ${generatorArch} -DRECORDLY_NVIDIA_VIDEO_CODEC_SDK_ROOT="${videoCodecSdkRoot}"`,
|
||||
{
|
||||
cwd: buildDir,
|
||||
stdio: "inherit",
|
||||
timeout: 120000,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
fallbackToBundledHelperOrExit(
|
||||
`CMake configure failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[build-nvidia-cuda-compositor] Building NVIDIA CUDA compositor...");
|
||||
try {
|
||||
execSync(`${cmake} --build . --config Release`, {
|
||||
cwd: buildDir,
|
||||
stdio: "inherit",
|
||||
timeout: 300000,
|
||||
});
|
||||
} catch (error) {
|
||||
fallbackToBundledHelperOrExit(
|
||||
`Build failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const exePath = path.join(buildDir, "Release", "recordly-nvidia-cuda-compositor.exe");
|
||||
if (!existsSync(exePath)) {
|
||||
console.error("[build-nvidia-cuda-compositor] Expected exe not found at", exePath);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
mkdirSync(bundledDir, { recursive: true });
|
||||
copyFileSync(exePath, bundledExePath);
|
||||
console.log(`[build-nvidia-cuda-compositor] Staged bundled helper: ${bundledExePath}`);
|
||||
const manifestPath = updateNativeHelperManifest({
|
||||
projectRoot,
|
||||
helperId,
|
||||
sourceDir,
|
||||
binaryPath: bundledExePath,
|
||||
binaryName: "recordly-nvidia-cuda-compositor.exe",
|
||||
});
|
||||
console.log(`[build-nvidia-cuda-compositor] Updated helper manifest: ${manifestPath}`);
|
||||
@@ -0,0 +1,80 @@
|
||||
param(
|
||||
[string]$AppPath,
|
||||
[string]$CudaScriptPath,
|
||||
[switch]$CloseExisting,
|
||||
[switch]$NoDiagnostics,
|
||||
[switch]$AllowCudaAudio
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
|
||||
|
||||
if (-not $AppPath) {
|
||||
$packagedAppPath = Join-Path $repoRoot "release\win-unpacked\Recordly.exe"
|
||||
$installedAppPath = Join-Path $env:LOCALAPPDATA "Programs\recordly\Recordly.exe"
|
||||
if (Test-Path $packagedAppPath) {
|
||||
$AppPath = $packagedAppPath
|
||||
} elseif (Test-Path $installedAppPath) {
|
||||
$AppPath = $installedAppPath
|
||||
} else {
|
||||
throw "Recordly.exe was not found. Build the Windows package first or pass -AppPath."
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $CudaScriptPath) {
|
||||
$CudaScriptPath = Join-Path $repoRoot "electron\native\nvidia-cuda-compositor\run-mp4-pipeline.mjs"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $AppPath)) {
|
||||
throw "Recordly app not found: $AppPath"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $CudaScriptPath)) {
|
||||
throw "NVIDIA CUDA/NVENC wrapper script not found: $CudaScriptPath"
|
||||
}
|
||||
|
||||
$existingRecordly = @(Get-Process -Name "Recordly" -ErrorAction SilentlyContinue)
|
||||
if ($existingRecordly.Count -gt 0) {
|
||||
if (-not $CloseExisting) {
|
||||
Write-Host "Recordly is already running. Close it first, or rerun with -CloseExisting so the CUDA env is inherited by the new app process."
|
||||
$existingRecordly | Select-Object Id, ProcessName, Path | Format-Table -AutoSize
|
||||
exit 2
|
||||
}
|
||||
|
||||
$existingRecordly | Stop-Process -Force
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
|
||||
$env:RECORDLY_EXPERIMENTAL_NVIDIA_CUDA_EXPORT = "1"
|
||||
$env:RECORDLY_NVIDIA_CUDA_EXPORT_SCRIPT = (Resolve-Path $CudaScriptPath).Path
|
||||
$env:RECORDLY_NVIDIA_CUDA_EXPORT_HIGH_PRIORITY = "1"
|
||||
$env:RECORDLY_NVIDIA_CUDA_SAMPLE_GPU = "1"
|
||||
|
||||
if ($AllowCudaAudio) {
|
||||
$env:RECORDLY_NVIDIA_CUDA_ALLOW_AUDIO_EXPORT = "1"
|
||||
} else {
|
||||
Remove-Item Env:\RECORDLY_NVIDIA_CUDA_ALLOW_AUDIO_EXPORT -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
if ($NoDiagnostics) {
|
||||
Remove-Item Env:\RECORDLY_NVIDIA_CUDA_EXPORT_DIAGNOSTICS -ErrorAction SilentlyContinue
|
||||
} else {
|
||||
$env:RECORDLY_NVIDIA_CUDA_EXPORT_DIAGNOSTICS = "1"
|
||||
}
|
||||
|
||||
$resolvedAppPath = (Resolve-Path $AppPath).Path
|
||||
$appDirectory = Split-Path $resolvedAppPath -Parent
|
||||
if ($AllowCudaAudio) {
|
||||
$cudaAudioMode = "candidate only; app still requires timestamp-aligned CUDA output"
|
||||
} else {
|
||||
$cudaAudioMode = "guarded; audio exports fall back to Windows D3D11"
|
||||
}
|
||||
|
||||
Write-Host "Launching Recordly with guarded NVIDIA CUDA/NVENC auto export enabled:"
|
||||
Write-Host " App: $resolvedAppPath"
|
||||
Write-Host " CUDA wrapper: $env:RECORDLY_NVIDIA_CUDA_EXPORT_SCRIPT"
|
||||
Write-Host " CUDA audio exports: $cudaAudioMode"
|
||||
Write-Host " Diagnostics: $($env:RECORDLY_NVIDIA_CUDA_EXPORT_DIAGNOSTICS -eq '1')"
|
||||
|
||||
Start-Process -FilePath $resolvedAppPath -WorkingDirectory $appDirectory
|
||||
@@ -171,6 +171,11 @@ function getExpectedNativeHelperFiles(archTag) {
|
||||
label: "Windows GPU export helper",
|
||||
executable: true,
|
||||
},
|
||||
{
|
||||
name: "recordly-nvidia-cuda-compositor.exe",
|
||||
label: "NVIDIA CUDA compositor helper",
|
||||
executable: true,
|
||||
},
|
||||
{ name: "helpers-manifest.json", label: "Windows helper manifest" },
|
||||
{ name: "whisper-cli.exe", label: "Whisper CLI runtime", executable: true },
|
||||
{ name: "whisper-runtime.json", label: "Whisper runtime manifest" },
|
||||
|
||||
@@ -731,16 +731,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
await window.electronAPI.muxNativeWindowsRecording(pauseSegments);
|
||||
if (!muxResult?.success || !muxResult.path) {
|
||||
void logNativeCaptureDiagnostics("mux-native-windows-recording");
|
||||
if (!muxResult?.path) {
|
||||
const failureMessage = await buildNativeCaptureFailureMessage(
|
||||
"mux-native-windows-recording",
|
||||
muxResult?.message ||
|
||||
"Failed to finalize the Windows recording, so the editor was not opened.",
|
||||
);
|
||||
await notifyRecordingFinalizationFailure(failureMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
const fallbackPath = muxResult?.path ?? finalPath;
|
||||
const warningMessage =
|
||||
muxResult?.error ||
|
||||
muxResult?.message ||
|
||||
@@ -749,8 +740,10 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
`${warningMessage}. Recording was saved, but audio playback or export may be incomplete.`,
|
||||
{ id: SOURCE_AUDIO_MUX_TOAST_ID, duration: 10000 },
|
||||
);
|
||||
finalPath = fallbackPath;
|
||||
} else {
|
||||
finalPath = muxResult.path;
|
||||
}
|
||||
finalPath = muxResult.path;
|
||||
}
|
||||
|
||||
await storeMicrophoneSidecar(
|
||||
@@ -1186,7 +1179,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
maxFrameRate: TARGET_FRAME_RATE,
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
} as unknown as MediaStreamConstraints);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1601,8 +1601,14 @@ export class ModernVideoExporter {
|
||||
return;
|
||||
}
|
||||
|
||||
const nativeCurrentFrame = Math.floor(progress.currentFrame);
|
||||
const nativeTotalFrames = Math.max(1, Math.floor(progress.totalFrames));
|
||||
const progressPercentFrame = Number.isFinite(progress.percentage)
|
||||
? Math.floor((nativeTotalFrames * progress.percentage) / 100)
|
||||
: 0;
|
||||
const nativeCurrentFrame = Math.max(
|
||||
Math.floor(progress.currentFrame),
|
||||
progressPercentFrame,
|
||||
);
|
||||
const nativeFramesComplete = nativeCurrentFrame >= nativeTotalFrames;
|
||||
const maxExtractingFrame = Math.max(
|
||||
0,
|
||||
|
||||
Reference in New Issue
Block a user