Add design docs for API and MKV muxer architecture
This commit is contained in:
@@ -0,0 +1,203 @@
|
|||||||
|
# libfreemkv API Design
|
||||||
|
|
||||||
|
## Principles
|
||||||
|
|
||||||
|
1. Lib provides building blocks. App composes them.
|
||||||
|
2. No English text in lib. Error codes only. App handles i18n.
|
||||||
|
3. No display logic in lib. App decides what to show.
|
||||||
|
4. Streams are the pipeline. Each stage wraps the next.
|
||||||
|
5. Lib fires events. App listens.
|
||||||
|
|
||||||
|
## Core API
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Open drive — explicit steps, app prints between them
|
||||||
|
let mut session = DriveSession::open(path)?;
|
||||||
|
session.wait_ready()?;
|
||||||
|
session.init()?;
|
||||||
|
session.probe_disc()?;
|
||||||
|
|
||||||
|
// Scan disc
|
||||||
|
let disc = Disc::scan(&mut session, &ScanOptions::default())?;
|
||||||
|
|
||||||
|
// Browse
|
||||||
|
disc.titles // Vec<Title>
|
||||||
|
disc.format // BD / UHD / DVD
|
||||||
|
disc.capacity_gb()
|
||||||
|
|
||||||
|
// Rip with events
|
||||||
|
disc.rip(&mut session, 0, output, |event| {
|
||||||
|
match event.kind {
|
||||||
|
EventKind::BytesRead { bytes, total } => ...,
|
||||||
|
EventKind::ReadError { sector, error } => ...,
|
||||||
|
EventKind::Retry { attempt } => ...,
|
||||||
|
EventKind::SpeedChange { speed_kbs } => ...,
|
||||||
|
EventKind::Complete { bytes, errors } => ...,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Rip without events
|
||||||
|
disc.rip(&mut session, 0, output, event::ignore)?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stream Chains
|
||||||
|
|
||||||
|
Each stream wraps the next. Builder pattern, no `.build()`.
|
||||||
|
|
||||||
|
### Raw m2ts
|
||||||
|
```rust
|
||||||
|
disc.rip(&mut session, 0, File::create("movie.m2ts")?, event::ignore)?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### MKV
|
||||||
|
```rust
|
||||||
|
let output = MkvStream::new(File::create("movie.mkv")?)
|
||||||
|
.title(&disc.titles[0])
|
||||||
|
.max_buffer(10 * 1024 * 1024);
|
||||||
|
|
||||||
|
disc.rip(&mut session, 0, output, |e| { ... })?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### MKV with progress (CLI)
|
||||||
|
```rust
|
||||||
|
let output = ProgressStream::new(
|
||||||
|
MkvStream::new(File::create("movie.mkv")?)
|
||||||
|
.title(&disc.titles[0])
|
||||||
|
.max_buffer(10 * 1024 * 1024),
|
||||||
|
total_bytes,
|
||||||
|
|pct, speed| eprint!("\r {}% {:.1} MB/s", pct, speed),
|
||||||
|
);
|
||||||
|
|
||||||
|
disc.rip(&mut session, 0, output, |e| { ... })?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Future: transcode
|
||||||
|
```rust
|
||||||
|
let output = ProgressStream::new(
|
||||||
|
TranscodeStream::new(
|
||||||
|
MkvStream::new(File::create("movie.mkv")?)
|
||||||
|
.title(&disc.titles[0])
|
||||||
|
.max_buffer(50 * 1024 * 1024),
|
||||||
|
)
|
||||||
|
.codec(H265)
|
||||||
|
.quality(22),
|
||||||
|
total_bytes,
|
||||||
|
|pct, speed| eprint!("\r {}% {:.1} MB/s", pct, speed),
|
||||||
|
);
|
||||||
|
|
||||||
|
disc.rip(&mut session, 0, output, |e| { ... })?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
Lib fires events during operations. App provides a callback. No display, no text.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct Event {
|
||||||
|
pub kind: EventKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum EventKind {
|
||||||
|
BytesRead { bytes: u64, total: u64 },
|
||||||
|
ReadError { sector: u64, error: Error },
|
||||||
|
Retry { attempt: u32 },
|
||||||
|
SpeedChange { speed_kbs: u16 },
|
||||||
|
ExtentStart { index: usize, start_sector: u64, sector_count: u64 },
|
||||||
|
Complete { bytes: u64, errors: u32 },
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Events report what happened. App decides what to do. GUI shows a dialog. CLI prints a line. Server logs to file.
|
||||||
|
|
||||||
|
## Error Codes
|
||||||
|
|
||||||
|
Lib errors are codes, not messages. Like HTTP status codes.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub enum Error {
|
||||||
|
// Drive
|
||||||
|
DriveNotFound,
|
||||||
|
DriveOpenFailed,
|
||||||
|
DriveNotReady,
|
||||||
|
|
||||||
|
// Unlock
|
||||||
|
UnlockFailed,
|
||||||
|
NoProfile,
|
||||||
|
|
||||||
|
// AACS
|
||||||
|
AacsNoKeys,
|
||||||
|
AacsCertVerifyFailed,
|
||||||
|
AacsAgidAllocFailed,
|
||||||
|
AacsHandshakeFailed,
|
||||||
|
AacsVidMacFailed,
|
||||||
|
|
||||||
|
// Disc
|
||||||
|
DiscReadError { sector: u64 },
|
||||||
|
MplsParseError,
|
||||||
|
ClpiParseError,
|
||||||
|
UdfFileNotFound { path: String },
|
||||||
|
|
||||||
|
// Mux
|
||||||
|
LookaheadOverflow,
|
||||||
|
MuxWriteError,
|
||||||
|
|
||||||
|
// SCSI
|
||||||
|
ScsiError { sense: u8 },
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
App maps codes to localized strings. Lib never contains display text.
|
||||||
|
|
||||||
|
## Streams the Lib Provides
|
||||||
|
|
||||||
|
| Stream | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| MkvStream | BD-TS → MKV (demux + mux) |
|
||||||
|
|
||||||
|
CLI provides:
|
||||||
|
| Stream | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| ProgressStream | Byte counting + progress callback |
|
||||||
|
| Future: TranscodeStream | Re-encode video |
|
||||||
|
|
||||||
|
Any `impl Write` works as a stream. Third-party apps create their own.
|
||||||
|
|
||||||
|
## MkvStream Internals
|
||||||
|
|
||||||
|
LookaheadBuffer (default 5MB, configurable):
|
||||||
|
1. Phase 1: buffer incoming data, scan for codec setup (SPS/PPS)
|
||||||
|
2. Found it? Write MKV header, flush buffer, switch to streaming
|
||||||
|
3. Buffer full? Error — app handles it
|
||||||
|
4. Phase 2: parse TS → frames → MKV clusters, direct to output
|
||||||
|
|
||||||
|
## File Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
libfreemkv/src/
|
||||||
|
├── lib.rs Public exports
|
||||||
|
├── error.rs Error codes (no English)
|
||||||
|
├── event.rs Event types for callbacks
|
||||||
|
├── drive.rs DriveSession (open, init, read)
|
||||||
|
├── disc.rs Disc (scan, rip, titles)
|
||||||
|
├── scsi/ SCSI transport (Linux, macOS)
|
||||||
|
├── platform/ Drive unlock (MT1959 A/B)
|
||||||
|
├── aacs/ AACS decryption
|
||||||
|
├── udf.rs UDF filesystem parser
|
||||||
|
├── mpls.rs Playlist parser
|
||||||
|
├── clpi.rs Clip info parser
|
||||||
|
├── mux/
|
||||||
|
│ ├── stream.rs MkvStream (builder pattern, impl Write)
|
||||||
|
│ ├── lookahead.rs LookaheadBuffer (generic, reusable)
|
||||||
|
│ ├── ts.rs BD-TS demuxer
|
||||||
|
│ ├── ebml.rs EBML primitives
|
||||||
|
│ ├── mkv.rs MKV muxer
|
||||||
|
│ └── codec/ Frame parsers (H.264, HEVC, VC-1, AC3, DTS, TrueHD, PGS)
|
||||||
|
└── ...
|
||||||
|
|
||||||
|
freemkv/src/
|
||||||
|
├── main.rs CLI entry, command routing
|
||||||
|
├── rip.rs Rip command (streams + progress)
|
||||||
|
├── remux.rs Remux command (m2ts → MKV, no drive)
|
||||||
|
├── info.rs Drive info display
|
||||||
|
└── disc_info.rs Disc info display
|
||||||
|
```
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
# MKV Native Muxer — Architecture
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Replace raw m2ts output with in-pipeline MKV muxing.
|
||||||
|
Disc → TS demux → MKV mux → .mkv file. One pass, no temp files.
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
ContentReader::read_batch() returns &[u8] of raw BD transport stream
|
||||||
|
↓
|
||||||
|
TsDemuxer::feed(batch) parses 192-byte BD-TS packets, extracts PES
|
||||||
|
↓
|
||||||
|
PES reassembly per PID builds complete PES packets with PTS/DTS
|
||||||
|
↓
|
||||||
|
ElementaryStreamParser per track finds frame boundaries, extracts codec headers
|
||||||
|
↓
|
||||||
|
MkvMuxer::write_frame(track, pts, data) writes EBML clusters + blocks
|
||||||
|
↓
|
||||||
|
.mkv file on disk
|
||||||
|
```
|
||||||
|
|
||||||
|
## Current Integration Point
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// rip.rs line ~287
|
||||||
|
match reader.read_batch() {
|
||||||
|
Ok(Some(batch)) => {
|
||||||
|
writer.write_all(batch)?; // ← replace with muxer.feed(batch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Becomes:
|
||||||
|
```rust
|
||||||
|
match reader.read_batch() {
|
||||||
|
Ok(Some(batch)) => {
|
||||||
|
muxer.feed(batch)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### 1. BD Transport Stream Demuxer (`ts.rs`)
|
||||||
|
|
||||||
|
BD uses 192-byte packets (not standard 188):
|
||||||
|
```
|
||||||
|
[0-3] TP_extra_header: 2-bit copy_permission + 30-bit arrival_time_stamp
|
||||||
|
[4] Sync byte: 0x47
|
||||||
|
[5] TEI + PUSI + priority + PID[12:8]
|
||||||
|
[6] PID[7:0]
|
||||||
|
[7] Scrambling + adaptation + continuity_counter
|
||||||
|
[8..] Adaptation field (if present) + payload
|
||||||
|
```
|
||||||
|
|
||||||
|
API:
|
||||||
|
```rust
|
||||||
|
pub struct TsDemuxer {
|
||||||
|
pes_assemblers: HashMap<u16, PesAssembler>, // PID → assembler
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TsDemuxer {
|
||||||
|
pub fn new(pids: &[u16]) -> Self;
|
||||||
|
pub fn feed(&mut self, data: &[u8]) -> Vec<PesPacket>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PesPacket {
|
||||||
|
pub pid: u16,
|
||||||
|
pub pts: Option<i64>, // 90kHz ticks
|
||||||
|
pub dts: Option<i64>, // 90kHz ticks
|
||||||
|
pub data: Vec<u8>, // elementary stream data
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Elementary Stream Parsers (`codec/`)
|
||||||
|
|
||||||
|
Each codec parser finds frame boundaries and extracts initialization data.
|
||||||
|
|
||||||
|
**H.264 (`codec/h264.rs`):**
|
||||||
|
- Parse NAL units (start code 00 00 01 or 00 00 00 01)
|
||||||
|
- Extract SPS + PPS for codecPrivate
|
||||||
|
- Frame boundary = Access Unit Delimiter (NAL type 9) or SPS
|
||||||
|
|
||||||
|
**HEVC (`codec/hevc.rs`):**
|
||||||
|
- Parse NAL units
|
||||||
|
- Extract VPS + SPS + PPS for codecPrivate
|
||||||
|
- Frame boundary = VCL NAL with first_slice_segment_in_pic_flag
|
||||||
|
|
||||||
|
**AC3/EAC3 (`codec/ac3.rs`):**
|
||||||
|
- Syncword 0x0B77
|
||||||
|
- Parse frame size from header
|
||||||
|
- No codecPrivate needed (or minimal)
|
||||||
|
|
||||||
|
**DTS (`codec/dts.rs`):**
|
||||||
|
- Syncword 0x7FFE8001
|
||||||
|
- Parse frame size
|
||||||
|
- No codecPrivate needed
|
||||||
|
|
||||||
|
**TrueHD (`codec/truehd.rs`):**
|
||||||
|
- Major sync: 0xF8726FBA
|
||||||
|
- Access unit = major sync + minor syncs
|
||||||
|
- AC3 core embedded in first substream
|
||||||
|
|
||||||
|
**LPCM (`codec/lpcm.rs`):**
|
||||||
|
- Fixed frame sizes based on sample rate + channels
|
||||||
|
- Header describes format
|
||||||
|
|
||||||
|
**PGS (`codec/pgs.rs`):**
|
||||||
|
- Segment types: PCS, WDS, PDS, ODS, END
|
||||||
|
- Each segment is a complete unit
|
||||||
|
- No codecPrivate needed
|
||||||
|
|
||||||
|
### 3. MKV/EBML Muxer (`mkv.rs`)
|
||||||
|
|
||||||
|
Matroska uses EBML (Extensible Binary Meta Language).
|
||||||
|
|
||||||
|
**EBML primitives:**
|
||||||
|
- Variable-length element ID (1-4 bytes)
|
||||||
|
- Variable-length size (1-8 bytes)
|
||||||
|
- Data: uint, int, float, string, UTF-8, binary, date
|
||||||
|
|
||||||
|
**MKV structure:**
|
||||||
|
```
|
||||||
|
EBML Header
|
||||||
|
Segment
|
||||||
|
├── SeekHead (index of top-level elements)
|
||||||
|
├── Info (title, duration, muxing app)
|
||||||
|
├── Tracks (one entry per stream)
|
||||||
|
│ ├── TrackEntry (video)
|
||||||
|
│ │ ├── CodecID: "V_MPEG4/ISO/AVC" or "V_MPEGH/ISO/HEVC"
|
||||||
|
│ │ ├── CodecPrivate: SPS+PPS (H.264) or VPS+SPS+PPS (HEVC)
|
||||||
|
│ │ └── Video: PixelWidth, PixelHeight, DisplayWidth, DisplayHeight
|
||||||
|
│ ├── TrackEntry (audio)
|
||||||
|
│ │ ├── CodecID: "A_AC3" or "A_TRUEHD" or "A_DTS"
|
||||||
|
│ │ └── Audio: SamplingFrequency, Channels, BitDepth
|
||||||
|
│ └── TrackEntry (subtitle)
|
||||||
|
│ └── CodecID: "S_HDMV/PGS"
|
||||||
|
├── Chapters (optional, from MPLS chapter marks)
|
||||||
|
├── Cluster (every ~5 seconds)
|
||||||
|
│ ├── Timestamp (cluster base time)
|
||||||
|
│ ├── SimpleBlock (track, relative_ts, data)
|
||||||
|
│ ├── SimpleBlock ...
|
||||||
|
│ └── ...
|
||||||
|
├── Cluster ...
|
||||||
|
├── Cues (seek index, written at end)
|
||||||
|
└── Tags (metadata)
|
||||||
|
```
|
||||||
|
|
||||||
|
**API:**
|
||||||
|
```rust
|
||||||
|
pub struct MkvMuxer<W: Write + Seek> {
|
||||||
|
writer: W,
|
||||||
|
tracks: Vec<MkvTrack>,
|
||||||
|
cluster_start: Option<i64>,
|
||||||
|
cue_points: Vec<CuePoint>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: Write + Seek> MkvMuxer<W> {
|
||||||
|
pub fn new(writer: W, tracks: &[MkvTrack]) -> Result<Self>;
|
||||||
|
pub fn write_frame(&mut self, track_idx: usize, pts_ns: i64, keyframe: bool, data: &[u8]) -> Result<()>;
|
||||||
|
pub fn finish(self) -> Result<()>; // writes Cues + fixes SeekHead
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Pipeline Glue (`mux.rs`)
|
||||||
|
|
||||||
|
Ties everything together:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct MuxPipeline<W: Write + Seek> {
|
||||||
|
demuxer: TsDemuxer,
|
||||||
|
parsers: HashMap<u16, Box<dyn CodecParser>>,
|
||||||
|
muxer: MkvMuxer<W>,
|
||||||
|
pid_to_track: HashMap<u16, usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: Write + Seek> MuxPipeline<W> {
|
||||||
|
pub fn new(writer: W, streams: &[Stream]) -> Result<Self>;
|
||||||
|
pub fn feed(&mut self, ts_data: &[u8]) -> Result<()>;
|
||||||
|
pub fn finish(self) -> Result<()>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
libfreemkv/src/
|
||||||
|
├── mux/
|
||||||
|
│ ├── mod.rs MuxPipeline (glue)
|
||||||
|
│ ├── ts.rs BD-TS demuxer (192-byte packets)
|
||||||
|
│ ├── ebml.rs EBML primitives (write variable-length ints)
|
||||||
|
│ ├── mkv.rs MKV muxer (Segment, Tracks, Clusters)
|
||||||
|
│ └── codec/
|
||||||
|
│ ├── mod.rs CodecParser trait
|
||||||
|
│ ├── h264.rs H.264 NAL parser
|
||||||
|
│ ├── hevc.rs HEVC NAL parser
|
||||||
|
│ ├── ac3.rs AC3/EAC3 frame parser
|
||||||
|
│ ├── dts.rs DTS frame parser
|
||||||
|
│ ├── truehd.rs TrueHD/Atmos parser
|
||||||
|
│ ├── lpcm.rs LPCM frame parser
|
||||||
|
│ └── pgs.rs PGS subtitle parser
|
||||||
|
```
|
||||||
|
|
||||||
|
## MKV Codec IDs
|
||||||
|
|
||||||
|
| Our Codec | MKV CodecID | codecPrivate |
|
||||||
|
|-----------|------------|--------------|
|
||||||
|
| H264 | V_MPEG4/ISO/AVC | AVCDecoderConfigurationRecord (SPS+PPS) |
|
||||||
|
| Hevc | V_MPEGH/ISO/HEVC | HEVCDecoderConfigurationRecord (VPS+SPS+PPS) |
|
||||||
|
| Vc1 | V_MS/VFW/FOURCC | BITMAPINFOHEADER |
|
||||||
|
| Mpeg2 | V_MPEG2 | sequence_header |
|
||||||
|
| Ac3 | A_AC3 | none |
|
||||||
|
| Ac3Plus | A_EAC3 | none |
|
||||||
|
| TrueHd | A_TRUEHD | none |
|
||||||
|
| DtsHdMa | A_DTS | none (core + extension) |
|
||||||
|
| Dts | A_DTS | none |
|
||||||
|
| Lpcm | A_PCM/INT/BIG | none |
|
||||||
|
| Pgs | S_HDMV/PGS | none |
|
||||||
|
|
||||||
|
## Timestamps
|
||||||
|
|
||||||
|
BD uses 90kHz PTS/DTS. MKV uses nanoseconds.
|
||||||
|
Conversion: `ns = pts * 1_000_000_000 / 90_000` = `pts * 100_000 / 9`
|
||||||
|
|
||||||
|
MKV TimestampScale default = 1,000,000 (1ms precision).
|
||||||
|
For BD content, 1ms is sufficient.
|
||||||
|
|
||||||
|
## Build Order
|
||||||
|
|
||||||
|
1. `ebml.rs` — EBML write primitives (smallest, no dependencies)
|
||||||
|
2. `ts.rs` — BD-TS demuxer (parse 192-byte packets, PES assembly)
|
||||||
|
3. `codec/ac3.rs` — simplest codec parser (fixed syncword)
|
||||||
|
4. `mkv.rs` — MKV muxer (header, tracks, clusters, blocks)
|
||||||
|
5. `mux.rs` — pipeline glue
|
||||||
|
6. Test with AC3-only stream (simplest case)
|
||||||
|
7. `codec/h264.rs` — video parser (NAL units, SPS/PPS)
|
||||||
|
8. Full BD rip test (video + audio + subs)
|
||||||
|
9. Remaining codecs (HEVC, DTS, TrueHD, PGS, LPCM, VC-1)
|
||||||
Reference in New Issue
Block a user