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: 309856 $")
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 char *last_slash;
00248 int errflag = 0;
00249
00250 if (option_verbose > 1)
00251 ast_verbose(VERBOSE_PREFIX_2 "Begin MixMonitor Recording %s\n", mixmonitor->name);
00252
00253 fs = &mixmonitor->mixmonitor_ds->fs;
00254
00255
00256 ast_audiohook_lock(&mixmonitor->audiohook);
00257 while (mixmonitor->audiohook.status == AST_AUDIOHOOK_STATUS_RUNNING && !mixmonitor->mixmonitor_ds->fs_quit) {
00258 struct ast_frame *fr = NULL;
00259
00260 if (!(fr = ast_audiohook_read_frame(&mixmonitor->audiohook, SAMPLES_PER_FRAME, AST_AUDIOHOOK_DIRECTION_BOTH, AST_FORMAT_SLINEAR))) {
00261 ast_audiohook_trigger_wait(&mixmonitor->audiohook);
00262
00263 if (mixmonitor->audiohook.status != AST_AUDIOHOOK_STATUS_RUNNING) {
00264 break;
00265 }
00266 continue;
00267 }
00268
00269
00270
00271 ast_audiohook_unlock(&mixmonitor->audiohook);
00272
00273 ast_mutex_lock(&mixmonitor->mixmonitor_ds->lock);
00274 if (!ast_test_flag(mixmonitor, MUXFLAG_BRIDGED) || (mixmonitor->mixmonitor_ds->chan && ast_bridged_channel(mixmonitor->mixmonitor_ds->chan))) {
00275 ast_mutex_unlock(&mixmonitor->mixmonitor_ds->lock);
00276
00277 if (!*fs && !errflag && !mixmonitor->mixmonitor_ds->fs_quit) {
00278 oflags = O_CREAT | O_WRONLY;
00279 oflags |= ast_test_flag(mixmonitor, MUXFLAG_APPEND) ? O_APPEND : O_TRUNC;
00280
00281 last_slash = strrchr(mixmonitor->filename, '/');
00282 if ((ext = strrchr(mixmonitor->filename, '.')) && (ext > last_slash))
00283 *(ext++) = '\0';
00284 else
00285 ext = "raw";
00286
00287 if (!(*fs = ast_writefile(mixmonitor->filename, ext, NULL, oflags, 0, 0644))) {
00288 ast_log(LOG_ERROR, "Cannot open %s.%s\n", mixmonitor->filename, ext);
00289 errflag = 1;
00290 }
00291 }
00292
00293
00294 if (*fs) {
00295 struct ast_frame *cur;
00296
00297 for (cur = fr; cur; cur = AST_LIST_NEXT(cur, frame_list)) {
00298 ast_writestream(*fs, cur);
00299 }
00300 }
00301 } else {
00302 ast_mutex_unlock(&mixmonitor->mixmonitor_ds->lock);
00303 }
00304
00305
00306 ast_frame_free(fr, 0);
00307
00308 ast_audiohook_lock(&mixmonitor->audiohook);
00309 }
00310 ast_audiohook_unlock(&mixmonitor->audiohook);
00311
00312
00313 ast_mutex_lock(&mixmonitor->mixmonitor_ds->lock);
00314 mixmonitor_ds_close_fs(mixmonitor->mixmonitor_ds);
00315 if (!mixmonitor->mixmonitor_ds->destruction_ok) {
00316 ast_cond_wait(&mixmonitor->mixmonitor_ds->destruction_condition, &mixmonitor->mixmonitor_ds->lock);
00317 }
00318 ast_mutex_unlock(&mixmonitor->mixmonitor_ds->lock);
00319
00320
00321 destroy_monitor_audiohook(mixmonitor);
00322
00323 if (mixmonitor->post_process) {
00324 if (option_verbose > 2)
00325 ast_verbose(VERBOSE_PREFIX_2 "Executing [%s]\n", mixmonitor->post_process);
00326 ast_safe_system(mixmonitor->post_process);
00327 }
00328
00329 if (option_verbose > 1)
00330 ast_verbose(VERBOSE_PREFIX_2 "End MixMonitor Recording %s\n", mixmonitor->name);
00331
00332 mixmonitor_free(mixmonitor);
00333
00334 return NULL;
00335 }
00336
00337 static int setup_mixmonitor_ds(struct mixmonitor *mixmonitor, struct ast_channel *chan)
00338 {
00339 struct ast_datastore *datastore = NULL;
00340 struct mixmonitor_ds *mixmonitor_ds;
00341
00342 if (!(mixmonitor_ds = ast_calloc(1, sizeof(*mixmonitor_ds)))) {
00343 return -1;
00344 }
00345
00346 ast_mutex_init(&mixmonitor_ds->lock);
00347 ast_cond_init(&mixmonitor_ds->destruction_condition, NULL);
00348
00349 if (!(datastore = ast_channel_datastore_alloc(&mixmonitor_ds_info, NULL))) {
00350 ast_mutex_destroy(&mixmonitor_ds->lock);
00351 ast_cond_destroy(&mixmonitor_ds->destruction_condition);
00352 ast_free(mixmonitor_ds);
00353 return -1;
00354 }
00355
00356
00357 mixmonitor_ds->chan = chan;
00358 mixmonitor_ds->audiohook = &mixmonitor->audiohook;
00359 datastore->data = mixmonitor_ds;
00360
00361 ast_channel_lock(chan);
00362 ast_channel_datastore_add(chan, datastore);
00363 ast_channel_unlock(chan);
00364
00365 mixmonitor->mixmonitor_ds = mixmonitor_ds;
00366 return 0;
00367 }
00368
00369 static void launch_monitor_thread(struct ast_channel *chan, const char *filename, unsigned int flags,
00370 int readvol, int writevol, const char *post_process)
00371 {
00372 pthread_attr_t attr;
00373 pthread_t thread;
00374 struct mixmonitor *mixmonitor;
00375 char postprocess2[1024] = "";
00376 size_t len;
00377
00378 len = sizeof(*mixmonitor) + strlen(chan->name) + strlen(filename) + 2;
00379
00380
00381 if (!ast_strlen_zero(post_process)) {
00382 char *p1, *p2;
00383
00384 p1 = ast_strdupa(post_process);
00385 for (p2 = p1; *p2 ; p2++) {
00386 if (*p2 == '^' && *(p2+1) == '{') {
00387 *p2 = '$';
00388 }
00389 }
00390
00391 pbx_substitute_variables_helper(chan, p1, postprocess2, sizeof(postprocess2) - 1);
00392 if (!ast_strlen_zero(postprocess2))
00393 len += strlen(postprocess2) + 1;
00394 }
00395
00396
00397 if (!(mixmonitor = calloc(1, len))) {
00398 return;
00399 }
00400
00401
00402 if (ast_audiohook_init(&mixmonitor->audiohook, AST_AUDIOHOOK_TYPE_SPY, mixmonitor_spy_type)) {
00403 mixmonitor_free(mixmonitor);
00404 return;
00405 }
00406
00407
00408 mixmonitor->flags = flags;
00409 if (setup_mixmonitor_ds(mixmonitor, chan)) {
00410 mixmonitor_free(mixmonitor);
00411 return;
00412 }
00413 mixmonitor->name = (char *) mixmonitor + sizeof(*mixmonitor);
00414 strcpy(mixmonitor->name, chan->name);
00415 if (!ast_strlen_zero(postprocess2)) {
00416 mixmonitor->post_process = mixmonitor->name + strlen(mixmonitor->name) + strlen(filename) + 2;
00417 strcpy(mixmonitor->post_process, postprocess2);
00418 }
00419
00420 mixmonitor->filename = (char *) mixmonitor + sizeof(*mixmonitor) + strlen(chan->name) + 1;
00421 strcpy(mixmonitor->filename, filename);
00422
00423 ast_set_flag(&mixmonitor->audiohook, AST_AUDIOHOOK_TRIGGER_SYNC);
00424
00425 if (readvol)
00426 mixmonitor->audiohook.options.read_volume = readvol;
00427 if (writevol)
00428 mixmonitor->audiohook.options.write_volume = writevol;
00429
00430 if (startmon(chan, &mixmonitor->audiohook)) {
00431 ast_log(LOG_WARNING, "Unable to add '%s' spy to channel '%s'\n",
00432 mixmonitor_spy_type, chan->name);
00433
00434 ast_audiohook_destroy(&mixmonitor->audiohook);
00435 mixmonitor_free(mixmonitor);
00436 return;
00437 }
00438
00439 pthread_attr_init(&attr);
00440 pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
00441 ast_pthread_create_background(&thread, &attr, mixmonitor_thread, mixmonitor);
00442 pthread_attr_destroy(&attr);
00443 }
00444
00445 static int mixmonitor_exec(struct ast_channel *chan, void *data)
00446 {
00447 int x, readvol = 0, writevol = 0;
00448 struct ast_module_user *u;
00449 struct ast_flags flags = {0};
00450 char *parse;
00451 AST_DECLARE_APP_ARGS(args,
00452 AST_APP_ARG(filename);
00453 AST_APP_ARG(options);
00454 AST_APP_ARG(post_process);
00455 );
00456
00457 if (ast_strlen_zero(data)) {
00458 ast_log(LOG_WARNING, "MixMonitor requires an argument (filename)\n");
00459 return -1;
00460 }
00461
00462 u = ast_module_user_add(chan);
00463
00464 parse = ast_strdupa(data);
00465
00466 AST_STANDARD_APP_ARGS(args, parse);
00467
00468 if (ast_strlen_zero(args.filename)) {
00469 ast_log(LOG_WARNING, "MixMonitor requires an argument (filename)\n");
00470 ast_module_user_remove(u);
00471 return -1;
00472 }
00473
00474 if (args.options) {
00475 char *opts[OPT_ARG_ARRAY_SIZE] = { NULL, };
00476
00477 ast_app_parse_options(mixmonitor_opts, &flags, opts, args.options);
00478
00479 if (ast_test_flag(&flags, MUXFLAG_READVOLUME)) {
00480 if (ast_strlen_zero(opts[OPT_ARG_READVOLUME])) {
00481 ast_log(LOG_WARNING, "No volume level was provided for the heard volume ('v') option.\n");
00482 } else if ((sscanf(opts[OPT_ARG_READVOLUME], "%2d", &x) != 1) || (x < -4) || (x > 4)) {
00483 ast_log(LOG_NOTICE, "Heard volume must be a number between -4 and 4, not '%s'\n", opts[OPT_ARG_READVOLUME]);
00484 } else {
00485 readvol = get_volfactor(x);
00486 }
00487 }
00488
00489 if (ast_test_flag(&flags, MUXFLAG_WRITEVOLUME)) {
00490 if (ast_strlen_zero(opts[OPT_ARG_WRITEVOLUME])) {
00491 ast_log(LOG_WARNING, "No volume level was provided for the spoken volume ('V') option.\n");
00492 } else if ((sscanf(opts[OPT_ARG_WRITEVOLUME], "%2d", &x) != 1) || (x < -4) || (x > 4)) {
00493 ast_log(LOG_NOTICE, "Spoken volume must be a number between -4 and 4, not '%s'\n", opts[OPT_ARG_WRITEVOLUME]);
00494 } else {
00495 writevol = get_volfactor(x);
00496 }
00497 }
00498
00499 if (ast_test_flag(&flags, MUXFLAG_VOLUME)) {
00500 if (ast_strlen_zero(opts[OPT_ARG_VOLUME])) {
00501 ast_log(LOG_WARNING, "No volume level was provided for the combined volume ('W') option.\n");
00502 } else if ((sscanf(opts[OPT_ARG_VOLUME], "%2d", &x) != 1) || (x < -4) || (x > 4)) {
00503 ast_log(LOG_NOTICE, "Combined volume must be a number between -4 and 4, not '%s'\n", opts[OPT_ARG_VOLUME]);
00504 } else {
00505 readvol = writevol = get_volfactor(x);
00506 }
00507 }
00508 }
00509
00510
00511 if (args.filename[0] != '/') {
00512 char *build;
00513
00514 build = alloca(strlen(ast_config_AST_MONITOR_DIR) + strlen(args.filename) + 3);
00515 sprintf(build, "%s/%s", ast_config_AST_MONITOR_DIR, args.filename);
00516 args.filename = build;
00517 }
00518
00519 pbx_builtin_setvar_helper(chan, "MIXMONITOR_FILENAME", args.filename);
00520 launch_monitor_thread(chan, args.filename, flags.flags, readvol, writevol, args.post_process);
00521
00522 ast_module_user_remove(u);
00523
00524 return 0;
00525 }
00526
00527 static int stop_mixmonitor_exec(struct ast_channel *chan, void *data)
00528 {
00529 struct ast_datastore *datastore = NULL;
00530
00531 ast_channel_lock(chan);
00532 ast_audiohook_detach_source(chan, mixmonitor_spy_type);
00533 if ((datastore = ast_channel_datastore_find(chan, &mixmonitor_ds_info, NULL))) {
00534 struct mixmonitor_ds *mixmonitor_ds = datastore->data;
00535
00536 ast_mutex_lock(&mixmonitor_ds->lock);
00537
00538
00539
00540 mixmonitor_ds_close_fs(mixmonitor_ds);
00541
00542
00543
00544
00545 if (mixmonitor_ds->audiohook) {
00546 ast_audiohook_lock(mixmonitor_ds->audiohook);
00547 ast_cond_signal(&mixmonitor_ds->audiohook->trigger);
00548 ast_audiohook_unlock(mixmonitor_ds->audiohook);
00549 mixmonitor_ds->audiohook = NULL;
00550 }
00551
00552 ast_mutex_unlock(&mixmonitor_ds->lock);
00553
00554
00555 if (!ast_channel_datastore_remove(chan, datastore)) {
00556 ast_channel_datastore_free(datastore);
00557 }
00558 }
00559 ast_channel_unlock(chan);
00560
00561 return 0;
00562 }
00563
00564 static int mixmonitor_cli(int fd, int argc, char **argv)
00565 {
00566 struct ast_channel *chan;
00567
00568 if (argc < 3)
00569 return RESULT_SHOWUSAGE;
00570
00571 if (!(chan = ast_get_channel_by_name_prefix_locked(argv[2], strlen(argv[2])))) {
00572 ast_cli(fd, "No channel matching '%s' found.\n", argv[2]);
00573 return RESULT_SUCCESS;
00574 }
00575
00576 if (!strcasecmp(argv[1], "start"))
00577 mixmonitor_exec(chan, argv[3]);
00578 else if (!strcasecmp(argv[1], "stop"))
00579 ast_audiohook_detach_source(chan, mixmonitor_spy_type);
00580
00581 ast_channel_unlock(chan);
00582
00583 return RESULT_SUCCESS;
00584 }
00585
00586 static char *complete_mixmonitor_cli(const char *line, const char *word, int pos, int state)
00587 {
00588 return ast_complete_channels(line, word, pos, state, 2);
00589 }
00590
00591 static struct ast_cli_entry cli_mixmonitor[] = {
00592 { { "mixmonitor", NULL, NULL },
00593 mixmonitor_cli, "Execute a MixMonitor command.",
00594 "mixmonitor <start|stop> <chan_name> [args]\n\n"
00595 "The optional arguments are passed to the\n"
00596 "MixMonitor application when the 'start' command is used.\n",
00597 complete_mixmonitor_cli },
00598 };
00599
00600 static int unload_module(void)
00601 {
00602 int res;
00603
00604 ast_cli_unregister_multiple(cli_mixmonitor, sizeof(cli_mixmonitor) / sizeof(struct ast_cli_entry));
00605 res = ast_unregister_application(stop_app);
00606 res |= ast_unregister_application(app);
00607
00608 ast_module_user_hangup_all();
00609
00610 return res;
00611 }
00612
00613 static int load_module(void)
00614 {
00615 int res;
00616
00617 ast_cli_register_multiple(cli_mixmonitor, sizeof(cli_mixmonitor) / sizeof(struct ast_cli_entry));
00618 res = ast_register_application(app, mixmonitor_exec, synopsis, desc);
00619 res |= ast_register_application(stop_app, stop_mixmonitor_exec, stop_synopsis, stop_desc);
00620
00621 return res;
00622 }
00623
00624 AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "Mixed Audio Monitoring Application");