Merge pull request #423 from org-cyber/fix/windows-export-clean

fix(windows): Fixed windows Export Issue and early decode Crash
This commit is contained in:
Sid
2026-04-18 10:54:13 -07:00
committed by GitHub
4 changed files with 48 additions and 10 deletions
+19 -3
View File
@@ -670,6 +670,16 @@ export function registerIpcHandlers(
}
});
/**
* Handles saving an exported video file.
* Shows a save dialog, normalizes the file path for the current OS,
* ensures the directory exists, and writes the video data.
* @param _ - Unused event parameter.
* @param videoData - The exported video as an ArrayBuffer.
* @param fileName - Suggested filename for the save dialog.
* @returns Object with success status, optional file path, and error details.
*/
ipcMain.handle("save-exported-video", async (_, videoData: ArrayBuffer, fileName: string) => {
try {
// Determine file type from extension
@@ -695,11 +705,18 @@ export function registerIpcHandlers(
};
}
await fs.writeFile(result.filePath, Buffer.from(videoData));
// --- FIX: Normalize the path for Windows compatibility ---
const normalizedPath = path.normalize(result.filePath);
// Ensure the parent directory exists (Windows may fail if the folder is missing)
await fs.mkdir(path.dirname(normalizedPath), { recursive: true });
// --- END FIX ---
await fs.writeFile(normalizedPath, Buffer.from(videoData));
return {
success: true,
path: result.filePath,
path: normalizedPath,
message: "Video exported successfully",
};
} catch (error) {
@@ -711,7 +728,6 @@ export function registerIpcHandlers(
};
}
});
ipcMain.handle("open-video-file-picker", async () => {
try {
const result = await dialog.showOpenDialog({
@@ -322,7 +322,6 @@ export default function VideoEditor() {
aspectRatio,
webcamLayoutPreset,
webcamMaskShape,
webcamSizePreset,
webcamPosition,
exportQuality,
exportFormat,
@@ -1322,7 +1322,6 @@ export default function TimelineEditor({
selectedBlurId,
selectedSpeedId,
annotationRegions,
blurRegions,
currentTime,
onSelectAnnotation,
keyShortcuts,
+29 -5
View File
@@ -281,7 +281,15 @@ export class StreamingVideoDecoder {
return this.metadata;
}
/**
* Decodes all video frames from the loaded source and invokes a callback for each.
* Handles trimming and speed adjustments, and resamples to the target frame rate.
* On Windows, early decode termination is tolerated to work around driver quirks.
* @param targetFrameRate - Desired output frame rate.
* @param trimRegions - Array of time regions to keep (others discarded).
* @param speedRegions - Array of speed adjustments for specific time ranges.
* @param onFrame - Async callback receiving each decoded VideoFrame.
*/
async decodeAll(
targetFrameRate: number,
trimRegions: TrimRegion[] | undefined,
@@ -309,6 +317,8 @@ export class StreamingVideoDecoder {
this.computeSegments(this.metadata.duration, trimRegions),
speedRegions,
);
const requiredEndSec = segments[segments.length - 1]?.endSec ?? 0;
const segmentOutputFrameCounts = segments.map((segment) =>
Math.ceil(
((segment.endSec - segment.startSec - EPSILON_SEC) / segment.speed) * targetFrameRate,
@@ -556,7 +566,8 @@ export class StreamingVideoDecoder {
}
this.decoder = null;
const requiredEndSec = segments.length > 0 ? segments[segments.length - 1].endSec : 0;
const isWindows = typeof navigator !== "undefined" && /Windows/.test(navigator.userAgent);
if (
shouldFailDecodeEndedEarly({
cancelled: this.cancelled,
@@ -567,9 +578,22 @@ export class StreamingVideoDecoder {
) {
const decodedAtLabel =
lastDecodedFrameSec === null ? "no decoded frame" : `${lastDecodedFrameSec.toFixed(3)}s`;
throw new Error(
`Video decode ended early at ${decodedAtLabel} (needed ${requiredEndSec.toFixed(3)}s).`,
);
const decodeGapSec =
lastDecodedFrameSec === null ? Infinity : requiredEndSec - lastDecodedFrameSec;
// On Windows, tolerate a small decode gap: up to 10% of required duration, capped at 3 seconds.
const maxToleratedGap = Math.min(3.0, requiredEndSec * 0.1);
if (isWindows && lastDecodedFrameSec !== null && decodeGapSec <= maxToleratedGap) {
console.warn(
`[StreamingVideoDecoder] Decode ended early on Windows with a gap of ${decodeGapSec.toFixed(2)}s ` +
`(max tolerated: ${maxToleratedGap.toFixed(2)}s) proceeding anyway.`,
);
} else {
throw new Error(
`Video decode ended early at ${decodedAtLabel} (needed ${requiredEndSec.toFixed(3)}s).`,
);
}
}
}