dailz ad28af6ff3 fix(webrtc): switch encoder time_base to 90kHz to stop RTP time inflation (closes #25)
Root cause:

After #24 fixed PTS propagation, browser jitter buffer still accumulated
to 10+ seconds during active mouse movement. User reported: stop moving
mouse, client continues showing motion for ~10 seconds.

The encoder time_base was 1/fps (33ms granularity at 30fps). When KWin
delivers frames at 60fps (16.7ms apart), compute_capture_pts integer
math mapped multiple captures to the same tick. The monotonicity guard
then bumped them to sequential ticks (0, 1, 2, 3, ...).

Result: 60 captures in 1 real second produced 60 sequential RTP
timestamps spanning 60 * 33ms = 1.98 seconds of RTP time. Browser
played at RTP rate (half real speed), buffer accumulated.

Math verification from test8 log:
- 2067 frames * 3000 RTP jumps = 68s of active RTP time
- 61 frames * 54000 RTP jumps = 37s of static RTP time
- Total RTP time 105s vs real time 98.6s (7% inflation in short session;
  long active sessions amplify to 50%+ inflation matching user-reported
  10-second trailing).

Fix (per Oracle round review):

Change WebRTC encoder time_base from 1/fps to 1/90000 (90kHz). This
matches the RTP video clock directly, providing 11us PTS granularity.
Captures 16.7ms apart now produce distinct ticks (~1500 each), no
quantization, RTP timestamps accurately reflect real time.

Oracle-required revisions incorporated:

1. **set_frame_rate alongside time_base** — libx264 infers fps from
   time_base when not explicit. With 1/90000 time_base and no explicit
   framerate, x264 would assume ~90000fps and VBV rate control would
   break. Setting framerate=fps/1 preserves real frame semantics while
   using 90kHz PTS precision.

2. **rtp_timestamp_from_pts_ticks returns u64 not u32** — MediaTime::new
   takes u64. Returning u32 would truncate at 13.25 hours and create
   backwards MediaTime. str0m handles RTP u32 wrap internally; we feed
   it full u64.

3. **wlr-screencopy path also updated** — state.rs:605 used fps-based
   PTS formula. Changed to 90kHz ticks so wlr path matches Portal path
   unit. Without this, wlr-screencopy users would have wrong PTS after
   the time_base change.

4. **MP4 path (create_software_h264_muxer) UNCHANGED** — verified at
   avhw.rs:1693-1789, keeps 1/fps time_base, no set_frame_rate added.
   File output doesn't need real-time PTS.

Implementation:

- src/avhw.rs: WEBRTC_RTP_CLOCK_HZ=90_000 const; create_software_h264_encoder
  uses 1/90000 time_base + explicit framerate
- src/state_portal.rs: compute_capture_pts uses WEBRTC_RTP_CLOCK_HZ for
  tick conversion (was fps multiplier)
- src/state.rs: wlr PTS formula uses 90_000 (was fps multiplier)
- src/webrtc.rs: rtp_timestamp_from_pts_ticks simplified to identity
  function (pts_ticks.max(0) as u64), drop fps parameter; write_h264_frame
  signature drops fps (was only used for rtp conversion); 5 unit tests
  updated to assert 90kHz identity (0->0, 1500->1500, 90000->90000)

Verification expectations:

- Active period jitterBufferDelay: 1000+ ms -> < 100 ms
- 'Stop mouse, client continues 10s' symptom: should disappear
- Static period behavior: unchanged (was already correct)
- MP4 file output: unchanged
- VBV-constrained IDR sizes: unchanged (framerate explicit preserves
  rate control semantics)
- All prior fixes (#19, #23, #15, #18, #24) preserved

Out of scope (Oracle noted, not blocking):

- build_swenc_filter_graph still uses 1/fps time_base at avhw.rs:1603/1620
  (semantic mismatch but no functional impact since scale_vaapi passes
  PTS integers through)
- Runtime VBV update on bitrate change (separate pre-existing issue)

Tests:
- cargo build --release: 0 new warnings (23 baseline preserved)
- cargo test: 96 lib + 3 integration, 0 failed
- 5 rtp_timestamp_* tests updated for 90kHz identity
- SAFETY comments preserved verbatim
- 4 files changed, +48/-35 lines
2026-06-20 22:59:43 +08:00
2026-06-13 22:46:41 +08:00

wl-webrtc

Wayland screen capture and encoding tool.

Prerequisites

  • Rust toolchain (1.70+): rustup default stable
  • FFmpeg 6.0+ dev libraries with VAAPI support:
    • Arch: pacman -S ffmpeg
    • Ubuntu/Debian: apt install libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libva-dev
    • Fedora: dnf install ffmpeg-devel libva-devel
  • Wayland dev libraries:
    • Arch: pacman -S wayland-protocols
    • Ubuntu/Debian: apt install libwayland-dev wayland-protocols
    • Fedora: dnf install wayland-devel wayland-protocols-devel
  • DRM dev libraries:
    • Arch: pacman -S libdrm
    • Ubuntu/Debian: apt install libdrm-dev
    • Fedora: dnf install libdrm-devel

Build

cargo build --release

Run

# Basic capture to file
wl-webrtc --output output.mp4

# With custom FPS and bitrate
wl-webrtc --output output.mp4 --fps 60 --bitrate 8000000

# Specify DRM device for hardware encoding
wl-webrtc --output output.mp4 --drm-device /dev/dri/renderD128

# Verbose mode
wl-webrtc --output output.mp4 -v

CLI Arguments

Argument Default Description
-o, --output (required) Output file path (e.g., output.mp4)
--output-name auto Wayland output name to capture
--fps 30 Target frames per second
--codec h264 Video codec (h264 only for MVP)
--hw-accel vaapi Hardware acceleration method
--drm-device auto DRM render device path
--bitrate auto Target bitrate in bps
--gop-size auto Group of Pictures size
-v, --verbose false Enable verbose logging
--port 0 WebTransport server port (unused in MVP)
S
Description
No description provided
Readme
2.2 MiB
Languages
Rust 98.7%
Shell 1.2%