Feat/advanced conferencing features (#730)

* update drachtio-fsmrf and fixes to setCoachMode

* wip

* wip

* wip

* wip

* wip

* update gh actions
This commit is contained in:
Dave Horton
2024-04-22 11:00:05 -04:00
committed by GitHub
parent 8d2b60c284
commit d474b9d604
4 changed files with 63 additions and 23 deletions

View File

@@ -60,6 +60,8 @@ class Conference extends Task {
this.emitter = new Emitter();
this.results = {};
this.coaching = [];
this.speakOnlyTo = this.data.speakOnlyTo;
// transferred from another server in order to bridge to a local caller?
if (this.data._ && this.data._.connectTime) {
@@ -348,7 +350,7 @@ class Conference extends Task {
Object.assign(opts, {flags: {
...(this.endConferenceOnExit && {endconf: true}),
...(this.startConferenceOnEnter && {moderator: true}),
...((this.joinMuted || this.data.speakOnlyTo) && {joinMuted: true}),
...((this.joinMuted || this.speakOnlyTo) && {joinMuted: true}),
}});
/**
@@ -361,7 +363,7 @@ class Conference extends Task {
try {
const {memberId, confUuid} = await this.ep.join(this.confName, opts);
this.logger.debug({memberId, confUuid}, `Conference:_joinConference: successfully joined ${this.confName}`);
this.memberId = memberId;
this.memberId = parseInt(memberId, 10);
this.confUuid = confUuid;
// set a tag for this member, if provided
@@ -395,8 +397,8 @@ class Conference extends Task {
.catch((err) => {});
}
if (this.data.speakOnlyTo) {
this.setCoachMode(this.data.speakOnlyTo);
if (this.speakOnlyTo) {
this.setCoachMode(this.speakOnlyTo);
}
} catch (err) {
this.logger.error(err, `Failed to join conference ${this.confName}`);
@@ -586,7 +588,7 @@ class Conference extends Task {
const response = await this.ep.api('conference', [this.confName, 'get', 'count']);
if (response.body && confNoMatch(response.body)) this.participantCount = 0;
else if (response.body && /^\d+$/.test(response.body)) this.participantCount = parseInt(response.body) - 1;
this.logger.debug({response}, `Conference:_doFinalMemberCheck conference count ${this.participantCount}`);
this.logger.debug(`Conference:_doFinalMemberCheck conference count ${this.participantCount}`);
} catch (err) {
this.logger.info({err}, 'Conference:_doFinalMemberCheck error retrieving count (we were probably kicked');
}
@@ -699,7 +701,12 @@ class Conference extends Task {
// conference event handlers
_onAddMember(logger, cs, evt) {
logger.debug({evt}, `Conference:_onAddMember - member added to conference ${this.confName}`);
const memberId = parseInt(evt.getHeader('Member-ID')) ;
if (this.speakOnlyTo) {
logger.debug(`Conference:_onAddMember - member ${memberId} added to ${this.confName}, updating coaching mode`);
this.setCoachMode(this.speakOnlyTo).catch(() => {});
}
else logger.debug(`Conference:_onAddMember - member ${memberId} added to conference ${this.confName}`);
}
_onDelMember(logger, cs, evt) {
const memberId = parseInt(evt.getHeader('Member-ID')) ;
@@ -734,13 +741,46 @@ class Conference extends Task {
}
}
_onTag(logger, cs, evt) {
const memberId = parseInt(evt.getHeader('Member-ID')) ;
const tag = evt.getHeader('Tag') || '';
if (memberId !== this.memberId && this.speakOnlyTo) {
logger.info(`Conference:_onTag - member ${memberId} set tag to '${tag }'; updating coach mode accordingly`);
this.setCoachMode(this.speakOnlyTo).catch(() => {});
}
}
/**
* Set the conference to "coaching" mode, where the audio of the participant is only heard
* by a subset of the participants in the conference.
* We do this by first getting all of the members who do *not* have this tag, and then
* we configure this members audio to not be sent to them.
* @param {string} speakOnlyTo - tag of the members who should receive our audio
*
* N.B.: this feature requires jambonz patches to freeswitch mod_conference
*/
async setCoachMode(speakOnlyTo) {
this.speakOnlyTo = speakOnlyTo;
if (!this.memberId) {
this.logger.info('Conference:_setCoachMode: no member id yet');
return;
}
try {
const response = await this.ep.api('conference', [this.confName, 'gettag', speakOnlyTo, 'nomatch']);
this.logger.info(`Conference:_setCoachMode: my audio will only be sent to particpants ${response}`);
await this.ep.api('conference', [this.confName, 'relate', this.memberId, response, 'nospeak']);
this.speakOnlyTo = speakOnlyTo;
this.coaching = response;
const members = (await this.ep.getNonMatchingConfParticipants(this.confName, speakOnlyTo))
.filter((m) => m !== this.memberId);
if (members.length === 0) {
this.logger.info({members}, 'Conference:_setCoachMode: all participants have the tag, so all will hear me');
if (this.coaching.length) {
await this.ep.api('conference', [this.confName, 'relate', this.memberId, this.coaching.join(','), 'clear']);
this.coaching = [];
}
}
else {
const memberList = members.join(',');
this.logger.info(`Conference:_setCoachMode: my audio will NOT be sent to ${memberList}`);
await this.ep.api('conference', [this.confName, 'relate', this.memberId, memberList, 'nospeak']);
this.coaching = members;
}
} catch (err) {
this.logger.error({err, speakOnlyTo}, '_setCoachMode: Error');
}
@@ -748,14 +788,14 @@ class Conference extends Task {
async clearCoachMode() {
try {
if (!this.coaching) {
if (this.coaching.length === 0) {
this.logger.info('Conference:_clearCoachMode: no coaching mode to clear');
return;
}
this.logger.info(`Conference:_clearCoachMode: now sending my audio to all, including ${this.coaching}`);
await this.ep.api('conference', [this.confName, 'relate', this.memberId, this.coaching, 'clear']);
this.speakOnlyTo = null;
this.coaching = null;
this.coaching = [];
} catch (err) {
this.logger.error({err}, '_clearCoachMode: Error');
}