Two quality improvements:
Preset: ultrafast -> veryfast. Unlocks Main profile with CABAC
entropy coding, hexagonal motion search, 3 reference frames, and
adaptive quantization — typically 30-40% better quality at the same
bitrate. The desktop handles the extra encoding cost trivially
(150+ fps at 1080p).
Downscaling: the receiver now advertises its display resolution in
the signaling answer (display_width/display_height, 0 = unknown).
When the capture exceeds the display (e.g. 2256x1504 source on a
1920x1080 receiver), the sender scales down preserving aspect ratio
before encoding — the same sws_scale pass that already converts the
pixel format also handles the resolution change, so there is no
extra step. This concentrates the entire bitrate into pixels the
display actually shows (~2.7x more bits per visible pixel at
4000 kbps when going from 2256x1504 to 1620x1080).
The renderer caches the display size during window creation
(native monitor resolution in fullscreen/KMSDRM; window size
otherwise). The receiver includes it in every signaling answer; the
sender pipeline computes aspect-preserving, even-rounded scaled
dimensions when the display is smaller than the capture.
meson test 5/5 in both configurations, valgrind clean.
Loss recovery for the streaming path:
- PLI over signaling: the depacketizer now reports damaged frames
(DepacketizeResult) and the receiver asks the sender for a keyframe
(SessionPli, rate-limited to one per 500 ms). The sender keeps the
signaling channel open during the session and honors PLIs through
the new thread-safe SenderPipeline::request_keyframe(). Recovery
takes one frame time instead of waiting out the GOP.
- RtpJitterBuffer: reorders RTP packets by sequence number (16 packets
/ 60 ms) before the in-order depacketizer, so Wi-Fi reordering is
not misread as loss; in-order streams release immediately, and a
straggler older than the delivered sequence is discarded.
- Hardware H.264 decode probe: DecoderFactory tries h264_v4l2m2m (the
VideoCore path on the Pi) with an automatic software fallback and a
clear journal line for the chosen path; --swdecode opts out.
Validated: PLI end-to-end with a probe that drops a mid-keyframe
packet over real UDP (receiver logged the damaged frame and the PLI
arrived with the session id); hardware probe fails cleanly and falls
back on this desktop; jitter reordering covered by unit tests.
meson test 5/5 in both build configurations, valgrind clean.
Add mDNS/DNS-SD discovery and JSON session negotiation so two peers on
a LAN connect without hard-coded addresses.
- Discovery (Avahi threaded-poll client): the receiver announces
_screencast._tcp with its signaling port; senders browse and resolve
peers. Strict lock ordering (poll lock before state mutex) keeps the
callbacks deadlock-free; name collisions rename via
avahi_alternative_service_name.
- Signaling: one JSON object per newline-terminated TCP line. The
receiver hosts a server (port 5005) and answers session offers with
its RTP port; senders connect, offer, and stream to the negotiated
endpoint. WebSocket was deferred: no WS library is installed, the
skill permits plain TCP, and the wire format is transport-agnostic.
- Dual-stack transports: this machine resolves its own services over
IPv6, so getaddrinfo now runs AF_UNSPEC and listeners bind IPv6 with
IPV6_V6ONLY=0 (IPv4 fallback), covering UDP and TCP alike. The
signaling client shutdown now uses shutdown() so a reader blocked in
recv() cannot hang the join (a plain close() does not wake it).
- CLI: --discover lists receivers (deduped to one entry per host);
--send auto-disovers when exactly one receiver is found; --peer
targets a receiver directly; --signaling-port overrides the default.
Validation: meson test 5/5 (new signaling round-trip test), valgrind
clean. End-to-end over loopback: --discover finds the announced
receiver, a probe negotiated a session and streamed 60 frames over
both IPv4 and IPv6, and the receiver reported stream started. mDNS
resolution was verified against avahi-browse as an independent
reference.