Move conference call debugger to its own class
This commit is contained in:
parent
ed427572e7
commit
274b3336c9
3 changed files with 406 additions and 383 deletions
391
src/ConferenceCallDebugger.js
Normal file
391
src/ConferenceCallDebugger.js
Normal file
|
@ -0,0 +1,391 @@
|
||||||
|
import EventEmitter from "events";
|
||||||
|
|
||||||
|
export class ConferenceCallDebugger extends EventEmitter {
|
||||||
|
constructor(manager) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.manager = manager;
|
||||||
|
|
||||||
|
this.debugState = {
|
||||||
|
users: new Map(),
|
||||||
|
calls: new Map(),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.manager.on("call", this._onCall);
|
||||||
|
this.manager.on("debugstate", this._onDebugStateChanged);
|
||||||
|
this.manager.client.on("event", this._onEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onEvent = (event) => {
|
||||||
|
const roomId = event.getRoomId();
|
||||||
|
const type = event.getType();
|
||||||
|
|
||||||
|
if (
|
||||||
|
roomId === this.manager.roomId &&
|
||||||
|
(type.startsWith("m.call.") || type === "me.robertlong.call.info")
|
||||||
|
) {
|
||||||
|
const sender = event.getSender();
|
||||||
|
const { call_id } = event.getContent();
|
||||||
|
|
||||||
|
if (call_id) {
|
||||||
|
if (this.debugState.calls.has(call_id)) {
|
||||||
|
const callState = this.debugState.calls.get(call_id);
|
||||||
|
callState.events.push(event);
|
||||||
|
} else {
|
||||||
|
this.debugState.calls.set(call_id, {
|
||||||
|
state: "unknown",
|
||||||
|
events: [event],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.debugState.users.has(sender)) {
|
||||||
|
const userState = this.debugState.users.get(sender);
|
||||||
|
userState.events.push(event);
|
||||||
|
} else {
|
||||||
|
this.debugState.users.set(sender, {
|
||||||
|
state: "unknown",
|
||||||
|
events: [event],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit("debug");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_onDebugStateChanged = (userId, callId, state) => {
|
||||||
|
if (userId) {
|
||||||
|
const userState = this.debugState.users.get(userId);
|
||||||
|
|
||||||
|
if (userState) {
|
||||||
|
userState.state = state;
|
||||||
|
} else {
|
||||||
|
this.debugState.users.set(userId, {
|
||||||
|
state,
|
||||||
|
events: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callId) {
|
||||||
|
const callState = this.debugState.calls.get(callId);
|
||||||
|
|
||||||
|
if (callState) {
|
||||||
|
callState.state = state;
|
||||||
|
} else {
|
||||||
|
this.debugState.calls.set(callId, {
|
||||||
|
state,
|
||||||
|
events: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit("debug");
|
||||||
|
};
|
||||||
|
|
||||||
|
_onCall = (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 {
|
||||||
|
priority:
|
||||||
|
candidateStats.priority && candidateStats.priority.toString(),
|
||||||
|
candidateType: candidateStats.candidateType,
|
||||||
|
protocol: candidateStats.protocol,
|
||||||
|
address: !!candidateStats.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.manager.client.sendEvent(
|
||||||
|
this.manager.roomId,
|
||||||
|
"me.robertlong.call.info",
|
||||||
|
event
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
let statsTimeout;
|
||||||
|
|
||||||
|
const sendStats = () => {
|
||||||
|
if (
|
||||||
|
call.state === "ended" ||
|
||||||
|
peerConnection.connectionState === "closed"
|
||||||
|
) {
|
||||||
|
clearTimeout(statsTimeout);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.manager.client.sendEvent(
|
||||||
|
this.manager.roomId,
|
||||||
|
"me.robertlong.call.ice_error",
|
||||||
|
{
|
||||||
|
call_id: call.callId,
|
||||||
|
errorCode,
|
||||||
|
url,
|
||||||
|
errorText,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
peerConnection.addEventListener("signalingstatechange", () => {
|
||||||
|
sendWebRTCInfoEvent("signalingstatechange");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
|
import { ConferenceCallDebugger } from "./ConferenceCallDebugger";
|
||||||
|
|
||||||
const CONF_ROOM = "me.robertlong.conf";
|
const CONF_ROOM = "me.robertlong.conf";
|
||||||
const CONF_PARTICIPANT = "me.robertlong.conf.participant";
|
const CONF_PARTICIPANT = "me.robertlong.conf.participant";
|
||||||
|
@ -144,14 +145,10 @@ export class ConferenceCallManager extends EventEmitter {
|
||||||
};
|
};
|
||||||
this.participants = [this.localParticipant];
|
this.participants = [this.localParticipant];
|
||||||
this.pendingCalls = [];
|
this.pendingCalls = [];
|
||||||
this.callUserMap = new Map();
|
|
||||||
this.debugState = {
|
|
||||||
users: new Map(),
|
|
||||||
calls: new Map(),
|
|
||||||
};
|
|
||||||
this.client.on("event", this._onEvent);
|
|
||||||
this.client.on("RoomState.members", this._onMemberChanged);
|
this.client.on("RoomState.members", this._onMemberChanged);
|
||||||
this.client.on("Call.incoming", this._onIncomingCall);
|
this.client.on("Call.incoming", this._onIncomingCall);
|
||||||
|
this.callDebugger = new ConferenceCallDebugger(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
setRoom(roomId) {
|
setRoom(roomId) {
|
||||||
|
@ -166,7 +163,7 @@ export class ConferenceCallManager extends EventEmitter {
|
||||||
|
|
||||||
this.joined = true;
|
this.joined = true;
|
||||||
|
|
||||||
this._setDebugState(this.client.getUserId(), null, "you");
|
this.emit("debugstate", this.client.getUserId(), null, "you");
|
||||||
|
|
||||||
const activeConf = this.room.currentState
|
const activeConf = this.room.currentState
|
||||||
.getStateEvents(CONF_ROOM, "")
|
.getStateEvents(CONF_ROOM, "")
|
||||||
|
@ -192,7 +189,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.emit("call", call);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pendingCalls = [];
|
this.pendingCalls = [];
|
||||||
|
@ -200,73 +197,6 @@ export class ConferenceCallManager extends EventEmitter {
|
||||||
this._updateParticipantState();
|
this._updateParticipantState();
|
||||||
}
|
}
|
||||||
|
|
||||||
_onEvent = (event) => {
|
|
||||||
const roomId = event.getRoomId();
|
|
||||||
const type = event.getType();
|
|
||||||
|
|
||||||
if (
|
|
||||||
roomId === this.roomId &&
|
|
||||||
(type.startsWith("m.call.") || type === "me.robertlong.call.info")
|
|
||||||
) {
|
|
||||||
const sender = event.getSender();
|
|
||||||
const { call_id } = event.getContent();
|
|
||||||
|
|
||||||
if (call_id) {
|
|
||||||
if (this.debugState.calls.has(call_id)) {
|
|
||||||
const callState = this.debugState.calls.get(call_id);
|
|
||||||
callState.events.push(event);
|
|
||||||
} else {
|
|
||||||
this.debugState.calls.set(call_id, {
|
|
||||||
state: "unknown",
|
|
||||||
events: [event],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.debugState.users.has(sender)) {
|
|
||||||
const userState = this.debugState.users.get(sender);
|
|
||||||
userState.events.push(event);
|
|
||||||
} else {
|
|
||||||
this.debugState.users.set(sender, {
|
|
||||||
state: "unknown",
|
|
||||||
events: [event],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emit("debug");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_setDebugState(userId, callId, state) {
|
|
||||||
if (userId) {
|
|
||||||
const userState = this.debugState.users.get(userId);
|
|
||||||
|
|
||||||
if (userState) {
|
|
||||||
userState.state = state;
|
|
||||||
} else {
|
|
||||||
this.debugState.users.set(userId, {
|
|
||||||
state,
|
|
||||||
events: [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (callId) {
|
|
||||||
const callState = this.debugState.calls.get(callId);
|
|
||||||
|
|
||||||
if (callState) {
|
|
||||||
callState.state = state;
|
|
||||||
} else {
|
|
||||||
this.debugState.calls.set(callId, {
|
|
||||||
state,
|
|
||||||
events: [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emit("debug");
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateParticipantState = () => {
|
_updateParticipantState = () => {
|
||||||
const userId = this.client.getUserId();
|
const userId = this.client.getUserId();
|
||||||
const currentMemberState = this.room.currentState.getStateEvents(
|
const currentMemberState = this.room.currentState.getStateEvents(
|
||||||
|
@ -328,319 +258,21 @@ export class ConferenceCallManager extends EventEmitter {
|
||||||
new Date().getTime() - participantTimeout > PARTICIPANT_TIMEOUT
|
new Date().getTime() - participantTimeout > PARTICIPANT_TIMEOUT
|
||||||
) {
|
) {
|
||||||
// Member is inactive so don't call them.
|
// Member is inactive so don't call them.
|
||||||
this._setDebugState(userId, null, "inactive");
|
this.emit("debugstate", userId, null, "inactive");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only initiate a call with a user who has a userId that is lexicographically
|
// Only initiate a call with a user who has a userId that is lexicographically
|
||||||
// less than your own. Otherwise, that user will call you.
|
// less than your own. Otherwise, that user will call you.
|
||||||
if (userId < localUserId) {
|
if (userId < localUserId) {
|
||||||
this._setDebugState(userId, null, "waiting for invite");
|
this.emit("debugstate", userId, null, "waiting for invite");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
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().then(() => {
|
call.placeVideoCall().then(() => {
|
||||||
this._observePeerConnection(call);
|
this.emit("call", 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 {
|
|
||||||
priority:
|
|
||||||
candidateStats.priority && candidateStats.priority.toString(),
|
|
||||||
candidateType: candidateStats.candidateType,
|
|
||||||
protocol: candidateStats.protocol,
|
|
||||||
address: !!candidateStats.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 = () => {
|
|
||||||
if (
|
|
||||||
call.state === "ended" ||
|
|
||||||
peerConnection.connectionState === "closed"
|
|
||||||
) {
|
|
||||||
clearTimeout(statsTimeout);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -684,7 +316,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.emit("call", call);
|
||||||
};
|
};
|
||||||
|
|
||||||
_addCall(call, userId) {
|
_addCall(call, userId) {
|
||||||
|
@ -707,7 +339,7 @@ export class ConferenceCallManager extends EventEmitter {
|
||||||
});
|
});
|
||||||
|
|
||||||
call.on("state", (state) =>
|
call.on("state", (state) =>
|
||||||
this._setDebugState(userId, call.callId, state)
|
this.emit("debugstate", userId, call.callId, state)
|
||||||
);
|
);
|
||||||
call.on("feeds_changed", () => this._onCallFeedsChanged(call));
|
call.on("feeds_changed", () => this._onCallFeedsChanged(call));
|
||||||
call.on("hangup", () => this._onCallHangup(call));
|
call.on("hangup", () => this._onCallHangup(call));
|
||||||
|
@ -765,7 +397,7 @@ export class ConferenceCallManager extends EventEmitter {
|
||||||
);
|
);
|
||||||
|
|
||||||
remoteParticipant.call = newCall;
|
remoteParticipant.call = newCall;
|
||||||
this._observePeerConnection(newCall);
|
this.emit("call", 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));
|
||||||
|
|
|
@ -40,19 +40,19 @@ function sortEntries(a, b) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DevTools({ manager }) {
|
export function DevTools({ manager }) {
|
||||||
const [debugState, setDebugState] = useState(manager.debugState);
|
const [debugState, setDebugState] = useState(manager.callDebugger.debugState);
|
||||||
const [selectedEvent, setSelectedEvent] = useState();
|
const [selectedEvent, setSelectedEvent] = useState();
|
||||||
const [activeTab, setActiveTab] = useState("users");
|
const [activeTab, setActiveTab] = useState("users");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function onRoomDebug() {
|
function onRoomDebug() {
|
||||||
setDebugState({ ...manager.debugState });
|
setDebugState({ ...manager.callDebugger.debugState });
|
||||||
}
|
}
|
||||||
|
|
||||||
manager.on("debug", onRoomDebug);
|
manager.callDebugger.on("debug", onRoomDebug);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
manager.removeListener("debug", onRoomDebug);
|
manager.callDebugger.removeListener("debug", onRoomDebug);
|
||||||
};
|
};
|
||||||
}, [manager]);
|
}, [manager]);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue