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
This commit is contained in:
David M. Lee
2013-07-03 17:58:45 +00:00
parent c4adaf9106
commit a75fd32212
29 changed files with 1247 additions and 146 deletions

View File

@@ -387,10 +387,10 @@ static void stasis_http_record_bridge_cb(
args.max_silence_seconds = atoi(i->value);
} else
if (strcmp(i->name, "append") == 0) {
args.append = atoi(i->value);
args.append = ast_true(i->value);
} else
if (strcmp(i->name, "beep") == 0) {
args.beep = atoi(i->value);
args.beep = ast_true(i->value);
} else
if (strcmp(i->name, "terminateOn") == 0) {
args.terminate_on = (i->value);

View File

@@ -765,11 +765,11 @@ static void stasis_http_record_channel_cb(
if (strcmp(i->name, "maxSilenceSeconds") == 0) {
args.max_silence_seconds = atoi(i->value);
} else
if (strcmp(i->name, "append") == 0) {
args.append = atoi(i->value);
if (strcmp(i->name, "ifExists") == 0) {
args.if_exists = (i->value);
} else
if (strcmp(i->name, "beep") == 0) {
args.beep = atoi(i->value);
args.beep = ast_true(i->value);
} else
if (strcmp(i->name, "terminateOn") == 0) {
args.terminate_on = (i->value);
@@ -788,8 +788,9 @@ static void stasis_http_record_channel_cb(
switch (code) {
case 500: /* Internal server error */
case 400: /* Invalid parameters */
case 404: /* Channel not found */
case 409: /* Channel is not in a Stasis application, or the channel is currently bridged with other channels. */
case 409: /* Channel is not in a Stasis application; the channel is currently bridged with other channels; A recording with the same name is currently in progress. */
is_valid = 1;
break;
default:

View File

@@ -91,7 +91,7 @@ static void stasis_http_get_stored_recordings_cb(
#endif /* AST_DEVMODE */
}
/*!
* \brief Parameter parsing callback for /recordings/stored/{recordingId}.
* \brief Parameter parsing callback for /recordings/stored/{recordingName}.
* \param get_params GET parameters in the HTTP request.
* \param path_vars Path variables extracted from the request.
* \param headers HTTP headers.
@@ -110,8 +110,8 @@ static void stasis_http_get_stored_recording_cb(
struct ast_variable *i;
for (i = path_vars; i; i = i->next) {
if (strcmp(i->name, "recordingId") == 0) {
args.recording_id = (i->value);
if (strcmp(i->name, "recordingName") == 0) {
args.recording_name = (i->value);
} else
{}
}
@@ -128,20 +128,20 @@ static void stasis_http_get_stored_recording_cb(
is_valid = ari_validate_stored_recording(
response->message);
} else {
ast_log(LOG_ERROR, "Invalid error response %d for /recordings/stored/{recordingId}\n", code);
ast_log(LOG_ERROR, "Invalid error response %d for /recordings/stored/{recordingName}\n", code);
is_valid = 0;
}
}
if (!is_valid) {
ast_log(LOG_ERROR, "Response validation failed for /recordings/stored/{recordingId}\n");
ast_log(LOG_ERROR, "Response validation failed for /recordings/stored/{recordingName}\n");
stasis_http_response_error(response, 500,
"Internal Server Error", "Response validation failed");
}
#endif /* AST_DEVMODE */
}
/*!
* \brief Parameter parsing callback for /recordings/stored/{recordingId}.
* \brief Parameter parsing callback for /recordings/stored/{recordingName}.
* \param get_params GET parameters in the HTTP request.
* \param path_vars Path variables extracted from the request.
* \param headers HTTP headers.
@@ -160,8 +160,8 @@ static void stasis_http_delete_stored_recording_cb(
struct ast_variable *i;
for (i = path_vars; i; i = i->next) {
if (strcmp(i->name, "recordingId") == 0) {
args.recording_id = (i->value);
if (strcmp(i->name, "recordingName") == 0) {
args.recording_name = (i->value);
} else
{}
}
@@ -178,13 +178,13 @@ static void stasis_http_delete_stored_recording_cb(
is_valid = ari_validate_void(
response->message);
} else {
ast_log(LOG_ERROR, "Invalid error response %d for /recordings/stored/{recordingId}\n", code);
ast_log(LOG_ERROR, "Invalid error response %d for /recordings/stored/{recordingName}\n", code);
is_valid = 0;
}
}
if (!is_valid) {
ast_log(LOG_ERROR, "Response validation failed for /recordings/stored/{recordingId}\n");
ast_log(LOG_ERROR, "Response validation failed for /recordings/stored/{recordingName}\n");
stasis_http_response_error(response, 500,
"Internal Server Error", "Response validation failed");
}
@@ -233,7 +233,7 @@ static void stasis_http_get_live_recordings_cb(
#endif /* AST_DEVMODE */
}
/*!
* \brief Parameter parsing callback for /recordings/live/{recordingId}.
* \brief Parameter parsing callback for /recordings/live/{recordingName}.
* \param get_params GET parameters in the HTTP request.
* \param path_vars Path variables extracted from the request.
* \param headers HTTP headers.
@@ -252,8 +252,8 @@ static void stasis_http_get_live_recording_cb(
struct ast_variable *i;
for (i = path_vars; i; i = i->next) {
if (strcmp(i->name, "recordingId") == 0) {
args.recording_id = (i->value);
if (strcmp(i->name, "recordingName") == 0) {
args.recording_name = (i->value);
} else
{}
}
@@ -270,20 +270,20 @@ static void stasis_http_get_live_recording_cb(
is_valid = ari_validate_live_recording(
response->message);
} else {
ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingId}\n", code);
ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingName}\n", code);
is_valid = 0;
}
}
if (!is_valid) {
ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingId}\n");
ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingName}\n");
stasis_http_response_error(response, 500,
"Internal Server Error", "Response validation failed");
}
#endif /* AST_DEVMODE */
}
/*!
* \brief Parameter parsing callback for /recordings/live/{recordingId}.
* \brief Parameter parsing callback for /recordings/live/{recordingName}.
* \param get_params GET parameters in the HTTP request.
* \param path_vars Path variables extracted from the request.
* \param headers HTTP headers.
@@ -302,8 +302,8 @@ static void stasis_http_cancel_recording_cb(
struct ast_variable *i;
for (i = path_vars; i; i = i->next) {
if (strcmp(i->name, "recordingId") == 0) {
args.recording_id = (i->value);
if (strcmp(i->name, "recordingName") == 0) {
args.recording_name = (i->value);
} else
{}
}
@@ -320,20 +320,20 @@ static void stasis_http_cancel_recording_cb(
is_valid = ari_validate_void(
response->message);
} else {
ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingId}\n", code);
ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingName}\n", code);
is_valid = 0;
}
}
if (!is_valid) {
ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingId}\n");
ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingName}\n");
stasis_http_response_error(response, 500,
"Internal Server Error", "Response validation failed");
}
#endif /* AST_DEVMODE */
}
/*!
* \brief Parameter parsing callback for /recordings/live/{recordingId}/stop.
* \brief Parameter parsing callback for /recordings/live/{recordingName}/stop.
* \param get_params GET parameters in the HTTP request.
* \param path_vars Path variables extracted from the request.
* \param headers HTTP headers.
@@ -352,8 +352,8 @@ static void stasis_http_stop_recording_cb(
struct ast_variable *i;
for (i = path_vars; i; i = i->next) {
if (strcmp(i->name, "recordingId") == 0) {
args.recording_id = (i->value);
if (strcmp(i->name, "recordingName") == 0) {
args.recording_name = (i->value);
} else
{}
}
@@ -370,20 +370,20 @@ static void stasis_http_stop_recording_cb(
is_valid = ari_validate_void(
response->message);
} else {
ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingId}/stop\n", code);
ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingName}/stop\n", code);
is_valid = 0;
}
}
if (!is_valid) {
ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingId}/stop\n");
ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingName}/stop\n");
stasis_http_response_error(response, 500,
"Internal Server Error", "Response validation failed");
}
#endif /* AST_DEVMODE */
}
/*!
* \brief Parameter parsing callback for /recordings/live/{recordingId}/pause.
* \brief Parameter parsing callback for /recordings/live/{recordingName}/pause.
* \param get_params GET parameters in the HTTP request.
* \param path_vars Path variables extracted from the request.
* \param headers HTTP headers.
@@ -402,8 +402,8 @@ static void stasis_http_pause_recording_cb(
struct ast_variable *i;
for (i = path_vars; i; i = i->next) {
if (strcmp(i->name, "recordingId") == 0) {
args.recording_id = (i->value);
if (strcmp(i->name, "recordingName") == 0) {
args.recording_name = (i->value);
} else
{}
}
@@ -420,20 +420,20 @@ static void stasis_http_pause_recording_cb(
is_valid = ari_validate_void(
response->message);
} else {
ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingId}/pause\n", code);
ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingName}/pause\n", code);
is_valid = 0;
}
}
if (!is_valid) {
ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingId}/pause\n");
ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingName}/pause\n");
stasis_http_response_error(response, 500,
"Internal Server Error", "Response validation failed");
}
#endif /* AST_DEVMODE */
}
/*!
* \brief Parameter parsing callback for /recordings/live/{recordingId}/unpause.
* \brief Parameter parsing callback for /recordings/live/{recordingName}/unpause.
* \param get_params GET parameters in the HTTP request.
* \param path_vars Path variables extracted from the request.
* \param headers HTTP headers.
@@ -452,8 +452,8 @@ static void stasis_http_unpause_recording_cb(
struct ast_variable *i;
for (i = path_vars; i; i = i->next) {
if (strcmp(i->name, "recordingId") == 0) {
args.recording_id = (i->value);
if (strcmp(i->name, "recordingName") == 0) {
args.recording_name = (i->value);
} else
{}
}
@@ -470,20 +470,20 @@ static void stasis_http_unpause_recording_cb(
is_valid = ari_validate_void(
response->message);
} else {
ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingId}/unpause\n", code);
ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingName}/unpause\n", code);
is_valid = 0;
}
}
if (!is_valid) {
ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingId}/unpause\n");
ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingName}/unpause\n");
stasis_http_response_error(response, 500,
"Internal Server Error", "Response validation failed");
}
#endif /* AST_DEVMODE */
}
/*!
* \brief Parameter parsing callback for /recordings/live/{recordingId}/mute.
* \brief Parameter parsing callback for /recordings/live/{recordingName}/mute.
* \param get_params GET parameters in the HTTP request.
* \param path_vars Path variables extracted from the request.
* \param headers HTTP headers.
@@ -502,8 +502,8 @@ static void stasis_http_mute_recording_cb(
struct ast_variable *i;
for (i = path_vars; i; i = i->next) {
if (strcmp(i->name, "recordingId") == 0) {
args.recording_id = (i->value);
if (strcmp(i->name, "recordingName") == 0) {
args.recording_name = (i->value);
} else
{}
}
@@ -520,20 +520,20 @@ static void stasis_http_mute_recording_cb(
is_valid = ari_validate_void(
response->message);
} else {
ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingId}/mute\n", code);
ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingName}/mute\n", code);
is_valid = 0;
}
}
if (!is_valid) {
ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingId}/mute\n");
ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingName}/mute\n");
stasis_http_response_error(response, 500,
"Internal Server Error", "Response validation failed");
}
#endif /* AST_DEVMODE */
}
/*!
* \brief Parameter parsing callback for /recordings/live/{recordingId}/unmute.
* \brief Parameter parsing callback for /recordings/live/{recordingName}/unmute.
* \param get_params GET parameters in the HTTP request.
* \param path_vars Path variables extracted from the request.
* \param headers HTTP headers.
@@ -552,8 +552,8 @@ static void stasis_http_unmute_recording_cb(
struct ast_variable *i;
for (i = path_vars; i; i = i->next) {
if (strcmp(i->name, "recordingId") == 0) {
args.recording_id = (i->value);
if (strcmp(i->name, "recordingName") == 0) {
args.recording_name = (i->value);
} else
{}
}
@@ -570,13 +570,13 @@ static void stasis_http_unmute_recording_cb(
is_valid = ari_validate_void(
response->message);
} else {
ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingId}/unmute\n", code);
ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingName}/unmute\n", code);
is_valid = 0;
}
}
if (!is_valid) {
ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingId}/unmute\n");
ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingName}/unmute\n");
stasis_http_response_error(response, 500,
"Internal Server Error", "Response validation failed");
}
@@ -584,8 +584,8 @@ static void stasis_http_unmute_recording_cb(
}
/*! \brief REST handler for /api-docs/recordings.{format} */
static struct stasis_rest_handlers recordings_stored_recordingId = {
.path_segment = "recordingId",
static struct stasis_rest_handlers recordings_stored_recordingName = {
.path_segment = "recordingName",
.is_wildcard = 1,
.callbacks = {
[AST_HTTP_GET] = stasis_http_get_stored_recording_cb,
@@ -601,10 +601,10 @@ static struct stasis_rest_handlers recordings_stored = {
[AST_HTTP_GET] = stasis_http_get_stored_recordings_cb,
},
.num_children = 1,
.children = { &recordings_stored_recordingId, }
.children = { &recordings_stored_recordingName, }
};
/*! \brief REST handler for /api-docs/recordings.{format} */
static struct stasis_rest_handlers recordings_live_recordingId_stop = {
static struct stasis_rest_handlers recordings_live_recordingName_stop = {
.path_segment = "stop",
.callbacks = {
[AST_HTTP_POST] = stasis_http_stop_recording_cb,
@@ -613,7 +613,7 @@ static struct stasis_rest_handlers recordings_live_recordingId_stop = {
.children = { }
};
/*! \brief REST handler for /api-docs/recordings.{format} */
static struct stasis_rest_handlers recordings_live_recordingId_pause = {
static struct stasis_rest_handlers recordings_live_recordingName_pause = {
.path_segment = "pause",
.callbacks = {
[AST_HTTP_POST] = stasis_http_pause_recording_cb,
@@ -622,7 +622,7 @@ static struct stasis_rest_handlers recordings_live_recordingId_pause = {
.children = { }
};
/*! \brief REST handler for /api-docs/recordings.{format} */
static struct stasis_rest_handlers recordings_live_recordingId_unpause = {
static struct stasis_rest_handlers recordings_live_recordingName_unpause = {
.path_segment = "unpause",
.callbacks = {
[AST_HTTP_POST] = stasis_http_unpause_recording_cb,
@@ -631,7 +631,7 @@ static struct stasis_rest_handlers recordings_live_recordingId_unpause = {
.children = { }
};
/*! \brief REST handler for /api-docs/recordings.{format} */
static struct stasis_rest_handlers recordings_live_recordingId_mute = {
static struct stasis_rest_handlers recordings_live_recordingName_mute = {
.path_segment = "mute",
.callbacks = {
[AST_HTTP_POST] = stasis_http_mute_recording_cb,
@@ -640,7 +640,7 @@ static struct stasis_rest_handlers recordings_live_recordingId_mute = {
.children = { }
};
/*! \brief REST handler for /api-docs/recordings.{format} */
static struct stasis_rest_handlers recordings_live_recordingId_unmute = {
static struct stasis_rest_handlers recordings_live_recordingName_unmute = {
.path_segment = "unmute",
.callbacks = {
[AST_HTTP_POST] = stasis_http_unmute_recording_cb,
@@ -649,15 +649,15 @@ static struct stasis_rest_handlers recordings_live_recordingId_unmute = {
.children = { }
};
/*! \brief REST handler for /api-docs/recordings.{format} */
static struct stasis_rest_handlers recordings_live_recordingId = {
.path_segment = "recordingId",
static struct stasis_rest_handlers recordings_live_recordingName = {
.path_segment = "recordingName",
.is_wildcard = 1,
.callbacks = {
[AST_HTTP_GET] = stasis_http_get_live_recording_cb,
[AST_HTTP_DELETE] = stasis_http_cancel_recording_cb,
},
.num_children = 5,
.children = { &recordings_live_recordingId_stop,&recordings_live_recordingId_pause,&recordings_live_recordingId_unpause,&recordings_live_recordingId_mute,&recordings_live_recordingId_unmute, }
.children = { &recordings_live_recordingName_stop,&recordings_live_recordingName_pause,&recordings_live_recordingName_unpause,&recordings_live_recordingName_mute,&recordings_live_recordingName_unmute, }
};
/*! \brief REST handler for /api-docs/recordings.{format} */
static struct stasis_rest_handlers recordings_live = {
@@ -666,7 +666,7 @@ static struct stasis_rest_handlers recordings_live = {
[AST_HTTP_GET] = stasis_http_get_live_recordings_cb,
},
.num_children = 1,
.children = { &recordings_live_recordingId, }
.children = { &recordings_live_recordingName, }
};
/*! \brief REST handler for /api-docs/recordings.{format} */
static struct stasis_rest_handlers recordings = {

View File

@@ -37,6 +37,7 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
#include "asterisk/file.h"
#include "asterisk/logger.h"
#include "asterisk/module.h"
#include "asterisk/paths.h"
#include "asterisk/stasis_app_impl.h"
#include "asterisk/stasis_app_playback.h"
#include "asterisk/stasis_channels.h"
@@ -195,7 +196,7 @@ static void *play_uri(struct stasis_app_control *control,
RAII_VAR(struct stasis_app_playback *, playback, NULL,
playback_cleanup);
RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
const char *file;
RAII_VAR(char *, file, NULL, ast_free);
int res;
long offsetms;
@@ -225,16 +226,27 @@ static void *play_uri(struct stasis_app_control *control,
if (ast_begins_with(playback->media, SOUND_URI_SCHEME)) {
/* Play sound */
file = playback->media + strlen(SOUND_URI_SCHEME);
file = ast_strdup(playback->media + strlen(SOUND_URI_SCHEME));
} else if (ast_begins_with(playback->media, RECORDING_URI_SCHEME)) {
/* Play recording */
file = playback->media + strlen(RECORDING_URI_SCHEME);
const char *relname =
playback->media + strlen(RECORDING_URI_SCHEME);
if (relname[0] == '/') {
file = ast_strdup(relname);
} else {
ast_asprintf(&file, "%s/%s",
ast_config_AST_RECORDING_DIR, relname);
}
} else {
/* Play URL */
ast_log(LOG_ERROR, "Unimplemented\n");
return NULL;
}
if (!file) {
return NULL;
}
res = ast_control_streamfile_lang(chan, file, fwd, rev, stop, pause,
restart, playback->skipms, playback->language, &offsetms);

443
res/res_stasis_recording.c Normal file
View File

@@ -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");

View File

@@ -0,0 +1,6 @@
{
global:
LINKER_SYMBOL_PREFIXstasis_app_*;
local:
*;
};

View File

@@ -1,4 +1,4 @@
/* -*- C -*-
/*
* Asterisk -- An open source telephony toolkit.
*
* Copyright (C) 2012 - 2013, Digium, Inc.
@@ -39,6 +39,7 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
#include "asterisk/callerid.h"
#include "asterisk/stasis_app.h"
#include "asterisk/stasis_app_playback.h"
#include "asterisk/stasis_app_recording.h"
#include "asterisk/stasis_channels.h"
#include "resource_channels.h"
@@ -249,10 +250,139 @@ void stasis_http_play_on_channel(struct ast_variable *headers,
stasis_http_response_created(response, playback_url, json);
}
void stasis_http_record_channel(struct ast_variable *headers, struct ast_record_channel_args *args, struct stasis_http_response *response)
void stasis_http_record_channel(struct ast_variable *headers,
struct ast_record_channel_args *args,
struct stasis_http_response *response)
{
ast_log(LOG_ERROR, "TODO: stasis_http_record_channel\n");
RAII_VAR(struct stasis_app_control *, control, NULL, ao2_cleanup);
RAII_VAR(struct ast_channel_snapshot *, snapshot, NULL, ao2_cleanup);
RAII_VAR(struct stasis_app_recording *, recording, NULL, ao2_cleanup);
RAII_VAR(char *, recording_url, NULL, ast_free);
RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
RAII_VAR(struct stasis_app_recording_options *, options, NULL,
ao2_cleanup);
RAII_VAR(char *, uri_encoded_name, NULL, ast_free);
size_t uri_name_maxlen;
ast_assert(response != NULL);
if (args->max_duration_seconds < 0) {
stasis_http_response_error(
response, 400, "Bad Request",
"max_duration_seconds cannot be negative");
return;
}
if (args->max_silence_seconds < 0) {
stasis_http_response_error(
response, 400, "Bad Request",
"max_silence_seconds cannot be negative");
return;
}
control = find_control(response, args->channel_id);
if (control == NULL) {
/* Response filled in by find_control */
return;
}
options = stasis_app_recording_options_create(args->name, args->format);
if (options == NULL) {
stasis_http_response_error(
response, 500, "Internal Server Error",
"Out of memory");
}
options->max_silence_seconds = args->max_silence_seconds;
options->max_duration_seconds = args->max_duration_seconds;
options->terminate_on =
stasis_app_recording_termination_parse(args->terminate_on);
options->if_exists =
stasis_app_recording_if_exists_parse(args->if_exists);
options->beep = args->beep;
if (options->terminate_on == STASIS_APP_RECORDING_TERMINATE_INVALID) {
stasis_http_response_error(
response, 400, "Bad Request",
"terminateOn invalid");
return;
}
if (options->if_exists == -1) {
stasis_http_response_error(
response, 400, "Bad Request",
"ifExists invalid");
return;
}
recording = stasis_app_control_record(control, options);
if (recording == NULL) {
switch(errno) {
case EINVAL:
/* While the arguments are invalid, we should have
* caught them prior to calling record.
*/
stasis_http_response_error(
response, 500, "Internal Server Error",
"Error parsing request");
break;
case EEXIST:
stasis_http_response_error(response, 409, "Conflict",
"Recording '%s' already in progress",
args->name);
break;
case ENOMEM:
stasis_http_response_error(
response, 500, "Internal Server Error",
"Out of memory");
break;
case EPERM:
stasis_http_response_error(
response, 400, "Bad Request",
"Recording name invalid");
break;
default:
ast_log(LOG_WARNING,
"Unrecognized recording error: %s\n",
strerror(errno));
stasis_http_response_error(
response, 500, "Internal Server Error",
"Internal Server Error");
break;
}
return;
}
uri_name_maxlen = strlen(args->name) * 3;
uri_encoded_name = ast_malloc(uri_name_maxlen);
if (!uri_encoded_name) {
stasis_http_response_error(
response, 500, "Internal Server Error",
"Out of memory");
return;
}
ast_uri_encode(args->name, uri_encoded_name, uri_name_maxlen,
ast_uri_http);
ast_asprintf(&recording_url, "/recordings/live/%s", uri_encoded_name);
if (!recording_url) {
stasis_http_response_error(
response, 500, "Internal Server Error",
"Out of memory");
return;
}
json = stasis_app_recording_to_json(recording);
if (!json) {
stasis_http_response_error(
response, 500, "Internal Server Error",
"Out of memory");
return;
}
stasis_http_response_created(response, recording_url, json);
}
void stasis_http_get_channel(struct ast_variable *headers,
struct ast_get_channel_args *args,
struct stasis_http_response *response)

View File

@@ -247,8 +247,8 @@ struct ast_record_channel_args {
int max_duration_seconds;
/*! \brief Maximum duration of silence, in seconds. 0 for no limit */
int max_silence_seconds;
/*! \brief If true, and recording already exists, append to recording */
int append;
/*! \brief Action to take if a recording with the same name already exists. */
const char *if_exists;
/*! \brief Play beep when recording begins */
int beep;
/*! \brief DTMF input to terminate recording */

View File

@@ -27,6 +27,7 @@
ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
#include "asterisk/stasis_app_recording.h"
#include "resource_recordings.h"
void stasis_http_get_stored_recordings(struct ast_variable *headers, struct ast_get_stored_recordings_args *args, struct stasis_http_response *response)
@@ -45,10 +46,31 @@ void stasis_http_get_live_recordings(struct ast_variable *headers, struct ast_ge
{
ast_log(LOG_ERROR, "TODO: stasis_http_get_live_recordings\n");
}
void stasis_http_get_live_recording(struct ast_variable *headers, struct ast_get_live_recording_args *args, struct stasis_http_response *response)
void stasis_http_get_live_recording(struct ast_variable *headers,
struct ast_get_live_recording_args *args,
struct stasis_http_response *response)
{
ast_log(LOG_ERROR, "TODO: stasis_http_get_live_recording\n");
RAII_VAR(struct stasis_app_recording *, recording, NULL, ao2_cleanup);
RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
recording = stasis_app_recording_find_by_name(args->recording_name);
if (recording == NULL) {
stasis_http_response_error(response, 404, "Not Found",
"Recording not found");
return;
}
json = stasis_app_recording_to_json(recording);
if (json == NULL) {
stasis_http_response_error(response, 500,
"Internal Server Error", "Error building response");
return;
}
stasis_http_response_ok(response, ast_json_ref(json));
}
void stasis_http_cancel_recording(struct ast_variable *headers, struct ast_cancel_recording_args *args, struct stasis_http_response *response)
{
ast_log(LOG_ERROR, "TODO: stasis_http_cancel_recording\n");

View File

@@ -52,8 +52,8 @@ struct ast_get_stored_recordings_args {
void stasis_http_get_stored_recordings(struct ast_variable *headers, struct ast_get_stored_recordings_args *args, struct stasis_http_response *response);
/*! \brief Argument struct for stasis_http_get_stored_recording() */
struct ast_get_stored_recording_args {
/*! \brief Recording's id */
const char *recording_id;
/*! \brief The name of the recording */
const char *recording_name;
};
/*!
* \brief Get a stored recording's details.
@@ -65,8 +65,8 @@ struct ast_get_stored_recording_args {
void stasis_http_get_stored_recording(struct ast_variable *headers, struct ast_get_stored_recording_args *args, struct stasis_http_response *response);
/*! \brief Argument struct for stasis_http_delete_stored_recording() */
struct ast_delete_stored_recording_args {
/*! \brief Recording's id */
const char *recording_id;
/*! \brief The name of the recording */
const char *recording_name;
};
/*!
* \brief Delete a stored recording.
@@ -89,8 +89,8 @@ struct ast_get_live_recordings_args {
void stasis_http_get_live_recordings(struct ast_variable *headers, struct ast_get_live_recordings_args *args, struct stasis_http_response *response);
/*! \brief Argument struct for stasis_http_get_live_recording() */
struct ast_get_live_recording_args {
/*! \brief Recording's id */
const char *recording_id;
/*! \brief The name of the recording */
const char *recording_name;
};
/*!
* \brief List live recordings.
@@ -102,8 +102,8 @@ struct ast_get_live_recording_args {
void stasis_http_get_live_recording(struct ast_variable *headers, struct ast_get_live_recording_args *args, struct stasis_http_response *response);
/*! \brief Argument struct for stasis_http_cancel_recording() */
struct ast_cancel_recording_args {
/*! \brief Recording's id */
const char *recording_id;
/*! \brief The name of the recording */
const char *recording_name;
};
/*!
* \brief Stop a live recording and discard it.
@@ -115,8 +115,8 @@ struct ast_cancel_recording_args {
void stasis_http_cancel_recording(struct ast_variable *headers, struct ast_cancel_recording_args *args, struct stasis_http_response *response);
/*! \brief Argument struct for stasis_http_stop_recording() */
struct ast_stop_recording_args {
/*! \brief Recording's id */
const char *recording_id;
/*! \brief The name of the recording */
const char *recording_name;
};
/*!
* \brief Stop a live recording and store it.
@@ -128,12 +128,14 @@ struct ast_stop_recording_args {
void stasis_http_stop_recording(struct ast_variable *headers, struct ast_stop_recording_args *args, struct stasis_http_response *response);
/*! \brief Argument struct for stasis_http_pause_recording() */
struct ast_pause_recording_args {
/*! \brief Recording's id */
const char *recording_id;
/*! \brief The name of the recording */
const char *recording_name;
};
/*!
* \brief Pause a live recording.
*
* Pausing a recording suspends silence detection, which will be restarted when the recording is unpaused.
*
* \param headers HTTP headers
* \param args Swagger parameters
* \param[out] response HTTP response
@@ -141,8 +143,8 @@ struct ast_pause_recording_args {
void stasis_http_pause_recording(struct ast_variable *headers, struct ast_pause_recording_args *args, struct stasis_http_response *response);
/*! \brief Argument struct for stasis_http_unpause_recording() */
struct ast_unpause_recording_args {
/*! \brief Recording's id */
const char *recording_id;
/*! \brief The name of the recording */
const char *recording_name;
};
/*!
* \brief Unpause a live recording.
@@ -154,12 +156,14 @@ struct ast_unpause_recording_args {
void stasis_http_unpause_recording(struct ast_variable *headers, struct ast_unpause_recording_args *args, struct stasis_http_response *response);
/*! \brief Argument struct for stasis_http_mute_recording() */
struct ast_mute_recording_args {
/*! \brief Recording's id */
const char *recording_id;
/*! \brief The name of the recording */
const char *recording_name;
};
/*!
* \brief Mute a live recording.
*
* Muting a recording suspends silence detection, which will be restarted when the recording is unmuted.
*
* \param headers HTTP headers
* \param args Swagger parameters
* \param[out] response HTTP response
@@ -167,8 +171,8 @@ struct ast_mute_recording_args {
void stasis_http_mute_recording(struct ast_variable *headers, struct ast_mute_recording_args *args, struct stasis_http_response *response);
/*! \brief Argument struct for stasis_http_unmute_recording() */
struct ast_unmute_recording_args {
/*! \brief Recording's id */
const char *recording_id;
/*! \brief The name of the recording */
const char *recording_name;
};
/*!
* \brief Unmute a live recording.