Skip to main content

Jitsi Services

Seven Docker services run on a single EC2 instance (t3.xlarge, 4 vCPU, 16GB RAM, 16.16.21.64) to provide video conferencing with automatic transcription at https://book.aiqlick.com.

Service Overview

ServiceImageMemory LimitPurpose
webECR (aiqlick-meeting)512 MBCustom Jitsi Meet frontend with Let's Encrypt
prosodyjitsi/prosody:stable-9823512 MBXMPP server, JWT auth, MUC rooms
jicofojitsi/jicofo:stable-9823768 MBConference focus, allocates JVB and invokes Jigasi
jvbjitsi/jvb:stable-98236 GBVideo bridge (SFU), handles all media traffic
jigasiECR (aiqlick-jigasi)1.5 GBTranscription gateway with Whisper WebSocket
coturncoturn/coturn:4.6256 MBTURN server for NAT traversal (global reach)
dozzleamir20/dozzle128 MBLive log viewer (localhost:8888, SSM tunnel only)

Total memory budget: ~9.7 GB allocated, ~6.3 GB reserved for OS/Docker/headroom.

Web (aiqlick-meeting)

Custom Jitsi Meet build served by nginx with Let's Encrypt TLS. Base image jitsi/web:stable-9823 with the aiqlick-meeting React build copied to /usr/share/jitsi-meet/. A custom-config.js is deployed by deploy.sh for video quality settings and STUN server configuration.

Nginx Routes

PathUpstreamPurpose
/Static filesJitsi Meet React app
/http-bindxmpp.meet.jitsi:5280BOSH connection to Prosody
/xmpp-websocketxmpp.meet.jitsi:5280WebSocket signaling to Prosody
/colibri-wsjvb:9090Colibri WebSocket to JVB

Video Quality Settings

Configured in custom-config.js:

  • Resolution: ideal 720p, max 1080p / 30fps (most webcams cap at 720p; the higher max lets capable cams send 1080p when bandwidth allows)
  • channelLastN=4 — only forward 4 active speaker video streams to each receiver. Drops bandwidth pressure on constrained networks where receiver BWE often sits at 200–650 kbps. Layer suspension still hides unwatched layers automatically.
  • enableLayerSuspension=true — JVB stops forwarding unwatched simulcast layers
  • Screen share capped at 15fps
  • Bitrate caps per simulcast layer (overrides upstream Jitsi defaults via config.videoQuality.maxBitratesVideo in custom-config.js):
CodecLOWSTANDARDHIGH
VP8 / H264300 kbps800 kbps2500 kbps
VP9200 kbps600 kbps2000 kbps
AV1150 kbps450 kbps1500 kbps

Constrained receivers are unaffected — TCC bandwidth estimation still pins them to LD/SD layers automatically. The bumped HIGH cap only unlocks crisper 720p for clients that can actually send the higher bitrate.

Nginx Proxy Timeouts

The Jitsi web container's upstream meet.conf template does NOT set proxy_read_timeout for the /xmpp-websocket location, so nginx defaults to 60s. An idle XMPP WebSocket gets silently terminated, smacks resume often fails, and Prosody evicts the MUC occupant via the kick path — visible as Member role changed from PARTICIPANT to VISITOR - not supported! in Jicofo logs.

nginx/custom-meet.conf (in jitsi-deploy) is appended to meet.conf by the web container's 10-config init and overrides the default at the server scope:

proxy_read_timeout 3600s;
proxy_send_timeout 3600s;

The colibri-ws location sets its own per-location timeouts (3600s) and is unaffected. See Diagnosing Jitsi kicks for the full incident analysis.

Prosody (XMPP Server)

Lua-based XMPP server (jitsi/prosody:stable-9823) handling JWT authentication, presence, messaging, and MUC rooms.

Authentication

Prosody uses JWT authentication (AUTH_TYPE=jwt) with token_verification module:

  • JWT_APP_ID=aiqlick — issuer and audience identifier
  • JWT_APP_SECRET — shared secret for HMAC-SHA256 verification
  • JWT_ALLOW_EMPTY=1 — allows non-JWT users to join as guests

The token_affiliation module maps the moderator flag in JWT tokens to XMPP MUC roles. Only users with "moderator": true in their JWT get the moderator role — all other participants join as regular users.

Virtual Hosts

DomainAuthPurpose
meet.jitsiJWT (token_verification)Main domain for end users
auth.meet.jitsiinternal_hashedInternal services (Jicofo, JVB, Jigasi)
hidden.meet.jitsiinternal_hashedInvisible participants (transcriber)
warning

The hidden.meet.jitsi VirtualHost is only generated when ENABLE_TRANSCRIPTIONS=1 is in Prosody's environment. Without it, Jigasi cannot join as a hidden participant.

MUC Components

MUCPurpose
muc.meet.jitsiConference rooms (user-facing)
internal-muc.meet.jitsiService discovery (brewery rooms)

Brewery rooms inside internal-muc:

  • jvbbrewery — JVB instances register here with load stats
  • JigasiBrewery — Jigasi instances register with supports_transcription feature

Smacks (Stream Management) Tuning

Upstream Jitsi defaults smacks_hibernation_time = 60s, smacks_max_unacked_stanzas = 5, smacks_max_old_sessions = 1. Mobile clients, WiFi handovers, and even brief proxy hiccups routinely exceed 60s, causing Prosody to evict occupants — see kick diagnosis.

custom-prosody.cfg.lua (in jitsi-deploy) is copied into /config/conf.d/ by deploy.sh and loaded after jitsi-meet.cfg.lua (alphabetical), so its globals override the upstream defaults:

smacks_hibernation_time = 600;
smacks_max_unacked_stanzas = 10;
smacks_max_old_sessions = 5;

This is defense-in-depth; the primary kick fix is the nginx WS timeout in Web → Nginx Proxy Timeouts.

TURN Credential Delivery

Prosody uses mod_external_services (XEP-0215) — loaded globally — to advertise TURN servers to XMPP clients. The external_service_secret is written to /config/conf.d/turn-secret.cfg.lua by deploy.sh from the .env TURN_SECRET value.

caution

Without external_service_secret, Prosody sends TURN URLs without credentials, causing browser error: InvalidAccessError: Failed to construct 'RTCPeerConnection'.

Jicofo (Conference Focus)

Java application (jitsi/jicofo:stable-9823) that orchestrates conferences. When the first user joins a room, Jicofo allocates a JVB, negotiates media sessions, and invites Jigasi when transcription is requested.

Key Responsibilities

  1. Conference creation — allocates resources on first join
  2. JVB selection — picks a video bridge from jvbbrewery based on load
  3. Jigasi invocation — finds a Jigasi with supports_transcription in JigasiBrewery
  4. Session negotiation — coordinates SDP offers/answers between participants and JVB

Configuration

jicofo.bridge.brewery-jid = "jvbbrewery@internal-muc.meet.jitsi"
jicofo.jigasi.brewery-jid = "JigasiBrewery@internal-muc.meet.jitsi"

ENABLE_TRANSCRIPTIONS=1 must be set in Jicofo's environment for it to detect and invoke Jigasi.

info

ENABLE_AUTO_OWNER is disabled — moderator roles are controlled exclusively by JWT tokens via Prosody's token_affiliation module, not by join order.

JVB (Jitsi Videobridge)

Selective Forwarding Unit (jitsi/jvb:stable-9823) that receives media from all participants and forwards each stream only to those who need it. JVB does not mix or transcode media.

Port Mapping

PortProtocolPurpose
10000UDPAll video and audio RTP/SRTP traffic
9090TCPColibri WebSocket (internal, proxied by nginx)

NAT Traversal

JVB runs inside Docker with a private IP (172.18.0.x). DOCKER_HOST_ADDRESS tells JVB its EC2 public IP (16.16.21.64) so it advertises the correct ICE candidate. On startup, JVB joins jvbbrewery@internal-muc.meet.jitsi and publishes load stats for Jicofo bridge selection.

Custom Configuration (custom-jvb.conf)

A custom JVB config extends first-transfer-timeout from the default 15 seconds to 120 seconds. This gives Jigasi extra time to establish its Colibri WebSocket connection — the URL extraction from Jingle IQs can be delayed when Smack re-serializes XML with inherited xmlns attributes. Without this, JVB expires the Jigasi endpoint and Jicofo tears down the conference.

Deployed by deploy.sh to /root/.jitsi-meet-cfg/jvb/custom-jvb.conf, auto-included by JVB's main jvb.conf via HOCON include.

Jigasi (Transcription Gateway)

Custom fork of Jitsi Jigasi running in transcriber mode (no SIP). Joins conferences as a hidden participant, streams audio to Whisper, and broadcasts captions back to the room.

Base imagejitsi/jigasi:stable-9823
Custom layerWhisperTranscriptionService.java (WebSocket client to background-tasks)

Transcription Flow

  1. Jigasi joins room on hidden.meet.jitsi (invisible to users)
  2. Establishes Colibri WebSocket connection with JVB for audio forwarding
  3. Receives audio from JVB
  4. Streams audio chunks via WebSocket to wss://ai.aiqlick.com/transcription/ws
  5. Receives transcript text from Whisper service
  6. Broadcasts captions to all participants via XMPP

Colibri WebSocket Connection

Jigasi must establish a Colibri WebSocket connection with JVB so JVB can forward audio via its EndpointMessageTransport. The URL is extracted from Jingle session-initiate/transport-info IQs using regex.

The extraction uses multiple patterns:

  1. Direct xmlnsxmlns attribute directly on <web-socket> element
  2. Relaxed fallback — matches any <web-socket url='wss://...'> without requiring xmlns (handles Smack namespace inheritance)

If the URL is not available when the call starts, a retry mechanism (5 attempts, 2 seconds apart) schedules reconnection attempts.

Configuration

org.jitsi.jigasi.mode=transcriber
org.jitsi.jigasi.transcriber.customService=org.jitsi.jigasi.transcription.WhisperTranscriptionService
org.jitsi.jigasi.transcriber.whisperUrl=wss://ai.aiqlick.com/transcription/ws
org.jitsi.jigasi.xmpp.hiddenDomain=hidden.meet.jitsi

ICE/NAT Notes

  • Jigasi does NOT need ICE4J_PUBLIC_ADDRESS — it only communicates with JVB on the Docker network
  • Setting ICE4J_PUBLIC_ADDRESS on Jigasi actually breaks ICE connectivity between JVB and Jigasi
  • JIGASI_ALWAYS_USE_JVB=true and JIGASI_DISABLE_P2P=true are required (container environments don't support P2P)

XMPP Race Condition

Jigasi does not auto-retry XMPP connections. If Prosody is restarted independently, Jigasi must also be restarted. The deploy.sh script handles this automatically — it checks for brewery join after deploy and restarts Jigasi if needed.

Coturn (TURN Server)

TURN relay server (coturn/coturn:4.6) for users behind corporate firewalls, strict NATs, or restricted networks.

How It Works

Prosody's mod_external_services sends TURN URLs to XMPP clients with time-limited HMAC-SHA1 credentials generated from TURN_SECRET.

Ports

PortProtocolPurpose
3478UDP/TCPSTUN + TURN
5349TCPTURN over TLS
49152-49200UDPTURN relay ports

Dozzle (Live Log Viewer)

Web UI for live container log monitoring. Bound to 127.0.0.1:8888not publicly accessible.

Access via SSM port forwarding:

aws ssm start-session --profile Administrator-842697652860 --region eu-north-1 \
--target i-0620d2e23695f5bfc --document-name AWS-StartPortForwardingSession \
--parameters '{"portNumber":["8888"],"localPortNumber":["8888"]}'
# Then open http://localhost:8888

Inter-Service Communication

FromToProtocolPurpose
BrowserWebHTTPS (443)Load app, proxy signaling
WebProsody / JVBHTTP (5280 / 9090)BOSH, WebSocket, Colibri proxy
BrowserJVBUDP (10000)Media (video/audio)
BrowserCoturnUDP/TCP (3478, 5349)TURN relay (when direct fails)
ProsodyJicofoXMPPRoom events, conference orchestration
JicofoJVB / JigasiXMPPChannel allocation, transcription invocation
JigasiJVBColibri WS + RTPAudio forwarding and media
Jigasibackground-tasksWSSStream audio, receive transcripts

Required Environment Variables

VariableServicesPurpose
DOCKER_HOST_ADDRESSJVB, CoturnEC2 public IP for ICE/NAT
ENABLE_TRANSCRIPTIONS=1Prosody, Jicofo, WebHidden domain + Jigasi detection
JIGASI_MODE=transcriberJigasiTranscription-only mode (no SIP)
JIGASI_TRANSCRIBER_USER / _PASSWORDProsody, JigasiXMPP credentials for transcriber
JWT_APP_ID / JWT_APP_SECRETProsodyJWT authentication for moderator roles
TURN_SECRETProsody, CoturnShared secret for TURN credentials
JIGASI_ALWAYS_USE_JVB=trueJigasiForce JVB mode (required in Docker)