The v0.31.3 CHANGELOG recorded this cut as a decision left open, on the same reasoning v0.31.0 used for 0.1.5: a plugin resolves `@punktfunk/host` from the registry, so types sitting in `sdk/` reach nobody until a version carries them. #374 added `PATCH /clients/{fingerprint}`, `RenameClient` and `PairedClient.label` to the management API and regenerated the client for them — so without this cut the route exists on every 0.31.3 host and no plugin can call it in a typed way. ONE FILE is the whole diff since sdk-v0.1.5: `sdk/src/gen/punktfunk.ts`. It is larger than the feature because regenerating it from the UNCHANGED committed spec already produced a ~700-line diff — the checked-in copy had drifted from its own pinned generator, and nothing in CI regenerates or verifies it (unlike api/openapi.json and include/punktfunk_core.h, which are both gated). #374 landed the clean regeneration rather than hand-patching generated code, and this cut publishes it. `SDK_VERSION` moves with `package.json`. It is a hand-maintained constant — the build sets `rootDir: "src"` so it cannot import the manifest, and the runner ships as one bundled `runner-cli.js` with no manifest beside it — and the runner compares it against the SDK installed in the plugins tree to decide whether to reinstall. Shipping 0.1.6 with the constant still reading 0.1.5 would publish the types and then never deliver them. `version.test.ts` gates exactly that, which is also what sdk-publish.yml's "Tag matches package version" step re-checks against the tag. GATES, all four steps sdk-publish.yml runs, in order and locally: `bun install --frozen-lockfile --ignore-scripts` clean, `bun run typecheck` clean, `bun test` 83 pass / 0 fail / 191 expect() calls across 12 files (the same 83 the 0.1.5 cut reported), `bun run build` clean. Nothing but the two version sites and the two release documents is touched — no dist/ or lockfile churn reached the tree. `@punktfunk/plugin-kit` is deliberately NOT re-cut: nothing under plugin-kit/ has moved since 0.4.4, which stays the registry's `latest`. Tag `sdk-v0.1.6` on the merge commit, alongside `v0.31.3`. The two version independently by design — sdk-publish.yml triggers on `sdk-v*` and the app's `v*` tags never republish the SDK — so the shared commit is a convenience, not a coupling.
18 KiB
Wire-compatible with 0.31.x — everything you have already paired keeps working, and you can update one side at a time. Nothing here changes how a host and a client agree on what to send each other, so an old client on a new host, or the other way round, streams exactly as it does today.
This is a fix release about streams that ended, froze, stuttered or never arrived while everything involved was doing something perfectly ordinary. Launching a Steam game that had shaders to process dropped the stream about ten seconds in, so people learned to launch everything twice. A fullscreen game that picks its own screen resolution mid-play froze the picture on a Windows host and ended the video a few seconds later with the sound still running. On an Android TV or a Fire Stick the app was quietly asking your host for a frame rate your television does not actually output, which is where the latency people had been working around by hand was coming from. And on a slower connection the very first thing a client does — a quick burst to measure what the link can carry — was big enough to choke the link it was measuring, delaying the picture by many seconds or losing it entirely. There is new work too: your Moonlight devices can be given names, and a 4K television or projector can now run the on-screen interface at a lower resolution to keep it smooth.
TL;DR
- A Steam game with shaders to process dropped the stream about ten seconds into launching it. You watched the "Processing Vulkan shaders" dialog, lost the stream, reconnected and launched again — and the second launch worked, which is why this looked like bad luck rather than a bug.
- Android TV and Fire Stick: the app negotiated a frame rate your TV does not output. Setting the refresh rate by hand was the known workaround; it is no longer needed, and the latency it was papering over is gone.
- A slow first picture, or none at all, on a constrained connection. The startup speed test was so large it could black-hole the very link it was measuring — one case took fourteen seconds to show video. It is now sized to the session, and if the test does swallow the opening frame the client asks for a new one instead of sitting on black.
- Windows: a game that changed your screen resolution mid-stream froze the picture and then ended the session. The sound carried on throughout, which is exactly what makes this look like a problem at the client's end.
- Fire TV: a DualSense had buttons that never reached the game, and its touchpad click and Mute button did nothing. Mute now mutes your microphone.
- New: name your Moonlight devices instead of a list of identical rows, and — on a 4K TV or projector — Reduce interface resolution for a smoother on-screen interface.
Before you update
- Steam Deck, and only if you installed the host from source: re-run your update after taking this release. A source install builds a patched compositor, and that build has been failing since mid-August because of a missing system package. The failure was silent — it reported success, and quietly dropped back to the system's own compositor, which is why HDR disappeared on boxes that had been streaming it minutes earlier. The missing package is added here, so the next build succeeds. Nothing to do on a packaged install.
New
- Give your Moonlight-paired devices names. This is not a display bug being fixed: every Moonlight-compatible client identifies itself with the same built-in name, so it says which app is connecting and nothing about which device. Until now that name was all the console could show, and someone who had paired a phone, a television and a handheld saw three rows reading identically. Each Moonlight row now has a pencil next to it — name it "Living room TV", and that is what the list says from then on, including when you are choosing which device to remove. Devices paired with Punktfunk's own apps already send a real name and are left alone. Names live on the host, so every browser you open the console in sees the same ones, and removing a device forgets its name.
- Reduce interface resolution, for a 4K television or projector. The on-screen interface is drawn at whatever resolution the panel hands it, and on a 4K set that is four times the work of 1080p on a chip built to decode video rather than to draw a moving interface — which is why the premium 4K boxes are the ones that feel sluggish, not the cheap 1080p sticks that never had the extra pixels. The new switch sits directly under Reduce motion, because it is the same kind of bargain: text goes a little softer, the interface gets smoother. It is off by default. It changes the interface only and does nothing to your stream — picture quality is still Resolution and Bitrate, which are separate settings and untouched by this.
Improved
- The on-screen interface got substantially cheaper to draw, on every device. Independently of the switch above, it was doing a surprising amount of work on every single frame whether or not anything had changed: re-measuring and re-laying-out every piece of text on screen sixty times a second, and allocating a full-screen scratch image to apply an effect that did nothing whenever the interface was sitting still. On a 4K panel that scratch image alone was larger than the memory budget the whole interface is allowed on a 2 GB box, so it was evicting real work in order to do nothing. Both are gone, and the result is pixel-for-pixel identical. The interface also now gets a scheduling priority just below the stream's, so a TV box cannot park it behind background work and leave it lagging your remote.
- When the interface is slow, the logs can now say so. It recorded which graphics version it had and how much memory it was allowed, and never what resolution it was drawing at or how long a frame took — so "it feels sluggish" could not be looked into from a log bundle at all. It now reports both.
Fixed
- Launching a Steam game dropped the stream while it was still starting, so you had to launch it twice. Reported on Rocket League: the stream showed the "Processing Vulkan shaders" dialog and then ended about ten seconds in, every time, with a second launch working fine. The host was doing this to itself. Steam does its preparation work for a game — processing shaders, most visibly — under the same marker it uses for the game itself, so a launch is a short chain of things that all look like your game, and only the last one is. The host accepted the first one, and from that moment it was no longer waiting for a game to start but watching for one to exit; when the preparation step finished a few seconds later, that was read as the game exiting and the session was closed. Two things change. The shader step is now recognised for what it is and never mistaken for a game. And anything else must be seen continuously for a few seconds before the host will believe it is your game — the rule it already applied to programs a launcher starts, now applied to what it finds by looking. The cost is a few seconds' delay before the host says a game is running; nothing about detecting a game exiting changes, so a game you quit still ends the session as promptly as before.
- Android TV and Fire Stick: the app asked your host for a frame rate the television does not output, and the latency went through the roof. People had already found the workaround — set the refresh rate by hand — without knowing what it was working around. The app pins the panel to its highest refresh rate while you are in the on-screen interface; that exists for phones whose systems otherwise cap apps at 60, and no television needs it. But when the stream started, the app read the panel's pinned rate rather than what the TV genuinely outputs over HDMI, negotiated the session at that — and then released the pin, because on a TV the video decoder is what should be driving the HDMI mode. The result was a 120-frame stream arriving at a 60 Hz output, by construction, on exactly the two kinds of device in the reports. The pin is no longer applied on a television at all. A TV that really can do 120 still gets it by choosing it. In the same chain: a TV that reports the fractional broadcast rates (59.94, 29.97, 23.976) had them cut down to 59, 29 and 23 — rates no display actually has — and they are rounded properly now.
- A slow first picture, or a black screen, on a constrained connection. Before any video, a client sends a short burst to work out how much the link can carry. That burst was a fixed, very large size on the reasoning that it should measure the link rather than itself — but the result is capped afterwards to what the session could plausibly use, so everything above that was measured and immediately thrown away. What it bought was nothing; what it cost was a flooded link. On a constrained Wi-Fi connection it could black-hole outright: one measured case spent six seconds timing out and took fourteen seconds to show any video, and the same shape came in from a Fire TV Stick 4K Max. The burst is now sized from what the session can actually use, which can never come out lower than what is needed to prove the ceiling. And the second half of the black screen is closed too: if the burst takes the opening frame down with it, the client now asks for another one instead of waiting for some unrelated recovery to happen along.
- Windows: a game that changed your screen resolution during a stream froze the picture and then ended the session. Reported from a 4K session where the game switched the display to 1080p while it ran. A fullscreen game is allowed to choose its own resolution, and your host followed it — but the part of the host that compresses the picture cannot change size while it is running, and it was being rebuilt over and over at the size the game had already left. After about three seconds of that, the video ended while the sound kept playing, so you were left with a frozen picture, working audio and no option but to reconnect. The host now rebuilds at the size the game actually chose and tells your client about the new one, exactly as it does when you change the resolution from the client. The same fix covers a game that switches HDR on or off mid-play, which failed in the same way. If a rebuild does not take the first time — a display that has just changed mode is often still settling — it is retried for the same few seconds rather than the session being given up on immediately.
- The same resolution change in a Moonlight-compatible session ended it too, and now does not. One caveat worth knowing, because it is a real trade: the protocol Moonlight speaks has no way for a host to announce a resolution change once a stream is running, so your client is not told. Most clients notice from the picture itself and adjust; a strict one — Media Foundation on Xbox is the known example — may stall instead and need reconnecting. That is the same bargain these sessions already take whenever the host's picture and the client's request disagree, and it is strictly better than what it replaces, which was every such stream ending.
- Moonlight-compatible sessions stuttered at high frame rates, and the host was doing it to itself. When a client loses its place in the video it asks the host for a complete picture to start again from, and the host is supposed to ignore repeat requests that arrive too quickly. The gap it waited for was measured in frames rather than in time, which at 120 frames a second is about a sixtieth of a second — far shorter than the time a client needs to ask, receive and decode — so the requests never looked like repeats and nearly all of them were honoured. One field session recorded 1,118 such requests in 91 seconds and honoured 1,115: a complete picture roughly every tenth frame, each one large enough to saturate the connection, causing the loss that prompted the next request. It reads as heavy stutter while every latency figure stays flat, because frames are being lost rather than delayed. It also looked like a codec fault, because the same session's H.264 stream — encoded by a different part of the host — asked twice in the whole session and was completely clean. The host now waits a fixed tenth of a second before honouring another request. The field case was a 120-frame session, but the old window was too short at 60 as well, so this is not only a fix for high-refresh displays.
- Fire TV: a DualSense had buttons that never reached the game, and its touchpad click and Mute button did nothing. Three separate faults on one controller, all reported together over Bluetooth. Some of its buttons were being labelled by the system as coming from a keyboard rather than a controller, and the app was dropping them on that basis — it now trusts what the device is rather than the system's per-press guess, and only ever for keycodes that are genuinely controller buttons, so a remote's Back button and a keyboard's arrow keys are untouched. The touchpad click and the Mute button had nowhere to go at all and were simply discarded; both now travel to your game. And Mute genuinely mutes your microphone, once per press — held down, it no longer flickers the microphone on and off — on controllers that actually have the button.
- Android: the app could crash outright while playing, most often on an NVIDIA Shield. The system call the app used to pick up the newest video frame hands back a resource it has already given away when more than one frame arrives at once, which the system's own safety check then catches by killing the app. It is a bug in Android that is still unfixed upstream, so the app stops using that call and picks the newest frame itself.
- Linux: after disconnecting, the box's own screen could stay black. Reported on both Bazzite and Nobara. The hand-back at the end of a session asked the system to bring the desktop session back and then walked away the moment the request was accepted — but "the request was accepted" and "the screen is showing something" are different questions, and nothing had ever asked the second one, so every way of ending up dark looked identical to success. It now checks: if the box is still dark twenty-five seconds after the hand-back, it works through a ladder of increasingly firm remedies, each of which was measured on real machines of both families, and if it still cannot fix it, it says exactly what a human should run. This is not a guess at one trigger — the specific fault people reported could not be reproduced. It closes the gap that lets any trigger end as a dark panel.
- Windows: duplicate "Punktfunk Speakers" and "Punktfunk Microphone" devices piled up in your sound settings. Creating one of these is two steps, and a host that died between them left behind a fully working device with no ownership mark on it. Nothing ever recognised that afterwards, so the next start created a second one and the stray outlived it — and because uninstalling also went by the ownership mark, uninstalling did not remove it either. One field machine showed exactly this. The host now recognises a stray from a previous run and adopts it instead of creating another, and uninstalling sweeps up ones already on the machine. Separately, on machines where the usual naming route is blocked, the microphone's name was being written to a location that only exists for speakers, so it silently kept the driver's default name.
- Steam Deck: HDR stopped working after updating to 0.31.2 on a source install. Two faults with one symptom. The build of the patched compositor had been failing since mid-August on a missing system package — added here — and the failure path then went on to unlink the compositor that was already installed and working. A build that never produced anything replaced nothing, so removing the perfectly good previous one meant the host fell back to the system's own compositor and fixed the session at 8-bit, which cannot be taken back once a session has started. A failed build now leaves the working installation alone.
Thanks
Almost everything above came from someone reporting exactly what they saw and on what — the game they launched and the dialog it hung on, the two 4K boxes that felt slow, the make of controller and which button did nothing, the card and the frame rate, the fourteen seconds before a picture appeared. Two entries are worth calling out for a different reason. The Linux black-screen fix ships without a reproduction: five scenarios were run across both distributions on real machines, the mechanism first proposed was disproved, and rather than guess, the fix closes the gap that lets any cause end the same way. And the DualSense work was re-implemented from a contributor's diagnosis rather than merged as sent — all three faults were real and correctly identified, but each proposed fix reached further than the hardware that needed it. The diagnosis was the hard part and it was right. Thank you.
For developers
Protocol, ABI, driver and embedder detail — including the version table — is in CHANGELOG.md.
The short version: nothing versioned moves. The streaming protocol, the embedding interface, the driver protocol, the gamepad channel and the add-on contract are exactly where 0.31.2 left them — include/punktfunk_core.h has no diff at all against the v0.31.2 tag — and no header, package or plugin needs rebuilding, re-pairing or re-publishing in any direction. The one surface that grows is the management API, additively: a PATCH /api/v1/clients/{fingerprint} route sets or clears a paired client's label, and GET /clients gains a label field alongside the existing certificate subject. Nothing existing changed shape, so a consumer that ignores both is unaffected. The TypeScript SDK is re-cut as @punktfunk/host 0.1.6 so an add-on can actually reach the generated types for that route; the add-on toolkit is unchanged. One dependency moves for a security advisory (h2, lockfile-only), and one behaviour worth knowing about if you integrate: the host now reports a game as running a few seconds later than it used to when it identifies that game by scanning processes rather than by a plugin's own report.