Skip to content

feat: add YouTube Shorts export preset#806

Closed
kseungyong wants to merge 2 commits into
OpenCut-app:mainfrom
kseungyong:feat/youtube-shorts-export-preset
Closed

feat: add YouTube Shorts export preset#806
kseungyong wants to merge 2 commits into
OpenCut-app:mainfrom
kseungyong:feat/youtube-shorts-export-preset

Conversation

@kseungyong
Copy link
Copy Markdown

@kseungyong kseungyong commented May 18, 2026

Summary

Introduces a small export-preset registry and the first preset — `youtube-shorts` — so a project with `projectType === "shorts"` can resolve sensible export defaults without the UI hard-coding them.

Changes

  • `export/index.ts` — Add `TExportPreset`, `EXPORT_PRESETS` registry, `YOUTUBE_SHORTS_EXPORT_PRESET`, `getExportPresetById`. `"custom"` is included in `EXPORT_PRESET_IDS` to represent absence of a named-preset selection, but has no registry entry (it's a UI state, not a stored preset).
  • `export/defaults.ts` — Add `getDefaultExportOptions({projectType})` helper. Preserve existing `DEFAULT_EXPORT_OPTIONS` export.
  • `export/tests/presets.test.ts` — Unit coverage (11 tests).

Why preset-level (not bitrate-level) in this PR

YouTube Shorts encoding recommends 8–12 Mbps for 1080p30 and 12–20 Mbps for 1080p60. The mapping from `quality: "very_high"` to an actual bitrate lives in the `mediabunny` package (imported in `scene-exporter.ts` as `QUALITY_VERY_HIGH`), not in `export/` TS. This PR sets the right `ExportOptions` shape so the existing quality→bitrate path produces near-target rates. A follow-up PR can introduce explicit bitrate overrides if measured output deviates from YouTube's recommendation.

Depends On

#804 (the `projectType` field PR) — `getDefaultExportOptions` switches on `TProjectType`. The base of this branch is `feat/project-type-field`, so PR2's commit is included. After #804 merges to main, this PR can rebase onto main.

Test Plan

  • `bun test apps/web/src/export/tests/presets.test.ts` — 11 tests pass
  • `bunx tsc --noEmit` — no new errors in modified files
  • Lint clean for modified files
  • Existing `DEFAULT_EXPORT_OPTIONS` consumers untouched

Summary by CodeRabbit

  • New Features

    • Specify a project type (standard or shorts) when creating projects
    • Built-in YouTube Shorts export preset with optimized MP4/very_high + audio
  • Behavior Changes

    • Default export options now adapt to the project's type (shorts → Shorts preset; otherwise → standard defaults)
  • Tests

    • Added test coverage for export presets and project-type defaulting behavior

Review Change Stack

Adds optional projectType discriminator on TProjectMetadata so downstream features (preset pickers, export defaults, UI affordances) can branch on intent without inferring from canvas dimensions.

- New const PROJECT_TYPES and type TProjectType
- TProjectMetadata.projectType (optional for back-compat with stored projects)
- ProjectManager.createNewProject accepts optional projectType, defaults to 'standard'
- Unit tests for PROJECT_TYPES invariants
@vercel
Copy link
Copy Markdown

vercel Bot commented May 18, 2026

@kseungyong is attempting to deploy a commit to the OpenCut OSS Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

📝 Walkthrough

Walkthrough

Adds a TProjectType system ("standard" | "shorts"), export presets with a YouTube Shorts preset, a getDefaultExportOptions selector, tests for types and presets, ProjectManager storage of projectType, and Export UI defaulting to project-specific export options.

Changes

Project Type and Export Presets

Layer / File(s) Summary
Project type system definition
apps/web/src/project/types.ts, apps/web/src/project/__tests__/project-type.test.ts
PROJECT_TYPES and TProjectType added; TProjectMetadata gains optional projectType?: TProjectType with back-compat rule; tests cover membership, defaulting, and iteration order.
Export presets and defaults
apps/web/src/export/index.ts, apps/web/src/export/defaults.ts, apps/web/src/export/__tests__/presets.test.ts
Adds export preset types and constants (EXPORT_PRESET_IDS, TExportPresetId, TExportPreset), YOUTUBE_SHORTS_EXPORT_PRESET, EXPORT_PRESETS, getExportPresetById, and getDefaultExportOptions which returns Shorts options for projectType === "shorts"; tests validate preset shape, registry, lookup, and default selection.
Project manager integration
apps/web/src/core/managers/project-manager.ts
ProjectManager.createNewProject now accepts optional projectType?: TProjectType and writes newProject.metadata.projectType = projectType ?? "standard".
Export UI defaulting
apps/web/src/components/editor/export-button.tsx
ExportPopover now calls getDefaultExportOptions({ projectType: activeProject?.metadata?.projectType }) and initializes format, quality, and shouldIncludeAudio from that result (with includeAudio fallback to true).

Sequence Diagram

sequenceDiagram
  participant User
  participant ExportPopover
  participant ProjectManager
  participant Defaults as getDefaultExportOptions
  participant Presets as EXPORT_PRESETS
  User->>ExportPopover: open export UI (activeProject)
  ExportPopover->>ProjectManager: read activeProject.metadata.projectType
  ExportPopover->>Defaults: getDefaultExportOptions({ projectType })
  Defaults->>Presets: select preset by id ("shorts" -> YOUTUBE_SHORTS_EXPORT_PRESET.options)
  Presets-->>Defaults: preset ExportOptions
  Defaults-->>ExportPopover: return ExportOptions
  ExportPopover-->>User: initialize UI with format/quality/includeAudio
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

A rabbit hops through code so spry,
"shorts" and "standard" now live nearby,
Presets tuned for every shot,
Defaults set, no detail forgot,
Hooray — exports ready to fly! 🐰🎬

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description does not follow the repository's template, which requires acknowledgment of the project's policy against PRs except critical bugs and maintainer approval for features. Add the required template section with checkboxes and acknowledgment that this is a feature PR, or open an issue for discussion first per the template policy.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add YouTube Shorts export preset' clearly and concisely summarizes the main change — introducing export presets with YouTube Shorts as the first preset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/web/src/export/__tests__/presets.test.ts (1)

56-70: ⚡ Quick win

Add a regression test for reference isolation in defaults.

These tests assert value equality, but not that each call returns a fresh object. Add not.toBe(...) assertions to prevent shared-mutation regressions.

Suggested test addition
 describe("getDefaultExportOptions", () => {
+	test("returns fresh objects (no shared references)", () => {
+		const shortsA = getDefaultExportOptions({ projectType: "shorts" });
+		const shortsB = getDefaultExportOptions({ projectType: "shorts" });
+		expect(shortsA).not.toBe(shortsB);
+		expect(shortsA).not.toBe(YOUTUBE_SHORTS_EXPORT_PRESET.options);
+
+		const standardA = getDefaultExportOptions({ projectType: "standard" });
+		const standardB = getDefaultExportOptions({ projectType: "standard" });
+		expect(standardA).not.toBe(standardB);
+		expect(standardA).not.toBe(DEFAULT_EXPORT_OPTIONS);
+	});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/export/__tests__/presets.test.ts` around lines 56 - 70, The
tests for getDefaultExportOptions currently only check deep equality and miss
verifying that each call returns a fresh object; add identity assertions using
not.toBe to each test so they assert that the returned object is not the same
reference as the presets/constants (e.g., in the "shorts" case assert opts
not.toBe(YOUTUBE_SHORTS_EXPORT_PRESET.options) and for "standard" and undefined
cases assert opts not.toBe(DEFAULT_EXPORT_OPTIONS)), ensuring
getDefaultExportOptions returns isolated copies rather than shared references.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/export/defaults.ts`:
- Around line 22-25: The function currently returns shared singleton objects
(YOUTUBE_SHORTS_EXPORT_PRESET.options and DEFAULT_EXPORT_OPTIONS), which allows
callers to mutate global presets; change the return to provide fresh copies
instead—e.g., return a cloned object instead of the original reference (use
structuredClone(YOUTUBE_SHORTS_EXPORT_PRESET.options) or
JSON.parse(JSON.stringify(...)) or a shallow copy like
{...DEFAULT_EXPORT_OPTIONS} depending on nested structure) so
DEFAULT_EXPORT_OPTIONS and YOUTUBE_SHORTS_EXPORT_PRESET.options are not mutated
by callers.

---

Nitpick comments:
In `@apps/web/src/export/__tests__/presets.test.ts`:
- Around line 56-70: The tests for getDefaultExportOptions currently only check
deep equality and miss verifying that each call returns a fresh object; add
identity assertions using not.toBe to each test so they assert that the returned
object is not the same reference as the presets/constants (e.g., in the "shorts"
case assert opts not.toBe(YOUTUBE_SHORTS_EXPORT_PRESET.options) and for
"standard" and undefined cases assert opts not.toBe(DEFAULT_EXPORT_OPTIONS)),
ensuring getDefaultExportOptions returns isolated copies rather than shared
references.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 121393d9-299a-4044-be12-4e0f14323184

📥 Commits

Reviewing files that changed from the base of the PR and between 6fdb155 and ad0185b.

📒 Files selected for processing (6)
  • apps/web/src/core/managers/project-manager.ts
  • apps/web/src/export/__tests__/presets.test.ts
  • apps/web/src/export/defaults.ts
  • apps/web/src/export/index.ts
  • apps/web/src/project/__tests__/project-type.test.ts
  • apps/web/src/project/types.ts

Comment thread apps/web/src/export/defaults.ts Outdated
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
@kseungyong kseungyong force-pushed the feat/youtube-shorts-export-preset branch from ad0185b to 5913abb Compare May 18, 2026 09:10
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/components/editor/export-button.tsx`:
- Around line 104-111: The export state (format, quality, shouldIncludeAudio) is
initialized from getDefaultExportOptions once and becomes stale when switching
projects because ExportPopover stays mounted; force a remount on project change
by adding a stable key prop tied to the active project (e.g., use
activeProject?.metadata?.projectType or activeProject?.id) on the ExportPopover
(or the parent component that renders the export-button UI) so the component
fully reinitializes and re-runs the useState initializers; alternatively, if you
prefer not to remount, implement a useEffect inside export-button.tsx that
watches activeProject (or activeProject.metadata.projectType) and resets
setFormat, setQuality, and setShouldIncludeAudio from getDefaultExportOptions
when it changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cfa94bd4-0b27-4f40-a727-93b3130184ed

📥 Commits

Reviewing files that changed from the base of the PR and between ad0185b and 5913abb.

📒 Files selected for processing (4)
  • apps/web/src/components/editor/export-button.tsx
  • apps/web/src/export/__tests__/presets.test.ts
  • apps/web/src/export/defaults.ts
  • apps/web/src/export/index.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/web/src/export/tests/presets.test.ts

Comment on lines +104 to 111
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,
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Export defaults become stale when switching projects without remounting.

useState only consumes initialOptions on the first render. If the user switches to a different active project while ExportPopover stays mounted (line 90 keeps it mounted when hasProject is true), the format/quality/audio states retain values from the previous project instead of reflecting the new project's projectType.

Example: User opens a "shorts" project (gets very_high quality) → closes dialog → switches to "standard" project → reopens dialog → still sees very_high instead of high.

🔑 Recommended fix: Add a key prop to force remount on project change
-			{hasProject && <ExportPopover onOpenChange={setIsExportPopoverOpen} />}
+			{hasProject && <ExportPopover key={activeProject.id} onOpenChange={setIsExportPopoverOpen} />}

Alternatively, replace the useState initialization with useEffect to sync when activeProject changes, though the key-based remount is simpler and ensures full component reset.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/editor/export-button.tsx` around lines 104 - 111, The
export state (format, quality, shouldIncludeAudio) is initialized from
getDefaultExportOptions once and becomes stale when switching projects because
ExportPopover stays mounted; force a remount on project change by adding a
stable key prop tied to the active project (e.g., use
activeProject?.metadata?.projectType or activeProject?.id) on the ExportPopover
(or the parent component that renders the export-button UI) so the component
fully reinitializes and re-runs the useState initializers; alternatively, if you
prefer not to remount, implement a useEffect inside export-button.tsx that
watches activeProject (or activeProject.metadata.projectType) and resets
setFormat, setQuality, and setShouldIncludeAudio from getDefaultExportOptions
when it changes.

@kseungyong
Copy link
Copy Markdown
Author

Closing — continuing on a private fork. The branch feat/youtube-shorts-export-preset (commit 5913abb5) remains on https://github.com/kseungyong/OpenCut under MIT. Thanks.

@kseungyong kseungyong closed this May 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant