You've already forked matrix-js-sdk
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:
@@ -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;
|
||||||
}
|
}
|
||||||
|
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user