Files
punktfunk/docs/embedding-the-c-abi.md
T
enricobuehler 0dc414f197
ci / web (push) Successful in 1m4s
ci / docs-site (push) Successful in 1m19s
decky / build-publish (push) Successful in 33s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 40s
apple / swift (push) Successful in 3m38s
ci / bench (push) Successful in 7m5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9m21s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8m25s
docker / deploy-docs (push) Successful in 35s
android / android (push) Successful in 12m34s
arch / build-publish (push) Successful in 15m3s
deb / build-publish (push) Successful in 16m18s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m7s
apple / screenshots (push) Successful in 19m20s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 12m17s
ci / rust (push) Successful in 23m12s
docs: guide for embedding the C ABI (webOS, Xbox, Tizen examples)
Developer integration guide for building a punktfunk client on any platform
by linking punktfunk-core through its stable C ABI: what the core does vs.
what the embedder supplies, build/link/cross-compile, the full client
lifecycle (identity/pairing, connect ladder, video+recovery loop, audio,
input, feedback planes, teardown), plus worked blueprints for webOS, Xbox
(GDK), and Tizen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 10:49:13 +02:00

720 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Embedding punktfunk-core (the C ABI)
This guide is for developers who want to build their **own** punktfunk client — on a platform we
don't ship an app for — by linking `punktfunk-core` through its stable C ABI. It covers what the
core does (and, importantly, what it doesn't), how to build and link it, the full client lifecycle,
and worked integration blueprints for **webOS**, **Xbox**, and **Tizen**.
The authoritative header is [`include/punktfunk_core.h`](../include/punktfunk_core.h) — it is
generated from Rust by cbindgen and every symbol carries a doc comment. This guide is the narrative;
the header is the contract.
---
## 1. What the core gives you — and what it doesn't
`punktfunk-core` is the *one* implementation of the wire format, shared by the host and every
first-party client. When you embed it you get the entire network side of a client for free:
**The core does:**
- The `punktfunk/1` handshake (Hello/Welcome/Start) over a **QUIC control plane**.
- The **UDP video data plane**: packetization, reassembly, pacing, socket tuning.
- **Forward error correction** (GF(2⁸) ReedSolomon and GF(2¹⁶) Leopard-RS) — loss recovery.
- **AES-128-GCM** session crypto, **cert pinning / TOFU**, and **SPAKE2 PIN pairing**.
- **Clock synchronization** (host↔client offset for glass-to-glass latency math).
- Delivery of every plane: reassembled **video access units**, **Opus audio** (raw or decoded to
PCM in-core), **rumble**, **DualSense HID output**, **HDR metadata**, **host timing**.
- The uplink: **input events**, **mic**, **rich input** (touchpad/motion).
- Loss-recovery signalling: keyframe requests and **reference-frame invalidation** (RFI).
**The core does NOT do (this is your job):**
- **Video decoding.** The core hands you encoded access units (H.264, HEVC, or AV1 NAL units /
OBUs). You feed them to the platform's hardware decoder.
- **Rendering / presentation.** You own the swapchain / surface and put decoded frames on glass.
- **Audio output.** You own the audio sink; the core only delivers Opus (or f32 PCM).
- **Input capture / controller enumeration.** You read the platform's input and translate it into
`PunktfunkInputEvent`s.
- **UI, discovery UX, settings.** (mDNS discovery is not part of the C ABI; see §4.)
Think of it as: **the core is the modem and the codec-agnostic protocol brain; you are the TV and
the remote.**
```
┌─────────────────────────── punktfunk-core (C ABI) ───────────────────────────┐
your platform │ │ punktfunk host
┌───────────┐ input │ punktfunk_connection_send_input / _send_mic / _send_rich_input ───────────────▶
│ remote / │──────────▶│ │
│ gamepad │ │ QUIC control + UDP data + FEC + AES-GCM │
└───────────┘ │ │
┌───────────┐ AUs │ ◀── punktfunk_connection_next_au (H.264 / HEVC / AV1 access units) │◀─── encoder
│ HW decoder│◀──────────│ ◀── punktfunk_connection_next_audio(_pcm)(Opus / f32 PCM) │
│ + display │ │ ◀── punktfunk_connection_next_rumble2 / _next_hidout / _next_hdr_meta │
└───────────┘ └──────────────────────────────────────────────────────────────────────────────┘
```
---
## 2. Building and linking the library
### 2.1 The `quic` feature is mandatory for clients
The entire connection API (`punktfunk_connect*`, `punktfunk_connection_*`, pairing, probe) is
gated, in both Rust and the C header, behind the **`quic`** Cargo feature. Everything under
`#if defined(PUNKTFUNK_FEATURE_QUIC)` in the header only exists when the core was built with it.
> **Two things must line up or nothing links:**
> 1. Build the library with `--features quic`.
> 2. Compile your C/C++ with `-DPUNKTFUNK_FEATURE_QUIC` so the header declares those prototypes.
The non-QUIC surface (`punktfunk_session_new`, `punktfunk_host_submit_frame`,
`punktfunk_client_poll_frame`, the loopback test pair) is the **raw transport** used by the host and
by tests. **Client embedders do not use it** — you use the `punktfunk_connect*` family.
### 2.2 Build outputs
```sh
# Native (host machine) — good for a desktop prototype
cargo build -p punktfunk-core --features quic --release
```
`crate-type = ["lib", "cdylib", "staticlib"]`, so one build produces all three:
| Output | File (per platform) | Use when |
|-------------|----------------------------------------------------------------|----------|
| `cdylib` | `libpunktfunk_core.so` / `.dylib` / `punktfunk_core.dll` | dynamic linking (most TV apps, sandboxed apps) |
| `staticlib` | `libpunktfunk_core.a` / `punktfunk_core.lib` | static embedding into a single binary |
The header lands at `include/punktfunk_core.h` (regenerated on build; also checked in).
### 2.3 Cross-compiling for your device
Every target below is a standard Rust target. Add it and point Cargo at the platform sysroot/linker.
```sh
rustup target add aarch64-unknown-linux-gnu # most ARM Smart TVs (webOS, Tizen)
rustup target add armv7-unknown-linux-gnueabihf # older 32-bit TV SoCs
rustup target add x86_64-pc-windows-msvc # Xbox (GDK consoles are x64)
# Tell Cargo which cross-linker + sysroot to use (example: aarch64 TV NDK)
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc
export CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc
cargo build -p punktfunk-core --features quic --release \
--target aarch64-unknown-linux-gnu
```
The QUIC tree is deliberately **ring-only** (no aws-lc-rs / no cmake), so cross builds do not need a
C crypto toolchain — this is what keeps the TV cross-compiles simple. `libopus` (for the in-core PCM
decode path) is the only C dependency and builds with the target `CC`.
### 2.4 Compiling and linking your app
```sh
# compile
cc -std=c11 -DPUNKTFUNK_FEATURE_QUIC -I path/to/include -c myclient.c
# link, dynamic
cc myclient.o -L target/aarch64-unknown-linux-gnu/release -lpunktfunk_core -o myclient
# link, static — you also need the native libs rustc pulls in
NATIVE=$(cargo rustc -p punktfunk-core --features quic --release \
--target aarch64-unknown-linux-gnu \
--crate-type staticlib -- --print native-static-libs 2>&1 \
| sed -n 's/.*native-static-libs: //p' | tail -1)
cc myclient.o target/aarch64-unknown-linux-gnu/release/libpunktfunk_core.a $NATIVE -o myclient
```
`--print native-static-libs` is the reliable way to discover the platform libs (`-lpthread`,
`-lm`, WinSock, etc.) a static link needs — the C harness at
[`crates/punktfunk-core/tests/c/run.sh`](../crates/punktfunk-core/tests/c/run.sh) does exactly this
and is your smallest end-to-end reference.
### 2.5 Version check first, always
```c
#include "punktfunk_core.h"
if (punktfunk_abi_version() != ABI_VERSION) {
// The .so you loaded is a different core than this header was generated from. Abort.
}
```
`ABI_VERSION` (the embeddable C surface) is distinct from `WIRE_VERSION` (what the handshake
carries). The ABI can grow — new `connect_ex` variants, new planes — without touching the wire, so a
newer client keeps talking to a deployed host. Only equal `WIRE_VERSION`s interoperate on the wire;
the core enforces that inside the handshake.
---
## 3. Core concepts
### 3.1 The connection handle
`PunktfunkConnection *` is an **opaque handle to one live client session** — QUIC control plane plus
UDP data plane, with all the I/O pumped on threads the core owns internally. You obtain it from a
`punktfunk_connect*` call and release it with `punktfunk_connection_close`.
### 3.2 The threading contract (read this before you design your loops)
The planes are **independent single-consumer queues**. The rule:
> Each plane (video, audio, rumble, HID-out, HDR, host-timing) may be pulled from **its own thread,
> at most one thread per plane**. Different planes may be pulled concurrently. Never pull the *same*
> plane from two threads.
A typical client runs **three threads**:
- **Video thread** — blocks on `punktfunk_connection_next_au`, decodes, presents.
- **Audio thread** — blocks on `punktfunk_connection_next_audio_pcm` (or `_next_audio`).
- **Feedback thread** — pulls rumble / HID-out / HDR-meta (all low-rate; poll or short-timeout).
Input is sent from whatever thread reads your input (send calls are non-blocking enqueues and are
safe to call from any thread).
### 3.3 Borrowed memory
`next_au`, `next_audio`, `next_audio_pcm` return a struct whose `data`/`samples` pointer **borrows
core-owned memory that is valid only until your next pull on that same handle/plane.** Decode or copy
before you call again. Cross-plane pulls do not invalidate each other (the audio pull won't free the
video buffer).
### 3.4 Status codes
Every fallible call returns `PunktfunkStatus`: `PUNKTFUNK_STATUS_OK == 0`, all errors negative
(`rc < 0`). The two you branch on constantly in the pull loops:
- `PUNKTFUNK_STATUS_NO_FRAME` (-5) — nothing ready within `timeout_ms`; loop again.
- `PUNKTFUNK_STATUS_CLOSED` (-10) — the session ended; tear down and leave the loop.
---
## 4. Identity, pairing, discovery, wake
### 4.1 Generate a persistent identity once
```c
char cert[4096], key[4096];
if (punktfunk_generate_identity(cert, sizeof cert, key, sizeof key) != PUNKTFUNK_STATUS_OK) { /* … */ }
// Persist BOTH strings securely (platform secure storage / keychain).
// The certificate's SHA-256 is how a host recognizes this client after pairing.
```
Do this **once per device/install** and store the PEM strings. Pass them to every `pair` and
`connect` call. Anonymous (`NULL`/`NULL`) sessions are rejected by hosts running `--require-pairing`.
### 4.2 Pair (PIN ceremony)
```c
uint8_t host_fp[32];
PunktfunkStatus rc = punktfunk_pair(host, port, cert, key,
user_typed_pin, "Living Room TV",
host_fp, /*timeout_ms=*/10000);
// rc == PUNKTFUNK_STATUS_CRYPTO => wrong PIN.
// rc == PUNKTFUNK_STATUS_OK => persist host_fp; it's the pin for future connects.
```
The host shows a PIN; the user types it into your UI. On success you get the host's verified
fingerprint — store it keyed by host, and pass it as `pin_sha256` on every later connect (that pins
the host and defeats MITM). For a first connect without prior pairing you may pass `NULL` for the pin
and capture `observed_sha256_out` (trust-on-first-use), then pin it thereafter.
### 4.3 Reachability probe and Wake-on-LAN
- `punktfunk_probe(host, port, timeout_ms)` — a bounded, trust-agnostic handshake attempt. Returns
`OK` if the host answered (drives "online" pips), `TIMEOUT` otherwise. Works over routed
networks (Tailscale/VPN) where mDNS never reaches. Call off the UI thread.
- `punktfunk_wake_on_lan(macs, mac_count, last_known_ip)` — sends WoL magic packets. The host's wake
MAC(s) arrive out-of-band via the mDNS `mac` TXT record, so no connection is needed to wake a
sleeping host.
> **Discovery is out of scope for the C ABI.** mDNS/DNS-SD browsing is the embedder's job (use the
> platform's Bonjour/Avahi/`nsd` API to find `_punktfunk._udp` hosts, or let the user type an
> IP/hostname). The core only *connects*.
---
## 5. Connecting
There is a **ladder** of `connect` variants; each adds parameters and defaults the rest to the prior
variant's behavior. Use the highest one you need — they all return `PunktfunkConnection *` (NULL on
failure) and block up to `timeout_ms` for the handshake, so **call off your UI thread.**
| Variant | Adds |
|------------------|------|
| `punktfunk_connect` | base: `width`, `height`, `refresh_hz`, pin, identity |
| `punktfunk_connect_ex` | `compositor` preference (Linux hosts) |
| `punktfunk_connect_ex2` | `gamepad` backend (X-Box 360 / DualSense / …) |
| `punktfunk_connect_ex3` | `bitrate_kbps` |
| `punktfunk_connect_ex4` | `launch_id` (auto-launch a library title) |
| `punktfunk_connect_ex5` | `video_caps` (10-bit / HDR / 4:4:4 / host-timing) |
| `punktfunk_connect_ex6` | `audio_channels` (2 / 6 / 8) |
| **`punktfunk_connect_ex7`** | **`video_codecs` + `preferred_codec`** — the recommended full call |
```c
uint8_t caps = 0; // add PUNKTFUNK_VIDEO_CAP_10BIT | _HDR | _444 as supported
uint8_t codecs = PUNKTFUNK_CODEC_HEVC; // OR-in _H264 (needed for software hosts) / _AV1
uint8_t host_fp[32]; uint8_t observed[32];
PunktfunkConnection *c = punktfunk_connect_ex7(
"192.168.1.50", 9777,
1920, 1080, 60, // mode
PUNKTFUNK_COMPOSITOR_AUTO,
PUNKTFUNK_GAMEPAD_XBOX360,
/*bitrate_kbps=*/20000,
caps,
/*audio_channels=*/2,
codecs, /*preferred_codec=*/PUNKTFUNK_CODEC_HEVC,
/*launch_id=*/NULL,
host_fp, // pin (or NULL for TOFU)
observed, // filled with the observed fp
cert, key, // identity
/*timeout_ms=*/8000);
if (!c) { /* handshake failed */ }
```
> **Advertise only what you can actually decode and present.** The host upgrades to 10-bit / HDR /
> 4:4:4 / AV1 **only** when you set the matching bit *and* it opted in. Setting a cap you can't honor
> gives you a stream you can't render.
### 5.1 Read the resolved session (right after connect)
The host echoes what it actually chose. Build your decoder and present path from **these**, never
from your request:
```c
uint8_t codec, prim, trc, mtx, full, depth, chroma, ch;
punktfunk_connection_codec(c, &codec); // PUNKTFUNK_CODEC_{H264,HEVC,AV1}
punktfunk_connection_color_info(c, &prim, &trc, &mtx, &full, &depth); // CICP + bit depth
punktfunk_connection_chroma_format(c, &chroma); // 1 = 4:2:0, 3 = 4:4:4
punktfunk_connection_audio_channels(c, &ch); // 2 / 6 / 8
int64_t clk = 0; punktfunk_connection_clock_offset_ns(c, &clk); // hostclient ns, for latency math
uint32_t w, h, hz; punktfunk_connection_mode(c, &w, &h, &hz);
```
`trc == 16` (PQ) or `18` (HLG) means an **HDR** session — set up an HDR present path and drain
`punktfunk_connection_next_hdr_meta`. On `PUNKTFUNK_COMPOSITOR_GAMESCOPE`
(`punktfunk_connection_compositor`) draw a client-side cursor by default (that capture carries none).
---
## 6. The video loop
The core delivers **complete, in-order access units** with in-band parameter sets. The first AU is
an IDR — build your decoder from it (and from the resolved codec/color above). The stream is then an
**infinite-GOP** of P-frames: no periodic IDRs, so loss recovery is explicit and is the part you must
get right.
```c
PunktfunkFrame f;
for (;;) {
PunktfunkStatus rc = punktfunk_connection_next_au(c, &f, /*timeout_ms=*/20);
if (rc == PUNKTFUNK_STATUS_NO_FRAME) continue;
if (rc == PUNKTFUNK_STATUS_CLOSED) break;
if (rc != PUNKTFUNK_STATUS_OK) continue;
// (1) Loss recovery — let the core do the hard part. Call this EVERY frame:
bool gap = false;
punktfunk_connection_note_frame_index(c, f.frame_index, &gap);
// On a forward gap it fires a throttled RFI request for you (clean P-frame recovery on
// AMD-LTR/NVENC hosts, no IDR spike). `gap == true` is your cue to freeze on the last good
// picture until re-anchor (see (3)).
// (2) Clean re-anchor points (infinite-GOP has no periodic keyframes):
bool recovery_point = f.flags & USER_FLAG_RECOVERY_POINT; // intra-refresh wave boundary
bool recovery_anchor = f.flags & USER_FLAG_RECOVERY_ANCHOR; // definitive LTR/RFI re-anchor
// (3) Decode + present. f.data/f.len are valid until the next next_au call.
decode_and_present(f.data, f.len, f.pts_ns);
// If you implement freeze-until-reanchor: while frozen, keep redrawing the last good frame
// and lift the freeze on a real keyframe, on the FIRST recovery_anchor, or the SECOND
// recovery_point after the gap.
// (4) Backstop trigger: poll the reassembler's unrecoverable-drop count. Under infinite GOP,
// unrecoverable loss yields reference-missing deltas the decoder *silently conceals*
// (frozen/garbage, no decode error) — so this counter, not a decode error, is the signal.
uint64_t dropped = 0; punktfunk_connection_frames_dropped(c, &dropped);
if (dropped > last_dropped) { // THROTTLE this (≤ ~1/100ms) — see below
punktfunk_connection_request_keyframe(c);
last_dropped = dropped;
}
}
```
**Recovery, in priority order:**
1. `note_frame_index` every frame — the cheapest, earliest signal. It issues a **throttled RFI**
request (`request_rfi`) for the exact lost range so RFI-capable hosts recover with a clean
P-frame (no 2040× IDR bandwidth spike).
2. `frames_dropped` climbing → `request_keyframe` as the backstop when RFI can't help or the recovery
frame itself was lost.
3. A wedged decoder (received AUs but no decoded output for several frames) → `request_keyframe`.
**Throttle** both `request_keyframe` and `request_rfi` — decode stays wedged for several frames until
recovery lands, so one request per ~100 ms is right; requesting per frame floods the control stream.
If you prefer not to hand-roll this, `note_frame_index` + a throttled `frames_dropped`→keyframe check
is a complete, correct recovery policy on its own.
### 6.1 Mid-session resolution / refresh changes
```c
punktfunk_connection_request_mode(c, new_w, new_h, new_hz);
```
Non-blocking. On acceptance the next AU is a fresh IDR with in-band parameter sets — **rebuild your
decoder from it** — and `punktfunk_connection_mode` reflects the switch. Use this when your window
resizes or the display refresh changes.
---
## 7. Audio
Two mutually-exclusive ways to consume audio — pick one, on one dedicated thread:
- **`punktfunk_connection_next_audio_pcm`** — the core decodes to interleaved **f32 PCM** at 48 kHz
in channel order `FL FR FC LFE RL RR SL SR`. Use this if you lack a *multistream*-capable Opus
decoder (e.g. Apple's AudioToolbox is stereo-only; many TV audio stacks are too). Simplest path.
- **`punktfunk_connection_next_audio`** — raw Opus packets (5 ms frames). Use only if you have a
multistream Opus decoder and build it from `punktfunk_connection_audio_channels` (see
`audio::layout_for`). Do **not** mix the two on one connection.
```c
PunktfunkAudioPcm a;
for (;;) {
PunktfunkStatus rc = punktfunk_connection_next_audio_pcm(c, &a, /*timeout_ms=*/100);
if (rc == PUNKTFUNK_STATUS_CLOSED) break;
if (rc != PUNKTFUNK_STATUS_OK) continue;
// a.samples = a.frame_count * a.channels f32s, valid until the next PCM call.
audio_sink_write(a.samples, a.frame_count, a.channels);
}
```
---
## 8. Input (uplink)
Fill a `PunktfunkInputEvent` and send it — non-blocking, from any thread:
```c
PunktfunkInputEvent ev; memset(&ev, 0, sizeof ev);
```
Field meaning depends on `kind` (`PunktfunkInputKind`). The contracts that trip people up:
- **Absolute pointer / touch** (`MOUSE_MOVE_ABS`, `TOUCH_DOWN/MOVE`): `x`/`y` are pixel coordinates
and **`flags` must pack your coordinate space as `(width << 16) | height`** — the host normalizes
against it. A **zero `flags` is dropped**, so always set it.
- **Gamepad button** (`GAMEPAD_BUTTON`): `code` = a `PUNKTFUNK_BTN_*` bit, `x != 0` = pressed,
`flags` = pad index (0..15).
- **Gamepad axis** (`GAMEPAD_AXIS`): `code` = `PUNKTFUNK_AXIS_*`, `flags` = pad index. Sticks are
**i16 (32768..32767), +y = up** (opposite of screen coordinates); triggers are 0..255.
- **Relative mouse** (`MOUSE_MOVE`): `x`/`y` carry `dx`/`dy`. **Scroll** (`MOUSE_SCROLL`): `x` is the
signed delta.
```c
// Example: press A on pad 0
ev.kind = PUNKTFUNK_INPUT_KIND_GAMEPAD_BUTTON;
ev.code = PUNKTFUNK_BTN_A; ev.x = 1; ev.flags = 0;
punktfunk_connection_send_input(c, &ev);
// Example: absolute touch at (640,360) on a 1280x720 surface
ev.kind = PUNKTFUNK_INPUT_KIND_TOUCH_DOWN;
ev.code = 0 /*finger id*/; ev.x = 640; ev.y = 360;
ev.flags = (1280u << 16) | 720u;
punktfunk_connection_send_input(c, &ev);
```
> Gamepad state on the C ABI uses **per-transition** button/axis events (above). The wire also has an
> idempotent full-pad *snapshot* mode (`HOST_CAP_GAMEPAD_STATE`), but it is not exposed as a
> dedicated C entry point — per-transition events are accepted by every host and are the C path.
**Other uplinks (all non-blocking):**
- `punktfunk_connection_send_mic(c, opus, len, seq, pts_ns)` — Opus mic frames (you encode).
- `punktfunk_connection_send_rich_input(c, &rich)` — DualSense touchpad contact / motion sample.
- `punktfunk_connection_send_rich_input2(c, &richEx)` — the forward-compatible superset (Steam
trackpads, signed coords, pressure); set `struct_size = sizeof(PunktfunkRichInputEx)`.
---
## 9. Feedback planes (rumble, HID, HDR)
Pull these on your feedback thread (or poll with `timeout_ms = 0`). Same
`NO_FRAME`/`CLOSED` semantics as everywhere.
- **Rumble** — `punktfunk_connection_next_rumble2(c, &pad, &low, &high, &ttl_ms, timeout)`.
Amplitudes 0..0xFFFF; `(0,0)` = stop. `ttl_ms` is a host-supplied self-terminating lease — render
the level for that long unless renewed; `PUNKTFUNK_RUMBLE_NO_TTL` means fall back to your own
staleness timeout. (The v1 `_next_rumble` drops the TTL — prefer v2.)
- **DualSense HID output** — `punktfunk_connection_next_hidout(c, &out, timeout)`. `out.kind` selects
lightbar RGB / player LEDs / adaptive-trigger effect / trackpad haptic. Replay on a real DualSense
via the platform's controller API. Only a DualSense-backend session emits these.
- **HDR metadata** — `punktfunk_connection_next_hdr_meta(c, &meta, timeout)`. ST.2086 mastering
display + content light level, in HDR10 SEI fixed-point units — ready to hand to DXGI
`DXGI_HDR_METADATA_HDR10`, Apple `CAEDRMetadata`, or Android `KEY_HDR_STATIC_INFO`. Only an HDR
session emits these; apply the latest to your display.
- **Host timing** (optional, if you advertised `VIDEO_CAP_HOST_TIMING`) —
`punktfunk_connection_next_host_timing` gives the host's capture→sent µs per AU, so your stats HUD
can split `host` vs `network` latency.
---
## 10. Speed test and stats
```c
punktfunk_connection_speed_test(c, /*target_kbps=*/50000, /*duration_ms=*/2000); // pauses video briefly
PunktfunkProbeResult r;
do { punktfunk_connection_probe_result(c, &r); } while (!r.done); // poll
// r.throughput_kbps drives a bitrate choice; r.loss_pct vs r.host_drop_pct
// distinguishes a lossy link from a host that can't keep up.
```
---
## 11. Teardown
```c
punktfunk_connection_disconnect_quit(c); // user pressed "stop": host tears down now, no linger
punktfunk_connection_close(c); // joins internal threads, frees the handle
```
Call `disconnect_quit` **only** on a deliberate user quit — it makes the host drop the virtual
display immediately. On a network drop / backgrounding, skip it (a plain `close`) so the host lingers
and a reconnect can resume. After `close`, the handle is dead; stop all your plane threads first.
---
# Platform integration blueprints
Everything above is exact — it's the punktfunk side, which is identical on every platform. The
sections below are **blueprints**: they name the real decode/present/input subsystems on each target
and show where the punktfunk glue plugs in. Treat the platform-SDK calls as illustrative — confirm
signatures against that platform's current SDK. The pattern is always the same:
> **connect → build HW decoder from the resolved codec/color → three threads (video decode+present,
> audio, feedback) → translate native input into `PunktfunkInputEvent`s.**
---
## 12. webOS (LG Smart TV)
**App model.** webOS ships both web apps and **native** apps via the **webOS NDK** (Linux, ARM —
usually `aarch64`, some older panels `armv7`). A low-latency streaming client wants the native path:
you get real threads, a hardware video pipeline, and SDL for window/input. (This mirrors how the
community Moonlight webOS client is built.)
**Cross-compile the core.** Build `libpunktfunk_core.so` with the webOS NDK sysroot:
```sh
rustup target add aarch64-unknown-linux-gnu
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=$WEBOS_NDK/.../aarch64-webos-linux-gnu-gcc
export CC_aarch64_unknown_linux_gnu=$WEBOS_NDK/.../aarch64-webos-linux-gnu-gcc
cargo build -p punktfunk-core --features quic --release --target aarch64-unknown-linux-gnu
```
Bundle the `.so` in your `.ipk` alongside the header-compiled client.
**Video decode + present.** Use the webOS media pipeline for the SoC's hardware decoder — historically
**NDL-DirectMedia** and on newer panels the **Starfish** media pipeline
(`com.webos.service.mediapipeline`). You push the punktfunk AUs (Annex-B H.264/HEVC/AV1) into the
pipeline's `feed`/`push` entry point; it decodes and composits to the TV's video plane, and you draw
your overlay/UI on the graphics plane over it. Present cadence is driven by the pipeline, not you.
**Audio.** Simplest is `punktfunk_connection_next_audio_pcm`**PulseAudio** (webOS's audio server)
via a simple playback stream. Or route the raw Opus into the media pipeline if it accepts a
secondary audio ES.
**Input.** The Magic Remote / standard remote and Bluetooth gamepads surface as **SDL2** events (LG
ships SDL for NDK apps). Map remote keys to `KEY_DOWN/UP`, the pointer to `MOUSE_MOVE_ABS` (set
`flags = (w<<16)|h`), and `SDL_GameController` axes/buttons to `GAMEPAD_AXIS`/`GAMEPAD_BUTTON`.
**Skeleton:**
```c
// 1. connect (HEVC, stereo, SDR to start)
PunktfunkConnection *c = punktfunk_connect_ex7(host, 9777, 1920,1080,60,
PUNKTFUNK_COMPOSITOR_AUTO, PUNKTFUNK_GAMEPAD_XBOX360, 20000,
/*caps=*/0, /*ch=*/2, PUNKTFUNK_CODEC_HEVC, PUNKTFUNK_CODEC_HEVC,
NULL, host_fp, observed, cert, key, 8000);
uint8_t codec; punktfunk_connection_codec(c, &codec);
StarfishPipeline *p = starfish_open(codec /*→ "video/hevc" etc*/, 1920,1080);
// 2. video thread
PunktfunkFrame f;
while (running) {
if (punktfunk_connection_next_au(c, &f, 20) != PUNKTFUNK_STATUS_OK) continue;
bool gap; punktfunk_connection_note_frame_index(c, f.frame_index, &gap);
starfish_feed(p, f.data, f.len, f.pts_ns); // HW decode + present
uint64_t d; punktfunk_connection_frames_dropped(c, &d);
if (d > last_d) { punktfunk_connection_request_keyframe(c); last_d = d; } // throttle
}
// 3. audio thread → next_audio_pcm → pulse_write(...)
// 4. SDL input thread → fill PunktfunkInputEvent → punktfunk_connection_send_input(c, &ev)
```
**Gotchas.** webOS app networking is permitted for UDP/QUIC, but background/suspend policy is
aggressive — pull-loop `CLOSED` on backgrounding and reconnect on resume. Video-plane / graphics-plane
z-order and scaling are set through the pipeline's display-window API, not by drawing pixels yourself.
---
## 13. Xbox (GDK — Series X|S / One)
**App model.** Use the **Microsoft GDK** (Game Development Kit) for consoles — a **C++** title with
**Direct3D 12** and the **GameInput** API. Consoles are **x64**, so the core target is
`x86_64-pc-windows-msvc`. (Requires an ID@Xbox / partner console in Developer Mode; UWP on retail is
possible but GDK is the low-latency path.)
**Build the core.** From an MSVC environment:
```sh
rustup target add x86_64-pc-windows-msvc
cargo build -p punktfunk-core --features quic --release --target x86_64-pc-windows-msvc
```
You get `punktfunk_core.dll` (+ import lib) and `punktfunk_core.lib` (static). Compile your C++ with
`/DPUNKTFUNK_FEATURE_QUIC` and add `include/` to the include path. The header is `extern "C"`-clean
for C++ (`__cplusplus` guards are in place).
**Video decode + present.** Feed the AUs to **Media Foundation** with a **DXVA2 / D3D12
hardware-accelerated decoder** (`IMFTransform` H.264/HEVC/AV1 decoder), or a D3D12 Video decode
(`ID3D12VideoDecoder`) if you want to own the DPB. Output NV12/P010 textures and present with your
D3D12 swapchain. For HDR, set the swapchain to `DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020` and pass
`punktfunk_connection_next_hdr_meta` straight into `IDXGISwapChain4::SetHDRMetaData` — the core's
`PunktfunkHdrMeta` is already in `DXGI_HDR_METADATA_HDR10` units.
**Audio.** `punktfunk_connection_next_audio_pcm` (f32) → **XAudio2** source voice, or **WASAPI**
shared-mode render. Request 6/8 channels at connect for surround.
**Input.** **GameInput** (`IGameInput::GetCurrentReading`) gives you gamepad state; diff it per frame
and emit `GAMEPAD_BUTTON`/`GAMEPAD_AXIS` events. Because a real Xbox pad drives this, connect with
`PUNKTFUNK_GAMEPAD_XBOXONE` for matching glyphs. Rumble comes **back** from the host — feed
`punktfunk_connection_next_rumble2` into `IGameInputDevice::SetRumbleState` (map `low`
low-frequency, `high`→high-frequency motors).
**Skeleton (C++):**
```cpp
auto* c = punktfunk_connect_ex7(host, 9777, 3840,2160,60,
PUNKTFUNK_COMPOSITOR_AUTO, PUNKTFUNK_GAMEPAD_XBOXONE, /*bitrate=*/60000,
PUNKTFUNK_VIDEO_CAP_10BIT | PUNKTFUNK_VIDEO_CAP_HDR, // 4K HDR
/*ch=*/6, PUNKTFUNK_CODEC_HEVC, PUNKTFUNK_CODEC_HEVC,
nullptr, hostFp, observed, cert, key, 8000);
uint8_t trc; punktfunk_connection_color_info(c, nullptr,&trc,nullptr,nullptr,nullptr);
bool hdr = (trc == 16 || trc == 18);
auto decoder = MakeMFHevcDecoder(d3d12Device, hdr /*P010*/);
// video thread
PunktfunkFrame f;
while (running) {
if (punktfunk_connection_next_au(c, &f, 20) != PUNKTFUNK_STATUS_OK) continue;
bool gap; punktfunk_connection_note_frame_index(c, f.frame_index, &gap);
decoder.Decode(f.data, f.len, f.pts_ns); // → NV12/P010 texture → D3D12 present
}
// feedback thread
uint16_t pad, lo, hi; uint32_t ttl;
while (punktfunk_connection_next_rumble2(c,&pad,&lo,&hi,&ttl, 100) == PUNKTFUNK_STATUS_OK)
SetRumble(pad, lo, hi, ttl);
// HDR: PunktfunkHdrMeta hm; next_hdr_meta(...) → swapChain4->SetHDRMetaData(HDR10, &hm-as-DXGI)
```
**Gotchas.** GDK console socket use is allowed but goes through the title's network stack — enable it
in your MicrosoftGame.config and test in the console sandbox. Retail devices need proper cert; keep
the DLL and header ABI versions locked (§2.5) in your CI.
---
## 14. Tizen (Samsung Smart TV)
**App model.** Use **Tizen Native** (C, EFL) — .NET/web apps can't get low-latency HW-decode feed
access cleanly. TVs are ARM (`aarch64` on modern panels, `armv7` on older). Build with the Tizen
Studio / GBS toolchain and its sysroot.
**Cross-compile the core.**
```sh
rustup target add aarch64-unknown-linux-gnu # or armv7-unknown-linux-gnueabihf
export CC_aarch64_unknown_linux_gnu=$TIZEN_TOOLCHAIN/bin/aarch64-tizen-linux-gnu-gcc
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=$CC_aarch64_unknown_linux_gnu
cargo build -p punktfunk-core --features quic --release --target aarch64-unknown-linux-gnu
```
Add the `.so` to your project's `lib/` and link with `-lpunktfunk_core`, compiling with
`-DPUNKTFUNK_FEATURE_QUIC`.
**Video decode + present.** Use **`capi-media-codec`** (`mediacodec_*`) — Tizen's hardware codec
binding over OpenMAX. Configure it from the resolved codec (`MEDIACODEC_H264`/`_HEVC`/AV1), wrap each
AU in a **`media_packet`** (`capi-media-tool`) and `mediacodec_process_input`; on the output callback
render the decoded packet to an **Evas GL** surface (or hand tunpacked frames to the TV video plane).
For HDR, drive the panel's HDR mode from `punktfunk_connection_color_info` + `next_hdr_meta`.
**Audio.** `punktfunk_connection_next_audio_pcm` (f32, 48 kHz) → **`capi-media-audio-io`**
(`audio_out_*`). Convert f32→s16 if you open the sink as PCM S16.
**Input.** TV remote and Bluetooth gamepads arrive as **Ecore** key events (`Ecore_Event_Key`).
Register key grabs (`elm_win_keygrab_set`) for the remote, translate D-pad/OK/back to
`KEY_DOWN/UP` or gamepad buttons, and map any HID gamepad to `GAMEPAD_AXIS`/`GAMEPAD_BUTTON`.
**Skeleton:**
```c
PunktfunkConnection *c = punktfunk_connect_ex7(host, 9777, 1920,1080,60,
PUNKTFUNK_COMPOSITOR_AUTO, PUNKTFUNK_GAMEPAD_XBOX360, 20000,
/*caps=*/0, /*ch=*/2, PUNKTFUNK_CODEC_HEVC | PUNKTFUNK_CODEC_H264,
PUNKTFUNK_CODEC_HEVC, NULL, host_fp, observed, cert, key, 8000);
uint8_t codec; punktfunk_connection_codec(c, &codec);
mediacodec_h mc; mediacodec_create(&mc);
mediacodec_set_codec(mc, codec==PUNKTFUNK_CODEC_HEVC?MEDIACODEC_HEVC:MEDIACODEC_H264,
MEDIACODEC_DECODER | MEDIACODEC_SUPPORT_TYPE_HW);
mediacodec_set_output_buffer_available_cb(mc, on_decoded /*→ Evas GL present*/, NULL);
mediacodec_prepare(mc);
// video thread
PunktfunkFrame f;
while (running) {
if (punktfunk_connection_next_au(c, &f, 20) != PUNKTFUNK_STATUS_OK) continue;
bool gap; punktfunk_connection_note_frame_index(c, f.frame_index, &gap);
media_packet_h pkt = wrap_au(f.data, f.len, f.pts_ns); // capi-media-tool
mediacodec_process_input(mc, pkt, 0);
uint64_t d; punktfunk_connection_frames_dropped(c, &d);
if (d > last_d) { punktfunk_connection_request_keyframe(c); last_d = d; } // throttle
}
// audio thread: next_audio_pcm → audio_out_write(...)
// Ecore key handler: fill PunktfunkInputEvent → punktfunk_connection_send_input(c, &ev)
```
**Gotchas.** Declare the `internet` and `network.get` privileges in `tizen-manifest.xml` or the QUIC
socket won't open. `mediacodec` prefers Annex-B start codes and periodic parameter sets — the
punktfunk IDR carries in-band parameter sets, but if your SoC decoder wants an explicit
codec-config/CSD, extract VPS/SPS/PPS from the first IDR and feed it before the stream. On a
mid-session mode change (`request_mode`), tear down and re-prepare the `mediacodec` from the new IDR.
---
## 15. Checklist for a new port
- [ ] Build the core with `--features quic`; compile your app with `-DPUNKTFUNK_FEATURE_QUIC`.
- [ ] `punktfunk_abi_version() == ABI_VERSION` at startup.
- [ ] Persist a generated identity; implement PIN pairing; pin the host fingerprint.
- [ ] Connect off the UI thread; **build the decoder from the *resolved* codec/color/chroma**, not
your request.
- [ ] One thread per plane; never two on the same plane; decode/copy borrowed buffers before the next
pull.
- [ ] `note_frame_index` every video frame; throttled `frames_dropped``request_keyframe` backstop.
- [ ] Rebuild the decoder on the IDR after any accepted `request_mode`.
- [ ] Set `flags = (w<<16)|h` on absolute pointer/touch events (nonzero!).
- [ ] `disconnect_quit` only on deliberate user quit; always `close` and stop plane threads on
teardown.
## 16. Reference
- **Header (the contract):** [`include/punktfunk_core.h`](../include/punktfunk_core.h)
- **Minimal C link + round-trip proof:** [`crates/punktfunk-core/tests/c/`](../crates/punktfunk-core/tests/c/)
- **Core crate README:** [`crates/punktfunk-core/README.md`](../crates/punktfunk-core/README.md)
- **A full reference client (Rust, same ABI surface):** `crates/pf-client-core/src/session.rs`