PunktfunkPunktfunk

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.

cargo build -p punktfunk-ffi --features quic --release

One 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     # static

To 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_OK is 0, errors are negative. In pull loops, NO_FRAME means nothing arrived before the timeout and CLOSED means the session ended. -20 to -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 with unlock. A Rust panic never crosses the boundary; it returns PANIC (-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_shape and next_clipboard hand 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_probe and punktfunk_wake_on_lan block on the network: keep them off your UI thread.

Lifecycle

  1. Logs (optional). punktfunk_set_log_callback(3, cb, user) routes the core's log lines to your sink; 3 is info and up. The callback runs on core threads.
  2. Identity, once per install. punktfunk_generate_identity writes 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.
  3. 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 with on_pending; the host holds the request for 3 minutes), or call punktfunk_pair with the PIN the host shows (CRYPTO means a wrong PIN). See Pairing. Store the host fingerprint you get back and pass it as pin_sha256 on every later connect.
  4. Connect with punktfunk_connect_opts (below), read the resolved session, and start one thread per plane you use. On NULL, status_out says why.
  5. Tear down. Stop your pull threads, call punktfunk_connection_disconnect_quit only when the user chose to stop (the host then drops the display instead of holding it for a reconnect), then punktfunk_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.

FieldMeaning
host, portRequired. The host's native port (Ports).
width, height, refresh_hzThe mode the host creates its virtual display at.
video_codecs, preferred_codecPUNKTFUNK_CODEC_* bits you decode; one bit to prefer.
video_capsPUNKTFUNK_VIDEO_CAP_10BIT / _HDR / _444.
audio_channels2, 6 or 8.
audio_rate_hz, audio_bitsLeave 0/0 for Opus. Any non-zero pair asks for lossless PCM.
gamepad, compositorPUNKTFUNK_GAMEPAD_* / PUNKTFUNK_COMPOSITOR_*; 0 lets the host pick.
bitrate_kbps0 = host default.
client_capsPUNKTFUNK_CLIENT_CAP_CURSOR only if you draw the cursor yourself.
video_fitPUNKTFUNK_VIDEO_FIT_*: how your view fills when the frame's shape differs.
preset_id, preset_nameThe settings preset this dial names; the host shows it and hands it to hooks.
pin_sha256, client_cert_pem, client_key_pem, device_nameTrust and identity (above).
launch_idsteam:<appid> or custom:<id> to start a library title.
profile_idThe profile to play as; null lets the host choose. punktfunk_connection_profile reads which it resolved.
unlockA 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_unlock1 if you ask for the password and connect again after REJECTED_PROFILE_LOCKED. A non-null unlock implies it.
timeout_msBounds the whole connect, including a wait for approval unless on_pending is set.
on_pending, on_pending_ctxCalled 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_frame for every AU before you decode it, in the order next_au returned them. Pass complete = true, and decode_order = 0 unless your decoder numbers its decodes. When out_admit is false, drop the AU: it arrived behind one you already took and would rewind the decoder. out_gap is the number of frames lost ahead of it, and the gap that punktfunk_au_admission_note takes.
  • Call punktfunk_connection_recovery_on_decoded for every picture the decoder returns, with its AU's flags. Pass keyframe = false, PUNKTFUNK_ANCHOR_EVIDENCE_UNAVAILABLE and decode_order = 0 unless your decoder reports them. When out_hold is true, keep the last good picture on screen and discard this one. out_forgive true tells a decoder that tracks damaged references to clear that record.
  • Call punktfunk_connection_recovery_on_no_output for an AU the decoder refused or returned nothing for. Call punktfunk_connection_recovery_lost for a loss the frame index doesn't show: a wedged decoder, an AU you had to drop.
  • Call punktfunk_connection_recovery_reset when 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_keyframe when 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:

KindFields
MOUSE_MOVE_ABS, TOUCH_DOWN/_MOVEx/y in pixels, flags = (width << 16) | height. A zero flags is dropped.
GAMEPAD_BUTTONcode = PUNKTFUNK_BTN_*, x ≠ 0 pressed, flags = pad index.
GAMEPAD_AXIScode = PUNKTFUNK_AXIS_*, flags = pad. Sticks are i16 with +y up; triggers 0–255.
MOUSE_SCROLLx = 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

PullWhat you do with it
next_rumble_cmd2Apply the motor levels; the core handles lease expiry. next_rumble, _rumble2 and _rumble_cmd read the same plane, so use exactly one.
next_hidoutReplay lightbar, player LEDs, adaptive triggers or raw HID on the physical pad.
next_hdr_metaHand ST.2086 and content light level to your HDR display path.
next_host_timingSplit host and network latency in a stats view.
next_cursor_shape / _stateDraw 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.

On this page