Self-hosted video meetings for teams and clients — WebRTC with a mediasoup SFU, on your own server, with Docker. No accounts for guests, no third-party cloud, your branding.
🌐 Website (in Italian): tastieredigitali.tech/casi-studio/tiditalk

On the phone: home, screen sharing with annotations, speaker view
The people in the screenshots are illustrations rendered into a fake camera; everything else is the real UI.
Meetings
- Waiting room like Google Meet: guests wait until a host admits them (Admit / Deny / Admit all card, on by default, switchable per meeting); registered users get straight in; guests go back to waiting if the last host leaves. Hosts can also lock the room and remove a guest. From the waiting room card hosts write to the people waiting (three quick replies or free text) and each guest also sees their place in line
- "Ready to join?" pre-join page: a large preview with your name and the microphone and camera buttons inside it, pills for microphone, speakers and webcam, who is already in (only with a valid link) and other ways to join: as a second screen (the laptop in the meeting room with no microphone, speakers or camera, so no echo; Back to the call makes it a normal participant) or joining and presenting right away
- End the meeting for everyone: pressing Leave, a host chooses whether to just leave or close the meeting for everyone, waiting room included
- Home page with a full device check (camera preview, microphone level, speakers test on the chosen output) — switching tab does not close and reopen camera and microphone (they are released only after 10 minutes away), and registered users then go straight into the meeting — plus recent meetings (date, duration, who took part, live badge, one tap to rejoin; the last 5, the others with Show more) and the next scheduled ones
- Scheduled meetings with email invitations and
.icscalendar event - Guest links that expire, no sign-up — works in the browser on desktop and phone
- Grid view that sizes every tile to the available space (all participants visible, same size, 16:9, last row centered) and speaker view
- Change layout from the View button: Auto, Grid, Spotlight or Sidebar, maximum number of tiles (a "+N" tile beyond that), hide people with the camera off, your own tile in the grid, as a draggable small window or hidden — only for you, saved in the browser
- Portrait (phone) cameras shown in full instead of cropped
Presenting
- Screen sharing with live annotations on top of the shared screen: pen, highlighter, arrow, rectangle, circle, text, eraser and laser pointer with the presenter's name; hide drawings for yourself, save a PNG of the shared screen with the drawings
- While someone presents, the participants sit in a row above the shared screen, smaller as more people join
- The presenter decides whether everyone can draw or only presenter and hosts
- Local recording that includes annotations and laser pointer; leaving the room waits for the file to be saved
In the room
- Chat, participant list and an Activities button with polls (hosts create, everyone votes, live results, anonymous or with names), a meeting timer (a countdown for everyone on the server clock, pause, +1 minute, rings at the end) and questions and answers (also anonymous, voted by everyone, marked answering now and answered by the host, downloadable for Excel)
- Chat with pinned messages (hosts, up to 3), reactions on each message, reply with a quote, clickable links and "… is typing"
- Notifications at the bottom left for joins, leaves, chat (reply without opening the chat) and recording, with the person's initials and the time
- Zoom on a shared screen (− % +, Ctrl + wheel or pinch, drag, minimap) and open it in a new window for a second monitor, with drawings still aligned
- Tiles that show who is talking: a voice badge with live bars or a crossed-out microphone, a ring around whoever talks, and with the camera off the avatar colour, a large circle with the initials and rings while talking
- Menu ⋮ on tiles and in the People list: pin for me or for everyone (up to 3), full screen, and for hosts mute one person, send back to the waiting room or remove
- Second screen from the pre-join screen, the home page or More in the room
- Phone layout: five large buttons (microphone, camera, hand, More, Leave), everything else in More, a floating chat button and your own video in a small draggable window
- Reactions bar with skin tones and every emoji, rising with the name of who sent them and staying for a few seconds on their tile
- Raised hands in a queue: order of arrival in the People list, place in line on the tile, Lower and Lower all; the hand goes down by itself after you have spoken for a few seconds
- Microphone and speakers are separate controls: mute yourself without
muting the others, or silence the others (
A) without touching your microphone. The speakers ▾ picks the output (system default unless you choose), shows the level, plays a test sound and can reconnect the audio - Push-to-talk: while muted, hold Space to talk
- Advanced noise reduction (RNNoise neural network in WebAssembly, runs in the browser, no data leaves the computer) and echo cancellation: on or off by default from Settings → Rooms, everyone can switch their own from the microphone popup for that meeting
- Network & quality window: live bandwidth, latency and jitter graphs, packet loss, per-stream stats, direct/TURN path; hosts also see a Participants section with every participant's connection as the server sees it (quality, bitrates, latency, jitter, loss, direct or TURN, browser and device network)
- End-of-call page for guests: branded two-column page with an animated constellation of the participants, stats, optional star rating, contact card and buttons — or a redirect to your own URL (Settings → End of call)
- Floating window with the whole meeting (Chrome/Edge): all participants, speaker highlight, mic/camera/hand/leave. Opens by itself when you switch tab
- Virtual backgrounds (blur, 56 photos — offices, homes, libraries, Italy, nature, space, abstract, video games — synthwave and pixel-art illustrations, or your own image), color styles and face effects (MediaPipe) in a side panel with a live preview, opened from More or from the button on your own tile
- 411 3D face effects like Meet (characters that copy your expressions, animals with real fur and kawaii animals, costumes, cute accessories, fun and Halloween effects that react to your smile, mouth and eyebrows, wigs, beards, hats, glasses, painted masks, scenes with your face in a frame) that follow the head in 3D: MediaPipe Face Landmarker in a Web Worker + three.js, all served locally. A search box at the top finds an effect by name in any of the app's languages, by section, or just "animals". The classic 2D effects stay in their own tab
- Call settings window with sections: audio & video devices, room rules (hosts), recording quality, appearance, shortcuts, info
- Microphone popup with live mic and speaker levels and a switch for automatic gain control
- Microphone watchdog: a mic silenced by the OS or another app, or dead after a device change, is reopened without interrupting the call
- Per-tile connection quality with detailed stats, data saver mode; native share sheet for the invite link on phones
- Tooltips and keyboard shortcuts on every control, optional first-join guide
- Light and dark theme, UI in Italian, English, French and German
Administration
- Branding panel: name, tagline, logo, favicon, accent color (automatically adjusted for contrast in both themes), default theme, footer, company info
- Users with admin and host roles — from the panel or from
.env - Room rules: waiting room, guest screen sharing, drawing permissions, first-join guide, default echo cancellation and noise reduction
- External REST API to create meetings and guest links from a CRM
- Automatic daily updates with email report, automatic rollback and ready-to-paste rollback commands
- A Linux server with Docker and the Docker Compose plugin
- A public IP and a domain name
- A reverse proxy with HTTPS and WebSocket support (Nginx Proxy Manager, Traefik, Caddy, plain Nginx…)
systemdfor scheduled updates (present on Debian/Ubuntu, LXC containers included);cronis used as a fallback- These ports reachable from the internet:
| Port | Protocol | What |
|---|---|---|
PORT (default 3010) |
TCP | Web app — behind your reverse proxy |
RTC_MIN_PORT–RTC_MAX_PORT (default 40000–40400) |
UDP + TCP | Media (mediasoup) — directly, not through the proxy |
| 3478 | UDP + TCP | STUN/TURN (coturn) |
TURN_MIN_PORT–TURN_MAX_PORT (default 49152–49200) |
UDP | TURN relay |
git clone https://github.com/Giuseppe-sciarra/TiDiTalk.git
cd TiDiTalk
# 1. Configuration
cp .env.template .env
nano .env # at least: SERVER_SECRET, BASE_URL, ANNOUNCED_IP, TURN_*, USERS, SMTP_*
# 2. Third-party assets (MediaPipe for backgrounds/effects and the 3D face
# effects, three.js, emoji images, background photos from Unsplash).
# Updating from 1.6.x: run setup-mediapipe.sh again for the 3D effects
bash setup-mediapipe.sh
bash setup-effects.sh
bash setup-backgrounds.sh
# 3. Start
docker compose up -d --build
docker compose logs -f app
# 4. Nightly automatic updates (at 05:00)
bash installa-aggiornamento.shGenerate SERVER_SECRET with:
openssl rand -hex 32ANNOUNCED_IP must be the numeric public IP of the server: mediasoup does
not resolve hostnames. Set DEFAULT_UI_LANGUAGE (it, en, fr, de) to
the language used for emails when the browser doesn't declare one.
Then open your BASE_URL, sign in with a user from USERS and finish the setup
from Settings.
| Role | Can |
|---|---|
| admin | everything, including Settings and user management |
| host | create, schedule and run meetings |
| guest | join through an invitation link, no account |
From .env — created at startup if missing:
USERS=alice:StrongPassword1:Alice Smith:admin,bob:StrongPassword2:Bob:host
USERS_MODE=create # or "sync": .env becomes the source of truthPasswords can also be bcrypt hashes ($2b$...; inside a compose env file write
$ as $$). With USERS_MODE=sync those users are read-only in the panel.
From the panel — Settings → Users.
| What | Where |
|---|---|
| Name, logo, favicon, accent color, theme, footer, company info | Settings → Branding |
| Guest screen sharing, drawing for everyone, first-join guide, echo cancellation and noise reduction by default | Settings → Rooms |
| Users and passwords | Settings → Users, or USERS in .env |
| SMTP, TURN, IPs, ports, secrets, API keys, updates | .env |
bash installa-aggiornamento.sh # every night at 05:00, Italian time
bash installa-aggiornamento.sh 4:30 # ...or at another time
bash installa-aggiornamento.sh --status # is it scheduled? next run? last result
bash installa-aggiornamento.sh --remove # stop itinstalla-aggiornamento.sh installs a systemd timer (persistent: if the
server was off at the scheduled time, the update runs as soon as it's back) on
Italian time, even when the container runs in UTC. Without systemd it falls
back to cron (/etc/cron.d). It removes the old install-cron.sh schedule
(up to 1.8.8, every morning at 08:00) by itself, so the two never run
together. install-cron.sh and auto-update.sh are still there and hand
everything over to the new names.
Every night aggiorna-tutto.sh:
- postpones if a meeting is in progress (3 × 15 minutes, then proceeds);
- downloads the latest
node:22-alpinebase image, so security fixes in Node and Alpine (like the Brotli one of October 2026) actually arrive; - upgrades every dependency in
server/package.jsonto its latest version, majors included, together withpackage-lock.json. This runs in a throwaway container that only gets those two files. A dependency with an upper limit (e.g."mediasoup": ">=3.14 <4") stays as it is: that's how you hold one back; - rebuilds the app (always with
--pull) when something changed: base image, dependencies, versions inside the image older than on npm, or Dockerfile and code different from the last successful build (e.g. after copying a patch); - tests it:
/api/healthmust answer and the container must not restart by itself for 20 seconds. Otherwise it restores the previouspackage.json, lock file and image — automatic rollback; - updates coturn when a new image is out, with the same check and a return to the previous image if it doesn't stay up;
- sends the report: a table of what changed, tagged major / minor / patch, what happened (app, Node, coturn) and the exact command to go back;
- cleans Docker safely: keeps the last
UPDATE_KEEP_ROLLBACKrollback images (default 5), removes stopped containers, untagged images and build cache older than 7 days. It never runsprune -a.
Backups: backups/ (last 10). Log: logs/aggiorna-tutto.log (plus
logs/build.log, logs/npm-risoluzione.log).
| Variable | Default | Meaning |
|---|---|---|
UPDATE_NOTIFY_EMAIL (or AGG_MAIL_A) |
— | where the report goes |
UPDATE_MAIL_ALWAYS (or AGG_MAIL_SEMPRE) |
0 |
1 = email even when nothing changed |
UPDATE_SKIP_IF_BUSY |
1 |
postpone while people are connected |
UPDATE_BUSY_RETRIES / UPDATE_BUSY_WAIT |
3 / 900 |
how many times / every how many seconds |
UPDATE_KEEP_ROLLBACK (or AGG_TIENI_ROLLBACK) |
5 |
rollback images to keep |
AGG_ATTESA_SERVIZI |
600 |
seconds to wait for coturn to stay up on a new image |
UPDATE_CHECK |
0 |
old "updates available" email, without updating |
bash aggiorna-tutto.sh --dry-run # show what would change, touch nothing
bash aggiorna-tutto.sh # update now
bash aggiorna-tutto.sh --force # update now even during meetings
docker compose build --pull app && docker compose up -d app # after changing server/, public/assets/i18n or the Dockerfile
docker compose restart app # after changing CSS/JS/HTML (mounted live)Enabled when EXTERNAL_API_KEY is set. Send the key in the x-api-key header.
| Method | Endpoint | Purpose |
|---|---|---|
GET |
/api/external/health |
liveness |
POST |
/api/external/generate-guest-token |
guest link for a room |
POST |
/api/external/schedule-meeting |
create a meeting and send invitations |
DELETE |
/api/external/meeting/:id |
delete a meeting |
GET /api/health (no key) returns { ok, uptime, rooms, peers, version } and
is used by the updater to avoid restarting during a call.
M microphone · A speakers · V camera · S present · D draw · H raise hand ·
C chat · U people · hold Space while muted to talk (push-to-talk).
While drawing: P pen, E highlighter, A arrow, R rectangle, O circle,
T text, X eraser, L laser, Ctrl+Z undo, Esc exit.
- Build fails with
gyp ERR! … FetchError … headers.tar.gz— from 1.5.3 the Dockerfile uses the Node headers already inside the image (npm_config_nodedir=/usr/local) instead of downloading them from unofficial-builds.nodejs.org. Update the Dockerfile and rebuild. - Video/audio doesn't connect for some users — the RTC port range must be
forwarded as UDP and TCP and
ANNOUNCED_IPmust be the public IP. Behind strict firewalls TURN is what saves the day: checkTURN_*. - Camera or microphone blocked — the page must be served over HTTPS.
- Someone sounds too quiet — microphone popup (arrow next to the mic): the level bar shows the real signal. If it barely moves, raise the input volume in the computer's sound settings. If the input volume keeps dropping by itself, turn off "Adjust microphone volume automatically".
- Echo — usually speakers without headphones, or two devices in the same room.
- Invitations not delivered — Settings → System shows the SMTP status and has a test button.
- Updates not happening —
bash installa-aggiornamento.sh --status. - You can't hear the others — speakers ▾ → Play a test sound. If you can't hear even that, the browser has lost its audio output: Reconnect the audio, and if that doesn't help close and reopen the browser. With Bluetooth headphones, pick the computer's microphone: using the headphones' own microphone switches them to "phone mode" (low quality, and on Mac it can leave the browser without sound).
- The camera does not come back after a FaceTime / Zoom call — when another app takes the webcam the camera turns off and a message appears; it comes back by itself as soon as the other app frees it (it retries every few seconds, always on the same camera). You can also press Retry or turn the camera on from the bar: no need to reload the page.
- Someone joins and can't hear — open ⋮ → Network & quality as a host:
the Participants section shows their connection. On the server,
docker logs tdt-app 2>&1 | grep CONNalso logs what their browser did with the audio (audio ev=play-blocked= the browser blocked playback until a tap,no-data= the stream carries no data,consume-error,sink-error), plusrejoinlines when someone comes back after a network drop. - Browser console diagnostics (hosts):
tdConnLog('Name')disconnections and microphone events of a participant,tdMicInfo()real state of your microphone,tdCamInfo()resolution of preview, sent video and camera.
Tiditalk is free software released under the GNU Affero General Public License v3.0 or later — see LICENSE.
You can use, modify and redistribute it, commercially too. If you run a
modified version as a network service, you must offer its source code to
your users: the "About" window in every room links to the source — set
SOURCE_URL in .env to point to your fork.
Third-party components and their licenses: THIRD-PARTY.md.
Copyright © Giuseppe Sciarra — Tastiere Digitali
If Tiditalk is useful to you, you can support its development:
Bug reports and pull requests are welcome.










