diff options
-rw-r--r-- | CHANGES | 3 | ||||
-rw-r--r-- | include/asterisk/stasis_app.h | 33 | ||||
-rw-r--r-- | res/ari/resource_bridges.c | 255 | ||||
-rw-r--r-- | res/ari/resource_bridges.h | 38 | ||||
-rw-r--r-- | res/ari/resource_channels.c | 11 | ||||
-rw-r--r-- | res/res_ari_bridges.c | 143 | ||||
-rw-r--r-- | res/res_stasis.c | 146 | ||||
-rw-r--r-- | res/res_stasis_playback.c | 2 | ||||
-rw-r--r-- | res/stasis/control.c | 10 | ||||
-rw-r--r-- | res/stasis/control.h | 11 | ||||
-rw-r--r-- | rest-api/api-docs/bridges.json | 93 |
11 files changed, 677 insertions, 68 deletions
@@ -150,7 +150,8 @@ ARI (POST bridges/my-bridge-id) or as a query parameter. * A playbackId can be provided when starting a playback, either in the uri - (POST channels/my-channel-id/play/my-playback-id) or as a query parameter. + (POST channels/my-channel-id/play/my-playback-id / + POST bridges/my-bridge-id/play/my-playback-id) or as a query parameter. * A snoop channel can be started with a snoopId, in the uri or query. diff --git a/include/asterisk/stasis_app.h b/include/asterisk/stasis_app.h index 8430cf330..02c67fdca 100644 --- a/include/asterisk/stasis_app.h +++ b/include/asterisk/stasis_app.h @@ -375,6 +375,15 @@ void stasis_app_control_execute_until_exhausted( struct stasis_app_control *control); /*! + * \brief Check if a control is marked as done + * \since 12.2.0 + * + * \param control Which control object is being evaluated + */ +int stasis_app_control_is_done( + struct stasis_app_control *control); + +/*! * \brief Returns the uniqueid of the channel associated with this control * * \param control Control object. @@ -638,6 +647,30 @@ int stasis_app_bridge_moh_stop( struct ast_bridge *bridge); /*! + * \brief Finds an existing ARI playback channel in a bridge + * + * \param bridge Bridge we want to find the playback channel for + * + * \return NULL if the playback channel can not be found for any reason. + * \return Pointer to the ;1 end of the playback channel chain. + */ +struct ast_channel *stasis_app_bridge_playback_channel_find( + struct ast_bridge *bridge); + +/*! + * \brief Adds a channel to the list of ARI playback channels for bridges. + * + * \param bridge Bridge we are adding the playback channel for + * \param chan Channel being added as a playback channel (must be ;1) + * + * \retval -1 failed to add channel for any reason + * \retval 0 on success + */ +int stasis_app_bridge_playback_channel_add(struct ast_bridge *bridge, + struct ast_channel *chan, + struct stasis_app_control *control); + +/*! * \brief Result codes used when adding/removing channels to/from bridges. */ enum stasis_app_control_channel_result { diff --git a/res/ari/resource_bridges.c b/res/ari/resource_bridges.c index d78601fae..eeb4237d0 100644 --- a/res/ari/resource_bridges.c +++ b/res/ari/resource_bridges.c @@ -320,31 +320,93 @@ static struct ast_channel *prepare_bridge_media_channel(const char *type) return ast_request(type, cap, NULL, NULL, "ARI", NULL); } -void ast_ari_bridges_play(struct ast_variable *headers, - struct ast_ari_bridges_play_args *args, - struct ast_ari_response *response) +/*! + * \brief Performs common setup for a bridge playback operation + * with both new controls and when existing controls are found. + * + * \param args_media media string split from arguments + * \param args_lang language string split from arguments + * \param args_offset_ms milliseconds offset split from arguments + * \param args_playback_id string to use for playback split from + * arguments (null valid) + * \param response ARI response being built + * \param bridge Bridge the playback is being peformed on + * \param control Control being used for the playback channel + * \param json contents of the response to ARI + * \param playback_url stores playback URL for use with response + * + * \retval -1 operation failed + * \retval operation was successful + */ +static int ari_bridges_play_helper(const char *args_media, + const char *args_lang, + int args_offset_ms, + int args_skipms, + const char *args_playback_id, + struct ast_ari_response *response, + struct ast_bridge *bridge, + struct stasis_app_control *control, + struct ast_json **json, + char **playback_url) { - 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); + + const char *language; + + snapshot = stasis_app_control_get_snapshot(control); + if (!snapshot) { + ast_ari_response_error( + response, 500, "Internal Error", "Failed to get control snapshot"); + return -1; + } + + language = S_OR(args_lang, snapshot->language); + + playback = stasis_app_control_play_uri(control, args_media, language, + bridge->uniqueid, STASIS_PLAYBACK_TARGET_BRIDGE, args_skipms, + args_offset_ms, args_playback_id); + + if (!playback) { + ast_ari_response_alloc_failed(response); + return -1; + } + + if (ast_asprintf(playback_url, "/playback/%s", + stasis_app_playback_get_id(playback)) == -1) { + playback_url = NULL; + ast_ari_response_alloc_failed(response); + return -1; + } + + *json = stasis_app_playback_to_json(playback); + if (!*json) { + ast_ari_response_alloc_failed(response); + return -1; + } + + return 0; +} + +static void ari_bridges_play_new(const char *args_media, + const char *args_lang, + int args_offset_ms, + int args_skipms, + const char *args_playback_id, + struct ast_ari_response *response, + struct ast_bridge *bridge) +{ + RAII_VAR(struct ast_channel *, play_channel, NULL, ast_hangup); + RAII_VAR(struct stasis_app_control *, control, NULL, ao2_cleanup); RAII_VAR(struct ast_json *, json, NULL, ast_json_unref); RAII_VAR(struct stasis_forward *, channel_forward, NULL, stasis_forward_cancel); + RAII_VAR(char *, playback_url, NULL, ast_free); struct stasis_topic *channel_topic; struct stasis_topic *bridge_topic; 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"))) { ast_ari_response_error( response, 500, "Internal Error", "Could not create playback channel"); @@ -378,34 +440,16 @@ void ast_ari_bridges_play(struct ast_variable *headers, return; } - snapshot = stasis_app_control_get_snapshot(control); - if (!snapshot) { - ast_ari_response_error( - response, 500, "Internal Error", "Failed to get control snapshot"); + ao2_lock(control); + if (ari_bridges_play_helper(args_media, args_lang, args_offset_ms, + args_skipms, args_playback_id, response, bridge, control, + &json, &playback_url)) { + ao2_unlock(control); return; } + ao2_unlock(control); - 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, NULL); - - if (!playback) { - ast_ari_response_alloc_failed(response); - return; - } - - ast_asprintf(&playback_url, "/playback/%s", - stasis_app_playback_get_id(playback)); - - if (!playback_url) { - ast_ari_response_alloc_failed(response); - return; - } - - json = stasis_app_playback_to_json(playback); - if (!json) { + if (stasis_app_bridge_playback_channel_add(bridge, play_channel, control)) { ast_ari_response_alloc_failed(response); return; } @@ -435,6 +479,134 @@ void ast_ari_bridges_play(struct ast_variable *headers, ast_ari_response_created(response, playback_url, ast_json_ref(json)); } +enum play_found_result { + PLAY_FOUND_SUCCESS, + PLAY_FOUND_FAILURE, + PLAY_FOUND_CHANNEL_UNAVAILABLE, +}; + +/*! + * \brief Performs common setup for a bridge playback operation + * with both new controls and when existing controls are found. + * + * \param args_media media string split from arguments + * \param args_lang language string split from arguments + * \param args_offset_ms milliseconds offset split from arguments + * \param args_playback_id string to use for playback split from + * arguments (null valid) + * \param response ARI response being built + * \param bridge Bridge the playback is being peformed on + * \param found_channel The channel that was found controlling playback + * + * \retval PLAY_FOUND_SUCCESS The operation was successful + * \retval PLAY_FOUND_FAILURE The operation failed (terminal failure) + * \retval PLAY_FOUND_CHANNEL_UNAVAILABLE The operation failed because + * the channel requested to playback with is breaking down. + */ +static enum play_found_result ari_bridges_play_found(const char *args_media, + const char *args_lang, + int args_offset_ms, + int args_skipms, + const char *args_playback_id, + struct ast_ari_response *response, + struct ast_bridge *bridge, + struct ast_channel *found_channel) +{ + RAII_VAR(struct ast_channel *, play_channel, found_channel, ao2_cleanup); + RAII_VAR(struct stasis_app_control *, control, NULL, ao2_cleanup); + RAII_VAR(char *, playback_url, NULL, ast_free); + RAII_VAR(struct ast_json *, json, NULL, ast_json_unref); + + control = stasis_app_control_find_by_channel(play_channel); + if (!control) { + ast_ari_response_error( + response, 500, "Internal Error", "Failed to get control snapshot"); + return PLAY_FOUND_FAILURE; + } + + ao2_lock(control); + if (stasis_app_control_is_done(control)) { + /* We failed to queue the action. Bailout and return that we aren't terminal. */ + ao2_unlock(control); + return PLAY_FOUND_CHANNEL_UNAVAILABLE; + } + + if (ari_bridges_play_helper(args_media, args_lang, args_offset_ms, + args_skipms, args_playback_id, response, bridge, control, + &json, &playback_url)) { + ao2_unlock(control); + return PLAY_FOUND_FAILURE; + } + ao2_unlock(control); + + ast_ari_response_created(response, playback_url, ast_json_ref(json)); + return PLAY_FOUND_SUCCESS; +} + +static void ari_bridges_handle_play( + const char *args_bridge_id, + const char *args_media, + const char *args_lang, + int args_offset_ms, + int args_skipms, + const char *args_playback_id, + struct ast_ari_response *response) +{ + RAII_VAR(struct ast_bridge *, bridge, find_bridge(response, args_bridge_id), ao2_cleanup); + struct ast_channel *play_channel; + + ast_assert(response != NULL); + + if (!bridge) { + return; + } + + while ((play_channel = stasis_app_bridge_playback_channel_find(bridge))) { + /* If ari_bridges_play_found fails because the channel is unavailable for + * playback, The channel will be removed from the playback list soon. We + * can keep trying to get channels from the list until we either get one + * that will work or else there isn't a channel for this bridge anymore, + * in which case we'll revert to ari_bridges_play_new. + */ + if (ari_bridges_play_found(args_media, args_lang, args_offset_ms, + args_skipms, args_playback_id, response,bridge, + play_channel) == PLAY_FOUND_CHANNEL_UNAVAILABLE) { + continue; + } + return; + } + + ari_bridges_play_new(args_media, args_lang, args_offset_ms, + args_skipms, args_playback_id, response, bridge); +} + + +void ast_ari_bridges_play(struct ast_variable *headers, + struct ast_ari_bridges_play_args *args, + struct ast_ari_response *response) +{ + ari_bridges_handle_play(args->bridge_id, + args->media, + args->lang, + args->offsetms, + args->skipms, + args->playback_id, + response); +} + +void ast_ari_bridges_play_with_id(struct ast_variable *headers, + struct ast_ari_bridges_play_with_id_args *args, + struct ast_ari_response *response) +{ + ari_bridges_handle_play(args->bridge_id, + args->media, + args->lang, + args->offsetms, + args->skipms, + args->playback_id, + response); +} + void ast_ari_bridges_record(struct ast_variable *headers, struct ast_ari_bridges_record_args *args, struct ast_ari_response *response) @@ -573,8 +745,9 @@ void ast_ari_bridges_record(struct ast_variable *headers, } 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) { + if (ast_asprintf(&recording_url, "/recordings/live/%s", + uri_encoded_name) == -1) { + recording_url = NULL; ast_ari_response_alloc_failed(response); return; } diff --git a/res/ari/resource_bridges.h b/res/ari/resource_bridges.h index 404760c91..f8cd6139a 100644 --- a/res/ari/resource_bridges.h +++ b/res/ari/resource_bridges.h @@ -253,6 +253,8 @@ struct ast_ari_bridges_play_args { int offsetms; /*! \brief Number of milliseconds to skip for forward/reverse operations. */ int skipms; + /*! \brief Playback Id. */ + const char *playback_id; }; /*! * \brief Body parsing function for /bridges/{bridgeId}/play. @@ -275,6 +277,42 @@ int ast_ari_bridges_play_parse_body( * \param[out] response HTTP response */ void ast_ari_bridges_play(struct ast_variable *headers, struct ast_ari_bridges_play_args *args, struct ast_ari_response *response); +/*! \brief Argument struct for ast_ari_bridges_play_with_id() */ +struct ast_ari_bridges_play_with_id_args { + /*! \brief Bridge's id */ + const char *bridge_id; + /*! \brief Playback ID. */ + const char *playback_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 Body parsing function for /bridges/{bridgeId}/play/{playbackId}. + * \param body The JSON body from which to parse parameters. + * \param[out] args The args structure to parse into. + * \retval zero on success + * \retval non-zero on failure + */ +int ast_ari_bridges_play_with_id_parse_body( + struct ast_json *body, + struct ast_ari_bridges_play_with_id_args *args); + +/*! + * \brief Start playback of media on a bridge. + * + * The media URI may be any of a number of URI's. Currently sound: and recording: URI's are supported. 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 ast_ari_bridges_play_with_id(struct ast_variable *headers, struct ast_ari_bridges_play_with_id_args *args, struct ast_ari_response *response); /*! \brief Argument struct for ast_ari_bridges_record() */ struct ast_ari_bridges_record_args { /*! \brief Bridge's id */ diff --git a/res/ari/resource_channels.c b/res/ari/resource_channels.c index 9d127b2ea..30ced1b2f 100644 --- a/res/ari/resource_channels.c +++ b/res/ari/resource_channels.c @@ -411,9 +411,9 @@ static void ari_channels_handle_play( return; } - ast_asprintf(&playback_url, "/playback/%s", - stasis_app_playback_get_id(playback)); - if (!playback_url) { + if (ast_asprintf(&playback_url, "/playback/%s", + stasis_app_playback_get_id(playback)) == -1) { + playback_url = NULL; ast_ari_response_error( response, 500, "Internal Server Error", "Out of memory"); @@ -579,8 +579,9 @@ void ast_ari_channels_record(struct ast_variable *headers, 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) { + if (ast_asprintf(&recording_url, "/recordings/live/%s", + uri_encoded_name) == -1) { + recording_url = NULL; ast_ari_response_error( response, 500, "Internal Server Error", "Out of memory"); diff --git a/res/res_ari_bridges.c b/res/res_ari_bridges.c index 890a2ace5..1dbd9d32b 100644 --- a/res/res_ari_bridges.c +++ b/res/res_ari_bridges.c @@ -948,6 +948,10 @@ int ast_ari_bridges_play_parse_body( if (field) { args->skipms = ast_json_integer_get(field); } + field = ast_json_object_get(body, "playbackId"); + if (field) { + args->playback_id = ast_json_string_get(field); + } return 0; } @@ -984,6 +988,9 @@ static void ast_ari_bridges_play_cb( if (strcmp(i->name, "skipms") == 0) { args.skipms = atoi(i->value); } else + if (strcmp(i->name, "playbackId") == 0) { + args.playback_id = (i->value); + } else {} } for (i = path_vars; i; i = i->next) { @@ -1045,6 +1052,128 @@ static void ast_ari_bridges_play_cb( fin: __attribute__((unused)) return; } +int ast_ari_bridges_play_with_id_parse_body( + struct ast_json *body, + struct ast_ari_bridges_play_with_id_args *args) +{ + struct ast_json *field; + /* Parse query parameters out of it */ + field = ast_json_object_get(body, "media"); + if (field) { + args->media = ast_json_string_get(field); + } + field = ast_json_object_get(body, "lang"); + if (field) { + args->lang = ast_json_string_get(field); + } + field = ast_json_object_get(body, "offsetms"); + if (field) { + args->offsetms = ast_json_integer_get(field); + } + field = ast_json_object_get(body, "skipms"); + if (field) { + args->skipms = ast_json_integer_get(field); + } + return 0; +} + +/*! + * \brief Parameter parsing callback for /bridges/{bridgeId}/play/{playbackId}. + * \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 ast_ari_bridges_play_with_id_cb( + struct ast_tcptls_session_instance *ser, + struct ast_variable *get_params, struct ast_variable *path_vars, + struct ast_variable *headers, struct ast_ari_response *response) +{ + struct ast_ari_bridges_play_with_id_args args = {}; + struct ast_variable *i; + RAII_VAR(struct ast_json *, body, NULL, ast_json_unref); +#if defined(AST_DEVMODE) + int is_valid; + int code; +#endif /* AST_DEVMODE */ + + 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 + if (strcmp(i->name, "playbackId") == 0) { + args.playback_id = (i->value); + } else + {} + } + /* Look for a JSON request entity */ + body = ast_http_get_json(ser, headers); + if (!body) { + switch (errno) { + case EFBIG: + ast_ari_response_error(response, 413, "Request Entity Too Large", "Request body too large"); + goto fin; + case ENOMEM: + ast_ari_response_error(response, 500, "Internal Server Error", "Error processing request"); + goto fin; + case EIO: + ast_ari_response_error(response, 400, "Bad Request", "Error parsing request body"); + goto fin; + } + } + if (ast_ari_bridges_play_with_id_parse_body(body, &args)) { + ast_ari_response_alloc_failed(response); + goto fin; + } + ast_ari_bridges_play_with_id(headers, &args, response); +#if defined(AST_DEVMODE) + code = response->response_code; + + switch (code) { + case 0: /* Implementation is still a stub, or the code wasn't set */ + is_valid = response->message == NULL; + break; + case 500: /* Internal Server Error */ + case 501: /* Not Implemented */ + 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 = ast_ari_validate_playback( + response->message); + } else { + ast_log(LOG_ERROR, "Invalid error response %d for /bridges/{bridgeId}/play/{playbackId}\n", code); + is_valid = 0; + } + } + + if (!is_valid) { + ast_log(LOG_ERROR, "Response validation failed for /bridges/{bridgeId}/play/{playbackId}\n"); + ast_ari_response_error(response, 500, + "Internal Server Error", "Response validation failed"); + } +#endif /* AST_DEVMODE */ + +fin: __attribute__((unused)) + return; +} int ast_ari_bridges_record_parse_body( struct ast_json *body, struct ast_ari_bridges_record_args *args) @@ -1217,13 +1346,23 @@ static struct stasis_rest_handlers bridges_bridgeId_moh = { .children = { } }; /*! \brief REST handler for /api-docs/bridges.{format} */ +static struct stasis_rest_handlers bridges_bridgeId_play_playbackId = { + .path_segment = "playbackId", + .is_wildcard = 1, + .callbacks = { + [AST_HTTP_POST] = ast_ari_bridges_play_with_id_cb, + }, + .num_children = 0, + .children = { } +}; +/*! \brief REST handler for /api-docs/bridges.{format} */ static struct stasis_rest_handlers bridges_bridgeId_play = { .path_segment = "play", .callbacks = { [AST_HTTP_POST] = ast_ari_bridges_play_cb, }, - .num_children = 0, - .children = { } + .num_children = 1, + .children = { &bridges_bridgeId_play_playbackId, } }; /*! \brief REST handler for /api-docs/bridges.{format} */ static struct stasis_rest_handlers bridges_bridgeId_record = { diff --git a/res/res_stasis.c b/res/res_stasis.c index 32ac7b686..d9542cd21 100644 --- a/res/res_stasis.c +++ b/res/res_stasis.c @@ -103,6 +103,8 @@ struct ao2_container *app_bridges; struct ao2_container *app_bridges_moh; +struct ao2_container *app_bridges_playback; + const char *stasis_app_name(const struct stasis_app *app) { return app_name(app); @@ -341,26 +343,26 @@ static int bridges_compare(void *obj, void *arg, int flags) } /*! - * Used with app_bridges_moh, provides links between bridges and existing music - * on hold channels that are being used with them. + * Used with app_bridges_moh and app_bridge_control, they provide links + * between bridges and channels used for ARI application purposes */ -struct stasis_app_bridge_moh_wrapper { +struct stasis_app_bridge_channel_wrapper { AST_DECLARE_STRING_FIELDS( AST_STRING_FIELD(channel_id); AST_STRING_FIELD(bridge_id); ); }; -static void stasis_app_bridge_moh_wrapper_destructor(void *obj) +static void stasis_app_bridge_channel_wrapper_destructor(void *obj) { - struct stasis_app_bridge_moh_wrapper *wrapper = obj; + struct stasis_app_bridge_channel_wrapper *wrapper = obj; ast_string_field_free_memory(wrapper); } /*! AO2 hash function for the bridges moh container */ -static int bridges_moh_hash_fn(const void *obj, const int flags) +static int bridges_channel_hash_fn(const void *obj, const int flags) { - const struct stasis_app_bridge_moh_wrapper *wrapper; + const struct stasis_app_bridge_channel_wrapper *wrapper; const char *key; switch (flags & OBJ_SEARCH_MASK) { @@ -379,10 +381,10 @@ static int bridges_moh_hash_fn(const void *obj, const int flags) return ast_str_hash(key); } -static int bridges_moh_sort_fn(const void *obj_left, const void *obj_right, const int flags) +static int bridges_channel_sort_fn(const void *obj_left, const void *obj_right, const int flags) { - const struct stasis_app_bridge_moh_wrapper *left = obj_left; - const struct stasis_app_bridge_moh_wrapper *right = obj_right; + const struct stasis_app_bridge_channel_wrapper *left = obj_left; + const struct stasis_app_bridge_channel_wrapper *right = obj_right; const char *right_key = obj_right; int cmp; @@ -469,7 +471,7 @@ static void *moh_channel_thread(void *data) */ static struct ast_channel *bridge_moh_create(struct ast_bridge *bridge) { - RAII_VAR(struct stasis_app_bridge_moh_wrapper *, new_wrapper, NULL, ao2_cleanup); + RAII_VAR(struct stasis_app_bridge_channel_wrapper *, new_wrapper, NULL, ao2_cleanup); RAII_VAR(char *, bridge_id, ast_strdup(bridge->uniqueid), ast_free); struct ast_channel *chan; pthread_t threadid; @@ -498,7 +500,7 @@ static struct ast_channel *bridge_moh_create(struct ast_bridge *bridge) } new_wrapper = ao2_alloc_options(sizeof(*new_wrapper), - stasis_app_bridge_moh_wrapper_destructor, AO2_ALLOC_OPT_LOCK_NOLOCK); + stasis_app_bridge_channel_wrapper_destructor, AO2_ALLOC_OPT_LOCK_NOLOCK); if (!new_wrapper) { ast_hangup(chan); return NULL; @@ -528,7 +530,7 @@ static struct ast_channel *bridge_moh_create(struct ast_bridge *bridge) struct ast_channel *stasis_app_bridge_moh_channel(struct ast_bridge *bridge) { - RAII_VAR(struct stasis_app_bridge_moh_wrapper *, moh_wrapper, NULL, ao2_cleanup); + RAII_VAR(struct stasis_app_bridge_channel_wrapper *, moh_wrapper, NULL, ao2_cleanup); { SCOPED_AO2LOCK(lock, app_bridges_moh); @@ -544,7 +546,7 @@ struct ast_channel *stasis_app_bridge_moh_channel(struct ast_bridge *bridge) int stasis_app_bridge_moh_stop(struct ast_bridge *bridge) { - RAII_VAR(struct stasis_app_bridge_moh_wrapper *, moh_wrapper, NULL, ao2_cleanup); + RAII_VAR(struct stasis_app_bridge_channel_wrapper *, moh_wrapper, NULL, ao2_cleanup); struct ast_channel *chan; moh_wrapper = ao2_find(app_bridges_moh, bridge->uniqueid, OBJ_SEARCH_KEY | OBJ_UNLINK); @@ -564,6 +566,92 @@ int stasis_app_bridge_moh_stop(struct ast_bridge *bridge) return 0; } +/*! Removes the bridge to playback channel link */ +static void remove_bridge_playback(char *bridge_id) +{ + struct stasis_app_bridge_channel_wrapper *wrapper; + struct stasis_app_control *control; + + wrapper = ao2_find(app_bridges_playback, bridge_id, OBJ_SEARCH_KEY | OBJ_UNLINK); + + if (wrapper) { + control = stasis_app_control_find_by_channel_id(wrapper->channel_id); + if (control) { + ao2_unlink(app_controls, control); + ao2_ref(control, -1); + } + ao2_ref(wrapper, -1); + } + ast_free(bridge_id); +} + +static void playback_after_bridge_cb_failed(enum ast_bridge_after_cb_reason reason, void *data) +{ + char *bridge_id = data; + + remove_bridge_playback(bridge_id); +} + +static void playback_after_bridge_cb(struct ast_channel *chan, void *data) +{ + char *bridge_id = data; + + remove_bridge_playback(bridge_id); +} + +int stasis_app_bridge_playback_channel_add(struct ast_bridge *bridge, + struct ast_channel *chan, + struct stasis_app_control *control) +{ + RAII_VAR(struct stasis_app_bridge_channel_wrapper *, new_wrapper, NULL, ao2_cleanup); + char *bridge_id = ast_strdup(bridge->uniqueid); + + if (!bridge_id) { + return -1; + } + + if (ast_bridge_set_after_callback(chan, + playback_after_bridge_cb, playback_after_bridge_cb_failed, bridge_id)) { + ast_free(bridge_id); + return -1; + } + + new_wrapper = ao2_alloc_options(sizeof(*new_wrapper), + stasis_app_bridge_channel_wrapper_destructor, AO2_ALLOC_OPT_LOCK_NOLOCK); + if (!new_wrapper) { + return -1; + } + + if (ast_string_field_init(new_wrapper, 32)) { + return -1; + } + + ast_string_field_set(new_wrapper, bridge_id, bridge->uniqueid); + ast_string_field_set(new_wrapper, channel_id, ast_channel_uniqueid(chan)); + + if (!ao2_link(app_bridges_playback, new_wrapper)) { + return -1; + } + + ao2_link(app_controls, control); + return 0; +} + +struct ast_channel *stasis_app_bridge_playback_channel_find(struct ast_bridge *bridge) +{ + struct stasis_app_bridge_channel_wrapper *playback_wrapper; + struct ast_channel *chan; + + playback_wrapper = ao2_find(app_bridges_playback, bridge->uniqueid, OBJ_SEARCH_KEY); + if (!playback_wrapper) { + return NULL; + } + + chan = ast_channel_get_by_name(playback_wrapper->channel_id); + ao2_ref(playback_wrapper, -1); + return chan; +} + struct ast_bridge *stasis_app_bridge_find_by_id( const char *bridge_id) { @@ -720,13 +808,31 @@ static int send_end_msg(struct stasis_app *app, struct ast_channel *chan) 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); + int command_count; + command_count = control_dispatch_all(control, chan); + + ao2_lock(control); + + if (control_command_count(control)) { + /* If the command queue isn't empty, something added to the queue before it was locked. */ + ao2_unlock(control); + continue; + } + if (command_count == 0 || ast_channel_fdno(chan) == -1) { + control_mark_done(control); + ao2_unlock(control); break; } + ao2_unlock(control); } } +int stasis_app_control_is_done(struct stasis_app_control *control) +{ + return control_is_done(control); +} + /*! /brief Stasis dialplan application callback */ int stasis_app_exec(struct ast_channel *chan, const char *app_name, int argc, char *argv[]) @@ -1230,6 +1336,9 @@ static int unload_module(void) ao2_cleanup(app_bridges_moh); app_bridges_moh = NULL; + ao2_cleanup(app_bridges_playback); + app_bridges_playback = NULL; + return 0; } @@ -1268,8 +1377,11 @@ static int load_module(void) app_bridges = ao2_container_alloc(BRIDGES_NUM_BUCKETS, bridges_hash, bridges_compare); app_bridges_moh = ao2_container_alloc_hash( AO2_ALLOC_OPT_LOCK_MUTEX, AO2_CONTAINER_ALLOC_OPT_DUPS_REJECT, - 37, bridges_moh_hash_fn, bridges_moh_sort_fn, NULL); - if (!apps_registry || !app_controls || !app_bridges || !app_bridges_moh) { + 37, bridges_channel_hash_fn, bridges_channel_sort_fn, NULL); + app_bridges_playback = ao2_container_alloc_hash( + AO2_ALLOC_OPT_LOCK_MUTEX, AO2_CONTAINER_ALLOC_OPT_DUPS_REJECT, + 37, bridges_channel_hash_fn, bridges_channel_sort_fn, NULL); + if (!apps_registry || !app_controls || !app_bridges || !app_bridges_moh || !app_bridges_playback) { unload_module(); return AST_MODULE_LOAD_FAILURE; } diff --git a/res/res_stasis_playback.c b/res/res_stasis_playback.c index 960f2e334..b434dca63 100644 --- a/res/res_stasis_playback.c +++ b/res/res_stasis_playback.c @@ -330,7 +330,7 @@ static void play_on_channel(struct stasis_app_playback *playback, res = ast_control_tone(chan, playback->media + strlen(TONE_URI_SCHEME)); } else { /* Play URL */ - ast_log(LOG_ERROR, "Attempted to play URI '%s' on channel '%s' but scheme is unsupported", + ast_log(LOG_ERROR, "Attempted to play URI '%s' on channel '%s' but scheme is unsupported\n", playback->media, ast_channel_name(chan)); return; } diff --git a/res/stasis/control.c b/res/stasis/control.c index d91a9f8d0..88ef8969d 100644 --- a/res/stasis/control.c +++ b/res/stasis/control.c @@ -368,12 +368,22 @@ void stasis_app_control_clear_roles(struct stasis_app_control *control) ast_channel_clear_bridge_roles(control->channel); } +int control_command_count(struct stasis_app_control *control) +{ + return ao2_container_count(control->command_queue); +} + int control_is_done(struct stasis_app_control *control) { /* Called from stasis_app_exec thread; no lock needed */ return control->is_done; } +void control_mark_done(struct stasis_app_control *control) +{ + control->is_done = 1; +} + struct stasis_app_control_continue_data { char context[AST_MAX_CONTEXT]; char extension[AST_MAX_EXTENSION]; diff --git a/res/stasis/control.h b/res/stasis/control.h index 6b602e770..0febd8438 100644 --- a/res/stasis/control.h +++ b/res/stasis/control.h @@ -58,6 +58,15 @@ int control_dispatch_all(struct stasis_app_control *control, void control_wait(struct stasis_app_control *control); /*! + * \brief Returns the count of items in a control's command queue. + * + * \param control Control to count commands on + * + * \retval number of commands in the command que + */ +int control_command_count(struct stasis_app_control *control); + +/*! * \brief Returns true if control_continue() has been called on this \a control. * * \param control Control to query. @@ -66,5 +75,7 @@ void control_wait(struct stasis_app_control *control); */ int control_is_done(struct stasis_app_control *control); +void control_mark_done(struct stasis_app_control *control); + #endif /* _ASTERISK_RES_STASIS_CONTROL_H */ diff --git a/rest-api/api-docs/bridges.json b/rest-api/api-docs/bridges.json index 4d2c77cea..8368af30a 100644 --- a/rest-api/api-docs/bridges.json +++ b/rest-api/api-docs/bridges.json @@ -368,7 +368,14 @@ "valueType": "RANGE", "min": 0 } - + }, + { + "name": "playbackId", + "description": "Playback Id.", + "paramType": "query", + "required": false, + "allowMultiple": false, + "dataType": "string" } ], "errorResponses": [ @@ -385,6 +392,90 @@ ] }, { + "path": "/bridges/{bridgeId}/play/{playbackId}", + "description": "Play media to 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. Currently sound: and recording: URI's are supported. This operation creates a playback resource that can be used to control the playback of media (pause, rewind, fast forward, etc.)", + "nickname": "playWithId", + "responseClass": "Playback", + "parameters": [ + { + "name": "bridgeId", + "description": "Bridge's id", + "paramType": "path", + "required": true, + "allowMultiple": false, + "dataType": "string" + }, + { + "name": "playbackId", + "description": "Playback 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": [ |