WIP8 - implement PeerCall.handleAnswer and other things

This commit is contained in:
Bruno Windels 2022-03-02 13:53:22 +01:00 committed by Bruno Windels
parent 25b0148073
commit ecf7eab3ee
4 changed files with 99 additions and 28 deletions

View File

@ -17,6 +17,7 @@ limitations under the License.
import {ObservableMap} from "../../observable/map/ObservableMap";
import {recursivelyAssign} from "../../utils/recursivelyAssign";
import {AsyncQueue} from "../../utils/AsyncQueue";
import {Disposables, Disposable} from "../../utils/Disposables";
import type {Room} from "../room/Room";
import type {StateEvent} from "../storage/types";
import type {ILogItem} from "../../logging/types";
@ -50,7 +51,8 @@ class PeerCall {
private responsePromiseChain?: Promise<void>;
private opponentPartyId?: PartyId;
private hangupParty: CallParty;
private hangupTimeout?: Timeout;
private disposables = new Disposables();
private statePromiseMap = new Map<CallState, {resolve: () => void, promise: Promise<void>}>();
constructor(
private readonly handler: PeerCallHandler,
@ -144,14 +146,13 @@ class PeerCall {
try {
await this.peerConnection.setLocalDescription(myAnswer);
this.setState(CallState.Connecting);
} catch (err) {
this.logger.debug(`Call ${this.callId} Error setting local description!`, err);
this.terminate(CallParty.Local, CallErrorCode.SetLocalDescription, true);
return;
}
// Allow a short time for initial candidates to be gathered
await this.createTimeout(200).elapsed();
await this.delay(200);
this.sendAnswer();
}
@ -193,7 +194,7 @@ class PeerCall {
if (this.peerConnection.iceGatheringState === 'gathering') {
// Allow a short time for initial candidates to be gathered
await this.createTimeout(200).elapsed();
await this.delay(200);
}
if (this.state === CallState.Ended) {
@ -221,8 +222,7 @@ class PeerCall {
this.sendCandidateQueue();
if (this.state === CallState.CreateOffer) {
this.hangupTimeout = this.createTimeout(CALL_TIMEOUT_MS);
await this.hangupTimeout.elapsed();
await this.delay(CALL_TIMEOUT_MS);
// @ts-ignore TS doesn't take the await above into account to know that the state could have changed in between
if (this.state === CallState.InviteSent) {
this.hangup(CallErrorCode.InviteTimeout);
@ -271,18 +271,55 @@ class PeerCall {
this.setState(CallState.Ringing);
setTimeout(() => {
if (this.state == CallState.Ringing) {
this.logger.debug(`Call ${this.callId} invite has expired. Hanging up.`);
this.hangupParty = CallParty.Remote; // effectively
this.setState(CallState.Ended);
this.stopAllMedia();
if (this.peerConnection.signalingState != 'closed') {
this.peerConnection.close();
}
this.emit(CallEvent.Hangup);
await this.delay(content.lifetime ?? CALL_TIMEOUT_MS);
// @ts-ignore TS doesn't take the await above into account to know that the state could have changed in between
if (this.state === CallState.Ringing) {
this.logger.debug(`Call ${this.callId} invite has expired. Hanging up.`);
this.hangupParty = CallParty.Remote; // effectively
this.setState(CallState.Ended);
this.stopAllMedia();
if (this.peerConnection.signalingState != 'closed') {
this.peerConnection.close();
}
}, content.lifetime ?? CALL_TIMEOUT_MS /* - event.getLocalAge() */ );
}
}
private async handleAnswer(content: AnwserContent, partyId: PartyId): Promise<void> {
this.logger.debug(`Got answer for call ID ${this.callId} from party ID ${content.party_id}`);
if (this.state === CallState.Ended) {
this.logger.debug(`Ignoring answer because call ID ${this.callId} has ended`);
return;
}
if (this.opponentPartyId !== undefined) {
this.logger.info(
`Call ${this.callId} ` +
`Ignoring answer from party ID ${content.party_id}: ` +
`we already have an answer/reject from ${this.opponentPartyId}`,
);
return;
}
this.opponentPartyId = partyId;
await this.addBufferedIceCandidates();
this.setState(CallState.Connecting);
const sdpStreamMetadata = content[SDPStreamMetadataKey];
if (sdpStreamMetadata) {
this.updateRemoteSDPStreamMetadata(sdpStreamMetadata);
} else {
this.logger.warn(`Call ${this.callId} Did not get any SDPStreamMetadata! Can not send/receive multiple streams`);
}
try {
await this.peerConnection.setRemoteDescription(content.answer);
} catch (e) {
this.logger.debug(`Call ${this.callId} Failed to set remote description`, e);
this.terminate(CallParty.Local, CallErrorCode.SetRemoteDescription, false);
return;
}
}
private async sendAnswer(): Promise<void> {
@ -333,8 +370,7 @@ class PeerCall {
// MSC2746 recommends these values (can be quite long when calling because the
// callee will need a while to answer the call)
const delay = this.direction === CallDirection.Inbound ? 500 : 2000;
this.createTimeout(delay).elapsed().then(() => {
this.delay(this.direction === CallDirection.Inbound ? 500 : 2000).then(() => {
this.sendCandidateQueue();
});
}
@ -384,13 +420,15 @@ class PeerCall {
private async addBufferedIceCandidates(): Promise<void> {
const bufferedCandidates = this.remoteCandidateBuffer!.get(this.opponentPartyId!);
if (bufferedCandidates) {
this.logger.info(`Call ${this.callId} Adding ${
bufferedCandidates.length} buffered candidates for opponent ${this.opponentPartyId}`);
await this.addIceCandidates(bufferedCandidates);
if (this.remoteCandidateBuffer && this.opponentPartyId) {
const bufferedCandidates = this.remoteCandidateBuffer.get(this.opponentPartyId);
if (bufferedCandidates) {
this.logger.info(`Call ${this.callId} Adding ${
bufferedCandidates.length} buffered candidates for opponent ${this.opponentPartyId}`);
await this.addIceCandidates(bufferedCandidates);
}
this.remoteCandidateBuffer = undefined;
}
this.remoteCandidateBuffer = undefined;
}
private async addIceCandidates(candidates: RTCIceCandidate[]): Promise<void> {
@ -417,11 +455,25 @@ class PeerCall {
private setState(state: CallState): void {
const oldState = this.state;
this.state = state;
this.handler.emitUpdate();
let deferred = this.statePromiseMap.get(state);
if (deferred) {
deferred.resolve();
this.statePromiseMap.delete(state);
}
this.handler.emitUpdate(this, undefined);
}
private waitForState(state: CallState): Promise<void> {
let deferred = this.statePromiseMap.get(state);
if (!deferred) {
let resolve;
const promise = new Promise<void>(r => {
resolve = r;
});
deferred = {resolve, promise};
this.statePromiseMap.set(state, deferred);
}
return deferred.promise;
}
private async terminate(hangupParty: CallParty, hangupReason: CallErrorCode, shouldEmit: boolean): Promise<void> {
@ -433,6 +485,18 @@ class PeerCall {
track.stop();
}
}
private async delay(timeoutMs: number): Promise<void> {
// Allow a short time for initial candidates to be gathered
const timeout = this.disposables.track(this.createTimeout(timeoutMs));
await timeout.elapsed();
this.disposables.untrack(timeout);
}
public dispose(): void {
this.disposables.dispose();
// TODO: dispose peerConnection?
}
}

View File

@ -105,7 +105,7 @@ Expose call objects
Write view model
write view
## Calls questions\
## Calls questions
- how do we handle glare between group calls (e.g. different state events with different call ids?)
- Split up DOM part into platform code? What abstractions to choose?
Does it make sense to come up with our own API very similar to DOM api?

View File

@ -42,6 +42,7 @@ export interface PeerConnection {
get remoteTracks(): Track[];
get dataChannel(): DataChannel | undefined;
get iceGatheringState(): RTCIceGatheringState;
get signalingState(): RTCSignalingState;
get localDescription(): RTCSessionDescription | undefined;
createOffer(): Promise<RTCSessionDescriptionInit>;
createAnswer(): Promise<RTCSessionDescriptionInit>;
@ -53,4 +54,5 @@ export interface PeerConnection {
replaceTrack(oldTrack: Track, newTrack: Track): Promise<boolean>;
createDataChannel(): DataChannel;
dispose(): void;
close(): void;
}

View File

@ -43,6 +43,7 @@ class DOMPeerConnection implements PeerConnection {
get dataChannel(): DataChannel | undefined { return undefined; }
get iceGatheringState(): RTCIceGatheringState { return this.peerConnection.iceGatheringState; }
get localDescription(): RTCSessionDescription | undefined { return this.peerConnection.localDescription ?? undefined; }
get signalingState(): RTCSignalingState { return this.peerConnection.signalingState; }
createOffer(): Promise<RTCSessionDescriptionInit> {
return this.peerConnection.createOffer();
@ -64,6 +65,10 @@ class DOMPeerConnection implements PeerConnection {
return this.peerConnection.addIceCandidate(candidate);
}
close(): void {
return this.peerConnection.close();
}
addTrack(track: Track): void {
if (!(track instanceof TrackWrapper)) {
throw new Error("Not a TrackWrapper");