From ccc0e63d1b829cdc14e7a60a4394797f7ebd533c Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Mon, 20 Apr 2026 20:11:58 +0700 Subject: [PATCH] fix(export): clean telemetry profiling rebase --- electron/electron-env.d.ts | 6 +- electron/ipc/nativeVideoExport.ts | 37 +- electron/ipc/register/export.ts | 704 ++++++++++++------------ src/lib/exporter/modernVideoExporter.ts | 2 - 4 files changed, 391 insertions(+), 358 deletions(-) diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index f178a715..90249b39 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -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, diff --git a/electron/ipc/nativeVideoExport.ts b/electron/ipc/nativeVideoExport.ts index db503beb..58dbf5cc 100644 --- a/electron/ipc/nativeVideoExport.ts +++ b/electron/ipc/nativeVideoExport.ts @@ -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 { diff --git a/electron/ipc/register/export.ts b/electron/ipc/register/export.ts index a12ac2ec..9583a748 100644 --- a/electron/ipc/register/export.ts +++ b/electron/ipc/register/export.ts @@ -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 - // 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; + // 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(), - completionPromise: new Promise((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(), + completionPromise: new Promise((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), + }; + } + }, + ); } diff --git a/src/lib/exporter/modernVideoExporter.ts b/src/lib/exporter/modernVideoExporter.ts index 4b9766c1..fea3219a 100644 --- a/src/lib/exporter/modernVideoExporter.ts +++ b/src/lib/exporter/modernVideoExporter.ts @@ -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();