Picture quality

Skip this for A, B and C: nothing is encoded in those. It is only for D and E.

The picture the visitor sees is set by a quality ceiling, not the bitrate. Two keys control this in Config/DefaultGame.ini:

[/Script/PixelStreaming2Settings.PixelStreaming2PluginSettings]
EncoderMinQuality=10
EncoderMaxQuality=60

They survive packaging. In a packaged build, the same file sits at StreamingDemo/Config/DefaultGame.ini beside the .exe, letting you change the number and restart Unreal without rebuilding. Only EncoderMaxQuality is worth touching.

The names read backwards, which catches everyone once. Unreal names both ends for quality, but the encoder works in QP, which runs the other way (QP = trunc((1 − Quality/100) × 51)). Therefore, *Max*Quality — the ceiling on how good the picture may get — is the bandwidth lever, while *Min*Quality is the safety net.

What we found, which is the useful part

The encoder never asks for the whole bitrate budget. A fixed camera pointed at a talking head is cheap to compress. Consequently, the encoder wants to be finer than the ceiling on nearly every frame; only the ceiling stops it. Measured over 12 minutes and 52 samples, the encoder sat exactly on the ceiling in every single one. Output remained 3–6 Mbit/s below the bitrate cap, which never bound.

Therefore, raising the bitrate cap changes nothing. Lowering the ceiling is what buys you bandwidth. Roughly, for portrait 720×1280 at 60 fps:

EncoderMaxQuality bitrate

90

~32 Mbit/s — pointless, nobody can see the difference

70

10.1 Mbit/s — measured

60

~5.7 Mbit/s — what we ship

55

~4.5 Mbit/s

Only the 70 row is measured; the others are projected from it. Read your own number from the log rather than trusting the table. The top of the scale is exponentially expensive — 100 means mathematically lossless — so "just uncap it" is not an option.

Leave EncoderMinQuality=10 alone. It lets the picture get ugly only as a last resort when the network collapses. Raising it takes that licence away, forcing the stream to drop frames instead of softening.

Reading the log

Every five seconds, for the whole session:

[stream-quality] settled at quality 60 (QP 20) over 300 frames | bounds 10-60 [CEILING BINDING] | est 15.0 Mbps
  • settled at quality N — where the encoder actually landed, on the same 0–100 scale as the ini.

  • CEILING BINDING — your ceiling is doing something. If it never appears, the number is inert.

  • bounds 10-60 should match your ini exactly. If not, see the trap below.

  • est is the bitrate cap, not a measurement. It cannot exceed what the launcher allows, telling you nothing about the viewer’s connection.

One trap, only if you build your own frontend

Older PixelStreaming frontend packages echo the two bounds back to Unreal through a legacy conversion that does not round-trip, causing both to drift looser on every viewer connect: 10-60 becomes 12-61, then 14-63. A kiosk reaches the end of that within its first half hour. The floor half is the harmful one, for the reason above. Fixed in ours, and upstream.

If you ship your own frontend, connect and disconnect four times in one Unreal session and check that every [stream-quality] line still reads your numbers — a single connect looks nearly right and hides the whole problem.

Two things that are deliberately not knobs

  • Frame rate. Fixed at 60. We investigated adaptive rates and found that every visible stutter came from the rate changing, not from it being low. Compression absorbs bandwidth instead, matching industry practice.

  • The bitrate cap (-PixelStreamingWebRTCMaxBitrate, 15 Mbit/s in our launchers). Headroom worth keeping, but nothing has ever reached it.

If you also pinned the encoder bitrate for the memory leak, the two live together happily and do different jobs: the pin stops the reconfiguration that leaks, while this ceiling decides the real bitrate, which sits well below the pin. So tune the picture here and leave the pin alone — lowering it below what the ceiling asks for just overrides the ceiling, and you end up tuning one thing from two places.