Overview
lib-jitsi-meet provides comprehensive statistics through:- Statistics Module: Collects and reports conference metrics
- RTCStats: Detailed WebRTC statistics via rtcstats-server
- Local Stats: Audio levels and track statistics
- RTP Stats: Peer connection statistics
- Speaker Stats: Track speaking time and dominant speaker
- Analytics Events: Send custom analytics events
Initializing Statistics
Configure statistics on init
JitsiMeetJS.init({
// Disable audio level stats
disableAudioLevels: false,
// Audio level collection interval (ms)
audioLevelsInterval: 200,
// Peer connection stats interval (ms)
pcStatsInterval: 10000,
// Disable third-party analytics
disableThirdPartyRequests: false
});
Initialize conference statistics
const conferenceOptions = {
// Statistics configuration
statisticsId: 'user-123',
statisticsDisplayName: 'John Doe',
confID: 'room-name-12345',
// Application name for analytics
applicationName: 'MyApp',
// Disable local stats (for testing)
disableLocalStats: false,
disableLocalStatsBroadcast: false,
// Average RTP stats sample size
avgRtpStatsN: 15,
// RTCStats configuration
analytics: {
rtcstatsEnabled: true,
rtcstatsEndpoint: 'wss://rtcstats-server.com',
rtcstatsPollInterval: 10000,
rtcstatsSendSdp: true
}
};
const conference = connection.initJitsiConference('room', conferenceOptions);
Audio Levels
Local Audio Levels
// Audio levels are collected automatically for local tracks
localAudioTrack.addEventListener(
JitsiMeetJS.events.track.TRACK_AUDIO_LEVEL_CHANGED,
(audioLevel) => {
// audioLevel: 0.0 to 1.0
console.log('Local audio level:', audioLevel);
updateAudioMeter(audioLevel);
}
);
// Disable audio levels if needed
const statisticsOptions = {
disableAudioLevels: true
};
Remote Audio Levels
// Listen for remote participant audio levels
conference.on(
JitsiMeetJS.events.conference.TRACK_AUDIO_LEVEL_CHANGED,
(participantId, audioLevel) => {
console.log(`Audio level for ${participantId}:`, audioLevel);
// Update UI
const participant = conference.getParticipantById(participantId);
if (participant) {
updateParticipantAudioMeter(participantId, audioLevel);
}
}
);
Connection Statistics
Monitor Connection Quality
// Overall connection quality
conference.on(
JitsiMeetJS.events.conference.CONNECTION_STATS,
(stats) => {
console.log('Connection stats:', {
bandwidth: stats.bandwidth,
bitrate: stats.bitrate,
packetLoss: stats.packetLoss,
connectionQuality: stats.connectionQuality, // 0-100
e2eRtt: stats.e2eRtt,
jvbRtt: stats.jvbRtt,
maxEnabledResolution: stats.maxEnabledResolution,
region: stats.region,
transport: stats.transport
});
// Update UI based on quality
if (stats.connectionQuality < 30) {
showPoorConnectionWarning();
}
}
);
Per-Participant Statistics
// Get statistics for specific participant
const participantStats = conference.getParticipantStats(participantId);
if (participantStats) {
console.log('Participant stats:', {
audioLevel: participantStats.audioLevel,
bitrate: participantStats.bitrate,
codec: participantStats.codec,
framerate: participantStats.framerate,
packetLoss: participantStats.packetLoss,
resolution: participantStats.resolution
});
}
RTP Statistics
Get RTP Stats
// RTP stats are collected automatically
conference.on(
JitsiMeetJS.events.conference.RTP_STATS,
(stats) => {
console.log('RTP stats:', stats);
// Stats include:
// - Bitrate (upload/download)
// - Packet loss
// - Jitter
// - RTT (round-trip time)
// - Frame rate
// - Resolution
}
);
Average RTP Stats
// Get average stats over time
conference.on(
JitsiMeetJS.events.conference.BEFORE_STATISTICS_DISPOSED,
() => {
const avgStats = conference.avgRtpStatsReporter?.getStats();
if (avgStats) {
console.log('Average stats:', {
bitrate: avgStats.bitrate,
packetLoss: avgStats.packetLoss,
framerate: avgStats.framerate,
jitter: avgStats.jitter,
rtt: avgStats.rtt
});
}
}
);
Speaker Statistics
Track Speaking Time
// Speaker stats are collected automatically
conference.on(
JitsiMeetJS.events.conference.SPEAKER_STATS_RECEIVED,
(stats) => {
// stats is an object with participant IDs as keys
Object.keys(stats).forEach(participantId => {
const speakerStats = stats[participantId];
console.log('Speaker stats:', {
participantId,
displayName: speakerStats.displayName,
totalDominantSpeakerTime: speakerStats.totalDominantSpeakerTime,
faceLandmarks: speakerStats.faceLandmarks
});
});
// Display speaking time in UI
displaySpeakerStats(stats);
}
);
Get Speaker Stats
// Get speaker stats from conference
const speakerStats = conference.getSpeakerStats();
if (speakerStats) {
const participants = Object.keys(speakerStats);
participants.forEach(participantId => {
const stats = speakerStats[participantId];
const timeInMs = stats.totalDominantSpeakerTime;
const timeInSeconds = Math.floor(timeInMs / 1000);
console.log(`${stats.displayName} spoke for ${timeInSeconds}s`);
});
}
RTCStats Integration
Enable RTCStats
const conferenceOptions = {
analytics: {
// Enable RTCStats
rtcstatsEnabled: true,
// RTCStats server WebSocket URL
rtcstatsEndpoint: 'wss://rtcstats.example.com',
// Stats collection interval (ms)
rtcstatsPollInterval: 10000,
// Send SDP in stats
rtcstatsSendSdp: true
}
};
RTCStats Events
// Listen for RTCStats events
JitsiMeetJS.analytics.on(
'rtcstats.trace',
(event) => {
console.log('RTCStats event:', event);
}
);
// Monitor connection to RTCStats server
JitsiMeetJS.analytics.on(
'rtcstats.disconnect',
() => {
console.warn('Disconnected from RTCStats server');
}
);
Send Custom RTCStats Data
// Send custom stats entry
JitsiMeetJS.analytics.sendEvent('custom_event', {
category: 'ui',
action: 'button_click',
label: 'mute_audio',
value: 1
});
Analytics Events
Send Analytics Events
// Send analytics event
JitsiMeetJS.analytics.sendEvent('conference.joined', {
roomName: 'room-123',
participants: 5,
isAudioMuted: false,
isVideoMuted: false
});
// Common events
JitsiMeetJS.analytics.sendEvent('track.added', {
mediaType: 'video',
videoType: 'camera'
});
JitsiMeetJS.analytics.sendEvent('connection.quality', {
quality: 'good',
score: 85
});
Add Permanent Properties
// Add properties to all events
JitsiMeetJS.analytics.addPermanentProperties({
appVersion: '1.0.0',
platform: 'web',
deploymentRegion: 'us-west',
userId: 'user-123'
});
Built-in Analytics Events
lib-jitsi-meet automatically sends these events:// Connection events
'conference.joined'
'conference.left'
'connection.established'
'connection.failed'
'connection.disconnected'
// Media events
'track.added'
'track.removed'
'track.muted'
'track.unmuted'
// Quality events
'encode.time.stats'
'quality.limitation'
'bandwidth.allocation'
// P2P events
'p2p.established'
'p2p.failed'
'p2p.switched'
Performance Monitoring
Monitor Encode Performance
// Track video encoding performance
conference.on(
JitsiMeetJS.events.conference.ENCODE_TIME_STATS_RECEIVED,
(tpc, stats) => {
// stats is a Map of SSRC -> encode stats
stats.forEach((stat, ssrc) => {
console.log('Encode stats:', {
ssrc,
codec: stat.codec,
resolution: stat.resolution,
encodeTime: stat.encodeTime,
qualityLimitationReason: stat.qualityLimitationReason,
timestamp: stat.timestamp
});
// Warn if CPU limited
if (stat.qualityLimitationReason === 'cpu') {
console.warn('Video quality limited by CPU');
}
});
}
);
Track Connection Times
// Get connection timing information
const connectionTimes = connection.getConnectionTimes();
console.log('Connection times:', {
connecting: connectionTimes.connecting,
connected: connectionTimes.connected,
duration: connectionTimes.connected - connectionTimes.connecting
});
const conferenceTimes = conference.getConnectionTimes();
console.log('Conference times:', {
'muc.joined': conferenceTimes['muc.joined'],
'session.initiate': conferenceTimes['session.initiate'],
'ice.state.connected': conferenceTimes['ice.state.connected']
});
Monitor Packet Loss
// Track packet loss
let packetLossHistory = [];
conference.on(
JitsiMeetJS.events.conference.CONNECTION_STATS,
(stats) => {
const packetLoss = stats.packetLoss;
packetLossHistory.push({
timestamp: Date.now(),
loss: packetLoss
});
// Keep last 60 samples
if (packetLossHistory.length > 60) {
packetLossHistory.shift();
}
// Calculate average
const avgLoss = packetLossHistory.reduce((sum, p) => sum + p.loss, 0)
/ packetLossHistory.length;
if (avgLoss > 5) {
console.warn('High packet loss detected:', avgLoss.toFixed(2) + '%');
}
}
);
Local Statistics Collector
Audio Input Statistics
// Local stats are automatically collected for audio tracks
// Access via Statistics class
const localStats = JitsiMeetJS.statistics.localStats;
localStats.forEach(stat => {
console.log('Local stat:', {
stream: stat.stream,
interval: stat.interval
});
});
Custom Local Stats
// Start collecting stats for a track
JitsiMeetJS.statistics.startLocalStats(
localAudioTrack,
(audioLevel) => {
console.log('Audio level:', audioLevel);
// Update visualizer
updateAudioVisualizer(audioLevel);
}
);
// Stop collecting stats
JitsiMeetJS.statistics.stopLocalStats(localAudioTrack);
Complete Statistics Dashboard
class StatisticsMonitor {
constructor(conference) {
this.conference = conference;
this.stats = {
connection: {},
participants: new Map(),
speaker: {},
performance: {
cpu: [],
bandwidth: [],
packetLoss: []
}
};
this.setupListeners();
}
setupListeners() {
// Connection stats
this.conference.on(
JitsiMeetJS.events.conference.CONNECTION_STATS,
(stats) => this.onConnectionStats(stats)
);
// Speaker stats
this.conference.on(
JitsiMeetJS.events.conference.SPEAKER_STATS_RECEIVED,
(stats) => this.onSpeakerStats(stats)
);
// Encode stats
this.conference.on(
JitsiMeetJS.events.conference.ENCODE_TIME_STATS_RECEIVED,
(tpc, stats) => this.onEncodeStats(tpc, stats)
);
// Audio levels
this.conference.on(
JitsiMeetJS.events.conference.TRACK_AUDIO_LEVEL_CHANGED,
(participantId, level) => this.onAudioLevel(participantId, level)
);
}
onConnectionStats(stats) {
this.stats.connection = {
quality: stats.connectionQuality,
bitrate: stats.bitrate,
packetLoss: stats.packetLoss,
bandwidth: stats.bandwidth,
e2eRtt: stats.e2eRtt,
jvbRtt: stats.jvbRtt,
timestamp: Date.now()
};
// Track packet loss history
this.stats.performance.packetLoss.push({
timestamp: Date.now(),
value: stats.packetLoss
});
// Keep last 100 samples
if (this.stats.performance.packetLoss.length > 100) {
this.stats.performance.packetLoss.shift();
}
this.updateDashboard();
}
onSpeakerStats(stats) {
this.stats.speaker = stats;
this.updateSpeakerDisplay();
}
onEncodeStats(tpc, stats) {
stats.forEach((stat, ssrc) => {
// Track CPU limitation
if (stat.qualityLimitationReason === 'cpu') {
this.stats.performance.cpu.push({
timestamp: Date.now(),
encodeTime: stat.encodeTime,
resolution: stat.resolution
});
}
});
this.updatePerformanceDisplay();
}
onAudioLevel(participantId, level) {
if (!this.stats.participants.has(participantId)) {
this.stats.participants.set(participantId, {});
}
const participantStats = this.stats.participants.get(participantId);
participantStats.audioLevel = level;
participantStats.lastAudioTime = Date.now();
this.updateParticipantDisplay(participantId);
}
getConnectionQuality() {
return this.stats.connection.quality || 0;
}
getAveragePacketLoss() {
const samples = this.stats.performance.packetLoss;
if (samples.length === 0) return 0;
const sum = samples.reduce((acc, s) => acc + s.value, 0);
return sum / samples.length;
}
getTopSpeakers(count = 3) {
const speakers = Object.entries(this.stats.speaker)
.map(([id, stats]) => ({
id,
name: stats.displayName,
time: stats.totalDominantSpeakerTime
}))
.sort((a, b) => b.time - a.time)
.slice(0, count);
return speakers;
}
getStatsSummary() {
return {
connection: {
quality: this.getConnectionQuality(),
packetLoss: this.getAveragePacketLoss(),
bitrate: this.stats.connection.bitrate,
rtt: this.stats.connection.e2eRtt
},
participants: {
total: this.stats.participants.size,
speaking: Array.from(this.stats.participants.values())
.filter(p => p.audioLevel > 0.1).length
},
topSpeakers: this.getTopSpeakers()
};
}
updateDashboard() {
const summary = this.getStatsSummary();
console.log('Stats summary:', summary);
// Update UI elements
}
updateSpeakerDisplay() {
// Update speaker stats UI
}
updatePerformanceDisplay() {
// Update performance charts
}
updateParticipantDisplay(participantId) {
// Update participant stats UI
}
}
// Usage
const statsMonitor = new StatisticsMonitor(conference);
// Get summary
const summary = statsMonitor.getStatsSummary();
console.log('Current stats:', summary);
// Get top speakers
const topSpeakers = statsMonitor.getTopSpeakers(5);
console.log('Top speakers:', topSpeakers);
Best Practices
Configure appropriate intervals
Configure appropriate intervals
Balance between data granularity and performance:
// Audio levels: 200ms (5 samples/second)
audioLevelsInterval: 200
// Peer connection stats: 10s
pcStatsInterval: 10000
// RTCStats: 10s
rtcstatsPollInterval: 10000
Monitor connection quality
Monitor connection quality
Track quality metrics and react to poor conditions:
if (stats.connectionQuality < 30) {
// Reduce quality
conference.setLastN(3);
conference.setSenderVideoConstraint(360);
}
Send meaningful analytics
Send meaningful analytics
Include context in analytics events:
JitsiMeetJS.analytics.sendEvent('feature.used', {
feature: 'screen-sharing',
duration: screenShareDuration,
participants: participantCount
});
Next Steps
Audio/Video Quality
Control media quality settings
Creating Connections
Learn about connection management