fix(export): clean telemetry profiling rebase

This commit is contained in:
wiiiii123
2026-04-20 20:11:58 +07:00
parent b7e5a4ab79
commit ccc0e63d1b
4 changed files with 391 additions and 358 deletions
+3 -3
View File
@@ -341,9 +341,9 @@ interface Window {
getCurrentVideoPath: () => Promise<{ success: boolean; path?: string }>;
clearCurrentVideoPath: () => Promise<{ success: boolean }>;
deleteRecordingFile: (filePath: string) => Promise<{ success: boolean; error?: string }>;
getLocalMediaUrl: (filePath: string) => Promise<
{ success: true; url: string } | { success: false }
>;
getLocalMediaUrl: (
filePath: string,
) => Promise<{ success: true; url: string } | { success: false }>;
saveProjectFile: (
projectData: unknown,
suggestedName?: string,
+21 -16
View File
@@ -169,23 +169,28 @@ export function buildTrimmedSourceAudioFilter(
* — no re-encoding step, no raw pixel IPC traffic.
*/
export function buildNativeH264StreamExportArgs(config: {
frameRate: number
outputPath: string
frameRate: number;
outputPath: string;
}): string[] {
return [
'-y',
'-hide_banner',
'-loglevel',
'error',
// Input 0: pre-encoded H.264 Annex B stream from browser VideoEncoder via stdin
'-f', 'h264',
'-r', String(config.frameRate),
'-i', 'pipe:0',
'-an', // audio handled separately by muxNativeVideoExportAudio
'-c:v', 'copy',
'-movflags', '+faststart',
config.outputPath,
]
return [
"-y",
"-hide_banner",
"-loglevel",
"error",
// Input 0: pre-encoded H.264 Annex B stream from browser VideoEncoder via stdin
"-f",
"h264",
"-r",
String(config.frameRate),
"-i",
"pipe:0",
"-an", // audio handled separately by muxNativeVideoExportAudio
"-c:v",
"copy",
"-movflags",
"+faststart",
config.outputPath,
];
}
export function getEditedAudioExtension(mimeType?: string | null): string {
+367 -337
View File
@@ -6,6 +6,22 @@ import { performance } from "node:perf_hooks";
import type { Readable, Writable } from "node:stream";
import type { SaveDialogOptions } from "electron";
import { app, BrowserWindow, dialog, ipcMain } from "electron";
import {
enqueueNativeVideoExportFrameWrite,
flushNativeVideoExportPendingWriteRequests,
getNativeVideoExportMaxQueuedWriteBytes,
getNativeVideoExportSessionError,
isHardwareAcceleratedVideoEncoder,
isIgnorableNativeVideoExportStreamError,
muxExportedVideoAudioBuffer,
muxNativeVideoExportAudio,
type NativeVideoExportSession,
nativeVideoExportSessions,
removeTemporaryExportFile,
resolveNativeVideoEncoder,
sendNativeVideoExportWriteFrameResult,
settleNativeVideoExportWriteFrameRequest,
} from "../export/native-video";
import { getFfmpegBinaryPath } from "../ffmpeg/binary";
import {
buildNativeH264StreamExportArgs,
@@ -14,381 +30,395 @@ import {
type NativeExportEncodingMode,
type NativeVideoExportFinishOptions,
} from "../nativeVideoExport";
import {
nativeVideoExportSessions,
getNativeVideoExportMaxQueuedWriteBytes,
isHardwareAcceleratedVideoEncoder,
removeTemporaryExportFile,
getNativeVideoExportSessionError,
sendNativeVideoExportWriteFrameResult,
settleNativeVideoExportWriteFrameRequest,
flushNativeVideoExportPendingWriteRequests,
isIgnorableNativeVideoExportStreamError,
enqueueNativeVideoExportFrameWrite,
resolveNativeVideoEncoder,
muxNativeVideoExportAudio,
muxExportedVideoAudioBuffer,
type NativeVideoExportSession,
} from "../export/native-video";
import { approveUserPath } from "../utils";
export function registerExportHandlers() {
ipcMain.handle(
'native-video-export-start',
async (
event,
options: {
width: number
height: number
frameRate: number
bitrate: number
encodingMode: NativeExportEncodingMode
inputMode?: 'rawvideo' | 'h264-stream'
},
) => {
try {
if (options.width % 2 !== 0 || options.height % 2 !== 0) {
throw new Error('Native export requires even output dimensions')
}
ipcMain.handle(
"native-video-export-start",
async (
event,
options: {
width: number;
height: number;
frameRate: number;
bitrate: number;
encodingMode: NativeExportEncodingMode;
inputMode?: "rawvideo" | "h264-stream";
},
) => {
try {
if (options.width % 2 !== 0 || options.height % 2 !== 0) {
throw new Error("Native export requires even output dimensions");
}
const ffmpegPath = getFfmpegBinaryPath()
const inputMode = options.inputMode ?? 'rawvideo'
const sessionId = `recordly-export-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
const outputPath = path.join(app.getPath('temp'), `${sessionId}.mp4`)
const ffmpegPath = getFfmpegBinaryPath();
const inputMode = options.inputMode ?? "rawvideo";
const sessionId = `recordly-export-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const outputPath = path.join(app.getPath("temp"), `${sessionId}.mp4`);
let encoderName: string
let ffmpegArgs: string[]
let encoderName: string;
let ffmpegArgs: string[];
if (inputMode === 'h264-stream') {
// Pre-encoded H.264 Annex B from browser VideoEncoder — just stream-copy into MP4
encoderName = 'h264-stream-copy'
ffmpegArgs = buildNativeH264StreamExportArgs({ frameRate: options.frameRate, outputPath })
} else {
encoderName = await resolveNativeVideoEncoder(ffmpegPath, options.encodingMode)
ffmpegArgs = buildNativeVideoExportArgs(encoderName, options, outputPath)
}
if (inputMode === "h264-stream") {
// Pre-encoded H.264 Annex B from browser VideoEncoder — just stream-copy into MP4
encoderName = "h264-stream-copy";
ffmpegArgs = buildNativeH264StreamExportArgs({
frameRate: options.frameRate,
outputPath,
});
} else {
encoderName = await resolveNativeVideoEncoder(ffmpegPath, options.encodingMode);
ffmpegArgs = buildNativeVideoExportArgs(encoderName, options, outputPath);
}
const ffmpegProcess = spawn(ffmpegPath, ffmpegArgs, {
stdio: ['pipe', 'ignore', 'pipe'],
}) as ChildProcessByStdio<Writable, null, Readable>
// For rawvideo, frames are a fixed RGBA size. For h264-stream, chunks are variable.
const inputByteSize = inputMode === 'rawvideo' ? getNativeVideoInputByteSize(options.width, options.height) : 0
const ffmpegProcess = spawn(ffmpegPath, ffmpegArgs, {
stdio: ["pipe", "ignore", "pipe"],
}) as ChildProcessByStdio<Writable, null, Readable>;
// For rawvideo, frames are a fixed RGBA size. For h264-stream, chunks are variable.
const inputByteSize =
inputMode === "rawvideo"
? getNativeVideoInputByteSize(options.width, options.height)
: 0;
const session: NativeVideoExportSession = {
ffmpegProcess,
outputPath,
inputByteSize,
inputMode,
maxQueuedWriteBytes: inputMode === 'h264-stream' ? 8 * 1024 * 1024 : getNativeVideoExportMaxQueuedWriteBytes(inputByteSize),
stderrOutput: '',
encoderName,
processError: null,
stdinError: null,
terminating: false,
writeSequence: Promise.resolve(),
sender: event.sender,
pendingWriteRequestIds: new Set<number>(),
completionPromise: new Promise<void>((resolve, reject) => {
ffmpegProcess.once('error', (error) => {
const processError = error instanceof Error ? error : new Error(String(error))
if (session.terminating) {
resolve()
return
}
const session: NativeVideoExportSession = {
ffmpegProcess,
outputPath,
inputByteSize,
inputMode,
maxQueuedWriteBytes:
inputMode === "h264-stream"
? 8 * 1024 * 1024
: getNativeVideoExportMaxQueuedWriteBytes(inputByteSize),
stderrOutput: "",
encoderName,
processError: null,
stdinError: null,
terminating: false,
writeSequence: Promise.resolve(),
sender: event.sender,
pendingWriteRequestIds: new Set<number>(),
completionPromise: new Promise<void>((resolve, reject) => {
ffmpegProcess.once("error", (error) => {
const processError =
error instanceof Error ? error : new Error(String(error));
if (session.terminating) {
resolve();
return;
}
session.processError = processError
reject(processError)
})
ffmpegProcess.stdin.once('error', (error) => {
const stdinError = error instanceof Error ? error : new Error(String(error))
if (session.terminating && isIgnorableNativeVideoExportStreamError(stdinError)) {
return
}
session.processError = processError;
reject(processError);
});
ffmpegProcess.stdin.once("error", (error) => {
const stdinError =
error instanceof Error ? error : new Error(String(error));
if (
session.terminating &&
isIgnorableNativeVideoExportStreamError(stdinError)
) {
return;
}
session.stdinError = stdinError
})
ffmpegProcess.once('close', (code, signal) => {
if (session.terminating) {
resolve()
return
}
session.stdinError = stdinError;
});
ffmpegProcess.once("close", (code, signal) => {
if (session.terminating) {
resolve();
return;
}
if (code === 0) {
resolve()
return
}
if (code === 0) {
resolve();
return;
}
reject(
new Error(
getNativeVideoExportSessionError(
session,
`FFmpeg exited with code ${code ?? 'unknown'}${signal ? ` (signal ${signal})` : ''}`,
),
),
)
})
}),
}
void session.completionPromise.catch(() => undefined)
reject(
new Error(
getNativeVideoExportSessionError(
session,
`FFmpeg exited with code ${code ?? "unknown"}${signal ? ` (signal ${signal})` : ""}`,
),
),
);
});
}),
};
void session.completionPromise.catch(() => undefined);
ffmpegProcess.stderr.on('data', (chunk: Buffer) => {
session.stderrOutput += chunk.toString()
})
ffmpegProcess.stderr.on("data", (chunk: Buffer) => {
session.stderrOutput += chunk.toString();
});
nativeVideoExportSessions.set(sessionId, session)
nativeVideoExportSessions.set(sessionId, session);
console.log(
`[native-export] Started ${isHardwareAcceleratedVideoEncoder(encoderName) ? 'hardware' : 'software'} session ${sessionId} with ${encoderName}`,
)
console.log(
`[native-export] Started ${isHardwareAcceleratedVideoEncoder(encoderName) ? "hardware" : "software"} session ${sessionId} with ${encoderName}`,
);
return {
success: true,
sessionId,
encoderName,
}
} catch (error) {
console.error('[native-export] Failed to start native video export session:', error)
return {
success: false,
error: String(error),
}
}
},
)
return {
success: true,
sessionId,
encoderName,
};
} catch (error) {
console.error(
"[native-export] Failed to start native video export session:",
error,
);
return {
success: false,
error: String(error),
};
}
},
);
ipcMain.on(
'native-video-export-write-frame-async',
(
event,
payload: {
sessionId: string
requestId: number
frameData: Uint8Array
},
) => {
const sessionId = payload?.sessionId
const requestId = payload?.requestId
const frameData = payload?.frameData
ipcMain.on(
"native-video-export-write-frame-async",
(
event,
payload: {
sessionId: string;
requestId: number;
frameData: Uint8Array;
},
) => {
const sessionId = payload?.sessionId;
const requestId = payload?.requestId;
const frameData = payload?.frameData;
if (typeof sessionId !== 'string' || typeof requestId !== 'number' || !frameData) {
return
}
if (typeof sessionId !== "string" || typeof requestId !== "number" || !frameData) {
return;
}
const session = nativeVideoExportSessions.get(sessionId)
if (!session) {
sendNativeVideoExportWriteFrameResult(event.sender, sessionId, requestId, {
success: false,
error: 'Invalid native export session',
})
return
}
const session = nativeVideoExportSessions.get(sessionId);
if (!session) {
sendNativeVideoExportWriteFrameResult(event.sender, sessionId, requestId, {
success: false,
error: "Invalid native export session",
});
return;
}
session.sender = event.sender
session.pendingWriteRequestIds.add(requestId)
session.sender = event.sender;
session.pendingWriteRequestIds.add(requestId);
if (session.terminating) {
settleNativeVideoExportWriteFrameRequest(sessionId, session, requestId, {
success: false,
error: 'Native video export session was cancelled',
})
return
}
if (session.terminating) {
settleNativeVideoExportWriteFrameRequest(sessionId, session, requestId, {
success: false,
error: "Native video export session was cancelled",
});
return;
}
if (session.inputMode !== 'h264-stream' && frameData.byteLength !== session.inputByteSize) {
settleNativeVideoExportWriteFrameRequest(sessionId, session, requestId, {
success: false,
error: `Native video export expected ${session.inputByteSize} bytes per frame but received ${frameData.byteLength}`,
})
return
}
if (
session.inputMode !== "h264-stream" &&
frameData.byteLength !== session.inputByteSize
) {
settleNativeVideoExportWriteFrameRequest(sessionId, session, requestId, {
success: false,
error: `Native video export expected ${session.inputByteSize} bytes per frame but received ${frameData.byteLength}`,
});
return;
}
void enqueueNativeVideoExportFrameWrite(session, frameData)
.then(() => {
settleNativeVideoExportWriteFrameRequest(sessionId, session, requestId, {
success: true,
})
})
.catch((error) => {
session.stdinError = error instanceof Error ? error : new Error(String(error))
settleNativeVideoExportWriteFrameRequest(sessionId, session, requestId, {
success: false,
error: getNativeVideoExportSessionError(
session,
session.stdinError.message,
),
})
})
},
)
void enqueueNativeVideoExportFrameWrite(session, frameData)
.then(() => {
settleNativeVideoExportWriteFrameRequest(sessionId, session, requestId, {
success: true,
});
})
.catch((error) => {
session.stdinError = error instanceof Error ? error : new Error(String(error));
settleNativeVideoExportWriteFrameRequest(sessionId, session, requestId, {
success: false,
error: getNativeVideoExportSessionError(
session,
session.stdinError.message,
),
});
});
},
);
ipcMain.handle(
'native-video-export-finish',
async (_, sessionId: string, options?: NativeVideoExportFinishOptions) => {
const session = nativeVideoExportSessions.get(sessionId)
if (!session) {
return { success: false, error: 'Invalid native export session' }
}
ipcMain.handle(
"native-video-export-finish",
async (_, sessionId: string, options?: NativeVideoExportFinishOptions) => {
const session = nativeVideoExportSessions.get(sessionId);
if (!session) {
return { success: false, error: "Invalid native export session" };
}
try {
await session.writeSequence
if (!session.ffmpegProcess.stdin.destroyed && !session.ffmpegProcess.stdin.writableEnded) {
session.ffmpegProcess.stdin.end()
}
await session.completionPromise
try {
await session.writeSequence;
if (
!session.ffmpegProcess.stdin.destroyed &&
!session.ffmpegProcess.stdin.writableEnded
) {
session.ffmpegProcess.stdin.end();
}
await session.completionPromise;
const finalized = await muxNativeVideoExportAudio(session.outputPath, options ?? {})
const muxedVideoReadStartedAt = performance.now()
const data = await fs.readFile(finalized.outputPath)
nativeVideoExportSessions.delete(sessionId)
await removeTemporaryExportFile(finalized.outputPath)
const finalized = await muxNativeVideoExportAudio(
session.outputPath,
options ?? {},
);
const muxedVideoReadStartedAt = performance.now();
const data = await fs.readFile(finalized.outputPath);
nativeVideoExportSessions.delete(sessionId);
await removeTemporaryExportFile(finalized.outputPath);
return {
success: true,
data: new Uint8Array(data),
encoderName: session.encoderName,
metrics: {
...finalized.metrics,
muxedVideoReadMs: performance.now() - muxedVideoReadStartedAt,
muxedVideoBytes: data.byteLength,
},
}
} catch (error) {
flushNativeVideoExportPendingWriteRequests(
sessionId,
session,
String(error),
)
nativeVideoExportSessions.delete(sessionId)
await removeTemporaryExportFile(session.outputPath)
const finalizedSuffix = session.outputPath.replace(/\.mp4$/, '-final.mp4')
await removeTemporaryExportFile(finalizedSuffix)
return {
success: false,
error: String(error),
}
}
},
)
return {
success: true,
data: new Uint8Array(data),
encoderName: session.encoderName,
metrics: {
...finalized.metrics,
muxedVideoReadMs: performance.now() - muxedVideoReadStartedAt,
muxedVideoBytes: data.byteLength,
},
};
} catch (error) {
flushNativeVideoExportPendingWriteRequests(sessionId, session, String(error));
nativeVideoExportSessions.delete(sessionId);
await removeTemporaryExportFile(session.outputPath);
const finalizedSuffix = session.outputPath.replace(/\.mp4$/, "-final.mp4");
await removeTemporaryExportFile(finalizedSuffix);
return {
success: false,
error: String(error),
};
}
},
);
ipcMain.handle(
'mux-exported-video-audio',
async (_, videoData: ArrayBuffer, options?: NativeVideoExportFinishOptions) => {
try {
const result = await muxExportedVideoAudioBuffer(videoData, options ?? {})
return {
success: true,
data: result.data,
metrics: result.metrics,
}
} catch (error) {
return {
success: false,
error: String(error),
}
}
},
)
ipcMain.handle(
"mux-exported-video-audio",
async (_, videoData: ArrayBuffer, options?: NativeVideoExportFinishOptions) => {
try {
const result = await muxExportedVideoAudioBuffer(videoData, options ?? {});
return {
success: true,
data: result.data,
metrics: result.metrics,
};
} catch (error) {
return {
success: false,
error: String(error),
};
}
},
);
ipcMain.handle('native-video-export-cancel', async (_, sessionId: string) => {
const session = nativeVideoExportSessions.get(sessionId)
if (!session) {
return { success: true }
}
ipcMain.handle("native-video-export-cancel", async (_, sessionId: string) => {
const session = nativeVideoExportSessions.get(sessionId);
if (!session) {
return { success: true };
}
session.terminating = true
nativeVideoExportSessions.delete(sessionId)
flushNativeVideoExportPendingWriteRequests(
sessionId,
session,
'Native video export session was cancelled',
)
session.terminating = true;
nativeVideoExportSessions.delete(sessionId);
flushNativeVideoExportPendingWriteRequests(
sessionId,
session,
"Native video export session was cancelled",
);
try {
if (!session.ffmpegProcess.stdin.destroyed && !session.ffmpegProcess.stdin.writableEnded) {
session.ffmpegProcess.stdin.destroy()
}
} catch {
// Stream may already be closed.
}
try {
if (
!session.ffmpegProcess.stdin.destroyed &&
!session.ffmpegProcess.stdin.writableEnded
) {
session.ffmpegProcess.stdin.destroy();
}
} catch {
// Stream may already be closed.
}
try {
session.ffmpegProcess.kill('SIGKILL')
} catch {
// Process may already be closed.
}
try {
session.ffmpegProcess.kill("SIGKILL");
} catch {
// Process may already be closed.
}
await session.completionPromise.catch(() => undefined)
await removeTemporaryExportFile(session.outputPath)
return { success: true }
})
await session.completionPromise.catch(() => undefined);
await removeTemporaryExportFile(session.outputPath);
return { success: true };
});
ipcMain.handle('save-exported-video', async (event, videoData: ArrayBuffer, fileName: string) => {
try {
// Determine file type from extension
const isGif = fileName.toLowerCase().endsWith('.gif');
const filters = isGif
? [{ name: 'GIF Image', extensions: ['gif'] }]
: [{ name: 'MP4 Video', extensions: ['mp4'] }];
const parentWindow = BrowserWindow.fromWebContents(event.sender)
const saveDialogOptions: SaveDialogOptions = {
title: isGif ? 'Save Exported GIF' : 'Save Exported Video',
defaultPath: path.join(app.getPath('downloads'), fileName),
filters,
properties: ['createDirectory', 'showOverwriteConfirmation'],
}
ipcMain.handle(
"save-exported-video",
async (event, videoData: ArrayBuffer, fileName: string) => {
try {
// Determine file type from extension
const isGif = fileName.toLowerCase().endsWith(".gif");
const filters = isGif
? [{ name: "GIF Image", extensions: ["gif"] }]
: [{ name: "MP4 Video", extensions: ["mp4"] }];
const parentWindow = BrowserWindow.fromWebContents(event.sender);
const saveDialogOptions: SaveDialogOptions = {
title: isGif ? "Save Exported GIF" : "Save Exported Video",
defaultPath: path.join(app.getPath("downloads"), fileName),
filters,
properties: ["createDirectory", "showOverwriteConfirmation"],
};
const result = parentWindow
? await dialog.showSaveDialog(parentWindow, saveDialogOptions)
: await dialog.showSaveDialog(saveDialogOptions)
const result = parentWindow
? await dialog.showSaveDialog(parentWindow, saveDialogOptions)
: await dialog.showSaveDialog(saveDialogOptions);
if (result.canceled || !result.filePath) {
return {
success: false,
canceled: true,
message: 'Export canceled'
};
}
if (result.canceled || !result.filePath) {
return {
success: false,
canceled: true,
message: "Export canceled",
};
}
await fs.writeFile(result.filePath, Buffer.from(videoData));
approveUserPath(result.filePath);
await fs.writeFile(result.filePath, Buffer.from(videoData));
approveUserPath(result.filePath);
return {
success: true,
path: result.filePath,
message: 'Video exported successfully'
};
} catch (error) {
console.error('Failed to save exported video:', error)
return {
success: false,
message: 'Failed to save exported video',
error: String(error)
}
}
})
return {
success: true,
path: result.filePath,
message: "Video exported successfully",
};
} catch (error) {
console.error("Failed to save exported video:", error);
return {
success: false,
message: "Failed to save exported video",
error: String(error),
};
}
},
);
ipcMain.handle('write-exported-video-to-path', async (_event, videoData: ArrayBuffer, outputPath: string) => {
try {
const resolvedPath = path.resolve(outputPath)
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
await fs.writeFile(resolvedPath, Buffer.from(videoData));
approveUserPath(resolvedPath);
return {
success: true,
path: resolvedPath,
message: 'Video exported successfully',
canceled: false,
};
} catch (error) {
console.error('Failed to write exported video to path:', error)
return {
success: false,
message: 'Failed to write exported video',
canceled: false,
error: String(error)
}
}
})
ipcMain.handle(
"write-exported-video-to-path",
async (_event, videoData: ArrayBuffer, outputPath: string) => {
try {
const resolvedPath = path.resolve(outputPath);
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
await fs.writeFile(resolvedPath, Buffer.from(videoData));
approveUserPath(resolvedPath);
return {
success: true,
path: resolvedPath,
message: "Video exported successfully",
canceled: false,
};
} catch (error) {
console.error("Failed to write exported video to path:", error);
return {
success: false,
message: "Failed to write exported video",
canceled: false,
error: String(error),
};
}
},
);
}
-2
View File
@@ -157,7 +157,6 @@ export class ModernVideoExporter {
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;
@@ -1667,7 +1666,6 @@ export class ModernVideoExporter {
this.lastFinalizationRenderProgress =
INITIAL_FINALIZATION_PROGRESS_STATE.lastRenderProgress;
this.lastFinalizationAudioProgress = INITIAL_FINALIZATION_PROGRESS_STATE.lastAudioProgress;
this.effectiveDurationSec = 0;
this.lastProgressSampleTimeMs = 0;
this.lastProgressSampleFrame = 0;
this.nativeWritePromises = new Set();