Embed the core (C ABI)
Build a client for a platform without a Punktfunk app by linking the punktfunk-ffi C ABI.
Link punktfunk-ffi into your own client and it speaks the whole protocol for you: the QUIC
handshake, pairing, the encrypted media path, FEC and loss recovery, clock sync. You decode
video, present it, play audio and read input. The contract is
include/punktfunk_core.h:
every symbol carries a doc comment, and this page is the map to it. A Rust client depends on
punktfunk-core with features = ["quic"] instead, as the Android client does.
Build and link
cargo build -p punktfunk-ffi --features quic --releaseOne build writes both library kinds to target/release/: libpunktfunk_ffi.a (static),
libpunktfunk_ffi.so / .dylib / punktfunk_ffi.dll (dynamic). cargo run -p gen-headers
regenerates the header from
punktfunk-ffi
with cbindgen; the header is checked in, and CI fails when it is stale.
Compile your code with -DPUNKTFUNK_FEATURE_QUIC. The whole client API (punktfunk_connect*,
punktfunk_connection_*, punktfunk_pair, punktfunk_probe, punktfunk_generate_identity) sits
behind that define. Without it the header declares only the version check, logging,
punktfunk_wake_on_lan, the decode helpers and the in-process loopback pair the tests use
(punktfunk_test_loopback_pair).
cc -std=c11 -DPUNKTFUNK_FEATURE_QUIC -I include -c myclient.c
cc myclient.o -L target/release -lpunktfunk_ffi -o myclient # dynamic
NATIVE=$(cargo rustc -p punktfunk-ffi --features quic --release --lib --crate-type staticlib \
-- --print native-static-libs 2>&1 | sed -n 's/.*native-static-libs: //p' | tail -1)
cc myclient.o target/release/libpunktfunk_ffi.a $NATIVE -o myclient # staticTo cross-compile, rustup target add <triple>, set CC_<triple> and
CARGO_TARGET_<TRIPLE>_LINKER to your SDK's compiler, and add --target <triple>. The QUIC build
compiles aws-lc (TLS) and libopus from C, so the target needs a working C compiler. Apple builds go
through scripts/build-xcframework.sh, which packs the C ABI into PunktfunkCore.xcframework with
PUNKTFUNK_FEATURE_QUIC already defined. Toolchain setup:
Build from source.
Check the version first
if (punktfunk_abi_version() != PUNKTFUNK_ABI_VERSION) abort(); /* library ≠ header */Do this before any other call: structs without a size prefix (PunktfunkHidOutput,
PunktfunkAudioPcm) rely on it to stop an older binary passing a slot that is too small. Structs
that start with struct_size (PunktfunkConnectOpts, PunktfunkRichInputEx, PunktfunkConfig)
are zero-initialised, then struct_size = sizeof(T); the core rejects a size below its minimum.
The wire protocol is versioned apart from the ABI, by ALPN, and the core checks it against the
host itself. ABI changes per release:
CHANGELOG.md.
Rules for every call
- Status. Fallible calls return
PunktfunkStatus:PUNKTFUNK_STATUS_OKis 0, errors are negative. In pull loops,NO_FRAMEmeans nothing arrived before the timeout andCLOSEDmeans the session ended.-20to-37(PUNKTFUNK_STATUS_REJECTED_*) is the host refusing you; show each as its own sentence.REJECTED_PROFILE_LOCKED(-37) asks for the profile's password: prompt, then connect again withunlock. A Rust panic never crosses the boundary; it returnsPANIC(-99). - One thread per plane. Video, audio, rumble, HID output, HDR metadata, host timing, cursor, clipboard and pad audio are separate queues. Pull each from at most one thread; different planes may run concurrently.
- Borrowed buffers.
next_au,next_audio,next_audio_pcm,next_cursor_shapeandnext_clipboardhand back pointers into core memory that stay valid until your next pull on the same plane. Decode or copy before you pull again. - Senders and setters (
send_*,set_*,request_*) enqueue and return, from any thread.punktfunk_connect*,punktfunk_pair,punktfunk_probeandpunktfunk_wake_on_lanblock on the network: keep them off your UI thread.
Lifecycle
- Logs (optional).
punktfunk_set_log_callback(3, cb, user)routes the core's log lines to your sink;3is info and up. The callback runs on core threads. - Identity, once per install.
punktfunk_generate_identitywrites a certificate and key as PEM (4096-byte buffers are enough). Store both securely and pass them to every pair and connect. Hosts require pairing unless started with--open, so an anonymous connect gets refused. - Admission. Either connect with the identity and let the operator click Approve in the
console (the connect waits up to
timeout_ms, or for the verdict withon_pending; the host holds the request for 3 minutes), or callpunktfunk_pairwith the PIN the host shows (CRYPTOmeans a wrong PIN). See Pairing. Store the host fingerprint you get back and pass it aspin_sha256on every later connect. - Connect with
punktfunk_connect_opts(below), read the resolved session, and start one thread per plane you use. On NULL,status_outsays why. - Tear down. Stop your pull threads, call
punktfunk_connection_disconnect_quitonly when the user chose to stop (the host then drops the display instead of holding it for a reconnect), thenpunktfunk_connection_close, which joins the core's threads and frees the handle.
Discovery isn't part of the C ABI. Browse mDNS for _punktfunk._udp with the platform's API, or
let the user type an address. punktfunk_probe checks a known address; compare its
observed_sha256_out with the stored fingerprint, since any machine on that address answers.
punktfunk_wake_on_lan wakes a host from the MACs in its mDNS mac TXT record.
Connect
Fill PunktfunkConnectOpts; zero means auto or unspecified for every field. Advertise only codecs
and caps you can decode and present, because the host upgrades to 10-bit, HDR, 4:4:4 or AV1 when
you set the bit. video_codecs = 0 means HEVC only.
| Field | Meaning |
|---|---|
host, port | Required. The host's native port (Ports). |
width, height, refresh_hz | The mode the host creates its virtual display at. |
video_codecs, preferred_codec | PUNKTFUNK_CODEC_* bits you decode; one bit to prefer. |
video_caps | PUNKTFUNK_VIDEO_CAP_10BIT / _HDR / _444. |
audio_channels | 2, 6 or 8. |
audio_rate_hz, audio_bits | Leave 0/0 for Opus. Any non-zero pair asks for lossless PCM. |
gamepad, compositor | PUNKTFUNK_GAMEPAD_* / PUNKTFUNK_COMPOSITOR_*; 0 lets the host pick. |
bitrate_kbps | 0 = host default. |
client_caps | PUNKTFUNK_CLIENT_CAP_CURSOR only if you draw the cursor yourself. |
video_fit | PUNKTFUNK_VIDEO_FIT_*: how your view fills when the frame's shape differs. |
preset_id, preset_name | The settings preset this dial names; the host shows it and hands it to hooks. |
pin_sha256, client_cert_pem, client_key_pem, device_name | Trust and identity (above). |
launch_id | steam:<appid> or custom:<id> to start a library title. |
profile_id | The profile to play as; null lets the host choose. punktfunk_connection_profile reads which it resolved. |
unlock | A PunktfunkProfileUnlock for a profile on a person's own account: the password as typed, or a key proof signed over punktfunk_profile_unlock_message. It goes only to a pinned host: without pin_sha256 the connect fails with INVALID_ARG. The core copies the secret; wipe yours after the call. |
prompts_unlock | 1 if you ask for the password and connect again after REJECTED_PROFILE_LOCKED. A non-null unlock implies it. |
timeout_ms | Bounds the whole connect, including a wait for approval unless on_pending is set. |
on_pending, on_pending_ctx | Called once, on the connecting thread, when the host's operator is deciding about this device: show "waiting for approval". The connect then waits for the host's verdict, not timeout_ms. |
The positional punktfunk_connect to punktfunk_connect_ex12 stay for existing callers and are
frozen; new options land only in the struct.
After connect, build your decoder and presenter from what the host chose, never from your request:
punktfunk_connection_codec, _color_info (CICP; transfer 16 or 18 is HDR), _chroma_format,
_mode, _audio_channels, _audio_sample_rate, _audio_bits, _gamepad, _host_caps.
Video
punktfunk_connection_next_au delivers complete access units in order, with parameter sets in
band. The first is an IDR; after that the stream is P-frames only. The core recovers from loss:
it freezes on the last good picture, asks the host for a repair, and asks again until one
decodes. Tell it what your decoder sees:
- Call
punktfunk_connection_recovery_on_framefor every AU before you decode it, in the ordernext_aureturned them. Passcomplete = true, anddecode_order = 0unless your decoder numbers its decodes. Whenout_admitis false, drop the AU: it arrived behind one you already took and would rewind the decoder.out_gapis the number of frames lost ahead of it, and thegapthatpunktfunk_au_admission_notetakes. - Call
punktfunk_connection_recovery_on_decodedfor every picture the decoder returns, with its AU'sflags. Passkeyframe = false,PUNKTFUNK_ANCHOR_EVIDENCE_UNAVAILABLEanddecode_order = 0unless your decoder reports them. Whenout_holdis true, keep the last good picture on screen and discard this one.out_forgivetrue tells a decoder that tracks damaged references to clear that record. - Call
punktfunk_connection_recovery_on_no_outputfor an AU the decoder refused or returned nothing for. Callpunktfunk_connection_recovery_lostfor a loss the frame index doesn't show: a wedged decoder, an AU you had to drop. - Call
punktfunk_connection_recovery_resetwhen the decoder stops while the session lives: you rebuild it, or you drain AUs undecoded in the background. Without it the core keeps asking for a keyframe that nothing decodes. - Call
punktfunk_connection_request_keyframewhen your decoder needs an IDR: it lost its state, or it has no parameter sets yet. Once a decoder reports, the core sends one ask per 100 ms at most, so you can call it for every AU you can't decode.
The core watches for unrecoverable frames itself. punktfunk_connection_frames_dropped is a
counter for a stats view.
punktfunk_connection_request_mode switches resolution or refresh mid-session. When the host
accepts, the next AU is an IDR: rebuild the decoder from it.
Audio
Use one of the two per connection. punktfunk_connection_next_audio_pcm decodes in the core to
interleaved f32 (FL FR FC LFE RL RR SL SR); open your device at _audio_sample_rate.
punktfunk_connection_next_audio hands you raw packets, for a client with a multistream Opus decoder.
Input
Fill a PunktfunkInputEvent and call punktfunk_connection_send_input. Fields depend on kind:
| Kind | Fields |
|---|---|
MOUSE_MOVE_ABS, TOUCH_DOWN/_MOVE | x/y in pixels, flags = (width << 16) | height. A zero flags is dropped. |
GAMEPAD_BUTTON | code = PUNKTFUNK_BTN_*, x ≠ 0 pressed, flags = pad index. |
GAMEPAD_AXIS | code = PUNKTFUNK_AXIS_*, flags = pad. Sticks are i16 with +y up; triggers 0–255. |
MOUSE_SCROLL | x = signed delta, 120 per detent; code 0 vertical, 1 horizontal. |
Check punktfunk_connection_host_caps before the capability-gated paths: send_pen needs
PUNKTFUNK_HOST_CAP_PEN, TEXT_INPUT events need PUNKTFUNK_HOST_CAP_TEXT_INPUT, and
GAMEPAD_STATE snapshots need PUNKTFUNK_HOST_CAP_GAMEPAD_STATE. Other uplinks: send_mic (Opus
you encode), send_rich_input2 (touchpads, motion), send_hid_report (raw reports for a
PUNKTFUNK_GAMEPAD_STEAMCONTROLLER2 pad).
Feedback planes
| Pull | What you do with it |
|---|---|
next_rumble_cmd2 | Apply the motor levels; the core handles lease expiry. next_rumble, _rumble2 and _rumble_cmd read the same plane, so use exactly one. |
next_hidout | Replay lightbar, player LEDs, adaptive triggers or raw HID on the physical pad. |
next_hdr_meta | Hand ST.2086 and content light level to your HDR display path. |
next_host_timing | Split host and network latency in a stats view. |
next_cursor_shape / _state | Draw the pointer. Only with PUNKTFUNK_CLIENT_CAP_CURSOR. |
After CLOSED, punktfunk_connection_end_reason says whether the session ended normally, and
_end_reject / _end_reject_said give the host's refusal and its sentence.
Minimal example
Connects, pulls 600 access units, and quits.
#include <stdio.h>
#include "punktfunk_core.h"
int main(int argc, char **argv) {
if (argc < 2 || punktfunk_abi_version() != PUNKTFUNK_ABI_VERSION) return 1;
static char cert[4096], key[4096]; /* generate once, then load from storage */
if (punktfunk_generate_identity(cert, sizeof cert, key, sizeof key)) return 1;
PunktfunkConnectOpts o = {0};
o.struct_size = sizeof o;
o.host = argv[1]; o.port = 9777;
o.width = 1920; o.height = 1080; o.refresh_hz = 60;
o.video_codecs = PUNKTFUNK_CODEC_HEVC | PUNKTFUNK_CODEC_H264;
o.client_cert_pem = cert; o.client_key_pem = key; o.device_name = "My Client";
o.timeout_ms = 180000; /* long enough to click Approve in the console */
uint8_t host_fp[32]; int32_t status = 0;
PunktfunkConnection *c = punktfunk_connect_opts(&o, host_fp, &status);
if (!c) { fprintf(stderr, "connect: status %d\n", status); return 1; }
uint8_t codec = 0; punktfunk_connection_codec(c, &codec);
PunktfunkFrame f;
for (int n = 0; n < 600;) {
PunktfunkStatus rc = punktfunk_connection_next_au(c, &f, 20);
if (rc == PUNKTFUNK_STATUS_CLOSED) break;
if (rc != PUNKTFUNK_STATUS_OK) continue;
bool admit = true, hold = false;
punktfunk_connection_recovery_on_frame(c, f.frame_index, true, 0, &admit, NULL);
if (!admit) continue; /* behind an AU already taken */
/* feed f.data / f.len to a decoder for `codec`, then for the picture it returns: */
punktfunk_connection_recovery_on_decoded(
c, f.flags, false, PUNKTFUNK_ANCHOR_EVIDENCE_UNAVAILABLE, 0, &hold, NULL);
/* show the picture unless `hold` */
n++;
}
punktfunk_connection_disconnect_quit(c);
punktfunk_connection_close(c);
return 0;
}A real client reports a refused AU and a stopped decoder too, persists the identity and
host_fp, and runs audio and feedback on their own threads. For a complete client over the same
API, read
crates/client/pf-client-core/src/session.rs.
bash crates/client/punktfunk-ffi/tests/c/run.sh proves the static library links from C on your machine.
Writing plugins
Build a host plugin with @punktfunk/plugin-kit — the manifest, the sandbox, a minimal library source, testing on a host and publishing to a catalog.
Multi-seat contract
The rules between a Punktfunk host and its seat supervisor — the control channel, the environment a seat host starts with, the Windows connector reservation, and on Linux the door, the owner's row and a seat on a person's own account.