chore(export): add finalization stage profiling

This commit is contained in:
wiiiii123
2026-04-20 20:09:21 +07:00
parent 2c00f18770
commit 8bc79ea1cd
3 changed files with 314 additions and 165 deletions
+122 -80
View File
@@ -39,6 +39,7 @@ import { type DecodedVideoInfo, StreamingVideoDecoder } from "./streamingDecoder
import type {
ExportConfig,
ExportEncodeBackend,
ExportFinalizationStageMetrics,
ExportMetrics,
ExportProgress,
ExportRenderBackend,
@@ -155,6 +156,8 @@ export class ModernVideoExporter {
private nativeCaptureTimeMs = 0;
private nativeWriteTimeMs = 0;
private finalizationTimeMs = 0;
private finalizationStageMs: ExportFinalizationStageMetrics = {};
private effectiveDurationSec = 0;
private processedFrameCount = 0;
private activeFinalizationProgressWatchdog: FinalizationProgressWatchdog | null = null;
private lastFinalizationRenderProgress = INITIAL_FINALIZATION_PROGRESS_STATE.lastRenderProgress;
@@ -172,6 +175,7 @@ export class ModernVideoExporter {
this.cancelled = false;
this.encoderError = null;
this.nativeEncoderError = null;
this.totalExportStartTimeMs = this.getNowMs();
const backendPreference = this.config.backendPreference ?? "auto";
let useNativeEncoder = false;
this.lastNativeExportError = null;
@@ -414,7 +418,9 @@ export class ModernVideoExporter {
stageStartedAt = this.getNowMs();
this.reportFinalizingProgress(totalFrames, 99);
if (this.nativeH264Encoder) {
await this.nativeH264Encoder.flush();
await this.measureFinalizationStage("nativeEncoderFlushMs", async () => {
await this.nativeH264Encoder!.flush();
});
}
const finishResult = await this.finishNativeVideoExport(nativeAudioPlan);
this.finalizationTimeMs = this.getNowMs() - stageStartedAt;
@@ -422,28 +428,32 @@ export class ModernVideoExporter {
return {
success: false,
error: finishResult.error || `${NATIVE_EXPORT_ENGINE_NAME} export failed`,
metrics: finishResult.metrics ?? this.buildExportMetrics(),
metrics: this.buildExportMetrics(),
};
}
return {
success: true,
blob: finishResult.blob,
metrics: finishResult.metrics ?? this.buildExportMetrics(),
metrics: this.buildExportMetrics(),
};
}
stageStartedAt = this.getNowMs();
if (this.encoder && this.encoder.state === "configured") {
this.reportFinalizingProgress(totalFrames, 97);
await this.awaitWithFinalizationTimeout(this.encoder.flush(), "encoder flush");
await this.measureFinalizationStage("encoderFlushMs", async () => {
await this.awaitWithFinalizationTimeout(this.encoder!.flush(), "encoder flush");
});
}
this.reportFinalizingProgress(totalFrames, 98);
await this.awaitWithFinalizationTimeout(
this.pendingMuxing,
"muxing queued video chunks",
);
await this.measureFinalizationStage("queuedMuxingMs", async () => {
await this.awaitWithFinalizationTimeout(
this.pendingMuxing,
"muxing queued video chunks",
);
});
// Surface muxing errors before proceeding with finalization
if (this.encoderError) {
@@ -466,54 +476,59 @@ export class ModernVideoExporter {
this.reportFinalizingProgress(totalFrames, 99, progress);
});
this.reportFinalizingProgress(totalFrames, 99);
await this.awaitWithFinalizationTimeout(
this.audioProcessor.process(
demuxer,
this.muxer!,
this.config.videoUrl,
this.config.trimRegions,
this.config.speedRegions,
undefined,
this.config.audioRegions,
this.config.sourceAudioFallbackPaths,
),
"audio processing",
"audio",
true,
);
await this.measureFinalizationStage("audioProcessingMs", async () => {
await this.awaitWithFinalizationTimeout(
this.audioProcessor!.process(
demuxer,
this.muxer!,
this.config.videoUrl,
this.config.trimRegions,
this.config.speedRegions,
undefined,
this.config.audioRegions,
this.config.sourceAudioFallbackPaths,
),
"audio processing",
"audio",
true,
);
});
}
}
this.reportFinalizingProgress(totalFrames, 99);
const blob = await this.awaitWithFinalizationTimeout(
this.muxer!.finalize(),
"muxer finalization",
nativeAudioPlan.audioMode !== "none" && !shouldUseFfmpegAudioFallback
? "audio"
: "default",
const blob = await this.measureFinalizationStage("muxerFinalizeMs", async () =>
this.awaitWithFinalizationTimeout(
this.muxer!.finalize(),
"muxer finalization",
nativeAudioPlan.audioMode !== "none" && !shouldUseFfmpegAudioFallback
? "audio"
: "default",
),
);
this.finalizationTimeMs = this.getNowMs() - stageStartedAt;
if (shouldUseFfmpegAudioFallback) {
console.warn(
`[VideoExporter] Browser AAC encoding is unavailable; falling back to FFmpeg audio muxing.`,
);
const muxedResult = await this.finalizeExportWithFfmpegAudio(blob, nativeAudioPlan);
this.finalizationTimeMs = this.getNowMs() - stageStartedAt;
if (!muxedResult.success || !muxedResult.blob) {
return {
success: false,
error: muxedResult.error || "Failed to mux audio with FFmpeg",
metrics: muxedResult.metrics ?? this.buildExportMetrics(),
metrics: this.buildExportMetrics(),
};
}
return {
success: true,
blob: muxedResult.blob,
metrics: muxedResult.metrics ?? this.buildExportMetrics(),
metrics: this.buildExportMetrics(),
};
}
this.finalizationTimeMs = this.getNowMs() - stageStartedAt;
return { success: true, blob, metrics: this.buildExportMetrics() };
} catch (error) {
if (this.cancelled && !this.encoderError) {
@@ -968,17 +983,19 @@ export class ModernVideoExporter {
this.audioProcessor.setOnProgress((progress) => {
this.reportFinalizingProgress(this.processedFrameCount, 99, progress);
});
const audioBlob = await this.awaitWithFinalizationTimeout(
this.audioProcessor.renderEditedAudioTrack(
this.config.videoUrl,
this.config.trimRegions,
this.config.speedRegions,
this.config.audioRegions,
this.config.sourceAudioFallbackPaths,
const audioBlob = await this.measureFinalizationStage("editedAudioRenderMs", async () =>
this.awaitWithFinalizationTimeout(
this.audioProcessor!.renderEditedAudioTrack(
this.config.videoUrl,
this.config.trimRegions,
this.config.speedRegions,
this.config.audioRegions,
this.config.sourceAudioFallbackPaths,
),
`${NATIVE_EXPORT_ENGINE_NAME} edited audio rendering`,
"audio",
true,
),
`${NATIVE_EXPORT_ENGINE_NAME} edited audio rendering`,
"audio",
true,
);
editedAudioBuffer = await audioBlob.arrayBuffer();
editedAudioMimeType = audioBlob.type || null;
@@ -993,20 +1010,23 @@ export class ModernVideoExporter {
await this.awaitPendingNativeWrites();
const result = await this.awaitWithFinalizationTimeout(
window.electronAPI.nativeVideoExportFinish(sessionId, {
audioMode: audioPlan.audioMode,
audioSourcePath:
audioPlan.audioMode === "copy-source" || audioPlan.audioMode === "trim-source"
? audioPlan.audioSourcePath
: null,
trimSegments:
audioPlan.audioMode === "trim-source" ? audioPlan.trimSegments : undefined,
editedAudioData: editedAudioBuffer,
editedAudioMimeType,
}),
`${NATIVE_EXPORT_ENGINE_NAME} export finalization`,
audioPlan.audioMode === "none" ? "default" : "audio",
const result = await this.measureFinalizationStage("nativeExportFinalizeMs", async () =>
this.awaitWithFinalizationTimeout(
window.electronAPI.nativeVideoExportFinish(sessionId, {
audioMode: audioPlan.audioMode,
audioSourcePath:
audioPlan.audioMode === "copy-source" ||
audioPlan.audioMode === "trim-source"
? audioPlan.audioSourcePath
: null,
trimSegments:
audioPlan.audioMode === "trim-source" ? audioPlan.trimSegments : undefined,
editedAudioData: editedAudioBuffer,
editedAudioMimeType,
}),
`${NATIVE_EXPORT_ENGINE_NAME} export finalization`,
audioPlan.audioMode === "none" ? "default" : "audio",
),
);
this.nativeExportSessionId = null;
@@ -1052,37 +1072,42 @@ export class ModernVideoExporter {
this.audioProcessor.setOnProgress((progress) => {
this.reportFinalizingProgress(this.processedFrameCount, 99, progress);
});
const audioBlob = await this.awaitWithFinalizationTimeout(
this.audioProcessor.renderEditedAudioTrack(
this.config.videoUrl,
this.config.trimRegions,
this.config.speedRegions,
this.config.audioRegions,
this.config.sourceAudioFallbackPaths,
const audioBlob = await this.measureFinalizationStage("editedAudioRenderMs", async () =>
this.awaitWithFinalizationTimeout(
this.audioProcessor!.renderEditedAudioTrack(
this.config.videoUrl,
this.config.trimRegions,
this.config.speedRegions,
this.config.audioRegions,
this.config.sourceAudioFallbackPaths,
),
"FFmpeg edited audio rendering",
"audio",
true,
),
"FFmpeg edited audio rendering",
"audio",
true,
);
editedAudioBuffer = await audioBlob.arrayBuffer();
editedAudioMimeType = audioBlob.type || null;
}
const videoBuffer = await videoBlob.arrayBuffer();
const result = await this.awaitWithFinalizationTimeout(
window.electronAPI.muxExportedVideoAudio(videoBuffer, {
audioMode: audioPlan.audioMode,
audioSourcePath:
audioPlan.audioMode === "copy-source" || audioPlan.audioMode === "trim-source"
? audioPlan.audioSourcePath
: null,
trimSegments:
audioPlan.audioMode === "trim-source" ? audioPlan.trimSegments : undefined,
editedAudioData: editedAudioBuffer,
editedAudioMimeType,
}),
"FFmpeg audio muxing",
"audio",
const result = await this.measureFinalizationStage("ffmpegAudioMuxMs", async () =>
this.awaitWithFinalizationTimeout(
window.electronAPI.muxExportedVideoAudio(videoBuffer, {
audioMode: audioPlan.audioMode,
audioSourcePath:
audioPlan.audioMode === "copy-source" ||
audioPlan.audioMode === "trim-source"
? audioPlan.audioSourcePath
: null,
trimSegments:
audioPlan.audioMode === "trim-source" ? audioPlan.trimSegments : undefined,
editedAudioData: editedAudioBuffer,
editedAudioMimeType,
}),
"FFmpeg audio muxing",
"audio",
),
);
if (!result.success || !result.data) {
@@ -1269,6 +1294,7 @@ export class ModernVideoExporter {
const totalElapsedMs =
this.totalExportStartTimeMs > 0 ? this.getNowMs() - this.totalExportStartTimeMs : 0;
const safeFrameCount = Math.max(this.processedFrameCount, 1);
const hasFinalizationStageMetrics = Object.keys(this.finalizationStageMs).length > 0;
return {
totalElapsedMs,
@@ -1290,6 +1316,8 @@ export class ModernVideoExporter {
encodeBackend: this.encodeBackend ?? undefined,
encoderName: this.encoderName ?? undefined,
backpressureProfile: this.backpressureProfile?.name,
effectiveDurationSec: this.effectiveDurationSec || undefined,
finalizationStageMs: hasFinalizationStageMetrics ? this.finalizationStageMs : undefined,
averageFrameCallbackMs:
this.processedFrameCount > 0
? this.frameCallbackTimeMs / safeFrameCount
@@ -1368,6 +1396,18 @@ export class ModernVideoExporter {
return Date.now();
}
private async measureFinalizationStage<T>(
stage: keyof ExportFinalizationStageMetrics,
task: () => Promise<T>,
): Promise<T> {
const startedAt = this.getNowMs();
try {
return await task();
} finally {
this.finalizationStageMs[stage] = this.getNowMs() - startedAt;
}
}
private async initializeEncoder(): Promise<SupportedMp4EncoderPath> {
this.encodeQueue = 0;
this.webCodecsEncodeQueueLimit =
@@ -1614,6 +1654,8 @@ export class ModernVideoExporter {
this.nativeCaptureTimeMs = 0;
this.nativeWriteTimeMs = 0;
this.finalizationTimeMs = 0;
this.finalizationStageMs = {};
this.effectiveDurationSec = 0;
this.processedFrameCount = 0;
this.activeFinalizationProgressWatchdog = null;
this.lastFinalizationRenderProgress =
+13
View File
@@ -32,6 +32,17 @@ export interface ExportProgress {
audioProgress?: number; // 0-1, progress of real-time audio rendering (speed/audio regions)
}
export interface ExportFinalizationStageMetrics {
encoderFlushMs?: number;
queuedMuxingMs?: number;
audioProcessingMs?: number;
muxerFinalizeMs?: number;
editedAudioRenderMs?: number;
ffmpegAudioMuxMs?: number;
nativeExportFinalizeMs?: number;
nativeEncoderFlushMs?: number;
}
export interface ExportMetrics {
totalElapsedMs: number;
metadataLoadMs?: number;
@@ -57,6 +68,8 @@ export interface ExportMetrics {
averageEncodeWaitMs?: number;
averageNativeCaptureMs?: number;
averageNativeWriteMs?: number;
effectiveDurationSec?: number;
finalizationStageMs?: ExportFinalizationStageMetrics;
}
export interface ExportResult {
+179 -85
View File
@@ -24,7 +24,13 @@ import { FrameRenderer } from "./frameRenderer";
import type { SupportedMp4EncoderPath } from "./mp4Support";
import { VideoMuxer } from "./muxer";
import { type DecodedVideoInfo, StreamingVideoDecoder } from "./streamingDecoder";
import type { ExportConfig, ExportProgress, ExportResult } from "./types";
import type {
ExportConfig,
ExportFinalizationStageMetrics,
ExportMetrics,
ExportProgress,
ExportResult,
} from "./types";
const DEFAULT_MAX_ENCODE_QUEUE = 240;
const PROGRESS_SAMPLE_WINDOW_MS = 1_000;
@@ -117,6 +123,9 @@ export class VideoExporter {
private activeFinalizationProgressWatchdog: FinalizationProgressWatchdog | null = null;
private lastFinalizationRenderProgress = INITIAL_FINALIZATION_PROGRESS_STATE.lastRenderProgress;
private lastFinalizationAudioProgress = INITIAL_FINALIZATION_PROGRESS_STATE.lastAudioProgress;
private finalizationTimeMs = 0;
private finalizationStageMs: ExportFinalizationStageMetrics = {};
private processedFrameCount = 0;
constructor(config: VideoExporterConfig) {
this.config = config;
@@ -262,6 +271,7 @@ export class VideoExporter {
await this.encodeRenderedFrame(timestamp, frameDuration, frameIndex);
}
frameIndex++;
this.processedFrameCount = frameIndex;
this.reportProgress(frameIndex, totalFrames);
},
);
@@ -269,40 +279,60 @@ export class VideoExporter {
if (this.cancelled) {
const encoderError = this.encoderError as Error | null;
if (encoderError) {
return { success: false, error: encoderError.message };
return {
success: false,
error: encoderError.message,
metrics: this.buildExportMetrics(),
};
}
return { success: false, error: "Export cancelled" };
return {
success: false,
error: "Export cancelled",
metrics: this.buildExportMetrics(),
};
}
this.reportFinalizingProgress(totalFrames, 96);
const finalizationStartedAt = this.getNowMs();
if (useNativeEncoder && nativeAudioPlan) {
if (this.nativeH264Encoder) {
await this.nativeH264Encoder.flush();
await this.awaitPendingNativeWrites();
if (this.nativeEncoderError) {
throw this.nativeEncoderError;
}
await this.measureFinalizationStage("nativeEncoderFlushMs", async () => {
await this.nativeH264Encoder!.flush();
await this.awaitPendingNativeWrites();
if (this.nativeEncoderError) {
throw this.nativeEncoderError;
}
});
this.nativeH264Encoder.close();
this.nativeH264Encoder = null;
}
this.reportFinalizingProgress(totalFrames, 99, 0);
return await this.finishNativeVideoExport(nativeAudioPlan, totalFrames);
const result = await this.finishNativeVideoExport(nativeAudioPlan, totalFrames);
this.finalizationTimeMs = this.getNowMs() - finalizationStartedAt;
return {
...result,
metrics: this.buildExportMetrics(),
};
}
// Finalize encoding
if (this.encoder && this.encoder.state === "configured") {
this.reportFinalizingProgress(totalFrames, 97);
await this.awaitWithFinalizationTimeout(this.encoder.flush(), "encoder flush");
await this.measureFinalizationStage("encoderFlushMs", async () => {
await this.awaitWithFinalizationTimeout(this.encoder!.flush(), "encoder flush");
});
}
// Wait for queued muxing operations to complete
this.reportFinalizingProgress(totalFrames, 98);
await this.awaitWithFinalizationTimeout(
this.pendingMuxing,
"muxing queued video chunks",
);
await this.measureFinalizationStage("queuedMuxingMs", async () => {
await this.awaitWithFinalizationTimeout(
this.pendingMuxing,
"muxing queued video chunks",
);
});
// Surface muxing errors before proceeding with finalization
if (this.encoderError) {
@@ -317,43 +347,61 @@ export class VideoExporter {
this.reportFinalizingProgress(totalFrames, 99, progress);
});
this.reportFinalizingProgress(totalFrames, 99, 0);
await this.awaitWithFinalizationTimeout(
this.audioProcessor.process(
demuxer,
this.muxer!,
this.config.videoUrl,
this.config.trimRegions,
this.config.speedRegions,
undefined,
this.config.audioRegions,
this.config.sourceAudioFallbackPaths,
),
"audio processing",
"audio",
true,
);
await this.measureFinalizationStage("audioProcessingMs", async () => {
await this.awaitWithFinalizationTimeout(
this.audioProcessor!.process(
demuxer,
this.muxer!,
this.config.videoUrl,
this.config.trimRegions,
this.config.speedRegions,
undefined,
this.config.audioRegions,
this.config.sourceAudioFallbackPaths,
),
"audio processing",
"audio",
true,
);
});
}
}
// Finalize muxer and get output blob
this.reportFinalizingProgress(totalFrames, 99);
const blob = await this.awaitWithFinalizationTimeout(
this.muxer!.finalize(),
"muxer finalization",
hasAudio && !shouldUseFfmpegAudioFallback ? "audio" : "default",
const blob = await this.measureFinalizationStage("muxerFinalizeMs", async () =>
this.awaitWithFinalizationTimeout(
this.muxer!.finalize(),
"muxer finalization",
hasAudio && !shouldUseFfmpegAudioFallback ? "audio" : "default",
),
);
if (shouldUseFfmpegAudioFallback) {
console.warn(
"[VideoExporter] Browser AAC encoding is unavailable; falling back to FFmpeg audio muxing.",
);
return await this.finalizeExportWithFfmpegAudio(blob, audioPlan, totalFrames);
const result = await this.finalizeExportWithFfmpegAudio(
blob,
audioPlan,
totalFrames,
);
this.finalizationTimeMs = this.getNowMs() - finalizationStartedAt;
return {
...result,
metrics: this.buildExportMetrics(),
};
}
return { success: true, blob };
this.finalizationTimeMs = this.getNowMs() - finalizationStartedAt;
return { success: true, blob, metrics: this.buildExportMetrics() };
} catch (error) {
if (this.cancelled && !this.encoderError) {
return { success: false, error: "Export cancelled" };
return {
success: false,
error: "Export cancelled",
metrics: this.buildExportMetrics(),
};
}
const resolvedError = this.encoderError ?? error;
@@ -362,6 +410,7 @@ export class VideoExporter {
success: false,
error:
resolvedError instanceof Error ? resolvedError.message : String(resolvedError),
metrics: this.buildExportMetrics(),
};
} finally {
this.cleanup();
@@ -693,44 +742,51 @@ export class VideoExporter {
this.audioProcessor.setOnProgress((progress) => {
this.reportFinalizingProgress(totalFrames, 99, progress);
});
const audioBlob = await this.awaitWithFinalizationTimeout(
this.audioProcessor.renderEditedAudioTrack(
this.config.videoUrl,
this.config.trimRegions,
this.config.speedRegions,
this.config.audioRegions,
this.config.sourceAudioFallbackPaths,
const audioBlob = await this.measureFinalizationStage("editedAudioRenderMs", async () =>
this.awaitWithFinalizationTimeout(
this.audioProcessor!.renderEditedAudioTrack(
this.config.videoUrl,
this.config.trimRegions,
this.config.speedRegions,
this.config.audioRegions,
this.config.sourceAudioFallbackPaths,
),
"native edited audio rendering",
"audio",
true,
),
"native edited audio rendering",
"audio",
true,
);
editedAudioBuffer = await audioBlob.arrayBuffer();
editedAudioMimeType = audioBlob.type || null;
}
const sessionId = this.nativeExportSessionId;
const result = await this.awaitWithFinalizationTimeout(
window.electronAPI.nativeVideoExportFinish(sessionId, {
audioMode: audioPlan.audioMode,
audioSourcePath:
audioPlan.audioMode === "copy-source" || audioPlan.audioMode === "trim-source"
? audioPlan.audioSourcePath
: null,
trimSegments:
audioPlan.audioMode === "trim-source" ? audioPlan.trimSegments : undefined,
editedAudioData: editedAudioBuffer,
editedAudioMimeType,
}),
"native export finalization",
audioPlan.audioMode === "none" ? "default" : "audio",
);
this.nativeExportSessionId = null;
const result = await this.measureFinalizationStage("nativeExportFinalizeMs", async () =>
this.awaitWithFinalizationTimeout(
window.electronAPI.nativeVideoExportFinish(sessionId, {
audioMode: audioPlan.audioMode,
audioSourcePath:
audioPlan.audioMode === "copy-source" ||
audioPlan.audioMode === "trim-source"
? audioPlan.audioSourcePath
: null,
trimSegments:
audioPlan.audioMode === "trim-source" ? audioPlan.trimSegments : undefined,
editedAudioData: editedAudioBuffer,
editedAudioMimeType,
}),
"native export finalization",
audioPlan.audioMode === "none" ? "default" : "audio",
),
);
if (!result.success || !result.data) {
return {
success: false,
error: result.error || "Failed to finalize native video export",
metrics: this.buildExportMetrics(),
};
}
@@ -740,6 +796,7 @@ export class VideoExporter {
return {
success: true,
blob: new Blob([blobData.buffer], { type: "video/mp4" }),
metrics: this.buildExportMetrics(),
};
}
@@ -763,43 +820,49 @@ export class VideoExporter {
this.audioProcessor.setOnProgress((progress) => {
this.reportFinalizingProgress(totalFrames, 99, progress);
});
const audioBlob = await this.awaitWithFinalizationTimeout(
this.audioProcessor.renderEditedAudioTrack(
this.config.videoUrl,
this.config.trimRegions,
this.config.speedRegions,
this.config.audioRegions,
this.config.sourceAudioFallbackPaths,
const audioBlob = await this.measureFinalizationStage("editedAudioRenderMs", async () =>
this.awaitWithFinalizationTimeout(
this.audioProcessor!.renderEditedAudioTrack(
this.config.videoUrl,
this.config.trimRegions,
this.config.speedRegions,
this.config.audioRegions,
this.config.sourceAudioFallbackPaths,
),
"ffmpeg edited audio rendering",
"audio",
true,
),
"ffmpeg edited audio rendering",
"audio",
true,
);
editedAudioBuffer = await audioBlob.arrayBuffer();
editedAudioMimeType = audioBlob.type || null;
}
const videoBuffer = await videoBlob.arrayBuffer();
const result = await this.awaitWithFinalizationTimeout(
window.electronAPI.muxExportedVideoAudio(videoBuffer, {
audioMode: audioPlan.audioMode,
audioSourcePath:
audioPlan.audioMode === "copy-source" || audioPlan.audioMode === "trim-source"
? audioPlan.audioSourcePath
: null,
trimSegments:
audioPlan.audioMode === "trim-source" ? audioPlan.trimSegments : undefined,
editedAudioData: editedAudioBuffer,
editedAudioMimeType,
}),
"ffmpeg audio muxing",
"audio",
const result = await this.measureFinalizationStage("ffmpegAudioMuxMs", async () =>
this.awaitWithFinalizationTimeout(
window.electronAPI.muxExportedVideoAudio(videoBuffer, {
audioMode: audioPlan.audioMode,
audioSourcePath:
audioPlan.audioMode === "copy-source" ||
audioPlan.audioMode === "trim-source"
? audioPlan.audioSourcePath
: null,
trimSegments:
audioPlan.audioMode === "trim-source" ? audioPlan.trimSegments : undefined,
editedAudioData: editedAudioBuffer,
editedAudioMimeType,
}),
"ffmpeg audio muxing",
"audio",
),
);
if (!result.success || !result.data) {
return {
success: false,
error: result.error || "Failed to mux exported audio with FFmpeg",
metrics: this.buildExportMetrics(),
};
}
@@ -808,6 +871,7 @@ export class VideoExporter {
return {
success: true,
blob: new Blob([blobData.buffer], { type: "video/mp4" }),
metrics: this.buildExportMetrics(),
};
}
@@ -952,6 +1016,32 @@ export class VideoExporter {
return typeof performance !== "undefined" ? performance.now() : Date.now();
}
private async measureFinalizationStage<T>(
stage: keyof ExportFinalizationStageMetrics,
task: () => Promise<T>,
): Promise<T> {
const startedAt = this.getNowMs();
try {
return await task();
} finally {
this.finalizationStageMs[stage] = this.getNowMs() - startedAt;
}
}
private buildExportMetrics(): ExportMetrics {
const totalElapsedMs =
this.exportStartTimeMs > 0 ? this.getNowMs() - this.exportStartTimeMs : 0;
const hasFinalizationStageMetrics = Object.keys(this.finalizationStageMs).length > 0;
return {
totalElapsedMs,
finalizationMs: this.finalizationTimeMs || undefined,
frameCount: this.processedFrameCount || undefined,
effectiveDurationSec: this.effectiveDurationSec || undefined,
finalizationStageMs: hasFinalizationStageMetrics ? this.finalizationStageMs : undefined,
};
}
private async initializeEncoder(): Promise<void> {
this.encodeQueue = 0;
this.pendingMuxing = Promise.resolve();
@@ -1163,6 +1253,10 @@ export class VideoExporter {
this.chunkCount = 0;
this.effectiveDurationSec = 0;
this.encoderError = null;
this.finalizationTimeMs = 0;
this.finalizationStageMs = {};
this.effectiveDurationSec = 0;
this.processedFrameCount = 0;
this.videoDescription = undefined;
this.videoColorSpace = undefined;
}