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

@@ -6,8 +6,8 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-node@v3 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
- run: npm ci - run: npm ci

View File

@@ -60,6 +60,8 @@ class Conference extends Task {
this.emitter = new Emitter(); this.emitter = new Emitter();
this.results = {}; this.results = {};
this.coaching = [];
this.speakOnlyTo = this.data.speakOnlyTo;
// transferred from another server in order to bridge to a local caller? // transferred from another server in order to bridge to a local caller?
if (this.data._ && this.data._.connectTime) { if (this.data._ && this.data._.connectTime) {
@@ -348,7 +350,7 @@ class Conference extends Task {
Object.assign(opts, {flags: { Object.assign(opts, {flags: {
...(this.endConferenceOnExit && {endconf: true}), ...(this.endConferenceOnExit && {endconf: true}),
...(this.startConferenceOnEnter && {moderator: 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 { try {
const {memberId, confUuid} = await this.ep.join(this.confName, opts); const {memberId, confUuid} = await this.ep.join(this.confName, opts);
this.logger.debug({memberId, confUuid}, `Conference:_joinConference: successfully joined ${this.confName}`); this.logger.debug({memberId, confUuid}, `Conference:_joinConference: successfully joined ${this.confName}`);
this.memberId = memberId; this.memberId = parseInt(memberId, 10);
this.confUuid = confUuid; this.confUuid = confUuid;
// set a tag for this member, if provided // set a tag for this member, if provided
@@ -395,8 +397,8 @@ class Conference extends Task {
.catch((err) => {}); .catch((err) => {});
} }
if (this.data.speakOnlyTo) { if (this.speakOnlyTo) {
this.setCoachMode(this.data.speakOnlyTo); this.setCoachMode(this.speakOnlyTo);
} }
} catch (err) { } catch (err) {
this.logger.error(err, `Failed to join conference ${this.confName}`); 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']); const response = await this.ep.api('conference', [this.confName, 'get', 'count']);
if (response.body && confNoMatch(response.body)) this.participantCount = 0; if (response.body && confNoMatch(response.body)) this.participantCount = 0;
else if (response.body && /^\d+$/.test(response.body)) this.participantCount = parseInt(response.body) - 1; 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) { } catch (err) {
this.logger.info({err}, 'Conference:_doFinalMemberCheck error retrieving count (we were probably kicked'); this.logger.info({err}, 'Conference:_doFinalMemberCheck error retrieving count (we were probably kicked');
} }
@@ -699,7 +701,12 @@ class Conference extends Task {
// conference event handlers // conference event handlers
_onAddMember(logger, cs, evt) { _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) { _onDelMember(logger, cs, evt) {
const memberId = parseInt(evt.getHeader('Member-ID')) ; 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) { async setCoachMode(speakOnlyTo) {
this.speakOnlyTo = speakOnlyTo;
if (!this.memberId) {
this.logger.info('Conference:_setCoachMode: no member id yet');
return;
}
try { try {
const response = await this.ep.api('conference', [this.confName, 'gettag', speakOnlyTo, 'nomatch']); const members = (await this.ep.getNonMatchingConfParticipants(this.confName, speakOnlyTo))
this.logger.info(`Conference:_setCoachMode: my audio will only be sent to particpants ${response}`); .filter((m) => m !== this.memberId);
await this.ep.api('conference', [this.confName, 'relate', this.memberId, response, 'nospeak']); if (members.length === 0) {
this.speakOnlyTo = speakOnlyTo; this.logger.info({members}, 'Conference:_setCoachMode: all participants have the tag, so all will hear me');
this.coaching = response; 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) { } catch (err) {
this.logger.error({err, speakOnlyTo}, '_setCoachMode: Error'); this.logger.error({err, speakOnlyTo}, '_setCoachMode: Error');
} }
@@ -748,14 +788,14 @@ class Conference extends Task {
async clearCoachMode() { async clearCoachMode() {
try { try {
if (!this.coaching) { if (this.coaching.length === 0) {
this.logger.info('Conference:_clearCoachMode: no coaching mode to clear'); this.logger.info('Conference:_clearCoachMode: no coaching mode to clear');
return; return;
} }
this.logger.info(`Conference:_clearCoachMode: now sending my audio to all, including ${this.coaching}`); 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']); await this.ep.api('conference', [this.confName, 'relate', this.memberId, this.coaching, 'clear']);
this.speakOnlyTo = null; this.speakOnlyTo = null;
this.coaching = null; this.coaching = [];
} catch (err) { } catch (err) {
this.logger.error({err}, '_clearCoachMode: Error'); this.logger.error({err}, '_clearCoachMode: Error');
} }

14
package-lock.json generated
View File

@@ -31,7 +31,7 @@
"bent": "^7.3.12", "bent": "^7.3.12",
"debug": "^4.3.4", "debug": "^4.3.4",
"deepcopy": "^2.1.0", "deepcopy": "^2.1.0",
"drachtio-fsmrf": "^3.0.40", "drachtio-fsmrf": "^3.0.41",
"drachtio-srf": "^4.5.31", "drachtio-srf": "^4.5.31",
"express": "^4.19.2", "express": "^4.19.2",
"express-validator": "^7.0.1", "express-validator": "^7.0.1",
@@ -4606,9 +4606,9 @@
} }
}, },
"node_modules/drachtio-fsmrf": { "node_modules/drachtio-fsmrf": {
"version": "3.0.40", "version": "3.0.41",
"resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-3.0.40.tgz", "resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-3.0.41.tgz",
"integrity": "sha512-Mlteu/e1fa1Y4ClkVehMGnto+AKp5NhgIgwKSkFlaCi7Xl8qOqK5IbzgHyPZ2pDE2q7euieNGo+vtB2dUMIIog==", "integrity": "sha512-LemyjXtOnd5tOcQSmMgGtKUSdFAM3pLkEwGtV1pRnRhLTS3oERqQwuqRv0LurfZQ38WpuMOrUBhCSb3+uW9t/w==",
"dependencies": { "dependencies": {
"camel-case": "^4.1.2", "camel-case": "^4.1.2",
"debug": "^2.6.9", "debug": "^2.6.9",
@@ -13678,9 +13678,9 @@
} }
}, },
"drachtio-fsmrf": { "drachtio-fsmrf": {
"version": "3.0.40", "version": "3.0.41",
"resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-3.0.40.tgz", "resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-3.0.41.tgz",
"integrity": "sha512-Mlteu/e1fa1Y4ClkVehMGnto+AKp5NhgIgwKSkFlaCi7Xl8qOqK5IbzgHyPZ2pDE2q7euieNGo+vtB2dUMIIog==", "integrity": "sha512-LemyjXtOnd5tOcQSmMgGtKUSdFAM3pLkEwGtV1pRnRhLTS3oERqQwuqRv0LurfZQ38WpuMOrUBhCSb3+uW9t/w==",
"requires": { "requires": {
"camel-case": "^4.1.2", "camel-case": "^4.1.2",
"debug": "^2.6.9", "debug": "^2.6.9",

View File

@@ -47,7 +47,7 @@
"bent": "^7.3.12", "bent": "^7.3.12",
"debug": "^4.3.4", "debug": "^4.3.4",
"deepcopy": "^2.1.0", "deepcopy": "^2.1.0",
"drachtio-fsmrf": "^3.0.40", "drachtio-fsmrf": "^3.0.41",
"drachtio-srf": "^4.5.31", "drachtio-srf": "^4.5.31",
"express": "^4.19.2", "express": "^4.19.2",
"express-validator": "^7.0.1", "express-validator": "^7.0.1",