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

View File

@@ -1458,4 +1458,50 @@ describe('Call', function() {
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';
/** 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 {
public readonly code: string;
@@ -376,7 +380,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
private remoteSDPStreamMetadata?: SDPStreamMetadata;
private callLengthInterval?: ReturnType<typeof setInterval>;
private callLength = 0;
private callStartTime?: number;
private opponentDeviceId?: string;
private opponentDeviceInfo?: DeviceInfo;
@@ -2083,11 +2087,12 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
clearTimeout(this.iceDisconnectedTimeout);
this.state = CallState.Connected;
if (!this.callLengthInterval) {
if (!this.callLengthInterval && !this.callStartTime) {
this.callStartTime = Date.now();
this.callLengthInterval = setInterval(() => {
this.callLength++;
this.emit(CallEvent.LengthChanged, this.callLength);
}, 1000);
this.emit(CallEvent.LengthChanged, Math.round((Date.now() - this.callStartTime!) / 1000));
}, CALL_LENGTH_INTERVAL);
}
} else if (this.peerConn?.iceConnectionState == 'failed') {
// Firefox for Android does not yet have support for restartIce()
@@ -2104,7 +2109,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.iceDisconnectedTimeout = setTimeout(() => {
logger.info(`Hanging up call ${this.callId} (ICE disconnected for too long)`);
this.hangup(CallErrorCode.IceFailed, false);
}, 30 * 1000);
}, ICE_DISCONNECTED_TIMEOUT);
this.state = CallState.Connecting;
}

View File

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