Resenha is a Discourse plugin that adds Discord-style voice rooms powered by WebRTC. Rooms appear in the sidebar; users join or leave with a single click and talk peer-to-peer — no media goes through the Discourse server.
Status: early alpha — test with small groups before opening to a full community.
- Sidebar-first UX — click a room to join/leave, see live participant avatars with speaking indicators, all without a route change.
- Mute, deafen, and per-user volume — right-click any participant (or use the kebab menu) for audio controls. Room managers can kick participants.
- Voice settings with mic test — input/output device pickers, a live input level meter, and an input sensitivity gate that stops transmitting below a chosen level. Preferences persist per device via
localStorage. - User room creation — users in the allowed group see a "+" button to create rooms directly from the sidebar; room creators and managers can edit rooms in-app.
- Audio cues — synthesized tones for connect/disconnect, user join/leave, and mute/deafen toggles.
- Noise suppression — optional DTLN-based background noise filtering via WebAssembly. See Noise Suppression.
- Video and screen sharing — optional, off by default. Each room gets a full page at
/resenha/r/<slug>with a tile grid; camera and screen share toggle without renegotiation, and senders only encode toward peers who are actually watching the page. Rooms can opt out individually. See Video. - Video settings with background blur — a per-room video settings modal with a live preview, camera device picker, and MediaPipe-powered background blur with an adjustable strength slider. See Background Blur.
- Pure browser WebRTC — signaling through Discourse + MessageBus; media stays peer-to-peer, no SFU/MCU required.
- Clone into your
pluginsdirectory:git clone https://github.com/xfalcox/resenha.git plugins/resenha - Rebuild or restart Discourse.
- Enable via Admin > Settings > Plugins > resenha enabled.
The plugin seeds a default "Watercooler" room on first enable.
| Setting | Description |
|---|---|
resenha_enabled |
Master switch. |
resenha_allowed_groups |
Groups that can access voice rooms (default: everyone). |
resenha_create_room_allowed_groups |
Groups that can create new rooms (default: admins, moderators, TL2). |
resenha_max_rooms_per_user |
Max rooms per creator (default 5). |
resenha_participant_ttl_seconds |
Redis presence TTL in seconds (default 30). Client heartbeat refreshes every 10s. |
resenha_video_enabled |
Allow camera video and screen sharing (default off). Rooms can opt out individually. |
resenha_video_max_publishers |
Max simultaneous video/screen publishers per room (default 8). |
resenha_video_background_blur_enabled |
Allow users to blur their camera background (default on; requires video). |
resenha_stun_servers |
STUN server addresses (pipe-separated). |
resenha_turn_servers |
TURN server addresses for NAT traversal. |
When resenha_video_enabled is on (and the room's own video toggle is too), the room view at /resenha/r/<slug> shows a video grid alongside the usual controls. Audio joins stay sidebar-first and unchanged; video lives on the page.
- Still pure mesh: a video m-line is pre-negotiated on every peer connection, so toggling the camera or a screen share is a
replaceTrackwith no renegotiation. - Senders attach video only toward participants currently on the room page (
watching_videopresence flag) — every skipped peer saves a full encoder session. - Encoding quality scales down with watcher count (720p ≤3 watchers, 480p ≤6, 360p beyond) and is capped by
resenha_video_max_publishers. - Camera and screen share are mutually exclusive per user. Stage rooms do not support video yet.
See docs/roadmap/video-screenshare.md for the full design.
Screen sharing has more environmental dependencies than the camera, and failures surface as a generic NotAllowedError in the browser console:
- Linux on Wayland: capture goes through
xdg-desktop-portal+ PipeWire. If the picker never appears and the error is instant, checksystemctl --user is-active graphical-session.target xdg-desktop-portal— a compositor session that isn't wired into systemd (common on minimal window manager setups) leaves the portal unable to start. The camera is unaffected, which makes this easy to misread as an application bug. - macOS Firefox: needs Screen Recording permission in System Settings, and only picks it up after a full browser restart.
- Insecure dev origins:
getDisplayMediahard-requires a secure context. Firefox'sabout:configoverrides that unlockgetUserMediaon plain-http dev hosts do not extend to screen capture — usehttps://or alocalhostorigin.
Camera publishers can blur their background from the video settings modal (cog menu on the room page). Person segmentation runs entirely on the publisher's device using MediaPipe selfie segmentation (Apache-2.0), compiled to WebAssembly — no media leaves the browser unprocessed, and viewers pay no extra cost.
Camera → hidden <video> → MediaPipe ImageSegmenter (person mask)
→ canvas composite (blurred frame + sharp person cutout)
→ canvas.captureStream() → WebRTC peers
The blur strength slider adjusts the composite live; the toggle swaps tracks on all peers via replaceTrack without renegotiation. Preferences persist per device via localStorage.
The MediaPipe runtime, wasm binaries, and the selfie_segmenter.tflite model (all Apache-2.0, © Google) are vendored under public/javascripts/mediapipe/. To re-fetch or bump versions:
cd plugins/resenha && bash scripts/fetch-mediapipe-assets.shThe script pins the @mediapipe/tasks-vision npm version and the model version, and verifies the model's SHA-256 checksum.
Optional DTLN-based noise suppression powered by dtln-rs, compiled to WebAssembly. Users can toggle it from the voice settings modal. The preference persists per device via localStorage.
Microphone → AudioContext → AudioWorkletNode (dtln) → MediaStreamDestination → WebRTC peers
A pre-built worklet bundle is committed at public/javascripts/dtln-worklet.js. To rebuild (requires Rust + Emscripten + pnpm):
rustup target add wasm32-unknown-emscripten
cd plugins/resenha && bash scripts/build-dtln-worklet.shbin/rspec plugins/resenha/spec # Ruby specs
bin/lint plugins/resenha # JS/SCSS/Ruby lintKey entry points:
app/controllers/resenha/rooms_controller.rb— room CRUD, signaling relay, participant state (mute/deafen/video/watching)app/controllers/resenha/page_controller.rb— serves the full-page room view at/resenha/r/:sluglib/resenha/guardian_extension.rb— authorization (group-based access and room creation permissions)assets/javascripts/discourse/app/services/resenha-webrtc.js— WebRTC orchestration, audio controls, video/screen-share publishing, sound effectsassets/javascripts/discourse/initializers/resenha-sidebar.js— sidebar section, click/context-menu handlersassets/javascripts/discourse/components/resenha/room-page.gjs— room page: tile grid, call controls, watching lifecycle
- Pure peer-to-peer topology; large rooms may hit browser limits. SFU support is on the roadmap.
- No call recording or moderation tools beyond kick.