VideoStage

fun VideoStage(player: VideoPlayer?, outputAspect: Float, modifier: Modifier = Modifier, contentScale: VideoContentScale = VideoContentScale.Fit, surface: VideoSurfaceState? = null, fallback: @Composable BoxScope.() -> Unit = { }, content: @Composable VideoStageScope.() -> Unit = { })(source)

A box the shape of the composition's output frame, holding the video and whatever is drawn over it.

The stage takes the largest rectangle of that shape which fits the space it is given, so give it space to fit into rather than a size to fill: a Modifier.padding inside a centred box leaves it room, while a Modifier.fillMaxSize pins both axes and leaves it nothing to letterbox with.

A live player reports the frame it is showing rather than the one it was last asked for, and that wins over outputAspect, so the stage and the picture in it change shape on the same frame instead of the stage moving a swap ahead of the video. outputAspect carries the shape until a player has one, and on its own where there is no player at all.

A null player is the whole signal for the fallback slot. A host whose preview stopped, or whose platform reported PlaybackError.BackendMissing , passes null and draws its own stand-in at the same letterboxed size rather than passing a player it does not want rendered.

val composition = remember(source) { compositionOf { clip(source) } }
val player = rememberFilmstripPlayer(filmstrip, composition)
val playerState by player.state.collectAsState()
var rect by remember(source) { mutableStateOf(NormalizedRect.Full) }

// Every platform ships a preview backend, so a build reaching this is one that never registered
// it, and the schematic stands in for the picture.
val missing = (playerState.status as? PlaybackStatus.Error)?.error is PlaybackError.BackendMissing

Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
VideoStage(
player = player.takeIf { !missing },
outputAspect = info?.video?.displaySize?.aspect ?: 0f,
modifier = Modifier.padding(16.dp),
fallback = { EditSchematic(edit, Modifier.fillMaxSize()) },
) {
CropOverlay(rect = rect, onRectChange = { rect = it })
}
}

Parameters

player

The player to render, or null to draw fallback instead.

outputAspect

Width over height of the frame the composition outputs, which the stage uses until player presents one of its own. Anything at or below zero leaves the stage filling whatever space it was given, the way Modifier.videoAspect stays inert until an output size arrives.

modifier

Modifier for the stage, applied outside the shape it lays itself out to, so a border or a clip follows the letterboxed frame rather than the surrounding space.

contentScale

How the video fills the stage.

surface

The holder the presented size is read from, for a host that already keeps one over player. Null leaves the stage keeping its own.

fallback

Drawn at the stage's size when player is null.

content

Drawn over the video or the fallback, for overlays that address the frame.