forked from mystiq/hydrogen-web
WIP7
This commit is contained in:
parent
179c7e74b5
commit
98b77fc761
3 changed files with 173 additions and 6 deletions
|
@ -21,7 +21,7 @@ import type {Room} from "../room/Room";
|
||||||
import type {StateEvent} from "../storage/types";
|
import type {StateEvent} from "../storage/types";
|
||||||
import type {ILogItem} from "../../logging/types";
|
import type {ILogItem} from "../../logging/types";
|
||||||
|
|
||||||
import {WebRTC, PeerConnection, PeerConnectionHandler, StreamPurpose} from "../../platform/types/WebRTC";
|
import {WebRTC, PeerConnection, PeerConnectionHandler} from "../../platform/types/WebRTC";
|
||||||
import {MediaDevices, Track, AudioTrack, TrackType} from "../../platform/types/MediaDevices";
|
import {MediaDevices, Track, AudioTrack, TrackType} from "../../platform/types/MediaDevices";
|
||||||
import type {LocalMedia} from "./LocalMedia";
|
import type {LocalMedia} from "./LocalMedia";
|
||||||
|
|
||||||
|
@ -48,6 +48,7 @@ class PeerCall {
|
||||||
private remoteSDPStreamMetadata?: SDPStreamMetadata;
|
private remoteSDPStreamMetadata?: SDPStreamMetadata;
|
||||||
private responsePromiseChain?: Promise<void>;
|
private responsePromiseChain?: Promise<void>;
|
||||||
private opponentPartyId?: PartyId;
|
private opponentPartyId?: PartyId;
|
||||||
|
private hangupParty: CallParty;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly handler: PeerCallHandler,
|
private readonly handler: PeerCallHandler,
|
||||||
|
@ -80,7 +81,7 @@ class PeerCall {
|
||||||
handleIncomingSignallingMessage(message: SignallingMessage, partyId: PartyId) {
|
handleIncomingSignallingMessage(message: SignallingMessage, partyId: PartyId) {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case EventType.Invite:
|
case EventType.Invite:
|
||||||
this.handleInvite(message.content);
|
this.handleInvite(message.content, partyId);
|
||||||
break;
|
break;
|
||||||
case EventType.Answer:
|
case EventType.Answer:
|
||||||
this.handleAnswer(message.content);
|
this.handleAnswer(message.content);
|
||||||
|
@ -112,15 +113,52 @@ class PeerCall {
|
||||||
await this.waitForState(CallState.InviteSent);
|
await this.waitForState(CallState.InviteSent);
|
||||||
}
|
}
|
||||||
|
|
||||||
async answer() {
|
async answer(localMediaPromise: Promise<LocalMedia>): Promise<void> {
|
||||||
|
if (this.state !== CallState.Ringing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState(CallState.WaitLocalMedia);
|
||||||
|
try {
|
||||||
|
this.localMedia = await localMediaPromise;
|
||||||
|
} catch (err) {
|
||||||
|
this.setState(CallState.Ended);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState(CallState.CreateAnswer);
|
||||||
|
for (const t of this.localMedia.tracks) {
|
||||||
|
this.peerConnection.addTrack(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
let myAnswer: RTCSessionDescriptionInit;
|
||||||
|
try {
|
||||||
|
myAnswer = await this.peerConnection.createAnswer();
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.debug(`Call ${this.callId} Failed to create answer: `, err);
|
||||||
|
this.terminate(CallParty.Local, CallErrorCode.CreateAnswer, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 new Promise(resolve => {
|
||||||
|
setTimeout(resolve, 200);
|
||||||
|
});
|
||||||
|
this.sendAnswer();
|
||||||
}
|
}
|
||||||
|
|
||||||
async hangup() {
|
async hangup() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateLocalMedia(localMediaPromise: Promise<LocalMedia>) {
|
async setMedia(localMediaPromise: Promise<LocalMedia>) {
|
||||||
const oldMedia = this.localMedia;
|
const oldMedia = this.localMedia;
|
||||||
this.localMedia = await localMediaPromise;
|
this.localMedia = await localMediaPromise;
|
||||||
|
|
||||||
|
@ -179,6 +217,16 @@ class PeerCall {
|
||||||
await this.handler.sendSignallingMessage({type: EventType.Invite, content});
|
await this.handler.sendSignallingMessage({type: EventType.Invite, content});
|
||||||
this.setState(CallState.InviteSent);
|
this.setState(CallState.InviteSent);
|
||||||
}
|
}
|
||||||
|
this.sendCandidateQueue();
|
||||||
|
|
||||||
|
if (this.state === CallState.CreateOffer) {
|
||||||
|
this.inviteTimeout = setTimeout(() => {
|
||||||
|
this.inviteTimeout = null;
|
||||||
|
if (this.state === CallState.InviteSent) {
|
||||||
|
this.hangup(CallErrorCode.InviteTimeout);
|
||||||
|
}
|
||||||
|
}, CALL_TIMEOUT_MS);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private async handleInvite(content: InviteContent, partyId: PartyId): Promise<void> {
|
private async handleInvite(content: InviteContent, partyId: PartyId): Promise<void> {
|
||||||
|
@ -202,6 +250,7 @@ class PeerCall {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Q: Why do we set the remote description before accepting the call? To start creating ICE candidates?
|
||||||
await this.peerConnection.setRemoteDescription(content.offer);
|
await this.peerConnection.setRemoteDescription(content.offer);
|
||||||
await this.addBufferedIceCandidates();
|
await this.addBufferedIceCandidates();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -235,6 +284,88 @@ class PeerCall {
|
||||||
}, content.lifetime ?? CALL_TIMEOUT_MS /* - event.getLocalAge() */ );
|
}, content.lifetime ?? CALL_TIMEOUT_MS /* - event.getLocalAge() */ );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async sendAnswer(): Promise<void> {
|
||||||
|
const answerMessage: AnswerMessage = {
|
||||||
|
type: EventType.Answer,
|
||||||
|
content: {
|
||||||
|
answer: {
|
||||||
|
sdp: this.peerConnection.localDescription!.sdp,
|
||||||
|
type: this.peerConnection.localDescription!.type,
|
||||||
|
},
|
||||||
|
[SDPStreamMetadataKey]: this.localMedia.getSDPMetadata(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// We have just taken the local description from the peerConn which will
|
||||||
|
// contain all the local candidates added so far, so we can discard any candidates
|
||||||
|
// we had queued up because they'll be in the answer.
|
||||||
|
this.logger.info(`Call ${this.callId} Discarding ${
|
||||||
|
this.candidateSendQueue.length} candidates that will be sent in answer`);
|
||||||
|
this.candidateSendQueue = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.handler.sendSignallingMessage(answerMessage);
|
||||||
|
} catch (error) {
|
||||||
|
this.terminate(CallParty.Local, CallErrorCode.SendAnswer, false);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// error handler re-throws so this won't happen on error, but
|
||||||
|
// we don't want the same error handling on the candidate queue
|
||||||
|
this.sendCandidateQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private queueCandidate(content: RTCIceCandidate): void {
|
||||||
|
// We partially de-trickle candidates by waiting for `delay` before sending them
|
||||||
|
// amalgamated, in order to avoid sending too many m.call.candidates events and hitting
|
||||||
|
// rate limits in Matrix.
|
||||||
|
// In practice, it'd be better to remove rate limits for m.call.*
|
||||||
|
|
||||||
|
// N.B. this deliberately lets you queue and send blank candidates, which MSC2746
|
||||||
|
// currently proposes as the way to indicate that candidate gathering is complete.
|
||||||
|
// This will hopefully be changed to an explicit rather than implicit notification
|
||||||
|
// shortly.
|
||||||
|
this.candidateSendQueue.push(content);
|
||||||
|
|
||||||
|
// Don't send the ICE candidates yet if the call is in the ringing state
|
||||||
|
if (this.state === CallState.Ringing) return;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.sendCandidateQueue();
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async sendCandidateQueue(): Promise<void> {
|
||||||
|
if (this.candidateSendQueue.length === 0 || this.state === CallState.Ended) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidates = this.candidateSendQueue;
|
||||||
|
this.candidateSendQueue = [];
|
||||||
|
const candidatesMessage: CandidatesMessage = {
|
||||||
|
type: EventType.Candidates,
|
||||||
|
content: {
|
||||||
|
candidates: candidates,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.logger.debug(`Call ${this.callId} attempting to send ${candidates.length} candidates`);
|
||||||
|
try {
|
||||||
|
await this.handler.sendSignallingMessage(candidatesMessage);
|
||||||
|
// Try to send candidates again just in case we received more candidates while sending.
|
||||||
|
this.sendCandidateQueue();
|
||||||
|
} catch (error) {
|
||||||
|
// don't retry this event: we'll send another one later as we might
|
||||||
|
// have more candidates by then.
|
||||||
|
// put all the candidates we failed to send back in the queue
|
||||||
|
this.terminate(CallParty.Local, CallErrorCode.SignallingFailed, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private updateRemoteSDPStreamMetadata(metadata: SDPStreamMetadata): void {
|
private updateRemoteSDPStreamMetadata(metadata: SDPStreamMetadata): void {
|
||||||
this.remoteSDPStreamMetadata = recursivelyAssign(this.remoteSDPStreamMetadata || {}, metadata, true);
|
this.remoteSDPStreamMetadata = recursivelyAssign(this.remoteSDPStreamMetadata || {}, metadata, true);
|
||||||
// will rerequest stream purpose for all tracks and set track.type accordingly
|
// will rerequest stream purpose for all tracks and set track.type accordingly
|
||||||
|
@ -296,6 +427,12 @@ class PeerCall {
|
||||||
private async terminate(hangupParty: CallParty, hangupReason: CallErrorCode, shouldEmit: boolean): Promise<void> {
|
private async terminate(hangupParty: CallParty, hangupReason: CallErrorCode, shouldEmit: boolean): Promise<void> {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private stopAllMedia(): void {
|
||||||
|
for (const track of this.localMedia.tracks) {
|
||||||
|
track.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -480,9 +617,34 @@ export type InviteMessage = {
|
||||||
content: InviteContent
|
content: InviteContent
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SignallingMessage = InviteMessage;
|
type AnwserContent = {
|
||||||
|
answer: {
|
||||||
|
sdp: string,
|
||||||
|
// type is now deprecated as of Matrix VoIP v1, but
|
||||||
|
// required to still be sent for backwards compat
|
||||||
|
type: RTCSdpType,
|
||||||
|
},
|
||||||
|
[SDPStreamMetadataKey]: SDPStreamMetadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AnswerMessage = {
|
||||||
|
type: EventType.Answer,
|
||||||
|
content: AnwserContent
|
||||||
|
}
|
||||||
|
|
||||||
|
type CandidatesContent = {
|
||||||
|
candidates: RTCIceCandidate[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CandidatesMessage = {
|
||||||
|
type: EventType.Candidates,
|
||||||
|
content: CandidatesContent
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type SignallingMessage = InviteMessage | AnswerMessage | CandidatesMessage;
|
||||||
|
|
||||||
export interface PeerCallHandler {
|
export interface PeerCallHandler {
|
||||||
emitUpdate(peerCall: PeerCall, params: any);
|
emitUpdate(peerCall: PeerCall, params: any);
|
||||||
sendSignallingMessage(message: InviteMessage);
|
sendSignallingMessage(message: SignallingMessage);
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ export interface Track {
|
||||||
get settings(): MediaTrackSettings;
|
get settings(): MediaTrackSettings;
|
||||||
get muted(): boolean;
|
get muted(): boolean;
|
||||||
setMuted(muted: boolean): void;
|
setMuted(muted: boolean): void;
|
||||||
|
stop(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AudioTrack extends Track {
|
export interface AudioTrack extends Track {
|
||||||
|
|
|
@ -107,6 +107,10 @@ export class TrackWrapper implements Track {
|
||||||
setType(type: TrackType): void {
|
setType(type: TrackType): void {
|
||||||
this._type = type;
|
this._type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.track.stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AudioTrackWrapper extends TrackWrapper {
|
export class AudioTrackWrapper extends TrackWrapper {
|
||||||
|
|
Loading…
Reference in a new issue