mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-24 14:55:37 +00:00
996 lines
30 KiB
TypeScript
996 lines
30 KiB
TypeScript
import { contextBridge, ipcRenderer } from "electron";
|
|
import type { RecordingSessionData } from "./ipc/types";
|
|
|
|
type NativeVideoExportWriteResult = { success: boolean; error?: string };
|
|
type NativeVideoAudioMuxMetrics = {
|
|
tempVideoWriteMs?: number;
|
|
tempEditedAudioWriteMs?: number;
|
|
ffmpegExecMs?: number;
|
|
muxedVideoReadMs?: number;
|
|
tempVideoBytes?: number;
|
|
tempEditedAudioBytes?: number;
|
|
muxedVideoBytes?: number;
|
|
};
|
|
type WindowsGpuExportSummary = {
|
|
success?: boolean;
|
|
width?: number;
|
|
height?: number;
|
|
fps?: number;
|
|
seconds?: number;
|
|
mediaMs?: number;
|
|
frames?: number;
|
|
cursorOverlay?: boolean;
|
|
zoomOverlay?: boolean;
|
|
adapterVendorId?: number;
|
|
adapterDeviceId?: number;
|
|
adapterDedicatedVideoMemoryMB?: number;
|
|
initializeMs?: number;
|
|
initCoInitializeMs?: number;
|
|
initMfStartupMs?: number;
|
|
initD3DDeviceMs?: number;
|
|
initSourceReaderMs?: number;
|
|
initWebcamReaderMs?: number;
|
|
initVideoProcessorMs?: number;
|
|
initTexturesMs?: number;
|
|
initShaderPipelineMs?: number;
|
|
initSinkWriterMs?: number;
|
|
totalMs?: number;
|
|
readMs?: number;
|
|
clearMs?: number;
|
|
videoProcessMs?: number;
|
|
writeSampleMs?: number;
|
|
finalizeMs?: number;
|
|
realtimeMultiplier?: number;
|
|
};
|
|
type NativeStaticLayoutChunkMetric = {
|
|
index: number;
|
|
startSec: number;
|
|
durationSec: number;
|
|
backend:
|
|
| "cuda-overlay"
|
|
| "cuda-scale-cpu-pad"
|
|
| "cuda-static-composite"
|
|
| "nvidia-cuda-compositor"
|
|
| "windows-d3d11-compositor";
|
|
elapsedMs: number;
|
|
outputBytes: number;
|
|
fallbackReason?: string;
|
|
windowsGpuSummary?: WindowsGpuExportSummary;
|
|
};
|
|
type NativeStaticLayoutMetrics = NativeVideoAudioMuxMetrics & {
|
|
chunkCount: number;
|
|
chunkDurationSec: number;
|
|
chunkExecMs: number;
|
|
concatExecMs?: number;
|
|
staticAssetExecMs?: number;
|
|
fallbackChunkCount: number;
|
|
videoOnlyBytes?: number;
|
|
chunks: NativeStaticLayoutChunkMetric[];
|
|
};
|
|
type NativeStaticLayoutProgress = {
|
|
sessionId?: string;
|
|
backend?: NativeStaticLayoutChunkMetric["backend"];
|
|
stage?: "preparing" | "finalizing";
|
|
elapsedMs?: number;
|
|
averageFps?: number;
|
|
currentFrame: number;
|
|
totalFrames: number;
|
|
percentage: number;
|
|
};
|
|
type NativeVideoMetadataProbe = {
|
|
width: number;
|
|
height: number;
|
|
duration: number;
|
|
mediaStartTime?: number;
|
|
streamStartTime?: number;
|
|
streamDuration?: number;
|
|
frameRate: number;
|
|
codec: string;
|
|
hasAudio: boolean;
|
|
audioCodec?: string;
|
|
audioSampleRate?: number;
|
|
};
|
|
type NativeExportCapabilities = {
|
|
platform: NodeJS.Platform;
|
|
nvidiaCuda: {
|
|
available: boolean;
|
|
skipReason: string | null;
|
|
hasNvidiaGpu: boolean | null;
|
|
hasWrapper: boolean;
|
|
explicitEnabled: boolean;
|
|
explicitDisabled: boolean;
|
|
userOptInRequired: boolean;
|
|
};
|
|
};
|
|
|
|
const nativeVideoExportWriteRequests = new Map<
|
|
number,
|
|
{
|
|
sessionId: string;
|
|
resolve: (result: NativeVideoExportWriteResult) => void;
|
|
}
|
|
>();
|
|
|
|
let nextNativeVideoExportWriteRequestId = 1;
|
|
let nativeVideoExportWriteResultListenerAttached = false;
|
|
|
|
function ensureNativeVideoExportWriteResultListener() {
|
|
if (nativeVideoExportWriteResultListenerAttached) {
|
|
return;
|
|
}
|
|
|
|
nativeVideoExportWriteResultListenerAttached = true;
|
|
ipcRenderer.on(
|
|
"native-video-export-write-frame-result",
|
|
(
|
|
_event,
|
|
payload: {
|
|
sessionId?: string;
|
|
requestId?: number;
|
|
success?: boolean;
|
|
error?: string;
|
|
},
|
|
) => {
|
|
if (typeof payload?.requestId !== "number") {
|
|
return;
|
|
}
|
|
|
|
const pendingRequest = nativeVideoExportWriteRequests.get(payload.requestId);
|
|
if (!pendingRequest) {
|
|
return;
|
|
}
|
|
|
|
nativeVideoExportWriteRequests.delete(payload.requestId);
|
|
pendingRequest.resolve({
|
|
success: payload.success === true,
|
|
error: payload.error,
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
function settleNativeVideoExportPendingRequests(
|
|
sessionId: string,
|
|
result: NativeVideoExportWriteResult,
|
|
) {
|
|
for (const [requestId, pendingRequest] of nativeVideoExportWriteRequests.entries()) {
|
|
if (pendingRequest.sessionId !== sessionId) {
|
|
continue;
|
|
}
|
|
|
|
nativeVideoExportWriteRequests.delete(requestId);
|
|
pendingRequest.resolve(result);
|
|
}
|
|
}
|
|
|
|
contextBridge.exposeInMainWorld("electronAPI", {
|
|
hudOverlaySetIgnoreMouse: (ignore: boolean) => {
|
|
ipcRenderer.send("hud-overlay-set-ignore-mouse", ignore);
|
|
},
|
|
hudOverlaySetSourceSelectionActive: (active: boolean) => {
|
|
ipcRenderer.send("hud-overlay-set-source-selection-active", active);
|
|
},
|
|
hudOverlayDrag: (phase: "start" | "move" | "end", screenX: number, screenY: number) => {
|
|
ipcRenderer.send("hud-overlay-drag", phase, screenX, screenY);
|
|
},
|
|
hudOverlayHide: () => {
|
|
ipcRenderer.send("hud-overlay-hide");
|
|
},
|
|
hudOverlayClose: () => {
|
|
ipcRenderer.send("hud-overlay-close");
|
|
},
|
|
hudOverlayRendererReady: () => {
|
|
ipcRenderer.send("hud-overlay-renderer-ready");
|
|
},
|
|
hudOverlaySetWebcamPreviewVisible: (visible: boolean) => {
|
|
ipcRenderer.send("hud-overlay-set-webcam-preview-visible", visible);
|
|
},
|
|
getHudOverlayCaptureProtection: () => {
|
|
return ipcRenderer.invoke("get-hud-overlay-capture-protection");
|
|
},
|
|
getHudOverlayMousePassthroughSupported: () => {
|
|
return ipcRenderer.invoke("get-hud-overlay-mouse-passthrough-supported");
|
|
},
|
|
setHudOverlayCaptureProtection: (enabled: boolean) => {
|
|
return ipcRenderer.invoke("set-hud-overlay-capture-protection", enabled);
|
|
},
|
|
getAssetBasePath: async () => {
|
|
return await ipcRenderer.invoke("get-asset-base-path");
|
|
},
|
|
listAssetDirectory: (relativeDir: string) => {
|
|
return ipcRenderer.invoke("list-asset-directory", relativeDir);
|
|
},
|
|
readLocalFile: (filePath: string) => {
|
|
return ipcRenderer.invoke("read-local-file", filePath);
|
|
},
|
|
generateWallpaperThumbnail: (filePath: string) => {
|
|
return ipcRenderer.invoke("generate-wallpaper-thumbnail", filePath);
|
|
},
|
|
probeNativeVideoMetadata: (filePath: string) => {
|
|
return ipcRenderer.invoke("probe-native-video-metadata", filePath) as Promise<{
|
|
success: boolean;
|
|
metadata?: NativeVideoMetadataProbe;
|
|
error?: string;
|
|
}>;
|
|
},
|
|
getNativeExportCapabilities: () => {
|
|
return ipcRenderer.invoke("get-native-export-capabilities") as Promise<{
|
|
success: boolean;
|
|
capabilities?: NativeExportCapabilities;
|
|
error?: string;
|
|
}>;
|
|
},
|
|
nativeStaticLayoutExport: (options: {
|
|
sessionId?: string;
|
|
inputPath: string;
|
|
width: number;
|
|
height: number;
|
|
frameRate: number;
|
|
bitrate: number;
|
|
encodingMode: "fast" | "balanced" | "quality";
|
|
durationSec: number;
|
|
contentWidth: number;
|
|
contentHeight: number;
|
|
offsetX: number;
|
|
offsetY: number;
|
|
sourceCropX?: number;
|
|
sourceCropY?: number;
|
|
sourceCropWidth?: number;
|
|
sourceCropHeight?: number;
|
|
backgroundColor: string;
|
|
backgroundImagePath?: string | null;
|
|
backgroundBlurPx?: number;
|
|
borderRadius?: number;
|
|
shadowIntensity?: number;
|
|
webcamInputPath?: string | null;
|
|
webcamLeft?: number;
|
|
webcamTop?: number;
|
|
webcamSize?: number;
|
|
webcamRadius?: number;
|
|
webcamShadowIntensity?: number;
|
|
webcamMirror?: boolean;
|
|
webcamTimeOffsetMs?: number;
|
|
cursorTelemetry?: Array<{
|
|
timeMs: number;
|
|
cx: number;
|
|
cy: number;
|
|
cursorTypeIndex?: number;
|
|
bounceScale?: number;
|
|
visible?: boolean;
|
|
}>;
|
|
cursorSize?: number;
|
|
cursorAtlasPngDataUrl?: string | null;
|
|
cursorAtlasEntries?: Array<{
|
|
index: number;
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
anchorX: number;
|
|
anchorY: number;
|
|
aspectRatio: number;
|
|
}>;
|
|
zoomTelemetry?: Array<{ timeMs: number; scale: number; x: number; y: number }>;
|
|
timelineSegments?: Array<{
|
|
sourceStartMs: number;
|
|
sourceEndMs: number;
|
|
outputStartMs: number;
|
|
outputEndMs: number;
|
|
speed: number;
|
|
}>;
|
|
chunkDurationSec?: number;
|
|
experimentalWindowsGpuCompositor?: boolean;
|
|
experimentalNvidiaCudaExport?: boolean;
|
|
audioOptions?: {
|
|
audioMode?: "none" | "copy-source" | "trim-source" | "edited-track";
|
|
audioSourcePath?: string | null;
|
|
audioSourceCodec?: string | null;
|
|
audioSourceSampleRate?: number;
|
|
outputDurationSec?: number;
|
|
trimSegments?: Array<{ startMs: number; endMs: number }>;
|
|
editedTrackStrategy?: "filtergraph-fast-path" | "offline-render-fallback";
|
|
editedTrackSegments?: Array<{ startMs: number; endMs: number; speed: number }>;
|
|
editedAudioData?: ArrayBuffer;
|
|
editedAudioMimeType?: string | null;
|
|
};
|
|
}) => {
|
|
return ipcRenderer.invoke("native-static-layout-export", options) as Promise<{
|
|
success: boolean;
|
|
tempPath?: string;
|
|
encoderName?: string;
|
|
error?: string;
|
|
metrics?: NativeStaticLayoutMetrics;
|
|
}>;
|
|
},
|
|
nativeStaticLayoutExportCancel: (sessionId: string) => {
|
|
return ipcRenderer.invoke("native-static-layout-export-cancel", sessionId) as Promise<{
|
|
success: boolean;
|
|
}>;
|
|
},
|
|
onNativeStaticLayoutExportProgress: (
|
|
callback: (progress: NativeStaticLayoutProgress) => void,
|
|
) => {
|
|
const listener = (_event: Electron.IpcRendererEvent, payload: NativeStaticLayoutProgress) =>
|
|
callback(payload);
|
|
ipcRenderer.on("native-static-layout-export-progress", listener);
|
|
return () => ipcRenderer.removeListener("native-static-layout-export-progress", listener);
|
|
},
|
|
nativeVideoExportStart: (options: {
|
|
width: number;
|
|
height: number;
|
|
frameRate: number;
|
|
bitrate: number;
|
|
encodingMode: "fast" | "balanced" | "quality";
|
|
inputMode?: "rawvideo" | "h264-stream";
|
|
}) => {
|
|
return ipcRenderer.invoke("native-video-export-start", options);
|
|
},
|
|
nativeVideoExportWriteFrame: (sessionId: string, frameData: Uint8Array) => {
|
|
ensureNativeVideoExportWriteResultListener();
|
|
|
|
return new Promise<NativeVideoExportWriteResult>((resolve) => {
|
|
const requestId = nextNativeVideoExportWriteRequestId++;
|
|
nativeVideoExportWriteRequests.set(requestId, {
|
|
sessionId,
|
|
resolve,
|
|
});
|
|
|
|
ipcRenderer.send("native-video-export-write-frame-async", {
|
|
sessionId,
|
|
requestId,
|
|
frameData,
|
|
});
|
|
});
|
|
},
|
|
nativeVideoExportWriteFrames: (sessionId: string, frameDataList: Uint8Array[]) => {
|
|
ensureNativeVideoExportWriteResultListener();
|
|
|
|
return new Promise<NativeVideoExportWriteResult>((resolve) => {
|
|
const requestId = nextNativeVideoExportWriteRequestId++;
|
|
nativeVideoExportWriteRequests.set(requestId, {
|
|
sessionId,
|
|
resolve,
|
|
});
|
|
|
|
ipcRenderer.send("native-video-export-write-frames-async", {
|
|
sessionId,
|
|
requestId,
|
|
frameDataList,
|
|
});
|
|
});
|
|
},
|
|
nativeVideoExportFinish: (
|
|
sessionId: string,
|
|
options?: {
|
|
audioMode?: "none" | "copy-source" | "trim-source" | "edited-track";
|
|
audioSourcePath?: string | null;
|
|
audioSourceCodec?: string | null;
|
|
audioSourceSampleRate?: number;
|
|
outputDurationSec?: number;
|
|
trimSegments?: Array<{ startMs: number; endMs: number }>;
|
|
editedTrackStrategy?: "filtergraph-fast-path" | "offline-render-fallback";
|
|
editedTrackSegments?: Array<{ startMs: number; endMs: number; speed: number }>;
|
|
editedAudioData?: ArrayBuffer;
|
|
editedAudioMimeType?: string | null;
|
|
},
|
|
) => {
|
|
return ipcRenderer
|
|
.invoke("native-video-export-finish", sessionId, options)
|
|
.then((result) => {
|
|
settleNativeVideoExportPendingRequests(
|
|
sessionId,
|
|
result?.success
|
|
? { success: true }
|
|
: {
|
|
success: false,
|
|
error:
|
|
typeof result?.error === "string"
|
|
? result.error
|
|
: "Native video export session finished before all frame writes settled.",
|
|
},
|
|
);
|
|
|
|
return result;
|
|
}) as Promise<{
|
|
success: boolean;
|
|
data?: Uint8Array;
|
|
encoderName?: string;
|
|
error?: string;
|
|
metrics?: NativeVideoAudioMuxMetrics;
|
|
}>;
|
|
},
|
|
nativeVideoExportCancel: (sessionId: string) => {
|
|
return ipcRenderer.invoke("native-video-export-cancel", sessionId).finally(() => {
|
|
settleNativeVideoExportPendingRequests(sessionId, {
|
|
success: false,
|
|
error: "Native video export session was cancelled",
|
|
});
|
|
});
|
|
},
|
|
muxExportedVideoAudio: (
|
|
videoData: ArrayBuffer,
|
|
options?: {
|
|
audioMode?: "none" | "copy-source" | "trim-source" | "edited-track";
|
|
audioSourcePath?: string | null;
|
|
audioSourceCodec?: string | null;
|
|
audioSourceSampleRate?: number;
|
|
outputDurationSec?: number;
|
|
trimSegments?: Array<{ startMs: number; endMs: number }>;
|
|
editedTrackStrategy?: "filtergraph-fast-path" | "offline-render-fallback";
|
|
editedTrackSegments?: Array<{ startMs: number; endMs: number; speed: number }>;
|
|
editedAudioData?: ArrayBuffer;
|
|
editedAudioMimeType?: string | null;
|
|
},
|
|
) => {
|
|
return ipcRenderer.invoke("mux-exported-video-audio", videoData, options) as Promise<{
|
|
success: boolean;
|
|
tempPath?: string;
|
|
error?: string;
|
|
metrics?: NativeVideoAudioMuxMetrics;
|
|
}>;
|
|
},
|
|
muxExportedVideoAudioFromPath: (
|
|
videoPath: string,
|
|
options?: {
|
|
audioMode?: "none" | "copy-source" | "trim-source" | "edited-track";
|
|
audioSourcePath?: string | null;
|
|
audioSourceCodec?: string | null;
|
|
audioSourceSampleRate?: number;
|
|
outputDurationSec?: number;
|
|
trimSegments?: Array<{ startMs: number; endMs: number }>;
|
|
editedTrackStrategy?: "filtergraph-fast-path" | "offline-render-fallback";
|
|
editedTrackSegments?: Array<{ startMs: number; endMs: number; speed: number }>;
|
|
editedAudioData?: ArrayBuffer;
|
|
editedAudioMimeType?: string | null;
|
|
},
|
|
) => {
|
|
return ipcRenderer.invoke("mux-exported-video-audio-from-path", videoPath, options);
|
|
},
|
|
openExportStream: (options?: { extension?: string }) => {
|
|
return ipcRenderer.invoke("export-stream-open", options);
|
|
},
|
|
writeExportStreamChunk: (streamId: string, position: number, chunk: Uint8Array) => {
|
|
return ipcRenderer.invoke("export-stream-write", streamId, position, chunk);
|
|
},
|
|
closeExportStream: (streamId: string, options?: { abort?: boolean }) => {
|
|
return ipcRenderer.invoke("export-stream-close", streamId, options);
|
|
},
|
|
finalizeExportedVideo: (payload: {
|
|
tempPath: string;
|
|
fileName: string;
|
|
outputPath?: string | null;
|
|
captionSidecar?: {
|
|
format: "srt" | "vtt" | "both";
|
|
cues: Array<{
|
|
startMs: number;
|
|
endMs: number;
|
|
text: string;
|
|
}>;
|
|
};
|
|
}) => {
|
|
return ipcRenderer.invoke("finalize-exported-video", payload);
|
|
},
|
|
discardExportedTemp: (tempPath: string) => {
|
|
return ipcRenderer.invoke("discard-exported-temp", tempPath);
|
|
},
|
|
getVideoAudioFallbackPaths: (videoPath: string) => {
|
|
return ipcRenderer.invoke("get-video-audio-fallback-paths", videoPath);
|
|
},
|
|
getSources: async (opts: Electron.SourcesOptions) => {
|
|
return await ipcRenderer.invoke("get-sources", opts);
|
|
},
|
|
switchToEditor: () => {
|
|
return ipcRenderer.invoke("switch-to-editor");
|
|
},
|
|
openSourceSelector: () => {
|
|
return ipcRenderer.invoke("open-source-selector");
|
|
},
|
|
selectSource: (source: ProcessedDesktopSource) => {
|
|
return ipcRenderer.invoke("select-source", source);
|
|
},
|
|
showSourceHighlight: (source: ProcessedDesktopSource) => {
|
|
return ipcRenderer.invoke("show-source-highlight", source);
|
|
},
|
|
getSelectedSource: () => {
|
|
return ipcRenderer.invoke("get-selected-source");
|
|
},
|
|
onSelectedSourceChanged: (callback: (source: ProcessedDesktopSource | null) => void) => {
|
|
const listener = (
|
|
_event: Electron.IpcRendererEvent,
|
|
payload: ProcessedDesktopSource | null,
|
|
) => callback(payload);
|
|
ipcRenderer.on("selected-source-changed", listener);
|
|
return () => ipcRenderer.removeListener("selected-source-changed", listener);
|
|
},
|
|
startNativeScreenRecording: (
|
|
source: ProcessedDesktopSource,
|
|
options?: {
|
|
capturesSystemAudio?: boolean;
|
|
capturesMicrophone?: boolean;
|
|
microphoneDeviceId?: string;
|
|
microphoneLabel?: string;
|
|
},
|
|
) => {
|
|
return ipcRenderer.invoke("start-native-screen-recording", source, options);
|
|
},
|
|
stopNativeScreenRecording: () => {
|
|
return ipcRenderer.invoke("stop-native-screen-recording");
|
|
},
|
|
recoverNativeScreenRecording: () => {
|
|
return ipcRenderer.invoke("recover-native-screen-recording");
|
|
},
|
|
getLastNativeCaptureDiagnostics: () => {
|
|
return ipcRenderer.invoke("get-last-native-capture-diagnostics");
|
|
},
|
|
pauseNativeScreenRecording: () => {
|
|
return ipcRenderer.invoke("pause-native-screen-recording");
|
|
},
|
|
resumeNativeScreenRecording: () => {
|
|
return ipcRenderer.invoke("resume-native-screen-recording");
|
|
},
|
|
pauseCursorCapture: (pausedAtMs?: number) => {
|
|
return ipcRenderer.invoke("pause-cursor-capture", pausedAtMs);
|
|
},
|
|
resumeCursorCapture: (resumedAtMs?: number) => {
|
|
return ipcRenderer.invoke("resume-cursor-capture", resumedAtMs);
|
|
},
|
|
startFfmpegRecording: (source: ProcessedDesktopSource) => {
|
|
return ipcRenderer.invoke("start-ffmpeg-recording", source);
|
|
},
|
|
stopFfmpegRecording: () => {
|
|
return ipcRenderer.invoke("stop-ffmpeg-recording");
|
|
},
|
|
storeRecordedVideo: (videoData: ArrayBuffer, fileName: string) => {
|
|
return ipcRenderer.invoke("store-recorded-video", videoData, fileName);
|
|
},
|
|
storeMicrophoneSidecar: (
|
|
audioData: ArrayBuffer,
|
|
videoPath: string,
|
|
options?: {
|
|
startDelayMs?: number;
|
|
browserMicrophoneProfile?: string;
|
|
requestedBrowserMicrophoneProfile?: string | null;
|
|
requestedConstraints?: unknown;
|
|
mediaTrackSettings?: Record<string, boolean | number | string>;
|
|
audioInputDevices?: Array<{
|
|
deviceId: string;
|
|
groupId?: string;
|
|
label: string;
|
|
}>;
|
|
mediaRecorder?: {
|
|
mimeType?: string;
|
|
audioBitsPerSecond?: number;
|
|
timesliceMs?: number;
|
|
};
|
|
chunkEvents?: Array<{
|
|
index: number;
|
|
size: number;
|
|
elapsedMs: number;
|
|
deltaMs: number | null;
|
|
}>;
|
|
},
|
|
) => {
|
|
return ipcRenderer.invoke("store-microphone-sidecar", audioData, videoPath, options);
|
|
},
|
|
getRecordedVideoPath: () => {
|
|
return ipcRenderer.invoke("get-recorded-video-path");
|
|
},
|
|
setRecordingState: (recording: boolean) => {
|
|
return ipcRenderer.invoke("set-recording-state", recording);
|
|
},
|
|
setCursorScale: (scale: number) => {
|
|
return ipcRenderer.invoke("set-cursor-scale", scale);
|
|
},
|
|
getCursorTelemetry: (videoPath?: string) => {
|
|
return ipcRenderer.invoke("get-cursor-telemetry", videoPath);
|
|
},
|
|
setCursorTelemetry: (videoPath: string | undefined, samples: CursorTelemetryPoint[]) => {
|
|
return ipcRenderer.invoke("set-cursor-telemetry", videoPath, samples);
|
|
},
|
|
getSystemCursorAssets: () => {
|
|
return ipcRenderer.invoke("get-system-cursor-assets");
|
|
},
|
|
onStopRecordingFromTray: (callback: () => void) => {
|
|
const listener = () => callback();
|
|
ipcRenderer.on("stop-recording-from-tray", listener);
|
|
return () => ipcRenderer.removeListener("stop-recording-from-tray", listener);
|
|
},
|
|
onRecordingStateChanged: (
|
|
callback: (state: { recording: boolean; sourceName: string }) => void,
|
|
) => {
|
|
const listener = (
|
|
_event: Electron.IpcRendererEvent,
|
|
payload: { recording: boolean; sourceName: string },
|
|
) => callback(payload);
|
|
ipcRenderer.on("recording-state-changed", listener);
|
|
return () => ipcRenderer.removeListener("recording-state-changed", listener);
|
|
},
|
|
onRecordingInterrupted: (callback: (state: { reason: string; message: string }) => void) => {
|
|
const listener = (
|
|
_event: Electron.IpcRendererEvent,
|
|
payload: { reason: string; message: string },
|
|
) => callback(payload);
|
|
ipcRenderer.on("recording-interrupted", listener);
|
|
return () => ipcRenderer.removeListener("recording-interrupted", listener);
|
|
},
|
|
onCursorStateChanged: (
|
|
callback: (state: { cursorType: CursorTelemetryPoint["cursorType"] }) => void,
|
|
) => {
|
|
const listener = (
|
|
_event: Electron.IpcRendererEvent,
|
|
payload: { cursorType: CursorTelemetryPoint["cursorType"] },
|
|
) => callback(payload);
|
|
ipcRenderer.on("cursor-state-changed", listener);
|
|
return () => ipcRenderer.removeListener("cursor-state-changed", listener);
|
|
},
|
|
openExternalUrl: (url: string) => {
|
|
return ipcRenderer.invoke("open-external-url", url);
|
|
},
|
|
getAccessibilityPermissionStatus: () => {
|
|
return ipcRenderer.invoke("get-accessibility-permission-status");
|
|
},
|
|
requestAccessibilityPermission: () => {
|
|
return ipcRenderer.invoke("request-accessibility-permission");
|
|
},
|
|
getScreenRecordingPermissionStatus: () => {
|
|
return ipcRenderer.invoke("get-screen-recording-permission-status");
|
|
},
|
|
openScreenRecordingPreferences: () => {
|
|
return ipcRenderer.invoke("open-screen-recording-preferences");
|
|
},
|
|
openAccessibilityPreferences: () => {
|
|
return ipcRenderer.invoke("open-accessibility-preferences");
|
|
},
|
|
saveExportedVideo: (
|
|
videoData: ArrayBuffer,
|
|
fileName: string,
|
|
captionSidecar?: {
|
|
format: "srt" | "vtt" | "both";
|
|
cues: Array<{
|
|
startMs: number;
|
|
endMs: number;
|
|
text: string;
|
|
}>;
|
|
},
|
|
) => {
|
|
return ipcRenderer.invoke("save-exported-video", videoData, fileName, captionSidecar);
|
|
},
|
|
writeExportedVideoToPath: (
|
|
videoData: ArrayBuffer,
|
|
outputPath: string,
|
|
captionSidecar?: {
|
|
format: "srt" | "vtt" | "both";
|
|
cues: Array<{
|
|
startMs: number;
|
|
endMs: number;
|
|
text: string;
|
|
}>;
|
|
},
|
|
) => {
|
|
return ipcRenderer.invoke(
|
|
"write-exported-video-to-path",
|
|
videoData,
|
|
outputPath,
|
|
captionSidecar,
|
|
);
|
|
},
|
|
openVideoFilePicker: (options?: { includeProjects?: boolean }) => {
|
|
return ipcRenderer.invoke("open-video-file-picker", options);
|
|
},
|
|
openAudioFilePicker: () => {
|
|
return ipcRenderer.invoke("open-audio-file-picker");
|
|
},
|
|
openWhisperExecutablePicker: () => {
|
|
return ipcRenderer.invoke("open-whisper-executable-picker");
|
|
},
|
|
openWhisperModelPicker: () => {
|
|
return ipcRenderer.invoke("open-whisper-model-picker");
|
|
},
|
|
getWhisperSmallModelStatus: () => {
|
|
return ipcRenderer.invoke("get-whisper-small-model-status");
|
|
},
|
|
downloadWhisperSmallModel: () => {
|
|
return ipcRenderer.invoke("download-whisper-small-model");
|
|
},
|
|
deleteWhisperSmallModel: () => {
|
|
return ipcRenderer.invoke("delete-whisper-small-model");
|
|
},
|
|
onWhisperSmallModelDownloadProgress: (
|
|
callback: (state: {
|
|
status: "idle" | "downloading" | "downloaded" | "error";
|
|
progress: number;
|
|
path?: string | null;
|
|
error?: string;
|
|
}) => void,
|
|
) => {
|
|
const listener = (
|
|
_event: Electron.IpcRendererEvent,
|
|
payload: {
|
|
status: "idle" | "downloading" | "downloaded" | "error";
|
|
progress: number;
|
|
path?: string | null;
|
|
error?: string;
|
|
},
|
|
) => callback(payload);
|
|
ipcRenderer.on("whisper-small-model-download-progress", listener);
|
|
return () => ipcRenderer.removeListener("whisper-small-model-download-progress", listener);
|
|
},
|
|
generateAutoCaptions: (options: {
|
|
videoPath: string;
|
|
whisperExecutablePath?: string;
|
|
whisperModelPath: string;
|
|
language?: string;
|
|
}) => {
|
|
return ipcRenderer.invoke("generate-auto-captions", options);
|
|
},
|
|
setCurrentVideoPath: (
|
|
path: string,
|
|
options?: {
|
|
preserveProjectPath?: boolean;
|
|
hideOverlayCursorByDefault?: boolean;
|
|
},
|
|
) => {
|
|
return ipcRenderer.invoke("set-current-video-path", path, options);
|
|
},
|
|
setCurrentRecordingSession: (
|
|
session: {
|
|
videoPath: string;
|
|
webcamPath?: string | null;
|
|
timeOffsetMs?: number;
|
|
hideOverlayCursorByDefault?: boolean;
|
|
},
|
|
options?: { preserveProjectPath?: boolean },
|
|
) => {
|
|
return ipcRenderer.invoke("set-current-recording-session", session, options);
|
|
},
|
|
onRecordingSessionChanged: (callback: (session: RecordingSessionData | null) => void) => {
|
|
const listener = (
|
|
_event: Electron.IpcRendererEvent,
|
|
payload: RecordingSessionData | null,
|
|
) => callback(payload);
|
|
ipcRenderer.on("recording-session-changed", listener);
|
|
return () => ipcRenderer.removeListener("recording-session-changed", listener);
|
|
},
|
|
getCurrentRecordingSession: () => {
|
|
return ipcRenderer.invoke("get-current-recording-session");
|
|
},
|
|
getCurrentVideoPath: () => {
|
|
return ipcRenderer.invoke("get-current-video-path");
|
|
},
|
|
clearCurrentVideoPath: () => {
|
|
return ipcRenderer.invoke("clear-current-video-path");
|
|
},
|
|
deleteRecordingFile: (filePath: string) => {
|
|
return ipcRenderer.invoke("delete-recording-file", filePath);
|
|
},
|
|
getLocalMediaUrl: (filePath: string) => {
|
|
return ipcRenderer.invoke("get-local-media-url", filePath) as Promise<
|
|
{ success: true; url: string } | { success: false }
|
|
>;
|
|
},
|
|
saveProjectFile: (
|
|
projectData: unknown,
|
|
suggestedName?: string,
|
|
existingProjectPath?: string,
|
|
thumbnailDataUrl?: string | null,
|
|
) => {
|
|
return ipcRenderer.invoke(
|
|
"save-project-file",
|
|
projectData,
|
|
suggestedName,
|
|
existingProjectPath,
|
|
thumbnailDataUrl,
|
|
);
|
|
},
|
|
saveProjectFileNamed: (
|
|
projectData: unknown,
|
|
projectName: string,
|
|
thumbnailDataUrl?: string | null,
|
|
mode?: "rename" | "copy",
|
|
) => {
|
|
return ipcRenderer.invoke(
|
|
"save-project-file-named",
|
|
projectData,
|
|
projectName,
|
|
thumbnailDataUrl,
|
|
mode,
|
|
);
|
|
},
|
|
loadProjectFile: () => {
|
|
return ipcRenderer.invoke("load-project-file");
|
|
},
|
|
loadCurrentProjectFile: () => {
|
|
return ipcRenderer.invoke("load-current-project-file");
|
|
},
|
|
getProjectsDirectory: () => {
|
|
return ipcRenderer.invoke("get-projects-directory");
|
|
},
|
|
listProjectFiles: () => {
|
|
return ipcRenderer.invoke("list-project-files");
|
|
},
|
|
openProjectFileAtPath: (filePath: string) => {
|
|
return ipcRenderer.invoke("open-project-file-at-path", filePath);
|
|
},
|
|
openProjectsDirectory: () => {
|
|
return ipcRenderer.invoke("open-projects-directory");
|
|
},
|
|
installDownloadedUpdate: () => {
|
|
return ipcRenderer.invoke("install-downloaded-update");
|
|
},
|
|
downloadAvailableUpdate: (installAfterDownload?: boolean) => {
|
|
return ipcRenderer.invoke("download-available-update", installAfterDownload);
|
|
},
|
|
deferDownloadedUpdate: (delayMs?: number) => {
|
|
return ipcRenderer.invoke("defer-downloaded-update", delayMs);
|
|
},
|
|
dismissUpdateToast: () => {
|
|
return ipcRenderer.invoke("dismiss-update-toast");
|
|
},
|
|
skipUpdateVersion: () => {
|
|
return ipcRenderer.invoke("skip-update-version");
|
|
},
|
|
getCurrentUpdateToastPayload: () => {
|
|
return ipcRenderer.invoke("get-current-update-toast-payload");
|
|
},
|
|
getUpdateStatusSummary: () => {
|
|
return ipcRenderer.invoke("get-update-status-summary");
|
|
},
|
|
getExperimentalUpdatesEnabled: () => {
|
|
return ipcRenderer.invoke("get-experimental-updates-enabled");
|
|
},
|
|
setExperimentalUpdatesEnabled: (enabled: boolean) => {
|
|
return ipcRenderer.invoke("set-experimental-updates-enabled", enabled);
|
|
},
|
|
previewUpdateToast: () => {
|
|
return ipcRenderer.invoke("preview-update-toast");
|
|
},
|
|
checkForAppUpdates: () => {
|
|
return ipcRenderer.invoke("check-for-app-updates");
|
|
},
|
|
onUpdateToastStateChanged: (
|
|
callback: (
|
|
payload: {
|
|
version: string;
|
|
detail: string;
|
|
phase: "available" | "downloading" | "ready" | "error";
|
|
delayMs: number;
|
|
isPreview?: boolean;
|
|
progressPercent?: number;
|
|
transferredBytes?: number;
|
|
totalBytes?: number;
|
|
remainingBytes?: number;
|
|
bytesPerSecond?: number;
|
|
primaryAction?: "install-and-restart" | "retry-check";
|
|
} | null,
|
|
) => void,
|
|
) => {
|
|
const listener = (
|
|
_event: Electron.IpcRendererEvent,
|
|
payload: {
|
|
version: string;
|
|
detail: string;
|
|
phase: "available" | "downloading" | "ready" | "error";
|
|
delayMs: number;
|
|
isPreview?: boolean;
|
|
progressPercent?: number;
|
|
transferredBytes?: number;
|
|
totalBytes?: number;
|
|
remainingBytes?: number;
|
|
bytesPerSecond?: number;
|
|
primaryAction?: "install-and-restart" | "retry-check";
|
|
} | null,
|
|
) => callback(payload);
|
|
ipcRenderer.on("update-toast-state", listener);
|
|
return () => ipcRenderer.removeListener("update-toast-state", listener);
|
|
},
|
|
onUpdateReadyToast: (
|
|
callback: (payload: {
|
|
version: string;
|
|
detail: string;
|
|
delayMs: number;
|
|
isPreview?: boolean;
|
|
}) => void,
|
|
) => {
|
|
const listener = (
|
|
_event: Electron.IpcRendererEvent,
|
|
payload: { version: string; detail: string; delayMs: number; isPreview?: boolean },
|
|
) => callback(payload);
|
|
ipcRenderer.on("update-ready-toast", listener);
|
|
return () => ipcRenderer.removeListener("update-ready-toast", listener);
|
|
},
|
|
onMenuLoadProject: (callback: () => void) => {
|
|
const listener = () => callback();
|
|
ipcRenderer.on("menu-load-project", listener);
|
|
return () => ipcRenderer.removeListener("menu-load-project", listener);
|
|
},
|
|
onMenuSaveProject: (callback: () => void) => {
|
|
const listener = () => callback();
|
|
ipcRenderer.on("menu-save-project", listener);
|
|
return () => ipcRenderer.removeListener("menu-save-project", listener);
|
|
},
|
|
onMenuSaveProjectAs: (callback: () => void) => {
|
|
const listener = () => callback();
|
|
ipcRenderer.on("menu-save-project-as", listener);
|
|
return () => ipcRenderer.removeListener("menu-save-project-as", listener);
|
|
},
|
|
getPlatform: () => {
|
|
return ipcRenderer.invoke("get-platform");
|
|
},
|
|
getLinuxWindowSystem: () => {
|
|
return ipcRenderer.invoke("get-linux-window-system");
|
|
},
|
|
revealInFolder: (filePath: string) => {
|
|
return ipcRenderer.invoke("reveal-in-folder", filePath);
|
|
},
|
|
openRecordingsFolder: () => {
|
|
return ipcRenderer.invoke("open-recordings-folder");
|
|
},
|
|
getRecordingsDirectory: () => {
|
|
return ipcRenderer.invoke("get-recordings-directory");
|
|
},
|
|
chooseRecordingsDirectory: () => {
|
|
return ipcRenderer.invoke("choose-recordings-directory");
|
|
},
|
|
getShortcuts: () => {
|
|
return ipcRenderer.invoke("get-shortcuts");
|
|
},
|
|
saveShortcuts: (shortcuts: unknown) => {
|
|
return ipcRenderer.invoke("save-shortcuts", shortcuts);
|
|
},
|
|
getAppSetting: (key: string) => {
|
|
const result = ipcRenderer.sendSync("app-settings:get", key) as {
|
|
success?: boolean;
|
|
value?: unknown;
|
|
};
|
|
return result?.success ? (result.value ?? null) : null;
|
|
},
|
|
setAppSetting: (key: string, value: unknown) => {
|
|
const result = ipcRenderer.sendSync("app-settings:set", key, value) as {
|
|
success?: boolean;
|
|
};
|
|
return result?.success === true;
|
|
},
|
|
setHasUnsavedChanges: (hasChanges: boolean) => {
|
|
ipcRenderer.send("set-has-unsaved-changes", hasChanges);
|
|
},
|
|
onRequestSaveBeforeClose: (callback: () => Promise<boolean>) => {
|
|
const listener = async () => {
|
|
let saved = false;
|
|
try {
|
|
saved = await callback();
|
|
} catch {
|
|
saved = false;
|
|
}
|
|
ipcRenderer.send("save-before-close-done", saved);
|
|
};
|
|
ipcRenderer.on("request-save-before-close", listener);
|
|
return () => ipcRenderer.removeListener("request-save-before-close", listener);
|
|
},
|
|
isNativeWindowsCaptureAvailable: () =>
|
|
ipcRenderer.invoke("is-native-windows-capture-available"),
|
|
muxNativeWindowsRecording: (expectedDurationMs?: number) =>
|
|
ipcRenderer.invoke("mux-native-windows-recording", expectedDurationMs),
|
|
hideOsCursor: () => ipcRenderer.invoke("hide-cursor"),
|
|
getAppVersion: () => ipcRenderer.invoke("app:getVersion"),
|
|
getAnnouncements: () => ipcRenderer.invoke("announcements:get"),
|
|
getRecordingPreferences: () => ipcRenderer.invoke("get-recording-preferences"),
|
|
getRecordingAudioLabConfig: () => ipcRenderer.invoke("get-recording-audio-lab-config"),
|
|
setRecordingPreferences: (prefs: {
|
|
microphoneEnabled?: boolean;
|
|
microphoneDeviceId?: string;
|
|
systemAudioEnabled?: boolean;
|
|
webcamEnabled?: boolean;
|
|
webcamDeviceId?: string;
|
|
}) => ipcRenderer.invoke("set-recording-preferences", prefs),
|
|
getCountdownDelay: () => ipcRenderer.invoke("get-countdown-delay"),
|
|
setCountdownDelay: (delay: number) => ipcRenderer.invoke("set-countdown-delay", delay),
|
|
startCountdown: (seconds: number) => ipcRenderer.invoke("start-countdown", seconds),
|
|
cancelCountdown: () => ipcRenderer.invoke("cancel-countdown"),
|
|
getActiveCountdown: () => ipcRenderer.invoke("get-active-countdown"),
|
|
onCountdownTick: (callback: (seconds: number) => void) => {
|
|
const listener = (_event: Electron.IpcRendererEvent, seconds: number) => callback(seconds);
|
|
ipcRenderer.on("countdown-tick", listener);
|
|
return () => ipcRenderer.removeListener("countdown-tick", listener);
|
|
},
|
|
});
|