Add stats reporting
This commit is contained in:
parent
a231154bd7
commit
56d1c9fa33
3 changed files with 321 additions and 5 deletions
|
|
@ -188,6 +188,7 @@ export class ConferenceCallManager extends EventEmitter {
|
||||||
const userId = call.opponentMember.userId;
|
const userId = call.opponentMember.userId;
|
||||||
this._addCall(call, userId);
|
this._addCall(call, userId);
|
||||||
call.answer();
|
call.answer();
|
||||||
|
this._observePeerConnection(call);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pendingCalls = [];
|
this.pendingCalls = [];
|
||||||
|
|
@ -199,7 +200,10 @@ export class ConferenceCallManager extends EventEmitter {
|
||||||
const roomId = event.getRoomId();
|
const roomId = event.getRoomId();
|
||||||
const type = event.getType();
|
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 sender = event.getSender();
|
||||||
const { call_id } = event.getContent();
|
const { call_id } = event.getContent();
|
||||||
|
|
||||||
|
|
@ -333,7 +337,298 @@ export class ConferenceCallManager extends EventEmitter {
|
||||||
|
|
||||||
const call = this.client.createCall(this.roomId, userId);
|
const call = this.client.createCall(this.roomId, userId);
|
||||||
this._addCall(call, 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) => {
|
_onIncomingCall = (call) => {
|
||||||
|
|
@ -376,6 +671,7 @@ export class ConferenceCallManager extends EventEmitter {
|
||||||
const userId = call.opponentMember.userId;
|
const userId = call.opponentMember.userId;
|
||||||
this._addCall(call, userId);
|
this._addCall(call, userId);
|
||||||
call.answer();
|
call.answer();
|
||||||
|
this._observePeerConnection(call);
|
||||||
};
|
};
|
||||||
|
|
||||||
_addCall(call, userId) {
|
_addCall(call, userId) {
|
||||||
|
|
@ -485,6 +781,7 @@ export class ConferenceCallManager extends EventEmitter {
|
||||||
);
|
);
|
||||||
|
|
||||||
remoteParticipant.call = newCall;
|
remoteParticipant.call = newCall;
|
||||||
|
this._observePeerConnection(newCall);
|
||||||
|
|
||||||
newCall.on("feeds_changed", () => this._onCallFeedsChanged(newCall));
|
newCall.on("feeds_changed", () => this._onCallFeedsChanged(newCall));
|
||||||
newCall.on("hangup", () => this._onCallHangup(newCall));
|
newCall.on("hangup", () => this._onCallHangup(newCall));
|
||||||
|
|
|
||||||
|
|
@ -155,19 +155,33 @@ function EventContainer({ title, state, events, ...rest }) {
|
||||||
function EventItem({ event, showCallId, showSender, onSelect }) {
|
function EventItem({ event, showCallId, showSender, onSelect }) {
|
||||||
const type = event.getType();
|
const type = event.getType();
|
||||||
const sender = event.getSender();
|
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 (
|
return (
|
||||||
<div className={styles.event} onClick={() => onSelect(event)}>
|
<div className={styles.event} onClick={() => onSelect(event)}>
|
||||||
{showSender && sender && (
|
{showSender && sender && (
|
||||||
<UserId className={styles.eventDetails} userId={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 && (
|
{showCallId && call_id && (
|
||||||
<CallId className={styles.eventDetails} callId={call_id} />
|
<CallId className={styles.eventDetails} callId={call_id} />
|
||||||
)}
|
)}
|
||||||
{invitee && <UserId className={styles.eventDetails} userId={invitee} />}
|
{invitee && <UserId className={styles.eventDetails} userId={invitee} />}
|
||||||
{reason && <span className={styles.eventDetails}>{reason}</span>}
|
{reason && <span className={styles.eventDetails}>{reason}</span>}
|
||||||
|
{eventType && <span className={styles.eventDetails}>{eventType}</span>}
|
||||||
|
{eventValue && <span className={styles.eventDetails}>{eventValue}</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background-color: #555;
|
background-color: #555;
|
||||||
min-width: 320px;
|
min-width: 512px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.userId {
|
.userId {
|
||||||
|
|
@ -64,12 +64,17 @@
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
padding: 4px 12px;
|
padding: 4px 12px;
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.event:nth-child(even) {
|
.event:nth-child(even) {
|
||||||
background-color: #444;
|
background-color: #444;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.event:hover {
|
||||||
|
background-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
.event > * {
|
.event > * {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue