- Move WebRTC send to dedicated wl-webrtc-webrtc thread (was inline in main loop)
- Reduce frame_rx 16→1, input_tx 2→1 (drop-on-full), webrtc_tx 32→2
- Recv_timeout 10ms→2ms to reduce pipeline latency
- Fix sent_gap_p95 stats bug: compute gap at actual send time in WebRTC
thread instead of batch-draining at snapshot time (was always 0.0ms)
- High profile via AVCodecContext.profile, veryfast preset, 5x bitrate
- Stats drain via sent_gap channel with record_send_from_thread()
- Shutdown: drop input_tx → join encode → drop webrtc_tx → join webrtc
Add lightweight per-second pipeline statistics for stutter diagnosis:
- --stats CLI flag enables structured stats logging
- PipelineStats tracks capture/encode/send timing with p95/pmax
- FrameTimings records import/scale/transfer/sws/encode per-frame
- StatsSnapshot produces one structured log line per second
The 500 error response previously included the raw error message {e}
in the body, potentially leaking internal implementation details (SDP
parse errors, ICE candidate info) to clients.
The detailed error is already logged server-side via tracing::error!,
so the response body is now a fixed generic string with a proper
HTTP/1.1 status line.
poll_rtc() always returned Ok(false), preventing WebRtcState from
clearing self.inner on disconnect. This leaked the UDP socket, Rtc
instance, and 65KB buffer permanently if the client never reconnected.
Closes#10
shutdown() calls enc.flush() → drain_encoder() → tx.send() on a
crossbeam bounded(32) channel. If the channel is full and the
receiver (webrtc_rx) is alive but not being drained, send() blocks
forever — a self-deadlock since both ends belong to the same struct.
Two-layer fix:
- avhw.rs: replace tx.send() with tx.try_send(); handle Full (drop
frame) and Disconnected (set flag) separately.
- state_portal.rs: drop webrtc_rx before flushing in shutdown() so
try_send returns Disconnected immediately.
Regression tests added for the channel semantics.
- Replace 'let _ = tx.send()' with proper error handling: log warning,
set webrtc_disconnected flag, and break drain loop on SendError
- Add Arc<AtomicBool> webrtc_paused shared between State/StatePortal
and SwEncState, synced from wrtc.is_connected() in poll_webrtc()
- Skip encoding in encode_filtered_frame() when paused or disconnected
- Drain and discard stale channel frames on disconnect
- Resume encoding automatically on WebRTC reconnection
- Add src/webrtc.rs: HTTP signaling server + str0m Sans-IO WebRTC transport
with H.264 Annex-B → RTP packetization and key-frame request handling
- avhw: introduce FrameOutput enum (Muxer | Channel) so SwEncState can
output to either MP4 muxer or crossbeam channel for WebRTC
- cap_portal: support portal session restore tokens (PersistMode::ExplicitlyRevoked)
to skip re-authorization dialog; add --no-persist flag to force fresh dialog
- args: make --output optional when --port is used for WebRTC mode
- state_portal: integrate WebRTC pipeline (encoder channel → RTP forwarding)
with shorter GOP for WebRTC (fps/2, min 10)
- main: redirect tracing to stderr; validate --output or --port required
- Add dependencies: str0m 0.20, serde_json 1, dirs 6
The old FpsLimit compared timestamps between CONSECUTIVE frames.
When PipeWire delivers at 60fps (16ms intervals) and target is 30fps
(33ms min_interval), the gap between consecutive frames is always
16ms < 33ms, so EVERY frame was rejected after the first.
Fix: track last_output_time and compare against that instead of the
previous frame's timestamp. Now frames pass when enough time has
elapsed since the last OUTPUT, not since the last INPUT.
Also adds PipeWire process callback counter logging and frame
diagnostic STATS in state_portal.rs for debugging.
ashpd caches zbus::Connection in a global OnceLock. When check_portal_available()
created a Screencast proxy, the connection was cached there. When the function
returned and its tokio Runtime dropped, the cached connection became dead.
Subsequent setup_portal() calls reused this dead connection and hung forever.
Fix: replace ashpd Screencast proxy with direct zbus D-Bus interface check,
which does not touch the ashpd global connection cache.
Add examples/test_portal.rs for minimal Portal ScreenCast testing.
- Rename PwEvent to PwCtrlEvent, separate frame data into its own channel
- Add null chunk check to prevent crash on malformed PipeWire buffer
- Remove redundant inline comments and signal handlers
- Use try_send for error events to avoid blocking on full channel
- Add Drop impl for StatePortal to flush encoder on drop (bug #2)
- Use enc.take() in shutdown() to prevent double-flush of write_trailer
- Null out data[0] after Box::from_raw recovery to avoid dangling pointer
- Extract compute_pts() for testable PTS calculation
- Add 8 tests: PTS calculation, DRM device resolution, descriptor building
BUG-2 (HIGH): SHM Buffer event caused permanent hang
In the ZwlrScreencopyFrameV1 dispatcher, receiving a SHM Buffer event
left in_flight_surface stuck at AllocQueued forever, preventing
queue_alloc_frame() from requesting new frames.
Fix: treat Buffer as a metadata offer (v3 protocol), wait for
BufferDone to decide failure, and add AllocQueued state guard to
LinuxDmabuf handler.
BUG-3 (MEDIUM): Portal backend picked wrong GPU on multi-GPU systems
state_portal.rs hardcoded /dev/dri/renderD128 then renderD129, which
selects the wrong GPU when PipeWire uses a different device.
Fix: extract find_drm_render_nodes() as shared utility; defer DRM
device selection to first PipeWire frame; test each candidate with
av_hwframe_transfer_data to find the GPU that can actually import
the DMA-BUF frame.
BUG-4 (LOW): VAAPI device context created twice unnecessarily
try_finalize_output() created an AvHwDevCtx stored in EverythingButFmt,
but negotiate_format() discarded it (_hw_device_ctx) and EncState::new
created a new one.
Fix: thread the existing hw_device_ctx through negotiate_format() and
create_encoder() to EncState::new() which reuses it when provided.
pw::init() is guarded by an internal OnceCell (process-global one-shot).
pw::deinit() is unsafe and requires 'only called once per process lifetime
after all PipeWire use has permanently stopped'. Since CapPortal can be
created/destroyed multiple times, calling deinit() from a function-local
scope would prevent re-initialization (OnceCell already consumed) and
violate the unsafe contract.
The 5 early-return error paths in pipewire_thread() that previously
leaked global state are now consistent with the success path — neither
calls pw::deinit(). Process exit reclaims global PipeWire state.
Add comprehensive Chinese documentation comments to cap_portal,
main, and state_portal modules covering architecture, lifecycle,
and data flow for each component.
Add check_portal_available() and check_screencopy_available() to
probe each backend independently before committing. This enables
smarter fallback logic and better diagnostics when no backend is
found. Includes Chinese documentation comments.
PipeWire spa_meta_header.pts is CLOCK_MONOTONIC in nanoseconds, but the
encoder expects frame-number units (time_base = 1/fps). The raw nanosecond
value was assigned directly to AVFrame.pts, causing the encoder/muxer to
interpret timestamps as billions of frames, producing corrupted duration
metadata and broken rate control.
Fix: record the first frame's PTS as a nanosecond base, compute elapsed
nanoseconds for each subsequent frame, then convert to frame numbers via
elapsed_ns * fps / 1_000_000_000. Using elapsed time avoids i64 overflow
on absolute timestamps (~10^18 ns).
Matches the WLR path pattern (state.rs:525-527) which converts microseconds
to frame numbers for the same encoder.
PipeWire .process callback called frame_tx.send() on a bounded(3)
channel. If the encoder stalled, this blocked the PipeWire data loop,
delaying buffer recycling and potentially causing XRUNs.
Replace with try_send + AtomicU64 drop counter. Frames are silently
dropped when the channel is full (preferred for screen capture: latest
frame wins). A warning is logged every 30 dropped frames.
Fixes#2 from BUGS.md.
The detached helper thread that called pw_main_loop_quit() through a raw
pointer cast to usize could outlive the mainloop if run() returned on its
own (event loop error, panic in callback, etc.), causing use-after-free.
Replace with an eventfd registered on the PipeWire loop via add_io(). The
shutdown callback runs on the loop thread during mainloop.run(), where the
mainloop is guaranteed alive. Drop order (reverse declaration) ensures the
IO source is unregistered before mainloop is destroyed.
Fixes: #1 (Critical UAF)
Add a second capture backend for compositors without wlr-screencopy
(KWin, GNOME, etc.) using the xdg-desktop-portal ScreenCast interface
and PipeWire DMA-BUF streaming.
New files:
- src/backend_detect.rs: auto-detect wlr-screencopy vs portal backend
- src/cap_portal.rs: Portal session setup + PipeWire DMA-BUF thread
- src/state_portal.rs: StatePortal encoder pipeline (DMA-BUF → VAAPI)
Changes:
- Cargo.toml: add ashpd 0.13, tokio 1, pipewire 0.9, libspa 0.9,
crossbeam-channel 0.5
- src/args.rs: add --backend CLI flag
- src/avhw.rs: extract create_encoder() from inline State code
- src/main.rs: route to portal or wlr-screencopy based on backend
- src/state.rs: fix params.destroy() on dup failure, cleanup
in_flight_surface on copy fail, use create_encoder()
- tests/integration_test.rs: add --backend flag tests
scale_vaapi defaults to the input sw_format (RGBZ) when no output format
is specified. h264_vaapi encoder only supports NV12/YUV formats.
Adding format=nv12 ensures the filter outputs the correct color format
for hardware encoding.
- registry_queue_init consumes registry events during its internal
roundtrip without forwarding them to Dispatch<WlRegistry>. Added
bind_initial_globals() to manually iterate GlobalList and bind all
initial globals (wl_output, xdg_output_manager, dmabuf, screencopy,
wlr_output_manager) at State::new time.
- Fix av_freep segfault in build_filter_graph: av_buffersrc_parameters_alloc
returns a plain pointer, use av_free instead of av_freep (which expects
pointer-to-pointer).
- Fix filter graph format negotiation: remove software format filter that
broke scale_vaapi hardware pipeline. Chain is now src -> scale -> sink.
- Downgrade repeat_pps error to warning (not available in FFmpeg 6.x).