mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-26 07:45:34 +00:00
Merge pull request #669 from webadderallorg/pr/export-caption-sidecars
Add caption sidecar export option
This commit is contained in:
Vendored
+24
@@ -543,6 +543,14 @@ interface Window {
|
||||
tempPath: string;
|
||||
fileName: string;
|
||||
outputPath?: string | null;
|
||||
captionSidecar?: {
|
||||
format: "srt" | "vtt" | "both";
|
||||
cues: Array<{
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
text: string;
|
||||
}>;
|
||||
};
|
||||
}) => Promise<{
|
||||
success: boolean;
|
||||
path?: string;
|
||||
@@ -614,10 +622,26 @@ interface Window {
|
||||
saveExportedVideo: (
|
||||
videoData: ArrayBuffer,
|
||||
fileName: string,
|
||||
captionSidecar?: {
|
||||
format: "srt" | "vtt" | "both";
|
||||
cues: Array<{
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
text: string;
|
||||
}>;
|
||||
},
|
||||
) => Promise<{ success: boolean; path?: string; message?: string; canceled?: boolean }>;
|
||||
writeExportedVideoToPath: (
|
||||
videoData: ArrayBuffer,
|
||||
outputPath: string,
|
||||
captionSidecar?: {
|
||||
format: "srt" | "vtt" | "both";
|
||||
cues: Array<{
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
text: string;
|
||||
}>;
|
||||
},
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
path?: string;
|
||||
|
||||
@@ -246,6 +246,121 @@ function isTempPathSafe(tempPath: string): boolean {
|
||||
return candidate.startsWith(withSep);
|
||||
}
|
||||
|
||||
type CaptionSidecarCue = {
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
text: string;
|
||||
};
|
||||
|
||||
type CaptionSidecarPayload = {
|
||||
format: "srt" | "vtt" | "both";
|
||||
cues: CaptionSidecarCue[];
|
||||
};
|
||||
|
||||
function toSrtTimestamp(totalMs: number): string {
|
||||
const ms = Math.max(0, Math.round(totalMs));
|
||||
const hours = Math.floor(ms / 3_600_000);
|
||||
const minutes = Math.floor((ms % 3_600_000) / 60_000);
|
||||
const seconds = Math.floor((ms % 60_000) / 1000);
|
||||
const millis = ms % 1000;
|
||||
return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")},${String(millis).padStart(3, "0")}`;
|
||||
}
|
||||
|
||||
function toVttTimestamp(totalMs: number): string {
|
||||
const ms = Math.max(0, Math.round(totalMs));
|
||||
const hours = Math.floor(ms / 3_600_000);
|
||||
const minutes = Math.floor((ms % 3_600_000) / 60_000);
|
||||
const seconds = Math.floor((ms % 60_000) / 1000);
|
||||
const millis = ms % 1000;
|
||||
return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}.${String(millis).padStart(3, "0")}`;
|
||||
}
|
||||
|
||||
function normalizeCaptionSidecarCues(cues: unknown): CaptionSidecarCue[] {
|
||||
if (!Array.isArray(cues)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return cues
|
||||
.filter((cue): cue is CaptionSidecarCue => {
|
||||
return (
|
||||
typeof cue === "object" &&
|
||||
cue !== null &&
|
||||
typeof cue.startMs === "number" &&
|
||||
typeof cue.endMs === "number" &&
|
||||
typeof cue.text === "string" &&
|
||||
Number.isFinite(cue.startMs) &&
|
||||
Number.isFinite(cue.endMs) &&
|
||||
cue.endMs > cue.startMs &&
|
||||
cue.text.trim().length > 0
|
||||
);
|
||||
})
|
||||
.map((cue) => ({
|
||||
startMs: cue.startMs,
|
||||
endMs: cue.endMs,
|
||||
text: cue.text.replace(/\r\n/g, "\n").trim(),
|
||||
}));
|
||||
}
|
||||
|
||||
function parseCaptionSidecarPayload(payload: unknown): CaptionSidecarPayload | null {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidate = payload as {
|
||||
format?: unknown;
|
||||
cues?: unknown;
|
||||
};
|
||||
|
||||
const format =
|
||||
candidate.format === "srt" || candidate.format === "vtt" || candidate.format === "both"
|
||||
? candidate.format
|
||||
: null;
|
||||
if (!format) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cues = normalizeCaptionSidecarCues(candidate.cues);
|
||||
if (cues.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { format, cues };
|
||||
}
|
||||
|
||||
function serializeSrt(cues: CaptionSidecarCue[]): string {
|
||||
return cues
|
||||
.map((cue, index) => {
|
||||
return `${index + 1}\n${toSrtTimestamp(cue.startMs)} --> ${toSrtTimestamp(cue.endMs)}\n${cue.text}`;
|
||||
})
|
||||
.join("\n\n");
|
||||
}
|
||||
|
||||
function serializeVtt(cues: CaptionSidecarCue[]): string {
|
||||
const body = cues
|
||||
.map((cue) => {
|
||||
return `${toVttTimestamp(cue.startMs)} --> ${toVttTimestamp(cue.endMs)}\n${cue.text}`;
|
||||
})
|
||||
.join("\n\n");
|
||||
return `WEBVTT\n\n${body}`;
|
||||
}
|
||||
|
||||
async function writeCaptionSidecars(videoPath: string, payload: CaptionSidecarPayload | null) {
|
||||
if (!payload) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = path.parse(videoPath);
|
||||
const basePath = path.join(parsed.dir, parsed.name);
|
||||
|
||||
if (payload.format === "srt" || payload.format === "both") {
|
||||
await fs.writeFile(`${basePath}.srt`, serializeSrt(payload.cues), "utf8");
|
||||
}
|
||||
|
||||
if (payload.format === "vtt" || payload.format === "both") {
|
||||
await fs.writeFile(`${basePath}.vtt`, serializeVtt(payload.cues), "utf8");
|
||||
}
|
||||
}
|
||||
|
||||
export function registerExportHandlers() {
|
||||
ipcMain.handle(
|
||||
"native-video-export-start",
|
||||
@@ -829,8 +944,14 @@ export function registerExportHandlers() {
|
||||
|
||||
ipcMain.handle(
|
||||
"save-exported-video",
|
||||
async (event, videoData: ArrayBuffer, fileName: string) => {
|
||||
async (
|
||||
event,
|
||||
videoData: ArrayBuffer,
|
||||
fileName: string,
|
||||
captionSidecar?: CaptionSidecarPayload,
|
||||
) => {
|
||||
try {
|
||||
const sidecarPayload = parseCaptionSidecarPayload(captionSidecar);
|
||||
const sizeError = getInMemoryExportTooLargeMessage(videoData.byteLength);
|
||||
if (sizeError) {
|
||||
return {
|
||||
@@ -866,6 +987,7 @@ export function registerExportHandlers() {
|
||||
}
|
||||
|
||||
await fs.writeFile(result.filePath, Buffer.from(videoData));
|
||||
await writeCaptionSidecars(result.filePath, sidecarPayload);
|
||||
approveUserPath(result.filePath);
|
||||
|
||||
return {
|
||||
@@ -886,8 +1008,14 @@ export function registerExportHandlers() {
|
||||
|
||||
ipcMain.handle(
|
||||
"write-exported-video-to-path",
|
||||
async (_event, videoData: ArrayBuffer, outputPath: string) => {
|
||||
async (
|
||||
_event,
|
||||
videoData: ArrayBuffer,
|
||||
outputPath: string,
|
||||
captionSidecar?: CaptionSidecarPayload,
|
||||
) => {
|
||||
try {
|
||||
const sidecarPayload = parseCaptionSidecarPayload(captionSidecar);
|
||||
const sizeError = getInMemoryExportTooLargeMessage(videoData.byteLength);
|
||||
if (sizeError) {
|
||||
return {
|
||||
@@ -901,6 +1029,7 @@ export function registerExportHandlers() {
|
||||
const resolvedPath = path.resolve(outputPath);
|
||||
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|
||||
await fs.writeFile(resolvedPath, Buffer.from(videoData));
|
||||
await writeCaptionSidecars(resolvedPath, sidecarPayload);
|
||||
approveUserPath(resolvedPath);
|
||||
|
||||
return {
|
||||
@@ -929,6 +1058,7 @@ export function registerExportHandlers() {
|
||||
tempPath: string;
|
||||
fileName: string;
|
||||
outputPath?: string | null;
|
||||
captionSidecar?: CaptionSidecarPayload;
|
||||
},
|
||||
) => {
|
||||
const tempPath = payload?.tempPath;
|
||||
@@ -954,9 +1084,11 @@ export function registerExportHandlers() {
|
||||
}
|
||||
|
||||
try {
|
||||
const sidecarPayload = parseCaptionSidecarPayload(payload.captionSidecar);
|
||||
if (payload.outputPath) {
|
||||
const resolvedPath = path.resolve(payload.outputPath);
|
||||
await moveExportedTempFile(tempPath, resolvedPath);
|
||||
await writeCaptionSidecars(resolvedPath, sidecarPayload);
|
||||
releaseOwnedExportPath(tempPath);
|
||||
approveUserPath(resolvedPath);
|
||||
return {
|
||||
@@ -994,6 +1126,7 @@ export function registerExportHandlers() {
|
||||
}
|
||||
|
||||
await moveExportedTempFile(tempPath, result.filePath);
|
||||
await writeCaptionSidecars(result.filePath, sidecarPayload);
|
||||
releaseOwnedExportPath(tempPath);
|
||||
approveUserPath(result.filePath);
|
||||
|
||||
|
||||
+39
-4
@@ -456,6 +456,14 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
tempPath: string;
|
||||
fileName: string;
|
||||
outputPath?: string | null;
|
||||
captionSidecar?: {
|
||||
format: "srt" | "vtt" | "both";
|
||||
cues: Array<{
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
text: string;
|
||||
}>;
|
||||
};
|
||||
}) => {
|
||||
return ipcRenderer.invoke("finalize-exported-video", payload);
|
||||
},
|
||||
@@ -630,11 +638,38 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
openAccessibilityPreferences: () => {
|
||||
return ipcRenderer.invoke("open-accessibility-preferences");
|
||||
},
|
||||
saveExportedVideo: (videoData: ArrayBuffer, fileName: string) => {
|
||||
return ipcRenderer.invoke("save-exported-video", videoData, fileName);
|
||||
saveExportedVideo: (
|
||||
videoData: ArrayBuffer,
|
||||
fileName: string,
|
||||
captionSidecar?: {
|
||||
format: "srt" | "vtt" | "both";
|
||||
cues: Array<{
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
text: string;
|
||||
}>;
|
||||
},
|
||||
) => {
|
||||
return ipcRenderer.invoke("save-exported-video", videoData, fileName, captionSidecar);
|
||||
},
|
||||
writeExportedVideoToPath: (videoData: ArrayBuffer, outputPath: string) => {
|
||||
return ipcRenderer.invoke("write-exported-video-to-path", videoData, outputPath);
|
||||
writeExportedVideoToPath: (
|
||||
videoData: ArrayBuffer,
|
||||
outputPath: string,
|
||||
captionSidecar?: {
|
||||
format: "srt" | "vtt" | "both";
|
||||
cues: Array<{
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
text: string;
|
||||
}>;
|
||||
},
|
||||
) => {
|
||||
return ipcRenderer.invoke(
|
||||
"write-exported-video-to-path",
|
||||
videoData,
|
||||
outputPath,
|
||||
captionSidecar,
|
||||
);
|
||||
},
|
||||
openVideoFilePicker: () => {
|
||||
return ipcRenderer.invoke("open-video-file-picker");
|
||||
|
||||
@@ -29,6 +29,9 @@ interface ExportSettingsMenuProps {
|
||||
experimentalNvidiaCudaExport?: boolean;
|
||||
onExperimentalNvidiaCudaExportChange?: (enabled: boolean) => void;
|
||||
nvidiaCudaExportAvailable?: boolean;
|
||||
showCaptionSidecarOption?: boolean;
|
||||
includeCaptionSidecar?: boolean;
|
||||
onIncludeCaptionSidecarChange?: (enabled: boolean) => void;
|
||||
mp4OutputDimensions?: Record<ExportQuality, { width: number; height: number }>;
|
||||
gifFrameRate: GifFrameRate;
|
||||
onGifFrameRateChange?: (rate: GifFrameRate) => void;
|
||||
@@ -55,6 +58,9 @@ export function ExportSettingsMenu({
|
||||
experimentalNvidiaCudaExport = false,
|
||||
onExperimentalNvidiaCudaExportChange,
|
||||
nvidiaCudaExportAvailable = false,
|
||||
showCaptionSidecarOption = false,
|
||||
includeCaptionSidecar = false,
|
||||
onIncludeCaptionSidecarChange,
|
||||
mp4OutputDimensions,
|
||||
gifFrameRate,
|
||||
onGifFrameRateChange,
|
||||
@@ -365,6 +371,30 @@ export function ExportSettingsMenu({
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{showCaptionSidecarOption ? (
|
||||
<div className="mb-3 flex min-h-12 items-center justify-between gap-3 rounded-lg border border-foreground/10 bg-foreground/5 px-3 py-2">
|
||||
<div className="min-w-0">
|
||||
<p className="text-[11px] font-semibold text-foreground">
|
||||
{tSettings("export.captionSidecar.title", "Export captions file")}
|
||||
</p>
|
||||
<p className="mt-0.5 truncate text-[10px] text-muted-foreground/75">
|
||||
{tSettings(
|
||||
"export.captionSidecar.hint",
|
||||
"Save .srt and .vtt files next to your exported video.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={includeCaptionSidecar}
|
||||
onCheckedChange={onIncludeCaptionSidecarChange}
|
||||
aria-label={tSettings(
|
||||
"export.captionSidecar.toggle",
|
||||
"Export captions sidecar files",
|
||||
)}
|
||||
className="shrink-0 scale-75 data-[state=checked]:bg-[#2563EB]"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</LayoutGroup>
|
||||
) : (
|
||||
<div className="mb-3 space-y-2">
|
||||
|
||||
@@ -226,6 +226,14 @@ type PendingExportSave = {
|
||||
fileName: string;
|
||||
arrayBuffer?: ArrayBuffer;
|
||||
tempFilePath?: string;
|
||||
captionSidecar?: {
|
||||
format: "srt" | "vtt" | "both";
|
||||
cues: Array<{
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
text: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
type CancelableExporter = {
|
||||
@@ -521,6 +529,7 @@ export default function VideoEditor() {
|
||||
const [autoCaptionSettings, setAutoCaptionSettings] = useState<AutoCaptionSettings>(
|
||||
DEFAULT_AUTO_CAPTION_SETTINGS,
|
||||
);
|
||||
const [includeCaptionSidecar, setIncludeCaptionSidecar] = useState(true);
|
||||
const [whisperExecutablePath, setWhisperExecutablePath] = useState<string | null>(
|
||||
initialEditorPreferences.whisperExecutablePath,
|
||||
);
|
||||
@@ -596,6 +605,32 @@ export default function VideoEditor() {
|
||||
const [gifSizePreset, setGifSizePreset] = useState<GifSizePreset>(
|
||||
initialEditorPreferences.gifSizePreset,
|
||||
);
|
||||
const hasCaptionsForSidecar = autoCaptionSettings.enabled && autoCaptions.length > 0;
|
||||
const captionSidecarCues = useMemo(
|
||||
() =>
|
||||
autoCaptions
|
||||
.filter(
|
||||
(cue) =>
|
||||
Number.isFinite(cue.startMs) &&
|
||||
Number.isFinite(cue.endMs) &&
|
||||
cue.endMs > cue.startMs &&
|
||||
typeof cue.text === "string" &&
|
||||
cue.text.trim().length > 0,
|
||||
)
|
||||
.map((cue) => ({
|
||||
startMs: cue.startMs,
|
||||
endMs: cue.endMs,
|
||||
text: cue.text,
|
||||
})),
|
||||
[autoCaptions],
|
||||
);
|
||||
const captionSidecarPayload =
|
||||
hasCaptionsForSidecar && captionSidecarCues.length > 0 && includeCaptionSidecar
|
||||
? {
|
||||
format: "both" as const,
|
||||
cues: captionSidecarCues,
|
||||
}
|
||||
: undefined;
|
||||
const [exportedFilePath, setExportedFilePath] = useState<string | undefined>(undefined);
|
||||
const [hasPendingExportSave, setHasPendingExportSave] = useState(false);
|
||||
const [lastSavedSnapshot, setLastSavedSnapshot] = useState<EditorProjectData | null>(null);
|
||||
@@ -1283,7 +1318,12 @@ export default function VideoEditor() {
|
||||
}, []);
|
||||
|
||||
const saveBlobExport = useCallback(
|
||||
async (blob: Blob, fileName: string, outputPath: string | null = null) => {
|
||||
async (
|
||||
blob: Blob,
|
||||
fileName: string,
|
||||
outputPath: string | null = null,
|
||||
captionSidecar?: PendingExportSave["captionSidecar"],
|
||||
) => {
|
||||
const extension = fileName.split(".").pop()?.toLowerCase() || "bin";
|
||||
const hasExportStreamApi =
|
||||
typeof window !== "undefined" &&
|
||||
@@ -1300,10 +1340,12 @@ export default function VideoEditor() {
|
||||
tempPath: tempFilePath,
|
||||
fileName,
|
||||
outputPath,
|
||||
captionSidecar,
|
||||
}),
|
||||
pendingSave: {
|
||||
fileName,
|
||||
tempFilePath,
|
||||
captionSidecar,
|
||||
} satisfies PendingExportSave,
|
||||
};
|
||||
}
|
||||
@@ -1342,11 +1384,20 @@ export default function VideoEditor() {
|
||||
const arrayBuffer = await blob.arrayBuffer();
|
||||
return {
|
||||
saveResult: outputPath
|
||||
? await window.electronAPI.writeExportedVideoToPath(arrayBuffer, outputPath)
|
||||
: await window.electronAPI.saveExportedVideo(arrayBuffer, fileName),
|
||||
? await window.electronAPI.writeExportedVideoToPath(
|
||||
arrayBuffer,
|
||||
outputPath,
|
||||
captionSidecar,
|
||||
)
|
||||
: await window.electronAPI.saveExportedVideo(
|
||||
arrayBuffer,
|
||||
fileName,
|
||||
captionSidecar,
|
||||
),
|
||||
pendingSave: {
|
||||
fileName,
|
||||
arrayBuffer,
|
||||
captionSidecar,
|
||||
} satisfies PendingExportSave,
|
||||
};
|
||||
},
|
||||
@@ -4458,6 +4509,10 @@ export default function VideoEditor() {
|
||||
if (result.success && (result.blob || result.tempFilePath)) {
|
||||
const timestamp = Date.now();
|
||||
const fileName = `export-${timestamp}.mp4`;
|
||||
const sidecarForThisExport =
|
||||
settings.includeCaptionSidecar && captionSidecarPayload
|
||||
? captionSidecarPayload
|
||||
: undefined;
|
||||
markExportAsSaving();
|
||||
|
||||
let saveResult: {
|
||||
@@ -4479,8 +4534,13 @@ export default function VideoEditor() {
|
||||
smokeExportConfig.enabled && smokeExportConfig.outputPath
|
||||
? smokeExportConfig.outputPath
|
||||
: null,
|
||||
captionSidecar: sidecarForThisExport,
|
||||
});
|
||||
pendingOnCancel = { fileName, tempFilePath: result.tempFilePath };
|
||||
pendingOnCancel = {
|
||||
fileName,
|
||||
tempFilePath: result.tempFilePath,
|
||||
captionSidecar: sidecarForThisExport,
|
||||
};
|
||||
} else if (result.blob) {
|
||||
// Legacy fallback: some export paths still surface a Blob, but in
|
||||
// Electron we stream it into a temp file first so save/finalize
|
||||
@@ -4489,6 +4549,7 @@ export default function VideoEditor() {
|
||||
result.blob,
|
||||
fileName,
|
||||
smokeExportConfig.enabled ? smokeExportConfig.outputPath : null,
|
||||
sidecarForThisExport,
|
||||
);
|
||||
saveResult = blobSave.saveResult;
|
||||
pendingOnCancel = blobSave.pendingSave;
|
||||
@@ -4701,6 +4762,7 @@ export default function VideoEditor() {
|
||||
annotationRegions,
|
||||
autoCaptions,
|
||||
autoCaptionSettings,
|
||||
captionSidecarPayload,
|
||||
isPlaying,
|
||||
exportQuality,
|
||||
effectiveZoomRegions,
|
||||
@@ -4858,6 +4920,7 @@ export default function VideoEditor() {
|
||||
sourceWidth,
|
||||
sourceHeight,
|
||||
exportFormat,
|
||||
includeCaptionSidecar: hasCaptionsForSidecar && includeCaptionSidecar,
|
||||
exportEncodingMode,
|
||||
exportQuality,
|
||||
mp4FrameRate,
|
||||
@@ -4881,6 +4944,8 @@ export default function VideoEditor() {
|
||||
gifFrameRate,
|
||||
gifLoop,
|
||||
gifSizePreset,
|
||||
hasCaptionsForSidecar,
|
||||
includeCaptionSidecar,
|
||||
exportBackendPreference,
|
||||
exportPipelineModel,
|
||||
handleExport,
|
||||
@@ -4925,11 +4990,13 @@ export default function VideoEditor() {
|
||||
tempPath: pendingSave.tempFilePath,
|
||||
fileName: pendingSave.fileName,
|
||||
outputPath: null,
|
||||
captionSidecar: pendingSave.captionSidecar,
|
||||
});
|
||||
} else if (pendingSave.arrayBuffer) {
|
||||
saveResult = await window.electronAPI.saveExportedVideo(
|
||||
pendingSave.arrayBuffer,
|
||||
pendingSave.fileName,
|
||||
pendingSave.captionSidecar,
|
||||
);
|
||||
} else {
|
||||
saveResult = { success: false, message: "No pending export to save" };
|
||||
@@ -5669,6 +5736,9 @@ export default function VideoEditor() {
|
||||
onGifLoopChange={setGifLoop}
|
||||
gifSizePreset={gifSizePreset}
|
||||
onGifSizePresetChange={setGifSizePreset}
|
||||
showCaptionSidecarOption={hasCaptionsForSidecar && exportFormat === "mp4"}
|
||||
includeCaptionSidecar={includeCaptionSidecar}
|
||||
onIncludeCaptionSidecarChange={setIncludeCaptionSidecar}
|
||||
mp4OutputDimensions={mp4OutputDimensions}
|
||||
gifOutputDimensions={gifOutputDimensions}
|
||||
onExport={handleStartExportFromDropdown}
|
||||
|
||||
@@ -5,6 +5,7 @@ const baseOptions = {
|
||||
sourceWidth: 1920,
|
||||
sourceHeight: 1080,
|
||||
exportFormat: "mp4" as const,
|
||||
includeCaptionSidecar: true,
|
||||
exportEncodingMode: "balanced" as const,
|
||||
exportQuality: "good" as const,
|
||||
mp4FrameRate: 30 as const,
|
||||
@@ -19,6 +20,7 @@ describe("resolveExportStartSettings", () => {
|
||||
it("preserves MP4 dropdown settings", () => {
|
||||
expect(resolveExportStartSettings(baseOptions)).toEqual({
|
||||
format: "mp4",
|
||||
includeCaptionSidecar: true,
|
||||
encodingMode: "balanced",
|
||||
mp4FrameRate: 30,
|
||||
backendPreference: "auto",
|
||||
@@ -41,6 +43,7 @@ describe("resolveExportStartSettings", () => {
|
||||
}),
|
||||
).toEqual({
|
||||
format: "gif",
|
||||
includeCaptionSidecar: false,
|
||||
encodingMode: undefined,
|
||||
mp4FrameRate: undefined,
|
||||
backendPreference: undefined,
|
||||
|
||||
@@ -16,6 +16,7 @@ export function resolveExportStartSettings({
|
||||
sourceWidth,
|
||||
sourceHeight,
|
||||
exportFormat,
|
||||
includeCaptionSidecar,
|
||||
exportEncodingMode,
|
||||
exportQuality,
|
||||
mp4FrameRate,
|
||||
@@ -28,6 +29,7 @@ export function resolveExportStartSettings({
|
||||
sourceWidth: number;
|
||||
sourceHeight: number;
|
||||
exportFormat: ExportFormat;
|
||||
includeCaptionSidecar: boolean;
|
||||
exportEncodingMode: ExportEncodingMode;
|
||||
exportQuality: ExportQuality;
|
||||
mp4FrameRate: ExportMp4FrameRate;
|
||||
@@ -44,6 +46,7 @@ export function resolveExportStartSettings({
|
||||
|
||||
return {
|
||||
format: exportFormat,
|
||||
includeCaptionSidecar: exportFormat === "mp4" ? includeCaptionSidecar : false,
|
||||
encodingMode: exportFormat === "mp4" ? exportEncodingMode : undefined,
|
||||
mp4FrameRate: exportFormat === "mp4" ? mp4FrameRate : undefined,
|
||||
backendPreference: exportFormat === "mp4" ? exportBackendPreference : undefined,
|
||||
|
||||
@@ -193,6 +193,7 @@ export interface GifExportConfig {
|
||||
|
||||
export interface ExportSettings {
|
||||
format: ExportFormat;
|
||||
includeCaptionSidecar?: boolean;
|
||||
// MP4 settings
|
||||
quality?: ExportQuality;
|
||||
encodingMode?: ExportEncodingMode;
|
||||
|
||||
Reference in New Issue
Block a user