Skip to content

feat: add ffmpeg encoding profiles for 'fast' export - #48

Open
PlkMarudny wants to merge 5 commits into
ronak-create:mainfrom
PlkMarudny:export-profiles
Open

feat: add ffmpeg encoding profiles for 'fast' export#48
PlkMarudny wants to merge 5 commits into
ronak-create:mainfrom
PlkMarudny:export-profiles

Conversation

@PlkMarudny

@PlkMarudny PlkMarudny commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Add encoding presets for ffmpeg

image

Add ffmpeg export profiles that live in encoding-profiles.json. During 'fast' export a profile can be set. The existing profile is called Delivery · H.264 balanced, three other profiles are added.

// The definition of a profile:

"delivery": {
      "label": "Delivery · H.264 balanced",
      "description": "Default export — good quality and compatibility.",
      "jpegQuality": 0.95,
      "encode": { "hideBanner": true, "loglevel": "info", "stats": true },
      "video": { "codec": "libx264", "preset": "fast", "crf": 18, "pixFmt": "yuv420p" },
      "audio": { "codec": "aac", "bitrate": "192k" },
      "mux": { "movflags": "+faststart" }
    },

Type of change

  • Bug fix
  • New feature (rendering / API)
  • Docs
  • Refactor / internal

How was it verified?

  • node --check server.js && node --check app.js && node --check mcp-server.js passes
  • Opened the editor and confirmed the change in preview
  • Confirmed the change in an export (fast or realtime), if it affects rendering
  • Updated CLAUDE.md / README.md if the schema, props, or API changed

Checklist

  • No new runtime dependencies added
  • Preview and export render identically (single compositor)
  • Commits are focused and messages are descriptive

Summary by CodeRabbit

  • New Features

    • Added selectable export encoding profiles (Draft, Delivery, High Quality, Broadcast) with per-profile JPEG compression and output format.
    • Export setup now includes an “Encoding profile” selector with dynamic description/hints when the engine is enabled.
    • Export API now lists available profiles and supports selecting a profile when starting an export; outputs use the chosen profile’s settings.
    • Projects can persist a preferred profile, and status/tools expose the active export profile information.
  • Documentation

    • Expanded export/profile documentation to cover profile configuration, validation behavior, and “effective profile” priority rules.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds configurable ffmpeg encoding profiles with JSON-backed validation, browser and project selection, REST and MCP discovery, and profile-specific single-pass video encoding.

Changes

Encoding profile support

Layer / File(s) Summary
Profile definition and ffmpeg argument generation
encode-profiles.js, encoding-profiles.json
Built-in and JSON-defined profiles are normalized, cached, resolved, listed, converted into ffmpeg arguments, and validated with synthetic dry runs.
Profile-aware export API and execution
server.js
The server exposes profile listings, validates profile IDs, watches configuration changes, and applies selected profiles through a single-pass ffmpeg export.
Browser profile selection and persistence
app.js, index.html, style.css
The export dialog loads profile metadata, persists project or browser selections, and sends profile-specific JPEG quality and export options.
MCP profile controls and documented contracts
mcp-server.js, CLAUDE.md
MCP tools expose profile listings and status, project patching validates profiles, and documentation describes configuration, selection priority, and REST behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.04% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding FFmpeg encoding profiles for fast export.
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.
✨ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 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 `@encode-profiles.js`:
- Around line 134-156: Update validateMux to validate movflags against the
profile’s permitted pattern/character set before it reaches buildMuxArgs,
correcting invalid values and calling warn for both invalid types and invalid
strings. When format and extension conflict, correct one value to the other’s
supported container/extension mapping and report the correction instead of only
warning, keeping exportContainer and exportOutputExtension aligned. Preserve
explicit clearing and fallback behavior for valid inputs.
- Around line 318-336: Update buildVideoEncodeArgs so the v.x264opts argument is
appended only when v.codec is "libx264", while retaining the existing preset,
CRF, and other codec-specific options for both libx264 and libx265.

In `@mcp-server.js`:
- Around line 323-328: Update encodeProfile validation in the setProject
handling loop to require a non-empty string, rejecting arrays, empty strings,
and other non-string values before persistence or dispatch; pass the original
validated value directly to resolveProfile instead of coercing with String().
Apply the same non-empty-string validation to the profile argument in the
related profile-listing path so an empty profile is not treated as omitted and
does not return the full list.
- Around line 175-183: Update the export-profile reporting logic around
loadEncodeProfiles so an explicitly set project encodeProfile with no matching
cfg.profiles entry reports the invalid profile value and its fallback to
cfg.default separately. Preserve the existing server-default message when no
project profile is configured, and keep valid profile reporting unchanged.
🪄 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 Plus

Run ID: cab1edfc-9160-4e48-8c5a-175bbaf24416

📥 Commits

Reviewing files that changed from the base of the PR and between 6ec153e and 4745884.

📒 Files selected for processing (8)
  • CLAUDE.md
  • app.js
  • encode-profiles.js
  • encoding-profiles.json
  • index.html
  • mcp-server.js
  • server.js
  • style.css

Comment thread encode-profiles.js Outdated
Comment on lines +134 to +156
function validateMux(m, fallback, warn) {
const given = m && typeof m === "object" ? m : {};
const out = { ...fallback, ...given };
// an explicit null/"" clears an inherited value (e.g. drop +faststart)
if ("movflags" in given && (given.movflags === null || given.movflags === "")) delete out.movflags;
else if (out.movflags != null && typeof out.movflags !== "string") out.movflags = fallback.movflags;
if (out.format) {
const fmt = MUX_FORMAT_ALIAS[String(out.format).toLowerCase()];
if (fmt) out.format = fmt;
else { delete out.format; warn(`mux.format "${given.format}" unknown — using mp4`); }
}
if (out.extension) {
const raw = String(out.extension).toLowerCase();
const ext = raw.startsWith(".") ? raw : `.${raw}`;
if (OUT_EXT.has(ext)) out.extension = ext;
else { delete out.extension; warn(`mux.extension "${given.extension}" unsupported`); }
}
// a bare extension implies the container; keeps -f and the filename in step
if (!out.format && out.extension) out.format = EXT_FORMAT[out.extension];
if (out.format && out.extension && EXT_FORMAT[out.extension] !== out.format)
warn(`mux.extension ${out.extension} does not match mux.format ${out.format}`);
return out;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

validateMux leaves movflags unvalidated and only warns (doesn't correct) on a format/extension mismatch.

Three gaps relative to the profile-validation contract:

  • movflags (a string) is never checked against any allowed pattern/character set, unlike every other field in this file (codec, preset, pixFmt, tune, profile, vf, x264opts all go through an allow-list or regex). Any string set in the JSON flows straight into -movflags in buildMuxArgs.
  • Line 139's type-mismatch correction (out.movflags = fallback.movflags) silently falls back without calling warn(), unlike every other correction in this file.
  • Lines 153-154 only warn() when mux.extension and mux.format disagree but don't correct it — exportContainer prefers format while exportOutputExtension prefers extension, so a profile with e.g. format:"mov", extension:".mkv" would mux as MOV but write the file with a .mkv extension.

As per coding guidelines, **encoding-profiles.json**: "Invalid encoding-profile values must be corrected and reported rather than silently applied."

🛡️ Proposed fix sketch
 function validateMux(m, fallback, warn) {
   const given = m && typeof m === "object" ? m : {};
   const out = { ...fallback, ...given };
   if ("movflags" in given && (given.movflags === null || given.movflags === "")) delete out.movflags;
-  else if (out.movflags != null && typeof out.movflags !== "string") out.movflags = fallback.movflags;
+  else if (out.movflags != null && typeof out.movflags !== "string") {
+    warn(`mux.movflags "${out.movflags}" ignored — must be a string`);
+    out.movflags = fallback.movflags;
+  } else if (out.movflags != null && !SAFE_FFMPEG_STR.test(out.movflags)) {
+    warn(`mux.movflags "${out.movflags}" contains disallowed characters — using ${fallback.movflags}`);
+    out.movflags = fallback.movflags;
+  }
   ...
-  if (out.format && out.extension && EXT_FORMAT[out.extension] !== out.format)
-    warn(`mux.extension ${out.extension} does not match mux.format ${out.format}`);
+  if (out.format && out.extension && EXT_FORMAT[out.extension] !== out.format) {
+    warn(`mux.extension ${out.extension} does not match mux.format ${out.format} — using ${out.format}`);
+    out.extension = FORMAT_EXT[out.format];
+  }
🤖 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 `@encode-profiles.js` around lines 134 - 156, Update validateMux to validate
movflags against the profile’s permitted pattern/character set before it reaches
buildMuxArgs, correcting invalid values and calling warn for both invalid types
and invalid strings. When format and extension conflict, correct one value to
the other’s supported container/extension mapping and report the correction
instead of only warning, keeping exportContainer and exportOutputExtension
aligned. Preserve explicit clearing and fallback behavior for valid inputs.

Source: Coding guidelines

Comment thread encode-profiles.js Outdated
Comment thread mcp-server.js
Comment thread mcp-server.js
Comment on lines +323 to +328
const allowed = ["name", "width", "height", "fps", "background", "markers", "disabledTracks", "encodeProfile"];
for (const [k, v] of Object.entries(op.set || {})) {
if (!allowed.includes(k)) throw new Error(`setProject: '${k}' not settable (allowed: ${allowed.join(", ")})`);
if (k === "encodeProfile" && v != null) {
resolveProfile(String(v)); // validate id exists
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject non-string and empty profile ids before persisting or dispatching.

String(v) is not strict validation: encodeProfile: "" resolves to the default and is stored, while ["delivery"] resolves successfully but stores an array. Separately, an empty profile argument is treated as if it were omitted and returns the full list. Enforce a non-empty string and pass that exact value to resolveProfile.

Suggested fix
             for (const [k, v] of Object.entries(op.set || {})) {
               if (!allowed.includes(k)) throw new Error(`setProject: '${k}' not settable (allowed: ${allowed.join(", ")})`);
               if (k === "encodeProfile" && v != null) {
-                resolveProfile(String(v)); // validate id exists
+                if (typeof v !== "string" || !v)
+                  throw new Error("setProject: encodeProfile must be a non-empty profile id");
+                resolveProfile(v);
               }
     case "fablecut_encode_profiles": {
-      if (args.profile) {
-        const p = resolveProfile(String(args.profile));
+      if (Object.prototype.hasOwnProperty.call(args || {}, "profile")) {
+        if (typeof args.profile !== "string" || !args.profile)
+          throw new Error("profile must be a non-empty profile id");
+        const p = resolveProfile(args.profile);

Also applies to: 461-464

🤖 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 `@mcp-server.js` around lines 323 - 328, Update encodeProfile validation in the
setProject handling loop to require a non-empty string, rejecting arrays, empty
strings, and other non-string values before persistence or dispatch; pass the
original validated value directly to resolveProfile instead of coercing with
String(). Apply the same non-empty-string validation to the profile argument in
the related profile-listing path so an empty profile is not treated as omitted
and does not return the full list.

= added 4 commits July 29, 2026 10:37
Ignore all the validation of parameters and the baroque processing - if someone is adding a ffmpeg profile, he knows what he is doing

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 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 `@CLAUDE.md`:
- Around line 570-572: Label the Markdown code fence containing the ffmpeg
command as shell by changing the opening fence to ```sh, while leaving the
command content unchanged.
- Around line 601-602: Update the MCP documentation entry for fablecut_status to
describe it as showing the MCP-visible project/default profile, not the
effective profile. Keep the fablecut_encode_profiles descriptions unchanged.

In `@encode-profiles.js`:
- Around line 80-82: Update the profile lookup logic in the export flow and the
profiles configuration handling to verify that IDs such as defaultId and pid are
own properties of the profiles object before using their values. Preserve normal
normalized-profile behavior, but treat inherited-property matches as missing so
profile.args is never read from an unassociated entry.

In `@encoding-profiles.json`:
- Around line 42-49: Update both the broadcast1080i50 and prores422 encoding
profiles to explicitly include “-movflags”, “+faststart” immediately before
“-shortest” in their args arrays, preserving the existing required flags and
ordering otherwise.
🪄 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 Plus

Run ID: 4b1cb70c-c287-4da6-a2d0-afe339d09991

📥 Commits

Reviewing files that changed from the base of the PR and between 7bb795a and c980a5a.

📒 Files selected for processing (6)
  • CLAUDE.md
  • app.js
  • encode-profiles.js
  • encoding-profiles.json
  • mcp-server.js
  • server.js
🚧 Files skipped from review as they are similar to previous changes (3)
  • mcp-server.js
  • app.js
  • server.js

Comment thread CLAUDE.md
Comment on lines +570 to +572
```
ffmpeg -y -f image2pipe -framerate <fps> -i - [-i audio.wav] <args…> exports/<name><extension>
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Label the command fence as shell.

The fence violates MD040. Use ```sh for the ffmpeg command example.

🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 570-570: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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 `@CLAUDE.md` around lines 570 - 572, Label the Markdown code fence containing
the ffmpeg command as shell by changing the opening fence to ```sh, while
leaving the command content unchanged.

Source: Linters/SAST tools

Comment thread CLAUDE.md
Comment on lines +601 to +602
**MCP:** `fablecut_encode_profiles` lists profiles; `{detail:true}` includes each `args`
array; `{profile:"hq"}` returns one profile. `fablecut_status` shows the effective profile.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not describe fablecut_status as showing the effective profile.

The status implementation only reads project.encodeProfile or the file default; it cannot see the higher-priority browser dialog or localStorage choice documented at lines 596-599. Describe it as the MCP-visible project/default profile instead.

🤖 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 `@CLAUDE.md` around lines 601 - 602, Update the MCP documentation entry for
fablecut_status to describe it as showing the MCP-visible project/default
profile, not the effective profile. Keep the fablecut_encode_profiles
descriptions unchanged.

Comment thread encode-profiles.js
Comment on lines +80 to +82
const profiles = {};
if (file?.profiles && typeof file.profiles === "object")
for (const [id, raw] of Object.entries(file.profiles)) profiles[id] = normalizeProfile(id, raw);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== available files =="
git ls-files | rg '(^|/)encode-profiles\.js$|profiles|default|normalizeProfile' || true

echo
echo "== file outline/size =="
if [ -f encode-profiles.js ]; then
  wc -l encode-profiles.js
  ast-grep outline encode-profiles.js || true
fi

echo
echo "== relevant references =="
rg -n "profiles|normalizeProfile|default|constructor|__proto__|toString" encode-profiles.js 2>/dev/null || true

echo
echo "== relevant code section =="
if [ -f encode-profiles.js ]; then
  sed -n '1,180p' encode-profiles.js | cat -n
fi

Repository: ronak-create/FableCut

Length of output: 11046


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import json

# Behavioral probe modeled after in-memory plain-object profile lookup.
def current_profiles(file):
    profiles = {}
    if file.get("profiles") and isinstance(file["profiles"], dict):
        for id_, raw in file["profiles"].items():
            profiles[id_] = {"args": "normalized"}
    return profiles

cases = [
    {"profiles": {"default": "toString"}},
    {"profiles": {"default": "constructor"}},
    {"profiles": {"default": "__proto__", "__proto__": "not a profile"}},
    {"profiles": {"default": "valueOf"}},
]

for file in cases:
    import copy
    p = copy.deepcopy(file)
    p["profiles"]["default"] = p["profiles"]["default"]  # no change; probe lookup only.
    pro = current_profiles(file)
    default = pro.get("default")
    try:
        print("case=", file, "default_lookup_type=", type(default).__name__, "default_lookup_is_function=", callable(default), "default_lookup_args=", getattr(default, "__name__", None))
    except Exception as exc:
        print("case=", file, "default_lookup_type=ERROR", str(exc))

print()
print("JSON parser behavior for constructor/toString/__proto__:")
samples = [
    '{"profiles": {"default": "constructor"}}',
    '{"profiles": {"default": "toString"}}',
    '{"profiles": {"default": "__proto__"}}',
    '{"profiles": {"default": "valueOf"}}',
    '{"profiles": {"default": "valueOf", "valueOf": "not a profile"}}',
]
for raw in samples:
    data = json.loads(raw)
    p = data.get("profiles")
    print(raw, ">", p.get("default"), "type", type(p.get("default")).__name__, "hasOwnProperty default", ({}).__class__.__name__ if "default" in p else "missing")
    print("  default in p:", "default" in p)
PY

Repository: ronak-create/FableCut

Length of output: 1337


Keep default IDs safe from inherited object properties.

profiles[defaultId] and cfg.profiles[pid] use plain-object lookup, so a user-supplied profile ID such as toString, constructor, hasOwnProperty, or valueOf can satisfy the lookup without an associated normalized profile. This can pass missing/incorrect profile metadata through export, since profile.args is assumed to exist.

Proposed fix
-  const profiles = {};
+  const profiles = Object.create(null);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const profiles = {};
if (file?.profiles && typeof file.profiles === "object")
for (const [id, raw] of Object.entries(file.profiles)) profiles[id] = normalizeProfile(id, raw);
const profiles = Object.create(null);
if (file?.profiles && typeof file.profiles === "object")
for (const [id, raw] of Object.entries(file.profiles)) profiles[id] = normalizeProfile(id, raw);
🤖 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 `@encode-profiles.js` around lines 80 - 82, Update the profile lookup logic in
the export flow and the profiles configuration handling to verify that IDs such
as defaultId and pid are own properties of the profiles object before using
their values. Preserve normal normalized-profile behavior, but treat
inherited-property matches as missing so profile.args is never read from an
unassociated entry.

Comment thread encoding-profiles.json
Comment on lines +42 to +49
"args": [
"-c:v", "libx264", "-preset", "veryfast", "-crf", "18", "-pix_fmt", "yuv420p",
"-g", "10", "-maxrate", "60M", "-bufsize", "70M",
"-x264opts", "open_gop=0:interlaced=1:me=hex:subme=2:cabac=1:no-deblock:analyse=all:8x8dct=1:bframes=2:weightp=0:sliced-threads:level=41",
"-vf", "scale=1920:1080:interl=-1,fps=50,tinterlace=interleave_top:bypass_il",
"-c:a", "aac", "-b:a", "384k", "-ar", "48000", "-strict", "-2",
"-shortest"
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Configure +faststart for both MOV profiles.

broadcast1080i50 and prores422 omit -movflags +faststart, unlike the MP4 profiles. Add it explicitly before -shortest so these exported MOV files retain the configured playback-optimized layout. As per coding guidelines, encoding profiles must explicitly configure required flags such as +faststart, -shortest, and pixel format.

Also applies to: 56-59

🤖 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 `@encoding-profiles.json` around lines 42 - 49, Update both the
broadcast1080i50 and prores422 encoding profiles to explicitly include
“-movflags”, “+faststart” immediately before “-shortest” in their args arrays,
preserving the existing required flags and ordering otherwise.

Source: Coding guidelines

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