Bindings
filmstrip-compose holds the Compose Multiplatform bindings: player state, a cache for frame strips and a bare video
surface. It draws no controls.
Player and surface
Section titled “Player and surface”rememberFilmstripPlayer builds a player and closes it when it leaves composition. A new composition is loaded for you,
so ordinary editing needs no debounce. VideoSurface renders the player and nothing else, and anything you draw after it
composites on top of the video.
val player = rememberFilmstripPlayer(filmstrip, composition)val surface = rememberVideoSurfaceState(player)
VideoSurface( player = player, modifier = Modifier.fillMaxWidth().videoAspect(surface, VideoContentScale.Fit),)Size the surface to the composition’s output aspect, not the source’s. videoAspect does that from
rememberVideoSurfaceState, and stays inert until the engine reports a size.
contentScale is a fit in the view. VideoContentScale.Fit shows bars in the view that never reach the file, unlike
letterboxing in the composition, which writes real pixels.
Playhead
Section titled “Playhead”rememberPlayheadState follows the playhead and the composition’s length. It has position, duration and progress,
read at PlayheadDefaults.Tick unless you ask for another tick.
val player = rememberFilmstripPlayer(filmstrip, composition)val playhead = rememberPlayheadState(player)
BasicText("${playhead.position} of ${playhead.duration}")Scrubbing
Section titled “Scrubbing”rememberScrubState holds the three calls a drag makes. Call onScrubStart on touch down, onScrubTo on every delta and
onScrubEnd on touch up, which settles on the exact frame. onScrubTo is ignored outside a gesture. A second form takes
your own onSeek, for a preview you run on your own clock. The timeline wires this up for you.
Events
Section titled “Events”PlaybackEventEffect collects a player’s events for as long as it stays in composition. Use it instead of your own
LaunchedEffect, since events replays nothing and a hand-rolled collector can race the command that produced an event.
val player = rememberFilmstripPlayer(filmstrip, composition)
PlaybackEventEffect(player) { event -> if (event is PlaybackEvent.Failed) println(event.error.message)}Frames
Section titled “Frames”rememberFilmstripFrame holds one exact frame, for a poster or a clip card. It fetches again when the position, the height
or anything that changes a rendered frame moves. The last frame stays in place while the next renders, and a failed render
clears it.
val poster = rememberFilmstripFrame(filmstrip, composition, at, heightPx = 360)
if (poster != null) { Image(poster, contentDescription = null)}rememberFilmstripFrames holds a whole strip. Tell it which items are showing with onVisibleRange, since nothing is
fetched until you do. It fetches the visible items plus an overscan either side and closes everything outside that window.
val frames = rememberFilmstripFrames(filmstrip, composition, positions, heightPx = 96)val listState = rememberLazyListState()
LaunchedEffect(frames, listState) { snapshotFlow { listState.firstVisibleItemIndex to listState.layoutInfo.visibleItemsInfo.size } .collect { (first, visible) -> frames.onVisibleRange(first, visible) }}
LazyRow(state = listState) { items(frames.count) { index -> Box(Modifier.size(64.dp, 96.dp)) { frames[index]?.let { frame -> Image(frame, contentDescription = null) } } }}Requests are served one at a time, since extraction next to a preview contends for the device’s decoders. maxBytes caps
how much decoded pixel data it holds. An edit that doesn’t change a rendered frame, like a clip’s gain, keeps every frame.