Files
mal/.codex/audits/frontend-performance-2026-07-03/fixes/06-player-loading-and-recovery.md

135 lines
4.8 KiB
Markdown

# Fix 06: improve player loading and recovery states
Priority: P1
Risk: Low to medium
Primary benefit: Perceived performance, accessibility, and error recovery
## Issue
The player displays an animated spinner over a black frame while loading. During the audited cold start, this state lasted almost ten seconds. The spinner does not explain whether the application is finding a source, loading metadata, buffering media, retrying, or stuck. It also lacks a useful live status for assistive technology.
Several player buttons also appeared without accessible names in the browser accessibility tree.
## Goal
Make waits understandable without adding noisy progress theater. The loading UI should expose a small state machine, announce meaningful changes, and always provide a recovery path.
## Proposed state model
```text
idle
resolving_source
loading_media
buffering
retrying
ready
unavailable
```
State meanings:
- `resolving_source`: waiting for the episode API/provider result.
- `loading_media`: source is known; waiting for manifest/metadata.
- `buffering`: metadata exists but playback has stalled.
- `retrying`: refreshing the source after a media error.
- `unavailable`: all bounded attempts failed.
- `ready`: overlay hidden.
Do not attempt to show a fake percentage. The application does not know enough to estimate completion accurately.
## Visual behavior
- Show the spinner immediately to acknowledge the action.
- Keep text visually quiet for short waits.
- After roughly 3 seconds, show a short status such as “Loading video…” or “Finding a source…”.
- After roughly 8-10 seconds, show “This is taking longer than usual” and a Retry action.
- On terminal failure, show Retry and Back to details.
- Keep episode title/context visible where possible.
- Avoid replacing the whole page; recovery stays inside the player.
## Accessibility behavior
Use a stable status element inside the player:
```html
<div role="status" aria-live="polite" aria-atomic="true">
<span data-loading-message>Loading video…</span>
</div>
```
Guidelines:
- Do not announce every buffering event; debounce announcements to avoid screen-reader noise.
- Use `aria-busy="true"` on the player container while a transition is active.
- Move focus only for terminal errors, not normal loading.
- Give play/pause, mute, fullscreen, seek, and navigation buttons stable accessible names.
- Ensure disabled controls expose native `disabled` or an equivalent semantic state.
## State ownership
Centralize overlay changes in one module/function. Currently media event handlers manipulate `loading.style.display` directly in several places. Replace scattered writes with a function such as `setPlayerLoadState(state, options)`.
That function should:
- Update `data-load-state` on the player root.
- Toggle visibility.
- Set `aria-busy`.
- Update status copy.
- Start/cancel the long-wait timer.
- Expose Retry only in retryable states.
## Retry policy
- Episode payload failure: one ordinary fallback navigation, unless already on that URL.
- Media source error: refresh the current mode source once.
- Repeated media failure: stop automatic retries and show Retry.
- User Retry: force source refresh, reset media element, and reload.
- Never create an infinite retry loop.
## Affected files
- `templates/components/video_player.gohtml`
- `static/player/main.ts`
- `static/player/source.ts`
- `static/player/video.ts`
- `static/player/state.ts`
- `static/player/controls.ts`
- Player browser-flow tests
## Tests
- Loader appears immediately on source change.
- Status text appears only after the delay.
- Ready metadata hides the loader and clears timers.
- Waiting after ready enters buffered/loading state without repeated announcements.
- One media error attempts one refresh.
- Repeated failure exposes Retry and stops looping.
- Retry performs a forced source refresh.
- Every icon-only control has an accessible name.
- `aria-busy` accurately follows transition state.
## Observability
Record client-side phase durations without sensitive URLs:
- Source resolution.
- Media metadata wait.
- Buffering count and duration.
- Retry count.
- Terminal unavailable state.
The existing HLS profiling hook can remain for HLS-specific detail; add source-independent timing for direct media too.
## Rollout
Ship the centralized state function and accessible labels first without changing retry counts. Then enable delayed copy and user Retry. This separates semantic/visual regressions from source-refresh behavior changes. Verify with direct media and HLS sources.
## Acceptance criteria
- No indefinite spinner exists without text or a recovery action.
- A useful status appears after 3 seconds.
- Retry appears after the long-wait threshold or a terminal failure.
- Player root exposes `aria-busy` during loading.
- All icon-only player controls have accessible names.
- Retry loops are bounded.