VideoSurface

actual fun VideoSurface(player: VideoPlayer, modifier: Modifier, contentScale: VideoContentScale)(source)

Android renders into a SurfaceView.

The view is handed to the engine behind VideoPlayer.nativePlayer, which points its player at it and reports back when it has pixels, which is what closes the shutter on VideoSurfaceState. A player with no media3 backend under it has no platform player to attach, so this lays out an empty box of the size it was given.

A SurfaceView carries HDR and protected content but ignores the UI layer's clipping, alpha and transforms, so the video draws under anything Compose puts over it rather than blending with it. The still this draws over the surface during a composition swap rides on that, and only holds while the view stays where it is: a SurfaceView that changes bounds re-punches its hole, and for a frame or two the surface composites over whatever Compose had drawn on top. So the view is sized from the frame on screen rather than the one being loaded, and moves when the still is dropped.

expect fun VideoSurface(player: VideoPlayer, modifier: Modifier = Modifier, contentScale: VideoContentScale = VideoContentScale.Fit)(source)

Renders player's video output, and nothing else.

No controls, no gestures, no shutter. Compose content drawn after this composable composites on top of the video on both platforms.

This composable never owns player. It only attaches and detaches the render target, so create the player somewhere that outlives configuration changes and pass it in.

Size the surface to the composition's output aspect rather than the source's. With effects attached the surface letterboxes instead of stretching, so a container with the wrong ratio turns mostly black. The output size is on the player's preview info.

actual fun VideoSurface(player: VideoPlayer, modifier: Modifier, contentScale: VideoContentScale)(source)

iOS renders into a UIView whose backing layer is an AVPlayerLayer.

The layer takes the player from VideoPlayer.nativePlayer and reports back to the engine when it has pixels, which is what closes the shutter on VideoSurfaceState. A player with no Apple backend under it has no platform player to attach, so this lays out an empty box of the size it was given.

The interop view sits below the Metal canvas and Compose punches a hole for it, so content drawn after this composable still composites over the video and Modifier.clip still rounds it.

actual fun VideoSurface(player: VideoPlayer, modifier: Modifier, contentScale: VideoContentScale)(source)

Desktop draws the readback, the same way the browser does.

The frames come off the ffmpeg pump behind VideoPlayer.readback, which is the only display path this platform has: there is no desktop player object to attach a surface to.

actual fun VideoSurface(player: VideoPlayer, modifier: Modifier, contentScale: VideoContentScale)(source)

The browser draws the composite itself, so there is no native view to attach.

This draws the readback, the post-effect frame the encoder takes.