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:
@@ -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,
|
||||
|
||||
@@ -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).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user