Klarity is a media (video and audio) player for Compose Multiplatform (desktop-only), built on top of the native FFMpeg and PortAudio libraries, and rendered using the Skiko library.
Since frames are rendered directly into the Composable
, this eliminates the need for compatibility components like
SwingPanel
, making it possible to display any Composable
as an overlay on top of a frame.
- Changelog
- Supported platforms
- Custom builds
- Features
- Roadmap
- Architecture
- Installation
- Usage
- Third-party libraries
- Video-only media playback synchronization
📦 Previous versions
[!WARNING]
Video-only media playback synchronization doesn't work
- Code refactoring
- Performance optimizations
- Bug fixes and stability improvements
- Renderer no longer depends on video format - uses width and height instead
- Fixed incorrect argument names
- Enhanced seeking precision
- Extended external state machine
- Changed rendering behavior - renders first video frame during: preparation, playback stop, seeking
- Performance and stability enhancements
- Fixed delayed external state updates after command execution
- Fixed incorrect first frame display after seeking
- Fixed event handling issues
- Fixed improper loop stopping
- Fixed buffer cleanup errors during channel closure
- Improved internal state machine
- Improved playback synchronization
- Improved seeking
- Stable release
The library provides pre-built JARs for the following platforms:
Platform | Architecture | JAR |
---|---|---|
Windows | x64 | klarity-windows-x64-*.jar |
Linux | x64 | klarity-linux-x64-*.jar |
macOS | x64 | klarity-macos-x64-*.jar |
macOS | arm64 | klarity-macos-arm64-*.jar |
If you need support for other platforms or architectures, you can build the library from source using the CI/CD configuration as a reference for the complete build process.
- Media files probing
- Audio and video playback of media files
- Slow down and speed up playback speed without changing pitch
- Getting a preview of a media file
- Getting frames (snapshots) of a media file
- Coroutine/Flow API
While hardware-accelerated decoding is technically available through FFmpeg, its practical application is currently limited:
-
Rendering bottleneck: Decoded frames are processed through CPU-bound Skia components
-
Latency issues: This creates a pipeline bottleneck that negates the benefits of hardware decoding
-
Architectural constraints: DirectX 12 and OpenGL implementations would require compatibility components, eliminating key advantages of the current architecture
Future solution:
- Implement Vulkan-based rendering when stable support becomes available in Skia, provided it maintains the current seamless Compose integration without compatibility layers.
Klarity implements an event-driven architecture designed for Kotlin developers. It focuses on simplicity and easy integration with minimal setup.
-
JVM Layer (Kotlin):
-
Contains all business logic and state management
-
Provides a modern, coroutine-based public API
-
Uses Kotlin Flows for event-driven communication
-
Manages playback control, seeking, and synchronization
-
-
JNI Layer::
-
Bridges Kotlin code with native C++ performance
-
Handles efficient data marshaling between layers
-
Minimizes overhead for data transfer
-
-
Native Layer (C++):
-
Uses FFmpeg for video/audio decoding
-
Employs PortAudio for low-latency audio playback
-
Handles audio playback including polyphonic audio time-stretching
-
The pipeline combines FFmpeg
and Skia
to decode video frames directly into native memory. The decoded frame data is
directly interpreted as a Pixmap
via pointer reference, then written to a Skia Surface
and rendered to a
Compose Canvas
.
This efficient approach eliminates compatibility layers like SwingPanel
and enables seamless overlaying of any
Composable
on top of video content.
graph TD
KlarityPlayer --> PlayerController
PlayerController --> Pipeline
PlayerController --> BufferLoop
PlayerController --> PlaybackLoop
PlayerController --> Settings
PlayerController --> PlayerState
PlayerController --> BufferTimestamp
PlayerController --> PlaybackTimestamp
PlayerController --> Events
PlayerController --> Renderer
BufferLoop --> Pipeline
PlaybackLoop --> BufferLoop
PlaybackLoop --> Pipeline
PlaybackLoop --> Renderer
PlaybackLoop --> Settings
subgraph Media Pipeline
Pipeline --> Media
Pipeline --> AudioPipeline
Pipeline --> VideoPipeline
AudioPipeline --> AudioDecoder
AudioPipeline --> AudioBuffer
AudioPipeline --> Sampler
VideoPipeline --> VideoDecoder
VideoPipeline --> VideoBuffer
VideoPipeline --> VideoPool
end
subgraph Native Components
Sampler --> NativeSampler[C++/JNI]
AudioDecoder --> NativeDecoder[C++/JNI]
VideoDecoder --> NativeDecoder[C++/JNI]
end
subgraph Loops
BufferLoop --> BufferHandler
PlaybackLoop --> PlaybackHandler
end
subgraph Media
Media --> AudioFormat
Media --> VideoFormat
end
stateDiagram-v2
state PlayerState {
[*] --> Empty
Empty --> Preparing: Prepare
Preparing --> Ready: Success
Preparing --> Error: Error
Preparing --> Empty: Release
state Ready {
[*] --> Stopped
Stopped --> Playing: Play
Playing --> Paused: Pause
Playing --> Stopped: Stop
Playing --> Seeking: SeekTo
Playing --> Error: Error
Paused --> Playing: Resume
Paused --> Stopped: Stop
Paused --> Seeking: SeekTo
Paused --> Error: Error
Stopped --> Completed: Playback Complete
Stopped --> Seeking: SeekTo
Stopped --> Error: Error
Completed --> Stopped: Stop
Completed --> Seeking: SeekTo
Completed --> Error: Error
Seeking --> Paused: Seek Complete
Seeking --> Stopped: Stop
Seeking --> Seeking: SeekTo
Seeking --> Error: Error
}
Ready --> Releasing: Release
Releasing --> Empty: Success
Releasing --> Error: Error
Error --> Empty: Reset
}
Current State \ Action | Empty | Preparing | Releasing | Ready.Stopped | Ready.Playing | Ready.Paused | Ready.Completed | Ready.Seeking | Error |
---|---|---|---|---|---|---|---|---|---|
Empty | - | Prepare | - | - | - | - | - | - | - |
Preparing | Release | - | - | Success | - | - | - | - | Error |
Releasing | Success | - | - | - | - | - | - | - | Error |
Error | Reset | - | - | - | - | - | - | - | - |
Ready.Stopped | - | - | Release | - | Play | - | Playback Complete | SeekTo | Error |
Ready.Playing | - | - | Release | Stop | - | Pause | - | SeekTo | Error |
Ready.Paused | - | - | Release | Stop | Resume | - | - | SeekTo | Error |
Ready.Completed | - | - | Release | Stop | - | - | - | SeekTo | Error |
Ready.Seeking | - | - | Release | Stop | - | Seek Complete | - | SeekTo | Error |
Download the latest release and include jar files to your project depending on your system.
Note
Check out the example to see a full implementation in Clean Architecture using the Reduce & Conquer pattern
- The
KlarityPlayer.load()
method should be called once during the application lifecycle
KlarityPlayer.load().onFailure { t -> }.getOrThrow()
Get probe (information about a media)
val media = ProbeManager.probe("path/to/media").onFailure { t -> }.getOrThrow()
Important
Snapshot must be closed using the close()
method
val snapshots = SnapshotManager.snapshots("path/to/media") { timestamps }.getOrThrow()
snapshots.forEach { snapshot ->
renderer.render(snapshot.frame).getOrThrow()
snapshot.close().getOrThrow()
}
val snapshot = SnapshotManager.snapshot("path/to/media") { timestamp }.getOrThrow()
renderer.render(snapshot.frame).getOrThrow()
snapshot.close().getOrThrow()
Important
PreviewManager must be closed using the
close()
method
val previewManager = PreviewManager.create("path/to/media").getOrThrow()
previewManager.render(renderer, timestamp).getOrThrow()
previewManager.close().getOrThrow()
Important
KlarityPlayer
and Renderer must be closed using the
close()
method
val player = KlarityPlayer.create().getOrThrow()
val probe = ProbeManager.probe("path/to/media").getOrThrow()
val renderer = probe.videoFormat?.run { Renderer.create(width = width, height = height).getOrThrow() }
if (renderer != null) {
player.attachRenderer(renderer).getOrThrow()
}
player.prepare("path/to/media").getOrThrow()
player.play().getOrThrow()
player.stop().getOrThrow()
player.detachRenderer().getOrThrow()?.close()?.getOrThrow()
player.close().getOrThrow()
- FFmpeg - Licensed under LGPLv2.1
- PortAudio - Licensed under MIT License
- Signalsmith Stretch - Licensed under MIT License