![russell](/assets/img/avatar_default.png)
once instead of 3 times. (closes issue #11053) Reported by: seanbright Patches: app_playback.patch uploaded by seanbright (license 71) git-svn-id: http://svn.digium.com/svn/asterisk/trunk@86835 f38db490-d61c-443f-a65b-d21fe96a405b
530 lines
15 KiB
C
530 lines
15 KiB
C
/*
|
|
* Asterisk -- An open source telephony toolkit.
|
|
*
|
|
* Copyright (C) 1999 - 2005, Digium, Inc.
|
|
*
|
|
* Mark Spencer <markster@digium.com>
|
|
*
|
|
* See http://www.asterisk.org for more information about
|
|
* the Asterisk project. Please do not directly contact
|
|
* any of the maintainers of this project for assistance;
|
|
* the project provides a web site, mailing lists and IRC
|
|
* channels for your use.
|
|
*
|
|
* This program is free software, distributed under the terms of
|
|
* the GNU General Public License Version 2. See the LICENSE file
|
|
* at the top of the source tree.
|
|
*/
|
|
|
|
/*! \file
|
|
*
|
|
* \brief Trivial application to playback a sound file
|
|
*
|
|
* \author Mark Spencer <markster@digium.com>
|
|
*
|
|
* \ingroup applications
|
|
*/
|
|
|
|
#include "asterisk.h"
|
|
|
|
ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
|
|
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
|
|
#include "asterisk/lock.h"
|
|
#include "asterisk/file.h"
|
|
#include "asterisk/logger.h"
|
|
#include "asterisk/channel.h"
|
|
#include "asterisk/pbx.h"
|
|
#include "asterisk/module.h"
|
|
#include "asterisk/translate.h"
|
|
#include "asterisk/utils.h"
|
|
#include "asterisk/options.h"
|
|
#include "asterisk/app.h"
|
|
#include "asterisk/cli.h"
|
|
#include "asterisk/localtime.h"
|
|
#include "asterisk/say.h"
|
|
|
|
static char *app = "Playback";
|
|
|
|
static char *synopsis = "Play a file";
|
|
|
|
static char *descrip =
|
|
" Playback(filename[&filename2...][,option]): Plays back given filenames (do not put\n"
|
|
"extension). Options may also be included following a pipe symbol. The 'skip'\n"
|
|
"option causes the playback of the message to be skipped if the channel\n"
|
|
"is not in the 'up' state (i.e. it hasn't been answered yet). If 'skip' is \n"
|
|
"specified, the application will return immediately should the channel not be\n"
|
|
"off hook. Otherwise, unless 'noanswer' is specified, the channel will\n"
|
|
"be answered before the sound is played. Not all channels support playing\n"
|
|
"messages while still on hook.\n"
|
|
"This application sets the following channel variable upon completion:\n"
|
|
" PLAYBACKSTATUS The status of the playback attempt as a text string, one of\n"
|
|
" SUCCESS | FAILED\n"
|
|
;
|
|
|
|
|
|
static struct ast_config *say_cfg = NULL;
|
|
/* save the say' api calls.
|
|
* The first entry is NULL if we have the standard source,
|
|
* otherwise we are sourcing from here.
|
|
* 'say load [new|old]' will enable the new or old method, or report status
|
|
*/
|
|
static const void *say_api_buf[40];
|
|
static const char *say_old = "old";
|
|
static const char *say_new = "new";
|
|
|
|
static void save_say_mode(const void *arg)
|
|
{
|
|
int i = 0;
|
|
say_api_buf[i++] = arg;
|
|
|
|
say_api_buf[i++] = ast_say_number_full;
|
|
say_api_buf[i++] = ast_say_enumeration_full;
|
|
say_api_buf[i++] = ast_say_digit_str_full;
|
|
say_api_buf[i++] = ast_say_character_str_full;
|
|
say_api_buf[i++] = ast_say_phonetic_str_full;
|
|
say_api_buf[i++] = ast_say_datetime;
|
|
say_api_buf[i++] = ast_say_time;
|
|
say_api_buf[i++] = ast_say_date;
|
|
say_api_buf[i++] = ast_say_datetime_from_now;
|
|
say_api_buf[i++] = ast_say_date_with_format;
|
|
}
|
|
|
|
static void restore_say_mode(void *arg)
|
|
{
|
|
int i = 0;
|
|
say_api_buf[i++] = arg;
|
|
|
|
ast_say_number_full = say_api_buf[i++];
|
|
ast_say_enumeration_full = say_api_buf[i++];
|
|
ast_say_digit_str_full = say_api_buf[i++];
|
|
ast_say_character_str_full = say_api_buf[i++];
|
|
ast_say_phonetic_str_full = say_api_buf[i++];
|
|
ast_say_datetime = say_api_buf[i++];
|
|
ast_say_time = say_api_buf[i++];
|
|
ast_say_date = say_api_buf[i++];
|
|
ast_say_datetime_from_now = say_api_buf[i++];
|
|
ast_say_date_with_format = say_api_buf[i++];
|
|
}
|
|
|
|
/*
|
|
* Typical 'say' arguments in addition to the date or number or string
|
|
* to say. We do not include 'options' because they may be different
|
|
* in recursive calls, and so they are better left as an external
|
|
* parameter.
|
|
*/
|
|
typedef struct {
|
|
struct ast_channel *chan;
|
|
const char *ints;
|
|
const char *language;
|
|
int audiofd;
|
|
int ctrlfd;
|
|
} say_args_t;
|
|
|
|
static int s_streamwait3(const say_args_t *a, const char *fn)
|
|
{
|
|
int res = ast_streamfile(a->chan, fn, a->language);
|
|
if (res) {
|
|
ast_log(LOG_WARNING, "Unable to play message %s\n", fn);
|
|
return res;
|
|
}
|
|
res = (a->audiofd > -1 && a->ctrlfd > -1) ?
|
|
ast_waitstream_full(a->chan, a->ints, a->audiofd, a->ctrlfd) :
|
|
ast_waitstream(a->chan, a->ints);
|
|
ast_stopstream(a->chan);
|
|
return res;
|
|
}
|
|
|
|
/*
|
|
* the string is 'prefix:data' or prefix:fmt:data'
|
|
* with ':' being invalid in strings.
|
|
*/
|
|
static int do_say(say_args_t *a, const char *s, const char *options, int depth)
|
|
{
|
|
struct ast_variable *v;
|
|
char *lang, *x, *rule = NULL;
|
|
int ret = 0;
|
|
struct varshead head = { .first = NULL, .last = NULL };
|
|
struct ast_var_t *n;
|
|
|
|
ast_log(LOG_WARNING, "string <%s> depth <%d>\n", s, depth);
|
|
if (depth++ > 10) {
|
|
ast_log(LOG_WARNING, "recursion too deep, exiting\n");
|
|
return -1;
|
|
} else if (!say_cfg) {
|
|
ast_log(LOG_WARNING, "no say.conf, cannot spell '%s'\n", s);
|
|
return -1;
|
|
}
|
|
|
|
/* scan languages same as in file.c */
|
|
if (a->language == NULL)
|
|
a->language = "en"; /* default */
|
|
ast_log(LOG_WARNING, "try <%s> in <%s>\n", s, a->language);
|
|
lang = ast_strdupa(a->language);
|
|
for (;;) {
|
|
for (v = ast_variable_browse(say_cfg, lang); v ; v = v->next) {
|
|
if (ast_extension_match(v->name, s)) {
|
|
rule = ast_strdupa(v->value);
|
|
break;
|
|
}
|
|
}
|
|
if (rule)
|
|
break;
|
|
if ( (x = strchr(lang, '_')) )
|
|
*x = '\0'; /* try without suffix */
|
|
else if (strcmp(lang, "en"))
|
|
lang = "en"; /* last resort, try 'en' if not done yet */
|
|
else
|
|
break;
|
|
}
|
|
if (!rule)
|
|
return 0;
|
|
|
|
/* skip up to two prefixes to get the value */
|
|
if ( (x = strchr(s, ':')) )
|
|
s = x + 1;
|
|
if ( (x = strchr(s, ':')) )
|
|
s = x + 1;
|
|
ast_log(LOG_WARNING, "value is <%s>\n", s);
|
|
n = ast_var_assign("SAY", s);
|
|
AST_LIST_INSERT_HEAD(&head, n, entries);
|
|
|
|
/* scan the body, one piece at a time */
|
|
while ( ret <= 0 && (x = strsep(&rule, ",")) ) { /* exit on key */
|
|
char fn[128];
|
|
const char *p, *fmt, *data; /* format and data pointers */
|
|
|
|
/* prepare a decent file name */
|
|
x = ast_skip_blanks(x);
|
|
ast_trim_blanks(x);
|
|
|
|
/* replace variables */
|
|
memset(fn, 0, sizeof(fn)); /* XXX why isn't done in pbx_substitute_variables_helper! */
|
|
pbx_substitute_variables_varshead(&head, x, fn, sizeof(fn));
|
|
ast_log(LOG_WARNING, "doing [%s]\n", fn);
|
|
|
|
/* locate prefix and data, if any */
|
|
fmt = index(fn, ':');
|
|
if (!fmt || fmt == fn) { /* regular filename */
|
|
ret = s_streamwait3(a, fn);
|
|
continue;
|
|
}
|
|
fmt++;
|
|
data = index(fmt, ':'); /* colon before data */
|
|
if (!data || data == fmt) { /* simple prefix-fmt */
|
|
ret = do_say(a, fn, options, depth);
|
|
continue;
|
|
}
|
|
/* prefix:fmt:data */
|
|
for (p = fmt; p < data && ret <= 0; p++) {
|
|
char fn2[sizeof(fn)];
|
|
if (*p == ' ' || *p == '\t') /* skip blanks */
|
|
continue;
|
|
if (*p == '\'') {/* file name - we trim them */
|
|
char *y;
|
|
strcpy(fn2, ast_skip_blanks(p+1)); /* make a full copy */
|
|
y = index(fn2, '\'');
|
|
if (!y) {
|
|
p = data; /* invalid. prepare to end */
|
|
break;
|
|
}
|
|
*y = '\0';
|
|
ast_trim_blanks(fn2);
|
|
p = index(p+1, '\'');
|
|
ret = s_streamwait3(a, fn2);
|
|
} else {
|
|
int l = fmt-fn;
|
|
strcpy(fn2, fn); /* copy everything */
|
|
/* after prefix, append the format */
|
|
fn2[l++] = *p;
|
|
strcpy(fn2 + l, data);
|
|
ret = do_say(a, fn2, options, depth);
|
|
}
|
|
}
|
|
}
|
|
ast_var_delete(n);
|
|
return ret;
|
|
}
|
|
|
|
static int say_full(struct ast_channel *chan, const char *string,
|
|
const char *ints, const char *lang, const char *options,
|
|
int audiofd, int ctrlfd)
|
|
{
|
|
say_args_t a = { chan, ints, lang, audiofd, ctrlfd };
|
|
return do_say(&a, string, options, 0);
|
|
}
|
|
|
|
static int say_number_full(struct ast_channel *chan, int num,
|
|
const char *ints, const char *lang, const char *options,
|
|
int audiofd, int ctrlfd)
|
|
{
|
|
char buf[64];
|
|
say_args_t a = { chan, ints, lang, audiofd, ctrlfd };
|
|
snprintf(buf, sizeof(buf), "num:%d", num);
|
|
return do_say(&a, buf, options, 0);
|
|
}
|
|
|
|
static int say_enumeration_full(struct ast_channel *chan, int num,
|
|
const char *ints, const char *lang, const char *options,
|
|
int audiofd, int ctrlfd)
|
|
{
|
|
char buf[64];
|
|
say_args_t a = { chan, ints, lang, audiofd, ctrlfd };
|
|
snprintf(buf, sizeof(buf), "enum:%d", num);
|
|
return do_say(&a, buf, options, 0);
|
|
}
|
|
|
|
static int say_date_generic(struct ast_channel *chan, time_t t,
|
|
const char *ints, const char *lang, const char *format, const char *timezone, const char *prefix)
|
|
{
|
|
char buf[128];
|
|
struct ast_tm tm;
|
|
struct timeval tv = { t, 0 };
|
|
say_args_t a = { chan, ints, lang, -1, -1 };
|
|
if (format == NULL)
|
|
format = "";
|
|
|
|
ast_localtime(&tv, &tm, NULL);
|
|
snprintf(buf, sizeof(buf), "%s:%s:%04d%02d%02d%02d%02d.%02d-%d-%3d",
|
|
prefix,
|
|
format,
|
|
tm.tm_year+1900,
|
|
tm.tm_mon+1,
|
|
tm.tm_mday,
|
|
tm.tm_hour,
|
|
tm.tm_min,
|
|
tm.tm_sec,
|
|
tm.tm_wday,
|
|
tm.tm_yday);
|
|
return do_say(&a, buf, NULL, 0);
|
|
}
|
|
|
|
static int say_date_with_format(struct ast_channel *chan, time_t t,
|
|
const char *ints, const char *lang, const char *format, const char *timezone)
|
|
{
|
|
return say_date_generic(chan, t, ints, lang, format, timezone, "datetime");
|
|
}
|
|
|
|
static int say_date(struct ast_channel *chan, time_t t, const char *ints, const char *lang)
|
|
{
|
|
return say_date_generic(chan, t, ints, lang, "", NULL, "date");
|
|
}
|
|
|
|
static int say_time(struct ast_channel *chan, time_t t, const char *ints, const char *lang)
|
|
{
|
|
return say_date_generic(chan, t, ints, lang, "", NULL, "time");
|
|
}
|
|
|
|
static int say_datetime(struct ast_channel *chan, time_t t, const char *ints, const char *lang)
|
|
{
|
|
return say_date_generic(chan, t, ints, lang, "", NULL, "datetime");
|
|
}
|
|
|
|
/*
|
|
* remap the 'say' functions to use those in this file
|
|
*/
|
|
static int say_init_mode(char *mode) {
|
|
if (!strcmp(mode, say_new)) {
|
|
if (say_cfg == NULL) {
|
|
ast_log(LOG_ERROR,"There is no say.conf file to use new mode\n");
|
|
return -1;
|
|
}
|
|
save_say_mode(say_new);
|
|
ast_say_number_full = say_number_full;
|
|
|
|
ast_say_enumeration_full = say_enumeration_full;
|
|
#if 0
|
|
ast_say_digits_full = say_digits_full;
|
|
ast_say_digit_str_full = say_digit_str_full;
|
|
ast_say_character_str_full = say_character_str_full;
|
|
ast_say_phonetic_str_full = say_phonetic_str_full;
|
|
ast_say_datetime_from_now = say_datetime_from_now;
|
|
#endif
|
|
ast_say_datetime = say_datetime;
|
|
ast_say_time = say_time;
|
|
ast_say_date = say_date;
|
|
ast_say_date_with_format = say_date_with_format;
|
|
} else if (!strcmp(mode, say_old) && say_api_buf[0] == say_new) {
|
|
restore_say_mode(NULL);
|
|
} else {
|
|
ast_log(LOG_WARNING, "unrecognized mode %s\n", mode);
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static char *__say_cli_init(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
|
|
{
|
|
const char *old_mode = say_api_buf[0] ? say_new : say_old;
|
|
char *mode;
|
|
switch (cmd) {
|
|
case CLI_INIT:
|
|
e->command = "say load [new|old]";
|
|
e->usage =
|
|
"Usage: say load [new|old]\n"
|
|
" say load\n"
|
|
" Report status of current say mode\n"
|
|
" say load new\n"
|
|
" Set say method, configured in say.conf\n"
|
|
" say load old\n"
|
|
" Set old say method, coded in asterisk core\n";
|
|
return NULL;
|
|
case CLI_GENERATE:
|
|
return NULL;
|
|
}
|
|
if (a->argc == 2) {
|
|
ast_cli(a->fd, "say mode is [%s]\n", old_mode);
|
|
return CLI_SUCCESS;
|
|
} else if (a->argc != 3)
|
|
return CLI_SHOWUSAGE;
|
|
mode = a->argv[2];
|
|
if (!strcmp(mode, old_mode))
|
|
ast_log(LOG_NOTICE, "say mode is %s already\n", mode);
|
|
else
|
|
if (say_init_mode(mode) == 0)
|
|
ast_log(LOG_NOTICE, "init say.c from %s to %s\n", old_mode, mode);
|
|
|
|
return CLI_SUCCESS;
|
|
}
|
|
|
|
static struct ast_cli_entry cli_playback[] = {
|
|
AST_CLI_DEFINE(__say_cli_init, "Set or show the say mode"),
|
|
};
|
|
|
|
static int playback_exec(struct ast_channel *chan, void *data)
|
|
{
|
|
int res = 0;
|
|
int mres = 0;
|
|
char *tmp;
|
|
int option_skip=0;
|
|
int option_say=0;
|
|
int option_noanswer = 0;
|
|
|
|
AST_DECLARE_APP_ARGS(args,
|
|
AST_APP_ARG(filenames);
|
|
AST_APP_ARG(options);
|
|
);
|
|
|
|
if (ast_strlen_zero(data)) {
|
|
ast_log(LOG_WARNING, "Playback requires an argument (filename)\n");
|
|
return -1;
|
|
}
|
|
|
|
tmp = ast_strdupa(data);
|
|
AST_STANDARD_APP_ARGS(args, tmp);
|
|
|
|
if (args.options) {
|
|
if (strcasestr(args.options, "skip"))
|
|
option_skip = 1;
|
|
if (strcasestr(args.options, "say"))
|
|
option_say = 1;
|
|
if (strcasestr(args.options, "noanswer"))
|
|
option_noanswer = 1;
|
|
}
|
|
if (chan->_state != AST_STATE_UP) {
|
|
if (option_skip) {
|
|
/* At the user's option, skip if the line is not up */
|
|
goto done;
|
|
} else if (!option_noanswer)
|
|
/* Otherwise answer unless we're supposed to send this while on-hook */
|
|
res = ast_answer(chan);
|
|
}
|
|
if (!res) {
|
|
char *back = args.filenames;
|
|
char *front;
|
|
|
|
ast_stopstream(chan);
|
|
while (!res && (front = strsep(&back, "&"))) {
|
|
if (option_say)
|
|
res = say_full(chan, front, "", chan->language, NULL, -1, -1);
|
|
else
|
|
res = ast_streamfile(chan, front, chan->language);
|
|
if (!res) {
|
|
res = ast_waitstream(chan, "");
|
|
ast_stopstream(chan);
|
|
} else {
|
|
ast_log(LOG_WARNING, "ast_streamfile failed on %s for %s\n", chan->name, (char *)data);
|
|
res = 0;
|
|
mres = 1;
|
|
}
|
|
}
|
|
}
|
|
done:
|
|
pbx_builtin_setvar_helper(chan, "PLAYBACKSTATUS", mres ? "FAILED" : "SUCCESS");
|
|
return res;
|
|
}
|
|
|
|
static int reload(void)
|
|
{
|
|
struct ast_variable *v;
|
|
struct ast_flags config_flags = { CONFIG_FLAG_FILEUNCHANGED };
|
|
struct ast_config *newcfg;
|
|
|
|
if ((newcfg = ast_config_load("say.conf", config_flags)) == CONFIG_STATUS_FILEUNCHANGED)
|
|
return 0;
|
|
|
|
if (say_cfg) {
|
|
ast_config_destroy(say_cfg);
|
|
ast_log(LOG_NOTICE, "Reloading say.conf\n");
|
|
say_cfg = newcfg;
|
|
}
|
|
|
|
if (say_cfg) {
|
|
for (v = ast_variable_browse(say_cfg, "general"); v ; v = v->next) {
|
|
if (ast_extension_match(v->name, "mode")) {
|
|
say_init_mode(v->value);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* XXX here we should sort rules according to the same order
|
|
* we have in pbx.c so we have the same matching behaviour.
|
|
*/
|
|
return 0;
|
|
}
|
|
|
|
static int unload_module(void)
|
|
{
|
|
int res;
|
|
|
|
res = ast_unregister_application(app);
|
|
|
|
ast_cli_unregister_multiple(cli_playback, sizeof(cli_playback) / sizeof(struct ast_cli_entry));
|
|
|
|
if (say_cfg)
|
|
ast_config_destroy(say_cfg);
|
|
|
|
return res;
|
|
}
|
|
|
|
static int load_module(void)
|
|
{
|
|
struct ast_variable *v;
|
|
struct ast_flags config_flags = { 0 };
|
|
|
|
say_cfg = ast_config_load("say.conf", config_flags);
|
|
if (say_cfg) {
|
|
for (v = ast_variable_browse(say_cfg, "general"); v ; v = v->next) {
|
|
if (ast_extension_match(v->name, "mode")) {
|
|
say_init_mode(v->value);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
ast_cli_register_multiple(cli_playback, sizeof(cli_playback) / sizeof(struct ast_cli_entry));
|
|
return ast_register_application(app, playback_exec, synopsis, descrip);
|
|
}
|
|
|
|
AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "Sound File Playback Application",
|
|
.load = load_module,
|
|
.unload = unload_module,
|
|
.reload = reload,
|
|
);
|