You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-26 17:03:12 +03:00
Move from constants to configureable public variables
This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
import EventEmitter from "events";
|
||||
import { CallFeed, CallFeedEvent } from "./callFeed";
|
||||
import { CallFeed } from "./callFeed";
|
||||
import { MatrixClient } from "../client";
|
||||
import { randomString } from "../randomstring";
|
||||
import { CallErrorCode, CallEvent, CallState, CallType, MatrixCall } from "./call";
|
||||
import { CallErrorCode, CallState, CallType, MatrixCall } from "./call";
|
||||
import { RoomMember } from "../models/room-member";
|
||||
import { SDPStreamMetadataPurpose } from "./callEventTypes";
|
||||
import { Room } from "../models/room";
|
||||
import { logger } from "../logger";
|
||||
import { Callback } from "../client";
|
||||
import { ReEmitter } from "../ReEmitter";
|
||||
import { GroupCallParticipant, GroupCallParticipantEvent } from "./groupCallParticipant";
|
||||
|
||||
export enum GroupCallEvent {
|
||||
Entered = "entered",
|
||||
@@ -20,217 +21,6 @@ export enum GroupCallEvent {
|
||||
|
||||
const CONF_ROOM = "me.robertlong.conf";
|
||||
const CONF_PARTICIPANT = "me.robertlong.conf.participant";
|
||||
const PARTICIPANT_TIMEOUT = 1000 * 15;
|
||||
const SPEAKING_THRESHOLD = -80;
|
||||
const ACTIVE_SPEAKER_INTERVAL = 1000;
|
||||
const ACTIVE_SPEAKER_SAMPLES = 8;
|
||||
|
||||
export enum GroupCallParticipantEvent {
|
||||
Speaking = "speaking",
|
||||
VolumeChanged = "volume_changed",
|
||||
MuteStateChanged = "mute_state_changed",
|
||||
Datachannel = "datachannel",
|
||||
CallReplaced = "call_replaced"
|
||||
}
|
||||
|
||||
export class GroupCallParticipant extends EventEmitter {
|
||||
public feeds: CallFeed[] = [];
|
||||
public activeSpeaker: boolean;
|
||||
public activeSpeakerSamples: number[];
|
||||
public dataChannel?: RTCDataChannel;
|
||||
|
||||
constructor(
|
||||
private groupCall: GroupCall,
|
||||
public member: RoomMember,
|
||||
// The session id is used to re-initiate calls if the user's participant
|
||||
// session id has changed
|
||||
public sessionId: string,
|
||||
public call?: MatrixCall,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.activeSpeakerSamples = Array(ACTIVE_SPEAKER_SAMPLES).fill(
|
||||
-Infinity,
|
||||
);
|
||||
|
||||
if (this.call) {
|
||||
this.call.on(CallEvent.State, this.onCallStateChanged);
|
||||
this.call.on(CallEvent.FeedsChanged, this.onCallFeedsChanged);
|
||||
this.call.on(CallEvent.Replaced, this.onCallReplaced);
|
||||
this.call.on(CallEvent.Hangup, this.onCallHangup);
|
||||
}
|
||||
}
|
||||
|
||||
public replaceCall(call: MatrixCall, sessionId: string) {
|
||||
const oldCall = this.call;
|
||||
|
||||
if (this.call) {
|
||||
this.call.hangup(CallErrorCode.Replaced, false);
|
||||
this.call.removeListener(CallEvent.State, this.onCallStateChanged);
|
||||
this.call.removeListener(
|
||||
CallEvent.FeedsChanged,
|
||||
this.onCallFeedsChanged,
|
||||
);
|
||||
this.call.removeListener(CallEvent.Replaced, this.onCallReplaced);
|
||||
this.call.removeListener(CallEvent.Hangup, this.onCallHangup);
|
||||
this.call.removeListener(CallEvent.DataChannel, this.onCallDataChannel);
|
||||
}
|
||||
|
||||
this.call = call;
|
||||
this.member = call.getOpponentMember();
|
||||
this.activeSpeaker = false;
|
||||
this.sessionId = sessionId;
|
||||
|
||||
this.call.on(CallEvent.State, this.onCallStateChanged);
|
||||
this.call.on(CallEvent.FeedsChanged, this.onCallFeedsChanged);
|
||||
this.call.on(CallEvent.Replaced, this.onCallReplaced);
|
||||
this.call.on(CallEvent.Hangup, this.onCallHangup);
|
||||
this.call.on(CallEvent.DataChannel, this.onCallDataChannel);
|
||||
|
||||
this.groupCall.emit(GroupCallParticipantEvent.CallReplaced, this, oldCall, call);
|
||||
}
|
||||
|
||||
public get usermediaFeed() {
|
||||
return this.feeds.find((feed) => feed.purpose === SDPStreamMetadataPurpose.Usermedia);
|
||||
}
|
||||
|
||||
public get usermediaStream(): MediaStream {
|
||||
return this.usermediaFeed?.stream;
|
||||
}
|
||||
|
||||
public isAudioMuted(): boolean {
|
||||
const feed = this.usermediaFeed;
|
||||
|
||||
if (!feed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return feed.isAudioMuted();
|
||||
}
|
||||
|
||||
public isVideoMuted(): boolean {
|
||||
const feed = this.usermediaFeed;
|
||||
|
||||
if (!feed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return feed.isVideoMuted();
|
||||
}
|
||||
|
||||
private onCallStateChanged = (state) => {
|
||||
const call = this.call;
|
||||
const audioMuted = this.groupCall.localParticipant.isAudioMuted();
|
||||
|
||||
if (
|
||||
call.localUsermediaStream &&
|
||||
call.isMicrophoneMuted() !== audioMuted
|
||||
) {
|
||||
call.setMicrophoneMuted(audioMuted);
|
||||
}
|
||||
|
||||
const videoMuted = this.groupCall.localParticipant.isVideoMuted();
|
||||
|
||||
if (
|
||||
call.localUsermediaStream &&
|
||||
call.isLocalVideoMuted() !== videoMuted
|
||||
) {
|
||||
call.setLocalVideoMuted(videoMuted);
|
||||
}
|
||||
};
|
||||
|
||||
onCallFeedsChanged = () => {
|
||||
const oldFeeds = this.feeds;
|
||||
const newFeeds = this.call.getRemoteFeeds();
|
||||
|
||||
this.feeds = [];
|
||||
|
||||
for (const feed of newFeeds) {
|
||||
if (oldFeeds.includes(feed)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.addCallFeed(feed);
|
||||
}
|
||||
};
|
||||
|
||||
onCallReplaced = (newCall) => {
|
||||
// TODO: Should we always reuse the sessionId?
|
||||
this.replaceCall(newCall, this.sessionId);
|
||||
};
|
||||
|
||||
onCallHangup = () => {
|
||||
if (this.call.hangupReason === CallErrorCode.Replaced) {
|
||||
return;
|
||||
}
|
||||
|
||||
const participantIndex = this.groupCall.participants.indexOf(this);
|
||||
|
||||
if (participantIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.groupCall.participants.splice(participantIndex, 1);
|
||||
|
||||
if (
|
||||
this.groupCall.activeSpeaker === this &&
|
||||
this.groupCall.participants.length > 0
|
||||
) {
|
||||
this.groupCall.activeSpeaker = this.groupCall.participants[0];
|
||||
this.groupCall.activeSpeaker.activeSpeaker = true;
|
||||
this.groupCall.emit(GroupCallEvent.ActiveSpeakerChanged, this.groupCall.activeSpeaker);
|
||||
}
|
||||
|
||||
this.groupCall.emit(GroupCallEvent.ParticipantsChanged, this.groupCall.participants);
|
||||
};
|
||||
|
||||
addCallFeed(callFeed: CallFeed) {
|
||||
if (callFeed.purpose === SDPStreamMetadataPurpose.Usermedia) {
|
||||
callFeed.setSpeakingThreshold(SPEAKING_THRESHOLD);
|
||||
callFeed.measureVolumeActivity(true);
|
||||
callFeed.on(CallFeedEvent.Speaking, this.onCallFeedSpeaking);
|
||||
callFeed.on(
|
||||
CallFeedEvent.VolumeChanged,
|
||||
this.onCallFeedVolumeChanged,
|
||||
);
|
||||
callFeed.on(
|
||||
CallFeedEvent.MuteStateChanged,
|
||||
this.onCallFeedMuteStateChanged,
|
||||
);
|
||||
this.onCallFeedMuteStateChanged(
|
||||
this.isAudioMuted(),
|
||||
this.isVideoMuted(),
|
||||
);
|
||||
}
|
||||
|
||||
this.feeds.push(callFeed);
|
||||
}
|
||||
|
||||
onCallFeedSpeaking = (speaking: boolean) => {
|
||||
this.emit(GroupCallParticipantEvent.Speaking, speaking);
|
||||
};
|
||||
|
||||
onCallFeedVolumeChanged = (maxVolume: number) => {
|
||||
this.activeSpeakerSamples.shift();
|
||||
this.activeSpeakerSamples.push(maxVolume);
|
||||
this.emit(GroupCallParticipantEvent.VolumeChanged, maxVolume);
|
||||
};
|
||||
|
||||
onCallFeedMuteStateChanged = (audioMuted: boolean, videoMuted: boolean) => {
|
||||
if (audioMuted) {
|
||||
this.activeSpeakerSamples = Array(ACTIVE_SPEAKER_SAMPLES).fill(
|
||||
-Infinity,
|
||||
);
|
||||
}
|
||||
|
||||
this.emit(GroupCallParticipantEvent.MuteStateChanged, audioMuted, videoMuted);
|
||||
};
|
||||
|
||||
onCallDataChannel = (dataChannel: RTCDataChannel) => {
|
||||
this.dataChannel = dataChannel;
|
||||
this.emit(GroupCallParticipantEvent.Datachannel, dataChannel);
|
||||
};
|
||||
}
|
||||
|
||||
export class GroupCall extends EventEmitter {
|
||||
public entered = false;
|
||||
@@ -238,6 +28,10 @@ export class GroupCall extends EventEmitter {
|
||||
public localParticipant: GroupCallParticipant;
|
||||
public participants: GroupCallParticipant[] = [];
|
||||
public room: Room;
|
||||
public activeSpeakerSampleCount = 8;
|
||||
public activeSpeakerInterval = 1000;
|
||||
public speakingThreshold = -80;
|
||||
public participantTimeout = 1000 * 15;
|
||||
|
||||
private speakerMap: Map<RoomMember, number[]> = new Map();
|
||||
private presenceLoopTimeout?: number;
|
||||
@@ -476,7 +270,7 @@ export class GroupCall extends EventEmitter {
|
||||
...currentMemberState.getContent(),
|
||||
[CONF_PARTICIPANT]: {
|
||||
sessionId: this.localParticipant.sessionId,
|
||||
expiresAt: new Date().getTime() + PARTICIPANT_TIMEOUT * 2,
|
||||
expiresAt: new Date().getTime() + this.participantTimeout * 2,
|
||||
},
|
||||
},
|
||||
userId,
|
||||
@@ -513,7 +307,7 @@ export class GroupCall extends EventEmitter {
|
||||
|
||||
this.presenceLoopTimeout = setTimeout(
|
||||
this.onPresenceLoop,
|
||||
PARTICIPANT_TIMEOUT,
|
||||
this.participantTimeout,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -698,10 +492,10 @@ export class GroupCall extends EventEmitter {
|
||||
|
||||
for (let i = 0; i < participant.activeSpeakerSamples.length; i++) {
|
||||
const volume = participant.activeSpeakerSamples[i];
|
||||
total += Math.max(volume, SPEAKING_THRESHOLD);
|
||||
total += Math.max(volume, this.speakingThreshold);
|
||||
}
|
||||
|
||||
const avg = total / ACTIVE_SPEAKER_SAMPLES;
|
||||
const avg = total / this.activeSpeakerSampleCount;
|
||||
|
||||
if (!topAvg || avg > topAvg) {
|
||||
topAvg = avg;
|
||||
@@ -709,7 +503,7 @@ export class GroupCall extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
if (nextActiveSpeaker && topAvg > SPEAKING_THRESHOLD) {
|
||||
if (nextActiveSpeaker && topAvg > this.speakingThreshold) {
|
||||
if (nextActiveSpeaker && this.activeSpeaker !== nextActiveSpeaker) {
|
||||
this.activeSpeaker.activeSpeaker = false;
|
||||
nextActiveSpeaker.activeSpeaker = true;
|
||||
@@ -720,7 +514,7 @@ export class GroupCall extends EventEmitter {
|
||||
|
||||
this.activeSpeakerLoopTimeout = setTimeout(
|
||||
this.onActiveSpeakerLoop,
|
||||
ACTIVE_SPEAKER_INTERVAL,
|
||||
this.activeSpeakerInterval,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
213
src/webrtc/groupCallParticipant.ts
Normal file
213
src/webrtc/groupCallParticipant.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import EventEmitter from "events";
|
||||
import { CallFeed, CallFeedEvent } from "./callFeed";
|
||||
import { CallErrorCode, CallEvent, MatrixCall } from "./call";
|
||||
import { RoomMember } from "../models/room-member";
|
||||
import { SDPStreamMetadataPurpose } from "./callEventTypes";
|
||||
import { GroupCall, GroupCallEvent } from "./groupCall";
|
||||
|
||||
export enum GroupCallParticipantEvent {
|
||||
Speaking = "speaking",
|
||||
VolumeChanged = "volume_changed",
|
||||
MuteStateChanged = "mute_state_changed",
|
||||
Datachannel = "datachannel",
|
||||
CallReplaced = "call_replaced"
|
||||
}
|
||||
|
||||
export class GroupCallParticipant extends EventEmitter {
|
||||
public feeds: CallFeed[] = [];
|
||||
public activeSpeaker: boolean;
|
||||
public activeSpeakerSamples: number[];
|
||||
public dataChannel?: RTCDataChannel;
|
||||
|
||||
constructor(
|
||||
private groupCall: GroupCall,
|
||||
public member: RoomMember,
|
||||
// The session id is used to re-initiate calls if the user's participant
|
||||
// session id has changed
|
||||
public sessionId: string,
|
||||
public call?: MatrixCall,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.activeSpeakerSamples = Array(groupCall.activeSpeakerSampleCount).fill(
|
||||
-Infinity,
|
||||
);
|
||||
|
||||
if (this.call) {
|
||||
this.call.on(CallEvent.State, this.onCallStateChanged);
|
||||
this.call.on(CallEvent.FeedsChanged, this.onCallFeedsChanged);
|
||||
this.call.on(CallEvent.Replaced, this.onCallReplaced);
|
||||
this.call.on(CallEvent.Hangup, this.onCallHangup);
|
||||
}
|
||||
}
|
||||
|
||||
public replaceCall(call: MatrixCall, sessionId: string) {
|
||||
const oldCall = this.call;
|
||||
|
||||
if (this.call) {
|
||||
this.call.hangup(CallErrorCode.Replaced, false);
|
||||
this.call.removeListener(CallEvent.State, this.onCallStateChanged);
|
||||
this.call.removeListener(
|
||||
CallEvent.FeedsChanged,
|
||||
this.onCallFeedsChanged,
|
||||
);
|
||||
this.call.removeListener(CallEvent.Replaced, this.onCallReplaced);
|
||||
this.call.removeListener(CallEvent.Hangup, this.onCallHangup);
|
||||
this.call.removeListener(CallEvent.DataChannel, this.onCallDataChannel);
|
||||
}
|
||||
|
||||
this.call = call;
|
||||
this.member = call.getOpponentMember();
|
||||
this.activeSpeaker = false;
|
||||
this.sessionId = sessionId;
|
||||
|
||||
this.call.on(CallEvent.State, this.onCallStateChanged);
|
||||
this.call.on(CallEvent.FeedsChanged, this.onCallFeedsChanged);
|
||||
this.call.on(CallEvent.Replaced, this.onCallReplaced);
|
||||
this.call.on(CallEvent.Hangup, this.onCallHangup);
|
||||
this.call.on(CallEvent.DataChannel, this.onCallDataChannel);
|
||||
|
||||
this.groupCall.emit(GroupCallParticipantEvent.CallReplaced, this, oldCall, call);
|
||||
}
|
||||
|
||||
public get usermediaFeed() {
|
||||
return this.feeds.find((feed) => feed.purpose === SDPStreamMetadataPurpose.Usermedia);
|
||||
}
|
||||
|
||||
public get usermediaStream(): MediaStream {
|
||||
return this.usermediaFeed?.stream;
|
||||
}
|
||||
|
||||
public isAudioMuted(): boolean {
|
||||
const feed = this.usermediaFeed;
|
||||
|
||||
if (!feed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return feed.isAudioMuted();
|
||||
}
|
||||
|
||||
public isVideoMuted(): boolean {
|
||||
const feed = this.usermediaFeed;
|
||||
|
||||
if (!feed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return feed.isVideoMuted();
|
||||
}
|
||||
|
||||
private onCallStateChanged = (state) => {
|
||||
const call = this.call;
|
||||
const audioMuted = this.groupCall.localParticipant.isAudioMuted();
|
||||
|
||||
if (
|
||||
call.localUsermediaStream &&
|
||||
call.isMicrophoneMuted() !== audioMuted
|
||||
) {
|
||||
call.setMicrophoneMuted(audioMuted);
|
||||
}
|
||||
|
||||
const videoMuted = this.groupCall.localParticipant.isVideoMuted();
|
||||
|
||||
if (
|
||||
call.localUsermediaStream &&
|
||||
call.isLocalVideoMuted() !== videoMuted
|
||||
) {
|
||||
call.setLocalVideoMuted(videoMuted);
|
||||
}
|
||||
};
|
||||
|
||||
onCallFeedsChanged = () => {
|
||||
const oldFeeds = this.feeds;
|
||||
const newFeeds = this.call.getRemoteFeeds();
|
||||
|
||||
this.feeds = [];
|
||||
|
||||
for (const feed of newFeeds) {
|
||||
if (oldFeeds.includes(feed)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.addCallFeed(feed);
|
||||
}
|
||||
};
|
||||
|
||||
onCallReplaced = (newCall) => {
|
||||
// TODO: Should we always reuse the sessionId?
|
||||
this.replaceCall(newCall, this.sessionId);
|
||||
};
|
||||
|
||||
onCallHangup = () => {
|
||||
if (this.call.hangupReason === CallErrorCode.Replaced) {
|
||||
return;
|
||||
}
|
||||
|
||||
const participantIndex = this.groupCall.participants.indexOf(this);
|
||||
|
||||
if (participantIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.groupCall.participants.splice(participantIndex, 1);
|
||||
|
||||
if (
|
||||
this.groupCall.activeSpeaker === this &&
|
||||
this.groupCall.participants.length > 0
|
||||
) {
|
||||
this.groupCall.activeSpeaker = this.groupCall.participants[0];
|
||||
this.groupCall.activeSpeaker.activeSpeaker = true;
|
||||
this.groupCall.emit(GroupCallEvent.ActiveSpeakerChanged, this.groupCall.activeSpeaker);
|
||||
}
|
||||
|
||||
this.groupCall.emit(GroupCallEvent.ParticipantsChanged, this.groupCall.participants);
|
||||
};
|
||||
|
||||
addCallFeed(callFeed: CallFeed) {
|
||||
if (callFeed.purpose === SDPStreamMetadataPurpose.Usermedia) {
|
||||
callFeed.setSpeakingThreshold(this.groupCall.speakingThreshold);
|
||||
callFeed.measureVolumeActivity(true);
|
||||
callFeed.on(CallFeedEvent.Speaking, this.onCallFeedSpeaking);
|
||||
callFeed.on(
|
||||
CallFeedEvent.VolumeChanged,
|
||||
this.onCallFeedVolumeChanged,
|
||||
);
|
||||
callFeed.on(
|
||||
CallFeedEvent.MuteStateChanged,
|
||||
this.onCallFeedMuteStateChanged,
|
||||
);
|
||||
this.onCallFeedMuteStateChanged(
|
||||
this.isAudioMuted(),
|
||||
this.isVideoMuted(),
|
||||
);
|
||||
}
|
||||
|
||||
this.feeds.push(callFeed);
|
||||
}
|
||||
|
||||
onCallFeedSpeaking = (speaking: boolean) => {
|
||||
this.emit(GroupCallParticipantEvent.Speaking, speaking);
|
||||
};
|
||||
|
||||
onCallFeedVolumeChanged = (maxVolume: number) => {
|
||||
this.activeSpeakerSamples.shift();
|
||||
this.activeSpeakerSamples.push(maxVolume);
|
||||
this.emit(GroupCallParticipantEvent.VolumeChanged, maxVolume);
|
||||
};
|
||||
|
||||
onCallFeedMuteStateChanged = (audioMuted: boolean, videoMuted: boolean) => {
|
||||
if (audioMuted) {
|
||||
this.activeSpeakerSamples = Array(this.groupCall.activeSpeakerSampleCount).fill(
|
||||
-Infinity,
|
||||
);
|
||||
}
|
||||
|
||||
this.emit(GroupCallParticipantEvent.MuteStateChanged, audioMuted, videoMuted);
|
||||
};
|
||||
|
||||
onCallDataChannel = (dataChannel: RTCDataChannel) => {
|
||||
this.dataChannel = dataChannel;
|
||||
this.emit(GroupCallParticipantEvent.Datachannel, dataChannel);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user