123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661 |
- /*
- * Asterisk -- An open source telephony toolkit.
- *
- * Copyright (C) 2013, Digium, Inc.
- *
- * David M. Lee, II <dlee@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 res_stasis recording support.
- *
- * \author David M. Lee, II <dlee@digium.com>
- */
- /*** MODULEINFO
- <depend type="module">res_stasis</depend>
- <support_level>core</support_level>
- ***/
- #include "asterisk.h"
- ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
- #include "asterisk/dsp.h"
- #include "asterisk/file.h"
- #include "asterisk/module.h"
- #include "asterisk/paths.h"
- #include "asterisk/stasis_app_impl.h"
- #include "asterisk/stasis_app_recording.h"
- #include "asterisk/stasis_channels.h"
- /*! Number of hash buckets for recording container. Keep it prime! */
- #define RECORDING_BUCKETS 127
- /*! Comment is ignored by most formats, so we will ignore it, too. */
- #define RECORDING_COMMENT NULL
- /*! Recording check is unimplemented. le sigh */
- #define RECORDING_CHECK 0
- /*! Container of all current recordings */
- static struct ao2_container *recordings;
- struct stasis_app_recording {
- /*! Recording options. */
- struct stasis_app_recording_options *options;
- /*! Absolute path (minus extension) of the recording */
- char *absolute_name;
- /*! Control object for the channel we're recording */
- struct stasis_app_control *control;
- /*! Current state of the recording. */
- enum stasis_app_recording_state state;
- /*! Duration calculations */
- struct {
- /*! Total duration */
- int total;
- /*! Duration minus any silence */
- int energy_only;
- } duration;
- /*! Indicates whether the recording is currently muted */
- int muted:1;
- };
- static struct ast_json *recording_to_json(struct stasis_message *message,
- const struct stasis_message_sanitizer *sanitize)
- {
- struct ast_channel_blob *channel_blob = stasis_message_data(message);
- struct ast_json *blob = channel_blob->blob;
- const char *state =
- ast_json_string_get(ast_json_object_get(blob, "state"));
- const char *type;
- if (!strcmp(state, "recording")) {
- type = "RecordingStarted";
- } else if (!strcmp(state, "done") || !strcasecmp(state, "canceled")) {
- type = "RecordingFinished";
- } else if (!strcmp(state, "failed")) {
- type = "RecordingFailed";
- } else {
- return NULL;
- }
- return ast_json_pack("{s: s, s: o}",
- "type", type,
- "recording", ast_json_deep_copy(blob));
- }
- STASIS_MESSAGE_TYPE_DEFN(stasis_app_recording_snapshot_type,
- .to_json = recording_to_json,
- );
- static int recording_hash(const void *obj, int flags)
- {
- const struct stasis_app_recording *recording = obj;
- const char *id = flags & OBJ_KEY ? obj : recording->options->name;
- return ast_str_hash(id);
- }
- static int recording_cmp(void *obj, void *arg, int flags)
- {
- struct stasis_app_recording *lhs = obj;
- struct stasis_app_recording *rhs = arg;
- const char *rhs_id = flags & OBJ_KEY ? arg : rhs->options->name;
- if (strcmp(lhs->options->name, rhs_id) == 0) {
- return CMP_MATCH | CMP_STOP;
- } else {
- return 0;
- }
- }
- static const char *state_to_string(enum stasis_app_recording_state state)
- {
- switch (state) {
- case STASIS_APP_RECORDING_STATE_QUEUED:
- return "queued";
- case STASIS_APP_RECORDING_STATE_RECORDING:
- return "recording";
- case STASIS_APP_RECORDING_STATE_PAUSED:
- return "paused";
- case STASIS_APP_RECORDING_STATE_COMPLETE:
- return "done";
- case STASIS_APP_RECORDING_STATE_FAILED:
- return "failed";
- case STASIS_APP_RECORDING_STATE_CANCELED:
- return "canceled";
- case STASIS_APP_RECORDING_STATE_MAX:
- return "?";
- }
- return "?";
- }
- static void recording_options_dtor(void *obj)
- {
- struct stasis_app_recording_options *options = obj;
- ast_string_field_free_memory(options);
- }
- struct stasis_app_recording_options *stasis_app_recording_options_create(
- const char *name, const char *format)
- {
- RAII_VAR(struct stasis_app_recording_options *, options, NULL,
- ao2_cleanup);
- options = ao2_alloc(sizeof(*options), recording_options_dtor);
- if (!options || ast_string_field_init(options, 128)) {
- return NULL;
- }
- ast_string_field_set(options, name, name);
- ast_string_field_set(options, format, format);
- ao2_ref(options, +1);
- return options;
- }
- char stasis_app_recording_termination_parse(const char *str)
- {
- if (ast_strlen_zero(str)) {
- return STASIS_APP_RECORDING_TERMINATE_NONE;
- }
- if (strcasecmp(str, "none") == 0) {
- return STASIS_APP_RECORDING_TERMINATE_NONE;
- }
- if (strcasecmp(str, "any") == 0) {
- return STASIS_APP_RECORDING_TERMINATE_ANY;
- }
- if (strcasecmp(str, "#") == 0) {
- return '#';
- }
- if (strcasecmp(str, "*") == 0) {
- return '*';
- }
- return STASIS_APP_RECORDING_TERMINATE_INVALID;
- }
- enum ast_record_if_exists stasis_app_recording_if_exists_parse(
- const char *str)
- {
- if (ast_strlen_zero(str)) {
- /* Default value */
- return AST_RECORD_IF_EXISTS_FAIL;
- }
- if (strcasecmp(str, "fail") == 0) {
- return AST_RECORD_IF_EXISTS_FAIL;
- }
- if (strcasecmp(str, "overwrite") == 0) {
- return AST_RECORD_IF_EXISTS_OVERWRITE;
- }
- if (strcasecmp(str, "append") == 0) {
- return AST_RECORD_IF_EXISTS_APPEND;
- }
- return AST_RECORD_IF_EXISTS_ERROR;
- }
- static void recording_publish(struct stasis_app_recording *recording, const char *cause)
- {
- RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
- RAII_VAR(struct stasis_message *, message, NULL, ao2_cleanup);
- ast_assert(recording != NULL);
- json = stasis_app_recording_to_json(recording);
- if (json == NULL) {
- return;
- }
- if (!ast_strlen_zero(cause)) {
- struct ast_json *failure_cause = ast_json_string_create(cause);
- if (!failure_cause) {
- return;
- }
- if (ast_json_object_set(json, "cause", failure_cause)) {
- return;
- }
- }
- message = ast_channel_blob_create_from_cache(
- stasis_app_control_get_channel_id(recording->control),
- stasis_app_recording_snapshot_type(), json);
- if (message == NULL) {
- return;
- }
- stasis_app_control_publish(recording->control, message);
- }
- static void recording_set_state(struct stasis_app_recording *recording,
- enum stasis_app_recording_state state,
- const char *cause)
- {
- SCOPED_AO2LOCK(lock, recording);
- recording->state = state;
- recording_publish(recording, cause);
- }
- static enum stasis_app_control_channel_result check_rule_recording(
- const struct stasis_app_control *control)
- {
- return STASIS_APP_CHANNEL_RECORDING;
- }
- /*
- * XXX This only works because there is one and only one rule in
- * the system so it can be added to any number of channels
- * without issue. However, as soon as there is another rule then
- * watch out for weirdness because of cross linked lists.
- */
- static struct stasis_app_control_rule rule_recording = {
- .check_rule = check_rule_recording
- };
- static void recording_fail(struct stasis_app_control *control,
- struct stasis_app_recording *recording,
- const char *cause)
- {
- stasis_app_control_unregister_add_rule(control, &rule_recording);
- recording_set_state(
- recording, STASIS_APP_RECORDING_STATE_FAILED, cause);
- }
- static void recording_cleanup(void *data)
- {
- struct stasis_app_recording *recording = data;
- ao2_unlink_flags(recordings, recording,
- OBJ_POINTER | OBJ_UNLINK | OBJ_NODATA);
- ao2_ref(recording, -1);
- }
- static int record_file(struct stasis_app_control *control,
- struct ast_channel *chan, void *data)
- {
- struct stasis_app_recording *recording = data;
- char *acceptdtmf;
- int res;
- ast_assert(recording != NULL);
- if (stasis_app_get_bridge(control)) {
- ast_log(LOG_ERROR, "Cannot record channel while in bridge\n");
- recording_fail(control, recording, "Cannot record channel while in bridge");
- return -1;
- }
- switch (recording->options->terminate_on) {
- case STASIS_APP_RECORDING_TERMINATE_NONE:
- case STASIS_APP_RECORDING_TERMINATE_INVALID:
- acceptdtmf = "";
- break;
- case STASIS_APP_RECORDING_TERMINATE_ANY:
- acceptdtmf = "#*0123456789abcd";
- break;
- default:
- acceptdtmf = ast_alloca(2);
- acceptdtmf[0] = recording->options->terminate_on;
- acceptdtmf[1] = '\0';
- }
- res = ast_auto_answer(chan);
- if (res != 0) {
- ast_debug(3, "%s: Failed to answer\n",
- ast_channel_uniqueid(chan));
- recording_fail(control, recording, "Failed to answer channel");
- return -1;
- }
- recording_set_state(
- recording, STASIS_APP_RECORDING_STATE_RECORDING, NULL);
- ast_play_and_record_full(chan,
- NULL, /* playfile */
- recording->absolute_name,
- recording->options->max_duration_seconds,
- recording->options->format,
- &recording->duration.total,
- recording->options->max_silence_seconds ? &recording->duration.energy_only : NULL,
- recording->options->beep,
- -1, /* silencethreshold */
- recording->options->max_silence_seconds * 1000,
- NULL, /* path */
- acceptdtmf,
- NULL, /* canceldtmf */
- 1, /* skip_confirmation_sound */
- recording->options->if_exists);
- ast_debug(3, "%s: Recording complete\n", ast_channel_uniqueid(chan));
- recording_set_state(
- recording, STASIS_APP_RECORDING_STATE_COMPLETE, NULL);
- stasis_app_control_unregister_add_rule(control, &rule_recording);
- return 0;
- }
- static void recording_dtor(void *obj)
- {
- struct stasis_app_recording *recording = obj;
- ast_free(recording->absolute_name);
- ao2_cleanup(recording->control);
- ao2_cleanup(recording->options);
- }
- struct stasis_app_recording *stasis_app_control_record(
- struct stasis_app_control *control,
- struct stasis_app_recording_options *options)
- {
- struct stasis_app_recording *recording;
- char *last_slash;
- errno = 0;
- if (options == NULL ||
- ast_strlen_zero(options->name) ||
- ast_strlen_zero(options->format) ||
- options->max_silence_seconds < 0 ||
- options->max_duration_seconds < 0) {
- errno = EINVAL;
- return NULL;
- }
- ast_debug(3, "%s: Sending record(%s.%s) command\n",
- stasis_app_control_get_channel_id(control), options->name,
- options->format);
- recording = ao2_alloc(sizeof(*recording), recording_dtor);
- if (!recording) {
- errno = ENOMEM;
- return NULL;
- }
- recording->duration.total = -1;
- recording->duration.energy_only = -1;
- ast_asprintf(&recording->absolute_name, "%s/%s",
- ast_config_AST_RECORDING_DIR, options->name);
- if (recording->absolute_name == NULL) {
- errno = ENOMEM;
- ao2_ref(recording, -1);
- return NULL;
- }
- if ((last_slash = strrchr(recording->absolute_name, '/'))) {
- *last_slash = '\0';
- if (ast_safe_mkdir(ast_config_AST_RECORDING_DIR,
- recording->absolute_name, 0777) != 0) {
- /* errno set by ast_mkdir */
- ao2_ref(recording, -1);
- return NULL;
- }
- *last_slash = '/';
- }
- ao2_ref(options, +1);
- recording->options = options;
- ao2_ref(control, +1);
- recording->control = control;
- recording->state = STASIS_APP_RECORDING_STATE_QUEUED;
- if ((recording->options->if_exists == AST_RECORD_IF_EXISTS_FAIL) &&
- (ast_fileexists(recording->absolute_name, NULL, NULL))) {
- ast_log(LOG_WARNING, "Recording file '%s' already exists and ifExists option is failure.\n",
- recording->absolute_name);
- errno = EEXIST;
- ao2_ref(recording, -1);
- return NULL;
- }
- {
- RAII_VAR(struct stasis_app_recording *, old_recording, NULL,
- ao2_cleanup);
- SCOPED_AO2LOCK(lock, recordings);
- old_recording = ao2_find(recordings, options->name,
- OBJ_KEY | OBJ_NOLOCK);
- if (old_recording) {
- ast_log(LOG_WARNING,
- "Recording %s already in progress\n",
- recording->options->name);
- errno = EEXIST;
- ao2_ref(recording, -1);
- return NULL;
- }
- ao2_link(recordings, recording);
- }
- stasis_app_control_register_add_rule(control, &rule_recording);
- stasis_app_send_command_async(control, record_file, ao2_bump(recording), recording_cleanup);
- return recording;
- }
- enum stasis_app_recording_state stasis_app_recording_get_state(
- struct stasis_app_recording *recording)
- {
- return recording->state;
- }
- const char *stasis_app_recording_get_name(
- struct stasis_app_recording *recording)
- {
- return recording->options->name;
- }
- struct stasis_app_recording *stasis_app_recording_find_by_name(const char *name)
- {
- return ao2_find(recordings, name, OBJ_KEY);
- }
- struct ast_json *stasis_app_recording_to_json(
- const struct stasis_app_recording *recording)
- {
- RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
- if (recording == NULL) {
- return NULL;
- }
- json = ast_json_pack("{s: s, s: s, s: s, s: s}",
- "name", recording->options->name,
- "format", recording->options->format,
- "state", state_to_string(recording->state),
- "target_uri", recording->options->target);
- if (json && recording->duration.total > -1) {
- ast_json_object_set(json, "duration",
- ast_json_integer_create(recording->duration.total));
- }
- if (json && recording->duration.energy_only > -1) {
- ast_json_object_set(json, "talking_duration",
- ast_json_integer_create(recording->duration.energy_only));
- ast_json_object_set(json, "silence_duration",
- ast_json_integer_create(recording->duration.total - recording->duration.energy_only));
- }
- return ast_json_ref(json);
- }
- typedef int (*recording_operation_cb)(struct stasis_app_recording *recording);
- static int recording_noop(struct stasis_app_recording *recording)
- {
- return 0;
- }
- static int recording_disregard(struct stasis_app_recording *recording)
- {
- recording->state = STASIS_APP_RECORDING_STATE_CANCELED;
- return 0;
- }
- static int recording_cancel(struct stasis_app_recording *recording)
- {
- int res = 0;
- recording->state = STASIS_APP_RECORDING_STATE_CANCELED;
- res |= stasis_app_control_queue_control(recording->control,
- AST_CONTROL_RECORD_CANCEL);
- res |= ast_filedelete(recording->absolute_name, NULL);
- return res;
- }
- static int recording_stop(struct stasis_app_recording *recording)
- {
- recording->state = STASIS_APP_RECORDING_STATE_COMPLETE;
- return stasis_app_control_queue_control(recording->control,
- AST_CONTROL_RECORD_STOP);
- }
- static int recording_pause(struct stasis_app_recording *recording)
- {
- recording->state = STASIS_APP_RECORDING_STATE_PAUSED;
- return stasis_app_control_queue_control(recording->control,
- AST_CONTROL_RECORD_SUSPEND);
- }
- static int recording_unpause(struct stasis_app_recording *recording)
- {
- recording->state = STASIS_APP_RECORDING_STATE_RECORDING;
- return stasis_app_control_queue_control(recording->control,
- AST_CONTROL_RECORD_SUSPEND);
- }
- static int recording_mute(struct stasis_app_recording *recording)
- {
- if (recording->muted) {
- /* already muted */
- return 0;
- }
- recording->muted = 1;
- return stasis_app_control_queue_control(recording->control,
- AST_CONTROL_RECORD_MUTE);
- }
- static int recording_unmute(struct stasis_app_recording *recording)
- {
- if (!recording->muted) {
- /* already unmuted */
- return 0;
- }
- return stasis_app_control_queue_control(recording->control,
- AST_CONTROL_RECORD_MUTE);
- }
- recording_operation_cb operations[STASIS_APP_RECORDING_STATE_MAX][STASIS_APP_RECORDING_OPER_MAX] = {
- [STASIS_APP_RECORDING_STATE_QUEUED][STASIS_APP_RECORDING_CANCEL] = recording_disregard,
- [STASIS_APP_RECORDING_STATE_QUEUED][STASIS_APP_RECORDING_STOP] = recording_disregard,
- [STASIS_APP_RECORDING_STATE_RECORDING][STASIS_APP_RECORDING_CANCEL] = recording_cancel,
- [STASIS_APP_RECORDING_STATE_RECORDING][STASIS_APP_RECORDING_STOP] = recording_stop,
- [STASIS_APP_RECORDING_STATE_RECORDING][STASIS_APP_RECORDING_PAUSE] = recording_pause,
- [STASIS_APP_RECORDING_STATE_RECORDING][STASIS_APP_RECORDING_UNPAUSE] = recording_noop,
- [STASIS_APP_RECORDING_STATE_RECORDING][STASIS_APP_RECORDING_MUTE] = recording_mute,
- [STASIS_APP_RECORDING_STATE_RECORDING][STASIS_APP_RECORDING_UNMUTE] = recording_unmute,
- [STASIS_APP_RECORDING_STATE_PAUSED][STASIS_APP_RECORDING_CANCEL] = recording_cancel,
- [STASIS_APP_RECORDING_STATE_PAUSED][STASIS_APP_RECORDING_STOP] = recording_stop,
- [STASIS_APP_RECORDING_STATE_PAUSED][STASIS_APP_RECORDING_PAUSE] = recording_noop,
- [STASIS_APP_RECORDING_STATE_PAUSED][STASIS_APP_RECORDING_UNPAUSE] = recording_unpause,
- [STASIS_APP_RECORDING_STATE_PAUSED][STASIS_APP_RECORDING_MUTE] = recording_mute,
- [STASIS_APP_RECORDING_STATE_PAUSED][STASIS_APP_RECORDING_UNMUTE] = recording_unmute,
- };
- enum stasis_app_recording_oper_results stasis_app_recording_operation(
- struct stasis_app_recording *recording,
- enum stasis_app_recording_media_operation operation)
- {
- recording_operation_cb cb;
- SCOPED_AO2LOCK(lock, recording);
- if ((unsigned int)recording->state >= STASIS_APP_RECORDING_STATE_MAX) {
- ast_log(LOG_WARNING, "Invalid recording state %u\n",
- recording->state);
- return -1;
- }
- if ((unsigned int)operation >= STASIS_APP_RECORDING_OPER_MAX) {
- ast_log(LOG_WARNING, "Invalid recording operation %u\n",
- operation);
- return -1;
- }
- cb = operations[recording->state][operation];
- if (!cb) {
- if (recording->state != STASIS_APP_RECORDING_STATE_RECORDING) {
- /* So we can be specific in our error message. */
- return STASIS_APP_RECORDING_OPER_NOT_RECORDING;
- } else {
- /* And, really, all operations should be valid during
- * recording */
- ast_log(LOG_ERROR,
- "Unhandled operation during recording: %u\n",
- operation);
- return STASIS_APP_RECORDING_OPER_FAILED;
- }
- }
- return cb(recording) ?
- STASIS_APP_RECORDING_OPER_FAILED : STASIS_APP_RECORDING_OPER_OK;
- }
- static int load_module(void)
- {
- int r;
- r = STASIS_MESSAGE_TYPE_INIT(stasis_app_recording_snapshot_type);
- if (r != 0) {
- return AST_MODULE_LOAD_DECLINE;
- }
- recordings = ao2_container_alloc_hash(AO2_ALLOC_OPT_LOCK_MUTEX, 0, RECORDING_BUCKETS,
- recording_hash, NULL, recording_cmp);
- if (!recordings) {
- STASIS_MESSAGE_TYPE_CLEANUP(stasis_app_recording_snapshot_type);
- return AST_MODULE_LOAD_DECLINE;
- }
- return AST_MODULE_LOAD_SUCCESS;
- }
- static int unload_module(void)
- {
- ao2_cleanup(recordings);
- recordings = NULL;
- STASIS_MESSAGE_TYPE_CLEANUP(stasis_app_recording_snapshot_type);
- return 0;
- }
- AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS | AST_MODFLAG_LOAD_ORDER, "Stasis application recording support",
- .support_level = AST_MODULE_SUPPORT_CORE,
- .load = load_module,
- .unload = unload_module,
- .load_pri = AST_MODPRI_APP_DEPEND,
- );
|