00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036 #include "asterisk.h"
00037
00038 ASTERISK_FILE_VERSION(__FILE__, "$Revision: 230508 $")
00039
00040 #include <stdlib.h>
00041 #include <stdio.h>
00042 #include <string.h>
00043 #include <unistd.h>
00044
00045 #include "asterisk/file.h"
00046 #include "asterisk/logger.h"
00047 #include "asterisk/channel.h"
00048 #include "asterisk/audiohook.h"
00049 #include "asterisk/pbx.h"
00050 #include "asterisk/module.h"
00051 #include "asterisk/lock.h"
00052 #include "asterisk/cli.h"
00053 #include "asterisk/options.h"
00054 #include "asterisk/app.h"
00055 #include "asterisk/linkedlists.h"
00056 #include "asterisk/utils.h"
00057
00058 #define get_volfactor(x) x ? ((x > 0) ? (1 << x) : ((1 << abs(x)) * -1)) : 0
00059
00060 static const char *app = "MixMonitor";
00061 static const char *synopsis = "Record a call and mix the audio during the recording";
00062 static const char *desc = ""
00063 " MixMonitor(<file>.<ext>[|<options>[|<command>]])\n\n"
00064 "Records the audio on the current channel to the specified file.\n"
00065 "If the filename is an absolute path, uses that path, otherwise\n"
00066 "creates the file in the configured monitoring directory from\n"
00067 "asterisk.conf. Use of StopMixMonitor is required to guarantee\n"
00068 "the audio file is available for processing during dialplan execution.\n\n"
00069 "Valid options:\n"
00070 " a - Append to the file instead of overwriting it.\n"
00071 " b - Only save audio to the file while the channel is bridged.\n"
00072 " Note: Does not include conferences or sounds played to each bridged\n"
00073 " party.\n"
00074 " Note: If you utilize this option inside a Local channel, you must\n"
00075 " make sure the Local channel is not optimized away. To do this,\n"
00076 " be sure to call your Local channel with the '/n' option.\n"
00077 " For example: Dial(Local/start@mycontext/n)\n"
00078 " v(<x>) - Adjust the heard volume by a factor of <x> (range -4 to 4)\n"
00079 " V(<x>) - Adjust the spoken volume by a factor of <x> (range -4 to 4)\n"
00080 " W(<x>) - Adjust the both heard and spoken volumes by a factor of <x>\n"
00081 " (range -4 to 4)\n\n"
00082 "<command> will be executed when the recording is over\n"
00083 "Any strings matching ^{X} will be unescaped to ${X}.\n"
00084 "All variables will be evaluated at the time MixMonitor is called.\n"
00085 "The variable MIXMONITOR_FILENAME will contain the filename used to record.\n"
00086 "";
00087
00088 static const char *stop_app = "StopMixMonitor";
00089 static const char *stop_synopsis = "Stop recording a call through MixMonitor";
00090 static const char *stop_desc = ""
00091 " StopMixMonitor()\n\n"
00092 "Stop recording a call through MixMonitor, and free the recording's file handle.\n"
00093 "";
00094
00095 struct module_symbols *me;
00096
00097 static const char *mixmonitor_spy_type = "MixMonitor";
00098
00099 struct mixmonitor {
00100 struct ast_audiohook audiohook;
00101 char *filename;
00102 char *post_process;
00103 char *name;
00104 unsigned int flags;
00105 struct mixmonitor_ds *mixmonitor_ds;
00106 };
00107
00108 enum {
00109 MUXFLAG_APPEND = (1 << 1),
00110 MUXFLAG_BRIDGED = (1 << 2),
00111 MUXFLAG_VOLUME = (1 << 3),
00112 MUXFLAG_READVOLUME = (1 << 4),
00113 MUXFLAG_WRITEVOLUME = (1 << 5),
00114 } mixmonitor_flags;
00115
00116 enum {
00117 OPT_ARG_READVOLUME = 0,
00118 OPT_ARG_WRITEVOLUME,
00119 OPT_ARG_VOLUME,
00120 OPT_ARG_ARRAY_SIZE,
00121 } mixmonitor_args;
00122
00123 AST_APP_OPTIONS(mixmonitor_opts, {
00124 AST_APP_OPTION('a', MUXFLAG_APPEND),
00125 AST_APP_OPTION('b', MUXFLAG_BRIDGED),
00126 AST_APP_OPTION_ARG('v', MUXFLAG_READVOLUME, OPT_ARG_READVOLUME),
00127 AST_APP_OPTION_ARG('V', MUXFLAG_WRITEVOLUME, OPT_ARG_WRITEVOLUME),
00128 AST_APP_OPTION_ARG('W', MUXFLAG_VOLUME, OPT_ARG_VOLUME),
00129 });
00130
00131
00132
00133
00134
00135 struct mixmonitor_ds {
00136 struct ast_channel *chan;
00137
00138
00139
00140
00141
00142
00143
00144 unsigned int destruction_ok;
00145 ast_cond_t destruction_condition;
00146 ast_mutex_t lock;
00147
00148
00149
00150 int fs_quit;
00151 struct ast_filestream *fs;
00152 struct ast_audiohook *audiohook;
00153 };
00154
00155
00156
00157
00158
00159 static void mixmonitor_ds_close_fs(struct mixmonitor_ds *mixmonitor_ds)
00160 {
00161 if (mixmonitor_ds->fs) {
00162 ast_closestream(mixmonitor_ds->fs);
00163 mixmonitor_ds->fs = NULL;
00164 mixmonitor_ds->fs_quit = 1;
00165 if (option_verbose > 1)
00166 ast_verbose(VERBOSE_PREFIX_2 "MixMonitor close filestream\n");
00167 }
00168 }
00169
00170 static void mixmonitor_ds_destroy(void *data)
00171 {
00172 struct mixmonitor_ds *mixmonitor_ds = data;
00173
00174 ast_mutex_lock(&mixmonitor_ds->lock);
00175 mixmonitor_ds->audiohook = NULL;
00176 mixmonitor_ds->chan = NULL;
00177 mixmonitor_ds->destruction_ok = 1;
00178 ast_cond_signal(&mixmonitor_ds->destruction_condition);
00179 ast_mutex_unlock(&mixmonitor_ds->lock);
00180 }
00181
00182 static void mixmonitor_ds_chan_fixup(void *data, struct ast_channel *old_chan, struct ast_channel *new_chan)
00183 {
00184 struct mixmonitor_ds *mixmonitor_ds = data;
00185
00186 ast_mutex_lock(&mixmonitor_ds->lock);
00187 mixmonitor_ds->chan = new_chan;
00188 ast_mutex_unlock(&mixmonitor_ds->lock);
00189 }
00190
00191 static struct ast_datastore_info mixmonitor_ds_info = {
00192 .type = "mixmonitor",
00193 .destroy = mixmonitor_ds_destroy,
00194 .chan_fixup = mixmonitor_ds_chan_fixup,
00195 };
00196
00197 static void destroy_monitor_audiohook(struct mixmonitor *mixmonitor)
00198 {
00199 if (mixmonitor->mixmonitor_ds) {
00200 ast_mutex_lock(&mixmonitor->mixmonitor_ds->lock);
00201 mixmonitor->mixmonitor_ds->audiohook = NULL;
00202 ast_mutex_unlock(&mixmonitor->mixmonitor_ds->lock);
00203 }
00204
00205 ast_audiohook_lock(&mixmonitor->audiohook);
00206 ast_audiohook_detach(&mixmonitor->audiohook);
00207 ast_audiohook_unlock(&mixmonitor->audiohook);
00208 ast_audiohook_destroy(&mixmonitor->audiohook);
00209 }
00210
00211 static int startmon(struct ast_channel *chan, struct ast_audiohook *audiohook)
00212 {
00213 struct ast_channel *peer;
00214 int res;
00215
00216 if (!chan)
00217 return -1;
00218
00219 res = ast_audiohook_attach(chan, audiohook);
00220
00221 if (!res && ast_test_flag(chan, AST_FLAG_NBRIDGE) && (peer = ast_bridged_channel(chan)))
00222 ast_softhangup(peer, AST_SOFTHANGUP_UNBRIDGE);
00223
00224 return res;
00225 }
00226
00227 #define SAMPLES_PER_FRAME 160
00228
00229 static void mixmonitor_free(struct mixmonitor *mixmonitor)
00230 {
00231 if (mixmonitor) {
00232 if (mixmonitor->mixmonitor_ds) {
00233 ast_mutex_destroy(&mixmonitor->mixmonitor_ds->lock);
00234 ast_cond_destroy(&mixmonitor->mixmonitor_ds->destruction_condition);
00235 ast_free(mixmonitor->mixmonitor_ds);
00236 }
00237 ast_free(mixmonitor);
00238 }
00239 }
00240
00241 static void *mixmonitor_thread(void *obj)
00242 {
00243 struct mixmonitor *mixmonitor = obj;
00244 struct ast_filestream **fs = NULL;
00245 unsigned int oflags;
00246 char *ext;
00247 int errflag = 0;
00248
00249 if (option_verbose > 1)
00250 ast_verbose(VERBOSE_PREFIX_2 "Begin MixMonitor Recording %s\n", mixmonitor->name);
00251
00252 fs = &mixmonitor->mixmonitor_ds->fs;
00253
00254
00255 ast_audiohook_lock(&mixmonitor->audiohook);
00256 while (mixmonitor->audiohook.status == AST_AUDIOHOOK_STATUS_RUNNING && !mixmonitor->mixmonitor_ds->fs_quit) {
00257 struct ast_frame *fr = NULL;
00258
00259 ast_audiohook_trigger_wait(&mixmonitor->audiohook);
00260
00261 if (mixmonitor->audiohook.status != AST_AUDIOHOOK_STATUS_RUNNING)
00262 break;
00263
00264 if (!(fr = ast_audiohook_read_frame(&mixmonitor->audiohook, SAMPLES_PER_FRAME, AST_AUDIOHOOK_DIRECTION_BOTH, AST_FORMAT_SLINEAR)))
00265 continue;
00266
00267
00268
00269 ast_audiohook_unlock(&mixmonitor->audiohook);
00270
00271 ast_mutex_lock(&mixmonitor->mixmonitor_ds->lock);
00272 if (!ast_test_flag(mixmonitor, MUXFLAG_BRIDGED) || (mixmonitor->mixmonitor_ds->chan && ast_bridged_channel(mixmonitor->mixmonitor_ds->chan))) {
00273 ast_mutex_unlock(&mixmonitor->mixmonitor_ds->lock);
00274
00275 if (!*fs && !errflag && !mixmonitor->mixmonitor_ds->fs_quit) {
00276 oflags = O_CREAT | O_WRONLY;
00277 oflags |= ast_test_flag(mixmonitor, MUXFLAG_APPEND) ? O_APPEND : O_TRUNC;
00278
00279 if ((ext = strrchr(mixmonitor->filename, '.')))
00280 *(ext++) = '\0';
00281 else
00282 ext = "raw";
00283
00284 if (!(*fs = ast_writefile(mixmonitor->filename, ext, NULL, oflags, 0, 0644))) {
00285 ast_log(LOG_ERROR, "Cannot open %s.%s\n", mixmonitor->filename, ext);
00286 errflag = 1;
00287 }
00288 }
00289
00290
00291 if (*fs) {
00292 struct ast_frame *cur;
00293
00294 for (cur = fr; cur; cur = AST_LIST_NEXT(cur, frame_list)) {
00295 ast_writestream(*fs, cur);
00296 }
00297 }
00298 } else {
00299 ast_mutex_unlock(&mixmonitor->mixmonitor_ds->lock);
00300 }
00301
00302
00303 ast_frame_free(fr, 0);
00304
00305 ast_audiohook_lock(&mixmonitor->audiohook);
00306 }
00307 ast_audiohook_unlock(&mixmonitor->audiohook);
00308
00309
00310 ast_mutex_lock(&mixmonitor->mixmonitor_ds->lock);
00311 mixmonitor_ds_close_fs(mixmonitor->mixmonitor_ds);
00312 if (!mixmonitor->mixmonitor_ds->destruction_ok) {
00313 ast_cond_wait(&mixmonitor->mixmonitor_ds->destruction_condition, &mixmonitor->mixmonitor_ds->lock);
00314 }
00315 ast_mutex_unlock(&mixmonitor->mixmonitor_ds->lock);
00316
00317
00318 destroy_monitor_audiohook(mixmonitor);
00319
00320 if (mixmonitor->post_process) {
00321 if (option_verbose > 2)
00322 ast_verbose(VERBOSE_PREFIX_2 "Executing [%s]\n", mixmonitor->post_process);
00323 ast_safe_system(mixmonitor->post_process);
00324 }
00325
00326 if (option_verbose > 1)
00327 ast_verbose(VERBOSE_PREFIX_2 "End MixMonitor Recording %s\n", mixmonitor->name);
00328
00329 mixmonitor_free(mixmonitor);
00330
00331 return NULL;
00332 }
00333
00334 static int setup_mixmonitor_ds(struct mixmonitor *mixmonitor, struct ast_channel *chan)
00335 {
00336 struct ast_datastore *datastore = NULL;
00337 struct mixmonitor_ds *mixmonitor_ds;
00338
00339 if (!(mixmonitor_ds = ast_calloc(1, sizeof(*mixmonitor_ds)))) {
00340 return -1;
00341 }
00342
00343 ast_mutex_init(&mixmonitor_ds->lock);
00344 ast_cond_init(&mixmonitor_ds->destruction_condition, NULL);
00345
00346 if (!(datastore = ast_channel_datastore_alloc(&mixmonitor_ds_info, NULL))) {
00347 ast_mutex_destroy(&mixmonitor_ds->lock);
00348 ast_cond_destroy(&mixmonitor_ds->destruction_condition);
00349 ast_free(mixmonitor_ds);
00350 return -1;
00351 }
00352
00353
00354 mixmonitor_ds->chan = chan;
00355 mixmonitor_ds->audiohook = &mixmonitor->audiohook;
00356 datastore->data = mixmonitor_ds;
00357
00358 ast_channel_lock(chan);
00359 ast_channel_datastore_add(chan, datastore);
00360 ast_channel_unlock(chan);
00361
00362 mixmonitor->mixmonitor_ds = mixmonitor_ds;
00363 return 0;
00364 }
00365
00366 static void launch_monitor_thread(struct ast_channel *chan, const char *filename, unsigned int flags,
00367 int readvol, int writevol, const char *post_process)
00368 {
00369 pthread_attr_t attr;
00370 pthread_t thread;
00371 struct mixmonitor *mixmonitor;
00372 char postprocess2[1024] = "";
00373 size_t len;
00374
00375 len = sizeof(*mixmonitor) + strlen(chan->name) + strlen(filename) + 2;
00376
00377
00378 if (!ast_strlen_zero(post_process)) {
00379 char *p1, *p2;
00380
00381 p1 = ast_strdupa(post_process);
00382 for (p2 = p1; *p2 ; p2++) {
00383 if (*p2 == '^' && *(p2+1) == '{') {
00384 *p2 = '$';
00385 }
00386 }
00387
00388 pbx_substitute_variables_helper(chan, p1, postprocess2, sizeof(postprocess2) - 1);
00389 if (!ast_strlen_zero(postprocess2))
00390 len += strlen(postprocess2) + 1;
00391 }
00392
00393
00394 if (!(mixmonitor = calloc(1, len))) {
00395 return;
00396 }
00397
00398
00399 if (ast_audiohook_init(&mixmonitor->audiohook, AST_AUDIOHOOK_TYPE_SPY, mixmonitor_spy_type)) {
00400 mixmonitor_free(mixmonitor);
00401 return;
00402 }
00403
00404
00405 mixmonitor->flags = flags;
00406 if (setup_mixmonitor_ds(mixmonitor, chan)) {
00407 mixmonitor_free(mixmonitor);
00408 return;
00409 }
00410 mixmonitor->name = (char *) mixmonitor + sizeof(*mixmonitor);
00411 strcpy(mixmonitor->name, chan->name);
00412 if (!ast_strlen_zero(postprocess2)) {
00413 mixmonitor->post_process = mixmonitor->name + strlen(mixmonitor->name) + strlen(filename) + 2;
00414 strcpy(mixmonitor->post_process, postprocess2);
00415 }
00416
00417 mixmonitor->filename = (char *) mixmonitor + sizeof(*mixmonitor) + strlen(chan->name) + 1;
00418 strcpy(mixmonitor->filename, filename);
00419
00420 ast_set_flag(&mixmonitor->audiohook, AST_AUDIOHOOK_TRIGGER_SYNC);
00421
00422 if (readvol)
00423 mixmonitor->audiohook.options.read_volume = readvol;
00424 if (writevol)
00425 mixmonitor->audiohook.options.write_volume = writevol;
00426
00427 if (startmon(chan, &mixmonitor->audiohook)) {
00428 ast_log(LOG_WARNING, "Unable to add '%s' spy to channel '%s'\n",
00429 mixmonitor_spy_type, chan->name);
00430
00431 ast_audiohook_destroy(&mixmonitor->audiohook);
00432 mixmonitor_free(mixmonitor);
00433 return;
00434 }
00435
00436 pthread_attr_init(&attr);
00437 pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
00438 ast_pthread_create_background(&thread, &attr, mixmonitor_thread, mixmonitor);
00439 pthread_attr_destroy(&attr);
00440 }
00441
00442 static int mixmonitor_exec(struct ast_channel *chan, void *data)
00443 {
00444 int x, readvol = 0, writevol = 0;
00445 struct ast_module_user *u;
00446 struct ast_flags flags = {0};
00447 char *parse;
00448 AST_DECLARE_APP_ARGS(args,
00449 AST_APP_ARG(filename);
00450 AST_APP_ARG(options);
00451 AST_APP_ARG(post_process);
00452 );
00453
00454 if (ast_strlen_zero(data)) {
00455 ast_log(LOG_WARNING, "MixMonitor requires an argument (filename)\n");
00456 return -1;
00457 }
00458
00459 u = ast_module_user_add(chan);
00460
00461 parse = ast_strdupa(data);
00462
00463 AST_STANDARD_APP_ARGS(args, parse);
00464
00465 if (ast_strlen_zero(args.filename)) {
00466 ast_log(LOG_WARNING, "MixMonitor requires an argument (filename)\n");
00467 ast_module_user_remove(u);
00468 return -1;
00469 }
00470
00471 if (args.options) {
00472 char *opts[OPT_ARG_ARRAY_SIZE] = { NULL, };
00473
00474 ast_app_parse_options(mixmonitor_opts, &flags, opts, args.options);
00475
00476 if (ast_test_flag(&flags, MUXFLAG_READVOLUME)) {
00477 if (ast_strlen_zero(opts[OPT_ARG_READVOLUME])) {
00478 ast_log(LOG_WARNING, "No volume level was provided for the heard volume ('v') option.\n");
00479 } else if ((sscanf(opts[OPT_ARG_READVOLUME], "%2d", &x) != 1) || (x < -4) || (x > 4)) {
00480 ast_log(LOG_NOTICE, "Heard volume must be a number between -4 and 4, not '%s'\n", opts[OPT_ARG_READVOLUME]);
00481 } else {
00482 readvol = get_volfactor(x);
00483 }
00484 }
00485
00486 if (ast_test_flag(&flags, MUXFLAG_WRITEVOLUME)) {
00487 if (ast_strlen_zero(opts[OPT_ARG_WRITEVOLUME])) {
00488 ast_log(LOG_WARNING, "No volume level was provided for the spoken volume ('V') option.\n");
00489 } else if ((sscanf(opts[OPT_ARG_WRITEVOLUME], "%2d", &x) != 1) || (x < -4) || (x > 4)) {
00490 ast_log(LOG_NOTICE, "Spoken volume must be a number between -4 and 4, not '%s'\n", opts[OPT_ARG_WRITEVOLUME]);
00491 } else {
00492 writevol = get_volfactor(x);
00493 }
00494 }
00495
00496 if (ast_test_flag(&flags, MUXFLAG_VOLUME)) {
00497 if (ast_strlen_zero(opts[OPT_ARG_VOLUME])) {
00498 ast_log(LOG_WARNING, "No volume level was provided for the combined volume ('W') option.\n");
00499 } else if ((sscanf(opts[OPT_ARG_VOLUME], "%2d", &x) != 1) || (x < -4) || (x > 4)) {
00500 ast_log(LOG_NOTICE, "Combined volume must be a number between -4 and 4, not '%s'\n", opts[OPT_ARG_VOLUME]);
00501 } else {
00502 readvol = writevol = get_volfactor(x);
00503 }
00504 }
00505 }
00506
00507
00508 if (args.filename[0] != '/') {
00509 char *build;
00510
00511 build = alloca(strlen(ast_config_AST_MONITOR_DIR) + strlen(args.filename) + 3);
00512 sprintf(build, "%s/%s", ast_config_AST_MONITOR_DIR, args.filename);
00513 args.filename = build;
00514 }
00515
00516 pbx_builtin_setvar_helper(chan, "MIXMONITOR_FILENAME", args.filename);
00517 launch_monitor_thread(chan, args.filename, flags.flags, readvol, writevol, args.post_process);
00518
00519 ast_module_user_remove(u);
00520
00521 return 0;
00522 }
00523
00524 static int stop_mixmonitor_exec(struct ast_channel *chan, void *data)
00525 {
00526 struct ast_datastore *datastore = NULL;
00527
00528 ast_channel_lock(chan);
00529 ast_audiohook_detach_source(chan, mixmonitor_spy_type);
00530 if ((datastore = ast_channel_datastore_find(chan, &mixmonitor_ds_info, NULL))) {
00531 struct mixmonitor_ds *mixmonitor_ds = datastore->data;
00532
00533 ast_mutex_lock(&mixmonitor_ds->lock);
00534
00535
00536
00537 mixmonitor_ds_close_fs(mixmonitor_ds);
00538
00539
00540
00541
00542 if (mixmonitor_ds->audiohook) {
00543 ast_audiohook_lock(mixmonitor_ds->audiohook);
00544 ast_cond_signal(&mixmonitor_ds->audiohook->trigger);
00545 ast_audiohook_unlock(mixmonitor_ds->audiohook);
00546 mixmonitor_ds->audiohook = NULL;
00547 }
00548
00549 ast_mutex_unlock(&mixmonitor_ds->lock);
00550
00551
00552 if (!ast_channel_datastore_remove(chan, datastore)) {
00553 ast_channel_datastore_free(datastore);
00554 }
00555 }
00556 ast_channel_unlock(chan);
00557
00558 return 0;
00559 }
00560
00561 static int mixmonitor_cli(int fd, int argc, char **argv)
00562 {
00563 struct ast_channel *chan;
00564
00565 if (argc < 3)
00566 return RESULT_SHOWUSAGE;
00567
00568 if (!(chan = ast_get_channel_by_name_prefix_locked(argv[2], strlen(argv[2])))) {
00569 ast_cli(fd, "No channel matching '%s' found.\n", argv[2]);
00570 return RESULT_SUCCESS;
00571 }
00572
00573 if (!strcasecmp(argv[1], "start"))
00574 mixmonitor_exec(chan, argv[3]);
00575 else if (!strcasecmp(argv[1], "stop"))
00576 ast_audiohook_detach_source(chan, mixmonitor_spy_type);
00577
00578 ast_channel_unlock(chan);
00579
00580 return RESULT_SUCCESS;
00581 }
00582
00583 static char *complete_mixmonitor_cli(const char *line, const char *word, int pos, int state)
00584 {
00585 return ast_complete_channels(line, word, pos, state, 2);
00586 }
00587
00588 static struct ast_cli_entry cli_mixmonitor[] = {
00589 { { "mixmonitor", NULL, NULL },
00590 mixmonitor_cli, "Execute a MixMonitor command.",
00591 "mixmonitor <start|stop> <chan_name> [args]\n\n"
00592 "The optional arguments are passed to the\n"
00593 "MixMonitor application when the 'start' command is used.\n",
00594 complete_mixmonitor_cli },
00595 };
00596
00597 static int unload_module(void)
00598 {
00599 int res;
00600
00601 ast_cli_unregister_multiple(cli_mixmonitor, sizeof(cli_mixmonitor) / sizeof(struct ast_cli_entry));
00602 res = ast_unregister_application(stop_app);
00603 res |= ast_unregister_application(app);
00604
00605 ast_module_user_hangup_all();
00606
00607 return res;
00608 }
00609
00610 static int load_module(void)
00611 {
00612 int res;
00613
00614 ast_cli_register_multiple(cli_mixmonitor, sizeof(cli_mixmonitor) / sizeof(struct ast_cli_entry));
00615 res = ast_register_application(app, mixmonitor_exec, synopsis, desc);
00616 res |= ast_register_application(stop_app, stop_mixmonitor_exec, stop_synopsis, stop_desc);
00617
00618 return res;
00619 }
00620
00621 AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "Mixed Audio Monitoring Application");