Skip to content

Add preview playback-speed control (1×/1.5×/2×/4×)#18

Merged
ronak-create merged 1 commit into
ronak-create:mainfrom
ur5fot:feat/preview-speed
Jul 22, 2026
Merged

Add preview playback-speed control (1×/1.5×/2×/4×)#18
ronak-create merged 1 commit into
ronak-create:mainfrom
ur5fot:feat/preview-speed

Conversation

@ur5fot

@ur5fot ur5fot commented Jul 14, 2026

Copy link
Copy Markdown

Adds a preview-only playback-speed control so long timelines can be reviewed faster, without touching clip speed or the export.

What

  • Toolbar toggle in the program monitor cycles 1× → 1.5× → 2× → 4×.
  • Shortcuts: Shift+. faster, Shift+, slower.
  • state.previewRate scales both the play-loop clock and each active clip's playbackRate, so video and audio stay in sync (the drift tolerance scales with it too — no re-seek fighting).

Preview only — export is unaffected

playRate() returns 1 whenever state.exporting is set, so the fast (ffmpeg) render and the realtime MediaRecorder capture both run at real time regardless of the toggle.

Notes

  • No new deps. node --check passes on app.js and server.js.
  • CHANGELOG [1.6.0] + README updated; the help overlay lists the shortcut.
  • Known minor: when a clip's own speed × previewRate exceeds 16 the video re-seeks slightly (the browser clamps playbackRate to 16 while the clock keeps advancing). Preview-only cosmetic, and only reachable above the documented 4× clip-speed max.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a Preview speed control to the monitor toolbar ( 1× / 1.5× / 2× / 4× ) with a tooltip noting it affects preview only.
    • Added J / K / L shortcuts to slow down, toggle play/pause/reset to , and speed up.
  • Bug Fixes
    • Preview speed changes are now decoupled from exports—export timing always remains real-time.
  • Documentation
    • Updated README, changelog, and the in-app keyboard shortcuts overlay to describe the preview speed controls and behavior.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e127eee5-ace2-4ae0-a3fc-1bffda9963bc

📥 Commits

Reviewing files that changed from the base of the PR and between aaa2c3b and 71922fc.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • README.md
  • app.js
  • index.html
🚧 Files skipped from review as they are similar to previous changes (2)
  • CHANGELOG.md
  • index.html

📝 Walkthrough

Walkthrough

Preview playback gains selectable 1×, 1.5×, 2×, and 4× rates through a toolbar button and J/K/L shortcuts. Timeline and media synchronization use the selected rate, while exports remain at 1×. Documentation describes the feature.

Changes

Preview playback speed

Layer / File(s) Summary
Preview speed control
app.js, index.html
Adds preview-rate state, supported speed cycling, a toolbar control, and J/K/L shortcuts.
Timeline and media playback
app.js
Applies the selected rate to timeline advancement and media synchronization, while forcing real-time playback during export.
Feature documentation
README.md, CHANGELOG.md, index.html
Documents supported rates, shortcuts, preview-only behavior, and the new release comparison link.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant btnSpeed
  participant cyclePreviewRate
  participant animationLoop
  participant syncMedia
  participant mediaElements
  User->>btnSpeed: click or press J, K, or L
  btnSpeed->>cyclePreviewRate: select preview rate
  cyclePreviewRate->>animationLoop: update state.previewRate
  animationLoop->>syncMedia: advance timeline and synchronize clips
  syncMedia->>mediaElements: apply effective rate and currentTime
  animationLoop->>animationLoop: use 1x during export
Loading

Possibly related PRs

Suggested reviewers: plkmarudny

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main change: adding a preview-only playback-speed control with the listed rates.
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.
✨ 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.

🧹 Nitpick comments (2)
app.js (2)

2765-2765: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use cached DOM element reference.

For consistency with other UI interactions in the app, consider using the els.btnSpeed reference you already cached on line 168 instead of querying the DOM again here.

💡 Proposed refactor
-$("btnSpeed").addEventListener("click", () => cyclePreviewRate(1));
+els.btnSpeed.addEventListener("click", () => cyclePreviewRate(1));
🤖 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 `@app.js` at line 2765, Update the click listener registration for btnSpeed to
use the existing cached els.btnSpeed reference instead of querying the DOM with
$("btnSpeed"). Preserve the current cyclePreviewRate(1) handler behavior.

1576-1580: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Clamp instead of wrap for directional keyboard shortcuts.

While wrapping around is the expected behavior for a single UI toggle button, it can be counter-intuitive when using discrete "faster" and "slower" keyboard commands (e.g., pressing "faster" at 4x shouldn't drop the speed down to 1x). Consider adding a wrap parameter to enforce array bounds when using the hotkeys.

  • app.js#L1576-L1580: update cyclePreviewRate to conditionally apply clamp or modulo based on a wrap flag.
  • app.js#L2819-L2820: pass false when invoking the function from the directional keyboard shortcuts.
💡 Proposed refactor

app.js#L1576-L1580:

-function cyclePreviewRate(dir) {
-  const i = PREVIEW_RATES.indexOf(state.previewRate);
-  const n = ((i < 0 ? 0 : i) + dir + PREVIEW_RATES.length) % PREVIEW_RATES.length;
-  setPreviewRate(PREVIEW_RATES[n]);
+function cyclePreviewRate(dir, wrap = true) {
+  const i = Math.max(0, PREVIEW_RATES.indexOf(state.previewRate));
+  const n = wrap 
+    ? (i + dir + PREVIEW_RATES.length) % PREVIEW_RATES.length
+    : clamp(i + dir, 0, PREVIEW_RATES.length - 1);
+  setPreviewRate(PREVIEW_RATES[n]);
 }

app.js#L2819-L2820:

-  else if (k === ">") cyclePreviewRate(1);   // Shift+.  → faster preview
-  else if (k === "<") cyclePreviewRate(-1);  // Shift+,  → slower preview
+  else if (k === ">") cyclePreviewRate(1, false);   // Shift+.  → faster preview
+  else if (k === "<") cyclePreviewRate(-1, false);  // Shift+,  → slower preview
🤖 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 `@app.js` around lines 1576 - 1580, Update app.js lines 1576-1580 in
cyclePreviewRate to accept a wrap flag and use bounded clamping when wrapping is
disabled, while preserving modulo wrapping by default for the toggle button.
Update app.js lines 2819-2820 to pass false from the directional keyboard
shortcut invocations so faster/slower commands stop at the first and last
PREVIEW_RATES values.
🤖 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.

Nitpick comments:
In `@app.js`:
- Line 2765: Update the click listener registration for btnSpeed to use the
existing cached els.btnSpeed reference instead of querying the DOM with
$("btnSpeed"). Preserve the current cyclePreviewRate(1) handler behavior.
- Around line 1576-1580: Update app.js lines 1576-1580 in cyclePreviewRate to
accept a wrap flag and use bounded clamping when wrapping is disabled, while
preserving modulo wrapping by default for the toggle button. Update app.js lines
2819-2820 to pass false from the directional keyboard shortcut invocations so
faster/slower commands stop at the first and last PREVIEW_RATES values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ac116552-ba8a-4eb4-8035-969b20d7a11d

📥 Commits

Reviewing files that changed from the base of the PR and between f44074d and 2073476.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • README.md
  • app.js
  • index.html

@PlkMarudny

Copy link
Copy Markdown
Contributor

Maybe it would be better to implement the standard JKL shortcuts, with double click on L speeds up, double click on J speeds down, K acts as spacebar now and resets speed to x1 ?

ur5fot added a commit to ur5fot/FableCut that referenced this pull request Jul 14, 2026
Monitor toolbar toggle cycles 1×/1.5×/2×/4×; J/K/L drive it like a video
editor — L faster, J slower, K play and reset to 1× (per @PlkMarudny's
suggestion on ronak-create#18). state.previewRate scales the play-loop clock and each
active clip's playbackRate; playRate() forces 1 during export so fast renders
and the MediaRecorder capture stay real-time. Toolbar click cycles (wraps),
the J/L keys clamp at the ends.

CHANGELOG [1.7.0], README and the help overlay updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SSmZYU7BhVLcmybTWZwWAB
@ur5fot
ur5fot force-pushed the feat/preview-speed branch from 2073476 to 79dbd14 Compare July 14, 2026 18:14

@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: 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 `@index.html`:
- Line 89: Update the btnSpeed tooltip and corresponding help text to accurately
document the J/K/L handlers: K toggles play/pause and resets speed to 1×, while
the first J or L press when paused only starts playback and later presses change
speed during playback. Keep the handler behavior unchanged unless necessary to
make the documented semantics explicit.
🪄 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: 55e86b14-659b-472b-b141-279b1222592d

📥 Commits

Reviewing files that changed from the base of the PR and between 2073476 and 79dbd14.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • README.md
  • app.js
  • index.html
🚧 Files skipped from review as they are similar to previous changes (1)
  • README.md

Comment thread index.html Outdated

@ronak-create ronak-create left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Nice work adopting the J/K/L shuttle keys from the discussion — the implementation is clean: playRate() returning 1 during state.exporting covers both export engines, the drift tolerance scaling with the effective rate avoids re-seek fighting, and the clamp to the browser's playbackRate range is documented honestly. Two things before merge:

  1. Rebase needed#20 just landed and touches the same syncMedia lines (it adds an enabled term to the play condition and rewrites the else branch to zero the gain node). When resolving, keep both: state.playing && enabled && activeAt(...) plus your eff rate. GitHub reports the branch as CONFLICTING against main now.

  2. CHANGELOG heading: the entry declares [1.7.0] - 2026-07-14 with a compare link — cutting a version is a release decision, and v1.7.0 doesn't exist yet. Please move the entry under an ## [Unreleased] heading and drop the 1.7.0 compare link (the added [1.6.0] link is a genuine fix, keep that).

Minor, no action needed: the rate also applies playbackRate to audio elements — modern browsers preserve pitch by default, so this is fine; and keeping previewRate as session-only UI state (not in project.json) is the right call.

Verified node --check passes on the branch head.

@ronak-create

Copy link
Copy Markdown
Owner

I'd like to take this — the preview-only design is right (playRate() forced to 1 during export keeps renders real-time) and the JKL scheme is a good call. Two things before it can land:

  1. It's conflicting with main now — Add luxury title style #30 and Gap closing #31 just merged and the IN/OUT work-area changes landed earlier, so README, CHANGELOG, and the keydown block all need a rebase onto current main.
  2. Please drop the [1.7.0] CHANGELOG entry / version bump — I handle versioning at release time, so the feature entry will get added then.

Happy to merge once it's rebased.

@ur5fot
ur5fot force-pushed the feat/preview-speed branch from 79dbd14 to 95f3e8a Compare July 19, 2026 15:01
ur5fot added a commit to ur5fot/FableCut that referenced this pull request Jul 19, 2026
Monitor toolbar toggle cycles 1×/1.5×/2×/4×; J/K/L drive it like a video
editor — from a stop J/L start playback, then L steps faster and J slower,
K toggles play/pause and resets to 1× (per @PlkMarudny's suggestion on ronak-create#18).
state.previewRate scales the play-loop clock and each active clip's
playbackRate; playRate() forces 1 during export so fast renders and the
MediaRecorder capture stay real-time. Toolbar click cycles (wraps), the J/L
keys clamp at the ends.

CHANGELOG entry filed under [Unreleased]; README and the help overlay updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@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: 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 `@CHANGELOG.md`:
- Line 11: Update the “Preview playback speed” changelog entry to describe J and
L as slowing down or speeding up through the available rates with clamping,
rather than cycling all rates; describe K as toggling playback while resetting
the preview speed to 1×, and preserve the existing toolbar, per-clip speed, and
export behavior details.
🪄 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: 7a340f62-a84d-4ded-bde8-09949fe41011

📥 Commits

Reviewing files that changed from the base of the PR and between 79dbd14 and 95f3e8a.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • README.md
  • app.js
  • index.html
🚧 Files skipped from review as they are similar to previous changes (2)
  • README.md
  • index.html

Comment thread CHANGELOG.md Outdated
@ur5fot
ur5fot force-pushed the feat/preview-speed branch from 95f3e8a to aaa2c3b Compare July 19, 2026 16:17
ur5fot added a commit to ur5fot/FableCut that referenced this pull request Jul 19, 2026
Monitor toolbar toggle cycles 1×/1.5×/2×/4×; J/K/L drive it like a video
editor — from a stop J/L start playback, then L steps faster and J slower,
K toggles play/pause and resets to 1× (per @PlkMarudny's suggestion on ronak-create#18).
state.previewRate scales the play-loop clock and each active clip's
playbackRate; playRate() forces 1 during export so fast renders and the
MediaRecorder capture stay real-time. Toolbar click cycles (wraps), the J/L
keys clamp at the ends.

CHANGELOG entry filed under [Unreleased]; README and the help overlay updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app.js (1)

2036-2036: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Escape user-supplied text to prevent UI breakage and XSS.

The p.text property is sourced from the project state, which can contain arbitrary user input. Inserting it directly into innerHTML without escaping < and > allows a user (or a loaded malicious project file) to prematurely close the <textarea> by including </textarea>. This will break the inspector UI and enables Cross-Site Scripting (XSS) if followed by malicious HTML.

🔒️ Proposed fix
-      ${row("Content", `<textarea data-k="text">${p.text}</textarea>`, "", "text")}
+      ${row("Content", `<textarea data-k="text">${String(p.text || "").replace(/&/g, "&amp;").replace(/</g, "&lt;")}</textarea>`, "", "text")}

(Using String(p.text || "") also ensures it handles undefined gracefully without displaying the word "undefined").

🤖 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 `@app.js` at line 2036, Update the Content textarea construction in the row
call to HTML-escape the user-supplied p.text value, converting missing values to
an empty string before escaping. Ensure both angle brackets and any other
HTML-significant characters are escaped so arbitrary text cannot close the
textarea or inject markup.
🧹 Nitpick comments (1)
app.js (1)

3725-3730: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Prevent default browser actions for J and L shortcuts.

While e.preventDefault() is correctly called for K, it is omitted for J and L. For consistency and to guarantee no unintended browser features are triggered (like Firefox's quick search), it is best to prevent the default action for all three shuttle keys.

🛠️ Proposed fix
   else if ((k === "l" || k === "L") && !e.ctrlKey && !e.metaKey && !e.altKey) {
+    e.preventDefault();
     if (!state.playing) play(); else stepPreviewRate(1);  // tap again = faster
   }
   else if ((k === "j" || k === "J") && !e.ctrlKey && !e.metaKey && !e.altKey) {
+    e.preventDefault();
     if (!state.playing) play(); else stepPreviewRate(-1); // tap again = slower
   }
🤖 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 `@app.js` around lines 3725 - 3730, Update the J/L keyboard shortcut branches
in the key handler alongside the existing K handling to call e.preventDefault()
before invoking play() or stepPreviewRate(). Preserve the current modifier
checks and playback behavior while preventing browser default actions for both
shuttle keys.
🤖 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.

Outside diff comments:
In `@app.js`:
- Line 2036: Update the Content textarea construction in the row call to
HTML-escape the user-supplied p.text value, converting missing values to an
empty string before escaping. Ensure both angle brackets and any other
HTML-significant characters are escaped so arbitrary text cannot close the
textarea or inject markup.

---

Nitpick comments:
In `@app.js`:
- Around line 3725-3730: Update the J/L keyboard shortcut branches in the key
handler alongside the existing K handling to call e.preventDefault() before
invoking play() or stepPreviewRate(). Preserve the current modifier checks and
playback behavior while preventing browser default actions for both shuttle
keys.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ba96381c-5a89-4a45-9374-4d80473bf4a3

📥 Commits

Reviewing files that changed from the base of the PR and between 95f3e8a and aaa2c3b.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • README.md
  • app.js
  • index.html
🚧 Files skipped from review as they are similar to previous changes (3)
  • index.html
  • CHANGELOG.md
  • README.md

Monitor toolbar toggle cycles 1×/1.5×/2×/4×; J/K/L drive it like a video
editor — from a stop J/L start playback, then L steps faster and J slower,
K toggles play/pause and resets to 1× (per @PlkMarudny's suggestion on ronak-create#18).
state.previewRate scales the play-loop clock and each active clip's
playbackRate; playRate() forces 1 during export so fast renders and the
MediaRecorder capture stay real-time. Toolbar click cycles (wraps), the J/L
keys clamp at the ends.

CHANGELOG entry filed under [Unreleased]; README and the help overlay updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ur5fot
ur5fot force-pushed the feat/preview-speed branch from aaa2c3b to 71922fc Compare July 19, 2026 17:18
@ur5fot

ur5fot commented Jul 19, 2026

Copy link
Copy Markdown
Author

Both points addressed, head is now 71922fc.

1. Rebase — rebased onto 06e9733. The only conflict was index.html: #33 split the shortcuts table into two columns and shortened the descriptions, while my commit added a row to the old single table. Kept the new two-column layout and moved the J/K/L row into the first column, rewritten to match the shorter phrasing.

In syncMedia both changes are in, as you asked — app.js:2298 keeps the enabled term from #20, app.js:2299 adds eff on top:

if (state.playing && enabled && activeAt(c, t)) {
  const eff = clamp(sp * playRate(), 0.0625, 16);

The else branch zeroing the gain node is untouched. git diff upstream/main..HEAD is 49 added lines across 4 files, nothing else of yours is modified.

2. CHANGELOG — entry moved under ## [Unreleased], the [1.7.0] heading and its compare link are gone. The [1.6.0] link stays.

Also picked up a CodeRabbit nit from this round: e.preventDefault() was missing on the J/L branches while K had it. Added to both.

Skipped its other finding — unescaped p.text in the inspector textarea at app.js:2036. It's on main already and this PR doesn't touch that line, so it doesn't belong here. It looks like a real bug though; say the word and I'll open a separate issue.

node --check app.js passes.

@ronak-create
ronak-create merged commit 07891ed into ronak-create:main Jul 22, 2026
1 check passed
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.

3 participants