Skip to content

perf(player): cut wasted per-frame rebuilds during playback#2

Closed
JenteJan wants to merge 1 commit into
developfrom
perf/player-optimizations
Closed

perf(player): cut wasted per-frame rebuilds during playback#2
JenteJan wants to merge 1 commit into
developfrom
perf/player-optimizations

Conversation

@JenteJan

@JenteJan JenteJan commented Jun 8, 2026

Copy link
Copy Markdown
Owner

What

Three rebuild-scope cleanups in the video player. No behaviour change — purely doing less redundant work.

  1. Unmount the overlay controls while hidden. They were kept mounted at opacity: 0 when auto-hidden, so their position-driven Consumers kept rebuilding (~1–2×/sec from position ticks) the whole time you're just watching. Now dropped from the tree once the hide animation finishes, remounted before fading back in.
  2. Only build the scrub-preview card while it's visible. chapterCard built and laid out its image / TrickPlayImage subtree on every frame behind opacity: 0. For content with trickplay this means decoding/laying out a preview tile every frame even when the preview isn't shown. Now gated on actual hover/drag.
  3. Narrow the queue watches. The audio queue + queue dialog watched the whole mediaPlaybackModel and re-.map()-ed the entire queue on every position tick; they only need repeatMode, so they now select() just that.

Profiling notes (honest)

Profiled in profile mode on macOS (the libmpv/media_kit desktop path, shared with Windows). Findings:

  • The render pipeline is jank-free in every scenario (steady-state and interacting): raster p99 < ~1ms, 0% frames over budget. So this is not a fix for dropped frames — it's removing wasted CPU/GC/main-thread work.
  • The steady-state win from (1) is real but small in absolute terms because position updates are already throttled to ~1/sec — it removes those residual hidden-control rebuilds entirely (layout while watching ≈ 5ms/12s → ~0).
  • (2)'s payoff scales with trickplay-enabled content, which I couldn't reproduce on the test server, so I'm not attaching an fps number to it — but rebuilding an image/trickplay widget every frame behind opacity: 0 is a clear anti-pattern worth removing.

No fabricated metrics: the case for these is the code itself (don't rebuild offscreen widgets; don't watch a whole model for one field) plus lighter idle CPU/battery, especially on weaker hardware.

Draft — opened against my fork's develop for review before sending upstream.

Profiling steady-state playback on macOS (libmpv path, shared with
Windows) showed the Flutter layer doing needless work while watching,
even though the video itself is composited cheaply by mpv.

- Unmount the overlay controls once their hide animation finishes, so
  their position-driven Consumers stop rebuilding while the overlay is
  hidden (the common 'just watching' case). Remount before fading back
  in. Steady-state UI work drops to ~3 idle frames/sec.
- Only build the progress-bar scrub-preview card while it is actually
  visible; it was building and laying out its image/trickplay subtree on
  every frame behind opacity 0 (the dominant LAYOUT cost when controls
  are shown).
- Narrow the audio queue / queue-dialog watches to repeatMode via
  select() instead of watching the whole playback model, so they no
  longer rebuild and re-map the entire queue on every position tick.

No behavioural change; pure rebuild-scope reduction.
@JenteJan JenteJan changed the base branch from develop to maktep June 8, 2026 06:04
@JenteJan JenteJan changed the base branch from maktep to develop June 8, 2026 06:04
@JenteJan

JenteJan commented Jun 8, 2026

Copy link
Copy Markdown
Owner Author

Superseded by DonutWare#1019 (opened upstream).

@JenteJan JenteJan closed this Jun 8, 2026
JenteJan added a commit that referenced this pull request Jun 16, 2026
…can-f#3 DonutWare#10 DonutWare#4)

- DonutWare#9 connect() tears down the active session before switching, so e.g.
  AirPlay is actually stopped before Chromecast starts.
- #2 DlnaPlayer sends UPnP Stop on dispose so the TV session closes on
  disconnect.
- irican-f#3 DLNA play/pause are optimistic (snappy UI) behind a short command guard,
  after which the poll is authoritative again — so play/seek done on the TV
  itself is reflected on the client.
- DonutWare#10 external-end handling generalized: DLNA poll detects STOPPED/NO_MEDIA or
  an unreachable renderer; a native Chromecast session-disconnect listener
  (guarded to chromecast sessions) restores local playback when the receiver
  is taken over or turned off. (web SESSION_ENDED already covered.)
- Responsiveness: connecting/disconnecting states with the target name; picker
  shows progress and disables tiles during transitions.
- DonutWare#4 device tiles keyed by id so the tap ripple lands on the tapped row.
JenteJan added a commit that referenced this pull request Jun 16, 2026
…utWare#4)

- #2 AirPlay now shows the item's backdrop behind the cast badge (fullscreen +
  mini), like the other cast targets — the video plays on the Apple TV, so we
  show the shared CastingPlaceholder instead of the local AVPlayer texture.
- DonutWare#4 Chromecast devices publish live via GoogleCastDiscoveryManager.devicesStream
  instead of only being snapshotted within the scan window, so they appear as
  the SDK finds them (notably on iOS, where the first scan often finished before
  the SDK had them — no more manual reload). Discovery now merges a persistent
  cast-device list with the per-scan DLNA renderers.
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