Stills
Stills go both ways in filmstrip. A photo can sit on the timeline as a clip, and a frame of an edit can be written out as an image.
A photo on the timeline
Section titled “A photo on the timeline”image adds a still held for a duration. It’s on a composition and on a track, and is marked
@ExperimentalFilmstripApi.
val composition = compositionOf { image(ImageSource.of("/path/to/title.jpg"), duration = 3.seconds) clip(MediaSource.of("/path/to/clip.mp4")) }A still has no frame rate of its own. The output uses spec.frameRate, then the rate of the first clip on the primary
track that decodes video, then 30, so an all-photo edit with no rate comes out at 30.
Where the primary track holds no clip that decodes video and the spec names no height, the output is held to 3840x2160, since a photo’s bounds are a sensor’s and not a frame an encoder is built for. Set both when the edit is all photos.
val spec = ExportSpec(heightPx = 1080, frameRate = 30)Android and Apple put a still on the timeline. The JVM and browser backends decode their picture from a video track, so they refuse a still by name wherever it appears, an audio track included.
Write a frame to a file
Section titled “Write a frame to a file”still renders a frame of a composition, encodes it and writes it to a sink. It decodes at the size it writes, so it
doesn’t render at full resolution and shrink.
val result = filmstrip.still( composition = composition, at = 3.seconds, to = MediaSink.of("/path/to/poster.jpg"), spec = StillSpec(format = StillFormat.Jpeg, quality = 85, heightPx = 720), )
when (result) { is StillResult.Success -> println("Wrote ${result.size} to ${result.output}") is StillResult.Failure -> println(result.error.message)}StillSpec is all optional.
formatisJpegby default.PngandJpegwrite on every target.qualityruns from 0 to 100, 90 by default. A value outside that is clamped, andPngignores it.heightPxis the height to encode at, and the width follows from the aspect. Null keeps the frame’s own height. Zero or less fails withInvalidComposition.
For one source with no edit, pass the source directly. MediaSink.Temporary picks a file for you, and
StillResult.Success.output says where it went.
filmstrip.still(MediaSource.of("/path/to/clip.mp4"), at = 1.seconds, to = MediaSink.Temporary)Encode to bytes
Section titled “Encode to bytes”To keep the bytes, encode a frame you already rendered with encodeStill. The frame stays open, so closing it is still
your job.
val encoded = frame.image.encodeStill(StillSpec(format = StillFormat.Png))