Skip to content

Vozduxan Engine (C++)

vozduxan is a custom C++ library that wraps libtorrent for low-latency audio streaming. It handles everything from magnet/torrent resolution to piece-priority scheduling to serving the audio stream over a local HTTP server. The Rust side never touches libtorrent directly — it only calls through the C API declared in vozduxan_ffi.rs.


C API surface

The library is linked as a static archive (libvozduxan.a) and exposes a flat C ABI:

#[link(name = "vozduxan", kind = "static")]
unsafe extern "C" {
    pub fn vozduxan_session_create(config: *const VozduxanConfig) -> *mut VozduxanSession;
    pub fn vozduxan_session_destroy(session: *mut VozduxanSession);

    pub fn vozduxan_stream_prepare(
        session: *mut VozduxanSession,
        magnet: *const c_char,
        torrent_data: *const u8,
        torrent_len: usize,
        file_idx: c_int,
        is_main: c_int,
        progress_fn: VozduxanProgressFn,
        userdata: *mut c_void,
    ) -> VozduxanStreamInfo;

    pub fn vozduxan_stream_notify_position(
        session: *mut VozduxanSession,
        token: *const c_char,
        byte_offset: i64,
    );
    pub fn vozduxan_stream_release(session: *mut VozduxanSession, token: *const c_char);
    pub fn vozduxan_list_files(
        session: *mut VozduxanSession,
        magnet: *const c_char,
        torrent_data: *const u8,
        torrent_len: usize,
    ) -> VozduxanFileList;
    pub fn vozduxan_file_list_free(list: *mut VozduxanFileList);
    pub fn vozduxan_session_evict(session: *mut VozduxanSession);
    pub fn vozduxan_session_http_port(session: *mut VozduxanSession) -> u16;
}

VozduxanSession is an opaque C++ object. The Rust side holds it as *mut VozduxanSession inside VozduxanSessionInner and never dereferences it — only passes it back to C functions.


Session creation

vozduxan_session_create takes a VozduxanConfig:

#[repr(C)]
pub struct VozduxanConfig {
    pub storage_path:    *const c_char,  // path for libtorrent state & piece cache
    pub cache_max_bytes: u64,            // 0 = 50 GB default
    pub cache_ttl_secs:  u32,            // 0 = 3600 s default
    pub listen_port:     c_int,          // 0 = random (OS picks)
    pub log_fn:          VozduxanLogFn,  // optional log callback (NULL = stderr only)
    pub log_userdata:    *mut c_void,    // forwarded verbatim to log_fn
}

In neegde all fields use defaults (zeros) except storage_path and the log callback. The storage path is bt/vozduxan/ relative to the executable (see Data Directory).

The log callback is wired to AppDebugLog — every C++ log line goes through:

unsafe extern "C" fn on_vozduxan_log(message: *const c_char, userdata: *mut c_void) {
    let debug_log = unsafe { &*(userdata as *const AppDebugLog) };
    let msg = CStr::from_ptr(message).to_string_lossy().into_owned();
    // Suppress high-frequency peer-connection noise
    let lower = msg.to_ascii_lowercase();
    if lower.contains("connecttopeer") || lower.contains("connect to peer") {
        return;
    }
    debug_log.push("vozduxan", msg, None);
}

The raw pointer in VozduxanConfig.log_userdata points into the Arc<AppDebugLog> that VozduxanSessionInner keeps alive in its _debug_log_arc field. The Arc must not be dropped while the C++ session exists — if it were, the pointer would dangle and the log callback would write into freed memory.


Stream preparation

vozduxan_stream_prepare is the primary entry point for playback. It is a blocking call — it does not return until the stream is ready to serve. On the Rust side it always runs inside tokio::task::spawn_blocking.

magnet URI  ─┐
.torrent bytes ─┤──→ vozduxan_stream_prepare ──→ VozduxanStreamInfo
file_idx    ─┘         (blocks until prebuffer full)

Two input modes

Mode Condition Behaviour
Magnet + DHT torrent_data == NULL vozduxan resolves metadata via DHT; may take 30–90 s on cold start
Torrent file torrent_data != NULL metadata is available immediately; metadata_received_alert is not fired

For RuTracker content, the Rust layer downloads the .torrent file and passes it as base64. The Tauri command decodes it before calling into vozduxan:

let torrent_bytes: Option<Vec<u8>> = match torrent_file_b64.as_deref() {
    None | Some("") => None,
    Some(s) => Some(
        base64::engine::general_purpose::STANDARD
            .decode(s.trim())
            .map_err(|e| format!("bad base64: {e}"))?,
    ),
};

This avoids the DHT metadata wait entirely.

Known invariant

When torrent_data is provided, libtorrent does not emit metadata_received_alert because the metadata is already set on the add_torrent_params struct. vozduxan handles this case internally in C++. Do not add a workaround on the Rust side.

is_main flag

is_main = 1 signals active playback — vozduxan cancels any in-flight fast-start for the same torrent and begins a fresh one at the requested file. is_main = 0 is used for background prefetch: the session warms up the file without disrupting any current download.

Return value

#[repr(C)]
pub struct VozduxanStreamInfo {
    pub url:       [u8; 512],  // "http://127.0.0.1:PORT/stream/TOKEN\0"
    pub token:     [u8; 128],  // opaque stream identifier, used in subsequent calls
    pub file_size: i64,
    pub mime_type: [u8; 64],
    pub error:     VozduxanError,
    pub error_msg: [u8; 256],
}

On success, error == VozduxanError::Ok and url contains the full localhost URL that is handed to the <audio> element. The token string identifies the stream for notify_position, release, and stats.

Error codes

pub enum VozduxanError {
    Ok = 0,
    MetadataTimeout = 1,  // DHT resolution exceeded 90 s
    InvalidFile = 2,      // file_idx out of range or not an audio file
    BadInput = 3,         // null magnet, malformed torrent data
    Internal = 99,        // unspecified C++ exception
}

Piece priority scheduling

Inside C++, vozduxan implements a three-tier piece priority scheme adapted from Tribler research on libtorrent:

Tier Piece range (from current position) Mechanism Purpose
TIER1 next 20 pieces set_piece_deadline(8000 ms) Time-critical; stall prevention
TIER2 pieces 20–60 ahead high priority Look-ahead buffer
TIER3 remaining pieces default priority Background download

The window slides forward on every vozduxan_notify_position call. The frontend calls this on every timeupdate event from the <audio> element:

// Player.vue
invoke("vozduxan_notify_position", { token, byteOffset: audioEl.currentTime * bytesPerSecond })

C++ recalculates the piece index from byte_offset and re-sets the priority window. This keeps the "deadline" pieces aligned with where playback actually is, not where it started.


Local HTTP server

vozduxan runs an internal HTTP/1.1 server bound to 127.0.0.1 on a randomly chosen port. The port is fixed for the lifetime of the session and can be queried with vozduxan_session_http_port. Every VozduxanStreamInfo.url is pre-formatted to include this port:

http://127.0.0.1:PORT/stream/TOKEN

The server supports HTTP Range requests, which is what browsers use for audio seeking. When <audio> seeks to a new position, it sends Range: bytes=X-. C++ checks a per-stream seek_generation counter between chunk writes. If a new seek arrives (counter incremented) while an old Range response is still in flight, the old transfer aborts immediately without waiting to finish.


Fast-start prebuffer

vozduxan_stream_prepare does not return until a minimum amount of data is buffered. The prebuffer size adapts to connection conditions:

  • Fast connection (many peers, high download rate): ~32 KB
  • Slow connection: up to 512 KB

This is why the call blocks — returning early would cause immediate stalling in the <audio> element. During prebuffering the VozduxanProgressFn callback fires periodically, which the Rust wrapper forwards as a torrent-prepare-progress Tauri event so the frontend can show a spinner with percentage.

unsafe extern "C" fn on_progress(progress: f32, status: *const c_char, userdata: *mut c_void) {
    let ctx = unsafe { &*(userdata as *const ProgressCtx) };
    let _ = ctx.app.emit(
        "torrent-prepare-progress",
        PrepareProgressPayload::buffering((progress as f64) * 100.0, msg),
    );
}

Release and eviction

vozduxan_stream_release releases a stream and joins the priority thread inside C++. This join takes approximately 100 ms. This is why release must never happen synchronously on the tokio async executor — it would block the thread for that duration. The Rust wrapper wraps every release in spawn_blocking:

tokio::task::spawn_blocking(move || {
    let token_c = CString::new(token.as_str()).unwrap();
    unsafe { ffi::vozduxan_stream_release(inner.ptr, token_c.as_ptr()) };
    // ... clear token bucket, maybe call evict
})
.await

Idle torrents accumulate in the libtorrent session until vozduxan_session_evict is called. neegde calls it:

  • Every 10 stream releases (tracked by evict_counter in VozduxanSessionInner)
  • On every explicit torrent_dispose_preview (cache purge)

File listing

vozduxan_list_files resolves a magnet or torrent and returns the file tree without starting playback. Used by the torrent panel to show tracks before the user picks one.

pub struct VozduxanFileEntry {
    pub name:  [u8; 512],
    pub mime:  [u8; 64],
    pub size:  i64,
    pub index: c_int,  // the file_idx to pass to vozduxan_stream_prepare later
}

pub struct VozduxanFileList {
    pub files:     *mut VozduxanFileEntry,
    pub count:     c_int,
    pub error:     VozduxanError,
    pub error_msg: [u8; 256],
}

VozduxanFileList is heap-allocated inside C++. The Rust wrapper frees it with vozduxan_file_list_free immediately after converting entries to Vec<TorrentFile>.


Download stats

vozduxan_stream_stats returns live telemetry for a stream token. It is a fast, non-blocking call (no I/O):

pub struct VozduxanStreamStats {
    pub download_rate_bytes: i32,
    pub num_peers: i32,
}

The frontend calls vozduxan_stream_stats periodically to show peer count and download speed in the stream status popup.


Build integration

vozduxan is compiled as a static library by build.rs via CMake. cargo:rerun-if-changed must enumerate individual C++ source files — macOS does not update directory mtime when a file inside changes, so without per-file tracking Cargo would not detect edits. See Vozduxan CMake Build.