Testgram supports the full phone.* calling surface via WebRTC:
- 1:1 voice & video calls (WebRTC, MTProto DH key exchange)
- Group calls / voice & video chats (multi-participant, SSRC-based)
- Conference calls (end-to-end encrypted group calls)
- Live streams (RTMP ingest + HLS playback for group calls and live stories)
For calls to work you need a STUN/TURN server. The docker compose stack bundles
this for you:
| Service | Image | Purpose |
|---|---|---|
coturn |
coturn/coturn:latest |
STUN/TURN server for WebRTC (1:1 and group calls) |
rtmp-server |
bluenviron/mediamtx:latest |
RTMP ingest + HLS playback for live streams |
call-init |
mongo:8 |
Creates MongoDB call indexes on first start |
You no longer need to install Coturn on the host. Just configure .env and start
the stack. Manual/external Coturn is still supported as an alternative (see below).
The bundled coturn service uses long-term credentials testgram:testgram2024
(see the coturn service --user flag in docker-compose.yml). The credentials
advertised to clients in .env must match the coturn user, otherwise TURN relay
authentication fails.
# REQUIRED configuration for calls
App__WebRtcConnections__0__Ip=YOUR_SERVER_IP
App__WebRtcConnections__0__Ipv6=
App__WebRtcConnections__0__Port=3478
App__WebRtcConnections__0__Turn=True
App__WebRtcConnections__0__Stun=True
App__WebRtcConnections__0__UserName=testgram
App__WebRtcConnections__0__Password=testgram2024
# Additional server for redundancy (optional)
App__WebRtcConnections__1__Ip=BACKUP_SERVER_IP
App__WebRtcConnections__1__Port=3478
App__WebRtcConnections__1__Turn=True
App__WebRtcConnections__1__Stun=True
App__WebRtcConnections__1__UserName=testgram
App__WebRtcConnections__1__Password=testgram2024The
coturnservice readsApp__WebRtcConnections__0__Ipas its--external-ip, so setting the value above is enough to bring TURN up with the right public address.
App__RtmpStreamUrl=rtmp://YOUR_SERVER_IP:1935/live
App__RtmpHlsUrl=http://rtmp-server:8888/live
RTMP_PORT=1935
RTMP_HLS_PORT=8888App__RtmpStreamUrlis the public RTMP ingest URL handed to streaming software (OBS, etc.). Use your server's public IP or domain.App__RtmpHlsUrlis the internal HLS URL the messenger reads segments from; keep it pointed at thertmp-serverservice name.
sudo ufw allow 3478/tcp
sudo ufw allow 3478/udp
sudo ufw allow 3479/tcp
sudo ufw allow 3479/udp
sudo ufw allow 49152:49172/udp # TURN relay port range (matches coturn min/max-port)
sudo ufw allow 1935/tcp # RTMP ingestThe bundled coturn service publishes 3478, 3479 and the relay range
49152-49172/udp; rtmp-server publishes 1935 (RTMP) and 8888 (HLS).
cd docker/compose
docker compose up -dOn first startup, automatically:
- The
call-initcontainer creates indexes for thecall_sessions/group_callscollections and configures TTL cleanup for old records. coturnandrtmp-serverstart alongside the messenger services.
Verify the helper services:
docker compose logs call-init # index creation
docker compose logs coturn # TURN server
docker compose logs rtmp-server # RTMP/HLS serverTo create call indexes manually:
cd scripts
./setup_call_indexes.sh
# or:
docker compose exec mongodb mongosh tg < scripts/setup_call_indexes.jsIf you prefer to run your own Coturn (e.g. on a dedicated relay host), disable or
remove the bundled coturn service and install Coturn yourself:
# Ubuntu/Debian
sudo apt-get update
sudo apt-get install coturn
sudo systemctl enable coturnEdit /etc/turnserver.conf:
listening-port=3478
tls-listening-port=5349
external-ip=YOUR_SERVER_IP
realm=testgram
user=testgram:testgram2024
fingerprint
lt-cred-mech
min-port=49152
max-port=49172
log-file=/var/log/turnserver.log
no-tls
no-dtlssudo systemctl start coturn
sudo systemctl status coturnThen point App__WebRtcConnections__0__Ip / UserName / Password in .env at your
external server, making sure the credentials match the user line above.
- Log in with two different accounts.
- Start a 1:1 call, then a group call (voice chat), between them.
- Check server logs:
docker compose logs -f messenger-command-server | grep -i callUse the online tool: https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/
Or via command line:
sudo apt-get install stuntman-client
# Test STUN
stunclient YOUR_SERVER_IP 3478
# Test TURN
turnutils_uclient -v -u testgram -w testgram2024 YOUR_SERVER_IPPoint streaming software (e.g. OBS) at rtmp://YOUR_SERVER_IP:1935/live with the
stream key returned by phone.getGroupCallStreamRtmpUrl, then join the group call as a
viewer to confirm HLS playback.
-
RequestCall — initiator creates the call.
- Record created in MongoDB
call_sessions(staterequested). updatePhoneCall{ phoneCallRequested }(carryingg_a_hash+ protocol) sent to receiver.
- Record created in MongoDB
-
AcceptCall — receiver accepts.
- State updated to
accepted;updatePhoneCall{ phoneCallAccepted }sent to caller. - The accepting device's other sessions get
phoneCallDiscarded(single-acceptance).
- State updated to
-
ConfirmCall — initiator confirms.
- Diffie-Hellman key exchange completed (
g_a/g_brange + hash validated). - WebRTC
connections(STUN/TURN reflectors + optional P2P) returned. - State changed to
confirmed.
- Diffie-Hellman key exchange completed (
-
SendSignalingData — exchange WebRTC signals (ICE candidates, SDP) via
updatePhoneCallSignalingData. -
DiscardCall — end the call.
- Duration and reason saved; state changed to
discarded(idempotent re-discard).
- Duration and reason saved; state changed to
requested— call initiatedreceived— callee's client received the requestaccepted— call acceptedconfirmed— keys exchanged, WebRTC connection establishingdiscarded— call ended (terminal)
- CreateGroupCall / JoinGroupCall / LeaveGroupCall / DiscardGroupCall manage the
group-call lifecycle; participants are tracked with unique SSRC (
Source) values and a monotonically increasingVersion. - EditGroupCallParticipant / ToggleGroupCallSettings / ToggleGroupCallRecord / EditGroupCallTitle manage per-participant and call-wide state.
- Scheduled calls support
schedule_dateandStartScheduledGroupCall. - Conference (E2E) calls add chain blocks, invites, and encrypted broadcasts.
- Live streams:
GetGroupCallStreamRtmpUrlreturns the RTMPurl/key(with key rotation onrevoke);GetGroupCallStreamChannelslists channels; HLS segments are served throughupload.getFilewith aninputGroupCallStreamlocation.
- Check logs:
docker compose logs messenger-command-server | grep -i "call\|webrtc"- Check MongoDB:
docker compose exec mongodb mongosh tg
db.call_sessions.find().sort({Date: -1}).limit(5)- Check the WebRTC configuration passed to the messenger:
docker compose exec messenger-command-server env | grep WebRtc- Check the bundled coturn container:
docker compose logs -f coturn
docker compose ps coturn-
Confirm the advertised credentials match coturn's
--user(testgram:testgram2024). A mismatch is the most common cause of one-way audio / failed relay. -
Check ports on the host:
sudo ss -tulpn | grep -E '3478|3479'
sudo ufw status- Check the RTMP server:
docker compose logs -f rtmp-server-
Confirm
App__RtmpStreamUrluses a reachable public address andApp__RtmpHlsUrlpoints at thertmp-serverservice name. -
Confirm port
1935is open and the stream key matches the one returned byphone.getGroupCallStreamRtmpUrl.
- Widen the relay port range in the coturn
--min-port/--max-portflags (and open the matching UDP range on the firewall). - Check network bandwidth.
- Add multiple TURN servers in different locations via
App__WebRtcConnections__N__*.