summaryrefslogtreecommitdiff
path: root/res/res_stasis_recording.c
diff options
context:
space:
mode:
authorDavid M. Lee <dlee@digium.com>2013-07-03 17:58:45 +0000
committerDavid M. Lee <dlee@digium.com>2013-07-03 17:58:45 +0000
commita75fd32212c35b41143442bd757387fad636177a (patch)
tree461033acf36f4596d8fc9800a1195e12207b3ea2 /res/res_stasis_recording.c
parentc4adaf91067559dd5aa90577e181693abade0602 (diff)
ARI - channel recording support
This patch is the first step in adding recording support to the Asterisk REST Interface. Recordings are stored in /var/spool/recording. Since recordings may be destructive (overwriting existing files), the API rejects attempts to escape the recording directory (avoiding issues if someone attempts to record to ../../lib/sounds/greeting, for example). (closes issue ASTERISK-21594) (closes issue ASTERISK-21581) Review: https://reviewboard.asterisk.org/r/2612/ git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@393550 65c4cc65-6c06-0410-ace0-fbb531ad65f3
Diffstat (limited to 'res/res_stasis_recording.c')
-rw-r--r--res/res_stasis_recording.c443
1 files changed, 443 insertions, 0 deletions
diff --git a/res/res_stasis_recording.c b/res/res_stasis_recording.c
new file mode 100644
index 000000000..3d8e11bbd
--- /dev/null
+++ b/res/res_stasis_recording.c
@@ -0,0 +1,443 @@
+/*
+ * 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
+
+STASIS_MESSAGE_TYPE_DEFN(stasis_app_recording_snapshot_type);
+
+/*! 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 playing back to */
+ struct stasis_app_control *control;
+
+ /*! Current state of the recording. */
+ enum stasis_app_recording_state state;
+};
+
+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";
+ }
+
+ 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 -1;
+}
+
+static void recording_publish(struct stasis_app_recording *recording)
+{
+ RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
+ RAII_VAR(struct ast_channel_snapshot *, snapshot, NULL, ao2_cleanup);
+ RAII_VAR(struct stasis_message *, message, NULL, ao2_cleanup);
+
+ ast_assert(recording != NULL);
+
+ json = stasis_app_recording_to_json(recording);
+ if (json == NULL) {
+ 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_fail(struct stasis_app_recording *recording)
+{
+ SCOPED_AO2LOCK(lock, recording);
+ recording->state = STASIS_APP_RECORDING_STATE_FAILED;
+ recording_publish(recording);
+}
+
+static void recording_cleanup(struct stasis_app_recording *recording)
+{
+ ao2_unlink_flags(recordings, recording,
+ OBJ_POINTER | OBJ_UNLINK | OBJ_NODATA);
+}
+
+static void *record_file(struct stasis_app_control *control,
+ struct ast_channel *chan, void *data)
+{
+ RAII_VAR(struct stasis_app_recording *, recording,
+ NULL, recording_cleanup);
+ char *acceptdtmf;
+ int res;
+ int duration = 0;
+
+ recording = data;
+ ast_assert(recording != NULL);
+
+ ao2_lock(recording);
+ recording->state = STASIS_APP_RECORDING_STATE_RECORDING;
+ recording_publish(recording);
+ ao2_unlock(recording);
+
+ 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(recording);
+ return NULL;
+ }
+
+ ast_play_and_record_full(chan,
+ recording->options->beep ? "beep" : NULL,
+ recording->absolute_name,
+ recording->options->max_duration_seconds,
+ recording->options->format,
+ &duration,
+ NULL, /* sound_duration */
+ -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));
+
+ ao2_lock(recording);
+ recording->state = STASIS_APP_RECORDING_STATE_COMPLETE;
+ recording_publish(recording);
+ ao2_unlock(recording);
+
+ return NULL;
+}
+
+static void recording_dtor(void *obj)
+{
+ struct stasis_app_recording *recording = obj;
+
+ ao2_cleanup(recording->options);
+}
+
+struct stasis_app_recording *stasis_app_control_record(
+ struct stasis_app_control *control,
+ struct stasis_app_recording_options *options)
+{
+ RAII_VAR(struct stasis_app_recording *, recording, NULL, ao2_cleanup);
+ 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;
+ }
+
+ ast_asprintf(&recording->absolute_name, "%s/%s",
+ ast_config_AST_RECORDING_DIR, options->name);
+
+ if (recording->absolute_name == NULL) {
+ errno = ENOMEM;
+ 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 */
+ return NULL;
+ }
+ *last_slash = '/';
+ }
+
+ ao2_ref(options, +1);
+ recording->options = options;
+ recording->control = control;
+ recording->state = STASIS_APP_RECORDING_STATE_QUEUED;
+
+ {
+ 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;
+ return NULL;
+ }
+ ao2_link(recordings, recording);
+ }
+
+ /* A ref is kept in the recordings container; no need to bump */
+ stasis_app_send_command_async(control, record_file, recording);
+
+ /* Although this should be bumped for the caller */
+ ao2_ref(recording, +1);
+ 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)
+{
+ RAII_VAR(struct stasis_app_recording *, recording, NULL, ao2_cleanup);
+
+ recording = ao2_find(recordings, name, OBJ_KEY);
+ if (recording == NULL) {
+ return NULL;
+ }
+
+ ao2_ref(recording, +1);
+ return recording;
+}
+
+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}",
+ "name", recording->options->name,
+ "format", recording->options->format,
+ "state", state_to_string(recording->state));
+
+ return ast_json_ref(json);
+}
+
+enum stasis_app_recording_oper_results stasis_app_recording_operation(
+ struct stasis_app_recording *recording,
+ enum stasis_app_recording_media_operation operation)
+{
+ ast_assert(0); // TODO
+ return STASIS_APP_RECORDING_OPER_FAILED;
+}
+
+static int load_module(void)
+{
+ int r;
+
+ r = STASIS_MESSAGE_TYPE_INIT(stasis_app_recording_snapshot_type);
+ if (r != 0) {
+ return AST_MODULE_LOAD_FAILURE;
+ }
+
+ recordings = ao2_container_alloc(RECORDING_BUCKETS, recording_hash,
+ recording_cmp);
+ if (!recordings) {
+ return AST_MODULE_LOAD_FAILURE;
+ }
+ 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,
+ "Stasis application recording support",
+ .load = load_module,
+ .unload = unload_module,
+ .nonoptreq = "res_stasis");