Skip to content

feat(playback): restart from beginning when play pressed at end of timeline (Fixes #536)#542

Merged
mazeincoding merged 1 commit into
OpenCut-app:stagingfrom
saiteja-in:feature/playhead-reset
Aug 11, 2025
Merged

feat(playback): restart from beginning when play pressed at end of timeline (Fixes #536)#542
mazeincoding merged 1 commit into
OpenCut-app:stagingfrom
saiteja-in:feature/playhead-reset

Conversation

@saiteja-in
Copy link
Copy Markdown
Contributor

@saiteja-in saiteja-in commented Aug 11, 2025

Description

When Play (or Space) is pressed while the playhead is at the end of the timeline, playback now restarts from the beginning (time 0) and continues playing.

  • Compute effectiveDuration from the actual timeline content duration (useTimelineStore.getTotalDuration()) with a fallback to the store’s duration.
  • Use the project FPS (default 30) to derive a one-frame offset: frameOffset = 1 / fps.
  • Define endThreshold = max(0, effectiveDuration - frameOffset).
  • If currentTime >= endThreshold, call seek(0) to reset to the beginning.
  • Then set isPlaying to true and start the rAF timer.

Why here

  • toggle() calls play() when resuming. Centralizing this logic in play() ensures pressing Space (or clicking the button) restarts from the beginning after reaching the end.
  • seek(0) emits the existing playback-seek event, keeping media elements in sync.
  • TimelinePlayhead already derives position from currentTime, so it reflects the jump to 0 automatically.

Fixes #536

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update
  • Performance improvement
  • Code refactoring
  • Tests

How Has This Been Tested?

  • Seek the playhead to the end of the timeline and press Space or click Play: it seeks to 0 and starts playback.
  • Press Space/Play when not at the end: playback resumes from the current position.
  • Empty timeline or zero duration: behavior unchanged and no errors.
  • Verified with different FPS values (24/30/60) to ensure correct end detection using a one-frame threshold.
  • Verified in both normal and fullscreen preview.

Repro steps:

  • bun dev
  • Open the editor, add media so the timeline has non-zero duration.
  • Drag the playhead to the end and press Space or click Play.
  • Observe: playhead jumps to 0 and playback starts.

Test Configuration:

  • Node version:
  • Browser (if applicable):
  • Operating System:

Screenshots (if applicable)

opencut-q.mp4

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have added screenshots if ui has been changed
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

Additional context

Centralizing the logic in the playback store’s play() ensures consistent behavior regardless of how playback is toggled

Summary by CodeRabbit

  • Bug Fixes
    • Playback now restarts from the beginning when Play is pressed at or near the end, preventing stalls or no-op behavior.
    • End-of-content detection is frame-accurate based on the project’s FPS, reducing one-frame offsets and improving smoothness.
    • Playback duration now aligns with the active timeline’s total length, ensuring consistent behavior after timeline edits and avoiding mismatches.

@vercel
Copy link
Copy Markdown

vercel Bot commented Aug 11, 2025

@saiteja-in is attempting to deploy a commit to the OpenCut OSS Team on Vercel.

A member of the Team first needs to authorize it.

@netlify
Copy link
Copy Markdown

netlify Bot commented Aug 11, 2025

👷 Deploy request for appcut pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit 03e5c15

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Aug 11, 2025

Walkthrough

Updated play() in playback-store to compute effective timeline duration using timeline store and project FPS, and if current time is at or beyond end minus one frame, it seeks to 0 before starting playback, then sets isPlaying and starts the timer.

Changes

Cohort / File(s) Summary
Playback logic update
apps/web/src/stores/playback-store.ts
In play(), compute effectiveDuration from timeline total duration or stored duration; derive endThreshold as effectiveDuration - 1/frameRate (FPS default 30); if currentTime ≥ endThreshold, seek to 0; then start playback (isPlaying true, start timer).

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant PlaybackStore
  participant TimelineStore
  participant Project as Project/FPS

  User->>PlaybackStore: play()
  PlaybackStore->>TimelineStore: getTotalDuration()
  PlaybackStore->>Project: getActiveFPS() (default 30)
  PlaybackStore->>PlaybackStore: compute endThreshold = duration - 1/FPS
  alt currentTime ≥ endThreshold
    PlaybackStore->>PlaybackStore: seek(0)
  end
  PlaybackStore->>PlaybackStore: set isPlaying = true
  PlaybackStore->>PlaybackStore: start timer
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Assessment against linked issues

Objective Addressed Explanation
Play from beginning when playhead is at end of timeline (#536)

Poem

A twitch of whiskers, a click—then go!
The playhead hops back, just so.
At timeline’s edge, I boop to start,
One-frame shy, I play my part.
Thump-thump beats the timeline drum—
From zero again, here we come! 🐇🎬

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@saiteja-in
Copy link
Copy Markdown
Contributor Author

@mazeincoding can you review this?

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 (2)
apps/web/src/stores/playback-store.ts (2)

87-92: DRY: extract effectiveDuration/frameOffset helpers to avoid divergence with updateTime

The same concepts (effectiveDuration via timeline store and 1-frame offset by FPS) live here and in updateTime. Extract small helpers to keep behavior consistent and easier to evolve.

Example (outside the store definition):

const getEffectiveDuration = (state: PlaybackStore) => {
  const actual = useTimelineStore.getState().getTotalDuration();
  return actual > 0 ? actual : state.duration;
};

const getFrameOffset = () => {
  const rawFps = useProjectStore.getState().activeProject?.fps;
  return 1 / (typeof rawFps === "number" && Number.isFinite(rawFps) && rawFps > 0 ? rawFps : 30);
};

Then reuse in play() and updateTime to compute thresholds.


98-101: Tests: cover restart-at-end path

Recommend adding a test for pressing play/space at endThreshold that verifies a seek(0) occurs before playback starts (e.g., assert playback-seek event fired with 0, isPlaying true afterwards). I can draft the test quickly if helpful.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 88cb750 and 03e5c15.

📒 Files selected for processing (1)
  • apps/web/src/stores/playback-store.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Don't use TypeScript enums.
Don't export imported variables.
Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions.
Don't use TypeScript namespaces.
Don't use non-null assertions with the ! postfix operator.
Don't use parameter properties in class constructors.
Don't use user-defined types.
Use as const instead of literal types and type annotations.
Use either T[] or Array<T> consistently.
Initialize each enum member value explicitly.
Use export type for types.
Use import type for types.
Make sure all enum members are literal values.
Don't use TypeScript const enum.
Don't declare empty interfaces.
Don't let variables evolve into any type through reassignments.
Don't use the any type.
Don't misuse the non-null assertion operator (!) in TypeScript files.
Don't use implicit any type on variable declarations.
Don't merge interfaces and classes unsafely.
Don't use overload signatures that aren't next to each other.
Use the namespace keyword instead of the module keyword to declare TypeScript namespaces.
Don't use empty type parameters in type aliases and interfaces.
Don't use any or unknown as type constraints.
Don't use the TypeScript directive @ts-ignore.
Use consistent accessibility modifiers on class properties and methods.
Use function types instead of object types with call signatures.
Don't use void type outside of generic or return types.

Files:

  • apps/web/src/stores/playback-store.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.github/copilot-instructions.md)

**/*.{js,jsx,ts,tsx}: Don't use the return value of React.render.
Don't use consecutive spaces in regular expression literals.
Don't use the arguments object.
Don't use primitive type aliases or misleading types.
Don't use the comma operator.
Don't write functions that exceed a given Cognitive Complexity score.
Don't use unnecessary boolean casts.
Don't use unnecessary callbacks with flatMap.
Use for...of statements instead of Array.forEach.
Don't create classes that only have static members (like a static namespace).
Don't use this and super in static contexts.
Don't use unnecessary catch clauses.
Don't use unnecessary constructors.
Don't use unnecessary continue statements.
Don't export empty modules that don't change anything.
Don't use unnecessary escape sequences in regular expression literals.
Don't use unnecessary labels.
Don't use unnecessary nested block statements.
Don't rename imports, exports, and destructured assignments to the same name.
Don't use unnecessary string or template literal concatenation.
Don't use String.raw in template literals when there are no escape sequences.
Don't use useless case statements in switch statements.
Don't use ternary operators when simpler alternatives exist.
Don't use useless this aliasing.
Don't initialize variables to undefined.
Don't use the void operators (they're not familiar).
Use arrow functions instead of function expressions.
Use Date.now() to get milliseconds since the Unix Epoch.
Use .flatMap() instead of map().flat() when possible.
Use literal property access instead of computed property access.
Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work.
Use concise optional chaining instead of chained logical expressions.
Use regular expression literals instead of the RegExp constructor when possible.
Don't use number literal object member names that aren't base 10 or use underscore separators.
Remove redundant terms from logical expressions.
Use while loops instead of...

Files:

  • apps/web/src/stores/playback-store.ts
🔇 Additional comments (1)
apps/web/src/stores/playback-store.ts (1)

85-101: End-of-timeline restart behavior in play() is correct and aligns with #536

The effectiveDuration + frame-offset threshold check and seek(0) before starting playback achieve the desired UX. Good centralization in play().

Comment thread apps/web/src/stores/playback-store.ts
@enkei64
Copy link
Copy Markdown
Contributor

enkei64 commented Aug 11, 2025

@mazeincoding can you review this?

merge into staging branch not main

@saiteja-in saiteja-in changed the base branch from main to staging August 11, 2025 16:40
@mazeincoding mazeincoding merged commit 3807e14 into OpenCut-app:staging Aug 11, 2025
1 of 5 checks passed
@mazeincoding
Copy link
Copy Markdown
Collaborator

lfg!

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.

when you click play and the playhead is at the end of the timeline, play from beginning

3 participants