scan_with() collapsed every failure path from resolve_encryption() into None via .ok(), so callers couldn't tell the difference between "no KEYDB found", "KEYDB failed to parse", "disc hash not in KEYDB and fallback derivation failed", "AACS files unreadable on disc", and a handshake that rejected every host cert. autorip's UI was stuck printing "no decryption keys found (check KEYDB)" for all of them, which is a particularly bad message when the user has actually loaded a KEYDB and the real failure is something else. Changes: - New pub field Disc.aacs_error: Option<Error>. Populated by scan_with whenever encrypted && aacs.is_none(). Sentinel KeydbLoad path "<no keydb in search paths>" distinguishes the no-keydb case from a real load failure without adding a new Error variant (which would be a breaking change for downstream exhaustive matches). - tracing::warn in scan_with at scan_aacs_resolve_failed and scan_aacs_no_keydb, with error_code and keydb path for grepping. - tracing in do_handshake: keydb load failure, host-cert exhaustion (with cert count and last error code), VID read failure post-auth, and a debug-level success log. Lets us see whether handshake even got off the ground for a given disc. Test fixtures updated to set aacs_error: None.
libfreemkv
Rust library for 4K UHD / Blu-ray / DVD optical drives. Drive access, disc scanning, stream labels, AACS decryption, CSS decryption, KEYDB updates, and content reading in one crate. Bundled drive profiles — no external files needed.
12+ MB/s sustained read speeds on BD. Full init: unlock, firmware upload, speed calibration — all from pure Rust.
Multi-lingual by design — the library outputs structured data and numeric error codes, never English text. Build any UI or localization on top.
API Documentation · Technical Docs
Part of the freemkv project.
Install
[dependencies]
libfreemkv = "0.18"
Quick Start
use libfreemkv::{Drive, Disc, ScanOptions};
use std::path::Path;
// Open drive — profiles are bundled, auto-identified
let mut drive = Drive::open(Path::new("/dev/sg4"))?;
drive.wait_ready()?; // wait for disc
drive.init()?; // unlock + firmware upload
drive.probe_disc()?; // probe disc surface for optimal speeds
// Scan disc — UDF, playlists, streams, AACS (all automatic)
let disc = Disc::scan(&mut drive, &ScanOptions::default())?;
for title in &disc.titles {
println!("{} — {} streams", title.duration_display(), title.streams.len());
}
// Stream pipeline — read PES frames from any source, write to any output
let opts = libfreemkv::InputOptions::default();
let mut input = libfreemkv::input("iso://Disc.iso", &opts)?;
let title = input.info().clone();
let mut output = libfreemkv::output("mkv://Movie.mkv", &title)?;
while let Ok(Some(frame)) = input.read() {
output.write(&frame)?;
}
output.finish()?;
Multi-pass recovery rip
For damaged discs the library exposes two flat verbs — Disc::sweep for the
forward Pass 1 and Disc::patch for retrying bad ranges. The library never
loops; the multipass policy is the caller's job. See
docs/rip-recovery.md and the design notes in
freemkv-private/memory/0_18_redesign.md.
use libfreemkv::{SweepOptions, PatchOptions};
use libfreemkv::disc::{mapfile, mapfile_path_for};
use std::path::Path;
let iso = Path::new("disc.iso");
// Pass 1: disc → ISO. Skip-on-error, zero-fill, write the sidecar mapfile.
disc.sweep(&mut drive, iso, &SweepOptions {
decrypt: true,
resume: false,
batch_sectors: None,
skip_on_error: true,
progress: None,
halt: None,
})?;
// Pass 2..N: retry every non-finished range. Idempotent.
loop {
let map = mapfile::Mapfile::load(&mapfile_path_for(iso))?;
let stats = map.stats();
if stats.bytes_pending + stats.bytes_unreadable == 0 { break; }
let outcome = disc.patch(&mut drive, iso, &PatchOptions {
decrypt: true,
block_sectors: None,
full_recovery: true,
reverse: true,
wedged_threshold: 50,
progress: None,
halt: None,
})?;
if outcome.bytes_recovered_this_pass == 0 { break; }
}
// Mux from the ISO via the normal stream pipeline (no drive involvement).
What It Does
- Drive access — open, identify, unlock, firmware upload, speed calibration, eject
- 12+ MB/s reads — auto-detects kernel transfer limits, sustained full speed
- Disc scanning — UDF 2.50 filesystem, MPLS playlists, CLPI clip info
- Stream labels — 5 BD-J format parsers (Paramount, Criterion, Pixelogic, CTRM, Deluxe)
- AACS decryption — transparent key resolution and content decrypt (1.0 + 2.0 bus decryption)
- KEYDB updates — download, verify, save from any HTTP URL (zero deps, raw TCP)
- Content reading — adaptive batch reads with automatic decryption
- Stream I/O — unified stream pipeline for reading and writing any format
Streams
| Stream | Input | Output | Transport |
|---|---|---|---|
| DiscStream | Yes | -- | Optical drive via SCSI |
| IsoStream | Yes | -- | Blu-ray ISO image file (read via stream pipeline; written via Disc::sweep()) |
| MkvStream | Yes | Yes | Matroska container |
| M2tsStream | Yes | Yes | BD transport stream with FMKV metadata header |
| NetworkStream | Yes (listen) | Yes (connect) | TCP with FMKV metadata header |
| StdioStream | Yes (stdin) | Yes (stdout) | Raw byte pipe |
| NullStream | -- | Yes | Discard sink (byte counter for benchmarks) |
Streams implement FrameSource (read) and/or FrameSink (write); direction is type-checked. input() / output() resolve URL strings to PES stream instances. All URLs use the scheme://path format — bare paths are rejected.
AACS decryption requires a KEYDB.cfg file. If available at ~/.config/aacs/KEYDB.cfg or passed via ScanOptions, the library handles everything — handshake, key derivation, and per-sector decryption — without the application needing to know anything about encryption.
Architecture
Drive — open, identify, init, unlock, single-shot read
├── ScsiTransport — SG_IO (Linux), IOKit (macOS), SPTI (Windows)
├── DriveProfile — per-drive unlock parameters (bundled)
└── PlatformDriver — MediaTek (supported), Renesas (planned)
Disc — scan titles, streams, AACS/CSS state
├── UDF reader — Blu-ray UDF 2.50 with metadata partitions
├── MPLS parser — playlists → titles + clips + streams
├── CLPI parser — clip info → EP map → sector extents
├── IFO parser — DVD title sets, PGC chains, cell addresses
├── Labels — 5 BD-J format parsers (detect + parse)
├── AACS — key resolution + content decryption
├── CSS — DVD CSS cipher (table-driven, no keys needed)
└── KEYDB — download + verify + save
Streams — unified PES pipeline
├── FrameSource — read() PES frames (direction-typed)
├── FrameSink — write() PES frames (direction-typed)
├── DiscStream — sectors → decrypt → TS demux → PES
├── IsoStream — ISO file → decrypt → TS demux → PES
├── MkvStream — MKV mux/demux
├── M2tsStream — BD transport stream
├── NetworkStream — TCP with FMKV metadata header
├── StdioStream — stdin/stdout pipe
└── NullStream — discard sink
See docs/ for detailed technical documentation on each module.
Error Codes
All errors are structured with numeric codes. No user-facing English text — applications format their own messages.
| Range | Category |
|---|---|
| E1xxx | Device errors (not found, permission) |
| E2xxx | Profile errors (unsupported drive) |
| E3xxx | Unlock errors (failed, signature) |
| E4xxx | SCSI errors (command failed, timeout) |
| E5xxx | I/O errors |
| E6xxx | Disc format errors |
| E7xxx | AACS errors |
| E8xxx | KEYDB update errors |
Platform Support
| Platform | Status | Backend |
|---|---|---|
| Linux | Supported | SG_IO ioctl |
| macOS | Supported | IOKit SCSITask |
| Windows | Supported | SPTI |
Contributing
Run freemkv info disc:// --share with the freemkv CLI to contribute your drive's profile.
License
AGPL-3.0-only