Add stats reporting

This commit is contained in:
Robert Long 2021-08-03 15:05:29 -07:00
parent a231154bd7
commit 56d1c9fa33
3 changed files with 321 additions and 5 deletions

View File

@ -188,6 +188,7 @@ export class ConferenceCallManager extends EventEmitter {
const userId = call.opponentMember.userId;
this._addCall(call, userId);
call.answer();
this._observePeerConnection(call);
}
this.pendingCalls = [];
@ -199,7 +200,10 @@ export class ConferenceCallManager extends EventEmitter {
const roomId = event.getRoomId();
const type = event.getType();
if (roomId === this.roomId && type.startsWith("m.call.")) {
if (
roomId === this.roomId &&
(type.startsWith("m.call.") || type === "me.robertlong.call.info")
) {
const sender = event.getSender();
const { call_id } = event.getContent();
@ -333,7 +337,298 @@ export class ConferenceCallManager extends EventEmitter {
const call = this.client.createCall(this.roomId, userId);
this._addCall(call, userId);
call.placeVideoCall();
call.placeVideoCall().then(() => {
this._observePeerConnection(call);
});
}
_observePeerConnection(call) {
const peerConnection = call.peerConn;
if (!peerConnection) {
return;
}
const sendWebRTCInfoEvent = async (eventType) => {
const event = {
call_id: call.callId,
eventType,
iceConnectionState: peerConnection.iceConnectionState,
iceGatheringState: peerConnection.iceGatheringState,
signalingState: peerConnection.signalingState,
selectedCandidatePair: null,
localCandidate: null,
remoteCandidate: null,
};
// getStats doesn't support selectors in Firefox so get all stats by passing null.
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/getStats#browser_compatibility
const stats = await peerConnection.getStats(null);
const statsArr = Array.from(stats.values());
// Matrix doesn't support floats so we convert time in seconds to ms
function secToMs(time) {
if (time === undefined) {
return undefined;
}
return Math.round(time * 1000);
}
function processTransportStats(transportStats) {
if (!transportStats) {
return undefined;
}
return {
packetsSent: transportStats.packetsSent,
packetsReceived: transportStats.packetsReceived,
bytesSent: transportStats.bytesSent,
bytesReceived: transportStats.bytesReceived,
iceRole: transportStats.iceRole,
iceState: transportStats.iceState,
dtlsState: transportStats.dtlsState,
dtlsCipher: transportStats.dtlsCipher,
tlsVersion: transportStats.tlsVersion,
};
}
function processCandidateStats(candidateStats) {
if (!candidateStats) {
return undefined;
}
// TODO: Figure out how to normalize ip and address across browsers
// networkType property excluded for privacy reasons:
// https://www.w3.org/TR/webrtc-stats/#sotd
return {
protocol: candidateStats.protocol,
address: candidateStats.address || candidateStats.ip,
port: candidateStats.port,
url: candidateStats.url,
relayProtocol: candidateStats.relayProtocol,
};
}
function processCandidatePair(candidatePairStats) {
if (!candidatePairStats) {
return undefined;
}
const localCandidateStats = statsArr.find(
(stat) => stat.id === candidatePairStats.localCandidateId
);
event.localCandidate = processCandidateStats(localCandidateStats);
const remoteCandidateStats = statsArr.find(
(stat) => stat.id === candidatePairStats.remoteCandidateId
);
event.remoteCandidate = processCandidateStats(remoteCandidateStats);
const transportStats = statsArr.find(
(stat) => stat.id === candidatePairStats.transportId
);
event.transport = processTransportStats(transportStats);
return {
state: candidatePairStats.state,
bytesSent: candidatePairStats.bytesSent,
bytesReceived: candidatePairStats.bytesReceived,
requestsSent: candidatePairStats.requestsSent,
requestsReceived: candidatePairStats.requestsReceived,
responsesSent: candidatePairStats.responsesSent,
responsesReceived: candidatePairStats.responsesReceived,
currentRoundTripTime: secToMs(
candidatePairStats.currentRoundTripTime
),
totalRoundTripTime: secToMs(candidatePairStats.totalRoundTripTime),
};
}
// Firefox uses the deprecated "selected" property for the nominated ice candidate.
const selectedCandidatePair = statsArr.find(
(stat) =>
stat.type === "candidate-pair" && (stat.selected || stat.nominated)
);
event.selectedCandidatePair = processCandidatePair(selectedCandidatePair);
function processCodecStats(codecStats) {
if (!codecStats) {
return undefined;
}
// Payload type enums and MIME types listed here:
// https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml
return {
mimeType: codecStats.mimeType,
clockRate: codecStats.clockRate,
payloadType: codecStats.payloadType,
channels: codecStats.channels,
sdpFmtpLine: codecStats.sdpFmtpLine,
};
}
function processRTPStreamStats(rtpStreamStats) {
const codecStats = statsArr.find(
(stat) => stat.id === rtpStreamStats.codecId
);
const codec = processCodecStats(codecStats);
return {
kind: rtpStreamStats.kind,
codec,
};
}
function processInboundRTPStats(inboundRTPStats) {
const rtpStreamStats = processRTPStreamStats(inboundRTPStats);
return {
...rtpStreamStats,
decoderImplementation: inboundRTPStats.decoderImplementation,
bytesReceived: inboundRTPStats.bytesReceived,
packetsReceived: inboundRTPStats.packetsReceived,
packetsLost: inboundRTPStats.packetsLost,
jitter: secToMs(inboundRTPStats.jitter),
frameWidth: inboundRTPStats.frameWidth,
frameHeight: inboundRTPStats.frameHeight,
frameBitDepth: inboundRTPStats.frameBitDepth,
framesPerSecond:
inboundRTPStats.framesPerSecond &&
inboundRTPStats.framesPerSecond.toString(),
framesReceived: inboundRTPStats.framesReceived,
framesDecoded: inboundRTPStats.framesDecoded,
framesDropped: inboundRTPStats.framesDropped,
totalSamplesDecoded: inboundRTPStats.totalSamplesDecoded,
totalDecodeTime: secToMs(inboundRTPStats.totalDecodeTime),
totalProcessingDelay: secToMs(inboundRTPStats.totalProcessingDelay),
};
}
function processOutboundRTPStats(outboundRTPStats) {
const rtpStreamStats = processRTPStreamStats(outboundRTPStats);
return {
...rtpStreamStats,
encoderImplementation: outboundRTPStats.encoderImplementation,
bytesSent: outboundRTPStats.bytesSent,
packetsSent: outboundRTPStats.packetsSent,
frameWidth: outboundRTPStats.frameWidth,
frameHeight: outboundRTPStats.frameHeight,
frameBitDepth: outboundRTPStats.frameBitDepth,
framesPerSecond:
outboundRTPStats.framesPerSecond &&
outboundRTPStats.framesPerSecond.toString(),
framesSent: outboundRTPStats.framesSent,
framesEncoded: outboundRTPStats.framesEncoded,
qualityLimitationReason: outboundRTPStats.qualityLimitationReason,
qualityLimitationResolutionChanges:
outboundRTPStats.qualityLimitationResolutionChanges,
totalEncodeTime: secToMs(outboundRTPStats.totalEncodeTime),
totalPacketSendDelay: secToMs(outboundRTPStats.totalPacketSendDelay),
};
}
function processRemoteInboundRTPStats(remoteInboundRTPStats) {
const rtpStreamStats = processRTPStreamStats(remoteInboundRTPStats);
return {
...rtpStreamStats,
packetsReceived: remoteInboundRTPStats.packetsReceived,
packetsLost: remoteInboundRTPStats.packetsLost,
jitter: secToMs(remoteInboundRTPStats.jitter),
framesDropped: remoteInboundRTPStats.framesDropped,
roundTripTime: secToMs(remoteInboundRTPStats.roundTripTime),
totalRoundTripTime: secToMs(remoteInboundRTPStats.totalRoundTripTime),
fractionLost:
remoteInboundRTPStats.fractionLost !== undefined &&
remoteInboundRTPStats.fractionLost.toString(),
reportsReceived: remoteInboundRTPStats.reportsReceived,
roundTripTimeMeasurements:
remoteInboundRTPStats.roundTripTimeMeasurements,
};
}
function processRemoteOutboundRTPStats(remoteOutboundRTPStats) {
const rtpStreamStats = processRTPStreamStats(remoteOutboundRTPStats);
return {
...rtpStreamStats,
encoderImplementation: remoteOutboundRTPStats.encoderImplementation,
bytesSent: remoteOutboundRTPStats.bytesSent,
packetsSent: remoteOutboundRTPStats.packetsSent,
roundTripTime: secToMs(remoteOutboundRTPStats.roundTripTime),
totalRoundTripTime: secToMs(
remoteOutboundRTPStats.totalRoundTripTime
),
reportsSent: remoteOutboundRTPStats.reportsSent,
roundTripTimeMeasurements:
remoteOutboundRTPStats.roundTripTimeMeasurements,
};
}
event.inboundRTP = statsArr
.filter((stat) => stat.type === "inbound-rtp")
.map(processInboundRTPStats);
event.outboundRTP = statsArr
.filter((stat) => stat.type === "outbound-rtp")
.map(processOutboundRTPStats);
event.remoteInboundRTP = statsArr
.filter((stat) => stat.type === "remote-inbound-rtp")
.map(processRemoteInboundRTPStats);
event.remoteOutboundRTP = statsArr
.filter((stat) => stat.type === "remote-outbound-rtp")
.map(processRemoteOutboundRTPStats);
this.client.sendEvent(this.roomId, "me.robertlong.call.info", event);
};
let statsTimeout;
const sendStats = () => {
sendWebRTCInfoEvent("stats");
statsTimeout = setTimeout(sendStats, 30 * 1000);
};
setTimeout(sendStats, 30 * 1000);
peerConnection.addEventListener("iceconnectionstatechange", () => {
sendWebRTCInfoEvent("iceconnectionstatechange");
});
peerConnection.addEventListener("icegatheringstatechange", () => {
sendWebRTCInfoEvent("icegatheringstatechange");
});
peerConnection.addEventListener("negotiationneeded", () => {
sendWebRTCInfoEvent("negotiationneeded");
});
peerConnection.addEventListener("track", () => {
sendWebRTCInfoEvent("track");
});
// NOTE: Not available on Firefox
// https://bugzilla.mozilla.org/show_bug.cgi?id=1561441
peerConnection.addEventListener(
"icecandidateerror",
({ errorCode, url, errorText }) => {
this.client.sendEvent(this.roomId, "me.robertlong.call.ice_error", {
call_id: call.callId,
errorCode,
url,
errorText,
});
}
);
peerConnection.addEventListener("signalingstatechange", () => {
sendWebRTCInfoEvent("signalingstatechange");
if (peerConnection.signalingState === "closed") {
clearTimeout(statsTimeout);
}
});
}
_onIncomingCall = (call) => {
@ -376,6 +671,7 @@ export class ConferenceCallManager extends EventEmitter {
const userId = call.opponentMember.userId;
this._addCall(call, userId);
call.answer();
this._observePeerConnection(call);
};
_addCall(call, userId) {
@ -485,6 +781,7 @@ export class ConferenceCallManager extends EventEmitter {
);
remoteParticipant.call = newCall;
this._observePeerConnection(newCall);
newCall.on("feeds_changed", () => this._onCallFeedsChanged(newCall));
newCall.on("hangup", () => this._onCallHangup(newCall));

View File

@ -155,19 +155,33 @@ function EventContainer({ title, state, events, ...rest }) {
function EventItem({ event, showCallId, showSender, onSelect }) {
const type = event.getType();
const sender = event.getSender();
const { call_id, invitee, reason } = event.getContent();
const { call_id, invitee, reason, eventType, ...rest } = event.getContent();
let eventValue;
if (eventType === "icegatheringstatechange") {
eventValue = rest.iceGatheringState;
} else if (eventType === "iceconnectionstatechange") {
eventValue = rest.iceConnectionState;
} else if (eventType === "signalingstatechange") {
eventValue = rest.signalingState;
}
return (
<div className={styles.event} onClick={() => onSelect(event)}>
{showSender && sender && (
<UserId className={styles.eventDetails} userId={sender} />
)}
<span className={styles.eventType}>{type}</span>
<span className={styles.eventType}>
{type.replace("me.robertlong.", "x.")}
</span>
{showCallId && call_id && (
<CallId className={styles.eventDetails} callId={call_id} />
)}
{invitee && <UserId className={styles.eventDetails} userId={invitee} />}
{reason && <span className={styles.eventDetails}>{reason}</span>}
{eventType && <span className={styles.eventDetails}>{eventType}</span>}
{eventValue && <span className={styles.eventDetails}>{eventValue}</span>}
</div>
);
}

View File

@ -34,7 +34,7 @@
flex-direction: column;
flex: 1;
background-color: #555;
min-width: 320px;
min-width: 512px;
}
.userId {
@ -64,12 +64,17 @@
font-family: monospace;
padding: 4px 12px;
background-color: #333;
cursor: pointer;
}
.event:nth-child(even) {
background-color: #444;
}
.event:hover {
background-color: #555;
}
.event > * {
margin-right: 4px;
}