75627c8afe
apple / swift (push) Failing after 10s
release / apple (push) Failing after 7s
apple / screenshots (push) Has been skipped
audit / cargo-audit (push) Failing after 1m19s
windows-host / package (push) Failing after 2m44s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Failing after 39s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Failing after 39s
windows / build (aarch64-pc-windows-msvc) (push) Failing after 45s
android / android (push) Successful in 5m17s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 45s
ci / web (push) Successful in 57s
ci / docs-site (push) Successful in 56s
ci / rust (push) Successful in 9m19s
ci / bench (push) Successful in 4m40s
decky / build-publish (push) Successful in 26s
deb / build-publish (push) Successful in 2m57s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 33s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 2m56s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 2m35s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m20s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 53s
flatpak / build-publish (push) Successful in 4m22s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m51s
docker / deploy-docs (push) Successful in 21s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m50s
Adds negotiated 5.1/7.1 surround to the punktfunk/1 protocol and every client (previously stereo-only): - core: new shared `audio` layout table (LAYOUT_51/71 + identity multistream mapping, canonical wire order FL FR FC LFE RL RR SL SR); Hello/Welcome `audio_channels` negotiation via the trailing-byte back-compat pattern (old peers fall back to stereo); C-ABI `punktfunk_connect_ex6`, `punktfunk_connection_audio_channels`, and in-core multistream decode `punktfunk_connection_next_audio_pcm` for embedders without a multistream Opus decoder. Real-libopus channel-identity round-trip test. - host: native audio thread captures + Opus-(multi)stream-encodes at the negotiated count (with a cross-session cached-capturer channel-mismatch fix); GameStream surround unified onto the safe `opus::MSEncoder`, dropping `audiopus_sys` (~4 unsafe blocks) and un-gating Windows GameStream surround; WASAPI loopback capture relaxed to 2/6/8 with the correct dwChannelMask. - clients: Linux (PipeWire), Windows (WASAPI), Android (AAudio) decode via `opus::MSDecoder` + render multichannel; Apple decodes in-core to PCM → AVAudioEngine with an explicit wire-order channel layout; each gains a Stereo/5.1/7.1 setting. `punktfunk-probe --audio-channels N` is the headless validator. Verified on Linux: core/host/linux/probe test suites + the Android Rust (cargo-ndk) build, clippy -D warnings, and rustfmt all green. Windows/Apple builds, all on-glass checks, and the live native loopback are pending (CI / a free box). Also lands the concurrent in-tree HEVC 4:4:4 host work (PUNKTFUNK_444): it shares the same touched files (quic.rs, punktfunk1.rs, encode/*, ...) and so cannot be committed separately from the surround changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2070 lines
76 KiB
Rust
2070 lines
76 KiB
Rust
//! The stable `extern "C"` surface. `cbindgen` turns this module into
|
||
//! `include/punktfunk_core.h` (see `build.rs`).
|
||
//!
|
||
//! ## Principles (plan §5)
|
||
//! - Opaque handles only: C sees `PunktfunkSession*`, never a Rust type's fields.
|
||
//! - All cross-boundary structs are `#[repr(C)]`; buffers are pointer + length.
|
||
//! - Explicit ownership: every handle from `*_new` / `*_pair` must be passed to
|
||
//! [`punktfunk_session_free`]. A [`PunktfunkFrame`]'s `data` is borrowed until the next
|
||
//! `poll`/`free` on that session — copy it out before then.
|
||
//! - Versioned: [`punktfunk_abi_version`] + `PunktfunkConfig::struct_size` for forward-compat.
|
||
//! - Panics never cross the boundary: every entry point is wrapped in `catch_unwind`.
|
||
|
||
use crate::config::{Config, FecConfig, FecScheme, ProtocolPhase, Role};
|
||
use crate::error::PunktfunkStatus;
|
||
use crate::input::InputEvent;
|
||
use crate::session::Session;
|
||
use crate::stats::Stats;
|
||
use crate::transport::{loopback_pair, Transport, UdpTransport};
|
||
use std::ffi::{c_void, CStr};
|
||
use std::os::raw::c_char;
|
||
use std::panic::AssertUnwindSafe;
|
||
use std::ptr;
|
||
|
||
/// Opaque session handle. Pointer-only from C.
|
||
pub struct PunktfunkSession {
|
||
inner: Session,
|
||
/// Keeps the most recently polled frame alive so [`PunktfunkFrame::data`] stays valid
|
||
/// until the next poll or free.
|
||
last_frame: Option<crate::session::Frame>,
|
||
input_cb: Option<(PunktfunkInputCb, *mut c_void)>,
|
||
}
|
||
|
||
/// Forward-compatible session configuration. The caller MUST set `struct_size` to
|
||
/// `sizeof(PunktfunkConfig)`; the core uses it to detect ABI skew.
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy)]
|
||
pub struct PunktfunkConfig {
|
||
pub struct_size: u32,
|
||
/// 0 = host, 1 = client.
|
||
pub role: u32,
|
||
/// 1 = P1 (GameStream-compatible), 2 = P2 (`punktfunk/1`).
|
||
pub phase: u32,
|
||
/// 0 = GF(2⁸), 1 = GF(2¹⁶).
|
||
pub fec_scheme: u32,
|
||
pub fec_percent: u32,
|
||
pub max_data_per_block: u32,
|
||
pub shard_payload: u32,
|
||
/// Non-zero enables AES-128-GCM.
|
||
pub encrypt: u32,
|
||
pub key: [u8; 16],
|
||
pub salt: [u8; 4],
|
||
/// Test hook for the loopback transport; 0 in production.
|
||
pub loopback_drop_period: u32,
|
||
/// Largest encoded access unit the receiver will accept (bounds reassembler memory).
|
||
pub max_frame_bytes: u64,
|
||
}
|
||
|
||
impl PunktfunkConfig {
|
||
fn to_config(self) -> Result<Config, PunktfunkStatus> {
|
||
let role = match self.role {
|
||
0 => Role::Host,
|
||
1 => Role::Client,
|
||
_ => return Err(PunktfunkStatus::InvalidArg),
|
||
};
|
||
let phase = match self.phase {
|
||
1 => ProtocolPhase::P1GameStream,
|
||
2 => ProtocolPhase::P2Punktfunk,
|
||
_ => return Err(PunktfunkStatus::InvalidArg),
|
||
};
|
||
// Range-check before narrowing: a `300` fec_percent or `65600` block size must be
|
||
// rejected, not silently truncated to a valid-looking value.
|
||
let scheme = u8::try_from(self.fec_scheme)
|
||
.ok()
|
||
.and_then(FecScheme::from_u8)
|
||
.ok_or(PunktfunkStatus::InvalidArg)?;
|
||
let fec_percent =
|
||
u8::try_from(self.fec_percent).map_err(|_| PunktfunkStatus::InvalidArg)?;
|
||
let max_data_per_block =
|
||
u16::try_from(self.max_data_per_block).map_err(|_| PunktfunkStatus::InvalidArg)?;
|
||
let cfg = Config {
|
||
role,
|
||
phase,
|
||
fec: FecConfig {
|
||
scheme,
|
||
fec_percent,
|
||
max_data_per_block,
|
||
},
|
||
shard_payload: self.shard_payload as usize,
|
||
max_frame_bytes: self.max_frame_bytes as usize,
|
||
encrypt: self.encrypt != 0,
|
||
key: self.key,
|
||
salt: self.salt,
|
||
loopback_drop_period: self.loopback_drop_period,
|
||
};
|
||
cfg.validate().map_err(|e| e.status())?;
|
||
Ok(cfg)
|
||
}
|
||
}
|
||
|
||
/// Read a `PunktfunkConfig` from a caller pointer, enforcing the `struct_size` ABI-skew
|
||
/// guard *before* reading the whole struct: a caller compiled against a smaller (older)
|
||
/// layout is rejected rather than causing an out-of-bounds read.
|
||
///
|
||
/// # Safety
|
||
/// `cfg` must either be null or point to at least its own declared `struct_size` bytes.
|
||
unsafe fn config_from_ptr(cfg: *const PunktfunkConfig) -> Result<Config, PunktfunkStatus> {
|
||
if cfg.is_null() {
|
||
return Err(PunktfunkStatus::NullPointer);
|
||
}
|
||
// Read only the 4-byte size prefix first to bound the subsequent full read.
|
||
let declared = unsafe { std::ptr::addr_of!((*cfg).struct_size).read_unaligned() } as usize;
|
||
if declared < std::mem::size_of::<PunktfunkConfig>() {
|
||
return Err(PunktfunkStatus::InvalidArg);
|
||
}
|
||
unsafe { *cfg }.to_config()
|
||
}
|
||
|
||
/// A reassembled access unit. `data`/`len` borrow session-owned memory valid until the
|
||
/// next `punktfunk_client_poll_frame`/`punktfunk_session_free` on the same session.
|
||
#[repr(C)]
|
||
pub struct PunktfunkFrame {
|
||
pub data: *const u8,
|
||
pub len: usize,
|
||
pub frame_index: u32,
|
||
pub pts_ns: u64,
|
||
pub flags: u32,
|
||
}
|
||
|
||
/// Snapshot of session counters.
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy, Default)]
|
||
pub struct PunktfunkStats {
|
||
pub frames_submitted: u64,
|
||
pub frames_completed: u64,
|
||
pub frames_dropped: u64,
|
||
pub packets_sent: u64,
|
||
pub packets_received: u64,
|
||
pub packets_dropped: u64,
|
||
/// Packets dropped on the host send path because the kernel buffer was full (WouldBlock) — the
|
||
/// dominant loss mode at very high bitrate; distinct from `packets_dropped` (recv-side).
|
||
pub packets_send_dropped: u64,
|
||
pub fec_recovered_shards: u64,
|
||
pub bytes_sent: u64,
|
||
pub bytes_received: u64,
|
||
}
|
||
|
||
impl From<Stats> for PunktfunkStats {
|
||
fn from(s: Stats) -> Self {
|
||
PunktfunkStats {
|
||
frames_submitted: s.frames_submitted,
|
||
frames_completed: s.frames_completed,
|
||
frames_dropped: s.frames_dropped,
|
||
packets_sent: s.packets_sent,
|
||
packets_received: s.packets_received,
|
||
packets_dropped: s.packets_dropped,
|
||
packets_send_dropped: s.packets_send_dropped,
|
||
fec_recovered_shards: s.fec_recovered_shards,
|
||
bytes_sent: s.bytes_sent,
|
||
bytes_received: s.bytes_received,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Host-side callback invoked for each input event drained by `punktfunk_host_poll_input`.
|
||
pub type PunktfunkInputCb = extern "C" fn(event: *const InputEvent, user: *mut c_void);
|
||
|
||
#[inline]
|
||
fn guard<F: FnOnce() -> PunktfunkStatus>(f: F) -> PunktfunkStatus {
|
||
std::panic::catch_unwind(AssertUnwindSafe(f)).unwrap_or(PunktfunkStatus::Panic)
|
||
}
|
||
|
||
fn new_handle(session: Session) -> *mut PunktfunkSession {
|
||
Box::into_raw(Box::new(PunktfunkSession {
|
||
inner: session,
|
||
last_frame: None,
|
||
input_cb: None,
|
||
}))
|
||
}
|
||
|
||
/// Current ABI version. Mismatch with [`crate::ABI_VERSION`] means incompatible core.
|
||
#[no_mangle]
|
||
pub extern "C" fn punktfunk_abi_version() -> u32 {
|
||
crate::ABI_VERSION
|
||
}
|
||
|
||
/// Create a session over a real UDP transport (`local`/`peer` are `host:port` strings).
|
||
/// Returns NULL on error.
|
||
///
|
||
/// # Safety
|
||
/// `cfg`, `local`, `peer` must be valid pointers; the strings must be NUL-terminated.
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_session_new(
|
||
cfg: *const PunktfunkConfig,
|
||
local: *const c_char,
|
||
peer: *const c_char,
|
||
) -> *mut PunktfunkSession {
|
||
let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
||
if cfg.is_null() || local.is_null() || peer.is_null() {
|
||
return ptr::null_mut();
|
||
}
|
||
let config = match unsafe { config_from_ptr(cfg) } {
|
||
Ok(c) => c,
|
||
Err(_) => return ptr::null_mut(),
|
||
};
|
||
let local = match unsafe { CStr::from_ptr(local) }.to_str() {
|
||
Ok(s) => s,
|
||
Err(_) => return ptr::null_mut(),
|
||
};
|
||
let peer = match unsafe { CStr::from_ptr(peer) }.to_str() {
|
||
Ok(s) => s,
|
||
Err(_) => return ptr::null_mut(),
|
||
};
|
||
let transport: Box<dyn Transport> = match UdpTransport::connect(local, peer) {
|
||
Ok(t) => Box::new(t),
|
||
Err(_) => return ptr::null_mut(),
|
||
};
|
||
match Session::new(config, transport) {
|
||
Ok(s) => new_handle(s),
|
||
Err(_) => ptr::null_mut(),
|
||
}
|
||
}));
|
||
result.unwrap_or(ptr::null_mut())
|
||
}
|
||
|
||
/// Create a connected host+client session pair sharing an in-process loopback
|
||
/// transport. Test/dev only — exercises the full FEC + framing path without a network.
|
||
///
|
||
/// # Safety
|
||
/// All four pointers must be valid; the two out-params receive owned handles.
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_test_loopback_pair(
|
||
host_cfg: *const PunktfunkConfig,
|
||
client_cfg: *const PunktfunkConfig,
|
||
out_host: *mut *mut PunktfunkSession,
|
||
out_client: *mut *mut PunktfunkSession,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
if host_cfg.is_null() || client_cfg.is_null() || out_host.is_null() || out_client.is_null()
|
||
{
|
||
return PunktfunkStatus::NullPointer;
|
||
}
|
||
let hconf = match unsafe { config_from_ptr(host_cfg) } {
|
||
Ok(c) => c,
|
||
Err(s) => return s,
|
||
};
|
||
let cconf = match unsafe { config_from_ptr(client_cfg) } {
|
||
Ok(c) => c,
|
||
Err(s) => return s,
|
||
};
|
||
let (ht, ct) = loopback_pair(hconf.loopback_drop_period, cconf.loopback_drop_period);
|
||
let hs = match Session::new(hconf, Box::new(ht)) {
|
||
Ok(s) => s,
|
||
Err(e) => return e.status(),
|
||
};
|
||
let cs = match Session::new(cconf, Box::new(ct)) {
|
||
Ok(s) => s,
|
||
Err(e) => return e.status(),
|
||
};
|
||
unsafe {
|
||
*out_host = new_handle(hs);
|
||
*out_client = new_handle(cs);
|
||
}
|
||
PunktfunkStatus::Ok
|
||
})
|
||
}
|
||
|
||
/// Free a session handle. Safe to call with NULL.
|
||
///
|
||
/// # Safety
|
||
/// `s` must be a handle from `punktfunk_session_new`/`punktfunk_test_loopback_pair`, freed once.
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_session_free(s: *mut PunktfunkSession) {
|
||
if !s.is_null() {
|
||
drop(unsafe { Box::from_raw(s) });
|
||
}
|
||
}
|
||
|
||
/// Host: FEC-protect, packetize, seal and send one encoded access unit.
|
||
///
|
||
/// # Safety
|
||
/// `s` is a valid host handle; `data` points to `len` readable bytes (or `len == 0`).
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_host_submit_frame(
|
||
s: *mut PunktfunkSession,
|
||
data: *const u8,
|
||
len: usize,
|
||
pts_ns: u64,
|
||
flags: u32,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
let s = match unsafe { s.as_mut() } {
|
||
Some(s) => s,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
if data.is_null() && len != 0 {
|
||
return PunktfunkStatus::NullPointer;
|
||
}
|
||
let slice = if len == 0 {
|
||
&[][..]
|
||
} else {
|
||
unsafe { std::slice::from_raw_parts(data, len) }
|
||
};
|
||
match s.inner.submit_frame(slice, pts_ns, flags) {
|
||
Ok(()) => PunktfunkStatus::Ok,
|
||
Err(e) => e.status(),
|
||
}
|
||
})
|
||
}
|
||
|
||
/// Client: poll for the next reassembled access unit. Returns [`PunktfunkStatus::NoFrame`]
|
||
/// when nothing is ready yet. On `Ok`, `*out` borrows session memory until the next poll.
|
||
///
|
||
/// # Safety
|
||
/// `s` is a valid client handle; `out` points to a writable `PunktfunkFrame`.
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_client_poll_frame(
|
||
s: *mut PunktfunkSession,
|
||
out: *mut PunktfunkFrame,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
let s = match unsafe { s.as_mut() } {
|
||
Some(s) => s,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
if out.is_null() {
|
||
return PunktfunkStatus::NullPointer;
|
||
}
|
||
match s.inner.poll_frame() {
|
||
Ok(frame) => {
|
||
s.last_frame = Some(frame);
|
||
let f = s.last_frame.as_ref().unwrap();
|
||
unsafe {
|
||
*out = PunktfunkFrame {
|
||
data: f.data.as_ptr(),
|
||
len: f.data.len(),
|
||
frame_index: f.frame_index,
|
||
pts_ns: f.pts_ns,
|
||
flags: f.flags,
|
||
};
|
||
}
|
||
PunktfunkStatus::Ok
|
||
}
|
||
Err(e) => e.status(),
|
||
}
|
||
})
|
||
}
|
||
|
||
/// Client: serialize and send one input event to the host.
|
||
///
|
||
/// # Safety
|
||
/// `s` is a valid client handle; `ev` points to a valid [`InputEvent`].
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_send_input(
|
||
s: *mut PunktfunkSession,
|
||
ev: *const InputEvent,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
let s = match unsafe { s.as_mut() } {
|
||
Some(s) => s,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
let ev = match unsafe { ev.as_ref() } {
|
||
Some(e) => e,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
match s.inner.send_input(ev) {
|
||
Ok(()) => PunktfunkStatus::Ok,
|
||
Err(e) => e.status(),
|
||
}
|
||
})
|
||
}
|
||
|
||
/// Register the host-side input callback (pass a NULL fn pointer to clear). The callback
|
||
/// fires from within [`punktfunk_host_poll_input`], on the calling thread.
|
||
///
|
||
/// # Safety
|
||
/// `s` is a valid host handle; `user` is passed back verbatim to `cb`.
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_set_input_callback(
|
||
s: *mut PunktfunkSession,
|
||
// Written as an explicit `Option<fn>` (not the `PunktfunkInputCb` alias) so cbindgen
|
||
// emits a nullable C function pointer rather than an opaque wrapper struct.
|
||
cb: Option<extern "C" fn(event: *const InputEvent, user: *mut c_void)>,
|
||
user: *mut c_void,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
let s = match unsafe { s.as_mut() } {
|
||
Some(s) => s,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
s.input_cb = cb.map(|c| (c, user));
|
||
PunktfunkStatus::Ok
|
||
})
|
||
}
|
||
|
||
/// Host: drain all pending input events, invoking the registered callback for each.
|
||
/// Returns the count dispatched (≥ 0), or a negative [`PunktfunkStatus`] on error.
|
||
///
|
||
/// # Safety
|
||
/// `s` is a valid host handle.
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_host_poll_input(s: *mut PunktfunkSession) -> i32 {
|
||
let r = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
||
let s = match unsafe { s.as_mut() } {
|
||
Some(s) => s,
|
||
None => return PunktfunkStatus::NullPointer as i32,
|
||
};
|
||
let cb = s.input_cb;
|
||
let mut count = 0i32;
|
||
loop {
|
||
match s.inner.poll_input() {
|
||
Ok(Some(ev)) => {
|
||
if let Some((cb, user)) = cb {
|
||
cb(&ev as *const InputEvent, user);
|
||
}
|
||
count += 1;
|
||
}
|
||
Ok(None) => break,
|
||
Err(e) => return e.status() as i32,
|
||
}
|
||
}
|
||
count
|
||
}));
|
||
r.unwrap_or(PunktfunkStatus::Panic as i32)
|
||
}
|
||
|
||
/// Copy session counters into `*out`.
|
||
///
|
||
/// # Safety
|
||
/// `s` is a valid handle; `out` points to a writable `PunktfunkStats`.
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_get_stats(
|
||
s: *mut PunktfunkSession,
|
||
out: *mut PunktfunkStats,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
let s = match unsafe { s.as_ref() } {
|
||
Some(s) => s,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
if out.is_null() {
|
||
return PunktfunkStatus::NullPointer;
|
||
}
|
||
let stats = s.inner.stats();
|
||
unsafe { *out = PunktfunkStats::from(stats) };
|
||
PunktfunkStatus::Ok
|
||
})
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------
|
||
// punktfunk/1 connection API (`quic` feature) — the embeddable client connector platform clients
|
||
// link (SwiftUI/VideoToolbox, Android, …). In the generated header these are guarded by
|
||
// `PUNKTFUNK_FEATURE_QUIC`; define it when linking a punktfunk-core built with `--features quic`.
|
||
// ---------------------------------------------------------------------------------------------
|
||
|
||
/// Opaque handle to a live `punktfunk/1` connection (QUIC control plane + UDP data plane, all
|
||
/// pumped on internal threads).
|
||
///
|
||
/// Thread contract: each plane (video `next_au`, audio `next_audio`, rumble `next_rumble`)
|
||
/// may be pulled from its own thread, at most one thread per plane. The accessors only
|
||
/// take shared references internally (per-plane mutexed borrow slots), so cross-plane
|
||
/// concurrency is sound — never two threads on the *same* plane.
|
||
#[cfg(feature = "quic")]
|
||
pub struct PunktfunkConnection {
|
||
inner: crate::client::NativeClient,
|
||
/// Backs the pointer returned by the last `punktfunk_connection_next_au` (borrow-until-next-call).
|
||
last: std::sync::Mutex<Option<crate::session::Frame>>,
|
||
/// Same, for `punktfunk_connection_next_audio` (independent of the video slot).
|
||
last_audio: std::sync::Mutex<Option<crate::client::AudioPacket>>,
|
||
/// Decode-in-core state for `punktfunk_connection_next_audio_pcm` (Apple / any embedder
|
||
/// without a multistream Opus decoder). The decoder is built lazily from the negotiated
|
||
/// `inner.audio_channels`; `pcm` is a fixed-capacity reusable buffer the returned pointer
|
||
/// borrows until the next PCM call (same contract as `last_audio`).
|
||
audio_pcm: std::sync::Mutex<AudioPcmState>,
|
||
}
|
||
|
||
/// Lazily-initialized in-core Opus decode state. A coupled-1-stream multistream decoder is
|
||
/// equivalent to a plain stereo decoder, so one [`opus::MSDecoder`] handles 2/6/8 channels.
|
||
#[cfg(feature = "quic")]
|
||
#[derive(Default)]
|
||
struct AudioPcmState {
|
||
decoder: Option<opus::MSDecoder>,
|
||
/// Interleaved f32 PCM, wire channel order. Pre-sized to the largest legal Opus frame
|
||
/// (120 ms @ 48 kHz = 5760 samples/ch) × 8 channels so decode never reallocates (which would
|
||
/// dangle the pointer handed to the embedder).
|
||
pcm: Vec<f32>,
|
||
}
|
||
|
||
/// `PunktfunkHidOutput::kind` — lightbar RGB (`r`/`g`/`b` valid).
|
||
pub const PUNKTFUNK_HIDOUT_LED: u8 = 1;
|
||
/// `PunktfunkHidOutput::kind` — player-indicator LEDs (`player_bits` valid, low 5 bits).
|
||
pub const PUNKTFUNK_HIDOUT_PLAYER_LEDS: u8 = 2;
|
||
/// `PunktfunkHidOutput::kind` — one adaptive-trigger effect (`which` + `effect`/`effect_len` valid).
|
||
pub const PUNKTFUNK_HIDOUT_TRIGGER: u8 = 3;
|
||
/// Capacity of `PunktfunkHidOutput::effect` (the DualSense trigger parameter block).
|
||
pub const PUNKTFUNK_HID_EFFECT_MAX: u8 = 11;
|
||
|
||
/// One DualSense HID-output feedback event a game wrote to the host's virtual pad
|
||
/// ([`punktfunk_connection_next_hidout`]). `kind` selects which fields are meaningful — replay it
|
||
/// on a real DualSense (lightbar color, player LEDs, or an adaptive-trigger effect via the
|
||
/// platform's `GCDualSenseAdaptiveTrigger`-style API).
|
||
#[cfg(feature = "quic")]
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy)]
|
||
pub struct PunktfunkHidOutput {
|
||
/// One of `PUNKTFUNK_HIDOUT_*`.
|
||
pub kind: u8,
|
||
/// Gamepad index.
|
||
pub pad: u8,
|
||
/// LED: lightbar red.
|
||
pub r: u8,
|
||
/// LED: lightbar green.
|
||
pub g: u8,
|
||
/// LED: lightbar blue.
|
||
pub b: u8,
|
||
/// PlayerLeds: lit player indicators (low 5 bits).
|
||
pub player_bits: u8,
|
||
/// Trigger: 0 = L2, 1 = R2.
|
||
pub which: u8,
|
||
/// Trigger: number of valid bytes in `effect` (≤ `PUNKTFUNK_HID_EFFECT_MAX`).
|
||
pub effect_len: u8,
|
||
/// Trigger: the raw DualSense trigger parameter block (mode + params).
|
||
pub effect: [u8; 11],
|
||
}
|
||
|
||
#[cfg(feature = "quic")]
|
||
impl PunktfunkHidOutput {
|
||
fn from_hid(h: &crate::quic::HidOutput) -> PunktfunkHidOutput {
|
||
use crate::quic::HidOutput;
|
||
let mut out = PunktfunkHidOutput {
|
||
kind: 0,
|
||
pad: 0,
|
||
r: 0,
|
||
g: 0,
|
||
b: 0,
|
||
player_bits: 0,
|
||
which: 0,
|
||
effect_len: 0,
|
||
effect: [0u8; 11],
|
||
};
|
||
match h {
|
||
HidOutput::Led { pad, r, g, b } => {
|
||
out.kind = PUNKTFUNK_HIDOUT_LED;
|
||
out.pad = *pad;
|
||
out.r = *r;
|
||
out.g = *g;
|
||
out.b = *b;
|
||
}
|
||
HidOutput::PlayerLeds { pad, bits } => {
|
||
out.kind = PUNKTFUNK_HIDOUT_PLAYER_LEDS;
|
||
out.pad = *pad;
|
||
out.player_bits = *bits;
|
||
}
|
||
HidOutput::Trigger { pad, which, effect } => {
|
||
out.kind = PUNKTFUNK_HIDOUT_TRIGGER;
|
||
out.pad = *pad;
|
||
out.which = *which;
|
||
let n = effect.len().min(out.effect.len());
|
||
out.effect[..n].copy_from_slice(&effect[..n]);
|
||
out.effect_len = n as u8;
|
||
}
|
||
}
|
||
out
|
||
}
|
||
}
|
||
|
||
/// Static HDR metadata for an HDR session ([`punktfunk_connection_next_hdr_meta`]): SMPTE ST.2086
|
||
/// mastering display colour volume + CEA-861.3 content light level. All fields are in the standard
|
||
/// HDR10 SEI fixed-point units (primaries/white in 1/50000, luminance in 0.0001 cd/m²), ready for
|
||
/// DXGI `DXGI_HDR_METADATA_HDR10` / Apple `CAEDRMetadata` / Android `KEY_HDR_STATIC_INFO`.
|
||
#[cfg(feature = "quic")]
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy)]
|
||
pub struct PunktfunkHdrMeta {
|
||
/// Display-primaries x-chromaticities in 1/50000 units, ST.2086 order [green, blue, red].
|
||
pub display_primaries_x: [u16; 3],
|
||
/// Display-primaries y-chromaticities in 1/50000 units, ST.2086 order [green, blue, red].
|
||
pub display_primaries_y: [u16; 3],
|
||
/// White-point x-chromaticity, 1/50000 units.
|
||
pub white_point_x: u16,
|
||
/// White-point y-chromaticity, 1/50000 units.
|
||
pub white_point_y: u16,
|
||
/// Max display mastering luminance, 0.0001 cd/m² units.
|
||
pub max_display_mastering_luminance: u32,
|
||
/// Min display mastering luminance, 0.0001 cd/m² units.
|
||
pub min_display_mastering_luminance: u32,
|
||
/// Maximum content light level (MaxCLL), nits. 0 = unknown.
|
||
pub max_cll: u16,
|
||
/// Maximum frame-average light level (MaxFALL), nits. 0 = unknown.
|
||
pub max_fall: u16,
|
||
}
|
||
|
||
#[cfg(feature = "quic")]
|
||
impl PunktfunkHdrMeta {
|
||
fn from_meta(m: &crate::quic::HdrMeta) -> PunktfunkHdrMeta {
|
||
PunktfunkHdrMeta {
|
||
display_primaries_x: [
|
||
m.display_primaries[0][0],
|
||
m.display_primaries[1][0],
|
||
m.display_primaries[2][0],
|
||
],
|
||
display_primaries_y: [
|
||
m.display_primaries[0][1],
|
||
m.display_primaries[1][1],
|
||
m.display_primaries[2][1],
|
||
],
|
||
white_point_x: m.white_point[0],
|
||
white_point_y: m.white_point[1],
|
||
max_display_mastering_luminance: m.max_display_mastering_luminance,
|
||
min_display_mastering_luminance: m.min_display_mastering_luminance,
|
||
max_cll: m.max_cll,
|
||
max_fall: m.max_fall,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// `PunktfunkRichInput::kind` — a touchpad contact (`finger`/`active`/`x`/`y` valid).
|
||
pub const PUNKTFUNK_RICH_TOUCHPAD: u8 = 1;
|
||
/// `PunktfunkRichInput::kind` — a motion sample (`gyro`/`accel` valid).
|
||
pub const PUNKTFUNK_RICH_MOTION: u8 = 2;
|
||
|
||
/// One rich client→host input for the host's virtual DualSense
|
||
/// ([`punktfunk_connection_send_rich_input`]): a touchpad contact or a motion sample. Set `kind`
|
||
/// and the matching fields; the others are ignored.
|
||
#[cfg(feature = "quic")]
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy)]
|
||
pub struct PunktfunkRichInput {
|
||
/// One of `PUNKTFUNK_RICH_*`.
|
||
pub kind: u8,
|
||
/// Gamepad index.
|
||
pub pad: u8,
|
||
/// Touchpad: contact id (0 or 1).
|
||
pub finger: u8,
|
||
/// Touchpad: 1 = finger down, 0 = lifted.
|
||
pub active: u8,
|
||
/// Touchpad: normalized x, 0..=65535 across the touchpad.
|
||
pub x: u16,
|
||
/// Touchpad: normalized y, 0..=65535 across the touchpad.
|
||
pub y: u16,
|
||
/// Motion: gyro (pitch, yaw, roll), raw signed-16.
|
||
pub gyro: [i16; 3],
|
||
/// Motion: accelerometer (x, y, z), raw signed-16.
|
||
pub accel: [i16; 3],
|
||
}
|
||
|
||
#[cfg(feature = "quic")]
|
||
impl PunktfunkRichInput {
|
||
fn to_rich(self) -> Option<crate::quic::RichInput> {
|
||
use crate::quic::RichInput;
|
||
match self.kind {
|
||
PUNKTFUNK_RICH_TOUCHPAD => Some(RichInput::Touchpad {
|
||
pad: self.pad,
|
||
finger: self.finger,
|
||
active: self.active != 0,
|
||
x: self.x,
|
||
y: self.y,
|
||
}),
|
||
PUNKTFUNK_RICH_MOTION => Some(RichInput::Motion {
|
||
pad: self.pad,
|
||
gyro: self.gyro,
|
||
accel: self.accel,
|
||
}),
|
||
_ => None,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Read an optional NUL-terminated UTF-8 string parameter; `Err` = invalid pointer/UTF-8.
|
||
#[cfg(feature = "quic")]
|
||
unsafe fn opt_cstr<'a>(p: *const std::os::raw::c_char) -> std::result::Result<Option<&'a str>, ()> {
|
||
if p.is_null() {
|
||
return Ok(None);
|
||
}
|
||
unsafe { std::ffi::CStr::from_ptr(p) }
|
||
.to_str()
|
||
.map(Some)
|
||
.map_err(|_| ())
|
||
}
|
||
|
||
/// Compositor preference for [`punktfunk_connect_ex`] (`compositor` arg). `AUTO` lets the host
|
||
/// pick (auto-detect from its running desktop); a concrete value is honored only if that backend
|
||
/// is available on the host right now, else the host falls back to auto-detect. The resolved
|
||
/// choice is reported back over the protocol (see `punktfunk/1` `Welcome`).
|
||
pub const PUNKTFUNK_COMPOSITOR_AUTO: u32 = 0;
|
||
/// KWin / KDE Plasma.
|
||
pub const PUNKTFUNK_COMPOSITOR_KWIN: u32 = 1;
|
||
/// wlroots (Sway / Hyprland).
|
||
pub const PUNKTFUNK_COMPOSITOR_WLROOTS: u32 = 2;
|
||
/// Mutter / GNOME.
|
||
pub const PUNKTFUNK_COMPOSITOR_MUTTER: u32 = 3;
|
||
/// gamescope (spawned nested).
|
||
pub const PUNKTFUNK_COMPOSITOR_GAMESCOPE: u32 = 4;
|
||
|
||
/// Gamepad-backend preference for [`punktfunk_connect_ex2`] (`gamepad` arg): which virtual pad
|
||
/// the host creates for this session's controllers. Precedence host-side: an explicit client
|
||
/// choice > the host's `PUNKTFUNK_GAMEPAD` env var > X-Box 360. `AUTO` (or any unrecognized
|
||
/// value) = host decides. The resolved choice is echoed over the protocol (`Welcome`) and
|
||
/// readable via [`punktfunk_connection_gamepad`].
|
||
pub const PUNKTFUNK_GAMEPAD_AUTO: u32 = 0;
|
||
/// uinput X-Box 360 pad (the universal default — every game speaks XInput).
|
||
pub const PUNKTFUNK_GAMEPAD_XBOX360: u32 = 1;
|
||
/// UHID DualSense (kernel `hid-playstation`): adaptive triggers, lightbar, touchpad, motion —
|
||
/// feedback arrives on the HID-output plane ([`punktfunk_connection_next_hidout`]). Honored
|
||
/// only where available (Linux hosts); otherwise the host falls back to X-Box 360.
|
||
pub const PUNKTFUNK_GAMEPAD_DUALSENSE: u32 = 2;
|
||
/// uinput X-Box One / Series pad — the X-Box 360 backend with the One/Series USB identity, so
|
||
/// games show One/Series glyphs. XInput-identical to `XBOX360` otherwise (no game-visible gain;
|
||
/// impulse-trigger rumble is unreachable through a virtual pad). Useful for glyph-matching a
|
||
/// physical X-Box One/Series controller on the client.
|
||
pub const PUNKTFUNK_GAMEPAD_XBOXONE: u32 = 3;
|
||
/// UHID DualShock 4 (kernel `hid-playstation` ≥ 6.2): lightbar, touchpad, motion, rumble — the
|
||
/// touchpad/motion arrive over the rich-input plane and lightbar over the HID-output plane, like
|
||
/// DualSense (minus adaptive triggers / player LEDs / mute). Honored only where available (Linux
|
||
/// hosts); otherwise the host falls back to X-Box 360.
|
||
pub const PUNKTFUNK_GAMEPAD_DUALSHOCK4: u32 = 4;
|
||
|
||
/// Connect to a `punktfunk/1` host and start a session at `width`x`height`@`refresh_hz`.
|
||
/// Blocks up to `timeout_ms` for the handshake. Returns NULL on failure. Equivalent to
|
||
/// [`punktfunk_connect_ex`] with `compositor = PUNKTFUNK_COMPOSITOR_AUTO`.
|
||
///
|
||
/// Video-capability bit for [`punktfunk_connect_ex5`] (`video_caps`): the client can decode a
|
||
/// 10-bit (Main10) HEVC stream. (Mirrors `quic::VIDEO_CAP_10BIT`.)
|
||
pub const PUNKTFUNK_VIDEO_CAP_10BIT: u8 = 0x01;
|
||
/// Video-capability bit for [`punktfunk_connect_ex5`] (`video_caps`): the client can present
|
||
/// BT.2020 PQ HDR10 (implies 10-bit). (Mirrors `quic::VIDEO_CAP_HDR`.)
|
||
pub const PUNKTFUNK_VIDEO_CAP_HDR: u8 = 0x02;
|
||
/// Video-capability bit for [`punktfunk_connect_ex5`] (`video_caps`): the client can decode a
|
||
/// full-chroma 4:4:4 HEVC stream (Range Extensions). The host emits 4:4:4 only when this is set,
|
||
/// the host opted in, the codec is HEVC, and the GPU supports it — else the stream stays 4:2:0 and
|
||
/// [`punktfunk_connection_chroma_format`] reports the real value. (Mirrors `quic::VIDEO_CAP_444`.)
|
||
pub const PUNKTFUNK_VIDEO_CAP_444: u8 = 0x04;
|
||
|
||
// Keep the ABI cap bits in lockstep with the wire constants (compile-time guard against drift).
|
||
#[cfg(feature = "quic")]
|
||
const _: () = {
|
||
assert!(PUNKTFUNK_VIDEO_CAP_10BIT == crate::quic::VIDEO_CAP_10BIT);
|
||
assert!(PUNKTFUNK_VIDEO_CAP_HDR == crate::quic::VIDEO_CAP_HDR);
|
||
assert!(PUNKTFUNK_VIDEO_CAP_444 == crate::quic::VIDEO_CAP_444);
|
||
};
|
||
|
||
// Keep the ABI gamepad constants in lockstep with the wire enum (compile-time guard against drift).
|
||
const _: () = {
|
||
use crate::config::GamepadPref;
|
||
assert!(PUNKTFUNK_GAMEPAD_AUTO == GamepadPref::Auto.to_u8() as u32);
|
||
assert!(PUNKTFUNK_GAMEPAD_XBOX360 == GamepadPref::Xbox360.to_u8() as u32);
|
||
assert!(PUNKTFUNK_GAMEPAD_DUALSENSE == GamepadPref::DualSense.to_u8() as u32);
|
||
assert!(PUNKTFUNK_GAMEPAD_XBOXONE == GamepadPref::XboxOne.to_u8() as u32);
|
||
assert!(PUNKTFUNK_GAMEPAD_DUALSHOCK4 == GamepadPref::DualShock4.to_u8() as u32);
|
||
};
|
||
|
||
/// Trust: `pin_sha256` (NULL or 32 bytes) is the expected SHA-256 fingerprint of the host's
|
||
/// certificate — a mismatching host is rejected. NULL = trust on first use; persist the
|
||
/// fingerprint written to `observed_sha256_out` (NULL or 32 bytes, filled on success) and
|
||
/// pass it as the pin on every later connect.
|
||
///
|
||
/// Identity: `client_cert_pem`/`client_key_pem` (both NULL, or both NUL-terminated PEM
|
||
/// strings — see [`punktfunk_generate_identity`]) are presented via TLS client auth so a
|
||
/// host can recognize this client once paired ([`punktfunk_pair`]). NULL = anonymous;
|
||
/// hosts running `--require-pairing` reject anonymous sessions.
|
||
///
|
||
/// # Safety
|
||
/// `host` is a NUL-terminated UTF-8 string (IP or hostname resolvable by the platform);
|
||
/// `pin_sha256`/`observed_sha256_out` are each NULL or valid for 32 bytes;
|
||
/// `client_cert_pem`/`client_key_pem` are each NULL or NUL-terminated UTF-8.
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_connect(
|
||
host: *const std::os::raw::c_char,
|
||
port: u16,
|
||
width: u32,
|
||
height: u32,
|
||
refresh_hz: u32,
|
||
pin_sha256: *const u8,
|
||
observed_sha256_out: *mut u8,
|
||
client_cert_pem: *const std::os::raw::c_char,
|
||
client_key_pem: *const std::os::raw::c_char,
|
||
timeout_ms: u32,
|
||
) -> *mut PunktfunkConnection {
|
||
unsafe {
|
||
punktfunk_connect_ex(
|
||
host,
|
||
port,
|
||
width,
|
||
height,
|
||
refresh_hz,
|
||
PUNKTFUNK_COMPOSITOR_AUTO,
|
||
pin_sha256,
|
||
observed_sha256_out,
|
||
client_cert_pem,
|
||
client_key_pem,
|
||
timeout_ms,
|
||
)
|
||
}
|
||
}
|
||
|
||
/// Like [`punktfunk_connect`], but requests a specific `compositor` backend on the host (one of
|
||
/// the `PUNKTFUNK_COMPOSITOR_*` values). `PUNKTFUNK_COMPOSITOR_AUTO` (or any unrecognized value)
|
||
/// lets the host decide; a concrete value is honored only if available, else the host falls back
|
||
/// to auto-detect. The resolved choice is logged host-side and returned over the protocol.
|
||
/// Equivalent to [`punktfunk_connect_ex2`] with `gamepad = PUNKTFUNK_GAMEPAD_AUTO`.
|
||
///
|
||
/// # Safety
|
||
/// Same as [`punktfunk_connect`].
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_connect_ex(
|
||
host: *const std::os::raw::c_char,
|
||
port: u16,
|
||
width: u32,
|
||
height: u32,
|
||
refresh_hz: u32,
|
||
compositor: u32,
|
||
pin_sha256: *const u8,
|
||
observed_sha256_out: *mut u8,
|
||
client_cert_pem: *const std::os::raw::c_char,
|
||
client_key_pem: *const std::os::raw::c_char,
|
||
timeout_ms: u32,
|
||
) -> *mut PunktfunkConnection {
|
||
unsafe {
|
||
punktfunk_connect_ex2(
|
||
host,
|
||
port,
|
||
width,
|
||
height,
|
||
refresh_hz,
|
||
compositor,
|
||
PUNKTFUNK_GAMEPAD_AUTO,
|
||
pin_sha256,
|
||
observed_sha256_out,
|
||
client_cert_pem,
|
||
client_key_pem,
|
||
timeout_ms,
|
||
)
|
||
}
|
||
}
|
||
|
||
/// Like [`punktfunk_connect_ex`], but additionally requests which virtual `gamepad` backend the
|
||
/// host creates for this session's pads (one of the `PUNKTFUNK_GAMEPAD_*` values).
|
||
/// `PUNKTFUNK_GAMEPAD_AUTO` (or any unrecognized value) lets the host decide (its
|
||
/// `PUNKTFUNK_GAMEPAD` env var, else X-Box 360); a concrete value is honored only if that
|
||
/// backend is available on the host. The resolved choice is readable via
|
||
/// [`punktfunk_connection_gamepad`] — only a DualSense session emits HID-output feedback.
|
||
///
|
||
/// # Safety
|
||
/// Same as [`punktfunk_connect`].
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_connect_ex2(
|
||
host: *const std::os::raw::c_char,
|
||
port: u16,
|
||
width: u32,
|
||
height: u32,
|
||
refresh_hz: u32,
|
||
compositor: u32,
|
||
gamepad: u32,
|
||
pin_sha256: *const u8,
|
||
observed_sha256_out: *mut u8,
|
||
client_cert_pem: *const std::os::raw::c_char,
|
||
client_key_pem: *const std::os::raw::c_char,
|
||
timeout_ms: u32,
|
||
) -> *mut PunktfunkConnection {
|
||
unsafe {
|
||
punktfunk_connect_ex3(
|
||
host,
|
||
port,
|
||
width,
|
||
height,
|
||
refresh_hz,
|
||
compositor,
|
||
gamepad,
|
||
0, // bitrate_kbps = 0: let the host pick its default
|
||
pin_sha256,
|
||
observed_sha256_out,
|
||
client_cert_pem,
|
||
client_key_pem,
|
||
timeout_ms,
|
||
)
|
||
}
|
||
}
|
||
|
||
/// Like [`punktfunk_connect_ex2`], but additionally requests the video encoder `bitrate_kbps`
|
||
/// (kilobits per second). `0` lets the host pick its default; any other value is clamped to the
|
||
/// host's supported range. After a speed test ([`punktfunk_connection_speed_test`]) a client can
|
||
/// reconnect (or pick at connect time) with the measured rate. The value the host actually
|
||
/// configured is readable via [`punktfunk_connection_bitrate`].
|
||
///
|
||
/// # Safety
|
||
/// Same as [`punktfunk_connect`].
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_connect_ex3(
|
||
host: *const std::os::raw::c_char,
|
||
port: u16,
|
||
width: u32,
|
||
height: u32,
|
||
refresh_hz: u32,
|
||
compositor: u32,
|
||
gamepad: u32,
|
||
bitrate_kbps: u32,
|
||
pin_sha256: *const u8,
|
||
observed_sha256_out: *mut u8,
|
||
client_cert_pem: *const std::os::raw::c_char,
|
||
client_key_pem: *const std::os::raw::c_char,
|
||
timeout_ms: u32,
|
||
) -> *mut PunktfunkConnection {
|
||
// Delegate to the launch-aware variant with no game requested (the host's default session).
|
||
unsafe {
|
||
punktfunk_connect_ex4(
|
||
host,
|
||
port,
|
||
width,
|
||
height,
|
||
refresh_hz,
|
||
compositor,
|
||
gamepad,
|
||
bitrate_kbps,
|
||
std::ptr::null(),
|
||
pin_sha256,
|
||
observed_sha256_out,
|
||
client_cert_pem,
|
||
client_key_pem,
|
||
timeout_ms,
|
||
)
|
||
}
|
||
}
|
||
|
||
/// Like [`punktfunk_connect_ex3`], but additionally asks the host to launch a library title in
|
||
/// this session. `launch_id` is a store-qualified [`crate::library`-style] id as returned by the
|
||
/// host's `GET /api/v1/library` (`steam:<appid>` / `custom:<id>`); the host resolves it against
|
||
/// its OWN library and runs the matching recipe — the client never sends a raw command. `NULL`
|
||
/// (or an empty / unknown id) ⇒ the host's default session, no game launched.
|
||
///
|
||
/// # Safety
|
||
/// Same as [`punktfunk_connect`]; `launch_id`, when non-NULL, must be a NUL-terminated C string.
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_connect_ex4(
|
||
host: *const std::os::raw::c_char,
|
||
port: u16,
|
||
width: u32,
|
||
height: u32,
|
||
refresh_hz: u32,
|
||
compositor: u32,
|
||
gamepad: u32,
|
||
bitrate_kbps: u32,
|
||
launch_id: *const std::os::raw::c_char,
|
||
pin_sha256: *const u8,
|
||
observed_sha256_out: *mut u8,
|
||
client_cert_pem: *const std::os::raw::c_char,
|
||
client_key_pem: *const std::os::raw::c_char,
|
||
timeout_ms: u32,
|
||
) -> *mut PunktfunkConnection {
|
||
// Back-compat: ex4 advertises no video caps (8-bit BT.709 SDR). HDR-capable embedders call
|
||
// `punktfunk_connect_ex5` with the cap bits.
|
||
unsafe {
|
||
punktfunk_connect_ex5(
|
||
host,
|
||
port,
|
||
width,
|
||
height,
|
||
refresh_hz,
|
||
compositor,
|
||
gamepad,
|
||
bitrate_kbps,
|
||
0,
|
||
launch_id,
|
||
pin_sha256,
|
||
observed_sha256_out,
|
||
client_cert_pem,
|
||
client_key_pem,
|
||
timeout_ms,
|
||
)
|
||
}
|
||
}
|
||
|
||
/// Like [`punktfunk_connect_ex4`], but additionally advertises the embedder's video decode/present
|
||
/// capabilities as `video_caps` — a bitfield of `PUNKTFUNK_VIDEO_CAP_10BIT` (can decode 10-bit
|
||
/// Main10) and `PUNKTFUNK_VIDEO_CAP_HDR` (can present BT.2020 PQ HDR10). The host upgrades to a
|
||
/// 10-bit / HDR encode ONLY when the matching bit is set (and the host opted in); `0` keeps the
|
||
/// 8-bit BT.709 SDR stream. After connecting, read the resolved colour via
|
||
/// [`punktfunk_connection_color_info`] and drain the mastering metadata via
|
||
/// [`punktfunk_connection_next_hdr_meta`].
|
||
///
|
||
/// # Safety
|
||
/// Same as [`punktfunk_connect`]; `launch_id`, when non-NULL, must be a NUL-terminated C string.
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub unsafe extern "C" fn punktfunk_connect_ex5(
|
||
host: *const std::os::raw::c_char,
|
||
port: u16,
|
||
width: u32,
|
||
height: u32,
|
||
refresh_hz: u32,
|
||
compositor: u32,
|
||
gamepad: u32,
|
||
bitrate_kbps: u32,
|
||
video_caps: u8,
|
||
launch_id: *const std::os::raw::c_char,
|
||
pin_sha256: *const u8,
|
||
observed_sha256_out: *mut u8,
|
||
client_cert_pem: *const std::os::raw::c_char,
|
||
client_key_pem: *const std::os::raw::c_char,
|
||
timeout_ms: u32,
|
||
) -> *mut PunktfunkConnection {
|
||
// Delegate to the surround-aware variant requesting stereo (the pre-surround behaviour).
|
||
unsafe {
|
||
punktfunk_connect_ex6(
|
||
host,
|
||
port,
|
||
width,
|
||
height,
|
||
refresh_hz,
|
||
compositor,
|
||
gamepad,
|
||
bitrate_kbps,
|
||
video_caps,
|
||
2, // audio_channels = stereo
|
||
launch_id,
|
||
pin_sha256,
|
||
observed_sha256_out,
|
||
client_cert_pem,
|
||
client_key_pem,
|
||
timeout_ms,
|
||
)
|
||
}
|
||
}
|
||
|
||
/// Like [`punktfunk_connect_ex5`], but additionally requests the audio channel count:
|
||
/// `2` (stereo, the default behaviour of every earlier variant), `6` (5.1) or `8` (7.1). The host
|
||
/// clamps the request to what it can actually capture and echoes the resolved count via
|
||
/// [`punktfunk_connection_audio_channels`]; the `0xC9` audio frames are Opus-(multi)stream encoded
|
||
/// for that layout. A client that wants surround calls this; everything else inherits stereo.
|
||
///
|
||
/// # Safety
|
||
/// Same as [`punktfunk_connect`].
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub unsafe extern "C" fn punktfunk_connect_ex6(
|
||
host: *const std::os::raw::c_char,
|
||
port: u16,
|
||
width: u32,
|
||
height: u32,
|
||
refresh_hz: u32,
|
||
compositor: u32,
|
||
gamepad: u32,
|
||
bitrate_kbps: u32,
|
||
video_caps: u8,
|
||
audio_channels: u8,
|
||
launch_id: *const std::os::raw::c_char,
|
||
pin_sha256: *const u8,
|
||
observed_sha256_out: *mut u8,
|
||
client_cert_pem: *const std::os::raw::c_char,
|
||
client_key_pem: *const std::os::raw::c_char,
|
||
timeout_ms: u32,
|
||
) -> *mut PunktfunkConnection {
|
||
let r = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
||
if host.is_null() {
|
||
return std::ptr::null_mut();
|
||
}
|
||
let host = match unsafe { std::ffi::CStr::from_ptr(host) }.to_str() {
|
||
Ok(s) => s,
|
||
Err(_) => return std::ptr::null_mut(),
|
||
};
|
||
// A bad-UTF-8 launch id is non-fatal — treat it as "no game" rather than failing connect.
|
||
let launch = match unsafe { opt_cstr(launch_id) } {
|
||
Ok(Some(s)) if !s.is_empty() => Some(s.to_string()),
|
||
_ => None,
|
||
};
|
||
let mode = crate::config::Mode {
|
||
width,
|
||
height,
|
||
refresh_hz,
|
||
};
|
||
// "Any unrecognized value = Auto" must hold for the FULL u32 domain — `as u8`
|
||
// would wrap 0x101 into a concrete choice before from_u8's fallback could apply.
|
||
let pref = u8::try_from(compositor)
|
||
.map(crate::config::CompositorPref::from_u8)
|
||
.unwrap_or_default();
|
||
let gamepad = u8::try_from(gamepad)
|
||
.map(crate::config::GamepadPref::from_u8)
|
||
.unwrap_or_default();
|
||
let pin = if pin_sha256.is_null() {
|
||
None
|
||
} else {
|
||
let mut p = [0u8; 32];
|
||
p.copy_from_slice(unsafe { std::slice::from_raw_parts(pin_sha256, 32) });
|
||
Some(p)
|
||
};
|
||
let identity = match (unsafe { opt_cstr(client_cert_pem) }, unsafe {
|
||
opt_cstr(client_key_pem)
|
||
}) {
|
||
(Ok(Some(c)), Ok(Some(k))) => Some((c.to_string(), k.to_string())),
|
||
(Ok(None), Ok(None)) => None,
|
||
_ => return std::ptr::null_mut(), // half an identity / bad UTF-8: fail closed
|
||
};
|
||
match crate::client::NativeClient::connect(
|
||
host,
|
||
port,
|
||
mode,
|
||
pref,
|
||
gamepad,
|
||
bitrate_kbps,
|
||
video_caps,
|
||
crate::audio::normalize_channels(audio_channels),
|
||
launch,
|
||
pin,
|
||
identity,
|
||
std::time::Duration::from_millis(timeout_ms as u64),
|
||
) {
|
||
Ok(c) => {
|
||
if !observed_sha256_out.is_null() {
|
||
unsafe {
|
||
std::slice::from_raw_parts_mut(observed_sha256_out, 32)
|
||
.copy_from_slice(&c.host_fingerprint);
|
||
}
|
||
}
|
||
Box::into_raw(Box::new(PunktfunkConnection {
|
||
inner: c,
|
||
last: std::sync::Mutex::new(None),
|
||
last_audio: std::sync::Mutex::new(None),
|
||
audio_pcm: std::sync::Mutex::new(AudioPcmState::default()),
|
||
}))
|
||
}
|
||
Err(_) => std::ptr::null_mut(),
|
||
}
|
||
}));
|
||
r.unwrap_or(std::ptr::null_mut())
|
||
}
|
||
|
||
/// Generate a persistent client identity: a self-signed certificate + private key, both
|
||
/// PEM, NUL-terminated, written into the caller's buffers. Generate ONCE, store both
|
||
/// strings (Keychain etc.), pass them to [`punktfunk_pair`] and every
|
||
/// [`punktfunk_connect`] — the certificate's fingerprint is how hosts recognize this
|
||
/// client. 4096-byte buffers are ample.
|
||
///
|
||
/// # Safety
|
||
/// `cert_pem_out` is writable for `cert_cap` bytes; `key_pem_out` for `key_cap`.
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_generate_identity(
|
||
cert_pem_out: *mut std::os::raw::c_char,
|
||
cert_cap: usize,
|
||
key_pem_out: *mut std::os::raw::c_char,
|
||
key_cap: usize,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
if cert_pem_out.is_null() || key_pem_out.is_null() {
|
||
return PunktfunkStatus::NullPointer;
|
||
}
|
||
let (cert, key) = match crate::quic::endpoint::generate_identity() {
|
||
Ok(t) => t,
|
||
Err(_) => return PunktfunkStatus::Io,
|
||
};
|
||
if cert.len() + 1 > cert_cap || key.len() + 1 > key_cap {
|
||
return PunktfunkStatus::InvalidArg;
|
||
}
|
||
unsafe {
|
||
std::ptr::copy_nonoverlapping(cert.as_ptr(), cert_pem_out as *mut u8, cert.len());
|
||
*cert_pem_out.add(cert.len()) = 0;
|
||
std::ptr::copy_nonoverlapping(key.as_ptr(), key_pem_out as *mut u8, key.len());
|
||
*key_pem_out.add(key.len()) = 0;
|
||
}
|
||
PunktfunkStatus::Ok
|
||
})
|
||
}
|
||
|
||
/// Run the PIN pairing ceremony against a host (see the protocol docs in punktfunk-core):
|
||
/// the host displays a short PIN; the user types it into the client app, which passes it
|
||
/// here. On success the host has stored this client's identity, the now-verified host
|
||
/// fingerprint is written to `host_sha256_out` (32 bytes) — persist it and pass it as
|
||
/// `pin_sha256` to [`punktfunk_connect`] from then on. Returns
|
||
/// [`PunktfunkStatus::Crypto`] for a wrong PIN.
|
||
///
|
||
/// # Safety
|
||
/// `host`/`client_cert_pem`/`client_key_pem`/`pin`/`name` are NUL-terminated UTF-8;
|
||
/// `host_sha256_out` is writable for 32 bytes.
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_pair(
|
||
host: *const std::os::raw::c_char,
|
||
port: u16,
|
||
client_cert_pem: *const std::os::raw::c_char,
|
||
client_key_pem: *const std::os::raw::c_char,
|
||
pin: *const std::os::raw::c_char,
|
||
name: *const std::os::raw::c_char,
|
||
host_sha256_out: *mut u8,
|
||
timeout_ms: u32,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
let (Ok(Some(host)), Ok(Some(cert)), Ok(Some(key)), Ok(Some(pin)), Ok(Some(name))) = (
|
||
unsafe { opt_cstr(host) },
|
||
unsafe { opt_cstr(client_cert_pem) },
|
||
unsafe { opt_cstr(client_key_pem) },
|
||
unsafe { opt_cstr(pin) },
|
||
unsafe { opt_cstr(name) },
|
||
) else {
|
||
return PunktfunkStatus::NullPointer;
|
||
};
|
||
if host_sha256_out.is_null() {
|
||
return PunktfunkStatus::NullPointer;
|
||
}
|
||
match crate::client::NativeClient::pair(
|
||
host,
|
||
port,
|
||
(cert, key),
|
||
pin,
|
||
name,
|
||
std::time::Duration::from_millis(timeout_ms as u64),
|
||
) {
|
||
Ok(fp) => {
|
||
unsafe {
|
||
std::slice::from_raw_parts_mut(host_sha256_out, 32).copy_from_slice(&fp);
|
||
}
|
||
PunktfunkStatus::Ok
|
||
}
|
||
Err(e) => e.status(),
|
||
}
|
||
})
|
||
}
|
||
|
||
/// Pull the next reassembled access unit, waiting up to `timeout_ms`. Returns
|
||
/// [`PunktfunkStatus::NoFrame`] on timeout and [`PunktfunkStatus::Closed`] once the session ended.
|
||
/// On `Ok`, `*out` borrows connection memory **until the next `next_au` call** on this
|
||
/// handle (the audio/rumble planes do not invalidate it).
|
||
///
|
||
/// # Safety
|
||
/// `c` is a valid connection handle; `out` is writable. At most one thread pulls video —
|
||
/// it may run concurrently with one audio-pulling and one rumble-pulling thread.
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_connection_next_au(
|
||
c: *mut PunktfunkConnection,
|
||
out: *mut PunktfunkFrame,
|
||
timeout_ms: u32,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
// Shared reference only: video and audio threads must never alias a `&mut`.
|
||
let c = match unsafe { c.as_ref() } {
|
||
Some(c) => c,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
if out.is_null() {
|
||
return PunktfunkStatus::NullPointer;
|
||
}
|
||
match c
|
||
.inner
|
||
.next_frame(std::time::Duration::from_millis(timeout_ms as u64))
|
||
{
|
||
Ok(frame) => {
|
||
let mut slot = c.last.lock().unwrap();
|
||
*slot = Some(frame);
|
||
let f = slot.as_ref().unwrap();
|
||
unsafe {
|
||
*out = PunktfunkFrame {
|
||
data: f.data.as_ptr(),
|
||
len: f.data.len(),
|
||
frame_index: f.frame_index,
|
||
pts_ns: f.pts_ns,
|
||
flags: f.flags,
|
||
};
|
||
}
|
||
PunktfunkStatus::Ok
|
||
}
|
||
Err(e) => e.status(),
|
||
}
|
||
})
|
||
}
|
||
|
||
/// One Opus audio packet pulled off a `punktfunk/1` connection (48 kHz stereo, 5 ms frames).
|
||
/// `data` borrows connection memory until the next `punktfunk_connection_next_audio` call.
|
||
#[cfg(feature = "quic")]
|
||
#[repr(C)]
|
||
pub struct PunktfunkAudioPacket {
|
||
pub data: *const u8,
|
||
pub len: usize,
|
||
pub seq: u32,
|
||
pub pts_ns: u64,
|
||
}
|
||
|
||
/// Pull the next Opus audio packet, waiting up to `timeout_ms`. Returns
|
||
/// [`PunktfunkStatus::NoFrame`] on timeout and [`PunktfunkStatus::Closed`] once the session ended.
|
||
/// On `Ok`, `out->data` borrows connection memory **until the next audio call** on this
|
||
/// handle (independent of the video slot). Drain from a dedicated audio thread — packets
|
||
/// arrive every 5 ms and the internal queue holds 320 ms.
|
||
///
|
||
/// # Safety
|
||
/// `c` is a valid connection handle; `out` is writable. At most one thread pulls audio —
|
||
/// it may run concurrently with the video/rumble pullers.
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_connection_next_audio(
|
||
c: *mut PunktfunkConnection,
|
||
out: *mut PunktfunkAudioPacket,
|
||
timeout_ms: u32,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
let c = match unsafe { c.as_ref() } {
|
||
Some(c) => c,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
if out.is_null() {
|
||
return PunktfunkStatus::NullPointer;
|
||
}
|
||
match c
|
||
.inner
|
||
.next_audio(std::time::Duration::from_millis(timeout_ms as u64))
|
||
{
|
||
Ok(pkt) => {
|
||
let mut slot = c.last_audio.lock().unwrap();
|
||
*slot = Some(pkt);
|
||
let p = slot.as_ref().unwrap();
|
||
unsafe {
|
||
*out = PunktfunkAudioPacket {
|
||
data: p.data.as_ptr(),
|
||
len: p.data.len(),
|
||
seq: p.seq,
|
||
pts_ns: p.pts_ns,
|
||
};
|
||
}
|
||
PunktfunkStatus::Ok
|
||
}
|
||
Err(e) => e.status(),
|
||
}
|
||
})
|
||
}
|
||
|
||
/// Read the audio channel count the host resolved for this session (from its Welcome): `2`
|
||
/// (stereo), `6` (5.1) or `8` (7.1). `*out` is filled when non-NULL. The `0xC9` Opus frames are
|
||
/// (multistream-)encoded for this layout; an embedder decoding raw frames itself must build its
|
||
/// decoder from THIS value (see [`crate::audio::layout_for`]) — or use
|
||
/// [`punktfunk_connection_next_audio_pcm`], which decodes in-core. Available immediately after a
|
||
/// successful connect (it doesn't change without a reconfigure).
|
||
///
|
||
/// # Safety
|
||
/// `c` is a valid connection handle; `out` is NULL or writable for one `u8`.
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_connection_audio_channels(
|
||
c: *mut PunktfunkConnection,
|
||
out: *mut u8,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
let c = match unsafe { c.as_ref() } {
|
||
Some(c) => c,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
if !out.is_null() {
|
||
// SAFETY: `out` is non-null and the caller guarantees it is writable for one `u8`.
|
||
unsafe { *out = c.inner.audio_channels };
|
||
}
|
||
PunktfunkStatus::Ok
|
||
})
|
||
}
|
||
|
||
/// One decoded audio frame from [`punktfunk_connection_next_audio_pcm`]: interleaved 32-bit
|
||
/// float PCM at 48 kHz, in the canonical wire channel order `FL FR FC LFE RL RR SL SR` (the
|
||
/// first `channels` of it). `samples` points at `frame_count * channels` floats and borrows
|
||
/// connection memory **until the next PCM call** on this handle.
|
||
#[cfg(feature = "quic")]
|
||
#[repr(C)]
|
||
pub struct PunktfunkAudioPcm {
|
||
/// Interleaved f32 samples (wire channel order), `frame_count * channels` long.
|
||
pub samples: *const f32,
|
||
/// Samples per channel in this frame.
|
||
pub frame_count: u32,
|
||
/// Channel count (2/6/8) — the negotiated [`punktfunk_connection_audio_channels`].
|
||
pub channels: u8,
|
||
/// Source packet sequence number.
|
||
pub seq: u32,
|
||
/// Capture presentation timestamp (ns).
|
||
pub pts_ns: u64,
|
||
}
|
||
|
||
/// Pull the next audio frame and **decode it in-core** to interleaved f32 PCM — for embedders
|
||
/// without a multistream-capable Opus decoder (e.g. Apple, whose AudioToolbox Opus path is
|
||
/// stereo-only). The decoder is built once from the negotiated channel count and handles 2/6/8
|
||
/// channels (a 1-coupled-stream multistream decoder is exactly a stereo decoder). Same
|
||
/// timeout/closed semantics as [`punktfunk_connection_next_audio`]; `out->samples` borrows
|
||
/// connection memory until the next PCM call on this handle. Use EITHER this or
|
||
/// [`punktfunk_connection_next_audio`] on a given connection, from one dedicated audio thread —
|
||
/// not both (they share the underlying queue).
|
||
///
|
||
/// # Safety
|
||
/// `c` is a valid connection handle; `out` is writable. At most one thread pulls audio.
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_connection_next_audio_pcm(
|
||
c: *mut PunktfunkConnection,
|
||
out: *mut PunktfunkAudioPcm,
|
||
timeout_ms: u32,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
let c = match unsafe { c.as_ref() } {
|
||
Some(c) => c,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
if out.is_null() {
|
||
return PunktfunkStatus::NullPointer;
|
||
}
|
||
let channels = crate::audio::normalize_channels(c.inner.audio_channels);
|
||
let pkt = match c
|
||
.inner
|
||
.next_audio(std::time::Duration::from_millis(timeout_ms as u64))
|
||
{
|
||
Ok(pkt) => pkt,
|
||
Err(e) => return e.status(),
|
||
};
|
||
let mut state = c.audio_pcm.lock().unwrap();
|
||
if state.decoder.is_none() {
|
||
let layout = crate::audio::layout_for(channels, false);
|
||
match opus::MSDecoder::new(48_000, layout.streams, layout.coupled, layout.mapping) {
|
||
Ok(d) => {
|
||
// Largest legal Opus frame is 120 ms = 5760 samples/ch.
|
||
state.pcm = vec![0f32; 5760 * channels as usize];
|
||
state.decoder = Some(d);
|
||
}
|
||
Err(_) => return PunktfunkStatus::Unsupported,
|
||
}
|
||
}
|
||
let AudioPcmState { decoder, pcm } = &mut *state;
|
||
let dec = decoder.as_mut().unwrap();
|
||
// `decode_float` divides the output buffer length by the channel count to get the
|
||
// per-channel capacity; an empty payload requests packet-loss concealment.
|
||
match dec.decode_float(&pkt.data, pcm, false) {
|
||
Ok(frame_count) => {
|
||
unsafe {
|
||
*out = PunktfunkAudioPcm {
|
||
samples: pcm.as_ptr(),
|
||
frame_count: frame_count as u32,
|
||
channels,
|
||
seq: pkt.seq,
|
||
pts_ns: pkt.pts_ns,
|
||
};
|
||
}
|
||
PunktfunkStatus::Ok
|
||
}
|
||
Err(_) => PunktfunkStatus::BadPacket,
|
||
}
|
||
})
|
||
}
|
||
|
||
/// Pull the next rumble (force-feedback) update, waiting up to `timeout_ms`. Amplitudes
|
||
/// are 0..0xFFFF (`low` = low-frequency motor, `high` = high-frequency), `(0, 0)` = stop.
|
||
/// Same timeout/closed semantics as [`punktfunk_connection_next_audio`].
|
||
///
|
||
/// # Safety
|
||
/// `c` is a valid connection handle; out pointers are writable (NULLs are skipped). At
|
||
/// most one thread pulls rumble — it may run concurrently with the video/audio pullers.
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_connection_next_rumble(
|
||
c: *mut PunktfunkConnection,
|
||
pad: *mut u16,
|
||
low: *mut u16,
|
||
high: *mut u16,
|
||
timeout_ms: u32,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
let c = match unsafe { c.as_ref() } {
|
||
Some(c) => c,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
match c
|
||
.inner
|
||
.next_rumble(std::time::Duration::from_millis(timeout_ms as u64))
|
||
{
|
||
Ok((p, l, h)) => {
|
||
unsafe {
|
||
if !pad.is_null() {
|
||
*pad = p;
|
||
}
|
||
if !low.is_null() {
|
||
*low = l;
|
||
}
|
||
if !high.is_null() {
|
||
*high = h;
|
||
}
|
||
}
|
||
PunktfunkStatus::Ok
|
||
}
|
||
Err(e) => e.status(),
|
||
}
|
||
})
|
||
}
|
||
|
||
/// Pull the next DualSense HID-output feedback event (lightbar / player LEDs / adaptive trigger)
|
||
/// the host's virtual pad received from a game, into `*out`. [`PunktfunkStatus::NoFrame`] on
|
||
/// timeout, [`PunktfunkStatus::Closed`] once the session ended. Only the DualSense host backend
|
||
/// emits these. Same threading rules as [`punktfunk_connection_next_rumble`] (one puller, may run
|
||
/// alongside the other planes).
|
||
///
|
||
/// # Safety
|
||
/// `c` is a valid connection handle; `out` is writable for one `PunktfunkHidOutput`.
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_connection_next_hidout(
|
||
c: *mut PunktfunkConnection,
|
||
out: *mut PunktfunkHidOutput,
|
||
timeout_ms: u32,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
let c = match unsafe { c.as_ref() } {
|
||
Some(c) => c,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
if out.is_null() {
|
||
return PunktfunkStatus::NullPointer;
|
||
}
|
||
match c
|
||
.inner
|
||
.next_hidout(std::time::Duration::from_millis(timeout_ms as u64))
|
||
{
|
||
Ok(h) => {
|
||
unsafe { *out = PunktfunkHidOutput::from_hid(&h) };
|
||
PunktfunkStatus::Ok
|
||
}
|
||
Err(e) => e.status(),
|
||
}
|
||
})
|
||
}
|
||
|
||
/// Pull the next static HDR metadata update (ST.2086 mastering display + content light level) for
|
||
/// an HDR session, into `*out`. [`PunktfunkStatus::NoFrame`] on timeout, [`PunktfunkStatus::Closed`]
|
||
/// once the session ended. The host sends one near session start and re-sends it on mastering
|
||
/// changes / keyframes; apply the latest to the display (`SetHDRMetaData` / `CAEDRMetadata` /
|
||
/// `KEY_HDR_STATIC_INFO`). Only an HDR session (`punktfunk_connection_color_info` reports a PQ
|
||
/// transfer) ever emits these. Same threading rules as [`punktfunk_connection_next_rumble`] (one
|
||
/// puller, may run alongside the other planes).
|
||
///
|
||
/// # Safety
|
||
/// `c` is a valid connection handle; `out` is writable for one `PunktfunkHdrMeta`.
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_connection_next_hdr_meta(
|
||
c: *mut PunktfunkConnection,
|
||
out: *mut PunktfunkHdrMeta,
|
||
timeout_ms: u32,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
let c = match unsafe { c.as_ref() } {
|
||
Some(c) => c,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
if out.is_null() {
|
||
return PunktfunkStatus::NullPointer;
|
||
}
|
||
match c
|
||
.inner
|
||
.next_hdr_meta(std::time::Duration::from_millis(timeout_ms as u64))
|
||
{
|
||
Ok(m) => {
|
||
unsafe { *out = PunktfunkHdrMeta::from_meta(&m) };
|
||
PunktfunkStatus::Ok
|
||
}
|
||
Err(e) => e.status(),
|
||
}
|
||
})
|
||
}
|
||
|
||
/// Read the session's resolved colour signalling + encode bit depth (from the host's Welcome).
|
||
/// Each out pointer is filled when non-NULL: `primaries`/`transfer`/`matrix` are CICP code points
|
||
/// (BT.709 = 1; BT.2020 = 9; PQ transfer = 16, HLG = 18; BT.2020-NCL matrix = 9), `full_range` is
|
||
/// 0 (limited) or 1 (full), `bit_depth` is 8 or 10. A `transfer` of 16/18 means HDR — configure an
|
||
/// HDR present path and drain [`punktfunk_connection_next_hdr_meta`]. Available immediately after a
|
||
/// successful connect (these don't change without a reconfigure).
|
||
///
|
||
/// # Safety
|
||
/// `c` is a valid connection handle; each out pointer is NULL or writable for its scalar.
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_connection_color_info(
|
||
c: *mut PunktfunkConnection,
|
||
primaries: *mut u8,
|
||
transfer: *mut u8,
|
||
matrix: *mut u8,
|
||
full_range: *mut u8,
|
||
bit_depth: *mut u8,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
let c = match unsafe { c.as_ref() } {
|
||
Some(c) => c,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
let color = c.inner.color;
|
||
unsafe {
|
||
if !primaries.is_null() {
|
||
*primaries = color.primaries;
|
||
}
|
||
if !transfer.is_null() {
|
||
*transfer = color.transfer;
|
||
}
|
||
if !matrix.is_null() {
|
||
*matrix = color.matrix;
|
||
}
|
||
if !full_range.is_null() {
|
||
*full_range = color.full_range;
|
||
}
|
||
if !bit_depth.is_null() {
|
||
*bit_depth = c.inner.bit_depth;
|
||
}
|
||
}
|
||
PunktfunkStatus::Ok
|
||
})
|
||
}
|
||
|
||
/// Read the session's resolved chroma subsampling (from the host's Welcome) as the HEVC
|
||
/// `chroma_format_idc`: `1` = 4:2:0 (the default every pre-4:4:4 host produced), `3` = full-chroma
|
||
/// 4:4:4. `*out` is filled when non-NULL. The in-band SPS is authoritative; this lets the embedder
|
||
/// pre-size its decoder / pick a 4:4:4 pixel format up front. Available immediately after a
|
||
/// successful connect (it doesn't change without a reconfigure).
|
||
///
|
||
/// # Safety
|
||
/// `c` is a valid connection handle; `out` is NULL or writable for one `u8`.
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_connection_chroma_format(
|
||
c: *mut PunktfunkConnection,
|
||
out: *mut u8,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
let c = match unsafe { c.as_ref() } {
|
||
Some(c) => c,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
if !out.is_null() {
|
||
// SAFETY: `out` is non-null and the caller guarantees it is writable for one `u8`.
|
||
unsafe { *out = c.inner.chroma_format };
|
||
}
|
||
PunktfunkStatus::Ok
|
||
})
|
||
}
|
||
|
||
/// Send one input event to the host as a QUIC datagram (non-blocking enqueue).
|
||
///
|
||
/// # Safety
|
||
/// `c` is a valid connection handle; `ev` points to a valid [`InputEvent`].
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_connection_send_input(
|
||
c: *mut PunktfunkConnection,
|
||
ev: *const InputEvent,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
let c = match unsafe { c.as_ref() } {
|
||
Some(c) => c,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
let ev = match unsafe { ev.as_ref() } {
|
||
Some(e) => e,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
match c.inner.send_input(ev) {
|
||
Ok(()) => PunktfunkStatus::Ok,
|
||
Err(e) => e.status(),
|
||
}
|
||
})
|
||
}
|
||
|
||
/// Send one Opus mic frame to the host as a QUIC datagram (48 kHz; the host decodes it into a
|
||
/// virtual microphone source its apps can record). Non-blocking enqueue; the host uses `seq`/
|
||
/// `pts_ns` (the caller's own counters) only for diagnostics. `opus_data`/`len` may be empty
|
||
/// (a DTX silence frame). The data is copied; the caller may reuse the buffer after this returns.
|
||
///
|
||
/// # Safety
|
||
/// `c` is a valid connection handle; `opus_data` is valid for `len` bytes (or `len == 0`).
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_connection_send_mic(
|
||
c: *mut PunktfunkConnection,
|
||
opus_data: *const u8,
|
||
len: usize,
|
||
seq: u32,
|
||
pts_ns: u64,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
let c = match unsafe { c.as_ref() } {
|
||
Some(c) => c,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
if opus_data.is_null() && len != 0 {
|
||
return PunktfunkStatus::NullPointer;
|
||
}
|
||
let opus = if len == 0 {
|
||
Vec::new()
|
||
} else {
|
||
unsafe { std::slice::from_raw_parts(opus_data, len) }.to_vec()
|
||
};
|
||
match c.inner.send_mic(seq, pts_ns, opus) {
|
||
Ok(()) => PunktfunkStatus::Ok,
|
||
Err(e) => e.status(),
|
||
}
|
||
})
|
||
}
|
||
|
||
/// Send one rich input event (DualSense touchpad contact or motion sample) to the host as a QUIC
|
||
/// datagram (non-blocking enqueue). The host applies it to its virtual DualSense pad — a no-op
|
||
/// unless the host runs the DualSense gamepad backend. [`PunktfunkStatus::InvalidArg`] on an
|
||
/// unknown `kind`.
|
||
///
|
||
/// # Safety
|
||
/// `c` is a valid connection handle; `rich` points to a valid [`PunktfunkRichInput`].
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_connection_send_rich_input(
|
||
c: *mut PunktfunkConnection,
|
||
rich: *const PunktfunkRichInput,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
let c = match unsafe { c.as_ref() } {
|
||
Some(c) => c,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
let rich = match unsafe { rich.as_ref() } {
|
||
Some(r) => r,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
match rich.to_rich() {
|
||
Some(r) => match c.inner.send_rich_input(r) {
|
||
Ok(()) => PunktfunkStatus::Ok,
|
||
Err(e) => e.status(),
|
||
},
|
||
None => PunktfunkStatus::InvalidArg,
|
||
}
|
||
})
|
||
}
|
||
|
||
/// The currently active session mode — the Welcome's, until an accepted
|
||
/// [`punktfunk_connection_request_mode`] switches it. Safe any time after connect.
|
||
///
|
||
/// # Safety
|
||
/// `c` is a valid connection handle; out pointers are writable (NULLs are skipped).
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_connection_mode(
|
||
c: *const PunktfunkConnection,
|
||
width: *mut u32,
|
||
height: *mut u32,
|
||
refresh_hz: *mut u32,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
let c = match unsafe { c.as_ref() } {
|
||
Some(c) => c,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
let mode = c.inner.mode();
|
||
unsafe {
|
||
if !width.is_null() {
|
||
*width = mode.width;
|
||
}
|
||
if !height.is_null() {
|
||
*height = mode.height;
|
||
}
|
||
if !refresh_hz.is_null() {
|
||
*refresh_hz = mode.refresh_hz;
|
||
}
|
||
}
|
||
PunktfunkStatus::Ok
|
||
})
|
||
}
|
||
|
||
/// The virtual gamepad backend the host actually resolved for this session (one of the
|
||
/// `PUNKTFUNK_GAMEPAD_*` values; the `Welcome`'s echo of the [`punktfunk_connect_ex2`]
|
||
/// preference). `PUNKTFUNK_GAMEPAD_AUTO` = an older host that didn't say — assume X-Box 360,
|
||
/// no HID-output feedback. Safe any time after connect.
|
||
///
|
||
/// # Safety
|
||
/// `c` is a valid connection handle; `gamepad` is writable (NULL is skipped).
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_connection_gamepad(
|
||
c: *const PunktfunkConnection,
|
||
gamepad: *mut u32,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
let c = match unsafe { c.as_ref() } {
|
||
Some(c) => c,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
unsafe {
|
||
if !gamepad.is_null() {
|
||
*gamepad = c.inner.resolved_gamepad.to_u8() as u32;
|
||
}
|
||
}
|
||
PunktfunkStatus::Ok
|
||
})
|
||
}
|
||
|
||
/// The compositor backend the host actually resolved for this session (one of the
|
||
/// `PUNKTFUNK_COMPOSITOR_*` values; the `Welcome`'s echo of the [`punktfunk_connect_ex`]
|
||
/// preference). `PUNKTFUNK_COMPOSITOR_AUTO` = an older host that didn't say. Clients use it for
|
||
/// compositor-specific behavior — e.g. a client-side cursor by default on
|
||
/// `PUNKTFUNK_COMPOSITOR_GAMESCOPE`, whose PipeWire capture carries no cursor. Safe any time after
|
||
/// connect.
|
||
///
|
||
/// # Safety
|
||
/// `c` is a valid connection handle; `compositor` is writable (NULL is skipped).
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_connection_compositor(
|
||
c: *const PunktfunkConnection,
|
||
compositor: *mut u32,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
let c = match unsafe { c.as_ref() } {
|
||
Some(c) => c,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
unsafe {
|
||
if !compositor.is_null() {
|
||
*compositor = c.inner.resolved_compositor.to_u8() as u32;
|
||
}
|
||
}
|
||
PunktfunkStatus::Ok
|
||
})
|
||
}
|
||
|
||
/// The video encoder bitrate (kilobits per second) the host actually configured for this session
|
||
/// — the [`punktfunk_connect_ex3`] request clamped to the host's range, or its default when `0`
|
||
/// was requested. `0` = an older host that didn't report it. Safe any time after connect.
|
||
///
|
||
/// # Safety
|
||
/// `c` is a valid connection handle; `bitrate_kbps` is writable (NULL is skipped).
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_connection_bitrate(
|
||
c: *const PunktfunkConnection,
|
||
bitrate_kbps: *mut u32,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
let c = match unsafe { c.as_ref() } {
|
||
Some(c) => c,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
unsafe {
|
||
if !bitrate_kbps.is_null() {
|
||
*bitrate_kbps = c.inner.resolved_bitrate_kbps;
|
||
}
|
||
}
|
||
PunktfunkStatus::Ok
|
||
})
|
||
}
|
||
|
||
/// The host↔client wall-clock offset (nanoseconds, **host minus client**) measured by the
|
||
/// connect-time skew handshake. Add it to a local receive/present timestamp (same realtime clock,
|
||
/// `CLOCK_REALTIME` / `gettimeofday`-epoch nanoseconds) to express that instant in the host's
|
||
/// capture clock — the clock the per-access-unit `pts_ns` is stamped in — so glass-to-glass latency
|
||
/// (e.g. present-time minus `pts_ns`) is valid across machines. `0` = no correction: either an older
|
||
/// host that didn't answer the handshake, or genuinely synchronized clocks. Safe any time after
|
||
/// connect.
|
||
///
|
||
/// # Safety
|
||
/// `c` is a valid connection handle; `offset_ns` is writable (NULL is skipped).
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_connection_clock_offset_ns(
|
||
c: *const PunktfunkConnection,
|
||
offset_ns: *mut i64,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
let c = match unsafe { c.as_ref() } {
|
||
Some(c) => c,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
unsafe {
|
||
if !offset_ns.is_null() {
|
||
*offset_ns = c.inner.clock_offset_ns;
|
||
}
|
||
}
|
||
PunktfunkStatus::Ok
|
||
})
|
||
}
|
||
|
||
/// Ask the host to switch the live session to `width`x`height`@`refresh_hz` without
|
||
/// reconnecting (window resized, refresh changed). Non-blocking enqueue: on acceptance the
|
||
/// stream continues at the new mode — the first new-mode access unit is an IDR with
|
||
/// in-band parameter sets (rebuild the decoder from it) — and
|
||
/// [`punktfunk_connection_mode`] reflects the switch. A rejected request leaves the
|
||
/// session unchanged.
|
||
///
|
||
/// # Safety
|
||
/// `c` is a valid connection handle.
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_connection_request_mode(
|
||
c: *const PunktfunkConnection,
|
||
width: u32,
|
||
height: u32,
|
||
refresh_hz: u32,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
let c = match unsafe { c.as_ref() } {
|
||
Some(c) => c,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
match c.inner.request_mode(crate::config::Mode {
|
||
width,
|
||
height,
|
||
refresh_hz,
|
||
}) {
|
||
Ok(()) => PunktfunkStatus::Ok,
|
||
Err(e) => e.status(),
|
||
}
|
||
})
|
||
}
|
||
|
||
/// Ask the host's encoder to emit a fresh IDR keyframe now — client recovery when the
|
||
/// decoder has stalled (the infinite-GOP stream sends one opening IDR then P-frames only, so
|
||
/// a wedged decoder would otherwise freeze until the next loss-triggered recovery keyframe).
|
||
/// Non-blocking, fire-and-forget; the recovered keyframe is the only ack. The caller should
|
||
/// THROTTLE — the decode stays wedged for several frames until the IDR lands, so requesting
|
||
/// every frame would flood the control stream.
|
||
///
|
||
/// # Safety
|
||
/// `c` is a valid connection handle.
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_connection_request_keyframe(
|
||
c: *const PunktfunkConnection,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
let c = match unsafe { c.as_ref() } {
|
||
Some(c) => c,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
match c.inner.request_keyframe() {
|
||
Ok(()) => PunktfunkStatus::Ok,
|
||
Err(e) => e.status(),
|
||
}
|
||
})
|
||
}
|
||
|
||
/// Cumulative access units the host→client reassembler dropped as unrecoverable (FEC couldn't
|
||
/// rebuild them). A video loop polls this and calls [`punktfunk_connection_request_keyframe`]
|
||
/// when it climbs — the correct loss trigger under the host's infinite GOP, where unrecoverable
|
||
/// loss yields reference-missing delta frames the decoder *silently conceals* (frozen / garbage
|
||
/// picture, no decode error), so a decode-error trigger rarely fires. Monotonic for the session;
|
||
/// compare against the last observed value. Writes 0 to `out` on a NULL connection.
|
||
///
|
||
/// # Safety
|
||
/// `c` is a valid connection handle; `out` is writable (NULL is skipped).
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_connection_frames_dropped(
|
||
c: *const PunktfunkConnection,
|
||
out: *mut u64,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
let c = match unsafe { c.as_ref() } {
|
||
Some(c) => c,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
unsafe {
|
||
if !out.is_null() {
|
||
*out = c.inner.frames_dropped();
|
||
}
|
||
}
|
||
PunktfunkStatus::Ok
|
||
})
|
||
}
|
||
|
||
/// A speed-test measurement, filled by [`punktfunk_connection_probe_result`]. `done` is 0 until
|
||
/// the host's end-of-burst report lands, then 1 (the numbers are final). `throughput_kbps` is the
|
||
/// delivered wire throughput to drive a bitrate choice from; `loss_pct` is the link loss and
|
||
/// `host_drop_pct` the host-side send-buffer drop (raise `net.core.wmem_max`) — they're measured
|
||
/// separately so a host that can't keep up reads differently from a lossy link.
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy, Debug, Default)]
|
||
pub struct PunktfunkProbeResult {
|
||
/// 1 once the host's end-of-burst report arrived (measurement final); else 0 (partial).
|
||
pub done: u8,
|
||
/// Delivered wire bytes (header + shard) / packets the client received during the burst.
|
||
pub recv_bytes: u64,
|
||
pub recv_packets: u32,
|
||
/// Application goodput bytes / access units the host offered.
|
||
pub host_bytes: u64,
|
||
pub host_packets: u32,
|
||
/// The host's measured burst duration, milliseconds (the throughput denominator).
|
||
pub elapsed_ms: u32,
|
||
/// Delivered wire throughput = `recv_bytes * 8 / elapsed_ms` (kilobits/second).
|
||
pub throughput_kbps: u32,
|
||
/// Link loss `(wire_packets_sent − recv_packets) / wire_packets_sent` as a percentage.
|
||
pub loss_pct: f32,
|
||
/// Host-side send-buffer drop `send_dropped / (wire_packets_sent + send_dropped)`, percent.
|
||
pub host_drop_pct: f32,
|
||
/// Wire packets the host put on the link, and the ones its send buffer dropped (raw counts).
|
||
pub wire_packets_sent: u32,
|
||
pub send_dropped: u32,
|
||
}
|
||
|
||
/// Start a bandwidth speed test: ask the host to burst filler over the data plane at
|
||
/// `target_kbps` of goodput for `duration_ms` (each clamped host-side to ≤ 3 Gbps / ≤ 5 s),
|
||
/// *briefly pausing video*. Non-blocking — poll [`punktfunk_connection_probe_result`] until its
|
||
/// `done` field is 1. Starting a probe resets any prior measurement.
|
||
///
|
||
/// # Safety
|
||
/// `c` is a valid connection handle.
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_connection_speed_test(
|
||
c: *const PunktfunkConnection,
|
||
target_kbps: u32,
|
||
duration_ms: u32,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
let c = match unsafe { c.as_ref() } {
|
||
Some(c) => c,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
match c.inner.request_probe(target_kbps, duration_ms) {
|
||
Ok(()) => PunktfunkStatus::Ok,
|
||
Err(e) => e.status(),
|
||
}
|
||
})
|
||
}
|
||
|
||
/// Read the current speed-test measurement into `*out` (partial until `out->done == 1`). Safe to
|
||
/// poll repeatedly after [`punktfunk_connection_speed_test`]; before any probe it reports zeros.
|
||
///
|
||
/// # Safety
|
||
/// `c` is a valid connection handle; `out` is writable for one `PunktfunkProbeResult` (NULL is an
|
||
/// error).
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_connection_probe_result(
|
||
c: *const PunktfunkConnection,
|
||
out: *mut PunktfunkProbeResult,
|
||
) -> PunktfunkStatus {
|
||
guard(|| {
|
||
let c = match unsafe { c.as_ref() } {
|
||
Some(c) => c,
|
||
None => return PunktfunkStatus::NullPointer,
|
||
};
|
||
if out.is_null() {
|
||
return PunktfunkStatus::NullPointer;
|
||
}
|
||
let o = c.inner.probe_result();
|
||
unsafe {
|
||
*out = PunktfunkProbeResult {
|
||
done: o.done as u8,
|
||
recv_bytes: o.recv_bytes,
|
||
recv_packets: o.recv_packets,
|
||
host_bytes: o.host_bytes,
|
||
host_packets: o.host_packets,
|
||
elapsed_ms: o.elapsed_ms,
|
||
throughput_kbps: o.throughput_kbps,
|
||
loss_pct: o.loss_pct,
|
||
host_drop_pct: o.host_drop_pct,
|
||
wire_packets_sent: o.wire_packets_sent,
|
||
send_dropped: o.send_dropped,
|
||
};
|
||
}
|
||
PunktfunkStatus::Ok
|
||
})
|
||
}
|
||
|
||
/// Close the connection and free the handle (joins the internal threads). NULL is a no-op.
|
||
///
|
||
/// # Safety
|
||
/// `c` was returned by [`punktfunk_connect`] and is not used after this call.
|
||
#[cfg(feature = "quic")]
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn punktfunk_connection_close(c: *mut PunktfunkConnection) {
|
||
if !c.is_null() {
|
||
drop(unsafe { Box::from_raw(c) });
|
||
}
|
||
}
|