← Back to Integrations with 3rd parties

Jitsi Meet

Add rtcstats-js to Jitsi Meet through the custom.js entry point, and disable Jitsi's own rtcstats fork.

Jitsi Meet is a popular open source video conferencing application. It builds its peer connections on top of the browser's global RTCPeerConnection, which is exactly what rtcstats-js monkey-patches. Patch once at startup and every conference Jitsi creates gets traced automatically.

The catch with Jitsi is that the main bundle is not something you want to fork just to add a few lines. To make this clean, we contributed a dedicated custom.js entry point to Jitsi Meet (jitsi-meet#17524). It gets bundled with the app, so you can import modules like @rtcstats/rtcstats-js from it without touching the core code or fighting merge conflicts on every rebase.

Disable Jitsi's built-in rtcstats first

Jitsi ships its own rtcstats module. It is a fork of an older version of rtcstats and its output is not compatible with our format, so you have to turn it off before wiring in rtcstats-js. If you leave it on you get two tracers fighting over the same statistics and a dump rtcstats.com cannot read.

Disable it in your config.js:

var config = {
  // ...
  analytics: {
    rtcstatsEnabled: false,
  },
  // ...
}

With that set, Jitsi's own module stays dormant and rtcstats-js is the only thing collecting data.

Before you start

You need:

  • A running rtcstats-server (local or deployed)
  • A Jitsi Meet deployment you can build, with the custom.js entry point from jitsi-meet#17524
  • A signed rtcstats token (mint it server-side for production)

For local testing you can generate a token and embed it in the WebSocket URL. See the JWT-based authorization docs.

Step 1: install rtcstats-js

Jitsi Meet does not depend on rtcstats-js, so you have to add it to the jitsi-meet project yourself. Run this from the root of your Jitsi Meet checkout before you build:

npm install @rtcstats/rtcstats-js

Without this manual install the import in custom.js has nothing to resolve and the build fails.

Step 2: wrap the globals in custom.js

custom.js runs at import time, before Jitsi creates any peer connection, which makes it the right place to patch the globals. Wrapping once here covers every conference for the lifetime of the page:

// This file allows implementing custom javascript. window.APP is available at this point.
import {wrapRTCStatsWithDefaultOptions} from '@rtcstats/rtcstats-js';

const trace = wrapRTCStatsWithDefaultOptions();

Step 3: open the connection when you join a conference

custom.js runs before window.APP.store exists (the Redux store is attached during React bootstrap), so wait for the store, then subscribe to it. Open the connection to rtcstats-server the first time a conference appears, and reset on leave so a rejoin re-fires:

// custom.js runs at import time, before window.APP.store exists (it's attached
// during React bootstrap in BaseApp). Wait for it before subscribing.
function whenStoreReady(cb) {
    if (window.APP && APP.store) {
        cb(APP.store);
    } else {
        setTimeout(() => whenStoreReady(cb), 200);
    }
}

whenStoreReady(store => {
    let current;

    store.subscribe(() => {
        const { conference } = store.getState()['features/base/conference'];

        if (conference && conference !== current) {
            current = conference;
            // ---- joined the room ----
            // conference.getName() -> room name; conference.myUserId() -> your id
            const local = store.getState()['features/base/participants'].local;

            // This needs to generate a rtcstats JWT as described below.
            console.log('rtcstats: joined as', local && local.name, '(' + conference.myUserId() + ')');
            trace.connect('ws://localhost:3000/' + window.location.pathname);

            // Reset on leave so the next room rejoin re-fires.
            // 'conference.left' === JitsiConferenceEvents.CONFERENCE_LEFT; use the
            // literal since window.JitsiMeetJS isn't reliably the full lib in-page.
            conference.on('conference.left', () => {
                current = undefined;
            });
        }
    });
});

One trace.connect call opens one WebSocket connection to rtcstats-server, which writes and processes the dump on its side. The conference.left handler clears current, so the next time you join, the subscription fires again and opens a fresh connection. The ws://localhost:8404/ URL above is the no-auth local shortcut. The next step replaces it with a signed, identity-carrying URL.

Step 4: tag dumps with the Jitsi user and conference

Out of the box every dump is anonymous. The rtcstats token can carry an rtcStats claim so each dump is associated with the right user and call. It has three fields:

  • user - a long-lived identifier for the user. Use your authenticated user id, or fall back to Jitsi's conference.myUserId().
  • conference - an identifier for the call. Use Jitsi's conference.getName().
  • session - a short-lived per-join identifier.

The token is signed, so identity has to be set when the token is minted server-side. Never put the JWT secret in custom.js: the browser can claim to be anyone, so the server decides who it is.

For local testing the bin/generate-token.js script in the rtcstats repo prints a ready-to-use URL, but it does not set the identity claims:

node bin/generate-token.js --url ws://localhost:8404/

For production, mint the token in your own backend from the room and user the page sends it. The payload mirrors the three fields above:

// backend, e.g. /rtcstats-token?conference=<room>&user=<id>
import jwt from 'jsonwebtoken';

const token = jwt.sign(
    {
        rtcStats: {
            user: req.query.user,
            conference: req.query.conference,
            session: crypto.randomUUID(),
        },
    },
    process.env.RTCSTATS_JWT_SECRET,
    { expiresIn: '10m' },
);
// respond with { url: `wss://your-rtcstats-server/?rtcstats-token=${token}` }

Then fetch that URL at join time and connect with it. Replace the trace.connect(...) line in Step 3 with a call to this helper:

async function connectWithToken(conference, local) {
    const params = new URLSearchParams({
        conference: conference.getName(),
        user: (local && local.id) || conference.myUserId(),
    });
    // Ask your backend to mint a signed token for this user and conference.
    // The backend holds the JWT secret and returns the full WebSocket URL.
    const { url } = await fetch('/rtcstats-token?' + params).then(r => r.json());
    trace.connect(url);
}
// ...
console.log('rtcstats: joined as', local && local.name, '(' + conference.myUserId() + ')');
connectWithToken(conference, local);

Now each dump lands on rtcstats.com tagged with the Jitsi conference name and the user who produced it.

Verify it works

With rtcstats-server running, join a Jitsi conference. The server emits one log line on connect and another on disconnect when you leave:

Accepted new connection with uuid 93a750f8-223a-4c64-a39f-4c01f2731ae3
Connection with uuid 93a750f8-223a-4c64-a39f-4c01f2731ae3 disconnected, starting to process data

That pair confirms the connection opened and the dump was processed. Your session data is now flowing to rtcstats-server.

NOTE: Can't get this to work? Need help? Contact us

Was this page helpful?