The presets now include a frame rate cap alongside CRF and max
bitrate, reflecting three lessons learned since the CRF fix:
- Frame rate is the biggest bandwidth lever for desktop content:
15-25 fps is perfectly smooth for screencasting and cuts bandwidth
2-4x vs. uncapped monitor rate.
- CRF values are now actually applied (the global_quality bug is
fixed), so the ranges are meaningful.
- The 'Low bandwidth' preset targets 15 fps + CRF 26 + 3000 kbps,
which fits even the Pi's Wi-Fi hotspot; 'Maximum' uncaps the frame
rate for the smoothest possible output on a good link.
Low bandwidth CRF 26, 3000 kbps, 15 fps
Balanced CRF 22, 5000 kbps, 20 fps
Sharp CRF 18, 8000 kbps, 25 fps
Very sharp CRF 16, 12000 kbps, 30 fps
Maximum CRF 14, 20000 kbps, uncapped
A frame rate spinner (5-60, 0 = uncapped) sits alongside the
bitrate slider for independent fine-tuning.
Three changes to make the stream survive constrained links:
Adaptive quality: the sender pipeline now tracks the PLI rate from
the receiver. Every 5 seconds it evaluates: >0.5 PLI/s means the
link is saturated (the receiver is dropping frames), so the CRF
increases by 2 (lower quality, fewer bits) and the encoder restarts
with a keyframe. <0.1 PLI/s means the link is stable, so the CRF
decreases by 1 (better quality) and the encoder probes upward.
Clamped to [user CRF, user CRF + 10] so quality never degrades
below what the link can handle, and never exceeds what the user
asked for. The adaptation is logged to stderr for visibility.
Frame rate capping (--fps N): throttles the capture loop to N
frames per second (0 = no cap; monitor rate). At 15fps instead of
60fps, the bandwidth requirement drops 4x at the same quality
level. Desktop content is still smooth at 15-20fps.
Tighter VBV: one frame period of buffer instead of two. A two-frame
buffer lets a keyframe spike to twice the target rate in one burst,
which overflows any constrained hop (Wi-Fi hotspot, slow switch)
and cascades into PLI storms. One frame period keeps bursts
within what the link can absorb in real time.
Replaces the raw bitrate slider with a preset dropdown that maps
directly to common CLI invocations:
Low bandwidth CRF 28, max 2000 kbps (screencast --send)
Standard CRF 22, max 4000 kbps (screencast --send)
Sharp CRF 18, max 8000 kbps (--crf 18 --bitrate 8000)
Very sharp CRF 16, max 12000 kbps (--crf 16 --bitrate 12000)
Maximum CRF 14, max 20000 kbps (--crf 14 --bitrate 20000)
The bitrate slider remains for fine-tuning the cap independently of
the preset's quality target (CRF). Selecting a preset sets both; the
label shows the current values.
Four encoder quality improvements, all sender-side:
- CRF rate control (default 22, --crf to override): targets a
constant visual quality level instead of a fixed bitrate. Static
desktop content uses 300-800 kbps (vs. forced 4000+), and the saved
bits go to sharp text and clean motion when they appear. The VBV
max rate (the --bitrate value, now a cap rather than a target)
bounds bursts so the receiver's UDP buffers stay safe. Round-trip
test bitrate dropped from 1390 kb/s to 47 kb/s on synthetic frames
— the encoder uses only what it needs.
- 5-second GOP (was 1 second): 80% fewer keyframe bits freed for
detail frames. Screen content changes incrementally, not
wholesale; PLI feedback recovers from loss in one frame time
regardless of GOP length.
- faster preset (was veryfast): better sub-pixel estimation and
RDO on more decisions. The desktop handles it trivially at 1080p.
- Screen-content x264 tuning: aq-mode=2 (auto-variance AQ moves
bits away from flat areas toward text edges) and psy-rd=1.5
(preserves texture sharpness).
Combined with the earlier veryfast upgrade and sender-side
downscaling, this is roughly 2x the perceived quality at the same
average bandwidth compared to the original ultrafast ABR encoder.
meson test 5/5 in both configurations, valgrind clean.
The sc_app_core static library was always compiled with SC_HAS_SENDER=1,
pulling CaptureFactory into receiver-only builds where the capture
backend does not exist. Three fixes:
- sc_app_core's cpp_args and capture dependency now follow the sender
meson option, so pipelines.cpp and sender_session.cpp compile their
sender code out on receiver-only targets.
- sender_session.cpp is fully guarded by SC_HAS_SENDER; its body is
sender pipeline orchestration and has no business in a receiver.
- address_preference moved from a SenderSession static to a free inline
function in sender_session.h — run_discover (available in every
build) uses it for address sorting.
Also: the root meson.build now errors early if gui=true without
sender=true (the GUI is a sender panel).
Verified: meson test 5/5 in both configurations; the receiver-only
binary refuses --send with a clear message; the full build's sender,
GUI, and tests are unchanged.
A gtkmm-4.0 control panel (behind -Dgui=true, default off): refresh
shows discovered receivers (grouped and preference-sorted), a bitrate
scale, and start/stop that runs the whole session on a worker thread
so the interactive portal picker never blocks the UI. The CLI and the
GUI now share the new sc_app_core static library holding the
pipelines, session orchestration (negotiation + PLI feedback), and a
state store.
The sender pipeline publishes its state to
$XDG_RUNTIME_DIR/screencast/sender.json (session id, receiver,
bitrate, pid, start time; stale files detected by pid liveness) and
persists the last session for one-click restarts. The new
'screencast waybar' subcommand prints a waybar module line and its
--toggle flag stops a running sender gracefully or spawns a detached
restart of the last receiver.
Waybar on the dev machine is wired: custom/screencast module with
click-to-toggle and right-click panel, plus styles, with a timestamped
backup of both config files. Both binaries are installed to
/usr/local/bin.
Validated: waybar output (idle and streaming states with a synthetic
state file), GUI launches on the desktop (window observed via
hyprctl), meson test 5/5 in both build configurations, formatting
clean.