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.
- 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. - 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_noise_suppression |
Allow users to opt into DTLN noise suppression. |
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_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.
Optional DTLN-based noise suppression powered by dtln-rs, compiled to WebAssembly. When enabled by an admin, users can toggle it from their participant context menu. 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.