Skip to content

fix(macos): correct video aspect ratio and rewrite frame pipeline to fix tearing, stalls and frozen playback#231

Open
Buffersolve wants to merge 3 commits into
kdroidFilter:masterfrom
Buffersolve:fix-macos
Open

fix(macos): correct video aspect ratio and rewrite frame pipeline to fix tearing, stalls and frozen playback#231
Buffersolve wants to merge 3 commits into
kdroidFilter:masterfrom
Buffersolve:fix-macos

Conversation

@Buffersolve

@Buffersolve Buffersolve commented Jun 10, 2026

Copy link
Copy Markdown

Summary

Fixes several long-standing macOS playback bugs and rewrites MacVideoPlayerState to use the same producer/consumer frame pipeline as Windows.

Fixes

  • Stretched video — aspect ratio now comes from AVPlayerItem.presentationSize (handles HLS and anamorphic content) instead of raw decoded frame size, and updates live on HLS quality switches.
  • Tearing — triple-buffered bitmaps with a drop-oldest channel; the producer never writes to a bitmap being drawn.
  • Frozen timeline on static video — position is now polled from the AVPlayer clock instead of being tied to frame delivery. Also fixed a dedup bug that could freeze video entirely at certain resolutions.
  • Replay after end — works again instead of showing a frozen frame with audio.
  • Seeking — rapid drags are coalesced, no phantom "ended" after seeking away from the end, and stale seeks can't fire on newly opened media.
  • file:/path URIs and percent-encoded paths are now accepted.

Derive the display aspect ratio from AVPlayerItem.presentationSize (pixel
aspect ratio and clean aperture applied) and re-sync it per published frame.
The dimensions known at open time can be wrong or stale — HLS reports a
default/early-variant size and anamorphic content has non-square pixels — so
stretching the full bitmap into a Canvas sized from those dimensions distorted
the image. presentationSize is cached from a KVO observer so it can be read off
the main thread.
Replace the timer-driven frame/position loops with a producer/consumer
coroutine pipeline backed by a drop-oldest channel, content-hash dedup,
and triple-buffered Skia bitmaps to fix tearing and stalls. Drive the
timeline from a dedicated AVPlayer clock poll since AVFoundation reuses
CVPixelBuffers and frame-derived position freezes on low-motion content.
Honor row padding in calculateFrameHash so true pixels are sampled, and
correct display aspect ratio for anamorphic content.
…ayer

The frame-hash sampling step could land on a single x column (step
multiple of width), freezing dedup on static edges; the triple-buffer
round-robin could write into the displayed or in-flight bitmap and tear;
and several stale-state races caused wrong behavior across media loads:

- Bump the hash sampling step when it is a multiple of the frame width
- Skip the displayed and last-sent bitmaps when picking a write target,
  and never close the bitmap Compose is still bound to
- Clear pendingSeekTarget when opening new media so an old seek target
  cannot be applied to the new video
- Drain didPlayToEnd after a native seek and ignore it mid-drag to
  prevent spurious end-of-playback
- Keep the frame producer alive after playback ends so resume/seek can
  relaunch the pipeline
- Don't reload media when position observation is cancelled by dispose()
- Serialize stop()'s nSeekTo behind videoReaderMutex
- Rewrite checkExistsIfLocalFile with java.net.URI to accept all
  file-URI forms and percent-encoded paths

Adds tests for row-padding-aware hashing and the single-column step case.
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