openipc-core 0.1.21

Shared OpenIPC FPV packet, RTP, and Realtek RX parsing logic
Documentation
# openipc-core

Shared protocol code for `openipc-rs`.

This crate contains the parts of the OpenIPC receive path that do not need to
know whether bytes came from native USB, WebUSB, a capture file, or a test
fixture. It is the right dependency when you want to parse or reconstruct
OpenIPC video without taking a dependency on a specific USB frontend.

## What It Does

- Parse Realtek rtl88xx USB RX aggregates and 24-byte RX descriptors.
- Filter OpenIPC/WFB 802.11 frames by channel id and radio port.
- Handle WFB session packets, optional session TLVs, data decryption, and FEC
  recovery.
- Expose recovered non-video WFB payload bytes from telemetry, tunnel/data,
  audio, or custom radio ports without parsing those application protocols.
- Parse RTP and depacketize H.264/H.265 into Annex-B access units.
- Expose RTP payload bytes for app-owned sinks such as UDP forwarding or Opus
  audio decoding.
- Build adaptive-link feedback payloads and WFB uplink packets.
- Build radiotap + 802.11 WFB transmit frames. Hardware crates add their own
  USB/driver descriptors before transmission.

## Basic Receive Shape

```rust
use openipc_core::{
    ChannelId, FrameLayout, PayloadRouteId, ReceiverBatchOptions, ReceiverRuntime,
    WfbKeypair,
};

const VIDEO_ROUTE: PayloadRouteId = PayloadRouteId::new(1);

fn push_transfer(
    receiver: &mut ReceiverRuntime,
    transfer: &[u8],
) -> Result<Vec<Vec<u8>>, Box<dyn std::error::Error>> {
    let batch = receiver.push_rx_transfer(transfer, &ReceiverBatchOptions::default())?;
    Ok(batch.frames.into_iter().map(|frame| frame.data).collect())
}

fn receiver_from_keypair(keypair_bytes: &[u8]) -> Result<ReceiverRuntime, Box<dyn std::error::Error>> {
    let keypair = WfbKeypair::from_bytes(keypair_bytes)?;
    Ok(ReceiverRuntime::with_keyed_video_route(
        FrameLayout::WithFcs,
        VIDEO_ROUTE,
        ChannelId::default_video(),
        0,
        keypair,
        0,
    )?)
}
```

The returned frame data is still encoded video. Feed it to WebCodecs, a native
decoder, a file writer, or an RTP/Annex-B bridge depending on your application.
Long-running receivers should treat per-frame WFB errors as dropped packets and
continue scanning the current USB aggregate.

## Payload Routes And RTP

`PayloadPipeline` is the lower-level channel-recovery state machine in
`openipc-core`. It emits recovered bytes after channel filtering, WFB session
handling, decryption, and FEC. It does not parse RTP, MAVLink, MSP, CRSF, IP,
vendor data, or any other application protocol.

Most apps should start with `ReceiverRuntime`. It wraps `PayloadRouteManager`
and the RTP depacketizer used for the configured video route. The route manager
keeps one runtime per `(channel_id, key_slot)` and attaches one or more route
IDs to that runtime. This avoids decrypting and FEC-recovering the same channel
twice when you want multiple outputs, such as local video display plus RTP
forwarding.

For OpenIPC video, configure one video route. Recovered payloads on that route
are treated as RTP and are turned into Annex-B H.264/H.265 frames. If you also
want RTP forwarding, ask `ReceiverRuntime` to tap the same route as raw payload
bytes:

```rust
use openipc_core::{PayloadRouteId, ReceiverBatchOptions, ReceiverRuntime};

const VIDEO_ROUTE: PayloadRouteId = PayloadRouteId::new(1);

fn receive_with_rtp_tap(
    receiver: &mut ReceiverRuntime,
    transfer: &[u8],
) -> Result<(), Box<dyn std::error::Error>> {
    let batch = receiver.push_rx_transfer(
        transfer,
        &ReceiverBatchOptions {
            raw_payload_routes: vec![VIDEO_ROUTE],
            ..ReceiverBatchOptions::default()
        },
    )?;

    for rtp in batch.raw_payloads {
        println!("RTP bytes: {}", rtp.data.len());
    }
    for frame in batch.frames {
        println!("Annex-B frame bytes: {}", frame.data.len());
    }
    Ok(())
}
```

Add another route for a non-video WFB channel when you want recovered telemetry,
IP/VPN, or custom bytes:

```rust
use openipc_core::{
    ChannelId, PayloadRouteId, RadioPort, ReceiverBatchOptions, ReceiverRuntime,
    WfbKeypair,
};

const TELEMETRY_ROUTE: PayloadRouteId = PayloadRouteId::new(2);

fn add_telemetry_route(
    receiver: &mut ReceiverRuntime,
    keypair_bytes: &[u8],
    port: RadioPort,
) -> Result<(), Box<dyn std::error::Error>> {
    let keypair = WfbKeypair::from_bytes(keypair_bytes)?;
    receiver.add_keyed_route(
        TELEMETRY_ROUTE,
        ChannelId::from_link_port(7669206, port),
        0,
        keypair,
        0,
    )?;
    Ok(())
}

fn handle_transfer(
    receiver: &mut ReceiverRuntime,
    transfer: &[u8],
) -> Result<(), Box<dyn std::error::Error>> {
    let batch = receiver.push_rx_transfer(
        transfer,
        &ReceiverBatchOptions {
            raw_payload_routes: vec![TELEMETRY_ROUTE],
            ..ReceiverBatchOptions::default()
        },
    )?;
    for payload in batch.raw_payloads {
        println!(
            "route={} channel=0x{:08x} bytes={}",
            payload.route_id.raw(),
            payload.channel_id.raw(),
            payload.data.len()
        );
    }
    Ok(())
}
```

`RadioPort::TelemetryRx` is OpenIPC's observed telemetry downlink port. That
stream may contain MAVLink, MSP/OSD, or another router-specific payload. Use
`RadioPort::TunnelRx`, `RadioPort::AudioRx`, or `RadioPort::Custom(n)` for other
payload channels. `openipc-core` does not parse MAVLink or any other telemetry
protocol. Applications can parse, display, record, or inspect those bytes later.

Audio follows the same rule, with one extra helper. OpenIPC-documented audio is
usually Opus RTP payload type 98 mixed into the main video RTP route. Use
`ReceiverBatchOptions::rtp_payload_taps` to copy only that payload type while
the video depacketizer keeps consuming the same route. Custom wfb-ng profiles
can also carry audio on a separate route such as `RadioPort::AudioRx`.
`openipc-core` still does not own audio playback; it only provides recovered RTP
packet bytes and metadata.

`RtpDepacketizer` is also conservative about fragmented video. If a fragmented
H.264/H.265 RTP access unit has a sequence gap, the partial frame is dropped and
the depacketizer waits for the next clean fragment start.

## Crate Boundaries

`openipc-core` intentionally has no USB device ownership. Pair it with:

- `openipc-rtl88xx` for native or WebUSB Realtek adapter IO.
- `openipc-web` or `@openipc-rs/web` for browser/WASM applications.
- `apps/openipc-cli` for CLI-style native receive-loop examples.

## Status

The protocol pipeline has unit tests for parser, crypto, FEC, RTP, and uplink
helpers. Live radio behavior still depends on the USB driver and adapter
validation in higher-level crates.

An optional PixelPilot/wfb-ng reference test can compare Rust FEC output against
PixelPilot's vendored `zfex.c` implementation:

```sh
OPENIPC_PIXELPILOT_REF=/path/to/PixelPilot \
  cargo test -p openipc-core --test pixelpilot_reference -- --ignored --nocapture
```

The test builds a temporary C harness, generates PixelPilot parity/recovery
vectors, and compares them with `FecCode` plus a WFB-shaped
`PlainAssembler` recovery case. It is ignored by default so normal CI does not
depend on a PixelPilot checkout or a C compiler.