feat: add YouTube Shorts export preset
Adds an export preset registry and the first preset entry — YouTube Shorts — so users with projectType='shorts' get sensible export defaults aligned with YouTube's Shorts encoding recommendations.
- New types TExportPresetId and TExportPreset
- YOUTUBE_SHORTS_EXPORT_PRESET (mp4, very_high quality, audio on)
- EXPORT_PRESETS registry + getExportPresetById helper
- getDefaultExportOptions({projectType}) resolves Shorts vs standard defaults
- DEFAULT_EXPORT_OPTIONS export preserved for back-compat
- Unit tests cover preset shape, registry exhaustiveness, default resolution
This commit is contained in:
parent
7131f88bf4
commit
5913abb52c
|
|
@ -33,7 +33,7 @@ import {
|
|||
SectionTitle,
|
||||
} from "@/components/section";
|
||||
import { useEditor } from "@/editor/use-editor";
|
||||
import { DEFAULT_EXPORT_OPTIONS } from "@/export/defaults";
|
||||
import { getDefaultExportOptions } from "@/export/defaults";
|
||||
|
||||
function isExportFormat(value: string): value is ExportFormat {
|
||||
return EXPORT_FORMAT_VALUES.some((formatValue) => formatValue === value);
|
||||
|
|
@ -101,14 +101,13 @@ function ExportPopover({
|
|||
const activeProject = useEditor((e) => e.project.getActive());
|
||||
const exportState = useEditor((e) => e.project.getExportState());
|
||||
const { isExporting, progress, result: exportResult } = exportState;
|
||||
const [format, setFormat] = useState<ExportFormat>(
|
||||
DEFAULT_EXPORT_OPTIONS.format,
|
||||
);
|
||||
const [quality, setQuality] = useState<ExportQuality>(
|
||||
DEFAULT_EXPORT_OPTIONS.quality,
|
||||
);
|
||||
const initialOptions = getDefaultExportOptions({
|
||||
projectType: activeProject?.metadata?.projectType,
|
||||
});
|
||||
const [format, setFormat] = useState<ExportFormat>(initialOptions.format);
|
||||
const [quality, setQuality] = useState<ExportQuality>(initialOptions.quality);
|
||||
const [shouldIncludeAudio, setShouldIncludeAudio] = useState<boolean>(
|
||||
DEFAULT_EXPORT_OPTIONS.includeAudio ?? true,
|
||||
initialOptions.includeAudio ?? true,
|
||||
);
|
||||
|
||||
const handleExport = async () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
EXPORT_PRESETS,
|
||||
EXPORT_PRESET_IDS,
|
||||
YOUTUBE_SHORTS_EXPORT_PRESET,
|
||||
getExportPresetById,
|
||||
} from "@/export";
|
||||
import {
|
||||
DEFAULT_EXPORT_OPTIONS,
|
||||
getDefaultExportOptions,
|
||||
} from "@/export/defaults";
|
||||
|
||||
describe("YOUTUBE_SHORTS_EXPORT_PRESET", () => {
|
||||
test("has the expected encoding shape", () => {
|
||||
expect(YOUTUBE_SHORTS_EXPORT_PRESET.options).toMatchObject({
|
||||
format: "mp4",
|
||||
quality: "very_high",
|
||||
includeAudio: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("has descriptive label and description", () => {
|
||||
expect(YOUTUBE_SHORTS_EXPORT_PRESET.label).toMatch(/\S/);
|
||||
expect(YOUTUBE_SHORTS_EXPORT_PRESET.description).toMatch(/\S/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("EXPORT_PRESETS registry", () => {
|
||||
test("contains youtube-shorts", () => {
|
||||
const ids = EXPORT_PRESETS.map((p) => p.id);
|
||||
expect(ids).toContain("youtube-shorts");
|
||||
});
|
||||
|
||||
test("every registered preset's id is in EXPORT_PRESET_IDS", () => {
|
||||
for (const preset of EXPORT_PRESETS) {
|
||||
expect(EXPORT_PRESET_IDS).toContain(preset.id);
|
||||
}
|
||||
});
|
||||
|
||||
test("getExportPresetById resolves youtube-shorts", () => {
|
||||
const preset = getExportPresetById({ id: "youtube-shorts" });
|
||||
expect(preset).toBeDefined();
|
||||
expect(preset?.id).toBe("youtube-shorts");
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
describe("getDefaultExportOptions", () => {
|
||||
test("returns Shorts preset options when projectType is shorts", () => {
|
||||
const opts = getDefaultExportOptions({ projectType: "shorts" });
|
||||
expect(opts).toEqual(YOUTUBE_SHORTS_EXPORT_PRESET.options);
|
||||
});
|
||||
|
||||
test("returns DEFAULT_EXPORT_OPTIONS when projectType is standard", () => {
|
||||
const opts = getDefaultExportOptions({ projectType: "standard" });
|
||||
expect(opts).toEqual(DEFAULT_EXPORT_OPTIONS);
|
||||
});
|
||||
|
||||
test("returns DEFAULT_EXPORT_OPTIONS when projectType is undefined", () => {
|
||||
const opts = getDefaultExportOptions({});
|
||||
expect(opts).toEqual(DEFAULT_EXPORT_OPTIONS);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,26 @@
|
|||
import type { ExportOptions } from "./index";
|
||||
import type { TProjectType } from "@/project/types";
|
||||
import { YOUTUBE_SHORTS_EXPORT_PRESET, type ExportOptions } from "./index";
|
||||
|
||||
export const DEFAULT_EXPORT_OPTIONS = {
|
||||
format: "mp4",
|
||||
quality: "high",
|
||||
includeAudio: true,
|
||||
} satisfies ExportOptions;
|
||||
|
||||
/**
|
||||
* Resolve the export options that should be pre-filled when opening
|
||||
* the export dialog for a given project. Projects with intent === "shorts"
|
||||
* get the YouTube Shorts preset's options; everything else uses
|
||||
* DEFAULT_EXPORT_OPTIONS. Undefined projectType (back-compat for older
|
||||
* stored projects) is treated as "standard".
|
||||
*/
|
||||
export function getDefaultExportOptions({
|
||||
projectType,
|
||||
}: {
|
||||
projectType?: TProjectType;
|
||||
}): ExportOptions {
|
||||
if (projectType === "shorts") {
|
||||
return { ...YOUTUBE_SHORTS_EXPORT_PRESET.options };
|
||||
}
|
||||
return { ...DEFAULT_EXPORT_OPTIONS };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,54 @@ export interface ExportOptions {
|
|||
includeAudio?: boolean;
|
||||
}
|
||||
|
||||
// ─── Export presets ─────────────────────────────────────────────────────────
|
||||
|
||||
export const EXPORT_PRESET_IDS = ["youtube-shorts"] as const;
|
||||
|
||||
/**
|
||||
* Discriminator for export presets. "youtube-shorts" applies the
|
||||
* YouTube-recommended encoding shape for 1080×1920 vertical short videos.
|
||||
* New presets will be added here as their UI surface lands.
|
||||
*/
|
||||
export type TExportPresetId = (typeof EXPORT_PRESET_IDS)[number];
|
||||
|
||||
export interface TExportPreset {
|
||||
id: TExportPresetId;
|
||||
label: string;
|
||||
description: string;
|
||||
options: ExportOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* YouTube Shorts encoding preset. Targets YouTube's recommended
|
||||
* encoding shape: H.264 MP4, very_high quality (the bitrate mapping
|
||||
* lives downstream in the renderer/WASM layer), audio included.
|
||||
* FPS is intentionally not set — projects keep their own FPS, which
|
||||
* for Shorts is typically 30 or 60.
|
||||
*/
|
||||
export const YOUTUBE_SHORTS_EXPORT_PRESET: TExportPreset = {
|
||||
id: "youtube-shorts",
|
||||
label: "YouTube Shorts",
|
||||
description: "1080×1920 vertical, H.264 MP4 at very_high quality, AAC audio",
|
||||
options: {
|
||||
format: "mp4",
|
||||
quality: "very_high",
|
||||
includeAudio: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const EXPORT_PRESETS: readonly TExportPreset[] = [
|
||||
YOUTUBE_SHORTS_EXPORT_PRESET,
|
||||
];
|
||||
|
||||
export function getExportPresetById({
|
||||
id,
|
||||
}: {
|
||||
id: TExportPresetId;
|
||||
}): TExportPreset | undefined {
|
||||
return EXPORT_PRESETS.find((p) => p.id === id);
|
||||
}
|
||||
|
||||
export interface ExportResult {
|
||||
success: boolean;
|
||||
buffer?: ArrayBuffer;
|
||||
|
|
|
|||
Loading…
Reference in New Issue