| 🌟 | Support this project |
|---|---|
bc1qs6qq0fkqqhp4whwq8u8zc5egprakvqxewr5pmx |
|
0x3147bEE3179Df0f6a0852044BFe3C59086072e12 |
|
TKznmR65yhPt5qmYCML4tNSWFeeUkgYSEV |
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

