Skip to content
Snippets Groups Projects
app_confbridge.c 132 KiB
Newer Older
  • Learn to ignore specific revisions
  • struct async_playback_task_data {
    	struct confbridge_conference *conference;
    	int say_number;
    	struct ast_channel *initiator;
    	char filename[0];
    };
    
    struct async_datastore_data {
    	ast_mutex_t lock;
    	ast_cond_t cond;
    	int wait;
    };
    
    static void async_datastore_data_destroy(void *data)
    {
    	struct async_datastore_data *add = data;
    
    	ast_mutex_destroy(&add->lock);
    	ast_cond_destroy(&add->cond);
    
    	ast_free(add);
    }
    
    /*!
     * \brief Datastore used for timing of async announcement playback
     *
     * Announcements that are played to the entire conference can be played
     * asynchronously (i.e. The channel that queues the playback does not wait
     * for the playback to complete before continuing)
     *
     * The thing about async announcements is that the channel that queues the
     * announcement is either not in the bridge or is in some other way "occupied"
     * at the time the announcement is queued. Because of that, the initiator of
     * the announcement may enter after the announcement has already started,
     * resulting in the sound being "clipped".
     *
     * This datastore makes it so that the channel that queues the async announcement
     * can say "I'm ready now". This way the announcement does not start until the
     * initiator of the announcement is ready to hear the sound.
     */
    static struct ast_datastore_info async_datastore_info = {
    	.type = "Confbridge async playback",
    	.destroy = async_datastore_data_destroy,
    };
    
    static struct async_datastore_data *async_datastore_data_alloc(void)
    {
    	struct async_datastore_data *add;
    
    	add = ast_malloc(sizeof(*add));
    	if (!add) {
    		return NULL;
    	}
    
    	ast_mutex_init(&add->lock);
    	ast_cond_init(&add->cond, NULL);
    	add->wait = 1;
    
    	return add;
    }
    
    /*!
     * \brief Prepare the async playback datastore
     *
     * This is done prior to queuing an async announcement. If the
     * datastore has not yet been created, it is allocated and initialized.
     * If it already exists, we set it to be in "waiting" mode.
     *
     * \param initiator The channel that is queuing the async playback
     * \retval 0 Success
     * \retval -1 Failure :(
     */
    static int setup_async_playback_datastore(struct ast_channel *initiator)
    {
    	struct ast_datastore *async_datastore;
    
    	async_datastore = ast_channel_datastore_find(initiator, &async_datastore_info, NULL);
    	if (async_datastore) {
    		struct async_datastore_data *add;
    
    		add = async_datastore->data;
    		add->wait = 1;
    
    		return 0;
    	}
    
    	async_datastore = ast_datastore_alloc(&async_datastore_info, NULL);
    	if (!async_datastore) {
    		return -1;
    	}
    
    	async_datastore->data = async_datastore_data_alloc();
    	if (!async_datastore->data) {
    		ast_datastore_free(async_datastore);
    		return -1;
    	}
    
    	ast_channel_datastore_add(initiator, async_datastore);
    	return 0;
    }
    
    static struct async_playback_task_data *async_playback_task_data_alloc(
    	struct confbridge_conference *conference, const char *filename, int say_number,
    	struct ast_channel *initiator)
    {
    	struct async_playback_task_data *aptd;
    
    	aptd = ast_malloc(sizeof(*aptd) + strlen(filename) + 1);
    	if (!aptd) {
    		return NULL;
    	}
    
    	/* Safe */
    	strcpy(aptd->filename, filename);
    	aptd->say_number = say_number;
    
    	/* You may think that we need to bump the conference refcount since we are pushing
    	 * this task to the taskprocessor.
    	 *
    	 * In this case, that actually causes a problem. The destructor for the conference
    	 * pushes a hangup task into the taskprocessor and waits for it to complete before
    	 * continuing. If the destructor gets called from a taskprocessor task, we're
    	 * deadlocked.
    	 *
    	 * So is there a risk of the conference being freed out from under us? No. Since
    	 * the destructor pushes a task into the taskprocessor and waits for it to complete,
    	 * the destructor cannot free the conference out from under us. No further tasks
    	 * can be queued onto the taskprocessor after the hangup since no channels are referencing
    	 * the conference at that point any more.
    	 */
    	aptd->conference = conference;
    
    	aptd->initiator = initiator;
    	if (initiator) {
    		ast_channel_ref(initiator);
    		ast_channel_lock(aptd->initiator);
    		/* We don't really care if this fails. If the datastore fails to get set up
    		 * we'll still play the announcement. It's possible that the sound will be
    		 * clipped for the initiator, but that's not the end of the world.
    		 */
    		setup_async_playback_datastore(aptd->initiator);
    		ast_channel_unlock(aptd->initiator);
    	}
    
    	return aptd;
    }
    
    static void async_playback_task_data_destroy(struct async_playback_task_data *aptd)
    {
    	ast_channel_cleanup(aptd->initiator);
    	ast_free(aptd);
    }
    
    /*!
     * \brief Wait for the initiator of an async playback to be ready
     *
     * See the description on the async_datastore_info structure for more
     * information about what this is about.
     *
     * \param initiator The channel that queued the async announcement
     */
    static void wait_for_initiator(struct ast_channel *initiator)
    {
    	struct ast_datastore *async_datastore;
    	struct async_datastore_data *add;
    
    	ast_channel_lock(initiator);
    	async_datastore = ast_channel_datastore_find(initiator, &async_datastore_info, NULL);
    	ast_channel_unlock(initiator);
    
    	if (!async_datastore) {
    		return;
    	}
    
    	add = async_datastore->data;
    
    	ast_mutex_lock(&add->lock);
    	while (add->wait) {
    		ast_cond_wait(&add->cond, &add->lock);
    	}
    	ast_mutex_unlock(&add->lock);
    }
    
    /*!
     * \brief Play an announcement into a confbridge asynchronously
     *
     * This runs in the playback queue taskprocessor. This ensures that
     * all playbacks are handled in sequence and do not play over top one
     * another.
     *
     * \param data An async_playback_task_data
     * \return 0
     */
    static int async_playback_task(void *data)
    {
    	struct async_playback_task_data *aptd = data;
    
    	/* Wait for the initiator to get back in the bridge or be hung up */
    	if (aptd->initiator) {
    		wait_for_initiator(aptd->initiator);
    	}
    
    	playback_common(aptd->conference, aptd->filename, aptd->say_number);
    
    	async_playback_task_data_destroy(aptd);
    	return 0;
    }
    
    static int async_play_sound_helper(struct confbridge_conference *conference,
    	const char *filename, int say_number, struct ast_channel *initiator)
    {
    	struct async_playback_task_data *aptd;
    
    	/* Do not waste resources trying to play files that do not exist */
    	if (ast_strlen_zero(filename)) {
    		if (say_number < 0) {
    			return 0;
    		}
    	} else if (!sound_file_exists(filename)) {
    		return 0;
    	}
    
    	aptd = async_playback_task_data_alloc(conference, filename, say_number, initiator);
    	if (!aptd) {
    		return -1;
    	}
    
    	if (ast_taskprocessor_push(conference->playback_queue, async_playback_task, aptd)) {
    		if (!ast_strlen_zero(filename)) {
    			ast_log(LOG_WARNING, "Unable to play file '%s' to conference '%s'\n",
    				filename, conference->name);
    		} else {
    			ast_log(LOG_WARNING, "Unable to say number '%d' to conference '%s'\n",
    				say_number, conference->name);
    		}
    		async_playback_task_data_destroy(aptd);
    		return -1;
    	}
    
    	return 0;
    }
    
    int async_play_sound_file(struct confbridge_conference *conference,
    	const char *filename, struct ast_channel *initiator)
    {
    	return async_play_sound_helper(conference, filename, -1, initiator);
    }
    
    void async_play_sound_ready(struct ast_channel *chan)
    {
    	struct ast_datastore *async_datastore;
    	struct async_datastore_data *add;
    
    	ast_channel_lock(chan);
    	async_datastore = ast_channel_datastore_find(chan, &async_datastore_info, NULL);
    	ast_channel_unlock(chan);
    	if (!async_datastore) {
    		return;
    	}
    
    	add = async_datastore->data;
    
    	ast_mutex_lock(&add->lock);
    	add->wait = 0;
    	ast_cond_signal(&add->cond);
    	ast_mutex_unlock(&add->lock);
    }
    
    
    /*!
     * \brief Play number into the conference bridge
     *
    
     * \param conference The conference bridge to say the number into
    
     * \param say_number number to say
    
     *
     * \retval 0 success
     * \retval -1 failure
     */
    
    static int play_sound_number(struct confbridge_conference *conference, int say_number)
    
    	return play_sound_helper(conference, NULL, say_number);
    
    static int conf_handle_talker_cb(struct ast_bridge_channel *bridge_channel, void *hook_pvt, int talking)
    
    	struct confbridge_user *user = hook_pvt;
    
    	RAII_VAR(struct confbridge_conference *, conference, NULL, ao2_cleanup);
    
    	conference = ao2_find(conference_bridges, user->conference->name, OBJ_KEY);
    
    		/* Remove the hook since the conference does not exist. */
    		return -1;
    
    	ao2_lock(conference);
    	user->talking = talking;
    	ao2_unlock(conference);
    
    
    	talking_extras = ast_json_pack("{s: s, s: b}",
    		"talking_status", talking ? "on" : "off",
    		"admin", ast_test_flag(&user->u_profile, USER_OPT_ADMIN));
    
    	send_conf_stasis(conference, bridge_channel->chan, confbridge_talking_type(), talking_extras, 0);
    
    static int conf_get_pin(struct ast_channel *chan, struct confbridge_user *user)
    
    {
    	char pin_guess[MAX_PIN+1] = { 0, };
    
    	const char *pin = user->u_profile.pin;
    
    	char *tmp = pin_guess;
    	int i, res;
    
    	unsigned int len = MAX_PIN;
    
    	/*
    	 * NOTE: We have not joined a conference yet so we have to use
    	 * the bridge profile requested by the user.
    	 */
    
    
    	/* give them three tries to get the pin right */
    	for (i = 0; i < 3; i++) {
    		if (ast_app_getdata(chan,
    
    			conf_get_sound(CONF_SOUND_GET_PIN, user->b_profile.sounds),
    
    			tmp, len, 0) >= 0) {
    			if (!strcasecmp(pin, pin_guess)) {
    				return 0;
    			}
    
    		ast_streamfile(chan,
    
    			conf_get_sound(CONF_SOUND_INVALID_PIN, user->b_profile.sounds),
    
    			ast_channel_language(chan));
    
    		res = ast_waitstream(chan, AST_DIGIT_ANY);
    		if (res > 0) {
    			/* Account for digit already read during ivalid pin playback
    			 * resetting pin buf. */
    			pin_guess[0] = res;
    			pin_guess[1] = '\0';
    			tmp = pin_guess + 1;
    			len = MAX_PIN - 1;
    
    			/* reset pin buf as empty buffer. */
    			tmp = pin_guess;
    			len = MAX_PIN;
    
    static int user_timeout(struct ast_bridge_channel *bridge_channel, void *ignore)
    {
    	ast_bridge_channel_leave_bridge(bridge_channel, BRIDGE_CHANNEL_STATE_END, 0);
    	pbx_builtin_setvar_helper(bridge_channel->chan, "CONFBRIDGE_RESULT", "TIMEOUT");
    	return -1;
    }
    
    
    static int conf_rec_name(struct confbridge_user *user, const char *conf_name)
    
    {
    	char destdir[PATH_MAX];
    	int res;
    	int duration = 20;
    
    	snprintf(destdir, sizeof(destdir), "%s/confbridge", ast_config_AST_SPOOL_DIR);
    
    	if (ast_mkdir(destdir, 0777) != 0) {
    		ast_log(LOG_WARNING, "mkdir '%s' failed: %s\n", destdir, strerror(errno));
    		return -1;
    	}
    	snprintf(user->name_rec_location, sizeof(user->name_rec_location),
    		 "%s/confbridge-name-%s-%s", destdir,
    
    		 conf_name, ast_channel_uniqueid(user->chan));
    
    	if (!(ast_test_flag(&user->u_profile, USER_OPT_ANNOUNCE_JOIN_LEAVE_REVIEW))) {
    		res = ast_play_and_record(user->chan,
    			"vm-rec-name",
    			user->name_rec_location,
    			10,
    			"sln",
    			&duration,
    			NULL,
    			ast_dsp_get_threshold_from_settings(THRESHOLD_SILENCE),
    			0,
    			NULL);
    	} else {
    		res = ast_record_review(user->chan,
    			"vm-rec-name",
    			user->name_rec_location,
    			10,
    			"sln",
    			&duration,
    			NULL);
    	}
    
    		ast_filedelete(user->name_rec_location, NULL);
    
    		user->name_rec_location[0] = '\0';
    		return -1;
    	}
    	return 0;
    
    struct async_delete_name_rec_task_data {
    	struct confbridge_conference *conference;
    	char filename[0];
    };
    
    static struct async_delete_name_rec_task_data *async_delete_name_rec_task_data_alloc(
    	struct confbridge_conference *conference, const char *filename)
    {
    	struct async_delete_name_rec_task_data *atd;
    
    	atd = ast_malloc(sizeof(*atd) + strlen(filename) + 1);
    	if (!atd) {
    		return NULL;
    	}
    
    	/* Safe */
    	strcpy(atd->filename, filename);
    	atd->conference = conference;
    
    	return atd;
    }
    
    static void async_delete_name_rec_task_data_destroy(struct async_delete_name_rec_task_data *atd)
    {
    	ast_free(atd);
    }
    
    /*!
     * \brief Delete user's name file asynchronously
     *
     * This runs in the playback queue taskprocessor. This ensures that
     * sound file is removed after playback is finished and not before.
     *
     * \param data An async_delete_name_rec_task_data
     * \return 0
     */
    static int async_delete_name_rec_task(void *data)
    {
    	struct async_delete_name_rec_task_data *atd = data;
    
    	ast_filedelete(atd->filename, NULL);
    	ast_log(LOG_DEBUG, "Conference '%s' removed user name file '%s'\n",
    		atd->conference->name, atd->filename);
    
    	async_delete_name_rec_task_data_destroy(atd);
    	return 0;
    }
    
    static int async_delete_name_rec(struct confbridge_conference *conference,
    	const char *filename)
    {
    	struct async_delete_name_rec_task_data *atd;
    
    	if (ast_strlen_zero(filename)) {
    		return 0;
    	} else if (!sound_file_exists(filename)) {
    		return 0;
    	}
    
    	atd = async_delete_name_rec_task_data_alloc(conference, filename);
    	if (!atd) {
    		return -1;
    	}
    
    	if (ast_taskprocessor_push(conference->playback_queue, async_delete_name_rec_task, atd)) {
    		ast_log(LOG_WARNING, "Conference '%s' was unable to remove user name file '%s'\n",
    			conference->name, filename);
    		async_delete_name_rec_task_data_destroy(atd);
    		return -1;
    	}
    
    	return 0;
    }
    
    
    static int join_callback(struct ast_bridge_channel *bridge_channel, void *ignore)
    {
    	async_play_sound_ready(bridge_channel->chan);
    	return 0;
    }
    
    
    struct confbridge_hook_data {
    	struct confbridge_conference *conference;
    	struct confbridge_user *user;
    	enum ast_bridge_hook_type hook_type;
    };
    
    static int send_event_hook_callback(struct ast_bridge_channel *bridge_channel, void *data)
    {
    	struct confbridge_hook_data *hook_data = data;
    
    	if (hook_data->hook_type == AST_BRIDGE_HOOK_TYPE_JOIN) {
    		send_join_event(hook_data->user, hook_data->conference);
    	} else {
    		send_leave_event(hook_data->user, hook_data->conference);
    	}
    
    	return 0;
    }
    
    
    /*! \brief The ConfBridge application */
    
    static int confbridge_exec(struct ast_channel *chan, const char *data)
    
    {
    	int res = 0, volume_adjustments[2];
    
    	const char *b_profile_name = NULL;
    	const char *u_profile_name = NULL;
    	const char *menu_profile_name = NULL;
    
    	struct confbridge_conference *conference = NULL;
    	struct confbridge_user user = {
    
    		.tech_args.talking_threshold = DEFAULT_TALKING_THRESHOLD,
    		.tech_args.silence_threshold = DEFAULT_SILENCE_THRESHOLD,
    		.tech_args.drop_silence = 0,
    
    	struct confbridge_hook_data *join_hook_data;
    	struct confbridge_hook_data *leave_hook_data;
    
    
    	AST_DECLARE_APP_ARGS(args,
    		AST_APP_ARG(conf_name);
    
    		AST_APP_ARG(b_profile_name);
    		AST_APP_ARG(u_profile_name);
    
    	if (ast_channel_state(chan) != AST_STATE_UP) {
    
    	if (ast_bridge_features_init(&user.features)) {
    
    		pbx_builtin_setvar_helper(chan, "CONFBRIDGE_RESULT", "FAILED");
    
    	/* We need to make a copy of the input string if we are going to modify it! */
    	parse = ast_strdupa(data);
    
    	AST_STANDARD_APP_ARGS(args, parse);
    
    	if (ast_strlen_zero(args.conf_name)) {
    
    		pbx_builtin_setvar_helper(chan, "CONFBRIDGE_RESULT", "FAILED");
    
    		ast_log(LOG_WARNING, "%s requires an argument (conference name[,options])\n", app);
    
    		goto confbridge_cleanup;
    
    	if (strlen(args.conf_name) >= MAX_CONF_NAME) {
    		pbx_builtin_setvar_helper(chan, "CONFBRIDGE_RESULT", "FAILED");
    		ast_log(LOG_WARNING, "%s does not accept conference names longer than %d\n", app, MAX_CONF_NAME - 1);
    		res = -1;
    		goto confbridge_cleanup;
    	}
    
    	/* bridge profile name */
    	if (args.argc > 1 && !ast_strlen_zero(args.b_profile_name)) {
    		b_profile_name = args.b_profile_name;
    
    	if (!conf_find_bridge_profile(chan, b_profile_name, &user.b_profile)) {
    
    		pbx_builtin_setvar_helper(chan, "CONFBRIDGE_RESULT", "FAILED");
    
    		ast_log(LOG_WARNING, "Conference bridge profile %s does not exist\n", b_profile_name ?
    			b_profile_name : DEFAULT_BRIDGE_PROFILE);
    
    		res = -1;
    		goto confbridge_cleanup;
    	}
    
    	/* user profile name */
    	if (args.argc > 2 && !ast_strlen_zero(args.u_profile_name)) {
    		u_profile_name = args.u_profile_name;
    
    	if (!conf_find_user_profile(chan, u_profile_name, &user.u_profile)) {
    
    		pbx_builtin_setvar_helper(chan, "CONFBRIDGE_RESULT", "FAILED");
    
    		ast_log(LOG_WARNING, "Conference user profile %s does not exist\n", u_profile_name ?
    			u_profile_name : DEFAULT_USER_PROFILE);
    
    		res = -1;
    		goto confbridge_cleanup;
    	}
    
    	quiet = ast_test_flag(&user.u_profile, USER_OPT_QUIET);
    
    	/* ask for a PIN immediately after finding user profile.  This has to be
    	 * prompted for requardless of quiet setting. */
    
    	if (!ast_strlen_zero(user.u_profile.pin)) {
    		if (conf_get_pin(chan, &user)) {
    
    			pbx_builtin_setvar_helper(chan, "CONFBRIDGE_RESULT", "FAILED");
    
    			res = -1; /* invalid PIN */
    			goto confbridge_cleanup;
    		}
    
    	/* See if we need them to record a intro name */
    
    	if (!quiet &&
    		(ast_test_flag(&user.u_profile, USER_OPT_ANNOUNCE_JOIN_LEAVE) ||
    		(ast_test_flag(&user.u_profile, USER_OPT_ANNOUNCE_JOIN_LEAVE_REVIEW)))) {
    
    		if (conf_rec_name(&user, args.conf_name)) {
    			pbx_builtin_setvar_helper(chan, "CONFBRIDGE_RESULT", "FAILED");
    			res = -1; /* Hangup during name recording */
    			goto confbridge_cleanup;
    		}
    
    	/* menu name */
    
    	if (args.argc > 3 && !ast_strlen_zero(args.menu_profile_name)) {
    		menu_profile_name = args.menu_profile_name;
    	}
    
    	if (conf_set_menu_to_user(chan, &user, menu_profile_name)) {
    
    		pbx_builtin_setvar_helper(chan, "CONFBRIDGE_RESULT", "FAILED");
    
    		ast_log(LOG_WARNING, "Conference menu profile %s does not exist\n", menu_profile_name ?
    			menu_profile_name : DEFAULT_MENU_PROFILE);
    		res = -1;
    		goto confbridge_cleanup;
    
    
    	/* Set if DTMF should pass through for this user or not */
    
    	if (ast_test_flag(&user.u_profile, USER_OPT_DTMF_PASS)) {
    		user.features.dtmf_passthrough = 1;
    
    	} else {
    		user.features.dtmf_passthrough = 0;
    
    	/* Set dsp threshold values if present */
    
    	if (user.u_profile.talking_threshold) {
    		user.tech_args.talking_threshold = user.u_profile.talking_threshold;
    
    	if (user.u_profile.silence_threshold) {
    		user.tech_args.silence_threshold = user.u_profile.silence_threshold;
    
    	/* Set a talker indicate call back if talking detection is requested */
    
    	if (ast_test_flag(&user.u_profile, USER_OPT_TALKER_DETECT)) {
    
    		if (ast_bridge_talk_detector_hook(&user.features, conf_handle_talker_cb,
    
    			&user, NULL, AST_BRIDGE_HOOK_REMOVE_ON_PULL)) {
    
    			pbx_builtin_setvar_helper(chan, "CONFBRIDGE_RESULT", "FAILED");
    
    			res = -1;
    			goto confbridge_cleanup;
    		}
    
    	/* Look for a conference bridge matching the provided name */
    
    	if (!(conference = join_conference_bridge(args.conf_name, &user))) {
    
    		pbx_builtin_setvar_helper(chan, "CONFBRIDGE_RESULT", "FAILED");
    
    		goto confbridge_cleanup;
    	}
    
    	/* Keep a copy of volume adjustments so we can restore them later if need be */
    	volume_adjustments[0] = ast_audiohook_volume_get(chan, AST_AUDIOHOOK_DIRECTION_READ);
    	volume_adjustments[1] = ast_audiohook_volume_get(chan, AST_AUDIOHOOK_DIRECTION_WRITE);
    
    
    	if (ast_test_flag(&user.u_profile, USER_OPT_DROP_SILENCE)) {
    		user.tech_args.drop_silence = 1;
    
    	if (ast_test_flag(&user.u_profile, USER_OPT_JITTERBUFFER)) {
    
    		ast_func_write(chan, "JITTERBUFFER(adaptive)", "default");
    
    	if (ast_test_flag(&user.u_profile, USER_OPT_DENOISE)) {
    
    		ast_func_write(chan, "DENOISE(rx)", "on");
    
    	}
    
    	/* if this user has a intro, play it before entering */
    
    	if (!ast_strlen_zero(user.name_rec_location)) {
    
    		ast_autoservice_start(chan);
    
    		play_sound_file(conference, user.name_rec_location);
    		play_sound_file(conference,
    
    			conf_get_sound(CONF_SOUND_HAS_JOINED, conference->b_profile.sounds));
    
    		ast_autoservice_stop(chan);
    	}
    
    	/* Play the Join sound to both the conference and the user entering. */
    	if (!quiet) {
    
    		const char *join_sound = conf_get_sound(CONF_SOUND_JOIN, conference->b_profile.sounds);
    
    		if (strcmp(conference->b_profile.language, ast_channel_language(chan))) {
    			ast_stream_and_wait(chan, join_sound, "");
    			ast_autoservice_start(chan);
    			play_sound_file(conference, join_sound);
    			ast_autoservice_stop(chan);
    		} else {
    			async_play_sound_file(conference, join_sound, chan);
    		}
    
    	if (user.u_profile.timeout) {
    		ast_bridge_interval_hook(&user.features,
    			0,
    			user.u_profile.timeout * 1000,
    			user_timeout,
    			NULL,
    			NULL,
    			AST_BRIDGE_HOOK_REMOVE_ON_PULL);
    	}
    
    
    	/* See if we need to automatically set this user as a video source or not */
    
    	handle_video_on_join(conference, user.chan, ast_test_flag(&user.u_profile, USER_OPT_MARKEDUSER));
    
    	conf_moh_unsuspend(&user);
    
    	join_hook_data = ast_malloc(sizeof(*join_hook_data));
    	if (!join_hook_data) {
    		res = -1;
    		goto confbridge_cleanup;
    	}
    	join_hook_data->user = &user;
    	join_hook_data->conference = conference;
    	join_hook_data->hook_type = AST_BRIDGE_HOOK_TYPE_JOIN;
    	res = ast_bridge_join_hook(&user.features, send_event_hook_callback,
    		join_hook_data, ast_free_ptr, 0);
    	if (res) {
    		ast_free(join_hook_data);
    		ast_log(LOG_ERROR, "Couldn't add bridge join hook for channel '%s'\n", ast_channel_name(chan));
    		goto confbridge_cleanup;
    	}
    
    	leave_hook_data = ast_malloc(sizeof(*leave_hook_data));
    	if (!leave_hook_data) {
    		/* join_hook_data is cleaned up by ast_bridge_features_cleanup via the goto */
    		res = -1;
    		goto confbridge_cleanup;
    	}
    	leave_hook_data->user = &user;
    	leave_hook_data->conference = conference;
    	leave_hook_data->hook_type = AST_BRIDGE_HOOK_TYPE_LEAVE;
    	res = ast_bridge_leave_hook(&user.features, send_event_hook_callback,
    		leave_hook_data, ast_free_ptr, 0);
    	if (res) {
    		/* join_hook_data is cleaned up by ast_bridge_features_cleanup via the goto */
    		ast_free(leave_hook_data);
    		ast_log(LOG_ERROR, "Couldn't add bridge leave hook for channel '%s'\n", ast_channel_name(chan));
    		goto confbridge_cleanup;
    	}
    
    
    	if (ast_bridge_join_hook(&user.features, join_callback, NULL, NULL, 0)) {
    		async_play_sound_ready(user.chan);
    	}
    
    
    	ast_bridge_join(conference->bridge,
    
    	/* This is a catch-all in case joining the bridge failed or for some reason
    	 * an async announcement got queued up and hasn't been told to play yet
    	 */
    	async_play_sound_ready(chan);
    
    
    	if (!user.kicked && ast_check_hangup(chan)) {
    		pbx_builtin_setvar_helper(chan, "CONFBRIDGE_RESULT", "HANGUP");
    	}
    
    
    	/* if we're shutting down, don't attempt to do further processing */
    	if (ast_shutting_down()) {
    
    		/*
    		 * Not taking any new calls at this time.  We cannot create
    		 * the announcer channel if this is the first channel into
    		 * the conference and we certainly cannot create any
    		 * recording channel.
    		 */
    
    		leave_conference(&user);
    		conference = NULL;
    
    	/* If this user was a video source, we need to clean up and possibly pick a new source. */
    
    	handle_video_on_exit(conference, user.chan);
    
    	/* if this user has a intro, play it when leaving */
    
    	if (!quiet && !ast_strlen_zero(user.name_rec_location)) {
    
    		async_play_sound_file(conference, user.name_rec_location, NULL);
    		async_play_sound_file(conference,
    			conf_get_sound(CONF_SOUND_HAS_LEFT, conference->b_profile.sounds), NULL);
    
    		async_delete_name_rec(conference, user.name_rec_location);
    
    	}
    
    	/* play the leave sound */
    	if (!quiet) {
    
    		const char *leave_sound = conf_get_sound(CONF_SOUND_LEAVE, conference->b_profile.sounds);
    
    		async_play_sound_file(conference, leave_sound, NULL);
    
    	}
    
    	/* If the user was kicked from the conference play back the audio prompt for it */
    
    	if (!quiet && user.kicked) {
    
    		res = ast_stream_and_wait(chan,
    
    			conf_get_sound(CONF_SOUND_KICKED, conference->b_profile.sounds),
    
    	/* Easy as pie, depart this channel from the conference bridge */
    	leave_conference(&user);
    	conference = NULL;
    
    
    	/* Restore volume adjustments to previous values in case they were changed */
    	if (volume_adjustments[0]) {
    		ast_audiohook_volume_set(chan, AST_AUDIOHOOK_DIRECTION_READ, volume_adjustments[0]);
    	}
    	if (volume_adjustments[1]) {
    		ast_audiohook_volume_set(chan, AST_AUDIOHOOK_DIRECTION_WRITE, volume_adjustments[1]);
    	}
    
    
    confbridge_cleanup:
    
    	if (!async_delete_task_pushed && !ast_strlen_zero(user.name_rec_location)) {
    		ast_filedelete(user.name_rec_location, NULL);
    	}
    
    	ast_bridge_features_cleanup(&user.features);
    	conf_bridge_profile_destroy(&user.b_profile);
    
    static int action_toggle_mute(struct confbridge_conference *conference,
    
    			      struct confbridge_user *user,
    			      struct ast_bridge_channel *bridge_channel)
    
    	int mute;
    
    	/* Toggle user level mute request. */
    	mute = !user->muted;
    
    	generic_mute_unmute_user(conference, user, mute);
    
    	return play_file(bridge_channel, NULL,
    		conf_get_sound(mute ? CONF_SOUND_MUTED : CONF_SOUND_UNMUTED,
    			conference->b_profile.sounds)) < 0;
    
    static int action_toggle_binaural(struct confbridge_conference *conference,
    		struct confbridge_user *user,
    		struct ast_bridge_channel *bridge_channel)
    {
    	unsigned int binaural;
    	ast_bridge_channel_lock_bridge(bridge_channel);
    	binaural = !bridge_channel->binaural_suspended;
    	bridge_channel->binaural_suspended = binaural;
    	ast_bridge_unlock(bridge_channel->bridge);
    	return play_file(bridge_channel, NULL, (binaural ?
    				conf_get_sound(CONF_SOUND_BINAURAL_OFF, user->b_profile.sounds) :
    				conf_get_sound(CONF_SOUND_BINAURAL_ON, user->b_profile.sounds))) < 0;
    }
    
    
    static int action_toggle_mute_participants(struct confbridge_conference *conference, struct confbridge_user *user)
    
    	struct confbridge_user *cur_user = NULL;
    
    	/* Toggle bridge level mute request. */
    	mute = !conference->muted;
    	conference->muted = mute;
    
    	AST_LIST_TRAVERSE(&conference->active_list, cur_user, list) {
    		if (!ast_test_flag(&cur_user->u_profile, USER_OPT_ADMIN)) {
    
    			/* Set user level to bridge level mute request. */
    			cur_user->muted = mute;
    			conf_update_user_mute(cur_user);
    
    	sound_to_play = conf_get_sound(
    		mute ? CONF_SOUND_PARTICIPANTS_MUTED : CONF_SOUND_PARTICIPANTS_UNMUTED,
    		conference->b_profile.sounds);
    
    	if (strcmp(conference->b_profile.language, ast_channel_language(user->chan))) {
    		/* The host needs to hear it seperately, as they don't get the audio from play_sound_helper */
    		ast_stream_and_wait(user->chan, sound_to_play, "");
    
    		/* Announce to the group that all participants are muted */
    		ast_autoservice_start(user->chan);
    		play_sound_file(conference, sound_to_play);
    		ast_autoservice_stop(user->chan);
    	} else {
    		/* Playing the sound asynchronously lets the sound be heard by everyone at once */
    		async_play_sound_file(conference, sound_to_play, user->chan);
    	}
    
    static int action_playback(struct ast_bridge_channel *bridge_channel, const char *playback_file)
    {
    	char *file_copy = ast_strdupa(playback_file);
    	char *file = NULL;
    
    	while ((file = strsep(&file_copy, "&"))) {
    		if (ast_stream_and_wait(bridge_channel->chan, file, "")) {
    			ast_log(LOG_WARNING, "Failed to playback file %s to channel\n", file);
    			return -1;
    		}
    	}
    	return 0;
    }
    
    
    static int action_playback_and_continue(struct confbridge_conference *conference,
    	struct confbridge_user *user,
    
    	struct ast_bridge_channel *bridge_channel,
    	struct conf_menu *menu,
    	const char *playback_file,
    	const char *cur_dtmf,
    	int *stop_prompts)
    {
    	int i;
    	int digit = 0;
    	char dtmf[MAXIMUM_DTMF_FEATURE_STRING];
    	struct conf_menu_entry new_menu_entry = { { 0, }, };
    	char *file_copy = ast_strdupa(playback_file);
    	char *file = NULL;
    
    	while ((file = strsep(&file_copy, "&"))) {
    
    		if (ast_streamfile(bridge_channel->chan, file, ast_channel_language(bridge_channel->chan))) {
    
    			ast_log(LOG_WARNING, "Failed to playback file %s to channel\n", file);
    			return -1;
    		}
    
    		/* now wait for more digits. */
    		if (!(digit = ast_waitstream(bridge_channel->chan, AST_DIGIT_ANY))) {
    			/* streaming finished and no DTMF was entered */
    			continue;
    		} else if (digit == -1) {
    			/* error */
    			return -1;
    		} else {
    			break; /* dtmf was entered */
    		}
    	}
    	if (!digit) {
    		/* streaming finished on all files and no DTMF was entered */
    		return -1;
    	}
    	ast_stopstream(bridge_channel->chan);
    
    	/* If we get here, then DTMF has been entered, This means no
    	 * additional prompts should be played for this menu entry */
    	*stop_prompts = 1;
    
    	/* If a digit was pressed during the payback, update
    	 * the dtmf string and look for a new menu entry in the
    	 * menu structure */
    	ast_copy_string(dtmf, cur_dtmf, sizeof(dtmf));
    	for (i = 0; i < (MAXIMUM_DTMF_FEATURE_STRING - 1); i++) {
    		dtmf[i] = cur_dtmf[i];
    		if (!dtmf[i]) {
    			dtmf[i] = (char) digit;
    			dtmf[i + 1] = '\0';
    			i = -1;
    			break;
    		}
    	}
    	/* If i is not -1 then the new dtmf digit was _NOT_ added to the string.
    	 * If this is the case, no new DTMF sequence should be looked for. */
    	if (i != -1) {
    		return 0;
    	}
    
    	if (conf_find_menu_entry_by_sequence(dtmf, menu, &new_menu_entry)) {
    
    		execute_menu_entry(conference,
    			user,
    
    			bridge_channel,
    			&new_menu_entry, menu);
    		conf_menu_entry_destroy(&new_menu_entry);
    	}
    	return 0;
    }
    
    
    static int action_kick_last(struct confbridge_conference *conference,
    
    	struct ast_bridge_channel *bridge_channel,
    
    	struct confbridge_user *user)
    
    	struct confbridge_user *last_user = NULL;
    	int isadmin = ast_test_flag(&user->u_profile, USER_OPT_ADMIN);
    
    		play_file(bridge_channel, NULL,
    
    			conf_get_sound(CONF_SOUND_ERROR_MENU, conference->b_profile.sounds));
    
    		ast_log(LOG_WARNING, "Only admin users can use the kick_last menu action. Channel %s of conf %s is not an admin.\n",
    
    			ast_channel_name(bridge_channel->chan),
    
    	last_user = AST_LIST_LAST(&conference->active_list);
    	if (!last_user) {
    		ao2_unlock(conference);
    		return 0;
    	}
    
    	if (last_user == user || ast_test_flag(&last_user->u_profile, USER_OPT_ADMIN)) {