[svn-commits] dvossel: branch dvossel/hd_confbridge r311467 - in /team/dvossel/hd_confbridg...

SVN commits to the Digium repositories svn-commits at lists.digium.com
Mon Mar 21 09:11:59 CDT 2011


Author: dvossel
Date: Mon Mar 21 09:11:51 2011
New Revision: 311467

URL: http://svnview.digium.com/svn/asterisk?view=rev&rev=311467
Log:
Adds the ability for every sound in confbridge to be customized.

Modified:
    team/dvossel/hd_confbridge/apps/app_confbridge.c
    team/dvossel/hd_confbridge/apps/confbridge/conf_config_parser.c
    team/dvossel/hd_confbridge/apps/confbridge/include/confbridge.h

Modified: team/dvossel/hd_confbridge/apps/app_confbridge.c
URL: http://svnview.digium.com/svn/asterisk/team/dvossel/hd_confbridge/apps/app_confbridge.c?view=diff&rev=311467&r1=311466&r2=311467
==============================================================================
--- team/dvossel/hd_confbridge/apps/app_confbridge.c (original)
+++ team/dvossel/hd_confbridge/apps/app_confbridge.c Mon Mar 21 09:11:51 2011
@@ -121,6 +121,51 @@
 	return (!strcasecmp(conference_bridge0->name, conference_bridge1->name) ? CMP_MATCH | CMP_STOP : 0);
 }
 
+/*! \brief Looks to see if sound file is stored in bridge profile sounds, if not
+ *  default sound is provided.*/
+static const char *conf_get_sound(enum conf_sounds sound, struct bridge_profile_sounds *custom_sounds)
+{
+	switch (sound) {
+	case CONF_SOUND_HAS_JOINED:
+		return S_OR(custom_sounds->hasjoin, "conf-hasjoin");
+	case CONF_SOUND_HAS_LEFT:
+		return S_OR(custom_sounds->hasleft, "conf-hasleft");
+	case CONF_SOUND_KICKED:
+		return S_OR(custom_sounds->kicked, "conf-kicked");
+	case CONF_SOUND_MUTED:
+		return S_OR(custom_sounds->muted, "conf-muted");
+	case CONF_SOUND_UNMUTED:
+		return S_OR(custom_sounds->unmuted, "conf-unmuted");
+	case CONF_SOUND_ONLY_ONE:
+		return S_OR(custom_sounds->onlyone, "conf-onlyone");
+	case CONF_SOUND_THERE_ARE:
+		return S_OR(custom_sounds->thereare, "conf-thereare");
+	case CONF_SOUND_OTHER_IN_PARTY:
+		return S_OR(custom_sounds->otherinparty, "conf-otherinparty");
+	case CONF_SOUND_PLACE_IN_CONF:
+		return S_OR(custom_sounds->placeintoconf, "conf-placeintoconf");
+	case CONF_SOUND_WAIT_FOR_LEADER:
+		return S_OR(custom_sounds->waitforleader, "conf-waitforleader");
+	case CONF_SOUND_LEADER_HAS_LEFT:
+		return S_OR(custom_sounds->leaderhasleft, "conf-leaderhasleft");
+	case CONF_SOUND_GET_PIN:
+		return S_OR(custom_sounds->getpin, "conf-getpin");
+	case CONF_SOUND_INVALID_PIN:
+		return S_OR(custom_sounds->invalidpin, "conf-invalidpin");
+	case CONF_SOUND_ONLY_PERSON:
+		return S_OR(custom_sounds->onlyperson, "conf-onlyperson");
+	case CONF_SOUND_LOCKED:
+		return S_OR(custom_sounds->locked, "conf-locked");
+	case CONF_SOUND_LOCKED_NOW:
+		return S_OR(custom_sounds->lockednow, "conf-lockednow");
+	case CONF_SOUND_UNLOCKED_NOW:
+		return S_OR(custom_sounds->unlockednow, "conf-unlockednow");
+	case CONF_SOUND_ERROR_MENU:
+		return S_OR(custom_sounds->errormenu, "conf-errormenu");
+	}
+	return "";
+}
+
 static struct ast_frame *rec_read(struct ast_channel *ast)
 {
 	return &ast_null_frame;
@@ -281,18 +326,24 @@
 		return;
 	} else if (conference_bridge->users == 2) {
 		/* Eep, there is one other person */
-		if (ast_stream_and_wait(conference_bridge_user->chan, "conf-onlyone", "")) {
+		if (ast_stream_and_wait(conference_bridge_user->chan,
+			conf_get_sound(CONF_SOUND_ONLY_ONE, conference_bridge_user->b_profile.sounds),
+			"")) {
 			return;
 		}
 	} else {
 		/* Alas multiple others in here */
-		if (ast_stream_and_wait(conference_bridge_user->chan, "conf-thereare", "")) {
+		if (ast_stream_and_wait(conference_bridge_user->chan,
+			conf_get_sound(CONF_SOUND_THERE_ARE, conference_bridge_user->b_profile.sounds),
+			"")) {
 			return;
 		}
 		if (ast_say_number(conference_bridge_user->chan, conference_bridge->users - 1, "", conference_bridge_user->chan->language, NULL)) {
 			return;
 		}
-		if (ast_stream_and_wait(conference_bridge_user->chan, "conf-otherinparty", "")) {
+		if (ast_stream_and_wait(conference_bridge_user->chan,
+			conf_get_sound(CONF_SOUND_OTHER_IN_PARTY, conference_bridge_user->b_profile.sounds),
+			"")) {
 			return;
 		}
 	}
@@ -349,7 +400,8 @@
 		if (!ast_test_flag(&conference_bridge_user->u_profile, USER_OPT_QUIET)) {
 			ao2_unlock(conference_bridge);
 			ast_autoservice_start(conference_bridge_user->chan);
-			play_sound_file(conference_bridge, "conf-placeintoconf");
+			play_sound_file(conference_bridge,
+				conf_get_sound(CONF_SOUND_PLACE_IN_CONF, conference_bridge_user->b_profile.sounds));
 			ast_autoservice_stop(conference_bridge_user->chan);
 			ao2_lock(conference_bridge);
 		}
@@ -371,7 +423,9 @@
 		conference_bridge_user->features.mute = 1;
 		/* If we have not been quieted play back that they are waiting for the leader */
 		if (!ast_test_flag(&conference_bridge_user->u_profile, USER_OPT_QUIET)) {
-			play_prompt_to_channel(conference_bridge, conference_bridge_user->chan, "conf-waitforleader");
+			play_prompt_to_channel(conference_bridge,
+				conference_bridge_user->chan,
+				conf_get_sound(CONF_SOUND_WAIT_FOR_LEADER, conference_bridge_user->b_profile.sounds));
 		}
 		/* Start music on hold if needed */
 		/* We need to recheck the markedusers value here. play_prompt_to_channel unlocks the conference bridge, potentially
@@ -397,7 +451,9 @@
 	if (conference_bridge->users == 1) {
 		/* If audio prompts have not been quieted or this prompt quieted play it on out */
 		if (!ast_test_flag(&conference_bridge_user->u_profile, USER_OPT_QUIET | USER_OPT_NOONLYPERSON)) {
-			play_prompt_to_channel(conference_bridge, conference_bridge_user->chan, "conf-onlyperson");
+			play_prompt_to_channel(conference_bridge,
+				conference_bridge_user->chan,
+				conf_get_sound(CONF_SOUND_ONLY_PERSON, conference_bridge_user->b_profile.sounds));
 		}
 		/* If we need to start music on hold on the channel do so now */
 		/* We need to re-check the number of users in the conference bridge here because another conference bridge
@@ -455,6 +511,7 @@
 		ast_bridge_destroy(conference_bridge->bridge);
 		conference_bridge->bridge = NULL;
 	}
+	conf_bridge_profile_destroy(&conference_bridge->b_profile);
 }
 
 /*!
@@ -501,7 +558,9 @@
 		ao2_unlock(conference_bridges);
 		ao2_ref(conference_bridge, -1);
 		ast_debug(1, "Conference bridge '%s' is locked and caller is not an admin\n", name);
-		ast_stream_and_wait(conference_bridge_user->chan, "conf-locked", "");
+		ast_stream_and_wait(conference_bridge_user->chan,
+				conf_get_sound(CONF_SOUND_LOCKED, conference_bridge_user->b_profile.sounds),
+				"");
 		return NULL;
 	}
 
@@ -517,7 +576,7 @@
 		/* Setup conference bridge parameters */
 		conference_bridge->record_thread = AST_PTHREADT_NULL;
 		ast_copy_string(conference_bridge->name, name, sizeof(conference_bridge->name));
-		memcpy(&conference_bridge->b_profile, &conference_bridge_user->b_profile, sizeof(conference_bridge->b_profile));
+		conf_bridge_profile_copy(&conference_bridge->b_profile, &conference_bridge_user->b_profile);
 
 		/* Create an actual bridge that will do the audio mixing */
 		if (!(conference_bridge->bridge = ast_bridge_new(AST_BRIDGE_CAPABILITY_MULTIMIX, 0))) {
@@ -618,7 +677,8 @@
 			if (!ast_test_flag(&conference_bridge_user->u_profile, USER_OPT_QUIET)) {
 				ao2_unlock(conference_bridge);
 				ast_autoservice_start(conference_bridge_user->chan);
-				play_sound_file(conference_bridge, "conf-leaderhasleft");
+				play_sound_file(conference_bridge,
+					conf_get_sound(CONF_SOUND_LEADER_HAS_LEFT, conference_bridge_user->b_profile.sounds));
 				ast_autoservice_stop(conference_bridge_user->chan);
 				ao2_lock(conference_bridge);
 			}
@@ -744,21 +804,26 @@
 	      bridge_channel->chan->name, bridge_channel->chan->uniqueid, conf_name, talking ? "on" : "off");
 }
 
-static int conf_get_pin(struct ast_channel *chan, const char *pin)
+static int conf_get_pin(struct ast_channel *chan, struct conference_bridge_user *conference_bridge_user)
 {
 	char pin_guess[MAX_PIN] = { 0, };
+	const char *pin = conference_bridge_user->u_profile.pin;
 	char *tmp = pin_guess;
 	int i, res;
 	unsigned int len = MAX_PIN - 1;
 
 	/* give them three tries to get the pin right */
 	for (i = 0; i < 3; i++) {
-		if (ast_app_getdata(chan, "conf-getpin", tmp, len, 0) >= 0) {
+		if (ast_app_getdata(chan,
+			conf_get_sound(CONF_SOUND_GET_PIN, conference_bridge_user->b_profile.sounds),
+			tmp, len, 0) >= 0) {
 			if (!strcasecmp(pin, pin_guess)) {
 				return 0;
 			}
 		}
-		ast_streamfile(chan, "conf-invalidpin", chan->language);
+		ast_streamfile(chan,
+			conf_get_sound(CONF_SOUND_INVALID_PIN, conference_bridge_user->b_profile.sounds),
+			chan->language);
 		res = ast_waitstream(chan, AST_DIGIT_ANY);
 		if (res > 0) {
 			pin_guess[0] = res;
@@ -857,7 +922,7 @@
 	/* ask for a PIN immediately after finding user profile.  This has to be
 	 * prompted for requardless of quiet setting. */
 	if (!ast_strlen_zero(conference_bridge_user.u_profile.pin)) {
-		if (conf_get_pin(chan, conference_bridge_user.u_profile.pin)) {
+		if (conf_get_pin(chan, &conference_bridge_user)) {
 			res = -1; /* invalid PIN */
 			goto confbridge_cleanup;
 		}
@@ -939,7 +1004,8 @@
 	if (!ast_strlen_zero(conference_bridge_user.name_rec_location)) {
 		ast_autoservice_start(chan);
 		play_sound_file(conference_bridge, conference_bridge_user.name_rec_location);
-		play_sound_file(conference_bridge, "conf-hasjoin");
+		play_sound_file(conference_bridge,
+			conf_get_sound(CONF_SOUND_HAS_JOINED, conference_bridge_user.b_profile.sounds));
 		ast_autoservice_stop(chan);
 	}
 
@@ -961,7 +1027,8 @@
 	if (!quiet && !ast_strlen_zero(conference_bridge_user.name_rec_location)) {
 		ast_autoservice_start(chan);
 		play_sound_file(conference_bridge, conference_bridge_user.name_rec_location);
-		play_sound_file(conference_bridge, "conf-hasleft");
+		play_sound_file(conference_bridge,
+			conf_get_sound(CONF_SOUND_HAS_LEFT, conference_bridge_user.b_profile.sounds));
 		ast_autoservice_stop(chan);
 	}
 
@@ -981,7 +1048,9 @@
 
 	/* If the user was kicked from the conference play back the audio prompt for it */
 	if (!quiet && conference_bridge_user.kicked) {
-		res = ast_stream_and_wait(chan, "conf-kicked", "");
+		res = ast_stream_and_wait(chan,
+			conf_get_sound(CONF_SOUND_KICKED, conference_bridge_user.b_profile.sounds),
+			"");
 	}
 
 	/* Restore volume adjustments to previous values in case they were changed */
@@ -998,7 +1067,7 @@
 
 confbridge_cleanup:
 	ast_bridge_features_cleanup(&conference_bridge_user.features);
-
+	conf_bridge_profile_destroy(&conference_bridge_user.b_profile);
 	return res;
 }
 
@@ -1010,7 +1079,10 @@
 	if (!ast_test_flag(&conference_bridge_user->u_profile, USER_OPT_WAITMARKED) || conference_bridge->markedusers) {
 		conference_bridge_user->features.mute = (!conference_bridge_user->features.mute ? 1 : 0);
 	}
-	return ast_stream_and_wait(bridge_channel->chan, (conference_bridge_user->features.mute ? "conf-muted" : "conf-unmuted"), "");
+	return ast_stream_and_wait(bridge_channel->chan, (conference_bridge_user->features.mute ?
+		conf_get_sound(CONF_SOUND_MUTED, conference_bridge_user->b_profile.sounds) :
+		conf_get_sound(CONF_SOUND_UNMUTED, conference_bridge_user->b_profile.sounds)),
+		"");
 }
 
 static int action_playback(struct ast_bridge_channel *bridge_channel, const char *playback_file)
@@ -1176,7 +1248,11 @@
 			}
 			conference_bridge->locked = (!conference_bridge->locked ? 1 : 0);
 			res |= ast_stream_and_wait(bridge_channel->chan,
-				(conference_bridge->locked ? "conf-lockednow" : "conf-unlockednow"), "");
+				(conference_bridge->locked ?
+				conf_get_sound(CONF_SOUND_LOCKED_NOW, conference_bridge_user->b_profile.sounds) :
+				conf_get_sound(CONF_SOUND_UNLOCKED_NOW, conference_bridge_user->b_profile.sounds)),
+				"");
+
 			break;
 		case MENU_ACTION_ADMIN_KICK_LAST:
 			if (!isadmin) {
@@ -1186,7 +1262,10 @@
 			if (((last_participant = AST_LIST_LAST(&conference_bridge->users_list)) == conference_bridge_user)
 				|| (ast_test_flag(&last_participant->u_profile, USER_OPT_ADMIN))) {
 				ao2_unlock(conference_bridge);
-				res = ast_stream_and_wait(bridge_channel->chan, "conf-errormenu", "");
+				res = ast_stream_and_wait(bridge_channel->chan,
+					conf_get_sound(CONF_SOUND_ERROR_MENU, conference_bridge_user->b_profile.sounds),
+					"");
+
 			} else if (last_participant) {
 				last_participant->kicked = 1;
 				ast_bridge_remove(conference_bridge->bridge, last_participant->chan);

Modified: team/dvossel/hd_confbridge/apps/confbridge/conf_config_parser.c
URL: http://svnview.digium.com/svn/asterisk/team/dvossel/hd_confbridge/apps/confbridge/conf_config_parser.c?view=diff&rev=311467&r1=311466&r2=311467
==============================================================================
--- team/dvossel/hd_confbridge/apps/confbridge/conf_config_parser.c (original)
+++ team/dvossel/hd_confbridge/apps/confbridge/conf_config_parser.c Mon Mar 21 09:11:51 2011
@@ -32,6 +32,7 @@
 #include "asterisk/astobj2.h"
 #include "asterisk/cli.h"
 #include "asterisk/bridging_features.h"
+#include "asterisk/stringfields.h"
 
 #define CONF_CONFIG "confbridge.conf"
 
@@ -119,6 +120,43 @@
 {
 	struct user_profile *entry = obj;
 	return entry->delme ? CMP_MATCH : 0;
+}
+
+/*! Bridge Profile Sounds functions */
+static void bridge_profile_sounds_destroy_cb(void *obj)
+{
+	struct bridge_profile_sounds *sounds = ao2_alloc(sizeof(*sounds), bridge_profile_sounds_destroy_cb);
+	ast_string_field_free_memory(sounds);
+}
+
+static struct bridge_profile_sounds *bridge_profile_sounds_alloc(void)
+{
+	struct bridge_profile_sounds *sounds = ao2_alloc(sizeof(*sounds), bridge_profile_sounds_destroy_cb);
+
+	if (!sounds) {
+		return NULL;
+	}
+	if (ast_string_field_init(sounds, 1024)) {
+		ao2_ref(sounds, -1);
+		return NULL;
+	}
+
+	return sounds;
+}
+
+static int set_sound(const char *sound_name, const char *sound_file, struct bridge_profile_sounds *sounds)
+{
+	if (ast_strlen_zero(sound_file)) {
+		return -1;
+	}
+
+	if (!strcasecmp(sound_name, "sound_onlyperson")) {
+		ast_string_field_set(sounds, onlyperson, sound_file);
+	} else {
+		return -1;
+	}
+
+	return 0;
 }
 
 /*!
@@ -145,17 +183,26 @@
 	b_profile->internal_sample_rate = 0;
 	b_profile->flags = 0;
 	b_profile->max_members = 0;
+	ao2_ref(b_profile->sounds, -1); /* sounds is read only.  Once it has been created
+	                                 * it can never be altered. This prevents having to
+	                                 * do any locking after it is built from the config. */
+	if (!(b_profile->sounds = bridge_profile_sounds_alloc())) {
+		ao2_unlock(b_profile);
+		ao2_ref(b_profile, -1);
+		ao2_unlink(bridge_profiles, b_profile);
+		return -1;
+	}
 
 	for (var = ast_variable_browse(cfg, cat); var; var = var->next) {
-		if (!strcasecmp(var->name, "internal_sample_rate")) {
+		if (!strcasecmp(var->name, "type")) {
+			continue;
+		} else if (!strcasecmp(var->name, "internal_sample_rate")) {
 			if (!strcasecmp(var->value, "auto")) {
 				b_profile->internal_sample_rate = 0;
 			} else if (sscanf(var->value, "%30u", &b_profile->internal_sample_rate) != 1) {
 				ast_log(LOG_WARNING, "internal_sample_rate '%s' at line %d of %s is not supported.\n",
 						var->value, var->lineno, CONF_CONFIG);
 			}
-		} else if (!strcasecmp(var->name, "type")) {
-			continue;
 		} else if (!strcasecmp(var->name, "record_conference")) {
 			b_profile->flags = ast_true(var->value) ?
 				b_profile->flags | BRIDGE_OPT_RECORD_CONFERENCE :
@@ -163,6 +210,11 @@
 		} else if (!strcasecmp(var->name, "max_members")) {
 			if (sscanf(var->value, "%30u", &b_profile->max_members) != 1) {
 				ast_log(LOG_WARNING, "max_members '%s' at line %d of %s is not supported.\n",
+						var->value, var->lineno, CONF_CONFIG);
+			}
+		} else if (strlen(var->name) >= 5 && !strncasecmp(var->name, "sound", 8)) {
+			if (set_sound(var->name, var->value, b_profile->sounds)) {
+				ast_log(LOG_WARNING, "'%s' at line %d of %s is not supported.\n",
 						var->value, var->lineno, CONF_CONFIG);
 			}
 		} else {
@@ -600,6 +652,7 @@
 	ast_cli(a->fd,"Internal Sample Rate: %s\n", tmp);
 	ast_cli(a->fd,"\n");
 
+	conf_bridge_profile_destroy(&b_profile);
 	return CLI_SUCCESS;
 }
 
@@ -827,6 +880,22 @@
 	return result;
 }
 
+void conf_bridge_profile_copy(struct bridge_profile *dst, struct bridge_profile *src)
+{
+	memcpy(dst, src, sizeof(*dst));
+	if (src->sounds) {
+		ao2_ref(src->sounds, +1);
+	}
+}
+
+void conf_bridge_profile_destroy(struct bridge_profile *b_profile)
+{
+	if (b_profile->sounds) {
+		ao2_ref(b_profile->sounds, -1);
+		b_profile->sounds = NULL;
+	}
+}
+
 const struct bridge_profile *conf_find_bridge_profile(const char *bridge_profile_name, struct bridge_profile *result)
 {
 	struct bridge_profile tmp;
@@ -837,7 +906,7 @@
 		return NULL;
 	}
 	ao2_lock(tmp2);
-	memcpy(result, tmp2, sizeof(*result));
+	conf_bridge_profile_copy(result, tmp2);
 	ao2_unlock(tmp2);
 	ao2_ref(tmp2, -1);
 

Modified: team/dvossel/hd_confbridge/apps/confbridge/include/confbridge.h
URL: http://svnview.digium.com/svn/asterisk/team/dvossel/hd_confbridge/apps/confbridge/include/confbridge.h?view=diff&rev=311467&r1=311466&r2=311467
==============================================================================
--- team/dvossel/hd_confbridge/apps/confbridge/include/confbridge.h (original)
+++ team/dvossel/hd_confbridge/apps/confbridge/include/confbridge.h Mon Mar 21 09:11:51 2011
@@ -111,11 +111,56 @@
 	int delme;
 };
 
+enum conf_sounds {
+	CONF_SOUND_HAS_JOINED,
+	CONF_SOUND_HAS_LEFT,
+	CONF_SOUND_KICKED,
+	CONF_SOUND_MUTED,
+	CONF_SOUND_UNMUTED,
+	CONF_SOUND_ONLY_ONE,
+	CONF_SOUND_THERE_ARE,
+	CONF_SOUND_OTHER_IN_PARTY,
+	CONF_SOUND_PLACE_IN_CONF,
+	CONF_SOUND_WAIT_FOR_LEADER,
+	CONF_SOUND_LEADER_HAS_LEFT,
+	CONF_SOUND_GET_PIN,
+	CONF_SOUND_INVALID_PIN,
+	CONF_SOUND_ONLY_PERSON,
+	CONF_SOUND_LOCKED,
+	CONF_SOUND_LOCKED_NOW,
+	CONF_SOUND_UNLOCKED_NOW,
+	CONF_SOUND_ERROR_MENU,
+};
+
+struct bridge_profile_sounds {
+	AST_DECLARE_STRING_FIELDS(
+		AST_STRING_FIELD(hasjoin);
+		AST_STRING_FIELD(hasleft);
+		AST_STRING_FIELD(kicked);
+		AST_STRING_FIELD(muted);
+		AST_STRING_FIELD(unmuted);
+		AST_STRING_FIELD(onlyone);
+		AST_STRING_FIELD(thereare);
+		AST_STRING_FIELD(otherinparty);
+		AST_STRING_FIELD(placeintoconf);
+		AST_STRING_FIELD(waitforleader);
+		AST_STRING_FIELD(leaderhasleft);
+		AST_STRING_FIELD(getpin);
+		AST_STRING_FIELD(invalidpin);
+		AST_STRING_FIELD(onlyperson);
+		AST_STRING_FIELD(locked);
+		AST_STRING_FIELD(lockednow);
+		AST_STRING_FIELD(unlockednow);
+		AST_STRING_FIELD(errormenu);
+	);
+};
+
 struct bridge_profile {
 	char name[64];
 	unsigned int flags;
 	unsigned int max_members;          /*!< The maximum number of participants allowed in the conference */
 	unsigned int internal_sample_rate; /*!< The internal sample rate of the bridge. 0 when set to auto adjust mode. */
+	struct bridge_profile_sounds *sounds;
 	int delme;
 };
 
@@ -166,10 +211,24 @@
 /*!
  * \brief find a bridge profile given a bridge profile's name.
  *
+ * \details Any bridge profile found using this function must be
+ * destroyed using conf_bridge_profile_destroy.
+ *
  * \retval Bridge profile on success
  * \retval NULL on failure
  */
 const struct bridge_profile *conf_find_bridge_profile(const char *bridge_profile_name, struct bridge_profile *result);
+
+/*!
+ * \brief Destroy a bridge profile found by 'conf_find_bridge_profile'
+ */
+void conf_bridge_profile_destroy(struct bridge_profile *b_profile);
+
+/*!
+ * \brief copies a bridge profile
+ * \note conf_bridge_profile_destroy must be called on the dst structure
+ */
+void conf_bridge_profile_copy(struct bridge_profile *dst, struct bridge_profile *src);
 
 /*!
  * \brief Set a DTMF menu to a conference user by menu name.




More information about the svn-commits mailing list