mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 07:16:02 +00:00
fix(recording): add windows mic diagnostics fallback
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { WINDOWS_NATIVE_MIC_PRE_FILTERS } from "./audioFilters";
|
||||
import {
|
||||
RECORDING_AUDIO_SIDECAR_DEBUG_ENV,
|
||||
shouldKeepRecordingAudioSidecars,
|
||||
WINDOWS_NATIVE_MIC_PRE_FILTERS,
|
||||
} from "./audioFilters";
|
||||
|
||||
describe("Windows native mic pre-filter policy", () => {
|
||||
it("keeps repair filters without automatic gain or loudness normalization", () => {
|
||||
@@ -11,4 +15,23 @@ describe("Windows native mic pre-filter policy", () => {
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps native audio sidecars only when explicitly requested", () => {
|
||||
expect(shouldKeepRecordingAudioSidecars({})).toBe(false);
|
||||
expect(
|
||||
shouldKeepRecordingAudioSidecars({
|
||||
[RECORDING_AUDIO_SIDECAR_DEBUG_ENV]: "1",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldKeepRecordingAudioSidecars({
|
||||
[RECORDING_AUDIO_SIDECAR_DEBUG_ENV]: "true",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldKeepRecordingAudioSidecars({
|
||||
[RECORDING_AUDIO_SIDECAR_DEBUG_ENV]: "off",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
// Keep native mic capture dry by default. Automatic loudness normalization
|
||||
// amplified wireless-headset noise and WASAPI discontinuities during beta tests.
|
||||
export const WINDOWS_NATIVE_MIC_PRE_FILTERS = ["adeclip=threshold=1"];
|
||||
|
||||
export const RECORDING_AUDIO_SIDECAR_DEBUG_ENV = "RECORDLY_KEEP_RECORDING_AUDIO_SIDECARS";
|
||||
|
||||
export function shouldKeepRecordingAudioSidecars(env: NodeJS.ProcessEnv = process.env) {
|
||||
const value = env[RECORDING_AUDIO_SIDECAR_DEBUG_ENV]?.trim().toLowerCase();
|
||||
return value === "1" || value === "true" || value === "yes" || value === "on";
|
||||
}
|
||||
|
||||
@@ -23,7 +23,11 @@ import {
|
||||
} from "../state";
|
||||
import type { AudioSyncAdjustment } from "../types";
|
||||
import { moveFileWithOverwrite } from "../utils";
|
||||
import { WINDOWS_NATIVE_MIC_PRE_FILTERS } from "./audioFilters";
|
||||
import {
|
||||
RECORDING_AUDIO_SIDECAR_DEBUG_ENV,
|
||||
shouldKeepRecordingAudioSidecars,
|
||||
WINDOWS_NATIVE_MIC_PRE_FILTERS,
|
||||
} from "./audioFilters";
|
||||
import {
|
||||
getCompanionAudioStartDelayMs,
|
||||
getRecordingAudioMuxTimeoutMs,
|
||||
@@ -222,6 +226,7 @@ export async function muxNativeWindowsVideoWithAudio(
|
||||
micAudioPath: string | null,
|
||||
) {
|
||||
const ffmpegPath = getFfmpegBinaryPath();
|
||||
const keepAudioSidecars = shouldKeepRecordingAudioSidecars();
|
||||
const inputs: string[] = ["-i", videoPath];
|
||||
const audioInputs: string[] = [];
|
||||
const audioFilePaths: string[] = [];
|
||||
@@ -235,7 +240,9 @@ export async function muxNativeWindowsVideoWithAudio(
|
||||
const stat = await fs.stat(audioPath);
|
||||
if (stat.size <= 0) {
|
||||
console.warn(`[mux-win] Skipping ${label} audio: file is empty (${audioPath})`);
|
||||
await fs.rm(audioPath, { force: true }).catch(() => undefined);
|
||||
if (!keepAudioSidecars) {
|
||||
await fs.rm(audioPath, { force: true }).catch(() => undefined);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
inputs.push("-i", audioPath);
|
||||
@@ -387,6 +394,13 @@ export async function muxNativeWindowsVideoWithAudio(
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (keepAudioSidecars) {
|
||||
console.log(
|
||||
`[mux-win] Keeping native audio sidecars because ${RECORDING_AUDIO_SIDECAR_DEBUG_ENV} is enabled`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const audioPath of [systemAudioPath, micAudioPath]) {
|
||||
if (audioPath) {
|
||||
await Promise.all([
|
||||
|
||||
@@ -1,8 +1,30 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { shouldUseWindowsBrowserMicrophoneFallback } from "./windowsFallbacks";
|
||||
import {
|
||||
shouldStartWindowsBrowserMicrophoneFallback,
|
||||
shouldUseWindowsBrowserMicrophoneFallback,
|
||||
WINDOWS_MIC_CAPTURE_MODE_ENV,
|
||||
} from "./windowsFallbacks";
|
||||
|
||||
describe("shouldUseWindowsBrowserMicrophoneFallback", () => {
|
||||
it("can be forced before native capture starts", () => {
|
||||
expect(
|
||||
shouldStartWindowsBrowserMicrophoneFallback(
|
||||
{ capturesMicrophone: true },
|
||||
{ [WINDOWS_MIC_CAPTURE_MODE_ENV]: "browser" },
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not force fallback when microphone capture was not requested", () => {
|
||||
expect(
|
||||
shouldStartWindowsBrowserMicrophoneFallback(
|
||||
{ capturesMicrophone: false },
|
||||
{ [WINDOWS_MIC_CAPTURE_MODE_ENV]: "browser" },
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when native Windows mic initialization fails", () => {
|
||||
expect(
|
||||
shouldUseWindowsBrowserMicrophoneFallback(
|
||||
@@ -20,4 +42,14 @@ describe("shouldUseWindowsBrowserMicrophoneFallback", () => {
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("returns true when browser fallback is forced", () => {
|
||||
expect(
|
||||
shouldUseWindowsBrowserMicrophoneFallback(
|
||||
"Recording started",
|
||||
{ capturesMicrophone: true },
|
||||
{ [WINDOWS_MIC_CAPTURE_MODE_ENV]: "fallback" },
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,26 @@
|
||||
const WINDOWS_MIC_CAPTURE_INIT_WARNING = "WARNING: Failed to initialize WASAPI mic capture";
|
||||
export const WINDOWS_MIC_CAPTURE_MODE_ENV = "RECORDLY_WINDOWS_MIC_CAPTURE";
|
||||
|
||||
export function shouldStartWindowsBrowserMicrophoneFallback(
|
||||
options?: { capturesMicrophone?: boolean },
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
) {
|
||||
if (!options?.capturesMicrophone) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const mode = env[WINDOWS_MIC_CAPTURE_MODE_ENV]?.trim().toLowerCase();
|
||||
return mode === "browser" || mode === "fallback" || mode === "renderer";
|
||||
}
|
||||
|
||||
export function shouldUseWindowsBrowserMicrophoneFallback(
|
||||
captureOutput: string,
|
||||
options?: { capturesMicrophone?: boolean },
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
) {
|
||||
return (
|
||||
Boolean(options?.capturesMicrophone) &&
|
||||
captureOutput.includes(WINDOWS_MIC_CAPTURE_INIT_WARNING)
|
||||
(shouldStartWindowsBrowserMicrophoneFallback(options, env) ||
|
||||
captureOutput.includes(WINDOWS_MIC_CAPTURE_INIT_WARNING))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
getWindowsCaptureExePath,
|
||||
} from "../paths/binaries";
|
||||
import { rememberApprovedLocalReadPath } from "../project/manager";
|
||||
import { shouldKeepRecordingAudioSidecars } from "../recording/audioFilters";
|
||||
import {
|
||||
getCompanionAudioFallbackInfo,
|
||||
getFileSizeIfPresent,
|
||||
@@ -65,7 +66,10 @@ import {
|
||||
waitForWindowsCaptureStart,
|
||||
waitForWindowsCaptureStop,
|
||||
} from "../recording/windows";
|
||||
import { shouldUseWindowsBrowserMicrophoneFallback } from "../recording/windowsFallbacks";
|
||||
import {
|
||||
shouldStartWindowsBrowserMicrophoneFallback,
|
||||
shouldUseWindowsBrowserMicrophoneFallback,
|
||||
} from "../recording/windowsFallbacks";
|
||||
import {
|
||||
cachedSystemCursorAssets,
|
||||
cachedSystemCursorAssetsSourceMtimeMs,
|
||||
@@ -123,11 +127,7 @@ import {
|
||||
windowsPendingVideoPath,
|
||||
windowsSystemAudioPath,
|
||||
} from "../state";
|
||||
import type {
|
||||
CursorTelemetryPoint,
|
||||
NativeMacRecordingOptions,
|
||||
SelectedSource,
|
||||
} from "../types";
|
||||
import type { CursorTelemetryPoint, NativeMacRecordingOptions, SelectedSource } from "../types";
|
||||
import {
|
||||
getMacPrivacySettingsUrl,
|
||||
getRecordingsDir,
|
||||
@@ -191,6 +191,11 @@ async function cleanupWindowsOrphanedMicAudioPath(filePath: string | null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldKeepRecordingAudioSidecars()) {
|
||||
console.log(`[recording] Keeping orphaned native mic sidecar for diagnostics: ${filePath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await fs.rm(filePath, { force: true }).catch(() => undefined);
|
||||
}
|
||||
|
||||
@@ -238,6 +243,8 @@ export function registerRecordingHandlers(
|
||||
let systemAudioPath: string | null = null;
|
||||
let microphonePath: string | null = null;
|
||||
let orphanedMicAudioPath: string | null = null;
|
||||
const browserMicFallbackRequested =
|
||||
shouldStartWindowsBrowserMicrophoneFallback(options);
|
||||
const resolvedDisplay = resolveWindowsCaptureDisplay(
|
||||
source,
|
||||
getScreen().getAllDisplays(),
|
||||
@@ -266,7 +273,7 @@ export function registerRecordingHandlers(
|
||||
setWindowsSystemAudioPath(systemAudioPath);
|
||||
}
|
||||
|
||||
if (options?.capturesMicrophone) {
|
||||
if (options?.capturesMicrophone && !browserMicFallbackRequested) {
|
||||
microphonePath = path.join(recordingsDir, `recording-${timestamp}.mic.wav`);
|
||||
config.captureMic = true;
|
||||
config.micOutputPath = microphonePath;
|
||||
@@ -274,6 +281,9 @@ export function registerRecordingHandlers(
|
||||
config.micDeviceName = options.microphoneLabel;
|
||||
}
|
||||
setWindowsMicAudioPath(microphonePath);
|
||||
} else if (browserMicFallbackRequested) {
|
||||
config.captureMic = false;
|
||||
setWindowsMicAudioPath(null);
|
||||
}
|
||||
|
||||
recordNativeCaptureDiagnostics({
|
||||
@@ -312,10 +322,9 @@ export function registerRecordingHandlers(
|
||||
});
|
||||
|
||||
await waitForWindowsCaptureStart(wcProc);
|
||||
const microphoneFallbackRequired = shouldUseWindowsBrowserMicrophoneFallback(
|
||||
captureOutput,
|
||||
options,
|
||||
);
|
||||
const microphoneFallbackRequired =
|
||||
browserMicFallbackRequested ||
|
||||
shouldUseWindowsBrowserMicrophoneFallback(captureOutput, options);
|
||||
if (microphoneFallbackRequired) {
|
||||
orphanedMicAudioPath = microphonePath;
|
||||
setWindowsOrphanedMicAudioPath(orphanedMicAudioPath);
|
||||
@@ -1034,92 +1043,89 @@ export function registerRecordingHandlers(
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle(
|
||||
"mux-native-windows-recording",
|
||||
async (_event, expectedDurationMs?: number) => {
|
||||
const videoPath = windowsPendingVideoPath;
|
||||
const orphanedMicAudioPath = windowsOrphanedMicAudioPath;
|
||||
setWindowsPendingVideoPath(null);
|
||||
setWindowsOrphanedMicAudioPath(null);
|
||||
ipcMain.handle("mux-native-windows-recording", async (_event, expectedDurationMs?: number) => {
|
||||
const videoPath = windowsPendingVideoPath;
|
||||
const orphanedMicAudioPath = windowsOrphanedMicAudioPath;
|
||||
setWindowsPendingVideoPath(null);
|
||||
setWindowsOrphanedMicAudioPath(null);
|
||||
|
||||
if (!videoPath) {
|
||||
return { success: false, message: "No native Windows video pending for mux" };
|
||||
if (!videoPath) {
|
||||
return { success: false, message: "No native Windows video pending for mux" };
|
||||
}
|
||||
|
||||
try {
|
||||
try {
|
||||
const padding = await extendNativeWindowsVideoToDuration(
|
||||
videoPath,
|
||||
expectedDurationMs,
|
||||
);
|
||||
if (padding.padded) {
|
||||
console.log(
|
||||
`[mux-win] Extended native Windows video to ${padding.durationSeconds.toFixed(3)}s using the final frame`,
|
||||
);
|
||||
}
|
||||
} catch (paddingError) {
|
||||
console.warn(
|
||||
"[mux-win] Failed to extend native Windows video duration:",
|
||||
paddingError,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
try {
|
||||
const padding = await extendNativeWindowsVideoToDuration(
|
||||
videoPath,
|
||||
expectedDurationMs,
|
||||
);
|
||||
if (padding.padded) {
|
||||
console.log(
|
||||
`[mux-win] Extended native Windows video to ${padding.durationSeconds.toFixed(3)}s using the final frame`,
|
||||
);
|
||||
}
|
||||
} catch (paddingError) {
|
||||
console.warn(
|
||||
"[mux-win] Failed to extend native Windows video duration:",
|
||||
paddingError,
|
||||
);
|
||||
}
|
||||
|
||||
if (windowsSystemAudioPath || windowsMicAudioPath) {
|
||||
await muxNativeWindowsVideoWithAudio(
|
||||
videoPath,
|
||||
windowsSystemAudioPath,
|
||||
windowsMicAudioPath,
|
||||
);
|
||||
setWindowsSystemAudioPath(null);
|
||||
setWindowsMicAudioPath(null);
|
||||
}
|
||||
|
||||
recordNativeCaptureDiagnostics({
|
||||
backend: "windows-wgc",
|
||||
phase: "mux",
|
||||
outputPath: videoPath,
|
||||
fileSizeBytes: await getFileSizeIfPresent(videoPath),
|
||||
});
|
||||
await cleanupWindowsOrphanedMicAudioPath(orphanedMicAudioPath);
|
||||
return await finalizeStoredVideo(videoPath);
|
||||
} catch (error) {
|
||||
console.error("Failed to mux native Windows recording:", error);
|
||||
recordNativeCaptureDiagnostics({
|
||||
backend: "windows-wgc",
|
||||
phase: "mux",
|
||||
outputPath: videoPath,
|
||||
systemAudioPath: windowsSystemAudioPath,
|
||||
microphonePath: windowsMicAudioPath,
|
||||
fileSizeBytes: await getFileSizeIfPresent(videoPath),
|
||||
error: String(error),
|
||||
});
|
||||
if (windowsSystemAudioPath || windowsMicAudioPath) {
|
||||
await muxNativeWindowsVideoWithAudio(
|
||||
videoPath,
|
||||
windowsSystemAudioPath,
|
||||
windowsMicAudioPath,
|
||||
);
|
||||
setWindowsSystemAudioPath(null);
|
||||
setWindowsMicAudioPath(null);
|
||||
await cleanupWindowsOrphanedMicAudioPath(orphanedMicAudioPath);
|
||||
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.
|
||||
}
|
||||
}
|
||||
|
||||
recordNativeCaptureDiagnostics({
|
||||
backend: "windows-wgc",
|
||||
phase: "mux",
|
||||
outputPath: videoPath,
|
||||
fileSizeBytes: await getFileSizeIfPresent(videoPath),
|
||||
});
|
||||
await cleanupWindowsOrphanedMicAudioPath(orphanedMicAudioPath);
|
||||
return await finalizeStoredVideo(videoPath);
|
||||
} catch (error) {
|
||||
console.error("Failed to mux native Windows recording:", error);
|
||||
recordNativeCaptureDiagnostics({
|
||||
backend: "windows-wgc",
|
||||
phase: "mux",
|
||||
outputPath: videoPath,
|
||||
systemAudioPath: windowsSystemAudioPath,
|
||||
microphonePath: windowsMicAudioPath,
|
||||
fileSizeBytes: await getFileSizeIfPresent(videoPath),
|
||||
error: String(error),
|
||||
});
|
||||
setWindowsSystemAudioPath(null);
|
||||
setWindowsMicAudioPath(null);
|
||||
await cleanupWindowsOrphanedMicAudioPath(orphanedMicAudioPath);
|
||||
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",
|
||||
error: String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("start-ffmpeg-recording", async (_, source: SelectedSource) => {
|
||||
if (ffmpegCaptureProcess) {
|
||||
|
||||
@@ -59,12 +59,16 @@ function pauseRecording(
|
||||
paused: boolean,
|
||||
isNativeRecording: boolean,
|
||||
webcamRecorder?: ReturnType<typeof createMockMediaRecorder> | null,
|
||||
micFallbackRecorder?: ReturnType<typeof createMockMediaRecorder> | null,
|
||||
): boolean {
|
||||
if (!recording || paused) return false;
|
||||
if (isNativeRecording) {
|
||||
if (webcamRecorder?.state === "recording") {
|
||||
webcamRecorder.pause();
|
||||
}
|
||||
if (micFallbackRecorder?.state === "recording") {
|
||||
micFallbackRecorder.pause();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (recorder.state === "recording") {
|
||||
@@ -83,12 +87,16 @@ function resumeRecording(
|
||||
paused: boolean,
|
||||
isNativeRecording: boolean,
|
||||
webcamRecorder?: ReturnType<typeof createMockMediaRecorder> | null,
|
||||
micFallbackRecorder?: ReturnType<typeof createMockMediaRecorder> | null,
|
||||
): boolean {
|
||||
if (!recording || !paused) return false;
|
||||
if (isNativeRecording) {
|
||||
if (webcamRecorder?.state === "paused") {
|
||||
webcamRecorder.resume();
|
||||
}
|
||||
if (micFallbackRecorder?.state === "paused") {
|
||||
micFallbackRecorder.resume();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (recorder.state === "paused") {
|
||||
@@ -104,6 +112,7 @@ function resumeRecording(
|
||||
async function pauseNativeRecording(
|
||||
webcamRecorder?: ReturnType<typeof createMockMediaRecorder> | null,
|
||||
result: { success: boolean } = { success: true },
|
||||
micFallbackRecorder?: ReturnType<typeof createMockMediaRecorder> | null,
|
||||
): Promise<boolean> {
|
||||
if (!result.success) {
|
||||
return false;
|
||||
@@ -112,6 +121,9 @@ async function pauseNativeRecording(
|
||||
if (webcamRecorder?.state === "recording") {
|
||||
webcamRecorder.pause();
|
||||
}
|
||||
if (micFallbackRecorder?.state === "recording") {
|
||||
micFallbackRecorder.pause();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -119,6 +131,7 @@ async function pauseNativeRecording(
|
||||
async function resumeNativeRecording(
|
||||
webcamRecorder?: ReturnType<typeof createMockMediaRecorder> | null,
|
||||
result: { success: boolean } = { success: true },
|
||||
micFallbackRecorder?: ReturnType<typeof createMockMediaRecorder> | null,
|
||||
): Promise<boolean> {
|
||||
if (!result.success) {
|
||||
return false;
|
||||
@@ -127,6 +140,9 @@ async function resumeNativeRecording(
|
||||
if (webcamRecorder?.state === "paused") {
|
||||
webcamRecorder.resume();
|
||||
}
|
||||
if (micFallbackRecorder?.state === "paused") {
|
||||
micFallbackRecorder.resume();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -324,6 +340,15 @@ describe("useScreenRecorder state machine", () => {
|
||||
expect(webcam.state).toBe("paused");
|
||||
});
|
||||
|
||||
it("pauses browser mic fallback during native recording pause", () => {
|
||||
const micFallback = createMockMediaRecorder("recording");
|
||||
|
||||
const result = pauseRecording(recorder, true, false, true, null, micFallback);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(micFallback.state).toBe("paused");
|
||||
});
|
||||
|
||||
it("skips webcam pause when webcam is not recording", () => {
|
||||
const webcam = createMockMediaRecorder("inactive");
|
||||
|
||||
@@ -378,6 +403,16 @@ describe("useScreenRecorder state machine", () => {
|
||||
expect(webcam.state).toBe("recording");
|
||||
});
|
||||
|
||||
it("resumes browser mic fallback during native recording resume", () => {
|
||||
const micFallback = createMockMediaRecorder("recording");
|
||||
micFallback.pause();
|
||||
|
||||
const result = resumeRecording(recorder, true, true, true, null, micFallback);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(micFallback.state).toBe("recording");
|
||||
});
|
||||
|
||||
it("skips webcam resume when webcam is not paused", () => {
|
||||
recorder.pause();
|
||||
const webcam = createMockMediaRecorder("inactive");
|
||||
@@ -489,26 +524,40 @@ describe("useScreenRecorder state machine", () => {
|
||||
|
||||
it("native recording pauses webcam only after native pause succeeds", async () => {
|
||||
const webcam = createMockMediaRecorder("recording");
|
||||
const micFallback = createMockMediaRecorder("recording");
|
||||
|
||||
const pausedResult = await pauseNativeRecording(webcam);
|
||||
const pausedResult = await pauseNativeRecording(webcam, { success: true }, micFallback);
|
||||
expect(pausedResult).toBe(true);
|
||||
expect(webcam.state).toBe("paused");
|
||||
expect(micFallback.state).toBe("paused");
|
||||
expect(recorder.pause).not.toHaveBeenCalled();
|
||||
|
||||
const resumedResult = await resumeNativeRecording(webcam);
|
||||
const resumedResult = await resumeNativeRecording(
|
||||
webcam,
|
||||
{ success: true },
|
||||
micFallback,
|
||||
);
|
||||
expect(resumedResult).toBe(true);
|
||||
expect(webcam.state).toBe("recording");
|
||||
expect(micFallback.state).toBe("recording");
|
||||
expect(recorder.resume).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("native recording leaves webcam state alone when native pause fails", async () => {
|
||||
const webcam = createMockMediaRecorder("recording");
|
||||
const micFallback = createMockMediaRecorder("recording");
|
||||
|
||||
const pausedResult = await pauseNativeRecording(webcam, { success: false });
|
||||
const pausedResult = await pauseNativeRecording(
|
||||
webcam,
|
||||
{ success: false },
|
||||
micFallback,
|
||||
);
|
||||
|
||||
expect(pausedResult).toBe(false);
|
||||
expect(webcam.state).toBe("recording");
|
||||
expect(webcam.pause).not.toHaveBeenCalled();
|
||||
expect(micFallback.state).toBe("recording");
|
||||
expect(micFallback.pause).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("stops native capture before awaiting webcam finalization", async () => {
|
||||
|
||||
@@ -1037,14 +1037,14 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
? 0
|
||||
: webcamStartTime.current - mainStartedAt;
|
||||
|
||||
// When native mic capture is unavailable (macOS < 14), record mic
|
||||
// via browser getUserMedia so it can be saved as a sidecar file.
|
||||
// When native mic capture is unavailable or explicitly bypassed,
|
||||
// record mic via browser getUserMedia as a sidecar file.
|
||||
if (nativeResult.microphoneFallbackRequired && microphoneEnabled) {
|
||||
void logNativeCaptureDiagnostics("start-browser-microphone-fallback");
|
||||
toast.warning(
|
||||
"Native microphone capture is unavailable. Using browser microphone fallback for this recording.",
|
||||
{ id: MICROPHONE_FALLBACK_TOAST_ID, duration: 8000 },
|
||||
);
|
||||
toast.warning("Using browser microphone fallback for this recording.", {
|
||||
id: MICROPHONE_FALLBACK_TOAST_ID,
|
||||
duration: 8000,
|
||||
});
|
||||
try {
|
||||
const micStream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: microphoneDeviceId
|
||||
@@ -1431,6 +1431,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
if (webcamRecorder.current?.state === "recording") {
|
||||
webcamRecorder.current.pause();
|
||||
}
|
||||
if (micFallbackRecorder.current?.state === "recording") {
|
||||
micFallbackRecorder.current.pause();
|
||||
}
|
||||
const boundaryMs = Date.now();
|
||||
markRecordingPaused(boundaryMs);
|
||||
setPaused(true);
|
||||
@@ -1476,6 +1479,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
if (webcamRecorder.current?.state === "paused") {
|
||||
webcamRecorder.current.resume();
|
||||
}
|
||||
if (micFallbackRecorder.current?.state === "paused") {
|
||||
micFallbackRecorder.current.resume();
|
||||
}
|
||||
const boundaryMs = Date.now();
|
||||
markRecordingResumed(boundaryMs);
|
||||
setPaused(false);
|
||||
|
||||
Reference in New Issue
Block a user