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 ( |
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-60should match your ini exactly. If not, see the trap below. -
estis 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.