Player
preview opens a VideoPlayer over the same composition value an export takes. Each engine lowers it through the same
backend its exports use, so the compositing isn’t a second implementation.
val player = filmstrip.preview(composition)player.play()preview returns right away and loads in the background. You own the player and have to close it. Without
filmstrip-player registered, the player reports PlaybackError.BackendMissing instead of throwing. A browser reports
the same error, naming filmstrip-transform-webcodecs, when webCodecsBackend() isn’t registered.
A preview writes no file, so a device with no video encoder still previews and only its exports are refused. In a
browser that preview plays its audio too, given a WebCodecs AudioDecoder. A browser needs a WebCodecs VideoDecoder
to draw, and one without it reports PlaybackError.UnsupportedFormat. See
Limits.
config sets how hard a preview works. CapHeight limits its height without changing what an export writes.
val player = filmstrip.preview( composition, PlayerConfig(qualityPolicy = PreviewQualityPolicy.CapHeight(720)), )State and events
Section titled “State and events”state is a StateFlow of consistent snapshots.
player.state.collect { state -> when (val status = state.status) { is PlaybackStatus.Error -> println(status.error.message) PlaybackStatus.Ended -> println("Finished") else -> println("Playing: ${state.isPlaying}, busy: ${state.isBusy}") }}events carries the things that happen once, like a seek landing or a loop wrapping. It replays nothing, so subscribe
before you issue commands.
player.events.collect { event -> when (event) { is PlaybackEvent.SeekCompleted -> println("Landed on ${event.position}") is PlaybackEvent.RangeLooped -> println("Wrapped in ${event.range}") is PlaybackEvent.Failed -> println(event.error.message) else -> Unit }}positionFlow follows the playhead, snapped to the tick you ask for. currentPosition reads it once. Both report a
seek’s target as soon as it’s issued, so a scrubber thumb doesn’t snap back.
player.positionFlow(tick = 100.milliseconds).collect { position -> println(position)}Each of these has a listener form too, addStateListener, addEventListener and addPositionListener, for callers
without coroutines.
Transport
Section titled “Transport”player.seekTo(12.seconds)player.seekTo(30.seconds, SeekAccuracy.Nearest)player.setLoopRange(TimeRange(10.seconds, 20.seconds))player.setVolume(0.5f)
if (player.features.supports(PlayerFeature.FrameStepping)) { player.stepFrames(1)}seekToalways yields oneSeekCompleted, carrying the frame it landed on.SeekAccuracy.Nearestlands on a nearby frame, before or after the one asked for, which suits a moving finger. ItsSeekCompletednames the frame it shows. Until the newest seek completes, the playhead reads the position it asked for.beginScrubandendScrubwrap a drag. Between them the engine relaxes seek accuracy, andendScrubsettles on the exact frame.setLoopRangewraps playback at the end of the range and reportsRangeLooped. A range the composition ends inside wraps there and reports noEnded. A range that starts at or past the end has nothing to loop, so playback runs to the end. A wrap only happens during playback, so a paused seek past the range stays where it lands. A wrap is a seek too, so from the wrap on the playhead reads the range’s first frame and never reports the range’s end again after a lap.setPlaybackRangeconfines playback without changing the composition, which is how a trim drag previews without a rebuild.setVolumesets monitor volume, which only the player hears. To change the export’s level, see Audio.- On Apple,
playon an item AVFoundation failed starts nothing and the status staysPlaybackStatus.Error, so a load is what tries again. An item that stops before its end reportsPlaybackEvent.Failedonce per play or seek, with the platform’s error in it, and the status reads that error until the next play, seek or load. - A failure stops the player and leaves
playWhenReadyas it was, on every backend. Any load after it tries again, the same composition or an edit of one parameter included, and plays if a play is still wanted.
Features
Section titled “Features”features says what this backend can do on this device. Ask it before you draw a control, since a control for a missing
feature looks frozen.
| Feature | Android | Apple | Browser | JVM |
|---|---|---|---|---|
FrameReadback |
Yes | Yes | Needs WebGL2 | Yes |
FrameStepping |
Yes | Yes | Needs WebGL2 | Yes |
LiveParameterRedraw |
Yes | Yes | Needs WebGL2 | No |
AudioMonitoring |
When media3 can | No | Needs Web Audio and AudioDecoder |
No |
HdrPreview |
When the display can | No | No | No |
PlaybackSpeed |
No | No | No | No |
readback hands you a rendered preview frame, after effects and before encoding.
Changing the composition
Section titled “Changing the composition”setComposition loads a new value. It’s cheap to call often. An equal composition does nothing, a change to effect
parameters updates the live pipeline and redraws, and only a structural change rebuilds, like adding a clip or a trim that
changes the duration. Debounce structural edits, so a drag-reorder issues one rebuild and not thirty.
when (val result = player.setComposition(edited)) { is SetCompositionResult.Success -> println("Now showing the edit") is SetCompositionResult.Failure -> println(result.error.message) SetCompositionResult.Superseded -> Unit}A load with no startAt holds the playhead. The transport stays where it was, so a playing preview keeps playing and a
paused one stays paused. Where the playhead lands depends on that transport:
- A paused playhead lands on the frame it rounds to.
- A playing playhead lands on the first frame at or after its time.
- A playhead past the end of a shorter composition lands on the last frame.
- A playing playhead at or past a loop range’s wrap lands on the start of the range.
previewInfo says what’s being delivered, including its outputSize, its parity with the export and its inertNotices,
which match what the export’s plan carries.