mirror of
https://github.com/vector-im/element-call.git
synced 2024-11-15 00:04:59 +08:00
Add stats reporting
This commit is contained in:
parent
a231154bd7
commit
56d1c9fa33
@ -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));
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user