-
Notifications
You must be signed in to change notification settings - Fork 29
Suggested Performance Improvements for Mac #179
Description
Suggested Performance Improvements for macOS AVFoundation Player
ComposeMediaPlayer is a fantastic library. Thanks for making it. It was my reference for getting native AVFoundation video playback working in Compose Desktop for Mac. I've been adapting parts of it for my project and discovered a few performance improvements along the way that I thought might be useful upstream.
The suggestions can be seen in a PR here: tunjid/heron#1099
1. Replace polling with native AVFoundation callbacks
The current implementation uses a 33ms polling loop to check for new frames, position updates, and status changes. AVFoundation provides native callback mechanisms that are more efficient and responsive:
- Status changes: Use KVO on
player.timeControlStatusinstead of polling. This fires immediately when the player pauses, plays, or stalls. - Time/position tracking: Use
player.addPeriodicTimeObserver(forInterval:queue:using:): AVFoundation will call back at the requested interval with the current time. - End of playback: Use
NotificationCenterobserver forNSNotification.Name.AVPlayerItemDidPlayToEndTimeinstead of checking position vs. duration each tick.
This eliminates the polling coroutine entirely and gives more accurate, lower-latency updates.
2. Deferred observer attachment pattern
One subtlety: callbacks may be registered before the AVPlayer instance exists (e.g., during initialization). The fix is to store the callback references eagerly, but defer actual observer creation to an attachCallbackObservers() method called after the player is created in setupVideoOutputAndPlayer. Each register method stores the callback, then calls attachCallbackObservers() which idempotently creates observers only if the player exists and the observer hasn't been set up yet.
3. timeControlStatus KVO should not be HLS-only
Currently the KVO observer for player.timeControlStatus is set up inside setupHLSMonitoring(), which means it only fires for HLS streams. This observer is valuable for all content types, it's how you get notified of play/pause/buffering state changes. Moving it to attachCallbackObservers() (or equivalent) ensures it works for all media.
4. Remove mutex / synchronization on Kotlin side
The Compose snapshot system (mutableStateOf) is already thread-safe — writes from any thread are safe and reads are consistent within a snapshot. This means the mutex protecting state updates on the Kotlin side can be removed. The JNA calls to Swift are synchronous and Swift handles its own threading internally, so there's no need for an IO dispatcher either. The controller methods (play, pause, seekTo, etc.) can be made non-suspending.
5. Use limitedParallelism(1) for frame processing
Instead of a mutex around the frame pixel copy, Dispatchers.Default.limitedParallelism(parallelism = 1) provides a serial dispatcher that ensures only one frame is being processed at a time. This is simpler and avoids mutex overhead. Double-buffering of Skia bitmaps is still needed since the UI thread reads while the background thread writes.
6. Remove frame hash computation
With callback-driven frame delivery (the native side only calls the frame callback when a new frame is actually available), there's no need to compute a hash of the frame data to detect changes. This saves a non-trivial amount of computation per frame.
7. Observer cleanup consolidation
Observer setup can be consolidated into a single attachCallbackObservers() method rather than duplicating creation logic across individual registerXxxCallback methods and a separate attachCallbackObservers function. Similarly, cleanup can be split clearly: callback observers are cleaned up by their respective unregisterXxx methods, while HLS-specific observers (buffer empty/full/likely-to-keep-up, player item status) are cleaned up separately.
These are changes I made while adapting your code. Happy to discuss any of these further or provide code snippets if helpful. Thanks again for the great library!