summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonathan Rose <jrose@digium.com>2013-07-19 19:35:21 +0000
committerJonathan Rose <jrose@digium.com>2013-07-19 19:35:21 +0000
commit17c546173fe1f24749af4643f19b40be180803de (patch)
tree46d8cf430d12142587196703c732d5e9ce9bba8e
parent5a8f32703c445f7d09b5e029e85d76692626a67f (diff)
ARI: Bridge Playback, Bridge Record
Adds a new channel driver for creating channels for specific purposes in bridges, primarily to act as either recorders or announcers. Adds ARI commands for playing announcements to ever participant in a bridge as well as for recording a bridge. This patch also includes some documentation/reponse fixes to related ARI models such as playback controls. (closes issue ASTERISK-21592) Reported by: Matt Jordan (closes issue ASTERISK-21593) Reported by: Matt Jordan Review: https://reviewboard.asterisk.org/r/2670/ git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@394809 65c4cc65-6c06-0410-ace0-fbb531ad65f3
-rw-r--r--channels/chan_bridge_media.c218
-rw-r--r--include/asterisk/core_unreal.h15
-rw-r--r--include/asterisk/logger.h11
-rw-r--r--include/asterisk/stasis_app.h23
-rw-r--r--include/asterisk/stasis_app_playback.h13
-rw-r--r--main/core_unreal.c90
-rw-r--r--res/res_stasis.c17
-rw-r--r--res/res_stasis_http_bridges.c87
-rw-r--r--res/res_stasis_http_channels.c2
-rw-r--r--res/res_stasis_http_playback.c2
-rw-r--r--res/res_stasis_playback.c29
-rw-r--r--res/stasis/control.c5
-rw-r--r--res/stasis_http/ari_model_validators.c44
-rw-r--r--res/stasis_http/ari_model_validators.h4
-rw-r--r--res/stasis_http/resource_bridges.c274
-rw-r--r--res/stasis_http/resource_bridges.h29
-rw-r--r--res/stasis_http/resource_channels.c2
-rw-r--r--rest-api/api-docs/bridges.json113
-rw-r--r--rest-api/api-docs/channels.json2
-rw-r--r--rest-api/api-docs/playback.json2
-rw-r--r--rest-api/api-docs/recordings.json10
21 files changed, 961 insertions, 31 deletions
diff --git a/channels/chan_bridge_media.c b/channels/chan_bridge_media.c
new file mode 100644
index 000000000..d24201207
--- /dev/null
+++ b/channels/chan_bridge_media.c
@@ -0,0 +1,218 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2013 Digium, Inc.
+ *
+ * Jonathan Rose <jrose@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 Bridge Media Channels driver
+ *
+ * \author Jonathan Rose <jrose@digium.com>
+ * \author Richard Mudgett <rmudgett@digium.com>
+ *
+ * \brief Bridge Media Channels
+ *
+ * \ingroup channel_drivers
+ */
+
+/*** MODULEINFO
+ <support_level>core</support_level>
+ ***/
+
+#include "asterisk.h"
+
+ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
+
+#include "asterisk/channel.h"
+#include "asterisk/bridging.h"
+#include "asterisk/core_unreal.h"
+#include "asterisk/module.h"
+
+static int media_call(struct ast_channel *chan, const char *addr, int timeout)
+{
+ /* ast_call() will fail unconditionally against channels provided by this driver */
+ return -1;
+}
+
+static int media_hangup(struct ast_channel *ast)
+{
+ struct ast_unreal_pvt *p = ast_channel_tech_pvt(ast);
+ int res;
+
+ if (!p) {
+ return -1;
+ }
+
+ /* Give the pvt a ref to fulfill calling requirements. */
+ ao2_ref(p, +1);
+ res = ast_unreal_hangup(p, ast);
+ ao2_ref(p, -1);
+
+ return res;
+}
+
+static struct ast_channel *announce_request(const char *type, struct ast_format_cap *cap,
+ const struct ast_channel *requestor, const char *data, int *cause);
+
+static struct ast_channel *record_request(const char *type, struct ast_format_cap *cap,
+ const struct ast_channel *requestor, const char *data, int *cause);
+
+static struct ast_channel_tech announce_tech = {
+ .type = "Announcer",
+ .description = "Bridge Media Announcing Channel Driver",
+ .requester = announce_request,
+ .call = media_call,
+ .hangup = media_hangup,
+
+ .send_digit_begin = ast_unreal_digit_begin,
+ .send_digit_end = ast_unreal_digit_end,
+ .read = ast_unreal_read,
+ .write = ast_unreal_write,
+ .write_video = ast_unreal_write,
+ .exception = ast_unreal_read,
+ .indicate = ast_unreal_indicate,
+ .fixup = ast_unreal_fixup,
+ .send_html = ast_unreal_sendhtml,
+ .send_text = ast_unreal_sendtext,
+ .queryoption = ast_unreal_queryoption,
+ .setoption = ast_unreal_setoption,
+ .properties = AST_CHAN_TP_ANNOUNCER,
+};
+
+static struct ast_channel_tech record_tech = {
+ .type = "Recorder",
+ .description = "Bridge Media Recording Channel Driver",
+ .requester = record_request,
+ .call = media_call,
+ .hangup = media_hangup,
+
+ .send_digit_begin = ast_unreal_digit_begin,
+ .send_digit_end = ast_unreal_digit_end,
+ .read = ast_unreal_read,
+ .write = ast_unreal_write,
+ .write_video = ast_unreal_write,
+ .exception = ast_unreal_read,
+ .indicate = ast_unreal_indicate,
+ .fixup = ast_unreal_fixup,
+ .send_html = ast_unreal_sendhtml,
+ .send_text = ast_unreal_sendtext,
+ .queryoption = ast_unreal_queryoption,
+ .setoption = ast_unreal_setoption,
+ .properties = AST_CHAN_TP_RECORDER,
+};
+
+static struct ast_channel *media_request_helper(struct ast_format_cap *cap,
+ const struct ast_channel *requestor, const char *data, struct ast_channel_tech *tech, const char *role)
+{
+ struct ast_channel *chan;
+
+ RAII_VAR(struct ast_callid *, callid, NULL, ast_callid_cleanup);
+ RAII_VAR(struct ast_unreal_pvt *, pvt, NULL, ao2_cleanup);
+
+ if (!(pvt = ast_unreal_alloc(sizeof(*pvt), ast_unreal_destructor, cap))) {
+ return NULL;
+ }
+
+ ast_copy_string(pvt->name, data, sizeof(pvt->name));
+
+ ast_set_flag(pvt, AST_UNREAL_NO_OPTIMIZATION);
+
+ callid = ast_read_threadstorage_callid();
+
+ chan = ast_unreal_new_channels(pvt, tech,
+ AST_STATE_UP, AST_STATE_UP, NULL, NULL, requestor, callid);
+ if (!chan) {
+ return NULL;
+ }
+
+ ast_answer(pvt->owner);
+ ast_answer(pvt->chan);
+
+ if (ast_channel_add_bridge_role(pvt->chan, role)) {
+ ast_hangup(chan);
+ return NULL;
+ }
+
+ return chan;
+}
+
+static struct ast_channel *announce_request(const char *type, struct ast_format_cap *cap,
+ const struct ast_channel *requestor, const char *data, int *cause)
+{
+ return media_request_helper(cap, requestor, data, &announce_tech, "announcer");
+}
+
+static struct ast_channel *record_request(const char *type, struct ast_format_cap *cap,
+ const struct ast_channel *requestor, const char *data, int *cause)
+{
+ return media_request_helper(cap, requestor, data, &record_tech, "recorder");
+}
+
+static void cleanup_capabilities(void)
+{
+ if (announce_tech.capabilities) {
+ announce_tech.capabilities = ast_format_cap_destroy(announce_tech.capabilities);
+ }
+
+ if (record_tech.capabilities) {
+ record_tech.capabilities = ast_format_cap_destroy(record_tech.capabilities);
+ }
+}
+
+static int unload_module(void)
+{
+ ast_channel_unregister(&announce_tech);
+ ast_channel_unregister(&record_tech);
+ cleanup_capabilities();
+ return 0;
+}
+
+static int load_module(void)
+{
+ announce_tech.capabilities = ast_format_cap_alloc();
+ if (!announce_tech.capabilities) {
+ return AST_MODULE_LOAD_DECLINE;
+ }
+
+ record_tech.capabilities = ast_format_cap_alloc();
+ if (!record_tech.capabilities) {
+ return AST_MODULE_LOAD_DECLINE;
+ }
+
+ ast_format_cap_add_all(announce_tech.capabilities);
+ ast_format_cap_add_all(record_tech.capabilities);
+
+ if (ast_channel_register(&announce_tech)) {
+ ast_log(LOG_ERROR, "Unable to register channel technology %s(%s).\n",
+ announce_tech.type, announce_tech.description);
+ cleanup_capabilities();
+ return AST_MODULE_LOAD_DECLINE;
+ }
+
+ if (ast_channel_register(&record_tech)) {
+ ast_log(LOG_ERROR, "Unable to register channel technology %s(%s).\n",
+ record_tech.type, record_tech.description);
+ cleanup_capabilities();
+ return AST_MODULE_LOAD_DECLINE;
+ }
+
+ return AST_MODULE_LOAD_SUCCESS;
+}
+
+AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "Bridge Media Channel Driver",
+ .load = load_module,
+ .unload = unload_module,
+);
diff --git a/include/asterisk/core_unreal.h b/include/asterisk/core_unreal.h
index a6e98895a..751c5d455 100644
--- a/include/asterisk/core_unreal.h
+++ b/include/asterisk/core_unreal.h
@@ -31,6 +31,7 @@
#include "asterisk/astobj2.h"
#include "asterisk/channel.h"
+#include "asterisk/bridging.h"
#include "asterisk/abstract_jb.h"
#if defined(__cplusplus) || defined(c_plusplus)
@@ -208,6 +209,20 @@ struct ast_channel *ast_unreal_new_channels(struct ast_unreal_pvt *p,
*/
void ast_unreal_call_setup(struct ast_channel *semi1, struct ast_channel *semi2);
+/*!
+ * \brief Push the semi2 unreal channel into a bridge from either member of the unreal pair
+ * \since 12.0.0
+ *
+ * \param ast A member of the unreal channel being pushed
+ * \param bridge Which bridge we want to push the channel to
+ *
+ * \retval 0 if the channel is successfully imparted onto the bridge
+ * \retval -1 on failure
+ *
+ * \note This is equivalent to ast_call() on unreal based channel drivers that are designed to use it instead.
+ */
+int ast_unreal_channel_push_to_bridge(struct ast_channel *ast, struct ast_bridge *bridge);
+
/* ------------------------------------------------------------------- */
#if defined(__cplusplus) || defined(c_plusplus)
diff --git a/include/asterisk/logger.h b/include/asterisk/logger.h
index 09d2b4c7a..bbad84bb3 100644
--- a/include/asterisk/logger.h
+++ b/include/asterisk/logger.h
@@ -281,7 +281,16 @@ struct ast_callid *ast_read_threadstorage_callid(void);
*
* \retval NULL always
*/
-#define ast_callid_unref(c) ({ ao2_ref(c, -1); (NULL); })
+#define ast_callid_unref(c) ({ ao2_ref(c, -1); (struct ast_callid *) (NULL); })
+
+/*!
+ * \brief Cleanup a callid reference (NULL safe ao2 unreference)
+ *
+ * \param c the ast_callid
+ *
+ * \retval NULL always
+ */
+#define ast_callid_cleanup(c) ({ ao2_cleanup(c); (struct ast_callid *) (NULL); })
/*!
* \brief Sets what is stored in the thread storage to the given
diff --git a/include/asterisk/stasis_app.h b/include/asterisk/stasis_app.h
index 731133674..244d9d1e5 100644
--- a/include/asterisk/stasis_app.h
+++ b/include/asterisk/stasis_app.h
@@ -127,6 +127,29 @@ struct stasis_app_control *stasis_app_control_find_by_channel_id(
const char *channel_id);
/*!
+ * \brief Creates a control handler for a channel that isn't in a stasis app.
+ * \since 12.0.0
+ *
+ * \param chan Channel to create controller handle for
+ *
+ * \return NULL on failure to create the handle
+ * \return Pointer to \c res_stasis handler.
+ */
+struct stasis_app_control *stasis_app_control_create(
+ struct ast_channel *chan);
+
+/*!
+ * \brief Act on a stasis app control queue until it is empty
+ * \since 12.0.0
+ *
+ * \param chan Channel to handle
+ * \param control Control object to execute
+ */
+void stasis_app_control_execute_until_exhausted(
+ struct ast_channel *chan,
+ struct stasis_app_control *control);
+
+/*!
* \brief Returns the uniqueid of the channel associated with this control
*
* \param control Control object.
diff --git a/include/asterisk/stasis_app_playback.h b/include/asterisk/stasis_app_playback.h
index 59c2aab49..3587871a9 100644
--- a/include/asterisk/stasis_app_playback.h
+++ b/include/asterisk/stasis_app_playback.h
@@ -69,6 +69,13 @@ enum stasis_app_playback_media_operation {
STASIS_PLAYBACK_MEDIA_OP_MAX,
};
+enum stasis_app_playback_target_type {
+ /*! The target is a channel */
+ STASIS_PLAYBACK_TARGET_CHANNEL = 0,
+ /*! The target is a bridge */
+ STASIS_PLAYBACK_TARGET_BRIDGE,
+};
+
/*!
* \brief Play a file to the control's channel.
*
@@ -79,6 +86,8 @@ enum stasis_app_playback_media_operation {
* \param control Control for \c res_stasis.
* \param file Base filename for the file to play.
* \param language Selects the file based on language.
+ * \param target_id ID of the target bridge or channel.
+ * \param target_type What the target type is
* \param skipms Number of milliseconds to skip for forward/reverse operations.
* \param offsetms Number of milliseconds to skip before playing.
* \return Playback control object.
@@ -86,7 +95,9 @@ enum stasis_app_playback_media_operation {
*/
struct stasis_app_playback *stasis_app_control_play_uri(
struct stasis_app_control *control, const char *file,
- const char *language, int skipms, long offsetms);
+ const char *language, const char *target_id,
+ enum stasis_app_playback_target_type target_type,
+ int skipms, long offsetms);
/*!
* \brief Gets the current state of a playback operation.
diff --git a/main/core_unreal.c b/main/core_unreal.c
index 71d0f6c8f..3d14a716f 100644
--- a/main/core_unreal.c
+++ b/main/core_unreal.c
@@ -668,6 +668,96 @@ void ast_unreal_call_setup(struct ast_channel *semi1, struct ast_channel *semi2)
ast_channel_datastore_inherit(semi1, semi2);
}
+int ast_unreal_channel_push_to_bridge(struct ast_channel *ast, struct ast_bridge *bridge)
+{
+ struct ast_bridge_features *features;
+ struct ast_channel *chan;
+ struct ast_channel *owner;
+ RAII_VAR(struct ast_unreal_pvt *, p, NULL, ao2_cleanup);
+
+ RAII_VAR(struct ast_callid *, bridge_callid, NULL, ast_callid_cleanup);
+
+ ast_bridge_lock(bridge);
+ bridge_callid = bridge->callid ? ast_callid_ref(bridge->callid) : NULL;
+ ast_bridge_unlock(bridge);
+
+ {
+ SCOPED_CHANNELLOCK(lock, ast);
+ p = ast_channel_tech_pvt(ast);
+ if (!p) {
+ return -1;
+ }
+ ao2_ref(p, +1);
+ }
+
+ {
+ SCOPED_AO2LOCK(lock, p);
+ chan = p->chan;
+ if (!chan) {
+ return -1;
+ }
+
+ owner = p->owner;
+ if (!owner) {
+ return -1;
+ }
+
+ ast_channel_ref(chan);
+ ast_channel_ref(owner);
+ }
+
+ if (bridge_callid) {
+ struct ast_callid *chan_callid;
+ struct ast_callid *owner_callid;
+
+ /* chan side call ID setting */
+ ast_channel_lock(chan);
+
+ chan_callid = ast_channel_callid(chan);
+ if (!chan_callid) {
+ ast_channel_callid_set(chan, bridge_callid);
+ }
+ ast_channel_unlock(chan);
+ ast_callid_cleanup(chan_callid);
+
+ /* owner side call ID setting */
+ ast_channel_lock(owner);
+
+ owner_callid = ast_channel_callid(owner);
+ if (!owner_callid) {
+ ast_channel_callid_set(owner, bridge_callid);
+ }
+
+ ast_channel_unlock(owner);
+ ast_callid_cleanup(owner_callid);
+ }
+
+ /* We are done with the owner now that its call ID matches the bridge */
+ ast_channel_unref(owner);
+ owner = NULL;
+
+ features = ast_bridge_features_new();
+ if (!features) {
+ ast_channel_unref(chan);
+ return -1;
+ }
+ ast_set_flag(&features->feature_flags, AST_BRIDGE_CHANNEL_FLAG_IMMOVABLE);
+
+ /* Impart the semi2 channel into the bridge */
+ if (ast_bridge_impart(bridge, chan, NULL, features, 1)) {
+ ast_bridge_features_destroy(features);
+ ast_channel_unref(chan);
+ return -1;
+ }
+
+ ao2_lock(p);
+ ast_set_flag(p, AST_UNREAL_CARETAKER_THREAD);
+ ao2_unlock(p);
+ ast_channel_unref(chan);
+
+ return 0;
+}
+
int ast_unreal_hangup(struct ast_unreal_pvt *p, struct ast_channel *ast)
{
int hangup_chan = 0;
diff --git a/res/res_stasis.c b/res/res_stasis.c
index ed3823051..a294db915 100644
--- a/res/res_stasis.c
+++ b/res/res_stasis.c
@@ -146,6 +146,11 @@ static int control_compare(void *lhs, void *rhs, int flags)
}
}
+struct stasis_app_control *stasis_app_control_create(struct ast_channel *chan)
+{
+ return control_create(chan);
+}
+
struct stasis_app_control *stasis_app_control_find_by_channel(
const struct ast_channel *chan)
{
@@ -531,6 +536,16 @@ int app_send_end_msg(struct app *app, struct ast_channel *chan)
return 0;
}
+void stasis_app_control_execute_until_exhausted(struct ast_channel *chan, struct stasis_app_control *control)
+{
+ while (!control_is_done(control)) {
+ int command_count = control_dispatch_all(control, chan);
+ if (command_count == 0 || ast_channel_fdno(chan) == -1) {
+ break;
+ }
+ }
+}
+
/*! /brief Stasis dialplan application callback */
int stasis_app_exec(struct ast_channel *chan, const char *app_name, int argc,
char *argv[])
@@ -750,7 +765,7 @@ static struct ast_json *simple_bridge_channel_event(
struct ast_channel_snapshot *channel_snapshot,
const struct timeval *tv)
{
- return ast_json_pack("{s: s, s: o, s: o}",
+ return ast_json_pack("{s: s, s: o, s: o, s: o}",
"type", type,
"timestamp", ast_json_timeval(*tv, NULL),
"bridge", ast_bridge_snapshot_to_json(bridge_snapshot),
diff --git a/res/res_stasis_http_bridges.c b/res/res_stasis_http_bridges.c
index f46b7ac1c..86093abb2 100644
--- a/res/res_stasis_http_bridges.c
+++ b/res/res_stasis_http_bridges.c
@@ -358,6 +358,73 @@ static void stasis_http_remove_channel_from_bridge_cb(
#endif /* AST_DEVMODE */
}
/*!
+ * \brief Parameter parsing callback for /bridges/{bridgeId}/play.
+ * \param get_params GET parameters in the HTTP request.
+ * \param path_vars Path variables extracted from the request.
+ * \param headers HTTP headers.
+ * \param[out] response Response to the HTTP request.
+ */
+static void stasis_http_play_on_bridge_cb(
+ struct ast_variable *get_params, struct ast_variable *path_vars,
+ struct ast_variable *headers, struct stasis_http_response *response)
+{
+#if defined(AST_DEVMODE)
+ int is_valid;
+ int code;
+#endif /* AST_DEVMODE */
+
+ struct ast_play_on_bridge_args args = {};
+ struct ast_variable *i;
+
+ for (i = get_params; i; i = i->next) {
+ if (strcmp(i->name, "media") == 0) {
+ args.media = (i->value);
+ } else
+ if (strcmp(i->name, "lang") == 0) {
+ args.lang = (i->value);
+ } else
+ if (strcmp(i->name, "offsetms") == 0) {
+ args.offsetms = atoi(i->value);
+ } else
+ if (strcmp(i->name, "skipms") == 0) {
+ args.skipms = atoi(i->value);
+ } else
+ {}
+ }
+ for (i = path_vars; i; i = i->next) {
+ if (strcmp(i->name, "bridgeId") == 0) {
+ args.bridge_id = (i->value);
+ } else
+ {}
+ }
+ stasis_http_play_on_bridge(headers, &args, response);
+#if defined(AST_DEVMODE)
+ code = response->response_code;
+
+ switch (code) {
+ case 500: /* Internal server error */
+ case 404: /* Bridge not found */
+ case 409: /* Bridge not in a Stasis application */
+ is_valid = 1;
+ break;
+ default:
+ if (200 <= code && code <= 299) {
+ is_valid = ari_validate_playback(
+ response->message);
+ } else {
+ ast_log(LOG_ERROR, "Invalid error response %d for /bridges/{bridgeId}/play\n", code);
+ is_valid = 0;
+ }
+ }
+
+ if (!is_valid) {
+ ast_log(LOG_ERROR, "Response validation failed for /bridges/{bridgeId}/play\n");
+ stasis_http_response_error(response, 500,
+ "Internal Server Error", "Response validation failed");
+ }
+#endif /* AST_DEVMODE */
+}
+/*!
* \brief Parameter parsing callback for /bridges/{bridgeId}/record.
* \param get_params GET parameters in the HTTP request.
* \param path_vars Path variables extracted from the request.
@@ -380,14 +447,17 @@ static void stasis_http_record_bridge_cb(
if (strcmp(i->name, "name") == 0) {
args.name = (i->value);
} else
+ if (strcmp(i->name, "format") == 0) {
+ args.format = (i->value);
+ } else
if (strcmp(i->name, "maxDurationSeconds") == 0) {
args.max_duration_seconds = atoi(i->value);
} else
if (strcmp(i->name, "maxSilenceSeconds") == 0) {
args.max_silence_seconds = atoi(i->value);
} else
- if (strcmp(i->name, "append") == 0) {
- args.append = ast_true(i->value);
+ if (strcmp(i->name, "ifExists") == 0) {
+ args.if_exists = (i->value);
} else
if (strcmp(i->name, "beep") == 0) {
args.beep = ast_true(i->value);
@@ -448,6 +518,15 @@ static struct stasis_rest_handlers bridges_bridgeId_removeChannel = {
.children = { }
};
/*! \brief REST handler for /api-docs/bridges.{format} */
+static struct stasis_rest_handlers bridges_bridgeId_play = {
+ .path_segment = "play",
+ .callbacks = {
+ [AST_HTTP_POST] = stasis_http_play_on_bridge_cb,
+ },
+ .num_children = 0,
+ .children = { }
+};
+/*! \brief REST handler for /api-docs/bridges.{format} */
static struct stasis_rest_handlers bridges_bridgeId_record = {
.path_segment = "record",
.callbacks = {
@@ -464,8 +543,8 @@ static struct stasis_rest_handlers bridges_bridgeId = {
[AST_HTTP_GET] = stasis_http_get_bridge_cb,
[AST_HTTP_DELETE] = stasis_http_delete_bridge_cb,
},
- .num_children = 3,
- .children = { &bridges_bridgeId_addChannel,&bridges_bridgeId_removeChannel,&bridges_bridgeId_record, }
+ .num_children = 4,
+ .children = { &bridges_bridgeId_addChannel,&bridges_bridgeId_removeChannel,&bridges_bridgeId_play,&bridges_bridgeId_record, }
};
/*! \brief REST handler for /api-docs/bridges.{format} */
static struct stasis_rest_handlers bridges = {
diff --git a/res/res_stasis_http_channels.c b/res/res_stasis_http_channels.c
index 050ef00d1..806c370ed 100644
--- a/res/res_stasis_http_channels.c
+++ b/res/res_stasis_http_channels.c
@@ -796,7 +796,7 @@ static void stasis_http_record_channel_cb(
break;
default:
if (200 <= code && code <= 299) {
- is_valid = ari_validate_void(
+ is_valid = ari_validate_live_recording(
response->message);
} else {
ast_log(LOG_ERROR, "Invalid error response %d for /channels/{channelId}/record\n", code);
diff --git a/res/res_stasis_http_playback.c b/res/res_stasis_http_playback.c
index 0e56e6229..9b6ab594d 100644
--- a/res/res_stasis_http_playback.c
+++ b/res/res_stasis_http_playback.c
@@ -192,7 +192,7 @@ static void stasis_http_control_playback_cb(
break;
default:
if (200 <= code && code <= 299) {
- is_valid = ari_validate_playback(
+ is_valid = ari_validate_void(
response->message);
} else {
ast_log(LOG_ERROR, "Invalid error response %d for /playback/{playbackId}/control\n", code);
diff --git a/res/res_stasis_playback.c b/res/res_stasis_playback.c
index 5b55ebc51..483aff8c2 100644
--- a/res/res_stasis_playback.c
+++ b/res/res_stasis_playback.c
@@ -64,6 +64,7 @@ struct stasis_app_playback {
AST_STRING_FIELD(id); /*!< Playback unique id */
AST_STRING_FIELD(media); /*!< Playback media uri */
AST_STRING_FIELD(language); /*!< Preferred language */
+ AST_STRING_FIELD(target); /*!< Playback device uri */
);
/*! Control object for the channel we're playing back to */
struct stasis_app_control *control;
@@ -263,9 +264,31 @@ static void playback_dtor(void *obj)
ast_string_field_free_memory(playback);
}
+static void set_target_uri(
+ struct stasis_app_playback *playback,
+ enum stasis_app_playback_target_type target_type,
+ const char *target_id)
+{
+ const char *type = NULL;
+ switch (target_type) {
+ case STASIS_PLAYBACK_TARGET_CHANNEL:
+ type = "channel";
+ break;
+ case STASIS_PLAYBACK_TARGET_BRIDGE:
+ type = "bridge";
+ break;
+ }
+
+ ast_assert(type != NULL);
+
+ ast_string_field_build(playback, target, "%s:%s", type, target_id);
+}
+
struct stasis_app_playback *stasis_app_control_play_uri(
struct stasis_app_control *control, const char *uri,
- const char *language, int skipms, long offsetms)
+ const char *language, const char *target_id,
+ enum stasis_app_playback_target_type target_type,
+ int skipms, long offsetms)
{
RAII_VAR(struct stasis_app_playback *, playback, NULL, ao2_cleanup);
char id[AST_UUID_STR_LEN];
@@ -290,6 +313,7 @@ struct stasis_app_playback *stasis_app_control_play_uri(
ast_string_field_set(playback, id, id);
ast_string_field_set(playback, media, uri);
ast_string_field_set(playback, language, language);
+ set_target_uri(playback, target_type, target_id);
playback->control = control;
playback->skipms = skipms;
playback->offsetms = offsetms;
@@ -342,9 +366,10 @@ struct ast_json *stasis_app_playback_to_json(
return NULL;
}
- json = ast_json_pack("{s: s, s: s, s: s, s: s}",
+ json = ast_json_pack("{s: s, s: s, s: s, s: s, s: s}",
"id", playback->id,
"media_uri", playback->media,
+ "target_uri", playback->target,
"language", playback->language,
"state", state_to_string(playback->state));
diff --git a/res/stasis/control.c b/res/stasis/control.c
index df57a90a7..9d8abe0cc 100644
--- a/res/stasis/control.c
+++ b/res/stasis/control.c
@@ -65,6 +65,11 @@ struct stasis_app_control *control_create(struct ast_channel *channel)
control->command_queue = ao2_container_alloc_list(
AO2_ALLOC_OPT_LOCK_MUTEX, 0, NULL, NULL);
+ if (!control->command_queue) {
+ ao2_cleanup(control);
+ return NULL;
+ }
+
control->channel = channel;
return control;
diff --git a/res/stasis_http/ari_model_validators.c b/res/stasis_http/ari_model_validators.c
index bf3c0e7d0..bc5f25aea 100644
--- a/res/stasis_http/ari_model_validators.c
+++ b/res/stasis_http/ari_model_validators.c
@@ -578,16 +578,38 @@ int ari_validate_live_recording(struct ast_json *json)
{
int res = 1;
struct ast_json_iter *iter;
- int has_id = 0;
+ int has_format = 0;
+ int has_name = 0;
+ int has_state = 0;
for (iter = ast_json_object_iter(json); iter; iter = ast_json_object_iter_next(json, iter)) {
- if (strcmp("id", ast_json_object_iter_key(iter)) == 0) {
+ if (strcmp("format", ast_json_object_iter_key(iter)) == 0) {
int prop_is_valid;
- has_id = 1;
+ has_format = 1;
+ prop_is_valid = ari_validate_string(
+ ast_json_object_iter_value(iter));
+ if (!prop_is_valid) {
+ ast_log(LOG_ERROR, "ARI LiveRecording field format failed validation\n");
+ res = 0;
+ }
+ } else
+ if (strcmp("name", ast_json_object_iter_key(iter)) == 0) {
+ int prop_is_valid;
+ has_name = 1;
+ prop_is_valid = ari_validate_string(
+ ast_json_object_iter_value(iter));
+ if (!prop_is_valid) {
+ ast_log(LOG_ERROR, "ARI LiveRecording field name failed validation\n");
+ res = 0;
+ }
+ } else
+ if (strcmp("state", ast_json_object_iter_key(iter)) == 0) {
+ int prop_is_valid;
+ has_state = 1;
prop_is_valid = ari_validate_string(
ast_json_object_iter_value(iter));
if (!prop_is_valid) {
- ast_log(LOG_ERROR, "ARI LiveRecording field id failed validation\n");
+ ast_log(LOG_ERROR, "ARI LiveRecording field state failed validation\n");
res = 0;
}
} else
@@ -599,8 +621,18 @@ int ari_validate_live_recording(struct ast_json *json)
}
}
- if (!has_id) {
- ast_log(LOG_ERROR, "ARI LiveRecording missing required field id\n");
+ if (!has_format) {
+ ast_log(LOG_ERROR, "ARI LiveRecording missing required field format\n");
+ res = 0;
+ }
+
+ if (!has_name) {
+ ast_log(LOG_ERROR, "ARI LiveRecording missing required field name\n");
+ res = 0;
+ }
+
+ if (!has_state) {
+ ast_log(LOG_ERROR, "ARI LiveRecording missing required field state\n");
res = 0;
}
diff --git a/res/stasis_http/ari_model_validators.h b/res/stasis_http/ari_model_validators.h
index 2f6418657..3cf630020 100644
--- a/res/stasis_http/ari_model_validators.h
+++ b/res/stasis_http/ari_model_validators.h
@@ -816,7 +816,9 @@ ari_validator ari_validate_stasis_start_fn(void);
* - id: string (required)
* - technology: string (required)
* LiveRecording
- * - id: string (required)
+ * - format: string (required)
+ * - name: string (required)
+ * - state: string (required)
* StoredRecording
* - duration_seconds: int
* - formats: List[string] (required)
diff --git a/res/stasis_http/resource_bridges.c b/res/stasis_http/resource_bridges.c
index 6dad91116..b378eb8a5 100644
--- a/res/stasis_http/resource_bridges.c
+++ b/res/stasis_http/resource_bridges.c
@@ -35,8 +35,14 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
#include "asterisk/stasis.h"
#include "asterisk/stasis_bridging.h"
#include "asterisk/stasis_app.h"
+#include "asterisk/stasis_app_playback.h"
+#include "asterisk/stasis_app_recording.h"
+#include "asterisk/stasis_channels.h"
+#include "asterisk/core_unreal.h"
#include "asterisk/channel.h"
#include "asterisk/bridging.h"
+#include "asterisk/format_cap.h"
+#include "asterisk/file.h"
/*!
* \brief Finds a bridge, filling the response with an error, if appropriate.
@@ -144,9 +150,275 @@ void stasis_http_remove_channel_from_bridge(struct ast_variable *headers, struct
stasis_http_response_no_content(response);
}
+struct bridge_channel_control_thread_data {
+ struct ast_channel *bridge_channel;
+ struct stasis_app_control *control;
+};
+
+static void *bridge_channel_control_thread(void *data)
+{
+ struct bridge_channel_control_thread_data *thread_data = data;
+ struct ast_channel *bridge_channel = thread_data->bridge_channel;
+ struct stasis_app_control *control = thread_data->control;
+
+ RAII_VAR(struct ast_callid *, callid, ast_channel_callid(bridge_channel), ast_callid_cleanup);
+
+ if (callid) {
+ ast_callid_threadassoc_add(callid);
+ }
+
+ ast_free(thread_data);
+ thread_data = NULL;
+
+ stasis_app_control_execute_until_exhausted(bridge_channel, control);
+
+ ast_hangup(bridge_channel);
+ ao2_cleanup(control);
+ return NULL;
+}
+
+static struct ast_channel *prepare_bridge_media_channel(const char *type)
+{
+ RAII_VAR(struct ast_format_cap *, cap, NULL, ast_format_cap_destroy);
+ struct ast_format format;
+
+ cap = ast_format_cap_alloc_nolock();
+ if (!cap) {
+ return NULL;
+ }
+
+ ast_format_cap_add(cap, ast_format_set(&format, AST_FORMAT_SLINEAR, 0));
+
+ if (!cap) {
+ return NULL;
+ }
+
+ return ast_request(type, cap, NULL, "ARI", NULL);
+}
+
+void stasis_http_play_on_bridge(struct ast_variable *headers, struct ast_play_on_bridge_args *args, struct stasis_http_response *response)
+{
+ RAII_VAR(struct ast_bridge *, bridge, find_bridge(response, args->bridge_id), ao2_cleanup);
+ RAII_VAR(struct ast_channel *, play_channel, NULL, ast_hangup);
+ 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_playback *, playback, NULL, ao2_cleanup);
+ RAII_VAR(char *, playback_url, NULL, ast_free);
+ RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
+
+ struct bridge_channel_control_thread_data *thread_data;
+ const char *language;
+ pthread_t threadid;
+
+ ast_assert(response != NULL);
+
+ if (!bridge) {
+ return;
+ }
+
+ if (!(play_channel = prepare_bridge_media_channel("Announcer"))) {
+ stasis_http_response_error(
+ response, 500, "Internal Error", "Could not create playback channel");
+ return;
+ }
+ ast_debug(1, "Created announcer channel '%s'\n", ast_channel_name(play_channel));
+
+ if (ast_unreal_channel_push_to_bridge(play_channel, bridge)) {
+ stasis_http_response_error(
+ response, 500, "Internal Error", "Failed to put playback channel into the bridge");
+ return;
+ }
+
+ control = stasis_app_control_create(play_channel);
+ if (control == NULL) {
+ stasis_http_response_alloc_failed(response);
+ return;
+ }
+
+ snapshot = stasis_app_control_get_snapshot(control);
+ if (!snapshot) {
+ stasis_http_response_error(
+ response, 500, "Internal Error", "Failed to get control snapshot");
+ return;
+ }
+
+ language = S_OR(args->lang, snapshot->language);
+
+ playback = stasis_app_control_play_uri(control, args->media, language,
+ args->bridge_id, STASIS_PLAYBACK_TARGET_BRIDGE, args->skipms,
+ args->offsetms);
+
+ if (!playback) {
+ stasis_http_response_alloc_failed(response);
+ return;
+ }
+
+ ast_asprintf(&playback_url, "/playback/%s",
+ stasis_app_playback_get_id(playback));
+
+ if (!playback_url) {
+ stasis_http_response_alloc_failed(response);
+ return;
+ }
+
+ json = stasis_app_playback_to_json(playback);
+ if (!json) {
+ stasis_http_response_alloc_failed(response);
+ return;
+ }
+
+ /* Give play_channel and control reference to the thread data */
+ thread_data = ast_calloc(1, sizeof(*thread_data));
+ if (!thread_data) {
+ stasis_http_response_alloc_failed(response);
+ return;
+ }
+
+ thread_data->bridge_channel = play_channel;
+ thread_data->control = control;
+
+ if (ast_pthread_create_detached(&threadid, NULL, bridge_channel_control_thread, thread_data)) {
+ stasis_http_response_alloc_failed(response);
+ ast_free(thread_data);
+ return;
+ }
+
+ /* These are owned by the other thread now, so we don't want RAII_VAR disposing of them. */
+ play_channel = NULL;
+ control = NULL;
+
+ stasis_http_response_created(response, playback_url, json);
+}
+
void stasis_http_record_bridge(struct ast_variable *headers, struct ast_record_bridge_args *args, struct stasis_http_response *response)
{
- ast_log(LOG_ERROR, "TODO: stasis_http_record_bridge\n");
+ RAII_VAR(struct ast_bridge *, bridge, find_bridge(response, args->bridge_id), ao2_cleanup);
+ RAII_VAR(struct ast_channel *, record_channel, NULL, ast_hangup);
+ RAII_VAR(struct stasis_app_control *, control, 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;
+ struct bridge_channel_control_thread_data *thread_data;
+ pthread_t threadid;
+
+ ast_assert(response != NULL);
+
+ if (bridge == NULL) {
+ return;
+ }
+
+ if (!(record_channel = prepare_bridge_media_channel("Recorder"))) {
+ stasis_http_response_error(
+ response, 500, "Internal Server Error", "Failed to create recording channel");
+ return;
+ }
+
+ if (ast_unreal_channel_push_to_bridge(record_channel, bridge)) {
+ stasis_http_response_error(
+ response, 500, "Internal Error", "Failed to put recording channel into the bridge");
+ return;
+ }
+
+ control = stasis_app_control_create(record_channel);
+ if (control == NULL) {
+ stasis_http_response_alloc_failed(response);
+ return;
+ }
+
+ options = stasis_app_recording_options_create(args->name, args->format);
+ if (options == NULL) {
+ stasis_http_response_alloc_failed(response);
+ return;
+ }
+
+ 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;
+
+ 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_alloc_failed(response);
+ 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_alloc_failed(response);
+ 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_alloc_failed(response);
+ return;
+ }
+
+ json = stasis_app_recording_to_json(recording);
+ if (!json) {
+ stasis_http_response_alloc_failed(response);
+ return;
+ }
+
+ thread_data = ast_calloc(1, sizeof(*thread_data));
+ if (!thread_data) {
+ stasis_http_response_alloc_failed(response);
+ return;
+ }
+
+ thread_data->bridge_channel = record_channel;
+ thread_data->control = control;
+
+ if (ast_pthread_create_detached(&threadid, NULL, bridge_channel_control_thread, thread_data)) {
+ stasis_http_response_alloc_failed(response);
+ ast_free(thread_data);
+ return;
+ }
+
+ /* These are owned by the other thread now, so we don't want RAII_VAR disposing of them. */
+ record_channel = NULL;
+ control = NULL;
+
+ stasis_http_response_created(response, recording_url, json);
}
void stasis_http_get_bridge(struct ast_variable *headers, struct ast_get_bridge_args *args, struct stasis_http_response *response)
diff --git a/res/stasis_http/resource_bridges.h b/res/stasis_http/resource_bridges.h
index ec1992f26..3935a116c 100644
--- a/res/stasis_http/resource_bridges.h
+++ b/res/stasis_http/resource_bridges.h
@@ -123,18 +123,43 @@ struct ast_remove_channel_from_bridge_args {
* \param[out] response HTTP response
*/
void stasis_http_remove_channel_from_bridge(struct ast_variable *headers, struct ast_remove_channel_from_bridge_args *args, struct stasis_http_response *response);
+/*! \brief Argument struct for stasis_http_play_on_bridge() */
+struct ast_play_on_bridge_args {
+ /*! \brief Bridge's id */
+ const char *bridge_id;
+ /*! \brief Media's URI to play. */
+ const char *media;
+ /*! \brief For sounds, selects language for sound. */
+ const char *lang;
+ /*! \brief Number of media to skip before playing. */
+ int offsetms;
+ /*! \brief Number of milliseconds to skip for forward/reverse operations. */
+ int skipms;
+};
+/*!
+ * \brief Start playback of media on a bridge.
+ *
+ * The media URI may be any of a number of URI's. You may use http: and https: URI's, as well as sound: and recording: URI's. This operation creates a playback resource that can be used to control the playback of media (pause, rewind, fast forward, etc.)
+ *
+ * \param headers HTTP headers
+ * \param args Swagger parameters
+ * \param[out] response HTTP response
+ */
+void stasis_http_play_on_bridge(struct ast_variable *headers, struct ast_play_on_bridge_args *args, struct stasis_http_response *response);
/*! \brief Argument struct for stasis_http_record_bridge() */
struct ast_record_bridge_args {
/*! \brief Bridge's id */
const char *bridge_id;
/*! \brief Recording's filename */
const char *name;
+ /*! \brief Format to encode audio in */
+ const char *format;
/*! \brief Maximum duration of the recording, in seconds. 0 for no limit. */
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. */
diff --git a/res/stasis_http/resource_channels.c b/res/stasis_http/resource_channels.c
index f0bbd4b1f..c25917bb6 100644
--- a/res/stasis_http/resource_channels.c
+++ b/res/stasis_http/resource_channels.c
@@ -273,7 +273,7 @@ void stasis_http_play_on_channel(struct ast_variable *headers,
language = S_OR(args->lang, snapshot->language);
playback = stasis_app_control_play_uri(control, args->media, language,
- args->skipms, args->offsetms);
+ args->channel_id, STASIS_PLAYBACK_TARGET_CHANNEL, args->skipms, args->offsetms);
if (!playback) {
stasis_http_response_error(
response, 500, "Internal Server Error",
diff --git a/rest-api/api-docs/bridges.json b/rest-api/api-docs/bridges.json
index 87d5b3d4f..7b3c4a37b 100644
--- a/rest-api/api-docs/bridges.json
+++ b/rest-api/api-docs/bridges.json
@@ -169,6 +169,83 @@
]
},
{
+ "path": "/bridges/{bridgeId}/play",
+ "description": "Play media to the participants of a bridge",
+ "operations": [
+ {
+ "httpMethod": "POST",
+ "summary": "Start playback of media on a bridge.",
+ "notes": "The media URI may be any of a number of URI's. You may use http: and https: URI's, as well as sound: and recording: URI's. This operation creates a playback resource that can be used to control the playback of media (pause, rewind, fast forward, etc.)",
+ "nickname": "playOnBridge",
+ "responseClass": "Playback",
+ "parameters": [
+ {
+ "name": "bridgeId",
+ "description": "Bridge's id",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "media",
+ "description": "Media's URI to play.",
+ "paramType": "query",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "lang",
+ "description": "For sounds, selects language for sound.",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "offsetms",
+ "description": "Number of media to skip before playing.",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "int",
+ "defaultValue": 0,
+ "allowableValues": {
+ "valueType": "RANGE",
+ "min": 0
+ }
+
+ },
+ {
+ "name": "skipms",
+ "description": "Number of milliseconds to skip for forward/reverse operations.",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "int",
+ "defaultValue": 3000,
+ "allowableValues": {
+ "valueType": "RANGE",
+ "min": 0
+ }
+
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 404,
+ "reason": "Bridge not found"
+ },
+ {
+ "code": 409,
+ "reason": "Bridge not in a Stasis application"
+ }
+ ]
+ }
+ ]
+ },
+ {
"path": "/bridges/{bridgeId}/record",
"description": "Record audio on a bridge",
"operations": [
@@ -196,13 +273,25 @@
"dataType": "string"
},
{
+ "name": "format",
+ "description": "Format to encode audio in",
+ "paramType": "query",
+ "required": true,
+ "allowMultiple": true,
+ "dataType": "string"
+ },
+ {
"name": "maxDurationSeconds",
"description": "Maximum duration of the recording, in seconds. 0 for no limit.",
"paramType": "query",
"required": false,
"allowMultiple": false,
"dataType": "int",
- "defaultValue": 0
+ "defaultValue": 0,
+ "allowableValues": {
+ "valueType": "RANGE",
+ "min": 0
+ }
},
{
"name": "maxSilenceSeconds",
@@ -211,16 +300,28 @@
"required": false,
"allowMultiple": false,
"dataType": "int",
- "defaultValue": 0
+ "defaultValue": 0,
+ "allowableValues": {
+ "valueType": "RANGE",
+ "min": 0
+ }
},
{
- "name": "append",
- "description": "If true, and recording already exists, append to recording.",
+ "name": "ifExists",
+ "description": "Action to take if a recording with the same name already exists.",
"paramType": "query",
"required": false,
"allowMultiple": false,
- "dataType": "boolean",
- "defaultValue": false
+ "dataType": "string",
+ "defaultValue": "fail",
+ "allowableValues": {
+ "valueType": "LIST",
+ "values": [
+ "fail",
+ "overwrite",
+ "append"
+ ]
+ }
},
{
"name": "beep",
diff --git a/rest-api/api-docs/channels.json b/rest-api/api-docs/channels.json
index 07c9750a7..69e905875 100644
--- a/rest-api/api-docs/channels.json
+++ b/rest-api/api-docs/channels.json
@@ -538,7 +538,7 @@
"summary": "Start a recording.",
"notes": "Record audio from a channel. Note that this will not capture audio sent to the channel. The bridge itself has a record feature if that's what you want.",
"nickname": "recordChannel",
- "responseClass": "void",
+ "responseClass": "LiveRecording",
"parameters": [
{
"name": "channelId",
diff --git a/rest-api/api-docs/playback.json b/rest-api/api-docs/playback.json
index 884c0db26..734abdbff 100644
--- a/rest-api/api-docs/playback.json
+++ b/rest-api/api-docs/playback.json
@@ -53,7 +53,7 @@
"httpMethod": "POST",
"summary": "Get a playback's details.",
"nickname": "controlPlayback",
- "responseClass": "Playback",
+ "responseClass": "void",
"parameters": [
{
"name": "playbackId",
diff --git a/rest-api/api-docs/recordings.json b/rest-api/api-docs/recordings.json
index 9efdc7bb3..b564edee0 100644
--- a/rest-api/api-docs/recordings.json
+++ b/rest-api/api-docs/recordings.json
@@ -243,7 +243,15 @@
"id": "LiveRecording",
"description": "A recording that is in progress",
"properties": {
- "id": {
+ "name": {
+ "required": true,
+ "type": "string"
+ },
+ "state": {
+ "required": true,
+ "type": "string"
+ },
+ "format": {
"required": true,
"type": "string"
}