Low-level Go library for programmatic connection to Jitsi Meet calls. Handles XMPP signaling, joins MUC, catches Jingle session-initiate, parses SDP/ICE/SSRC, opens bridge channel (colibri-ws) and integrates with pion/webrtc for media.
Built for the olcRTC project.
No hardcoded addresses/rooms — everything is user-supplied.
- Anonymous SASL → XMPP MUC join → focus → Jingle session-initiate
- Auto-discovery of MUC domain from server
config.js(works with bothconference.hostandmuc.host) - Full Jingle XML ↔ SDP converter (BUNDLE, RTP payload-types, RTCP-FB, SSRC, fingerprint, ICE candidates, rtcp-mux filtering)
- Binds
*webrtc.PeerConnection(pion) to Jingle session: automatic session-accept, trickle ICE, reconnect onsession-terminate moving - Sending sendonly video track (included in session-accept SDP, Jicofo distributes to other participants)
- Receiving video:
RequestVideo(ctx, maxHeight)— sendsReceiverVideoConstraintsvia bridge channel (without this JVB will NOT forward video!) - Plan B detection:
Negotiator.Acceptdetects Plan B offers,peer.IsPlanBError(err)for handling source-add/source-remove/transport-info/session-terminatehelpers- Bridge channel with two transports (selected automatically based on what server offers):
- colibri-ws (modern, default for new deployments) — WebSocket to JVB
- SCTP datachannel (legacy, used by older Jitsi deployments) — pion DataChannel to JVB
- Both share the same
colibri.Bridgeinterface — broadcast/unicastEndpointMessage, raw bytes via base64
- Groupchat, raise/lower hand, leave
- Low-level XMPP API (
Send(rawXML),SendJingle,NextID,LastJingleStanza) -insecureflag to skip TLS verification (handy for self-signed certs in testing)- CLI with 6 modes for testing and benchmarking
On meet.cryptopro.ru (single bridge):
| payload | rx steady | tx side | notes |
|---|---|---|---|
| 8 KB | ~135 Mbit/s | ~220 Mbit/s | stable, no drops |
| 16 KB | 80–160 Mbit/s | ~195 Mbit/s | fluctuates |
| 64 KB | — | — | bridge closes connection (max-message-size) |
Up to ~1 Gbit/s achievable with close geolocation to JVB and multiple parallel endpoints.
j/
├── j.go # public API: Join, JoinMUC, Session, Negotiator()
├── internal/
│ ├── xmpp/ # WS + ANONYMOUS SASL + bind + MUC + focus + Stream Mgmt + raw Jingle/IQ helpers
│ ├── jingle/ # Jingle XML ↔ SDP (BUNDLE, RTP, RTCP-FB, SSRC, fingerprint, candidates)
│ ├── colibri/ # bridge channel WebSocket — JVB protocol (EndpointMessage, LastN, …)
│ └── peer/ # pion PeerConnection ↔ Jingle bridge (Accept, trickle ICE, source-add)
├── cmd/cli/ # CLI: jingle | chat | dc | dc-raw | media (+send-video) | bench
└── readme.md
WebSocket wss://host/xmpp-websocket?room=ROOM (subprotocol: xmpp)
│
├─ ANONYMOUS SASL → bind → session → Stream Management (XEP-0198)
├─ extdisco:2 → TURN/STUN credentials
├─ focus.host conference allocation
├─ MUC join (presence + codecList + SourceInfo + nick + caps)
├─ ← Jingle session-initiate (SDP-as-XML, ICE candidates, colibri-ws URL or SCTP map)
├─ → Jingle session-accept (with pion-generated SDP→Jingle)
├─ → Jingle transport-info (trickle late candidates)
├─ → Jingle source-add / source-remove (for late tracks)
├─ ← Jingle session-terminate (reason="moving" → reconnect)
│
└─ Bridge channel (one of):
│
├─ colibri-ws (wss URL from session-initiate) ──→ JVB
│ ClientHello / ServerHello / EndpointMessage / EndpointStats / …
│
└─ SCTP datachannel (pion DataChannel "JVB data channel") ──→ JVB
Same JSON protocol, but transport is dc.SendText() (NOT binary!)
— JVB only processes DataChannelStringMessage, ignores binary frames.
type Config struct {
Host string // Jitsi host, e.g. "meet.example.com"
Room string // room name (no @domain)
Nick string // display name
Debug bool // verbose XMPP/WS logging
Insecure bool // skip TLS verification (self-signed / expired certs)
}Host is enough — j fetches https://<host>/config.js to discover the actual MUC
domain (muc.host or conference.host). Override with -debug to see the choice.
j.JoinMUC — XMPP only, no Jingle (doesn't wait for session-initiate).
sess, _ := j.JoinMUC(ctx, j.Config{Host: "meet.example.com", Room: "myroom", Nick: "thejproject"})
defer sess.Close()
sess.Chat("hello")
sess.RaiseHand()
sess.LowerHand()
for m := range sess.Messages() {
fmt.Printf("<%s> %s\n", m.From, m.Body)
}The bridge channel carries control/data messages between participants and JVB.
j supports both transports automatically — pick the one your server uses.
sess, _ := j.Join(ctx, j.Config{...}) // waits for Jingle (needs ≥1 other participant)
defer sess.Close()
if sess.ColibriWS != "" {
sess.OpenBridge(ctx) // dials wss://<jvb>/colibri-ws/...
}If sess.ColibriWS is empty, the server uses SCTP. The DataChannel must be
created before Negotiator.Accept so it ends up in the SDP answer:
pc, _ := webrtc.NewPeerConnection(sess.IceConfig())
// 1. Create DC before Accept (so it's in the SDP)
sess.PrepareBridgeSCTP(pc)
// 2. Negotiate SDP / send session-accept
neg := sess.Negotiator()
neg.PC = pc
neg.Accept(ctx)
// 3. Wait for DC to open + ServerHello from JVB
sess.WaitBridgeSCTP(ctx)// raw bytes — broadcast to all
sess.BridgeSendRaw("", []byte{0xDE, 0xAD, 0xBE, 0xEF})
// unicast to specific endpoint
sess.BridgeSendRaw("2968719f", payload)
// JSON EndpointMessage with arbitrary fields (bridge doesn't parse, relays as-is)
sess.BridgeSendMessage("", map[string]any{"type": "chat", "text": "hi"})
for m := range sess.BridgeMessages() {
if raw := colibri.DecodeRaw(m); raw != nil {
// received raw bytes from peer
}
}Low-level bridge (works with both transports via colibri.Bridge interface):
br := sess.Bridge()
br.SendLastN(8)
br.SendVideoType("camera") // "camera" | "desktop" | "none"
br.SendReceiverVideoConstraints(map[string]any{ /* … */ })
br.SendJSON(anyJSONserialisable)import "github.com/pion/webrtc/v4"
pc, _ := webrtc.NewPeerConnection(sess.IceConfig())
// receive audio
pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio,
webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionRecvonly})
// send video — pion puts ssrc/cname/msid in SDP, our Negotiator
// automatically converts it to <source> inside session-accept,
// Jicofo distributes to other participants
videoTrack, _ := webrtc.NewTrackLocalStaticSample(
webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8, ClockRate: 90000},
"myvideo", "mystream")
pc.AddTrack(videoTrack)
pc.OnTrack(func(t *webrtc.TrackRemote, _ *webrtc.RTPReceiver) {
buf := make([]byte, 1500)
for {
_, _, err := t.Read(buf)
if err != nil { return }
// RTP packet from remote participant — process as needed
}
})
neg := sess.Negotiator()
neg.PC = pc
if err := neg.Accept(ctx); err != nil { panic(err) }
defer neg.Terminate("success")
// IMPORTANT: without this JVB will NOT forward video!
sess.RequestVideo(ctx, 720)
// ICE candidates discovered AFTER session-accept are automatically
// sent via transport-info (trickle ICE)
// send VP8 frames
go func() {
for { videoTrack.WriteSample(media.Sample{Data: vp8Frame, Duration: 33*time.Millisecond}) }
}()Critical: JVB does not forward video until the receiver sends
ReceiverVideoConstraints via bridge channel. Without this, OnTrack will never fire.
// Simple — request all video at 720p:
sess.RequestVideo(ctx, 720)
// Or manually via bridge for fine-grained control:
sess.OpenBridge(ctx)
sess.Bridge().SendJSON(map[string]any{
"colibriClass": "ReceiverVideoConstraints",
"lastN": 3, // max 3 video streams
"onStageSources": []string{"alice-v0"}, // prioritized source
"defaultConstraints": map[string]any{"maxHeight": 180},
"constraints": map[string]any{
"alice-v0": map[string]any{"maxHeight": 720}, // alice in HD
},
})When there are already participants with video in the room, Jicofo sends the offer in
Plan B format (multiple SSRCs in a single m=video). pion defaults to Unified Plan and will fail.
neg := sess.Negotiator()
neg.PC = pc
err := neg.Accept(ctx)
if peer.IsPlanBError(err) {
// Recreate PC with Plan B semantics
pc.Close()
cfg := sess.IceConfig()
cfg.SDPSemantics = webrtc.SDPSemanticsPlanB
pc, _ = webrtc.NewPeerConnection(cfg)
// ... add transceivers/tracks again ...
neg = sess.Negotiator()
neg.PC = pc
neg.Accept(ctx)
}CLI -media handles this automatically.
Jicofo sometimes switches to another bridge (session-terminate reason="moving").
Session.WaitJingleReinitiate(ctx) blocks until the next session-initiate:
for {
pc, _ := webrtc.NewPeerConnection(sess.IceConfig())
// … add tracks/transceivers …
neg := sess.Negotiator()
neg.PC = pc
neg.Accept(ctx)
// Wait until pc.OnConnectionStateChange hits Failed/Closed
waitForFailed(pc)
pc.Close()
neg.Terminate("success")
if _, err := sess.WaitJingleReinitiate(ctx); err != nil { return }
// loop — next session-initiate
}CLI -media does this automatically.
If you add a track after session-accept:
pc.AddTrack(newTrack)
sdp := pc.LocalDescription().SDP // pion regenerates with new SSRC
neg.SendSourceAddFromSDP(sdp) // → <jingle action="source-add"> to JicofoIf the track is added before Accept — it's included in session-accept SDP automatically, source-add is not needed (otherwise Jicofo returns SSRC is already used).
xc := sess.LowLevel() // *xmpp.Conn
xc.Send(`<message …>…</message>`) // any stanza with xmlns="jabber:client"
xc.SendJingle(to, "transport-info", sid, initiator, innerXML)
id := xc.NextID() // monotonic id for IQ
stanza := xc.LastJingleStanza() // raw <iq><jingle action="session-initiate"…/></iq>go build -o jcli ./cmd/cli| Mode | Description |
|---|---|
| (no flag) | Wait for Jingle and output JSON with SDP/ICE/SSRC/colibriWS |
-chat |
MUC chat: stdin → groupchat. Commands: /raise, /lower, /quit |
-dc |
Bridge channel: stdin (text) → broadcast EndpointMessage{text:line} |
-dc-raw |
Bridge channel raw: pipe raw bytes between two CLIs via JVB |
-media |
pion + session-accept + reconnect loop. Receives RTP from other tracks |
-media -send-video |
same + sendonly VP8 track (dummy keyframe loop) with auto SSRC announcement |
-bench |
colibri-ws throughput benchmark (-bench-size, -bench-secs) |
# chat
./jcli -host meet.example.com -room myroom -nick alice -chat
# pipe raw bytes between two CLIs via JVB
./jcli -host meet.example.com -room myroom -nick alice -dc-raw <input.bin
./jcli -host meet.example.com -room myroom -nick bob -dc-raw >output.bin
# receive + send video to room (needs ≥1 other participant in room)
./jcli -host meet.example.com -room myroom -nick mediabot -media -send-video
# bridge channel throughput benchmark
./jcli -host meet.example.com -room myroom -nick recv -bench -bench-secs 30 &
./jcli -host meet.example.com -room myroom -nick send -bench -bench-size 8192 -bench-secs 20
# common flags
-host Jitsi server
-room room name
-nick display name
-debug verbose XMPP/WS logging
-insecure skip TLS certificate verification (self-signed / expired certs)
-timeout 5m how long to wait for Jingle session-initiate- Go 1.21+
github.com/coder/websocketgithub.com/pion/webrtc/v4(for media)
git clone https://github.com/zarazaex69/j
cd j
go build ./...provider.Provideradapter for olcRTC (that's part of olcRTC, notj)vp8channel/seichannel/videochanneltransports (also olcRTC, we only provide a sendable VideoTrack)- TLS fingerprint Chrome / XHR telemetry for TSPU evasion — higher-level concern (utls, connection wrappers)
