mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2025-12-20 08:40:38 +00:00
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:
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -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
|
||||||
|
|||||||
@@ -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
14
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user