From 17c546173fe1f24749af4643f19b40be180803de Mon Sep 17 00:00:00 2001 From: Jonathan Rose Date: Fri, 19 Jul 2013 19:35:21 +0000 Subject: 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 --- channels/chan_bridge_media.c | 218 ++++++++++++++++++++++++++ include/asterisk/core_unreal.h | 15 ++ include/asterisk/logger.h | 11 +- include/asterisk/stasis_app.h | 23 +++ include/asterisk/stasis_app_playback.h | 13 +- main/core_unreal.c | 90 +++++++++++ res/res_stasis.c | 17 +- res/res_stasis_http_bridges.c | 87 ++++++++++- res/res_stasis_http_channels.c | 2 +- res/res_stasis_http_playback.c | 2 +- res/res_stasis_playback.c | 29 +++- res/stasis/control.c | 5 + res/stasis_http/ari_model_validators.c | 44 +++++- res/stasis_http/ari_model_validators.h | 4 +- res/stasis_http/resource_bridges.c | 274 ++++++++++++++++++++++++++++++++- res/stasis_http/resource_bridges.h | 29 +++- res/stasis_http/resource_channels.c | 2 +- rest-api/api-docs/bridges.json | 113 +++++++++++++- rest-api/api-docs/channels.json | 2 +- rest-api/api-docs/playback.json | 2 +- rest-api/api-docs/recordings.json | 10 +- 21 files changed, 961 insertions(+), 31 deletions(-) create mode 100644 channels/chan_bridge_media.c 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 + * + * 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 + * \author Richard Mudgett + * + * \brief Bridge Media Channels + * + * \ingroup channel_drivers + */ + +/*** MODULEINFO + core + ***/ + +#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 @@ -126,6 +126,29 @@ struct stasis_app_control *stasis_app_control_find_by_channel( 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 * 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 @@ -357,6 +357,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. @@ -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 @@ -168,6 +168,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", @@ -195,6 +272,14 @@ "allowMultiple": false, "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.", @@ -202,7 +287,11 @@ "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" } -- cgit v1.2.3