Skip to content

Add FF/REW tap seeking and independent playback navigation options - #921

Merged
GLinnik21 merged 21 commits into
GLinnik21:mainfrom
stefan-ovezea:codex/legacy-playback-controls
Oct 25, 2026
Merged

GLinnik21 merged 21 commits into
GLinnik21:mainfrom
stefan-ovezea:codex/legacy-playback-controls

Conversation

@stefan-ovezea

@stefan-ovezea stefan-ovezea commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Playback behavior

Dedicated FF/REW keys now distinguish tap from hold. A tap jumps by the configured Skip interval on release, keeps the HUD hidden and preserves pause. A hold uses the existing shared continuous scrub and waits for OK, without committing an initial jump. SeekBurst accumulates quick taps from the last requested target, so an earlier keyframe landing cannot erase the requested travel. The long-GOP regression now exercises FF with the D-pad option Off.

D-pad Left/Right retains main's reveal → target → OK behavior by default. Settings → Video & playback now contains two independent per-profile options:

Option Default Effect
Instant D-pad seeking Off Fresh Left/Right presses jump immediately only while the HUD is hidden. With the HUD visible they still preview and wait for OK.
Previous/Next buttons On Shows episode navigation when the existing PlayQueue has an episode on either side. Can be hidden independently of seeking.

Why retain an opt-in D-pad option?

This is a proposed compromise with the request to remove the setting. Some compact TV remotes only have a D-pad and no dedicated FF/REW keys, so moving immediate jumps exclusively to transport keys makes them inaccessible on those remotes. An opt-in D-pad mode is useful when watching anime that mixes filler and canon material: viewers may make frequent jumps through unwanted sections, then use Next to move to the following episode. It does not detect or automatically skip filler.

The option starts Off, so viewers who never enable it keep the deliberate reveal → target → OK interaction. Separating it from the episode buttons also means neither preference forces the other behavior. The buttons start On to match the requested default visibility. All previous combined-setting names, code references and translation shards have been replaced with names describing these two options.

Episode row and queue continuity

  • There is one default row: [Previous][Next], a wider gap, then [Subtitles][Audio][More]. Next uses the shared HStack's Item::new(BTN_S).after(2.0 * CONTROL_GAP); absent episode controls use Item::absent(). A layout regression verifies that the three existing discs have identical x positions with and without the pair.
  • Movies, extras and queues with no neighbors omit the pair. Unavailable sides are dimmed, cannot receive pointer focus and are skipped by shared focus travel. Entering the row prefers Next when enabled, otherwise the first enabled button. Real focus-travel tests cover disabled and absent controls.
  • Previous restarts at five seconds or later and shows Icon::Restart. Before five seconds it plays the previous queue item, or is disabled on the first episode. Next plays the following item, or is disabled at the end.
  • TransportButton rejects focused+disabled styling in either builder order and at draw time, preventing a blank white disc even if a caller supplies inconsistent state.
  • The title boundary reserves the wider row and focus growth. Long-title fitting tests cover all nine shipped languages and the largest HUD title size.

The existing queue-window approach is retained: handoffs read the current PlayQueue around the target using center, window, includeBefore, includeAfter and own=0. They preserve the queue ID and preceding rows instead of POSTing a replacement tail. Up Next uses the same handoff. Failed or mismatched refreshes retain the known snapshot.

Merged upstream main through b835afb8 and preserved its typed metadata runner and same-turn effect handling. A boundary regression also caught the application dispatcher discarding FF/REW identity by classifying those keys as ordinary focus arrows; transport keys now retain their original identity through classification. The requested-target change does not modify production decoding or the rebuffering engine.

Validation

Focused checks ran on the clean, committed tree at 05aa02d9; the full local suite was intentionally not rerun.

  • 283 focused unit tests passed: player input, row geometry/actions/title fitting, disabled-widget styling, shared focus travel, independent preference persistence/defaults, queue navigation/window reads, Settings navigation and the application transport-key boundary. Representative exact results:

    test result: ok. 139 passed; 0 failed; 0 ignored; 0 measured; 1581 filtered out; finished in 0.31s
    test result: ok. 59 passed; 0 failed; 0 ignored; 0 measured; 291 filtered out; finished in 8.95s
    test result: ok. 18 passed; 0 failed; 0 ignored; 0 measured; 1372 filtered out; finished in 0.00s
    test result: ok. 32 passed; 0 failed; 0 ignored; 0 measured; 1688 filtered out; finished in 0.90s
    test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1058 filtered out; finished in 0.00s
    
  • 12 simulator scenarios passed against mock PMS/generated media: default D-pad reveal; optional hidden-HUD D-pad jumps; FF tap/hold separation; visible-HUD pending target/OK; five-second restart; Next→Previous queue continuity; and rapid FF taps over 12-second keyframes. The rapid-tap requested targets were 12.432s → 22.432s → 32.432s, while the native seek destinations were 12s → 12s → 24s.

  • The Home, Filmography and Settings replay anchors still needed re-recording because the canonical screen bundle now includes FF tap and seek-burst state. They were re-recorded using plxnative-rec rerecord, retaining anchor flags, rather than editing hashes. All six Target/Resolve replays passed with zero differences; all three recordings pass the synthetic-data checker. The Settings capture includes an explicit settled return and stays within the existing recording-reader limit.

  • make check-nodef, the ARM debug build (make FLAVOR=debug, tvdev profile), and the macOS simulator build passed. Module layers, list caps, Plex request registration and check-deps.sh passed without expanding structural allowlists.

  • A manual audit checked affected documentation claims. Public source and PR text were checked with the outbound guard.

This revision has simulator playback and ARM build evidence. It has not been exercised on the LG TV; the simulator does not establish LG decoding, video-plane composition, text rasterization or device performance.

Build benchmark

Ran make build-bench ARGS='--only noop,leaf --runs 1' on the base and feature commits in this checkout, on the same Apple M1 Pro (10 cores), rustc 1.99.1, serial front end. This compiles tests without running the unit suite.

Scenario Base b835afb8 Feature 05aa02d9
No-op host test build Blocked during warm-up: five required Russian catalog keys missing 0.8 s; 0/169 units rebuilt
Non-incremental base-leaf edit rebuild Not reached because baseline warm-up failed 44.1 s; 27/169 units rebuilt

The feature tree includes the five missing Russian entries. The benchmark restored a clean tree. This is a single sample with host-load warnings, and the baseline did not build, so it establishes no before/after speed claim.

Screenshots

Captured from the simulator's own 1920×1080 framebuffer at 05aa02d9, using mock PMS, synthetic titles and generated media. The clock sink and system-FFmpeg screenshot compositor demonstrate UI states only. These are not desktop screenshots or device performance measurements.

Next focused; Previous disabled before five seconds

Next focused; Previous disabled

Previous focused; Next disabled on the final episode

Previous focused; Next disabled

Previous uses the restart icon after five seconds

Previous restart icon

FF tap seeks with the HUD hidden

Hidden-HUD FF tap

Visible-HUD D-pad seek previews a pending target and waits for OK

Visible-HUD pending target

Independent settings and their defaults (before choosing a profile)

Independent playback options

Review notes

  • AI-assisted implementation. The eight non-English option shards (be, de, es, fr, hu, it, pt, ru) are AI-drafted and still need native-speaker review under CONTRIBUTING.md and docs/localization.md. No native reviewer is claimed. Five missing existing Russian catalog keys were also completed so the integrated upstream catalog builds; those additions need the same review.
  • Two isolated test-infrastructure fixes from the earlier feature revision remain: bounded GC-test subprocess pipe cleanup and a pinned feed-slice test pacing reference. Neither changes production pipeline behavior.
  • New contributions are offered under GPL-3.0-or-later, with Signed-off-by trailers. No third-party implementation was copied; existing notices are retained.

Behavior, maintenance seams, queue-window details and synthetic reproduction commands are in docs/playback-navigation.md.

Signed-off-by: Stefan Ovezea <sovezea@gmail.com>
…ansport

Signed-off-by: Stefan Ovezea <sovezea@gmail.com>
Signed-off-by: Stefan Ovezea <sovezea@gmail.com>
Signed-off-by: Stefan Ovezea <sovezea@gmail.com>
Signed-off-by: Stefan Ovezea <sovezea@gmail.com>
Signed-off-by: Stefan Ovezea <sovezea@gmail.com>
Signed-off-by: Stefan Ovezea <sovezea@gmail.com>
Signed-off-by: Stefan Ovezea <sovezea@gmail.com>
Signed-off-by: Stefan Ovezea <sovezea@gmail.com>
Signed-off-by: Stefan Ovezea <sovezea@gmail.com>
Signed-off-by: Stefan Ovezea <sovezea@gmail.com>
@GLinnik21

Copy link
Copy Markdown
Owner

Thanks! I think we should pick a different name than "legacy". It's unclear what it means, and the project is only a few months old and still in beta.

Signed-off-by: Stefan Ovezea <sovezea@gmail.com>
@stefan-ovezea

Copy link
Copy Markdown
Contributor Author

Fair enough, I went with "legacy" thinking of Plex's original controls, but I agree, in the context of this repo it's confusing. Maybe a name like "classic" would work here?

@stefan-ovezea stefan-ovezea changed the title Add opt-in legacy playback controls and previous/next navigation Add opt-in Classic Playback Controls and previous/next navigation Oct 22, 2026
@GLinnik21

Copy link
Copy Markdown
Owner

Could you add a few screenshots to the PR description? The HUD row with Previous/Next (focused and disabled), a hidden-HUD seek, and a visible-HUD seek with the pending target would do. Simulator + mock PMS is fine.

@stefan-ovezea

Copy link
Copy Markdown
Contributor Author

Sure. I’ve added four simulator + mock PMS screenshots to the PR description, showing Previous/Next focused and disabled, seeking with the HUD hidden, and a pending seek with the HUD visible. Please let me know if you’d like anything else

Signed-off-by: Stefan Ovezea <sovezea@gmail.com>
Signed-off-by: Stefan Ovezea <sovezea@gmail.com>
Signed-off-by: Stefan Ovezea <sovezea@gmail.com>

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

Thanks for this, and for the screenshots. I'd like to merge it, with a different split of the seeking behaviour and some changes to the buttons.

Seeking: move it to the remote's rewind / fast-forward keys and drop the setting

The current D-pad behaviour with the controls hidden is deliberate. The first Left/Right shows the controls, the next moves the target, and OK confirms. That's YouTube's model on TV and I'd like to keep it. The dedicated rewind / fast-forward keys are where an instant jump fits, so:

  1. No setting. Remove Classic Playback Controls: the preference, its locale shards and the settings row.
  2. D-pad Left/Right: unchanged from main. Revert them to today's reveal → target → OK.
  3. Rewind / fast-forward, tapped: jump straight away. Jump by the skip interval with the controls staying hidden. This is your current hidden-seek path, keyed on FF/REW instead of Left/Right, including SeekBurst, so quick taps add up from the last target. Please keep the long-GOP regression and point it at FF/REW.
  4. Rewind / fast-forward, held: keep today's behaviour. The continuous scrub with OK to confirm. A tap jumps, a hold scrubs.

Previous / Next: always in the row, with these changes

  1. Show them by default whenever there's an episode either side in the play queue. Leave them out for movies and extras.
  2. Keep the order and add a wider gap after Next: [Previous][Next], gap, [Subtitles][Audio][More]. Use the shared HStack's per-item gap, Item::new(BTN_S).after(2.0 * CONTROL_GAP) on Next, rather than new layout code or spacing in the screen.
  3. When there's nothing either side, use Item::absent(). That keeps Subtitles/Audio/More exactly where they are today, since the row is right-anchored. Please add a layout test that their x positions match with and without the pair. This also folds the separate classic row (ITEMS/ACTIONS) back into the default one.
  4. Dim the side that's unavailable. That's the first episode under 5 s, or the last episode. The current INK_DISABLED look is fine.
  5. A disabled button shouldn't be selectable. Today Left/Right step through every button whatever its state (the nav.btn clamp in screens/src/player/mod.rs), and Up parks on index 0 (primary_btn() returns 0), which is a disabled Previous at the start of a first episode.
    • Please skip disabled buttons.
    • Land on Next when it's enabled, otherwise on the first enabled button.
    • Use the shared focus mechanism rather than the hand-written clamp if you can.
    • Add a test for moving focus along the row. The current tests only check action().
  6. Focused + disabled draws a blank white disc. TransportButton picks INK_DISABLED without looking at focus, so 22% white lands on the near-white focused face. #9 makes it unreachable, but the widget shouldn't be able to draw it either.
  7. Show the 5-second rule on the button. Past 5 s, draw Previous with the existing restart icon (Icon::Restart, assets/icons/restart.svg) instead of the skip-back glyph. No text label.

Smaller things

  • Check the title still fits beside the wider row at the longest title and largest text size (title_right).
  • Rename the classic_controls modules, the doc and the translation keys to match. Nothing is "classic" any more. Update docs/classic-playback-controls.md to describe the FF/REW jump, the buttons and the queue window.
  • With Left/Right reverted, check which replay anchors still need the re-recording.

The queue-window work is the right approach, so please keep it as is: reading the existing PlayQueue around the item so Previous survives moving on, and sharing that handoff with Up Next.


Generated by Claude Code

Signed-off-by: Stefan Ovezea <sovezea@gmail.com>
…acy-playback-controls

Signed-off-by: Stefan Ovezea <sovezea@gmail.com>

# Conflicts:
#	rust-modules/plex/src/plex/timeline.rs
#	rust-modules/screens/src/player/mod.rs
#	rust-modules/screens/src/registry.rs
#	rust-modules/src/app/playback.rs
#	tests/fixtures/replay/1-boot-home-chip-grid/manifest.json
#	tests/fixtures/replay/1-boot-home-chip-grid/rec-0000.jsonl
#	tests/fixtures/replay/12-filmography-detail-return/manifest.json
#	tests/fixtures/replay/12-filmography-detail-return/rec-0000.jsonl
#	tests/fixtures/replay/6-settings-family/manifest.json
#	tests/fixtures/replay/6-settings-family/rec-0000.jsonl
Signed-off-by: Stefan Ovezea <sovezea@gmail.com>
@stefan-ovezea stefan-ovezea changed the title Add opt-in Classic Playback Controls and previous/next navigation Add FF/REW tap seeking and independent playback navigation options Oct 25, 2026
@stefan-ovezea

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review! I’ve updated the seeking behavior, button layout, focus handling and restart icon, while keeping the queue-window work. I’ve also refreshed the screenshots and added focused regression tests.

One compromise I’d like to propose: keeping Instant D-pad seeking as a separate option, disabled by default. My remote only has a D-pad, so FF/REW alone wouldn’t cover my use case. Instant jumps are especially useful when watching anime that mixes canon and filler material. The default reveal → target → OK behavior stays unchanged.
Previous/Next buttons is now independent and enabled by default. Would you be comfortable with that split?

The maintainer review asked for no toggle on the episode buttons, so the
"Previous/Next buttons" preference is removed end to end: the session
field and setting row, the live cell, the Settings picker, its locale
strings in every shard, the boot scenario and the docs. The pair now
shows whenever an episode has a neighbour in its PlayQueue. Instant
D-pad seeking stays as the one opt-in playback option.

Also drops the leftover "legacy"/"modern" wording and the empty
ScrubPress::Row arm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xn4z8v7ojwK4Xm9cci5Qon
Behaviour-preserving cleanups from a reuse/simplification/efficiency
review of the branch:
- delete the now-uncalled request_play_up_next and disc_hit_rect; the
  show-title rule lives once in request_play_queue_item
- InstantDpadSeeking is written like DeckPress (LADDER, soft
  deserializer beside its siblings) instead of a one-use macro
- Controls::pair is derived; Controls::now skips the queue lookup and
  the playhead read for non-episodes
- the player row maps its actions straight to overlays; one disc stack
  builder; one row_index decoder; end_jump_gesture/drop_transport_tap
  replace the copied resets
- TransportButton rules out a focused disabled face once, at draw time
- queue_navigation shares current_index and stores fetched windows
  without copying

CI fixes on the tree merged with main: the focus probe prints the
episode-pair keys for every row occupant, and the per-profile and
Settings picker counts match the merged settings list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xn4z8v7ojwK4Xm9cci5Qon
@GLinnik21
GLinnik21 merged commit 2d238e7 into GLinnik21:main Oct 25, 2026
19 checks passed

Copy link
Copy Markdown
Owner

Thanks @stefan-ovezea, merged! I kept Instant D-pad seeking as you proposed, and pushed a few finishing commits to your branch before merging:

  • Previous/Next now show without a setting
  • main merged in; its shorter Russian "Playback speed" string fixed the menu-fit tests
  • small simplifications, plus the CI fixes

Thanks again for the work and for turning the review around so fast.


Generated by Claude Code

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