revert: undo PRs 625 and 626

This commit is contained in:
wiiiii123
2026-05-30 01:20:36 +07:00
parent 06794779fb
commit 62323ee3ee
16 changed files with 73 additions and 509 deletions
@@ -4,6 +4,7 @@ import { forwardRef, useEffect, useMemo, useRef, useState } from "react";
import type {
SourceAudioTrackMeta,
SourceAudioTrackSettings,
SourceAudioTrackWithPeaks,
} from "@/components/video-editor/audio/audioTypes";
import { useScopedT } from "@/contexts/I18nContext";
import { useShortcuts } from "@/contexts/ShortcutsContext";
@@ -25,10 +26,6 @@ import { calculateTimelineScale } from "./core/time";
import { useTimelineAudioPeaks } from "./hooks/useTimelineAudioPeaks";
import { useTimelineEditorRuntime } from "./hooks/useTimelineEditorRuntime";
import { useTimelineRange } from "./hooks/useTimelineRange";
import {
buildSourceSidecarPathCandidates,
buildTimelineSourceAudioTracks,
} from "./sourceAudioTracks";
export interface TimelineEditorProps {
videoDuration: number;
@@ -93,6 +90,16 @@ function extractLocalPathFromMediaServerUrl(input: string | null | undefined): s
}
}
function buildSourceSidecarPath(source: string, suffix: "mic" | "system"): string {
const normalized = source.replace(/\\/g, "/");
const lastSlash = normalized.lastIndexOf("/");
const dir = lastSlash >= 0 ? normalized.slice(0, lastSlash + 1) : "";
const fileName = lastSlash >= 0 ? normalized.slice(lastSlash + 1) : normalized;
const dotIndex = fileName.lastIndexOf(".");
const baseName = dotIndex > 0 ? fileName.slice(0, dotIndex) : fileName;
return `${dir}${baseName}.${suffix}.wav`;
}
export interface TimelineEditorHandle {
addZoom: () => void;
suggestZooms: () => void;
@@ -227,8 +234,12 @@ const TimelineEditor = forwardRef<TimelineEditorHandle, TimelineEditorProps>(
return { previewSpans, hiddenZoomIds };
}, [clipRegions, liveSpanPreviewById, zoomRegions]);
const { shortcuts: keyShortcuts, isMac } = useShortcuts();
const { peaks: sourceAudioPeaks, loading: sourceAudioLoading } =
useTimelineAudioPeaks(videoPath);
const { peaks: sourceAudioPeaks, loading: sourceAudioLoading } = useTimelineAudioPeaks(
videoPath,
{
enableSourceSidecarFallback: true,
},
);
const localSourcePath = useMemo(() => {
if (!videoPath) return null;
return (
@@ -236,44 +247,45 @@ const TimelineEditor = forwardRef<TimelineEditorHandle, TimelineEditorProps>(
(/^file:\/\//i.test(videoPath) ? fromFileUrl(videoPath) : videoPath)
);
}, [videoPath]);
const micSidecarPaths = useMemo(
() => (localSourcePath ? buildSourceSidecarPathCandidates(localSourcePath, "mic") : []),
const micSidecarPath = useMemo(
() => (localSourcePath ? buildSourceSidecarPath(localSourcePath, "mic") : null),
[localSourcePath],
);
const micSidecarFallbackPaths = useMemo(() => micSidecarPaths.slice(1), [micSidecarPaths]);
const systemSidecarPaths = useMemo(
() =>
localSourcePath ? buildSourceSidecarPathCandidates(localSourcePath, "system") : [],
const systemSidecarPath = useMemo(
() => (localSourcePath ? buildSourceSidecarPath(localSourcePath, "system") : null),
[localSourcePath],
);
const systemSidecarFallbackPaths = useMemo(
() => systemSidecarPaths.slice(1),
[systemSidecarPaths],
);
const { peaks: micSidecarPeaks, loading: micSidecarLoading } = useTimelineAudioPeaks(
micSidecarPaths[0] ?? null,
{ fallbackResources: micSidecarFallbackPaths },
);
const { peaks: systemSidecarPeaks, loading: systemSidecarLoading } = useTimelineAudioPeaks(
systemSidecarPaths[0] ?? null,
{
fallbackResources: systemSidecarFallbackPaths,
},
);
const sourceAudioTracks = useMemo(
() =>
buildTimelineSourceAudioTracks({
sourceAudioPeaks,
micSidecarPeaks,
systemSidecarPeaks,
labels: {
system: t("audio.systemLabel", "Source System"),
mic: t("audio.micLabel", "Source Mic"),
mixed: t("audio.mixedLabel", "Source"),
},
}),
[micSidecarPeaks, sourceAudioPeaks, systemSidecarPeaks, t],
);
const { peaks: micSidecarPeaks, loading: micSidecarLoading } =
useTimelineAudioPeaks(micSidecarPath);
const { peaks: systemSidecarPeaks, loading: systemSidecarLoading } =
useTimelineAudioPeaks(systemSidecarPath);
const sourceAudioTracks = useMemo<SourceAudioTrackWithPeaks[]>(() => {
if (systemSidecarPeaks || micSidecarPeaks) {
const tracks: SourceAudioTrackWithPeaks[] = [];
if (systemSidecarPeaks)
tracks.push({
id: "system",
label: t("audio.systemLabel", "Source System"),
peaks: systemSidecarPeaks,
});
if (micSidecarPeaks)
tracks.push({
id: "mic",
label: t("audio.micLabel", "Source Mic"),
peaks: micSidecarPeaks,
});
return tracks;
}
return sourceAudioPeaks
? [
{
id: "mixed",
label: t("audio.mixedLabel", "Source"),
peaks: sourceAudioPeaks,
},
]
: [];
}, [micSidecarPeaks, sourceAudioPeaks, systemSidecarPeaks, t]);
const isLoading = useMemo(() => {
// If we are still actively trying to load audio peaks (main or sidecars)
@@ -5,8 +5,6 @@ import { fromFileUrl } from "../../projectPersistence";
import { WAVEFORM_DEFAULT_PEAK_COUNT } from "../core/constants";
import type { AudioPeaksData } from "../core/timelineTypes";
const EMPTY_FALLBACK_RESOURCES: string[] = [];
function buildSidecarAudioCandidates(sourcePath: string): string[] {
const normalized = sourcePath.replace(/\\/g, "/");
const lastSlash = normalized.lastIndexOf("/");
@@ -20,8 +18,6 @@ function buildSidecarAudioCandidates(sourcePath: string): string[] {
`${dir}${baseName}.mic.wav`,
`${dir}${baseName}.system.m4a`,
`${dir}${baseName}.mic.m4a`,
`${dir}${baseName}.system.webm`,
`${dir}${baseName}.mic.webm`,
];
}
@@ -41,7 +37,6 @@ function extractLocalPathFromMediaServerUrl(input: string): string | null {
interface TimelineAudioPeaksOptions {
enableSourceSidecarFallback?: boolean;
fallbackResources?: string[];
peakCount?: number;
}
@@ -58,7 +53,6 @@ export function useTimelineAudioPeaks(
const [loading, setLoading] = useState(false);
const sourceRef = useRef(mediaResource);
const enableSourceSidecarFallback = options.enableSourceSidecarFallback ?? false;
const fallbackResources = options.fallbackResources ?? EMPTY_FALLBACK_RESOURCES;
const peakCount = options.peakCount ?? WAVEFORM_DEFAULT_PEAK_COUNT;
useEffect(() => {
@@ -89,29 +83,25 @@ export function useTimelineAudioPeaks(
// fallthrough
}
if (!enableSourceSidecarFallback && fallbackResources.length === 0) {
if (!enableSourceSidecarFallback) {
if (!cancelled && sourceRef.current === mediaResource) {
setLoading(false);
}
return;
}
let sourceSidecarCandidates: string[] = [];
if (enableSourceSidecarFallback) {
const localPathFromServer = extractLocalPathFromMediaServerUrl(mediaResource);
const localSourcePath =
localPathFromServer ||
(/^file:\/\//i.test(mediaResource)
? fromFileUrl(mediaResource)
: mediaResource);
if (localSourcePath) {
sourceSidecarCandidates = buildSidecarAudioCandidates(localSourcePath);
const localPathFromServer = extractLocalPathFromMediaServerUrl(mediaResource);
const localSourcePath =
localPathFromServer ||
(/^file:\/\//i.test(mediaResource) ? fromFileUrl(mediaResource) : mediaResource);
if (!localSourcePath) {
if (!cancelled && sourceRef.current === mediaResource) {
setLoading(false);
}
return;
}
const candidates = Array.from(
new Set([...fallbackResources, ...sourceSidecarCandidates]),
);
const candidates = buildSidecarAudioCandidates(localSourcePath);
for (const candidate of candidates) {
try {
const result = await tryGenerate(candidate);
@@ -135,7 +125,7 @@ export function useTimelineAudioPeaks(
return () => {
cancelled = true;
};
}, [mediaResource, enableSourceSidecarFallback, fallbackResources, peakCount]);
}, [mediaResource, enableSourceSidecarFallback, peakCount]);
return { peaks, loading };
}
@@ -1,90 +0,0 @@
import { describe, expect, it } from "vitest";
import type { AudioPeaksData } from "./core/timelineTypes";
import {
buildSourceSidecarPathCandidates,
buildTimelineSourceAudioTracks,
} from "./sourceAudioTracks";
function peaks(id: number): AudioPeaksData {
return {
durationMs: 1000,
peaks: new Float32Array([id]),
};
}
const labels = {
system: "Source System",
mic: "Source Mic",
mixed: "Source",
};
describe("timeline source audio tracks", () => {
it("builds candidates for Windows and macOS sidecar containers", () => {
expect(buildSourceSidecarPathCandidates("C:\\Recordly\\recording-1.mp4", "mic")).toEqual([
"C:/Recordly/recording-1.mic.wav",
"C:/Recordly/recording-1.mic.m4a",
"C:/Recordly/recording-1.mic.webm",
]);
});
it("keeps embedded system audio controllable when mic is a sidecar", () => {
const source = peaks(1);
const mic = peaks(2);
expect(
buildTimelineSourceAudioTracks({
sourceAudioPeaks: source,
micSidecarPeaks: mic,
systemSidecarPeaks: null,
labels,
}),
).toEqual([
{ id: "system", label: "Source System", peaks: source },
{ id: "mic", label: "Source Mic", peaks: mic },
]);
});
it("does not invent a system track when only the mic sidecar exists", () => {
const mic = peaks(2);
expect(
buildTimelineSourceAudioTracks({
sourceAudioPeaks: null,
micSidecarPeaks: mic,
systemSidecarPeaks: null,
labels,
}),
).toEqual([{ id: "mic", label: "Source Mic", peaks: mic }]);
});
it("uses dedicated sidecars over the embedded track when both source tracks exist", () => {
const source = peaks(1);
const system = peaks(2);
const mic = peaks(3);
expect(
buildTimelineSourceAudioTracks({
sourceAudioPeaks: source,
micSidecarPeaks: mic,
systemSidecarPeaks: system,
labels,
}),
).toEqual([
{ id: "system", label: "Source System", peaks: system },
{ id: "mic", label: "Source Mic", peaks: mic },
]);
});
it("falls back to one mixed source track when no dedicated sidecar exists", () => {
const source = peaks(1);
expect(
buildTimelineSourceAudioTracks({
sourceAudioPeaks: source,
micSidecarPeaks: null,
systemSidecarPeaks: null,
labels,
}),
).toEqual([{ id: "mixed", label: "Source", peaks: source }]);
});
});
@@ -1,68 +0,0 @@
import type { SourceAudioTrackWithPeaks } from "@/components/video-editor/audio/audioTypes";
import type { AudioPeaksData } from "./core/timelineTypes";
const SOURCE_SIDECAR_EXTENSIONS = [".wav", ".m4a", ".webm"] as const;
export function buildSourceSidecarPathCandidates(
source: string,
suffix: "mic" | "system",
): string[] {
const normalized = source.replace(/\\/g, "/");
const lastSlash = normalized.lastIndexOf("/");
const dir = lastSlash >= 0 ? normalized.slice(0, lastSlash + 1) : "";
const fileName = lastSlash >= 0 ? normalized.slice(lastSlash + 1) : normalized;
const dotIndex = fileName.lastIndexOf(".");
const baseName = dotIndex > 0 ? fileName.slice(0, dotIndex) : fileName;
return SOURCE_SIDECAR_EXTENSIONS.map((extension) => `${dir}${baseName}.${suffix}${extension}`);
}
export function buildTimelineSourceAudioTracks({
sourceAudioPeaks,
micSidecarPeaks,
systemSidecarPeaks,
labels,
}: {
sourceAudioPeaks: AudioPeaksData | null;
micSidecarPeaks: AudioPeaksData | null;
systemSidecarPeaks: AudioPeaksData | null;
labels: {
system: string;
mic: string;
mixed: string;
};
}): SourceAudioTrackWithPeaks[] {
if (systemSidecarPeaks || micSidecarPeaks) {
const tracks: SourceAudioTrackWithPeaks[] = [];
if (systemSidecarPeaks) {
tracks.push({
id: "system",
label: labels.system,
peaks: systemSidecarPeaks,
});
} else if (micSidecarPeaks && sourceAudioPeaks) {
tracks.push({
id: "system",
label: labels.system,
peaks: sourceAudioPeaks,
});
}
if (micSidecarPeaks) {
tracks.push({
id: "mic",
label: labels.mic,
peaks: micSidecarPeaks,
});
}
return tracks;
}
return sourceAudioPeaks
? [
{
id: "mixed",
label: labels.mixed,
peaks: sourceAudioPeaks,
},
]
: [];
}
-27
View File
@@ -7,7 +7,6 @@ import {
normalizeBrowserMicrophoneProfile,
resolveBrowserCaptureCursorPolicy,
resolveLinuxPortalCursorPresentation,
shouldLockHudDuringDisplaySelection,
shouldUseLinuxPortalCapture,
shouldUseNativeWindowsCaptureForSource,
} from "./useScreenRecorder";
@@ -247,32 +246,6 @@ describe("shouldUseLinuxPortalCapture", () => {
});
});
describe("shouldLockHudDuringDisplaySelection", () => {
it("locks HUD fallback resizing while Linux portal selection is active", () => {
expect(
shouldLockHudDuringDisplaySelection({
platform: "linux",
useLinuxPortal: true,
}),
).toBe(true);
});
it("keeps non-portal capture flows interactive", () => {
expect(
shouldLockHudDuringDisplaySelection({
platform: "linux",
useLinuxPortal: false,
}),
).toBe(false);
expect(
shouldLockHudDuringDisplaySelection({
platform: "win32",
useLinuxPortal: true,
}),
).toBe(false);
});
});
describe("getScreenCaptureCursorSetting", () => {
it("normalizes only supported screen-capture cursor settings", () => {
expect(getScreenCaptureCursorSetting({ cursor: "motion" } as MediaTrackSettings)).toBe(
+2 -37
View File
@@ -145,16 +145,6 @@ export function shouldUseLinuxPortalCapture({
);
}
export function shouldLockHudDuringDisplaySelection({
platform,
useLinuxPortal,
}: {
platform?: string;
useLinuxPortal: boolean;
}) {
return platform === "linux" && useLinuxPortal;
}
type UseScreenRecorderReturn = {
recording: boolean;
paused: boolean;
@@ -1439,16 +1429,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
return;
}
let hudSourceSelectionActive = false;
const setHudSourceSelectionActive = (active: boolean) => {
if (hudSourceSelectionActive === active) {
return;
}
hudSourceSelectionActive = active;
window.electronAPI?.hudOverlaySetSourceSelectionActive?.(active);
};
hasPromptedForReselect.current = false;
startInFlight.current = true;
setStarting(true);
@@ -1648,14 +1628,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
}
setRecording(true);
try {
await window.electronAPI?.setRecordingState(true);
} catch (stateError) {
console.warn(
"Failed to notify main process that native recording started:",
stateError,
);
}
window.electronAPI?.setRecordingState(true);
return;
}
@@ -1703,9 +1676,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
browserCaptureSourceId: browserCaptureSource.id,
selectedSourceId: selectedSource.id,
});
if (shouldLockHudDuringDisplaySelection({ platform, useLinuxPortal })) {
setHudSourceSelectionActive(true);
}
const browserScreenVideoConstraints = {
mandatory: {
chromeMediaSource: CHROME_MEDIA_SOURCE,
@@ -2020,11 +1990,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
webcamStartTime.current === null ? 0 : webcamStartTime.current - mainStartedAt;
recorder.start(RECORDER_TIMESLICE_MS);
setRecording(true);
try {
await window.electronAPI?.setRecordingState(true);
} catch (stateError) {
console.warn("Failed to notify main process that recording started:", stateError);
}
window.electronAPI?.setRecordingState(true);
} catch (error) {
console.error("Failed to start recording:", error);
alert(
@@ -2042,7 +2008,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
await stopWebcamRecorder();
}
} finally {
setHudSourceSelectionActive(false);
startInFlight.current = false;
setStarting(false);
}
+1 -92
View File
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import { AudioProcessor, softLimitOfflineMixPeaksInPlace } from "./audioEncoder";
import { AudioProcessor } from "./audioEncoder";
type OfflineRenderTestHarness = AudioProcessor & {
decodeAudioFromUrl(url: string): Promise<AudioBuffer | null>;
@@ -26,32 +26,8 @@ type OfflineRenderTestHarness = AudioProcessor & {
sourceAudioFallbackStartDelayMsByPath: Record<string, number> | undefined,
muxer: unknown,
): Promise<void>;
renderChunked(
prepared: {
mainBufferEntry: null;
companionEntries: [];
regionEntries: [];
mutedSourceOutputRangesSec: [];
slices: [];
outputDurationMs: number;
numChannels: number;
},
totalOutputSec: number,
onChunk: (
rendered: AudioBuffer,
outputOffsetSec: number,
chunkIndex: number,
) => Promise<void>,
): Promise<void>;
};
function fakeAudioBuffer(channels: Float32Array[]): AudioBuffer {
return {
numberOfChannels: channels.length,
getChannelData: (channel: number) => channels[channel],
} as AudioBuffer;
}
describe("AudioProcessor offline render preparation", () => {
it("keeps embedded source audio separate from external companion sidecars", async () => {
const processor = new AudioProcessor() as unknown as OfflineRenderTestHarness;
@@ -160,71 +136,4 @@ describe("AudioProcessor offline render preparation", () => {
expect(loadAudioFileDemuxer).not.toHaveBeenCalled();
expect(renderAndMuxOfflineAudio).toHaveBeenCalled();
});
it("soft-limits mixed peaks before encoding or WAV conversion", () => {
const samples = new Float32Array([
-1.6,
-0.5,
Number.NEGATIVE_INFINITY,
Number.NaN,
0,
0.5,
0.95,
Number.POSITIVE_INFINITY,
1.6,
]);
const changed = softLimitOfflineMixPeaksInPlace(fakeAudioBuffer([samples]));
expect(changed).toBe(true);
expect(samples[0]).toBeGreaterThanOrEqual(-0.986);
expect(samples[1]).toBe(-0.5);
expect(samples[2]).toBe(0);
expect(samples[3]).toBe(0);
expect(samples[5]).toBe(0.5);
expect(samples[6]).toBeLessThan(0.95);
expect(samples[6]).toBeGreaterThan(0.9);
expect(samples[7]).toBe(0);
expect(samples[8]).toBeLessThanOrEqual(0.986);
});
it("runs the offline mix limiter for every rendered chunk", async () => {
const processor = new AudioProcessor() as unknown as OfflineRenderTestHarness;
const renderedSamples = new Float32Array([1.4]);
const renderedBuffer = fakeAudioBuffer([renderedSamples]);
const originalOfflineAudioContext = globalThis.OfflineAudioContext;
(
globalThis as unknown as { OfflineAudioContext: typeof OfflineAudioContext }
).OfflineAudioContext = class {
constructor() {}
startRendering() {
return Promise.resolve(renderedBuffer);
}
} as unknown as typeof OfflineAudioContext;
try {
let observedPeak = Number.POSITIVE_INFINITY;
await processor.renderChunked(
{
mainBufferEntry: null,
companionEntries: [],
regionEntries: [],
mutedSourceOutputRangesSec: [],
slices: [],
outputDurationMs: 100,
numChannels: 1,
},
0.1,
async (rendered) => {
observedPeak = rendered.getChannelData(0)[0] ?? 0;
},
);
expect(observedPeak).toBeLessThanOrEqual(0.986);
} finally {
(
globalThis as unknown as { OfflineAudioContext: typeof OfflineAudioContext }
).OfflineAudioContext = originalOfflineAudioContext;
}
});
});
-39
View File
@@ -21,44 +21,6 @@ const MP4_AUDIO_CODEC = "mp4a.40.2";
const OFFLINE_AUDIO_SAMPLE_RATE = 48_000;
const OFFLINE_ENCODE_CHUNK_FRAMES = 1024;
const OFFLINE_CHUNK_DURATION_SEC = 30;
const OFFLINE_MIX_SOFT_LIMITER_THRESHOLD = 0.9;
const OFFLINE_MIX_SOFT_LIMITER_CEILING = 0.985;
function softLimitSample(sample: number): number {
const magnitude = Math.abs(sample);
if (magnitude <= OFFLINE_MIX_SOFT_LIMITER_THRESHOLD) {
return sample;
}
const sign = sample < 0 ? -1 : 1;
const kneeRange = 1 - OFFLINE_MIX_SOFT_LIMITER_THRESHOLD;
const limitedMagnitude =
OFFLINE_MIX_SOFT_LIMITER_THRESHOLD +
kneeRange * Math.tanh((magnitude - OFFLINE_MIX_SOFT_LIMITER_THRESHOLD) / kneeRange);
return sign * Math.min(OFFLINE_MIX_SOFT_LIMITER_CEILING, limitedMagnitude);
}
export function softLimitOfflineMixPeaksInPlace(buffer: AudioBuffer): boolean {
let changed = false;
for (let channel = 0; channel < buffer.numberOfChannels; channel += 1) {
const data = buffer.getChannelData(channel);
for (let index = 0; index < data.length; index += 1) {
const sample = data[index];
if (!Number.isFinite(sample)) {
data[index] = 0;
changed = true;
continue;
}
const limited = softLimitSample(sample);
if (limited !== sample) {
data[index] = limited;
changed = true;
}
}
}
return changed;
}
function resolveSourceTrackGain(
sourceAudioTrackSettings: SourceAudioTrackSettings | undefined,
@@ -985,7 +947,6 @@ export class AudioProcessor {
const rendered = await offlineCtx.startRendering();
if (this.cancelled) break;
softLimitOfflineMixPeaksInPlace(rendered);
await onChunk(rendered, outputOffsetSec, i);