mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-24 23:05:49 +00:00
@@ -51,6 +51,16 @@ function getPartialExportDestinationPath(destinationPath: string) {
|
||||
return path.join(parsed.dir, `.recordly-partial-${parsed.name}-${suffix}${parsed.ext}`);
|
||||
}
|
||||
|
||||
const MAX_IN_MEMORY_EXPORT_BYTES = 0x7fffffff;
|
||||
|
||||
function getInMemoryExportTooLargeMessage(byteLength: number) {
|
||||
if (byteLength <= MAX_IN_MEMORY_EXPORT_BYTES) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return "Export is too large for the legacy in-memory save path. Please retry with temp-file streaming enabled.";
|
||||
}
|
||||
|
||||
export async function moveExportedTempFile(tempPath: string, destinationPath: string) {
|
||||
await fs.mkdir(path.dirname(destinationPath), { recursive: true });
|
||||
try {
|
||||
@@ -700,6 +710,14 @@ export function registerExportHandlers() {
|
||||
"mux-exported-video-audio",
|
||||
async (_, videoData: ArrayBuffer, options?: NativeVideoExportFinishOptions) => {
|
||||
try {
|
||||
const sizeError = getInMemoryExportTooLargeMessage(videoData.byteLength);
|
||||
if (sizeError) {
|
||||
return {
|
||||
success: false,
|
||||
error: sizeError,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await muxExportedVideoAudioBuffer(videoData, options ?? {});
|
||||
// Register the muxed output so finalize-exported-video / discard-
|
||||
// exported-temp accept it. Returning a temp path (instead of the
|
||||
@@ -797,6 +815,15 @@ export function registerExportHandlers() {
|
||||
"save-exported-video",
|
||||
async (event, videoData: ArrayBuffer, fileName: string) => {
|
||||
try {
|
||||
const sizeError = getInMemoryExportTooLargeMessage(videoData.byteLength);
|
||||
if (sizeError) {
|
||||
return {
|
||||
success: false,
|
||||
message: sizeError,
|
||||
error: sizeError,
|
||||
};
|
||||
}
|
||||
|
||||
// Determine file type from extension
|
||||
const isGif = fileName.toLowerCase().endsWith(".gif");
|
||||
const filters = isGif
|
||||
@@ -845,6 +872,16 @@ export function registerExportHandlers() {
|
||||
"write-exported-video-to-path",
|
||||
async (_event, videoData: ArrayBuffer, outputPath: string) => {
|
||||
try {
|
||||
const sizeError = getInMemoryExportTooLargeMessage(videoData.byteLength);
|
||||
if (sizeError) {
|
||||
return {
|
||||
success: false,
|
||||
message: sizeError,
|
||||
canceled: false,
|
||||
error: sizeError,
|
||||
};
|
||||
}
|
||||
|
||||
const resolvedPath = path.resolve(outputPath);
|
||||
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|
||||
await fs.writeFile(resolvedPath, Buffer.from(videoData));
|
||||
|
||||
@@ -76,6 +76,10 @@ import {
|
||||
VideoExporter,
|
||||
} from "@/lib/exporter";
|
||||
import { getMp4ExportBitrate, getSourceQualityBitrate } from "@/lib/exporter/exportBitrate";
|
||||
import {
|
||||
canUseInMemoryExportSaveFallback,
|
||||
describeBlockedInMemoryExportSave,
|
||||
} from "@/lib/exporter/exportSavePolicy";
|
||||
import { resolveMediaElementSource } from "@/lib/exporter/localMediaSource";
|
||||
import { resolveSourceAudioFallbackPaths } from "@/lib/exporter/sourceAudioFallback";
|
||||
import {
|
||||
@@ -1485,6 +1489,12 @@ export default function VideoEditor() {
|
||||
const saveBlobExport = useCallback(
|
||||
async (blob: Blob, fileName: string, outputPath: string | null = null) => {
|
||||
const extension = fileName.split(".").pop()?.toLowerCase() || "bin";
|
||||
const hasExportStreamApi =
|
||||
typeof window !== "undefined" &&
|
||||
typeof window.electronAPI?.openExportStream === "function" &&
|
||||
typeof window.electronAPI?.writeExportStreamChunk === "function" &&
|
||||
typeof window.electronAPI?.closeExportStream === "function";
|
||||
let streamError: unknown = null;
|
||||
|
||||
try {
|
||||
const tempFilePath = await streamExportBlobToTempFile(blob, extension);
|
||||
@@ -1502,9 +1512,37 @@ export default function VideoEditor() {
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[export] Falling back to in-memory blob save", error);
|
||||
streamError = error;
|
||||
console.warn("[export] Temp-file blob save failed", error);
|
||||
}
|
||||
|
||||
if (
|
||||
!canUseInMemoryExportSaveFallback({
|
||||
blobSize: blob.size,
|
||||
extension,
|
||||
hasExportStreamApi,
|
||||
})
|
||||
) {
|
||||
const message = describeBlockedInMemoryExportSave({
|
||||
blobSize: blob.size,
|
||||
extension,
|
||||
});
|
||||
console.error("[export] Refusing in-memory blob save fallback", {
|
||||
fileName,
|
||||
blobSize: blob.size,
|
||||
extension,
|
||||
hasExportStreamApi,
|
||||
streamError,
|
||||
});
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
console.warn("[export] Falling back to in-memory blob save", {
|
||||
fileName,
|
||||
blobSize: blob.size,
|
||||
extension,
|
||||
hasExportStreamApi,
|
||||
});
|
||||
const arrayBuffer = await blob.arrayBuffer();
|
||||
return {
|
||||
saveResult: outputPath
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
canUseInMemoryExportSaveFallback,
|
||||
describeBlockedInMemoryExportSave,
|
||||
isExportTooLargeForInMemorySave,
|
||||
MAX_IN_MEMORY_EXPORT_BYTES,
|
||||
normalizeExportExtension,
|
||||
} from "./exportSavePolicy";
|
||||
|
||||
describe("exportSavePolicy", () => {
|
||||
it("normalizes export extensions before policy checks", () => {
|
||||
expect(normalizeExportExtension(" MP4 ")).toBe("mp4");
|
||||
});
|
||||
|
||||
it("blocks the legacy in-memory save path above Node's Buffer limit", () => {
|
||||
expect(isExportTooLargeForInMemorySave(MAX_IN_MEMORY_EXPORT_BYTES + 1)).toBe(true);
|
||||
expect(
|
||||
canUseInMemoryExportSaveFallback({
|
||||
blobSize: MAX_IN_MEMORY_EXPORT_BYTES + 1,
|
||||
extension: "gif",
|
||||
hasExportStreamApi: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps Electron MP4 exports on the temp-file save path", () => {
|
||||
expect(
|
||||
canUseInMemoryExportSaveFallback({
|
||||
blobSize: 1024,
|
||||
extension: "mp4",
|
||||
hasExportStreamApi: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("allows small non-MP4 exports to use the legacy save fallback", () => {
|
||||
expect(
|
||||
canUseInMemoryExportSaveFallback({
|
||||
blobSize: 1024,
|
||||
extension: "gif",
|
||||
hasExportStreamApi: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("explains blocked large saves without mentioning implementation stack traces", () => {
|
||||
expect(
|
||||
describeBlockedInMemoryExportSave({
|
||||
blobSize: MAX_IN_MEMORY_EXPORT_BYTES + 1,
|
||||
extension: "mp4",
|
||||
}),
|
||||
).toContain("too large");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
export const MAX_IN_MEMORY_EXPORT_BYTES = 0x7fffffff;
|
||||
|
||||
export function normalizeExportExtension(extension: string): string {
|
||||
return extension.trim().toLowerCase();
|
||||
}
|
||||
|
||||
export function isExportTooLargeForInMemorySave(byteLength: number): boolean {
|
||||
return byteLength > MAX_IN_MEMORY_EXPORT_BYTES;
|
||||
}
|
||||
|
||||
export function canUseInMemoryExportSaveFallback({
|
||||
blobSize,
|
||||
extension,
|
||||
hasExportStreamApi,
|
||||
}: {
|
||||
blobSize: number;
|
||||
extension: string;
|
||||
hasExportStreamApi: boolean;
|
||||
}): boolean {
|
||||
if (isExportTooLargeForInMemorySave(blobSize)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// In Electron, MP4 exports should stay on the temp-file path. If that path
|
||||
// failed, silently falling back to ArrayBuffer reintroduces the >2 GiB crash.
|
||||
if (hasExportStreamApi && normalizeExportExtension(extension) === "mp4") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function describeBlockedInMemoryExportSave({
|
||||
blobSize,
|
||||
extension,
|
||||
}: {
|
||||
blobSize: number;
|
||||
extension: string;
|
||||
}): string {
|
||||
const normalizedExtension = normalizeExportExtension(extension) || "export";
|
||||
if (isExportTooLargeForInMemorySave(blobSize)) {
|
||||
return `The ${normalizedExtension.toUpperCase()} export is too large to save through the legacy in-memory path. Please retry the export so Recordly can save it through the temp-file streaming path.`;
|
||||
}
|
||||
|
||||
return `The ${normalizedExtension.toUpperCase()} export could not be saved through the temp-file streaming path, and Recordly will not fall back to the legacy in-memory path for MP4 exports. Please retry the export.`;
|
||||
}
|
||||
Reference in New Issue
Block a user