bridge_builtin_features.c

Go to the documentation of this file.
00001 /*
00002  * Asterisk -- An open source telephony toolkit.
00003  *
00004  * Copyright (C) 2009, Digium, Inc.
00005  *
00006  * Joshua Colp <jcolp@digium.com>
00007  *
00008  * See http://www.asterisk.org for more information about
00009  * the Asterisk project. Please do not directly contact
00010  * any of the maintainers of this project for assistance;
00011  * the project provides a web site, mailing lists and IRC
00012  * channels for your use.
00013  *
00014  * This program is free software, distributed under the terms of
00015  * the GNU General Public License Version 2. See the LICENSE file
00016  * at the top of the source tree.
00017  */
00018 
00019 /*! \file
00020  *
00021  * \brief Built in bridging features
00022  *
00023  * \author Joshua Colp <jcolp@digium.com>
00024  *
00025  * \ingroup bridges
00026  */
00027 
00028 /*** MODULEINFO
00029    <support_level>core</support_level>
00030  ***/
00031 
00032 #include "asterisk.h"
00033 
00034 ASTERISK_FILE_VERSION(__FILE__, "$Revision: 431672 $")
00035 
00036 #include <stdio.h>
00037 #include <stdlib.h>
00038 #include <string.h>
00039 #include <sys/types.h>
00040 #include <sys/stat.h>
00041 
00042 #include "asterisk/module.h"
00043 #include "asterisk/channel.h"
00044 #include "asterisk/bridge.h"
00045 #include "asterisk/bridge_technology.h"
00046 #include "asterisk/frame.h"
00047 #include "asterisk/file.h"
00048 #include "asterisk/app.h"
00049 #include "asterisk/astobj2.h"
00050 #include "asterisk/pbx.h"
00051 #include "asterisk/parking.h"
00052 #include "asterisk/features_config.h"
00053 #include "asterisk/monitor.h"
00054 #include "asterisk/mixmonitor.h"
00055 #include "asterisk/audiohook.h"
00056 #include "asterisk/causes.h"
00057 
00058 enum set_touch_variables_res {
00059    SET_TOUCH_SUCCESS,
00060    SET_TOUCH_UNSET,
00061    SET_TOUCH_ALLOC_FAILURE,
00062 };
00063 
00064 static void set_touch_variable(enum set_touch_variables_res *res, struct ast_channel *chan, const char *var_name, char **touch)
00065 {
00066    const char *c_touch;
00067 
00068    if (*res == SET_TOUCH_ALLOC_FAILURE) {
00069       return;
00070    }
00071    c_touch = pbx_builtin_getvar_helper(chan, var_name);
00072    if (!ast_strlen_zero(c_touch)) {
00073       *touch = ast_strdup(c_touch);
00074       if (!*touch) {
00075          *res = SET_TOUCH_ALLOC_FAILURE;
00076       } else {
00077          *res = SET_TOUCH_SUCCESS;
00078       }
00079    }
00080 }
00081 
00082 static enum set_touch_variables_res set_touch_variables(struct ast_channel *chan, int is_mixmonitor, char **touch_format, char **touch_monitor, char **touch_monitor_prefix)
00083 {
00084    enum set_touch_variables_res res = SET_TOUCH_UNSET;
00085    const char *var_format;
00086    const char *var_monitor;
00087    const char *var_prefix;
00088 
00089    SCOPED_CHANNELLOCK(lock, chan);
00090 
00091    if (is_mixmonitor) {
00092       var_format = "TOUCH_MIXMONITOR_FORMAT";
00093       var_monitor = "TOUCH_MIXMONITOR";
00094       var_prefix = "TOUCH_MIXMONITOR_PREFIX";
00095    } else {
00096       var_format = "TOUCH_MONITOR_FORMAT";
00097       var_monitor = "TOUCH_MONITOR";
00098       var_prefix = "TOUCH_MONITOR_PREFIX";
00099    }
00100    set_touch_variable(&res, chan, var_format, touch_format);
00101    set_touch_variable(&res, chan, var_monitor, touch_monitor);
00102    set_touch_variable(&res, chan, var_prefix, touch_monitor_prefix);
00103 
00104    return res;
00105 }
00106 
00107 static void stop_automonitor(struct ast_bridge_channel *bridge_channel, struct ast_channel *peer_chan, struct ast_features_general_config *features_cfg, const char *stop_message)
00108 {
00109    ast_verb(4, "AutoMonitor used to stop recording call.\n");
00110 
00111    ast_channel_lock(peer_chan);
00112    if (ast_channel_monitor(peer_chan)) {
00113       if (ast_channel_monitor(peer_chan)->stop(peer_chan, 1)) {
00114          ast_verb(4, "Cannot stop AutoMonitor for %s\n", ast_channel_name(bridge_channel->chan));
00115          if (features_cfg && !(ast_strlen_zero(features_cfg->recordingfailsound))) {
00116             ast_bridge_channel_queue_playfile(bridge_channel, NULL, features_cfg->recordingfailsound, NULL);
00117          }
00118          ast_channel_unlock(peer_chan);
00119          return;
00120       }
00121    } else {
00122       /* Something else removed the Monitor before we got to it. */
00123       ast_channel_unlock(peer_chan);
00124       return;
00125    }
00126 
00127    ast_channel_unlock(peer_chan);
00128 
00129    if (features_cfg && !(ast_strlen_zero(features_cfg->courtesytone))) {
00130       ast_bridge_channel_queue_playfile(bridge_channel, NULL, features_cfg->courtesytone, NULL);
00131       ast_bridge_channel_write_playfile(bridge_channel, NULL, features_cfg->courtesytone, NULL);
00132    }
00133 
00134    if (!ast_strlen_zero(stop_message)) {
00135       ast_bridge_channel_queue_playfile(bridge_channel, NULL, stop_message, NULL);
00136       ast_bridge_channel_write_playfile(bridge_channel, NULL, stop_message, NULL);
00137    }
00138 }
00139 
00140 static void start_automonitor(struct ast_bridge_channel *bridge_channel, struct ast_channel *peer_chan, struct ast_features_general_config *features_cfg, const char *start_message)
00141 {
00142    char *touch_filename;
00143    size_t len;
00144    int x;
00145    enum set_touch_variables_res set_touch_res;
00146 
00147    RAII_VAR(char *, touch_format, NULL, ast_free);
00148    RAII_VAR(char *, touch_monitor, NULL, ast_free);
00149    RAII_VAR(char *, touch_monitor_prefix, NULL, ast_free);
00150 
00151    set_touch_res = set_touch_variables(bridge_channel->chan, 0, &touch_format,
00152       &touch_monitor, &touch_monitor_prefix);
00153    switch (set_touch_res) {
00154    case SET_TOUCH_SUCCESS:
00155       break;
00156    case SET_TOUCH_UNSET:
00157       set_touch_res = set_touch_variables(peer_chan, 0, &touch_format, &touch_monitor,
00158          &touch_monitor_prefix);
00159       if (set_touch_res == SET_TOUCH_ALLOC_FAILURE) {
00160          return;
00161       }
00162       break;
00163    case SET_TOUCH_ALLOC_FAILURE:
00164       return;
00165    }
00166 
00167    if (!ast_strlen_zero(touch_monitor)) {
00168       len = strlen(touch_monitor) + 50;
00169       touch_filename = ast_alloca(len);
00170       snprintf(touch_filename, len, "%s-%ld-%s",
00171          S_OR(touch_monitor_prefix, "auto"),
00172          (long) time(NULL),
00173          touch_monitor);
00174    } else {
00175       char *caller_chan_id;
00176       char *peer_chan_id;
00177 
00178       caller_chan_id = ast_strdupa(S_COR(ast_channel_caller(bridge_channel->chan)->id.number.valid,
00179          ast_channel_caller(bridge_channel->chan)->id.number.str, ast_channel_name(bridge_channel->chan)));
00180       peer_chan_id = ast_strdupa(S_COR(ast_channel_caller(peer_chan)->id.number.valid,
00181          ast_channel_caller(peer_chan)->id.number.str, ast_channel_name(peer_chan)));
00182       len = strlen(caller_chan_id) + strlen(peer_chan_id) + 50;
00183       touch_filename = ast_alloca(len);
00184       snprintf(touch_filename, len, "%s-%ld-%s-%s",
00185          S_OR(touch_monitor_prefix, "auto"),
00186          (long) time(NULL),
00187          caller_chan_id,
00188          peer_chan_id);
00189    }
00190 
00191    for (x = 0; x < strlen(touch_filename); x++) {
00192       if (touch_filename[x] == '/') {
00193          touch_filename[x] = '-';
00194       }
00195    }
00196 
00197    ast_verb(4, "AutoMonitor used to record call. Filename: %s\n", touch_filename);
00198 
00199    if (ast_monitor_start(peer_chan, touch_format, touch_filename, 1, X_REC_IN | X_REC_OUT, NULL)) {
00200       ast_verb(4, "AutoMonitor feature was tried by '%s' but monitor failed to start.\n",
00201          ast_channel_name(bridge_channel->chan));
00202       return;
00203    }
00204 
00205    if (features_cfg && !ast_strlen_zero(features_cfg->courtesytone)) {
00206       ast_bridge_channel_queue_playfile(bridge_channel, NULL, features_cfg->courtesytone, NULL);
00207       ast_bridge_channel_write_playfile(bridge_channel, NULL, features_cfg->courtesytone, NULL);
00208    }
00209 
00210    if (!ast_strlen_zero(start_message)) {
00211       ast_bridge_channel_queue_playfile(bridge_channel, NULL, start_message, NULL);
00212       ast_bridge_channel_write_playfile(bridge_channel, NULL, start_message, NULL);
00213    }
00214 
00215    pbx_builtin_setvar_helper(peer_chan, "TOUCH_MONITOR_OUTPUT", touch_filename);
00216 }
00217 
00218 static int feature_automonitor(struct ast_bridge_channel *bridge_channel, void *hook_pvt)
00219 {
00220    const char *start_message;
00221    const char *stop_message;
00222    struct ast_bridge_features_automonitor *options = hook_pvt;
00223    enum ast_bridge_features_monitor start_stop = options ? options->start_stop : AUTO_MONITOR_TOGGLE;
00224    int is_monitoring;
00225 
00226    RAII_VAR(struct ast_channel *, peer_chan, NULL, ast_channel_cleanup);
00227    RAII_VAR(struct ast_features_general_config *, features_cfg, NULL, ao2_cleanup);
00228 
00229    ast_channel_lock(bridge_channel->chan);
00230    features_cfg = ast_get_chan_features_general_config(bridge_channel->chan);
00231    ast_channel_unlock(bridge_channel->chan);
00232    ast_bridge_channel_lock_bridge(bridge_channel);
00233    peer_chan = ast_bridge_peer_nolock(bridge_channel->bridge, bridge_channel->chan);
00234    ast_bridge_unlock(bridge_channel->bridge);
00235 
00236    if (!peer_chan) {
00237       ast_verb(4, "Cannot start AutoMonitor for %s - can not determine peer in bridge.\n",
00238          ast_channel_name(bridge_channel->chan));
00239       if (features_cfg && !ast_strlen_zero(features_cfg->recordingfailsound)) {
00240          ast_bridge_channel_queue_playfile(bridge_channel, NULL, features_cfg->recordingfailsound, NULL);
00241       }
00242       return 0;
00243    }
00244 
00245    ast_channel_lock(bridge_channel->chan);
00246    start_message = pbx_builtin_getvar_helper(bridge_channel->chan,
00247       "TOUCH_MONITOR_MESSAGE_START");
00248    start_message = ast_strdupa(S_OR(start_message, ""));
00249    stop_message = pbx_builtin_getvar_helper(bridge_channel->chan,
00250       "TOUCH_MONITOR_MESSAGE_STOP");
00251    stop_message = ast_strdupa(S_OR(stop_message, ""));
00252    ast_channel_unlock(bridge_channel->chan);
00253 
00254    is_monitoring = ast_channel_monitor(peer_chan) != NULL;
00255    switch (start_stop) {
00256    case AUTO_MONITOR_TOGGLE:
00257       if (is_monitoring) {
00258          stop_automonitor(bridge_channel, peer_chan, features_cfg, stop_message);
00259       } else {
00260          start_automonitor(bridge_channel, peer_chan, features_cfg, start_message);
00261       }
00262       return 0;
00263    case AUTO_MONITOR_START:
00264       if (!is_monitoring) {
00265          start_automonitor(bridge_channel, peer_chan, features_cfg, start_message);
00266          return 0;
00267       }
00268       ast_verb(4, "AutoMonitor already recording call.\n");
00269       break;
00270    case AUTO_MONITOR_STOP:
00271       if (is_monitoring) {
00272          stop_automonitor(bridge_channel, peer_chan, features_cfg, stop_message);
00273          return 0;
00274       }
00275       ast_verb(4, "AutoMonitor already stopped on call.\n");
00276       break;
00277    }
00278 
00279    /*
00280     * Fake start/stop to invoker so will think it did something but
00281     * was already in that mode.
00282     */
00283    if (features_cfg && !ast_strlen_zero(features_cfg->courtesytone)) {
00284       ast_bridge_channel_queue_playfile(bridge_channel, NULL, features_cfg->courtesytone, NULL);
00285    }
00286    if (is_monitoring) {
00287       if (!ast_strlen_zero(start_message)) {
00288          ast_bridge_channel_queue_playfile(bridge_channel, NULL, start_message, NULL);
00289       }
00290    } else {
00291       if (!ast_strlen_zero(stop_message)) {
00292          ast_bridge_channel_queue_playfile(bridge_channel, NULL, stop_message, NULL);
00293       }
00294    }
00295    return 0;
00296 }
00297 
00298 static void stop_automixmonitor(struct ast_bridge_channel *bridge_channel, struct ast_channel *peer_chan, struct ast_features_general_config *features_cfg, const char *stop_message)
00299 {
00300    ast_verb(4, "AutoMixMonitor used to stop recording call.\n");
00301 
00302    if (ast_stop_mixmonitor(peer_chan, NULL)) {
00303       ast_verb(4, "Failed to stop AutoMixMonitor for %s.\n", ast_channel_name(bridge_channel->chan));
00304       if (features_cfg && !(ast_strlen_zero(features_cfg->recordingfailsound))) {
00305          ast_bridge_channel_queue_playfile(bridge_channel, NULL, features_cfg->recordingfailsound, NULL);
00306       }
00307       return;
00308    }
00309 
00310    if (features_cfg && !ast_strlen_zero(features_cfg->courtesytone)) {
00311       ast_bridge_channel_queue_playfile(bridge_channel, NULL, features_cfg->courtesytone, NULL);
00312       ast_bridge_channel_write_playfile(bridge_channel, NULL, features_cfg->courtesytone, NULL);
00313    }
00314 
00315    if (!ast_strlen_zero(stop_message)) {
00316       ast_bridge_channel_queue_playfile(bridge_channel, NULL, stop_message, NULL);
00317       ast_bridge_channel_write_playfile(bridge_channel, NULL, stop_message, NULL);
00318    }
00319 }
00320 
00321 static void start_automixmonitor(struct ast_bridge_channel *bridge_channel, struct ast_channel *peer_chan, struct ast_features_general_config *features_cfg, const char *start_message)
00322 {
00323    char *touch_filename;
00324    size_t len;
00325    int x;
00326    enum set_touch_variables_res set_touch_res;
00327 
00328    RAII_VAR(char *, touch_format, NULL, ast_free);
00329    RAII_VAR(char *, touch_monitor, NULL, ast_free);
00330    RAII_VAR(char *, touch_monitor_prefix, NULL, ast_free);
00331 
00332    set_touch_res = set_touch_variables(bridge_channel->chan, 1, &touch_format,
00333       &touch_monitor, &touch_monitor_prefix);
00334    switch (set_touch_res) {
00335    case SET_TOUCH_SUCCESS:
00336       break;
00337    case SET_TOUCH_UNSET:
00338       set_touch_res = set_touch_variables(peer_chan, 1, &touch_format, &touch_monitor,
00339          &touch_monitor_prefix);
00340       if (set_touch_res == SET_TOUCH_ALLOC_FAILURE) {
00341          return;
00342       }
00343       break;
00344    case SET_TOUCH_ALLOC_FAILURE:
00345       return;
00346    }
00347 
00348    if (!ast_strlen_zero(touch_monitor)) {
00349       len = strlen(touch_monitor) + 50;
00350       touch_filename = ast_alloca(len);
00351       snprintf(touch_filename, len, "%s-%ld-%s.%s",
00352          S_OR(touch_monitor_prefix, "auto"),
00353          (long) time(NULL),
00354          touch_monitor,
00355          S_OR(touch_format, "wav"));
00356    } else {
00357       char *caller_chan_id;
00358       char *peer_chan_id;
00359 
00360       caller_chan_id = ast_strdupa(S_COR(ast_channel_caller(bridge_channel->chan)->id.number.valid,
00361          ast_channel_caller(bridge_channel->chan)->id.number.str, ast_channel_name(bridge_channel->chan)));
00362       peer_chan_id = ast_strdupa(S_COR(ast_channel_caller(peer_chan)->id.number.valid,
00363          ast_channel_caller(peer_chan)->id.number.str, ast_channel_name(peer_chan)));
00364       len = strlen(caller_chan_id) + strlen(peer_chan_id) + 50;
00365       touch_filename = ast_alloca(len);
00366       snprintf(touch_filename, len, "%s-%ld-%s-%s.%s",
00367          S_OR(touch_monitor_prefix, "auto"),
00368          (long) time(NULL),
00369          caller_chan_id,
00370          peer_chan_id,
00371          S_OR(touch_format, "wav"));
00372    }
00373 
00374    for (x = 0; x < strlen(touch_filename); x++) {
00375       if (touch_filename[x] == '/') {
00376          touch_filename[x] = '-';
00377       }
00378    }
00379 
00380    ast_verb(4, "AutoMixMonitor used to record call. Filename: %s\n", touch_filename);
00381 
00382    if (ast_start_mixmonitor(peer_chan, touch_filename, "b")) {
00383       ast_verb(4, "AutoMixMonitor feature was tried by '%s' but MixMonitor failed to start.\n",
00384          ast_channel_name(bridge_channel->chan));
00385 
00386       if (features_cfg && !ast_strlen_zero(features_cfg->recordingfailsound)) {
00387          ast_bridge_channel_queue_playfile(bridge_channel, NULL, features_cfg->recordingfailsound, NULL);
00388       }
00389       return;
00390    }
00391 
00392    if (features_cfg && !ast_strlen_zero(features_cfg->courtesytone)) {
00393       ast_bridge_channel_queue_playfile(bridge_channel, NULL, features_cfg->courtesytone, NULL);
00394       ast_bridge_channel_write_playfile(bridge_channel, NULL, features_cfg->courtesytone, NULL);
00395    }
00396 
00397    if (!ast_strlen_zero(start_message)) {
00398       ast_bridge_channel_queue_playfile(bridge_channel, NULL, start_message, NULL);
00399       ast_bridge_channel_write_playfile(bridge_channel, NULL, start_message, NULL);
00400    }
00401 
00402    pbx_builtin_setvar_helper(peer_chan, "TOUCH_MIXMONITOR_OUTPUT", touch_filename);
00403 }
00404 
00405 static int feature_automixmonitor(struct ast_bridge_channel *bridge_channel, void *hook_pvt)
00406 {
00407    static const char *mixmonitor_spy_type = "MixMonitor";
00408    const char *stop_message;
00409    const char *start_message;
00410    struct ast_bridge_features_automixmonitor *options = hook_pvt;
00411    enum ast_bridge_features_monitor start_stop = options ? options->start_stop : AUTO_MONITOR_TOGGLE;
00412    int is_monitoring;
00413 
00414    RAII_VAR(struct ast_channel *, peer_chan, NULL, ast_channel_cleanup);
00415    RAII_VAR(struct ast_features_general_config *, features_cfg, NULL, ao2_cleanup);
00416 
00417    ast_channel_lock(bridge_channel->chan);
00418    features_cfg = ast_get_chan_features_general_config(bridge_channel->chan);
00419    ast_channel_unlock(bridge_channel->chan);
00420    ast_bridge_channel_lock_bridge(bridge_channel);
00421    peer_chan = ast_bridge_peer_nolock(bridge_channel->bridge, bridge_channel->chan);
00422    ast_bridge_unlock(bridge_channel->bridge);
00423 
00424    if (!peer_chan) {
00425       ast_verb(4, "Cannot start AutoMixMonitor for %s - cannot determine peer in bridge.\n",
00426          ast_channel_name(bridge_channel->chan));
00427       if (features_cfg && !ast_strlen_zero(features_cfg->recordingfailsound)) {
00428          ast_bridge_channel_queue_playfile(bridge_channel, NULL, features_cfg->recordingfailsound, NULL);
00429       }
00430       return 0;
00431    }
00432 
00433    ast_channel_lock(bridge_channel->chan);
00434    start_message = pbx_builtin_getvar_helper(bridge_channel->chan,
00435       "TOUCH_MIXMONITOR_MESSAGE_START");
00436    start_message = ast_strdupa(S_OR(start_message, ""));
00437    stop_message = pbx_builtin_getvar_helper(bridge_channel->chan,
00438       "TOUCH_MIXMONITOR_MESSAGE_STOP");
00439    stop_message = ast_strdupa(S_OR(stop_message, ""));
00440    ast_channel_unlock(bridge_channel->chan);
00441 
00442    is_monitoring =
00443       0 < ast_channel_audiohook_count_by_source(peer_chan, mixmonitor_spy_type, AST_AUDIOHOOK_TYPE_SPY);
00444    switch (start_stop) {
00445    case AUTO_MONITOR_TOGGLE:
00446       if (is_monitoring) {
00447          stop_automixmonitor(bridge_channel, peer_chan, features_cfg, stop_message);
00448       } else {
00449          start_automixmonitor(bridge_channel, peer_chan, features_cfg, start_message);
00450       }
00451       return 0;
00452    case AUTO_MONITOR_START:
00453       if (!is_monitoring) {
00454          start_automixmonitor(bridge_channel, peer_chan, features_cfg, start_message);
00455          return 0;
00456       }
00457       ast_verb(4, "AutoMixMonitor already recording call.\n");
00458       break;
00459    case AUTO_MONITOR_STOP:
00460       if (is_monitoring) {
00461          stop_automixmonitor(bridge_channel, peer_chan, features_cfg, stop_message);
00462          return 0;
00463       }
00464       ast_verb(4, "AutoMixMonitor already stopped on call.\n");
00465       break;
00466    }
00467 
00468    /*
00469     * Fake start/stop to invoker so will think it did something but
00470     * was already in that mode.
00471     */
00472    if (features_cfg && !ast_strlen_zero(features_cfg->courtesytone)) {
00473       ast_bridge_channel_queue_playfile(bridge_channel, NULL, features_cfg->courtesytone, NULL);
00474    }
00475    if (is_monitoring) {
00476       if (!ast_strlen_zero(start_message)) {
00477          ast_bridge_channel_queue_playfile(bridge_channel, NULL, start_message, NULL);
00478       }
00479    } else {
00480       if (!ast_strlen_zero(stop_message)) {
00481          ast_bridge_channel_queue_playfile(bridge_channel, NULL, stop_message, NULL);
00482       }
00483    }
00484    return 0;
00485 }
00486 
00487 /*! \brief Internal built in feature for hangup */
00488 static int feature_hangup(struct ast_bridge_channel *bridge_channel, void *hook_pvt)
00489 {
00490    /*
00491     * This is very simple, we simply change the state on the
00492     * bridge_channel to force the channel out of the bridge and the
00493     * core takes care of the rest.
00494     */
00495    ast_bridge_channel_leave_bridge(bridge_channel, BRIDGE_CHANNEL_STATE_END,
00496       AST_CAUSE_NORMAL_CLEARING);
00497    return 0;
00498 }
00499 
00500 static int unload_module(void)
00501 {
00502    ast_bridge_features_unregister(AST_BRIDGE_BUILTIN_HANGUP);
00503    ast_bridge_features_unregister(AST_BRIDGE_BUILTIN_AUTOMON);
00504    ast_bridge_features_unregister(AST_BRIDGE_BUILTIN_AUTOMIXMON);
00505 
00506    return 0;
00507 }
00508 
00509 static int load_module(void)
00510 {
00511    ast_bridge_features_register(AST_BRIDGE_BUILTIN_HANGUP, feature_hangup, NULL);
00512    ast_bridge_features_register(AST_BRIDGE_BUILTIN_AUTOMON, feature_automonitor, NULL);
00513    ast_bridge_features_register(AST_BRIDGE_BUILTIN_AUTOMIXMON, feature_automixmonitor, NULL);
00514 
00515    /* This module cannot be unloaded until shutdown */
00516    ast_module_shutdown_ref(ast_module_info->self);
00517 
00518    return AST_MODULE_LOAD_SUCCESS;
00519 }
00520 
00521 AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "Built in bridging features");

Generated on Thu Apr 16 06:27:15 2015 for Asterisk - The Open Source Telephony Project by  doxygen 1.5.6