mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-11-09 15:33:31 +00:00
Fix Windows app and webcam lifecycle
This commit is contained in:
1 parent
d90b3f67ec
commit
7dffa98ae6
6 files changed
+260
-71
No files matched your search
+57
-23
@@ -40,8 +40,8 @@ import {
|
||||
getUpdateStatusSummary,
|
||||
installDownloadedUpdateNow,
|
||||
previewUpdateToast,
|
||||
setupAutoUpdates,
|
||||
setExperimentalUpdatesEnabled,
|
||||
setupAutoUpdates,
|
||||
skipAvailableUpdateVersion,
|
||||
} from "./updater";
|
||||
import {
|
||||
@@ -175,6 +175,7 @@ let trayContextMenu: Menu | null = null;
|
||||
let selectedSourceName = "";
|
||||
let editorHasUnsavedChanges = false;
|
||||
let isForceClosing = false;
|
||||
let isAppQuitting = false;
|
||||
let isCreatingMainWindow = false;
|
||||
let isCreatingEditorWindow = false;
|
||||
const shouldEnforceSingleInstanceLock = !IS_DEV;
|
||||
@@ -198,6 +199,22 @@ function closeEditorWindowBypassingUnsavedPrompt(window: BrowserWindow | null) {
|
||||
window.close();
|
||||
}
|
||||
|
||||
function closeEditorWindowToHud(window: BrowserWindow | null) {
|
||||
if (!window || window.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The HUD renderer normally remains hidden while the editor is open so
|
||||
// recording finalization can continue. Restore that HUD before destroying
|
||||
// the editor, keeping Recordly in its ready-to-record state on the taskbar.
|
||||
window.hide();
|
||||
if (mainWindow === window) {
|
||||
mainWindow = null;
|
||||
}
|
||||
createWindow();
|
||||
closeEditorWindowBypassingUnsavedPrompt(window);
|
||||
}
|
||||
|
||||
function restoreWindowSafely(window: BrowserWindow | null) {
|
||||
if (!window || window.isDestroyed()) {
|
||||
return;
|
||||
@@ -414,26 +431,23 @@ function sendEditorMenuAction(
|
||||
|
||||
function setupApplicationMenu() {
|
||||
const isMac = process.platform === "darwin";
|
||||
if (!isMac) {
|
||||
Menu.setApplicationMenu(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const template: Electron.MenuItemConstructorOptions[] = [];
|
||||
template.push({
|
||||
label: app.name,
|
||||
submenu: [
|
||||
{ role: "about" },
|
||||
{ type: "separator" },
|
||||
{ role: "services" },
|
||||
{ type: "separator" },
|
||||
{ role: "hide" },
|
||||
{ role: "hideOthers" },
|
||||
{ role: "unhide" },
|
||||
{ type: "separator" },
|
||||
{ role: "quit" },
|
||||
],
|
||||
});
|
||||
if (isMac) {
|
||||
template.push({
|
||||
label: app.name,
|
||||
submenu: [
|
||||
{ role: "about" },
|
||||
{ type: "separator" },
|
||||
{ role: "services" },
|
||||
{ type: "separator" },
|
||||
{ role: "hide" },
|
||||
{ role: "hideOthers" },
|
||||
{ role: "unhide" },
|
||||
{ type: "separator" },
|
||||
{ role: "quit" },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
template.push(
|
||||
{
|
||||
@@ -454,7 +468,12 @@ function setupApplicationMenu() {
|
||||
accelerator: "CmdOrCtrl+Shift+S",
|
||||
click: () => sendEditorMenuAction("menu-save-project-as"),
|
||||
},
|
||||
...(isMac ? [] : [{ type: "separator" as const }, { role: "quit" as const }]),
|
||||
...(isMac
|
||||
? []
|
||||
: [
|
||||
{ type: "separator" as const },
|
||||
{ role: "quit" as const, accelerator: "CmdOrCtrl+Q" },
|
||||
]),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -776,6 +795,10 @@ function createEditorWindowWrapper() {
|
||||
|
||||
editorWindow.on("close", (event) => {
|
||||
if (isForceClosing || !editorHasUnsavedChanges) {
|
||||
if (process.platform === "win32" && !isForceClosing && !isAppQuitting) {
|
||||
event.preventDefault();
|
||||
closeEditorWindowToHud(editorWindow);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -795,11 +818,21 @@ function createEditorWindowWrapper() {
|
||||
editorWindow.webContents.send("request-save-before-close");
|
||||
ipcMain.once("save-before-close-done", (_event, saved: boolean) => {
|
||||
if (saved) {
|
||||
closeEditorWindowBypassingUnsavedPrompt(editorWindow);
|
||||
if (process.platform === "win32" && !isAppQuitting) {
|
||||
closeEditorWindowToHud(editorWindow);
|
||||
} else {
|
||||
closeEditorWindowBypassingUnsavedPrompt(editorWindow);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (choice === 1) {
|
||||
closeEditorWindowBypassingUnsavedPrompt(editorWindow);
|
||||
if (process.platform === "win32" && !isAppQuitting) {
|
||||
closeEditorWindowToHud(editorWindow);
|
||||
} else {
|
||||
closeEditorWindowBypassingUnsavedPrompt(editorWindow);
|
||||
}
|
||||
} else {
|
||||
isAppQuitting = false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -817,6 +850,7 @@ function createSourceSelectorWindowWrapper() {
|
||||
// On macOS, applications and their menu bar stay active until the user quits
|
||||
// explicitly with Cmd + Q.
|
||||
app.on("before-quit", () => {
|
||||
isAppQuitting = true;
|
||||
killWindowsCaptureProcess();
|
||||
showCursor();
|
||||
cleanupNativeVideoExportSessions();
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "react";
|
||||
import { getAssetPath, getRenderableAssetUrl, getRenderableVideoUrl } from "@/lib/assetPath";
|
||||
import { getWebcamShadowFilter } from "@/lib/exporter/shadowProfile";
|
||||
import { getSquircleSvgPath } from "@/lib/geometry/squircle";
|
||||
import {
|
||||
clampMediaTimeToDuration,
|
||||
enablePitchPreservingPlayback,
|
||||
@@ -27,6 +28,8 @@ import {
|
||||
DEFAULT_WALLPAPER_RELATIVE_PATH,
|
||||
isVideoWallpaperSource,
|
||||
} from "@/lib/wallpapers";
|
||||
import { type AspectRatio, formatAspectRatioForCSS } from "@/utils/aspectRatioUtils";
|
||||
import { AnnotationOverlay } from "./AnnotationOverlay";
|
||||
import { type CaptionEditTarget, normalizeCaptionEditText } from "./captionEditing";
|
||||
import { buildActiveCaptionLayout } from "./captionLayout";
|
||||
import {
|
||||
@@ -44,36 +47,6 @@ import {
|
||||
type CaptionCue,
|
||||
type CursorClickEffectStyle,
|
||||
type CursorStyle,
|
||||
type Padding,
|
||||
type SpeedRegion,
|
||||
type TrimRegion,
|
||||
type WebcamOverlaySettings,
|
||||
ZOOM_DEPTH_SCALES,
|
||||
type ZoomDepth,
|
||||
type ZoomFocus,
|
||||
type ZoomMotionBlurTuning,
|
||||
type ZoomRegion,
|
||||
type ZoomTransitionEasing,
|
||||
} from "./types";
|
||||
import { DEFAULT_FOCUS } from "./videoPlayback/constants";
|
||||
import {
|
||||
DEFAULT_CURSOR_CONFIG,
|
||||
PixiCursorOverlay,
|
||||
preloadCursorAssets,
|
||||
} from "./videoPlayback/cursorRenderer";
|
||||
import { clamp01 } from "./videoPlayback/mathUtils";
|
||||
import {
|
||||
createSpringState,
|
||||
getZoomSpringConfig,
|
||||
resetSpringState,
|
||||
type SpringState,
|
||||
stepSpringValue,
|
||||
} from "./videoPlayback/motionSmoothing";
|
||||
|
||||
import { getSquircleSvgPath } from "@/lib/geometry/squircle";
|
||||
import { type AspectRatio, formatAspectRatioForCSS } from "@/utils/aspectRatioUtils";
|
||||
import { AnnotationOverlay } from "./AnnotationOverlay";
|
||||
import {
|
||||
DEFAULT_CONNECTED_ZOOM_DURATION_MS,
|
||||
DEFAULT_CONNECTED_ZOOM_EASING,
|
||||
DEFAULT_CONNECTED_ZOOM_GAP_MS,
|
||||
@@ -102,7 +75,18 @@ import {
|
||||
DEFAULT_ZOOM_OUT_DURATION_MS,
|
||||
DEFAULT_ZOOM_OUT_EASING,
|
||||
getDefaultCaptionFontFamily,
|
||||
type Padding,
|
||||
type SpeedRegion,
|
||||
type TrimRegion,
|
||||
type WebcamOverlaySettings,
|
||||
ZOOM_DEPTH_SCALES,
|
||||
type ZoomDepth,
|
||||
type ZoomFocus,
|
||||
type ZoomMotionBlurTuning,
|
||||
type ZoomRegion,
|
||||
type ZoomTransitionEasing,
|
||||
} from "./types";
|
||||
import { DEFAULT_FOCUS } from "./videoPlayback/constants";
|
||||
import {
|
||||
type CursorFollowCameraState,
|
||||
computeCursorFollowFocus,
|
||||
@@ -110,11 +94,28 @@ import {
|
||||
resetCursorFollowCamera,
|
||||
SNAP_TO_EDGES_RATIO_AUTO,
|
||||
} from "./videoPlayback/cursorFollowCamera";
|
||||
import {
|
||||
DEFAULT_CURSOR_CONFIG,
|
||||
PixiCursorOverlay,
|
||||
preloadCursorAssets,
|
||||
} from "./videoPlayback/cursorRenderer";
|
||||
import { clampFocusToStage as clampFocusToStageUtil } from "./videoPlayback/focusUtils";
|
||||
import { layoutVideoContent as layoutVideoContentUtil } from "./videoPlayback/layoutUtils";
|
||||
import { clamp01 } from "./videoPlayback/mathUtils";
|
||||
import {
|
||||
createSpringState,
|
||||
getZoomSpringConfig,
|
||||
resetSpringState,
|
||||
type SpringState,
|
||||
stepSpringValue,
|
||||
} from "./videoPlayback/motionSmoothing";
|
||||
import { updateOverlayIndicator } from "./videoPlayback/overlayUtils";
|
||||
import { createVideoEventHandlers } from "./videoPlayback/videoEventHandlers";
|
||||
import { getWebcamMediaTargetTimeSeconds, shouldSeekWebcamMedia } from "./videoPlayback/webcamSync";
|
||||
import {
|
||||
getWebcamMediaTargetTimeSeconds,
|
||||
isWebcamMediaSynchronized,
|
||||
shouldSeekWebcamMedia,
|
||||
} from "./videoPlayback/webcamSync";
|
||||
import { findDominantRegion } from "./videoPlayback/zoomRegionUtils";
|
||||
import {
|
||||
applyZoomTransform,
|
||||
@@ -419,6 +420,10 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
width: number;
|
||||
height: number;
|
||||
} | null>(null);
|
||||
const webcamSynchronizedPathRef = useRef<string | null>(null);
|
||||
const [webcamSynchronizedPath, setWebcamSynchronizedPath] = useState<string | null>(null);
|
||||
const webcamMediaSynchronized =
|
||||
Boolean(webcamVideoPath) && webcamSynchronizedPath === webcamVideoPath;
|
||||
const captionBoxRef = useRef<HTMLDivElement | null>(null);
|
||||
const captionEditInputRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const captionEditSessionRef = useRef<CaptionEditSession | null>(null);
|
||||
@@ -1641,7 +1646,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
? webcamVideo.duration
|
||||
: null;
|
||||
const targetTime = getWebcamMediaTargetTimeSeconds({
|
||||
currentTime,
|
||||
currentTime: currentTimeRef.current / 1000,
|
||||
webcamDuration,
|
||||
timeOffsetMs: webcamTimeOffsetMs,
|
||||
});
|
||||
@@ -1650,6 +1655,36 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
? Math.min(1 / 60, webcamDuration)
|
||||
: targetTime;
|
||||
|
||||
if (
|
||||
webcamSynchronizedPathRef.current !== webcamVideoPath &&
|
||||
!isWebcamMediaSynchronized({
|
||||
currentTime: webcamVideo.currentTime,
|
||||
targetTime: mediaTargetTime,
|
||||
readyState: webcamVideo.readyState,
|
||||
isSeeking: webcamVideo.seeking,
|
||||
})
|
||||
) {
|
||||
webcamVideo.pause();
|
||||
if (
|
||||
webcamVideo.readyState >= HTMLMediaElement.HAVE_METADATA &&
|
||||
!webcamVideo.seeking &&
|
||||
Math.abs(webcamVideo.currentTime - mediaTargetTime) > 0.01
|
||||
) {
|
||||
try {
|
||||
webcamVideo.currentTime = mediaTargetTime;
|
||||
} catch {
|
||||
// The next media-ready event retries once the source accepts seeks.
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (webcamSynchronizedPathRef.current !== webcamVideoPath) {
|
||||
webcamSynchronizedPathRef.current = webcamVideoPath;
|
||||
setWebcamSynchronizedPath(webcamVideoPath);
|
||||
lastWebcamSyncTimeRef.current = targetTime;
|
||||
}
|
||||
|
||||
const timelineTimeMs = currentTime * 1000;
|
||||
const activeSpeedRegion = speedRegionsRef.current.find(
|
||||
(region) => timelineTimeMs >= region.startMs && timelineTimeMs < region.endMs,
|
||||
@@ -2480,7 +2515,10 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 overflow-hidden"
|
||||
style={{
|
||||
opacity: webcamVideoDimensions ? 1 : 0,
|
||||
opacity:
|
||||
webcamVideoDimensions && webcamMediaSynchronized
|
||||
? 1
|
||||
: 0,
|
||||
transform: webcamMirror ? "scaleX(-1)" : undefined,
|
||||
}}
|
||||
>
|
||||
@@ -2498,6 +2536,8 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
aria-hidden="true"
|
||||
onLoadedMetadata={handleWebcamMediaReady}
|
||||
onLoadedData={handleWebcamMediaReady}
|
||||
onCanPlay={handleWebcamMediaReady}
|
||||
onSeeked={handleWebcamMediaReady}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { useCallback, useEffect, useId, useRef, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { CropRegion } from "./types";
|
||||
import { getWebcamPreviewTargetTimeSeconds } from "./videoPlayback/webcamSync";
|
||||
import {
|
||||
getWebcamPreviewTargetTimeSeconds,
|
||||
isWebcamMediaSynchronized,
|
||||
} from "./videoPlayback/webcamSync";
|
||||
import { normalizeWebcamCropRegion } from "./webcamOverlay";
|
||||
|
||||
type CropHandle = "move" | "nw" | "ne" | "sw" | "se";
|
||||
@@ -135,6 +138,9 @@ export function WebcamCropControl({
|
||||
const [activeHandle, setActiveHandle] = useState<CropHandle | null>(null);
|
||||
const [draftVisualCrop, setDraftVisualCrop] = useState<CropRegion | null>(null);
|
||||
const [previewFrame, setPreviewFrame] = useState<PreviewFrame | null>(null);
|
||||
const synchronizedPreviewSrcRef = useRef<string | null>(null);
|
||||
const [synchronizedPreviewSrc, setSynchronizedPreviewSrc] = useState<string | null>(null);
|
||||
const previewSynchronized = previewSrc !== null && synchronizedPreviewSrc === previewSrc;
|
||||
const hasPreviewFrame = previewSrc !== null && previewFrame?.src === previewSrc;
|
||||
const previewAspectRatio =
|
||||
hasPreviewFrame && previewFrame && previewFrame.width > 0 && previewFrame.height > 0
|
||||
@@ -178,6 +184,35 @@ export function WebcamCropControl({
|
||||
targetTime <= 0 && webcamDuration !== null && webcamDuration > 0
|
||||
? Math.min(1 / 60, webcamDuration)
|
||||
: targetTime;
|
||||
|
||||
if (
|
||||
synchronizedPreviewSrcRef.current !== previewSrc &&
|
||||
!isWebcamMediaSynchronized({
|
||||
currentTime: video.currentTime,
|
||||
targetTime: mediaTargetTime,
|
||||
readyState: video.readyState,
|
||||
isSeeking: video.seeking,
|
||||
})
|
||||
) {
|
||||
video.pause();
|
||||
if (
|
||||
video.readyState >= HTMLMediaElement.HAVE_METADATA &&
|
||||
!video.seeking &&
|
||||
Math.abs(video.currentTime - mediaTargetTime) > 0.01
|
||||
) {
|
||||
try {
|
||||
video.currentTime = mediaTargetTime;
|
||||
} catch {
|
||||
/* The next media-ready event retries once the source accepts seeks. */
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (synchronizedPreviewSrcRef.current !== previewSrc) {
|
||||
synchronizedPreviewSrcRef.current = previewSrc;
|
||||
setSynchronizedPreviewSrc(previewSrc);
|
||||
}
|
||||
const driftThreshold = previewPlaying ? 0.35 : 0.01;
|
||||
|
||||
if (Math.abs(video.currentTime - mediaTargetTime) > driftThreshold) {
|
||||
@@ -355,7 +390,7 @@ export function WebcamCropControl({
|
||||
src={previewSrc}
|
||||
className="pointer-events-none absolute inset-0 block h-full w-full object-fill"
|
||||
style={{
|
||||
opacity: hasPreviewFrame ? 1 : 0,
|
||||
opacity: hasPreviewFrame && previewSynchronized ? 1 : 0,
|
||||
transform: mirrored ? "scaleX(-1)" : undefined,
|
||||
}}
|
||||
muted
|
||||
@@ -364,6 +399,7 @@ export function WebcamCropControl({
|
||||
aria-hidden="true"
|
||||
onLoadedMetadata={handlePreviewFrameReady}
|
||||
onLoadedData={handlePreviewFrameReady}
|
||||
onCanPlay={handlePreviewFrameReady}
|
||||
onSeeked={handlePreviewFrameReady}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
getWebcamMediaTargetTimeSeconds,
|
||||
getWebcamPreviewTargetTimeSeconds,
|
||||
isWebcamMediaSynchronized,
|
||||
shouldSeekWebcamMedia,
|
||||
} from "./webcamSync";
|
||||
|
||||
@@ -99,3 +100,43 @@ describe("shouldSeekWebcamMedia", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isWebcamMediaSynchronized", () => {
|
||||
it("waits for current frame data and a completed seek", () => {
|
||||
expect(
|
||||
isWebcamMediaSynchronized({
|
||||
currentTime: 10,
|
||||
targetTime: 10,
|
||||
readyState: 1,
|
||||
isSeeking: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isWebcamMediaSynchronized({
|
||||
currentTime: 10,
|
||||
targetTime: 10,
|
||||
readyState: 2,
|
||||
isSeeking: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts a loaded webcam only when it is anchored to the timeline", () => {
|
||||
expect(
|
||||
isWebcamMediaSynchronized({
|
||||
currentTime: 9.95,
|
||||
targetTime: 10,
|
||||
readyState: 2,
|
||||
isSeeking: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isWebcamMediaSynchronized({
|
||||
currentTime: 9.5,
|
||||
targetTime: 10,
|
||||
readyState: 2,
|
||||
isSeeking: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
import { clampMediaTimeToDuration } from "@/lib/mediaTiming";
|
||||
|
||||
const HAVE_CURRENT_DATA_READY_STATE = 2;
|
||||
|
||||
/**
|
||||
* Maps the editor timeline time to the corresponding webcam media timestamp,
|
||||
* accounting for any recorded webcam start offset and media duration clamps.
|
||||
@@ -20,6 +22,26 @@ export function getWebcamMediaTargetTimeSeconds({
|
||||
|
||||
export const getWebcamPreviewTargetTimeSeconds = getWebcamMediaTargetTimeSeconds;
|
||||
|
||||
export function isWebcamMediaSynchronized({
|
||||
currentTime,
|
||||
targetTime,
|
||||
readyState,
|
||||
isSeeking,
|
||||
toleranceSeconds = 0.12,
|
||||
}: {
|
||||
currentTime: number;
|
||||
targetTime: number;
|
||||
readyState: number;
|
||||
isSeeking: boolean;
|
||||
toleranceSeconds?: number;
|
||||
}): boolean {
|
||||
return (
|
||||
readyState >= HAVE_CURRENT_DATA_READY_STATE &&
|
||||
!isSeeking &&
|
||||
Math.abs(currentTime - targetTime) <= toleranceSeconds
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decides whether the webcam media element needs a corrective seek for the
|
||||
* current preview frame, while avoiding repeated seeks during active media seeks.
|
||||
|
||||
@@ -1365,25 +1365,41 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
// We don't await this to keep the UI responsive
|
||||
void (async () => {
|
||||
try {
|
||||
// Await the webcam path in the background
|
||||
const webcamPath = await webcamPathPromise;
|
||||
console.log(
|
||||
"[useScreenRecorder] Background native processing: webcamPath is",
|
||||
webcamPath,
|
||||
);
|
||||
// Webcam persistence, microphone sidecars, and Windows muxing are
|
||||
// independent. Run them concurrently so the editor can keep playing
|
||||
// the main video and reveal the webcam as soon as only that file is ready.
|
||||
const webcamReadyPromise = webcamPathPromise.then(async (webcamPath) => {
|
||||
console.log(
|
||||
"[useScreenRecorder] Background native processing: webcamPath is",
|
||||
webcamPath,
|
||||
);
|
||||
|
||||
// Store sidecars
|
||||
await storeMicrophoneSidecar(
|
||||
if (webcamPath) {
|
||||
await window.electronAPI.setCurrentRecordingSession({
|
||||
videoPath: finalPath,
|
||||
webcamPath,
|
||||
timeOffsetMs: webcamTimeOffsetMs.current,
|
||||
hideOverlayCursorByDefault:
|
||||
hideEditorOverlayCursorByDefault.current,
|
||||
});
|
||||
}
|
||||
|
||||
return webcamPath;
|
||||
});
|
||||
const microphoneReadyPromise = storeMicrophoneSidecar(
|
||||
micFallbackBlobPromise,
|
||||
finalPath,
|
||||
fallbackStartDelayMs,
|
||||
fallbackTrackSettings,
|
||||
);
|
||||
|
||||
// Perform muxing/renaming if on Windows
|
||||
if (isNativeWindows) {
|
||||
await window.electronAPI.muxNativeWindowsRecording(expectedDurationMs);
|
||||
}
|
||||
const muxReadyPromise = isNativeWindows
|
||||
? window.electronAPI.muxNativeWindowsRecording(expectedDurationMs)
|
||||
: Promise.resolve(null);
|
||||
const [webcamPath] = await Promise.all([
|
||||
webcamReadyPromise,
|
||||
microphoneReadyPromise,
|
||||
muxReadyPromise,
|
||||
]);
|
||||
|
||||
console.log(
|
||||
"[useScreenRecorder] Emitting setCurrentRecordingSession with:",
|
||||
|
||||
Reference in new issue
Block a user