Skip to main content

XMPP Signaling

lib-jitsi-meet uses XMPP with the Jingle extension (XEP-0166) for WebRTC signaling. The library implements this using Strophe.js with custom plugins.

Architecture Overview

The XMPP signaling layer consists of:
  • Strophe.js - Core XMPP client library (Jitsi fork)
  • JingleConnectionPlugin - Handles Jingle session management
  • JingleSessionPC - Manages individual peer connections
  • Strophe Plugins - Protocol extensions (disco, jingle, etc.)
  • SignalingLayer - Abstraction for protocol details
Source: CLAUDE.md:94-99

Jingle Protocol

Jingle (XEP-0166) is an XMPP extension for establishing peer-to-peer media sessions.

Session Lifecycle

1. Session Initiate
case 'session-initiate': {
    logger.info('(TIME) received session-initiate:\t', now);
    
    const pcConfig = isP2P ? this.p2pIceConfig : this.jvbIceConfig;
    
    sess = new JingleSessionPC(
        sid,                          // Session ID
        iq.getAttribute('to'),        // Local JID
        fromJid,                      // Remote JID
        this.connection,              // XMPP connection
        this.mediaConstraints,        // Media constraints
        cloneDeep(pcConfig),          // ICE config
        isP2P,                        // P2P or JVB
        false                         // Not initiator
    );
    
    this.sessions[sess.sid] = sess;
    this.eventEmitter.emit(XMPPEvents.CALL_INCOMING, sess, jingleElement, now);
    break;
}
Source: modules/xmpp/strophe.jingle.js:181-200 2. Session Accept
case 'session-accept': {
    const ssrcs = [];
    
    // Extract SSRCs from session-accept (P2P)
    findAll(iq, 'jingle>content').forEach(content => {
        const ssrc = getAttribute(findFirst(content, 'description'), 'ssrc');
        ssrc && ssrcs.push(ssrc);
    });
    
    logger.debug(`Received ${action} from ${fromJid} with ssrcs=${ssrcs}`);
    this.eventEmitter.emit(XMPPEvents.CALL_ACCEPTED, sess, jingleElement);
    break;
}
Source: modules/xmpp/strophe.jingle.js:202-214 3. Transport Info ICE candidates are exchanged via transport-info actions:
case 'transport-info': {
    const candidates = _parseIceCandidates(
        findFirst(iq, 'jingle>content>transport')
    );
    
    logger.debug(`Received ${action} from ${fromJid} for candidates=${candidates.join(', ')}`);
    this.eventEmitter.emit(XMPPEvents.TRANSPORT_INFO, sess, jingleElement);
    break;
}
Source: modules/xmpp/strophe.jingle.js:221-226 4. Session Terminate
case 'session-terminate': {
    logger.info('terminating...', sess.sid);
    let reasonCondition = null;
    let reasonText = null;
    
    const reasonElement = findFirst(iq, ':scope>jingle>reason');
    
    if (reasonElement) {
        const firstReasonChild = reasonElement.children?.length > 0 
            ? reasonElement.children[0] : undefined;
        reasonCondition = firstReasonChild ? firstReasonChild.tagName : null;
        reasonText = getText(findFirst(iq, ':scope>jingle>reason>text'));
    }
    
    logger.debug(`Received ${action} from ${fromJid} disconnect reason=${reasonText}`);
    this.terminate(sess.sid, reasonCondition, reasonText);
    this.eventEmitter.emit(XMPPEvents.CALL_ENDED, sess, reasonCondition, reasonText);
    break;
}
Source: modules/xmpp/strophe.jingle.js:228-245

Source Signaling

Source Add
case 'source-add':
    sess.addRemoteStream(findAll(iq, ':scope>jingle>content'));
    break;
Source: modules/xmpp/strophe.jingle.js:250-252 Source Remove
case 'source-remove':
    sess.removeRemoteStream(findAll(iq, ':scope>jingle>content'));
    break;
Source: modules/xmpp/strophe.jingle.js:253-255

P2P vs JVB Detection

lib-jitsi-meet distinguishes P2P from JVB connections by checking the resource:
const isP2P = Strophe.getResourceFromJid(fromJid) !== 'focus';
Jicofo (the JVB focus component) always has the resource “focus”, while P2P connections use participant resources. Source: modules/xmpp/strophe.jingle.js:152

JSON Message Encoding

For large conferences, lib-jitsi-meet uses JSON encoding to compress source information:
const jsonMessages = findAll(iq, 'jingle>json-message');

if (jsonMessages?.length) {
    let audioVideoSsrcs;
    
    logger.info(`Found a JSON-encoded element in ${action}, translating to standard Jingle.`);
    for (let i = 0; i < jsonMessages.length; i++) {
        // Translate JSON to standard Jingle source elements
        audioVideoSsrcs = expandSourcesFromJson(iq, jsonMessages[i]);
    }
    
    if (audioVideoSsrcs?.size) {
        const logMessage = [];
        for (const endpoint of audioVideoSsrcs.keys()) {
            logMessage.push(`${endpoint}:[${audioVideoSsrcs.get(endpoint)}]`);
        }
        logger.debug(`Received ${action} from ${fromJid} with sources=${logMessage.join(', ')}`);
    }
}
Source: modules/xmpp/strophe.jingle.js:156-174

ICE Candidate Parsing

ICE candidates are extracted from transport elements:
function _parseIceCandidates(transport) {
    const candidates = findAll(transport, ':scope>candidate');
    const parseCandidates = [];
    
    // Extract candidate attributes
    candidates.forEach(candidate => {
        const attributes = candidate.attributes;
        const candidateAttrs = [];
        
        for (let i = 0; i < attributes.length; i++) {
            const attr = attributes[i];
            candidateAttrs.push(`${attr.name}: ${attr.value}`);
        }
        parseCandidates.push(candidateAttrs.join(' '));
    });
    
    return parseCandidates;
}
Source: modules/xmpp/strophe.jingle.js:26-44

Session Management

Creating P2P Sessions

newP2PJingleSession(me, peer) {
    const sess = new JingleSessionPC(
        RandomUtil.randomHexString(12),  // Random session ID
        me,                               // Local JID
        peer,                             // Remote JID
        this.connection,                  // XMPP connection
        this.mediaConstraints,            // Media constraints
        this.p2pIceConfig,                // P2P ICE config
        true,                             // Is P2P
        true                              // Is initiator
    );
    
    this.sessions[sess.sid] = sess;
    return sess;
}
Source: modules/xmpp/strophe.jingle.js:277-292

Session Validation

Session Not Found
if (!sess) {
    ack.attrs({ type: 'error' });
    ack.c('error', { type: 'cancel' })
        .c('item-not-found', {
            xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'
        })
        .up()
        .c('unknown-session', {
            xmlns: 'urn:xmpp:jingle:errors:1'
        });
    logger.warn(`invalid session id: ${sid}`);
    this.connection.send(ack);
    return true;
}
Source: modules/xmpp/strophe.jingle.js:99-114 JID Mismatch
if (fromJid !== sess.remoteJid) {
    logger.warn('jid mismatch for session id', sid, sess.remoteJid, iq);
    ack.attrs({ type: 'error' });
    ack.c('error', { type: 'cancel' })
        .c('item-not-found', {
            xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'
        })
        .up()
        .c('unknown-session', {
            xmlns: 'urn:xmpp:jingle:errors:1'
        });
    this.connection.send(ack);
    return true;
}
Source: modules/xmpp/strophe.jingle.js:117-132

Strophe.js Integration

lib-jitsi-meet uses a custom fork of Strophe.js with Jitsi-specific modifications.

Connection Plugin Pattern

export default class JingleConnectionPlugin extends ConnectionPlugin {
    constructor(xmpp, eventEmitter, iceConfig) {
        super();
        this.xmpp = xmpp;
        this.eventEmitter = eventEmitter;
        this.sessions = {};
        this.jvbIceConfig = iceConfig.jvb;
        this.p2pIceConfig = iceConfig.p2p;
        this.mediaConstraints = {
            offerToReceiveAudio: true,
            offerToReceiveVideo: true
        };
    }
    
    init(connection) {
        super.init(connection);
        this.connection.addHandler(
            this.onJingle.bind(this),
            'urn:xmpp:jingle:1',  // Namespace
            'iq',                  // Stanza type
            'set',                 // IQ type
            null,                  // ID
            null                   // From
        );
    }
}
Source: modules/xmpp/strophe.jingle.js:49-78

IQ Acknowledgment

All Jingle IQs must be acknowledged:
const ack = $iq({ 
    id: iq.getAttribute('id'),
    to: fromJid,
    type: 'result'
});

// ... process the Jingle action ...

this.connection.send(ack);
return true;  // Keep handler
Source: modules/xmpp/strophe.jingle.js:91-94, 265-267

Media Constraints

Default media constraints for Jingle sessions:
this.mediaConstraints = {
    offerToReceiveAudio: true,
    offerToReceiveVideo: true
};
Source: modules/xmpp/strophe.jingle.js:64-67

ICE Configuration

Separate ICE configurations for JVB and P2P:
constructor(xmpp, eventEmitter, iceConfig) {
    // ...
    this.jvbIceConfig = iceConfig.jvb;  // JVB connection config
    this.p2pIceConfig = iceConfig.p2p;  // P2P connection config
}
ICE config is cloned before use to prevent mutation:
const pcConfig = isP2P ? this.p2pIceConfig : this.jvbIceConfig;
sess = new JingleSessionPC(
    // ...
    cloneDeep(pcConfig),  // Deep clone prevents side effects
    // ...
);
Source: modules/xmpp/strophe.jingle.js:62-63, 194

Strophe Plugins

lib-jitsi-meet includes several Strophe plugins for XMPP protocol extensions:

Available Plugins

  • strophe.jingle.js - Jingle session management (XEP-0166)
  • strophe.disco.js - Service discovery (XEP-0030)
  • moderator.js - Moderator functionality
  • Lobby.js - Lobby/waiting room support
  • BreakoutRooms.js - Breakout room management
Source: modules/xmpp/ directory listing

Plugin Architecture

All plugins extend ConnectionPlugin:
import ConnectionPlugin from './ConnectionPlugin';

export default class MyPlugin extends ConnectionPlugin {
    init(connection) {
        super.init(connection);
        // Register handlers, etc.
    }
}
Source: modules/xmpp/strophe.jingle.js:9, 49

XMPP Events

The Jingle plugin emits events via the EventEmitter:
import { XMPPEvents } from '../../service/xmpp/XMPPEvents';

// Session events
this.eventEmitter.emit(XMPPEvents.CALL_INCOMING, sess, jingleElement, now);
this.eventEmitter.emit(XMPPEvents.CALL_ACCEPTED, sess, jingleElement);
this.eventEmitter.emit(XMPPEvents.CALL_ENDED, sess, reasonCondition, reasonText);
this.eventEmitter.emit(XMPPEvents.TRANSPORT_INFO, sess, jingleElement);
Source: modules/xmpp/strophe.jingle.js:5, 199, 213, 225, 244
Session ID CollisionIf a session-initiate is received with an existing session ID, lib-jitsi-meet rejects it:
if (sess !== undefined) {
    ack.attrs({ type: 'error' });
    ack.c('error', { type: 'cancel' })
        .c('service-unavailable', {
            xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'
        });
    logger.warn('duplicate session id', sid, iq);
    this.connection.send(ack);
    return true;
}
This prevents out-of-order message issues.Source: modules/xmpp/strophe.jingle.js:133-146

XML Utilities

lib-jitsi-meet provides XML helpers for parsing Jingle IQs:
import { findAll, findFirst, getAttribute, getText } from '../util/XMLUtils';

// Find elements
const jingleElement = findFirst(iq, 'jingle');
const contents = findAll(iq, 'jingle>content');

// Get attributes
const sid = getAttribute(jingleElement, 'sid');
const action = getAttribute(jingleElement, 'action');

// Get text content
const reasonText = getText(findFirst(iq, ':scope>jingle>reason>text'));
Source: modules/xmpp/strophe.jingle.js:7, 85-88, 239

XMPP Connection Management

The signaling layer is part of the broader XMPP architecture: Module Structure
  • ChatRoom.js - Multi-user chat room with presence
  • JingleSessionPC.js - Jingle protocol for media negotiation
  • SignalingLayerImpl.js - Abstraction layer for signaling
  • Strophe plugins - Protocol extensions
Transport Support
  • BOSH (Bidirectional-streams Over Synchronous HTTP)
  • WebSocket
Source: CLAUDE.md:94-99

Performance Considerations

Session Timing
const now = window.performance.now();
logger.info('(TIME) received session-initiate:\t', now);
lib-jitsi-meet tracks high-resolution timestamps for session events to measure signaling latency. Source: modules/xmpp/strophe.jingle.js:147, 182 P2P Optimization For P2P sessions, detailed logging is conditional:
isP2P && logger.debug(`Received ${action} from ${fromJid}`);
Source: modules/xmpp/strophe.jingle.js:184

Next Steps

Build docs developers (and LLMs) love