alhazen-core
A native media engine for Rust. Give it a file or a URL; it hands you video frames on time and plays the sound. Pure-Rust decoders everywhere, the operating system's GPU decoders where available, and no GStreamer, bundled ffmpeg or browser engine.
alhazen-core has no UI dependency. Use it to build a video player in any toolkit: GPUI (see alhazen-gpui), egui, iced, Slint, a game engine, or your own renderer.
Named after Ibn al-Haytham (Alhazen, c. 965–1040), whose Book of Optics first explained how images form, the science behind every camera and screen.
use ;
let player = open?;
player.play;
// In your render loop:
if let Some = player.current_frame
Features
- Plays out of the box, on every platform: AV1, VP9 (multi-threaded), VP8, ProRes, Opus, Vorbis, FLAC, AAC (LC, HE-AAC v1/v2), MP3 and PCM in pure Rust, under permissive licences.
- Music files too:
.mp3,.aac,.flac,.wav,.oggand.opusplay natively, with their tags and cover art (Player::metadata()). - The OS's own decoders on Windows: H.264, HEVC, VP9 and AV1 on the GPU, plus AAC, MP3, AC-3, E-AC-3 and ALAC, through Media Foundation. Nothing to install; Windows covers the codec licences.
- NVIDIA GPUs on Linux: H.264, HEVC, VP8, VP9 and AV1 (8- and 10-bit) decode on the GPU through NVDEC, scaled to your display size on the GPU. Loaded from the driver at runtime: nothing to install or build.
- Everything else through the user's ffmpeg, if they have one: found at runtime and run as a separate program, so nothing is linked and ffmpeg's licence never touches your app.
- Files and the web: local files,
file://, and HTTP(S) streaming withRangeseeking and automatic reconnects. - HLS streaming, VOD and live:
.m3u8playlists with MPEG-TS, fragmented MP4 or packed-audio segments, audio in a separate playlist (even on another host), adaptive bitrate with manual override, and AES-128. Demuxed natively: no ffmpeg needed. - A/V sync done right: audio drives the clock; video follows, drops late frames and catches up after stalls. The encoder's start-up padding (MP4 edit lists, Matroska CodecDelay) is trimmed, so sound lines up with the picture, and MP3/Opus/Vorbis files play gapless (LAME delay and padding, Ogg granules).
- Smooth on big files: frames are scaled to the size you display them at before colour conversion, and a decoder that can't keep up hands over to a faster one automatically.
Contents
- Getting started
- Integrating with a UI toolkit
- Supported formats
- How decoding is chosen
- Cargo features
- Configuration
- Events and state
- Environment variables
- Platform requirements
- Performance
- Development
- License
Getting started
[]
= "0.5"
# Decoding unoptimized is many times slower than real time: optimize the decoders even in
# debug builds.
[]
= 3
[]
= 3
use Duration;
use ;
Player is Send + Sync; keep it in an Arc and call it from any thread.
| Control | Query |
|---|---|
play(), pause() |
state() |
seek(Duration) |
position(), duration(), is_seekable() |
set_volume(0.0..=1.0), set_muted(bool) |
volume(), is_muted() |
set_max_output_size(Option<(w, h)>) |
has_video(), has_audio(), video_size() |
current_frame(), events(), stats(), metadata() |
Integrating with a UI toolkit
A player in any toolkit comes down to three things:
- Draw
current_frame()every frame while playing. It returns the frame due at the current audio position. Cache the texture and re-upload only whenptschanges. - Tell the engine how big you draw:
player.set_max_output_size(Some((w, h)))with the view's size in physical pixels. 4K frames shown in a small view are then scaled down before colour conversion, which saves most of the CPU time. - Listen to
events()to repaint when paused (FrameReady), and to showWarnings and errors.
alhazen-gpui is a complete example: about 450 lines for the player entity and the element.
Supported formats
Sources
| Source | Example | Notes |
|---|---|---|
| Local file | movie.mkv, C:\Videos\a.mp4 |
|
| File URL | file:///home/me/a.webm |
|
| HTTP(S) | https://host/a.webm |
Seeks with Range requests when the server allows it; reconnects with backoff. |
| HLS | https://host/a.m3u8 |
VOD and live (see HLS). |
| DASH | https://host/a.mpd |
Recognised, not playable yet (roadmap). |
Containers
| Container | Extensions |
|---|---|
| Matroska / WebM | .mkv, .webm, .mka |
| ISO BMFF / QuickTime | .mp4, .m4v, .m4a, .mov |
| MP3 | .mp3 (gapless with a LAME/Xing header) |
| AAC (ADTS) | .aac |
| FLAC | .flac |
| WAV | .wav (PCM, float, WAVE_FORMAT_EXTENSIBLE) |
| Ogg (Vorbis, Opus, FLAC) | .ogg, .opus, .oga |
| MPEG transport stream | .ts, .m2ts (H.264, HEVC, AAC, MP3, AC-3, E-AC-3) |
Containers are recognised by their content, not their extension. Raw video streams (.h264,
.hevc) need a container for now.
HLS
Source::parse("https://host/master.m3u8") plays HTTP Live Streaming through the same Player:
- Playlists: master playlists (variants,
EXT-X-MEDIAaudio renditions) and media playlists; VOD seeks like a file; live streams start three target durations behind the edge, follow the playlist, and end when it stops updating (live streams are not seekable). - Segments: MPEG-TS, fragmented MP4 / CMAF (
EXT-X-MAP), and packed audio (ADTS/MP3 with an ID3 timestamp), with byte ranges and discontinuities (a new resolution or codec mid-stream reopens the decoder). - Separate audio: variants whose audio lives in its own playlist (
AUDIO="group"), possibly on another host, play in sync with the video;Player::audio_renditions()andset_audio_rendition(i)switch between languages. - Quality: adaptive bitrate by default (throughput weighted by download time, buffer-aware,
one step up at a time, straight down when the link drops);
Player::variants(),current_variant()andset_variant(Variant::Index(i))/set_variant(Variant::Auto);PlayerEvent::VariantChanged(i)reports switches. - Encryption:
METHOD=AES-128. DRM (SAMPLE-AES, FairPlay, Widevine, PlayReady) is refused withError::Unsupported. - Robustness: segment downloads retry with backoff; a missing segment fails VOD with an error naming it and is skipped on live streams; dropping the player never waits on a stalled download.
Not yet: subtitles (WebVTT renditions), low-latency HLS, DASH.
Tags and cover art
Player::metadata() returns the title, artist, album, album artist, track, year, genre and the
embedded cover (still encoded, with its MIME type) from ID3v2/ID3v1, Vorbis comments, FLAC
pictures, MP4 ilst, Matroska tags and attachments, and RIFF INFO.
# use ;
let player = open?;
if let Some = player.metadata
# Ok::
Video
| Codec | Pure Rust (all platforms) | Windows (Media Foundation) | Linux, NVIDIA (NVDEC) | User's ffmpeg |
|---|---|---|---|---|
| AV1 | ✅ rav1d | ✅ GPU¹ | ✅ GPU¹ | ✅ |
| VP9 | ✅ vp9-mt, multi-threaded | ✅ GPU¹ | ✅ GPU¹ | ✅ |
| VP8 | ✅ oximedia-codec | ✅ GPU¹ | ✅ | |
| Apple ProRes (422, 4444, interlaced) | ✅ oxideav-prores | ✅ | ||
| H.264 / AVC | ✅ GPU or software | ✅ GPU | ✅ | |
| H.265 / HEVC | ✅ GPU² | ✅ GPU | ✅ | |
| MJPEG, MPEG-4 Part 2, others | ✅ |
¹ Used when the GPU decodes it and prefer_hardware is on (the default); otherwise the pure-Rust
decoder is used.
² Needs the HEVC Video Extensions from the Microsoft Store (preinstalled on many PCs).
8- and 10-bit video is supported. Frames are delivered as 8-bit BGRA, converted with the stream's BT.601/BT.709 matrix and range.
Audio
| Codec | Pure Rust (all platforms) | Windows (Media Foundation) | User's ffmpeg |
|---|---|---|---|
| Opus (incl. 5.1/7.1) | ✅ | ✅ | |
| Vorbis | ✅ lewton | ✅ | |
| FLAC | ✅ claxon | ✅ | |
| PCM (8–32-bit int, 32/64-bit float) | ✅ | ✅ | |
| AAC (LC, HE-AAC v1/v2) | ✅ rusty_aac | ✅ | ✅ |
| MP3 | ✅ rusty_mp3 | ✅ | ✅ |
| AC-3, E-AC-3 | ✅ | ✅ | |
| ALAC | ✅ | ✅ |
Audio is resampled to the output device's rate and mixed to its channel layout.
If a file's audio can't be decoded, the video still plays and a Warning explains why. If its
video can't be decoded, Player::open fails with Error::UnsupportedCodec, listing the backends
it tried.
How decoding is chosen
Each stream is offered to the backends in priority order; the first that claims it decodes it.
| Backend | Platforms | Claims |
|---|---|---|
media-foundation |
Windows | H.264/HEVC/audio whenever Windows has a decoder. VP9/AV1 only with a GPU decoder (Windows' software ones are no faster than ours). |
nvdec |
Linux, NVIDIA | Video the GPU decodes at the stream's size. VP8/VP9/AV1 only with prefer_hardware (otherwise after native). |
native |
all | The pure-Rust decoders. |
ffmpeg-cli |
all | Whatever the user's ffmpeg -decoders lists. GPU decoding with -hwaccel auto. |
PlayerConfig::prefer_hardware = falseputsnativebefore the GPU decoders (media-foundation,nvdec) for VP8/VP9/AV1.- A decoder that can't decode a stream hands it to the next backend automatically, e.g. a GPU without 10-bit H.264 or 4:4:4 support. GPU decoders don't even take such streams when the file's setup data shows the variant up front.
- Automatic fallback: if a decoder falls behind (more than 100 ms late for 1.5 s), the
stream switches once to the next backend that can decode it, at the same position.
auto_fallback = falseturns this off. backend_order = Some(vec!["ffmpeg-cli", "native"])tries backends in your order. For full control, build abackend::Registryand pass it asPlayerConfig::registry.player.stats().video_backendtells you which one is decoding.
ffmpeg is looked for in PlayerConfig::ffmpeg.path, then $ALHAZEN_FFMPEG, then PATH, and on
macOS also Homebrew's and MacPorts' locations (Finder-launched apps don't get the shell's
PATH). On Windows it runs inside a Job Object, so closing the player also kills the real
ffmpeg behind a Chocolatey/Scoop shim.
Cargo features
| Feature | Default | Adds |
|---|---|---|
native |
✅ | Pure-Rust decoders (AV1, VP9, VP8, ProRes, Opus, Vorbis, FLAC, PCM), the MP4/MOV demuxer and the audio-file readers (MP3, AAC/ADTS, FLAC, WAV, Ogg). |
http |
✅ | HTTP(S) sources (ureq, rustls). |
hls |
✅ | HTTP Live Streaming (needs http and native; adds the small RustCrypto aes/cbc crates). |
audio-output |
✅ | Sound through the default output device (cpal). Without it, audio is ignored and video runs on the system clock. |
ffmpeg-cli |
✅ | The runtime ffmpeg backend. Costs nothing when ffmpeg isn't installed. |
media-foundation |
✅ | Windows' decoders. Compiles to nothing on other platforms. |
nvdec |
✅ | NVIDIA GPU decoding on Linux (NVDEC), loaded from the driver at runtime. Compiles to nothing on other platforms. |
native-aac |
✅ | AAC (LC, Main, LTP, HE-AAC v1/v2) through rusty_aac (pure Rust, Apache-2.0). |
native-mp3 |
✅ | MP3 (MPEG-1/2/2.5 Layer III) through rusty_mp3 (pure Rust, Apache-2.0). |
Configuration
PlayerConfig (all fields public; start from PlayerConfig::default()):
| Field | Default | Meaning |
|---|---|---|
autoplay |
false |
Start as soon as the first frame is ready. |
audio_output |
Default |
Default (system device), Disabled (ignore audio), or Null(..) (you pull the samples: tests, offline rendering). |
prefer_hardware |
true |
Prefer GPU decoders (Windows) over the pure-Rust VP9/AV1 decoders. |
auto_fallback |
true |
Switch backend when decoding can't keep up. |
backend_order |
None |
Backend names to try first. |
ffmpeg |
default | enabled, path, hwaccel (default true). |
max_output_size |
None |
Largest frame size to produce; larger frames are scaled down. |
decoder_threads |
cores, max 8 | Threads inside a video decoder. |
thread_pool |
shared | Rayon pool for colour conversion and scaling. |
frame_queue_len / packet_queue_len |
4 / 64 |
Buffering ahead. |
registry |
None |
A custom backend Registry. |
clock |
None |
Clock when there is no audio (tests inject a mock). |
Events and state
PlayerState: Loading → Buffering → Playing ⇄ Paused → Ended, or Error(e) at any
point. Seeking passes through Buffering.
PlayerEvent |
When |
|---|---|
StateChanged(state) |
Every state change. |
FrameReady |
The first frame after opening or seeking is ready; repaint even when paused. |
Warning(String) |
Something non-fatal: an audio track without a decoder, no audio device, a backend fallback. |
Error(e) |
Playback stopped with an error. |
Ended |
The end was reached. |
VariantChanged(i) |
HLS: playback moved to variant i (adaptive bitrate or set_variant). |
Environment variables
| Variable | Effect |
|---|---|
ALHAZEN_FFMPEG=/path/to/ffmpeg |
The ffmpeg program to use (when PlayerConfig::ffmpeg.path isn't set). |
RUST_LOG=alhazen_core=debug |
Library logging through the log crate, with any logger. |
Platform requirements
- Rust 1.92+.
- x86-64 with the
nativefeature:nasmonPATH; rav1d assembles its fast AV1 code with it (apt install nasm,brew install nasm,winget install NASM.NASM). - Linux with
audio-output: ALSA headers (libasound2-dev/alsa-lib). - Windows: Windows 10 or 11. Media Foundation is missing from "N" editions until the Media Feature Pack is installed; the engine then falls back to the other backends.
- Linux GPU decoding: an NVIDIA GPU and its proprietary driver; nothing else at build or run time. Without it, playback uses the other backends. (Intel/AMD through VA-API is planned.) The GPU is only woken up once a video needs it, so audio-only playback leaves a laptop's discrete GPU asleep.
- ffmpeg (optional, at runtime): 4.0+. CI tests 6–9 on Linux, Windows and macOS.
Performance
- VP9: vp9-mt decodes tile columns in parallel, bit-identical to libvpx: 45 fps vs 17.5 fps single-threaded on a 4K 10-bit stream.
- GPU on Windows: H.264/HEVC/VP9/AV1 decode on the GPU's video engine; frames are copied back once (NV12/P010) and converted on the CPU.
- GPU on Linux (NVIDIA): frames are decoded and scaled to the display size on the GPU, so only display-sized frames are copied back and converted.
- Scale first: a 4K frame shown at 1280×720 is scaled down in YUV before conversion.
- Catch-up: after a stall, frames more than 50 ms late are skipped (one is still shown per 100 ms), so video rejoins the audio quickly.
Development
VP9_VECTORS_DIR=target/vp9-vectors
- Test fixtures and the ffmpeg commands that made them:
tests/fixtures/README.md. - On Windows,
cargo run --release --example mf-check -- tests/fixturesreports, per codec, which Media Foundation decoder ran (GPU or software) and whether it decoded correctly.mf-check --native-type <file>prints the media types Windows' own demuxer reports. - CI runs on Linux, Windows and macOS, and checks that the default build stays permissively licensed.
License
Apache License, Version 2.0 (LICENSE). If you distribute software that contains alhazen-core, include the NOTICE text (Apache-2.0 §4(d)), for example in an "About" or "Licenses" screen.
The default build depends only on permissively licensed crates (MIT, Apache-2.0, BSD).
AAC patents: AAC-LC's have largely expired; HE-AAC and Parametric Stereo patents may still apply in
some countries, and rusty_aac's licence grants no patent rights. Apps with that concern can turn
off native-aac. ffmpeg-cli links nothing: ffmpeg's licence applies to
the ffmpeg program the user installed, not to your app; don't bundle an ffmpeg build without
checking its licence and patent terms.
Part of Alhazen.