1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-09 10:22:46 +03:00

Minor VoIP stack improvements (#2946)

* Add `IGroupCallRoomState`

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Export values into `const`s

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Add `should correctly emit LengthChanged`

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Add `ICE disconnected timeout`

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Improve typing

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Don't cast `getContent()`

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Use `Date.now()` for call length

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Type fix

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
Šimon Brandner
2022-12-08 19:51:05 +01:00
committed by GitHub
parent 39cf212628
commit ae849fdd46
4 changed files with 81 additions and 16 deletions

View File

@@ -115,12 +115,14 @@ export class MockRTCPeerConnection {
private negotiationNeededListener?: () => void; private negotiationNeededListener?: () => void;
public iceCandidateListener?: (e: RTCPeerConnectionIceEvent) => void; public iceCandidateListener?: (e: RTCPeerConnectionIceEvent) => void;
public iceConnectionStateChangeListener?: () => void;
public onTrackListener?: (e: RTCTrackEvent) => void; public onTrackListener?: (e: RTCTrackEvent) => void;
public needsNegotiation = false; public needsNegotiation = false;
public readyToNegotiate: Promise<void>; public readyToNegotiate: Promise<void>;
private onReadyToNegotiate?: () => void; private onReadyToNegotiate?: () => void;
public localDescription: RTCSessionDescription; public localDescription: RTCSessionDescription;
public signalingState: RTCSignalingState = "stable"; public signalingState: RTCSignalingState = "stable";
public iceConnectionState: RTCIceConnectionState = "connected";
public transceivers: MockRTCRtpTransceiver[] = []; public transceivers: MockRTCRtpTransceiver[] = [];
public static triggerAllNegotiations(): void { public static triggerAllNegotiations(): void {
@@ -156,6 +158,8 @@ export class MockRTCPeerConnection {
this.negotiationNeededListener = listener; this.negotiationNeededListener = listener;
} else if (type == 'icecandidate') { } else if (type == 'icecandidate') {
this.iceCandidateListener = listener; this.iceCandidateListener = listener;
} else if (type === 'iceconnectionstatechange') {
this.iceConnectionStateChangeListener = listener;
} else if (type == 'track') { } else if (type == 'track') {
this.onTrackListener = listener; this.onTrackListener = listener;
} }

View File

@@ -1458,4 +1458,50 @@ describe('Call', function() {
expect(call.hasPeerConnection).toBe(true); expect(call.hasPeerConnection).toBe(true);
}); });
}); });
it("should correctly emit LengthChanged", async () => {
const advanceByArray = [2, 3, 5];
const lengthChangedListener = jest.fn();
jest.useFakeTimers();
call.addListener(CallEvent.LengthChanged, lengthChangedListener);
await fakeIncomingCall(client, call, "1");
(call.peerConn as unknown as MockRTCPeerConnection).iceConnectionStateChangeListener!();
let hasAdvancedBy = 0;
for (const advanceBy of advanceByArray) {
jest.advanceTimersByTime(advanceBy * 1000);
hasAdvancedBy += advanceBy;
expect(lengthChangedListener).toHaveBeenCalledTimes(hasAdvancedBy);
expect(lengthChangedListener).toBeCalledWith(hasAdvancedBy);
}
});
describe("ICE disconnected timeout", () => {
let mockPeerConn: MockRTCPeerConnection;
beforeEach(async () => {
jest.useFakeTimers();
jest.spyOn(call, "hangup");
await fakeIncomingCall(client, call, "1");
mockPeerConn = (call.peerConn as unknown as MockRTCPeerConnection);
mockPeerConn.iceConnectionState = "disconnected";
mockPeerConn.iceConnectionStateChangeListener!();
});
it("should hang up after being disconnected for 30 seconds", () => {
jest.advanceTimersByTime(31 * 1000);
expect(call.hangup).toHaveBeenCalledWith(CallErrorCode.IceFailed, false);
});
it("should not hangup if we've managed to re-connect", () => {
mockPeerConn.iceConnectionState = "connected";
mockPeerConn.iceConnectionStateChangeListener!();
jest.advanceTimersByTime(31 * 1000);
expect(call.hangup).not.toHaveBeenCalled();
});
});
}); });

View File

@@ -253,7 +253,11 @@ const VOIP_PROTO_VERSION = "1";
const FALLBACK_ICE_SERVER = 'stun:turn.matrix.org'; const FALLBACK_ICE_SERVER = 'stun:turn.matrix.org';
/** The length of time a call can be ringing for. */ /** The length of time a call can be ringing for. */
const CALL_TIMEOUT_MS = 60000; const CALL_TIMEOUT_MS = 60 * 1000; // ms
/** The time after which we increment callLength */
const CALL_LENGTH_INTERVAL = 1000; // ms
/** The time after which we end the call, if ICE got disconnected */
const ICE_DISCONNECTED_TIMEOUT = 30 * 1000; // ms
export class CallError extends Error { export class CallError extends Error {
public readonly code: string; public readonly code: string;
@@ -376,7 +380,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
private remoteSDPStreamMetadata?: SDPStreamMetadata; private remoteSDPStreamMetadata?: SDPStreamMetadata;
private callLengthInterval?: ReturnType<typeof setInterval>; private callLengthInterval?: ReturnType<typeof setInterval>;
private callLength = 0; private callStartTime?: number;
private opponentDeviceId?: string; private opponentDeviceId?: string;
private opponentDeviceInfo?: DeviceInfo; private opponentDeviceInfo?: DeviceInfo;
@@ -2083,11 +2087,12 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
clearTimeout(this.iceDisconnectedTimeout); clearTimeout(this.iceDisconnectedTimeout);
this.state = CallState.Connected; this.state = CallState.Connected;
if (!this.callLengthInterval) { if (!this.callLengthInterval && !this.callStartTime) {
this.callStartTime = Date.now();
this.callLengthInterval = setInterval(() => { this.callLengthInterval = setInterval(() => {
this.callLength++; this.emit(CallEvent.LengthChanged, Math.round((Date.now() - this.callStartTime!) / 1000));
this.emit(CallEvent.LengthChanged, this.callLength); }, CALL_LENGTH_INTERVAL);
}, 1000);
} }
} else if (this.peerConn?.iceConnectionState == 'failed') { } else if (this.peerConn?.iceConnectionState == 'failed') {
// Firefox for Android does not yet have support for restartIce() // Firefox for Android does not yet have support for restartIce()
@@ -2104,7 +2109,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.iceDisconnectedTimeout = setTimeout(() => { this.iceDisconnectedTimeout = setTimeout(() => {
logger.info(`Hanging up call ${this.callId} (ICE disconnected for too long)`); logger.info(`Hanging up call ${this.callId} (ICE disconnected for too long)`);
this.hangup(CallErrorCode.IceFailed, false); this.hangup(CallErrorCode.IceFailed, false);
}, 30 * 1000); }, ICE_DISCONNECTED_TIMEOUT);
this.state = CallState.Connecting; this.state = CallState.Connecting;
} }

View File

@@ -121,9 +121,17 @@ export interface IGroupCallDataChannelOptions {
protocol: string; protocol: string;
} }
export interface IGroupCallRoomState {
"m.intent": GroupCallIntent;
"m.type": GroupCallType;
"io.element.ptt"?: boolean;
// TODO: Specify data-channels
"dataChannelsEnabled"?: boolean;
"dataChannelOptions"?: IGroupCallDataChannelOptions;
}
export interface IGroupCallRoomMemberFeed { export interface IGroupCallRoomMemberFeed {
purpose: SDPStreamMetadataPurpose; purpose: SDPStreamMetadataPurpose;
// TODO: Sources for adaptive bitrate
} }
export interface IGroupCallRoomMemberDevice { export interface IGroupCallRoomMemberDevice {
@@ -228,17 +236,19 @@ export class GroupCall extends TypedEventEmitter<
this.client.groupCallEventHandler!.groupCalls.set(this.room.roomId, this); this.client.groupCallEventHandler!.groupCalls.set(this.room.roomId, this);
this.client.emit(GroupCallEventHandlerEvent.Outgoing, this); this.client.emit(GroupCallEventHandlerEvent.Outgoing, this);
const groupCallState: IGroupCallRoomState = {
"m.intent": this.intent,
"m.type": this.type,
"io.element.ptt": this.isPtt,
// TODO: Specify data-channels better
"dataChannelsEnabled": this.dataChannelsEnabled,
"dataChannelOptions": this.dataChannelsEnabled ? this.dataChannelOptions : undefined,
};
await this.client.sendStateEvent( await this.client.sendStateEvent(
this.room.roomId, this.room.roomId,
EventType.GroupCallPrefix, EventType.GroupCallPrefix,
{ groupCallState,
"m.intent": this.intent,
"m.type": this.type,
"io.element.ptt": this.isPtt,
// TODO: Specify datachannels
"dataChannelsEnabled": this.dataChannelsEnabled,
"dataChannelOptions": this.dataChannelOptions,
},
this.groupCallId, this.groupCallId,
); );