How iOS renders the video
On iOS the library doesn't draw into a Compose canvas — it hosts a native AVPlayerLayer inside a UIKitView (interop layer that sits above Compose). Scaling is applied in two places at once (VideoPlayerSurface.ios.kt):
- Compose-side sizing — a modifier that sets the interop view's box shape:
contentScale.toCanvasModifier(aspectRatio = playerState.aspectRatio, ...)
- Native-side gravity — AVPlayerLayer.videoGravity (Fit → resizeAspect, Crop → resizeAspectFill).
What Fit does — and why it breaks
For ContentScale.Fit, toCanvasModifier (in ContentScaleCanvasUtils.kt) returns:
Modifier.fillMaxHeight().aspectRatio(aspectRatio)
So the video host is sized to fill the height, then take width = height × aspectRatio. That whole thing hinges on aspectRatio being correct. It isn't. In VideoPlayerState.ios.kt:
private var _videoAspectRatio by mutableStateOf(16.0 / 9.0) // reactive, defaults to 16:9
override val aspectRatio: Float = _videoAspectRatio.toFloat() // plain val — evaluated ONCE
That's the bug. _videoAspectRatio is a reactive state and does get corrected later (line ~266, when item.presentationSize finally resolves the real dimensions). But aspectRatio is a plain val initialized = _videoAspectRatio.toFloat() at construction. It reads the state once, captures the 16:9 default, and freezes — it's not a get() getter, so it never recomposes when the true size arrives.
Consequences:
- The Fit layout always uses 16:9, no matter the actual clip.
- Reels are portrait (~9:16), so the surface becomes a landscape 16:9 box centered on screen — the video ends up mis-scaled with big black bands, the opposite of a full-bleed reel. And the native resizeAspect then letterboxes inside that already-wrong box, compounding it.
- It's worse in our setup specifically: we reuse one player state across all reels (openUri swaps media in place, the state object is built once), so aspectRatio is frozen at 16:9 for the entire session and never even reflects the first video. That's the "flaky aspectRatio" our config comment refers to.
The clean fix: make aspectRatio reactive — override val aspectRatio: Float get() = _videoAspectRatio.toFloat() — which would let real Fit work on iOS.
How iOS renders the video
On iOS the library doesn't draw into a Compose canvas — it hosts a native AVPlayerLayer inside a UIKitView (interop layer that sits above Compose). Scaling is applied in two places at once (VideoPlayerSurface.ios.kt):
contentScale.toCanvasModifier(aspectRatio = playerState.aspectRatio, ...)
What Fit does — and why it breaks
For ContentScale.Fit, toCanvasModifier (in ContentScaleCanvasUtils.kt) returns:
Modifier.fillMaxHeight().aspectRatio(aspectRatio)
So the video host is sized to fill the height, then take width = height × aspectRatio. That whole thing hinges on aspectRatio being correct. It isn't. In VideoPlayerState.ios.kt:
private var _videoAspectRatio by mutableStateOf(16.0 / 9.0) // reactive, defaults to 16:9
override val aspectRatio: Float = _videoAspectRatio.toFloat() // plain val — evaluated ONCE
That's the bug. _videoAspectRatio is a reactive state and does get corrected later (line ~266, when item.presentationSize finally resolves the real dimensions). But aspectRatio is a plain val initialized = _videoAspectRatio.toFloat() at construction. It reads the state once, captures the 16:9 default, and freezes — it's not a get() getter, so it never recomposes when the true size arrives.
Consequences:
The clean fix: make aspectRatio reactive — override val aspectRatio: Float get() = _videoAspectRatio.toFloat() — which would let real Fit work on iOS.