Commit Graph

12 Commits

Author SHA1 Message Date
fegger 9a24933203 fix(android): pass PROTOCOL_DNS_SD so mDNS registration works
Android 16 added NsdManager.checkProtocol(): registerService with protocol
0 threw IllegalArgumentException: Unsupported protocol — silently, since
the old code swallowed the exception into a dead reflection fallback and
start() overwrote the failure status with the Listening line. mDNS never
actually advertised (the Phase 8 session streamed via --peer, masking it).

Also advertise after the listening status so a registration failure stays
visible, and log the exception and the registration success.

Validated live on the Fairphone 6: the desktop --discover lists the phone
and a full --send session decodes (in-band SPS 320x240 -> 2496x1040) and
renders.
2026-09-10 12:35:00 +02:00
fegger bbe4f21a3b docs: document the Android receiver app (Phase 8)
README: phone-as-second-screen section (build, install, sender usage,
multi-monitor caveat). RUNBOOK: Android build/test procedure, device-test
commands, and the API-36 platform quirks found during validation.
PHASES: Phase 8 milestones, all validated. MEMORY: current phase state
and the hard-won Android/portal lessons.
2026-09-10 11:38:16 +02:00
fegger 5c39662cc2 feat(gui): add a GTK sender panel and a waybar widget
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.
2026-09-08 17:14:44 +02:00
fegger 74b3f04082 docs: record Phase 7 resilience state and re-scope 2026-09-08 16:54:09 +02:00
fegger 013c2519dc docs: record real two-machine validation of Phase 6 on the Pi
The full chain — mDNS discovery, signaling negotiation, RTP over
Wi-Fi, software H.264 decode, and fullscreen KMSDRM letterboxed
rendering — is confirmed working on a real two-machine setup
(desktop -> Raspberry Pi Zero 2 W headless receiver). The probe
gradient appeared on the Pi's display.
2026-09-07 14:09:30 +02:00
fegger 7be8d59d07 feat(network): implement Phase 6 LAN discovery and session signaling
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.
2026-09-07 12:16:34 +02:00
fegger d2736b8a94 fix(render): keep the whole SDL lifecycle on the render thread
The receiver window never appeared on Wayland. Two causes:

- The window was created on the main thread while event pumping and
  presenting ran on the render thread. SDL's Wayland backend requires a
  window's creation, event processing, drawing, and destruction to
  happen on one thread; a cross-thread surface simply never maps, with
  no error reported. The renderer now lives entirely on the render
  thread, with a condition-variable handshake so ReceiverPipeline::
  start() still reports renderer failures and timeouts.
- On Wayland a window is invisible until the first render commit, so
  even a healthy receiver showed nothing while waiting for a stream.
  The renderer presents one blank frame at init: the window is visible
  immediately, black until video arrives.

Also log 'stream started (WxH)' when the first frame decodes and the
first rendering failure, which is what made the remaining debugging
observable.

Validated headlessly end-to-end: a synthetic solid-color RTP feed drove
the real receiver over localhost UDP; the window mapped, reported
'stream started (320x240)', and a grim screenshot of the window region
showed the fed color (V=198, SATAVG=74 during the red feed). Phase 5
marked complete in docs/PHASES.md; current phase is now Phase 6.
2026-09-07 11:17:35 +02:00
fegger b5e8d7174c feat(network): implement Phase 4 RTP framing with FU-A fragmentation
Add the sc_network library: RFC 3550 RtpHeader/RtpPacket serialize and
parse (the receiver tolerates CSRC lists, extension headers, and
padding by skipping/stripping them) and RFC 6184 H.264 payloading via
H264Packetizer/H264Depacketizer.

The packetizer splits Annex-B frames into NAL units (3- and 4-byte
start codes), emitting single-NAL packets or FU-A fragments within the
configured MTU, with the marker bit closing each frame and randomized
SSRC/sequence by default. The depacketizer reassembles access units
with 3-byte start codes, so both start-code widths round-trip
byte-exactly; frames damaged by sequence gaps or missing fragments
are dropped until the Phase 7 loss-recovery work.

test_rtp covers header and packet round-trips, malformed-input
rejections, splitter behavior, FU-A chunk bounds, full packetize ->
depacketize round-trip, gap dropping, marker-only frame separation,
sequence wrap, and empty inputs. meson test 3/3, valgrind clean.
2026-09-07 10:48:17 +02:00
fegger ce52f64e52 feat(capture): implement Phase 3 PipeWire/portal desktop capture
Implement the xdg-desktop-portal ScreenCast backend via libportal: a
blocking portal handshake (interactive source picker), a PipeWire stream
on the portal's node enumerating BGRx/BGRA/RGBx/RGBA, and a latest-frame
slot handing frames to next_frame(). stop() is thread-safe; teardown
follows the order PipeWire requires. All proxy operations run under the
thread-loop lock to satisfy the protocol extension context checks
('impl_ext_end_proxy called from wrong context' otherwise).

The encoder now accepts padded strides for packed RGB inputs (real
PipeWire row pitches) and maps the new PixelFormat::Bgrx to
AV_PIX_FMT_BGRA.

Add tools/capture_smoke: a manual smoke tool (interactive, not in
meson test) that captures N frames, encodes them, and writes a
self-contained Annex-B elementary stream with prepended SPS/PPS.

Validated manually on Wayland/Hyprland: 2256x1504 H.264 elementary
stream, ffprobe clean. Phase 3 marked complete in docs/PHASES.md.
2026-09-07 10:37:14 +02:00
fegger 71218b1b1f feat(codec): implement software H.264 encode/decode round-trip
Add FFmpeg-based encoder/decoder with SPS/PPS extradata, Annex-B output

normalization, low-latency libx264 settings, and a round-trip unit test.

Includes review hardening: cached SwsContext, bitrate-only rate control,

std::byte/uin8_t cast helpers, and richer test assertions.
2026-09-07 10:07:50 +02:00
fegger 975e3b2974 Complete Phase 1: add clock utility, unit test, build targets, and docs 2026-08-28 21:57:43 +02:00
fegger 742611b841 Scaffold C++20 screencast project with Meson, agent workflow, and phase plan 2026-08-28 21:54:32 +02:00