mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-11-09 15:33:31 +00:00
fix: videoexports with audio (system/mic/mixed/user)... everything should work
This commit is contained in:
1 parent
d3ab348239
commit
3211e515fe
9 files changed
+105
-17
No files matched your search
@@ -3148,14 +3148,19 @@ export function SettingsPanel({
|
||||
<SectionLabel>{tSettings("audio.title", "Audio")}</SectionLabel>
|
||||
|
||||
<div className="flex items-center justify-between rounded-lg bg-foreground/[0.03] px-2.5 py-1.5">
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{selectedClipMuted
|
||||
? tSettings("clip.unmuteAudio", "Unmute Audio")
|
||||
: tSettings("clip.muteAudio", "Mute Audio")}
|
||||
</span>
|
||||
<div>
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{tSettings("clip.mute", "Mute")}
|
||||
</span>
|
||||
<p className="text-[9px] text-muted-foreground/50 mt-0.5">
|
||||
{selectedClipMuted
|
||||
? tSettings("clip.mutedState", "Audio is muted")
|
||||
: tSettings("clip.unmutedState", "Audio is playing")}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={!(selectedClipMuted ?? false)}
|
||||
onCheckedChange={(v) => onClipMutedChange?.(!v)}
|
||||
checked={selectedClipMuted ?? false}
|
||||
onCheckedChange={(v) => onClipMutedChange?.(v)}
|
||||
className="data-[state=checked]:bg-[#06b6d4] scale-75"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -4417,6 +4417,7 @@ export default function VideoEditor() {
|
||||
cursorSway,
|
||||
frame,
|
||||
audioRegions,
|
||||
clipRegions,
|
||||
sourceAudioFallbackPaths: audio.sourceAudioFallbackPaths,
|
||||
sourceAudioFallbackStartDelayMsByPath:
|
||||
audio.sourceAudioFallbackStartDelayMsByPath,
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
MusicNotes as Music,
|
||||
MouseLeftClickIcon as PhMouseLeftClick,
|
||||
Scissors,
|
||||
SpeakerX,
|
||||
MagnifyingGlassPlus as ZoomIn,
|
||||
} from "@phosphor-icons/react";
|
||||
import type { Span } from "dnd-timeline";
|
||||
@@ -31,6 +32,7 @@ interface ItemProps {
|
||||
waveformSegmentSpan?: Span;
|
||||
waveformGain?: number;
|
||||
waveformNormalize?: boolean;
|
||||
muted?: boolean;
|
||||
variant?: "zoom" | "trim" | "clip" | "annotation" | "speed" | "audio";
|
||||
}
|
||||
|
||||
@@ -69,6 +71,7 @@ export default function Item({
|
||||
waveformSegmentSpan,
|
||||
waveformGain = 1,
|
||||
waveformNormalize = false,
|
||||
muted = false,
|
||||
variant = "zoom",
|
||||
children,
|
||||
}: ItemProps) {
|
||||
@@ -170,6 +173,12 @@ export default function Item({
|
||||
className="absolute inset-0 w-full h-full pointer-events-none opacity-45"
|
||||
/>
|
||||
)}
|
||||
{/* Muted overlay for source audio track items */}
|
||||
{isAudio && muted && (
|
||||
<div className="absolute inset-0 z-20 flex items-center justify-center gap-1 bg-red-900/40 pointer-events-none">
|
||||
<SpeakerX className="w-3 h-3 text-red-300/90 shrink-0" />
|
||||
</div>
|
||||
)}
|
||||
{/* Content */}
|
||||
<div className="relative z-10 flex flex-col items-center justify-center text-black/70 dark:text-white/90 opacity-80 group-hover:opacity-100 transition-opacity select-none overflow-hidden">
|
||||
<div className="flex items-center gap-1.5">
|
||||
|
||||
@@ -402,6 +402,7 @@ const TimelineCanvasRows = memo(function TimelineCanvasRows({
|
||||
waveformSegmentSpan={liveSpanPreviewById?.[item.id] ?? item.span}
|
||||
waveformGain={Math.max(0, Math.min(2, settings.volume))}
|
||||
waveformNormalize={Boolean(settings.normalize)}
|
||||
muted={item.muted}
|
||||
>
|
||||
{track.label}
|
||||
</Item>
|
||||
|
||||
@@ -39,6 +39,7 @@ export interface TimelineRenderItem {
|
||||
zoomMode?: ZoomMode;
|
||||
speedValue?: number;
|
||||
showSourceAudio?: boolean;
|
||||
muted?: boolean;
|
||||
variant: "zoom" | "trim" | "clip" | "annotation" | "speed" | "audio";
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ export function buildTimelineItems(params: {
|
||||
span: { start: region.startMs, end: region.endMs },
|
||||
label: `Clip ${index + 1}`,
|
||||
showSourceAudio: region.showSourceAudio,
|
||||
muted: Boolean(region.muted),
|
||||
variant: "clip",
|
||||
}));
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ interface PreparedOfflineRender {
|
||||
mainBufferEntry: { buffer: AudioBuffer; gain: number } | null;
|
||||
companionEntries: Array<{ buffer: AudioBuffer; startDelaySec: number; gain: number }>;
|
||||
regionEntries: Array<{ buffer: AudioBuffer; region: AudioRegion }>;
|
||||
mutedSourceOutputRangesSec: Array<{ startSec: number; endSec: number }>;
|
||||
slices: TimelineSlice[];
|
||||
outputDurationMs: number;
|
||||
numChannels: number;
|
||||
@@ -163,6 +164,7 @@ export class AudioProcessor {
|
||||
sourceAudioFallbackPaths?: string[],
|
||||
sourceAudioFallbackStartDelayMsByPath?: Record<string, number>,
|
||||
sourceAudioTrackSettings?: SourceAudioTrackSettings,
|
||||
clipRegions?: ClipRegion[],
|
||||
): Promise<void> {
|
||||
const sortedTrims = trimRegions
|
||||
? [...trimRegions].sort((a, b) => a.startMs - b.startMs)
|
||||
@@ -206,6 +208,7 @@ export class AudioProcessor {
|
||||
sortedSourceAudioFallbackPaths,
|
||||
sourceAudioFallbackStartDelayMsByPath,
|
||||
sourceAudioTrackSettings,
|
||||
clipRegions,
|
||||
muxer,
|
||||
);
|
||||
return;
|
||||
@@ -238,6 +241,7 @@ export class AudioProcessor {
|
||||
routingPolicy.playbackPaths,
|
||||
sourceAudioFallbackStartDelayMsByPath,
|
||||
sourceAudioTrackSettings,
|
||||
clipRegions,
|
||||
muxer,
|
||||
);
|
||||
return;
|
||||
@@ -285,6 +289,7 @@ export class AudioProcessor {
|
||||
sourceAudioFallbackPaths?: string[],
|
||||
sourceAudioFallbackStartDelayMsByPath?: Record<string, number>,
|
||||
sourceAudioTrackSettings?: SourceAudioTrackSettings,
|
||||
clipRegions?: ClipRegion[],
|
||||
): Promise<Blob> {
|
||||
const sortedTrims = trimRegions
|
||||
? [...trimRegions].sort((a, b) => a.startMs - b.startMs)
|
||||
@@ -311,6 +316,7 @@ export class AudioProcessor {
|
||||
sortedSourceAudioFallbackPaths,
|
||||
sourceAudioFallbackStartDelayMsByPath,
|
||||
sourceAudioTrackSettings,
|
||||
clipRegions,
|
||||
);
|
||||
return this.renderToWavBlobChunked(prepared);
|
||||
}
|
||||
@@ -587,6 +593,7 @@ export class AudioProcessor {
|
||||
sourceAudioFallbackPaths: string[],
|
||||
sourceAudioFallbackStartDelayMsByPath: Record<string, number> | undefined,
|
||||
sourceAudioTrackSettings: SourceAudioTrackSettings | undefined,
|
||||
clipRegions: ClipRegion[] | undefined,
|
||||
muxer: VideoMuxer,
|
||||
): Promise<void> {
|
||||
const prepared = await this.prepareOfflineRender(
|
||||
@@ -597,6 +604,7 @@ export class AudioProcessor {
|
||||
sourceAudioFallbackPaths,
|
||||
sourceAudioFallbackStartDelayMsByPath,
|
||||
sourceAudioTrackSettings,
|
||||
clipRegions,
|
||||
);
|
||||
if (this.cancelled) return;
|
||||
await this.renderAndEncodeChunked(prepared, muxer);
|
||||
@@ -610,6 +618,7 @@ export class AudioProcessor {
|
||||
sourceAudioFallbackPaths: string[],
|
||||
sourceAudioFallbackStartDelayMsByPath?: Record<string, number>,
|
||||
sourceAudioTrackSettings?: SourceAudioTrackSettings,
|
||||
clipRegions?: ClipRegion[],
|
||||
): Promise<PreparedOfflineRender> {
|
||||
if (this.cancelled) throw new Error("Export cancelled");
|
||||
this.onProgress?.(0);
|
||||
@@ -711,11 +720,24 @@ export class AudioProcessor {
|
||||
}
|
||||
|
||||
const numChannels = Math.min(primaryBuffer?.numberOfChannels ?? 2, 2);
|
||||
const mutedSourceOutputRangesSec = (clipRegions ?? [])
|
||||
.filter(
|
||||
(clip) =>
|
||||
Boolean(clip.muted) &&
|
||||
Number.isFinite(clip.startMs) &&
|
||||
Number.isFinite(clip.endMs) &&
|
||||
clip.endMs > clip.startMs,
|
||||
)
|
||||
.map((clip) => ({
|
||||
startSec: Math.max(0, clip.startMs / 1000),
|
||||
endSec: Math.max(0, clip.endMs / 1000),
|
||||
}));
|
||||
|
||||
return {
|
||||
mainBufferEntry,
|
||||
companionEntries,
|
||||
regionEntries,
|
||||
mutedSourceOutputRangesSec,
|
||||
slices,
|
||||
outputDurationMs,
|
||||
numChannels,
|
||||
@@ -849,6 +871,7 @@ export class AudioProcessor {
|
||||
prepared.mainBufferEntry.gain,
|
||||
outputOffsetSec,
|
||||
chunkSec,
|
||||
prepared.mutedSourceOutputRangesSec,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -862,6 +885,7 @@ export class AudioProcessor {
|
||||
entry.gain,
|
||||
outputOffsetSec,
|
||||
chunkSec,
|
||||
prepared.mutedSourceOutputRangesSec,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1323,6 +1347,7 @@ export class AudioProcessor {
|
||||
gain = 1,
|
||||
chunkOutputStartSec = 0,
|
||||
chunkDurationSec = Number.POSITIVE_INFINITY,
|
||||
mutedOutputRangesSec: Array<{ startSec: number; endSec: number }> = [],
|
||||
): void {
|
||||
let outputOffsetSec = 0;
|
||||
|
||||
@@ -1386,15 +1411,51 @@ export class AudioProcessor {
|
||||
continue;
|
||||
}
|
||||
|
||||
const source = ctx.createBufferSource();
|
||||
const gainNode = ctx.createGain();
|
||||
gainNode.gain.value = Math.max(0, Math.min(2, gain));
|
||||
source.buffer = buffer;
|
||||
source.playbackRate.value = slice.speed;
|
||||
source.connect(gainNode);
|
||||
gainNode.connect(ctx.destination);
|
||||
const audibleRanges: Array<{ startSec: number; endSec: number }> = [
|
||||
{
|
||||
startSec: localOutputStartSec + chunkOutputStartSec,
|
||||
endSec:
|
||||
localOutputStartSec + chunkOutputStartSec + effectiveSourceDurationSec / slice.speed,
|
||||
},
|
||||
];
|
||||
for (const mutedRange of mutedOutputRangesSec) {
|
||||
for (let rangeIndex = audibleRanges.length - 1; rangeIndex >= 0; rangeIndex -= 1) {
|
||||
const current = audibleRanges[rangeIndex];
|
||||
const overlapStart = Math.max(current.startSec, mutedRange.startSec);
|
||||
const overlapEnd = Math.min(current.endSec, mutedRange.endSec);
|
||||
if (overlapEnd <= overlapStart) {
|
||||
continue;
|
||||
}
|
||||
audibleRanges.splice(rangeIndex, 1);
|
||||
if (current.startSec < overlapStart) {
|
||||
audibleRanges.push({ startSec: current.startSec, endSec: overlapStart });
|
||||
}
|
||||
if (overlapEnd < current.endSec) {
|
||||
audibleRanges.push({ startSec: overlapEnd, endSec: current.endSec });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
source.start(localOutputStartSec, effectiveBufferStartSec, effectiveSourceDurationSec);
|
||||
for (const audibleRange of audibleRanges) {
|
||||
const audibleDurationSec = audibleRange.endSec - audibleRange.startSec;
|
||||
if (audibleDurationSec <= 0.001) {
|
||||
continue;
|
||||
}
|
||||
const source = ctx.createBufferSource();
|
||||
const gainNode = ctx.createGain();
|
||||
gainNode.gain.value = Math.max(0, Math.min(2, gain));
|
||||
source.buffer = buffer;
|
||||
source.playbackRate.value = slice.speed;
|
||||
source.connect(gainNode);
|
||||
gainNode.connect(ctx.destination);
|
||||
|
||||
const sourceOffsetSec =
|
||||
effectiveBufferStartSec +
|
||||
(audibleRange.startSec - (localOutputStartSec + chunkOutputStartSec)) * slice.speed;
|
||||
const localStartSec = audibleRange.startSec - chunkOutputStartSec;
|
||||
const sourceDurationSec = audibleDurationSec * slice.speed;
|
||||
source.start(localStartSec, sourceOffsetSec, sourceDurationSec);
|
||||
}
|
||||
|
||||
outputOffsetSec += sliceOutputDurationSec;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
AudioRegion,
|
||||
AutoCaptionSettings,
|
||||
CaptionCue,
|
||||
ClipRegion,
|
||||
CropRegion,
|
||||
CursorStyle,
|
||||
CursorTelemetryPoint,
|
||||
@@ -136,6 +137,7 @@ interface VideoExporterConfig extends ExportConfig {
|
||||
zoomClassicMode?: boolean;
|
||||
frame?: string | null;
|
||||
audioRegions?: AudioRegion[];
|
||||
clipRegions?: ClipRegion[];
|
||||
sourceAudioFallbackPaths?: string[];
|
||||
sourceAudioFallbackStartDelayMsByPath?: Record<string, number>;
|
||||
sourceAudioTrackSettings?: SourceAudioTrackSettings;
|
||||
@@ -1195,7 +1197,8 @@ export class ModernVideoExporter {
|
||||
audioRegions.length > 0 ||
|
||||
sourceAudioFallbackPaths.length > 1 ||
|
||||
hasTimedSourceAudioFallback ||
|
||||
hasNonDefaultSourceTrackSettings(this.config.sourceAudioTrackSettings)
|
||||
hasNonDefaultSourceTrackSettings(this.config.sourceAudioTrackSettings) ||
|
||||
(this.config.clipRegions ?? []).some((clip) => Boolean(clip.muted))
|
||||
) {
|
||||
const sourceDurationMs = Math.max(
|
||||
0,
|
||||
@@ -1820,6 +1823,7 @@ export class ModernVideoExporter {
|
||||
this.config.sourceAudioFallbackPaths,
|
||||
this.config.sourceAudioFallbackStartDelayMsByPath,
|
||||
this.config.sourceAudioTrackSettings,
|
||||
this.config.clipRegions,
|
||||
),
|
||||
description,
|
||||
"audio",
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
AudioRegion,
|
||||
AutoCaptionSettings,
|
||||
CaptionCue,
|
||||
ClipRegion,
|
||||
CropRegion,
|
||||
CursorStyle,
|
||||
CursorTelemetryPoint,
|
||||
@@ -91,6 +92,7 @@ interface VideoExporterConfig extends ExportConfig {
|
||||
zoomSmoothness?: number;
|
||||
frame?: string | null;
|
||||
audioRegions?: AudioRegion[];
|
||||
clipRegions?: ClipRegion[];
|
||||
sourceAudioFallbackPaths?: string[];
|
||||
sourceAudioFallbackStartDelayMsByPath?: Record<string, number>;
|
||||
sourceAudioTrackSettings?: SourceAudioTrackSettings;
|
||||
@@ -574,7 +576,8 @@ export class VideoExporter {
|
||||
audioRegions.length > 0 ||
|
||||
sourceAudioFallbackPaths.length > 1 ||
|
||||
hasTimedSourceAudioFallback ||
|
||||
hasNonDefaultSourceTrackSettings(this.config.sourceAudioTrackSettings)
|
||||
hasNonDefaultSourceTrackSettings(this.config.sourceAudioTrackSettings) ||
|
||||
(this.config.clipRegions ?? []).some((clip) => Boolean(clip.muted))
|
||||
) {
|
||||
const sourceDurationMs = Math.max(
|
||||
0,
|
||||
@@ -862,6 +865,7 @@ export class VideoExporter {
|
||||
this.config.sourceAudioFallbackPaths,
|
||||
this.config.sourceAudioFallbackStartDelayMsByPath,
|
||||
this.config.sourceAudioTrackSettings,
|
||||
this.config.clipRegions,
|
||||
),
|
||||
"native edited audio rendering",
|
||||
"audio",
|
||||
@@ -959,6 +963,7 @@ export class VideoExporter {
|
||||
this.config.sourceAudioFallbackPaths,
|
||||
this.config.sourceAudioFallbackStartDelayMsByPath,
|
||||
this.config.sourceAudioTrackSettings,
|
||||
this.config.clipRegions,
|
||||
),
|
||||
"ffmpeg edited audio rendering",
|
||||
"audio",
|
||||
|
||||
Reference in new issue
Block a user