diff options
-rw-r--r-- | CHANGES | 9 | ||||
-rw-r--r-- | include/asterisk/stasis_app_playback.h | 17 | ||||
-rw-r--r-- | res/ari/ari_model_validators.c | 94 | ||||
-rw-r--r-- | res/ari/ari_model_validators.h | 24 | ||||
-rw-r--r-- | res/ari/resource_bridges.c | 42 | ||||
-rw-r--r-- | res/ari/resource_bridges.h | 20 | ||||
-rw-r--r-- | res/ari/resource_channels.c | 7 | ||||
-rw-r--r-- | res/ari/resource_channels.h | 20 | ||||
-rw-r--r-- | res/res_ari_bridges.c | 142 | ||||
-rw-r--r-- | res/res_ari_channels.c | 142 | ||||
-rw-r--r-- | res/res_stasis_playback.c | 210 | ||||
-rw-r--r-- | rest-api/api-docs/bridges.json | 12 | ||||
-rw-r--r-- | rest-api/api-docs/channels.json | 12 | ||||
-rw-r--r-- | rest-api/api-docs/events.json | 12 | ||||
-rw-r--r-- | rest-api/api-docs/playbacks.json | 10 |
15 files changed, 645 insertions, 128 deletions
@@ -23,6 +23,15 @@ ARI * To complement the "create" method, a "dial" method has been added to the channels resource in order to place a call to a created channel. + * All operations that initiate playback of media on a resource now support + a list of media URIs. The list of URIs are played in the order they are + presented to the resource. A new event, "PlaybackContinuing", is raised when + a media URI finishes but before the next media URI starts. When a list is + played, the "Playback" model will contain the optional attribute + "next_media_uri", which specifies the next media URI in the list to be played + back to the resource. The "PlaybackFinished" event is raised when all media + URIs are done. + Applications ------------------ diff --git a/include/asterisk/stasis_app_playback.h b/include/asterisk/stasis_app_playback.h index b35299581..0038fd6d0 100644 --- a/include/asterisk/stasis_app_playback.h +++ b/include/asterisk/stasis_app_playback.h @@ -41,6 +41,8 @@ enum stasis_app_playback_state { STASIS_PLAYBACK_STATE_PLAYING, /*! The media is currently playing */ STASIS_PLAYBACK_STATE_PAUSED, + /*! The media is transitioning to the next in the list */ + STASIS_PLAYBACK_STATE_CONTINUING, /*! The media has stopped playing */ STASIS_PLAYBACK_STATE_COMPLETE, /*! The playback was canceled. */ @@ -84,7 +86,8 @@ enum stasis_app_playback_target_type { * available codecs for the channel. * * \param control Control for \c res_stasis. - * \param file Base filename for the file to play. + * \param media Array of const char * media files to play. + * \param media_count The number of media files in \c media. * \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 @@ -95,8 +98,8 @@ enum stasis_app_playback_target_type { * \return \c NULL on error. */ struct stasis_app_playback *stasis_app_control_play_uri( - struct stasis_app_control *control, const char *file, - const char *language, const char *target_id, + struct stasis_app_control *control, const char **media, + size_t media_count, const char *language, const char *target_id, enum stasis_app_playback_target_type target_type, int skipms, long offsetms, const char *id); @@ -128,6 +131,14 @@ const char *stasis_app_playback_get_id( */ struct stasis_app_playback *stasis_app_playback_find_by_id(const char *id); +/*! + * \brief Convert a playback to its JSON representation + * + * \param playback The playback object to convert to JSON + * + * \retval \c NULL on error + * \retval A JSON object on success + */ struct ast_json *stasis_app_playback_to_json( const struct stasis_app_playback *playback); diff --git a/res/ari/ari_model_validators.c b/res/ari/ari_model_validators.c index 623d5b541..8f05db035 100644 --- a/res/ari/ari_model_validators.c +++ b/res/ari/ari_model_validators.c @@ -1744,6 +1744,15 @@ int ast_ari_validate_playback(struct ast_json *json) res = 0; } } else + if (strcmp("next_media_uri", ast_json_object_iter_key(iter)) == 0) { + int prop_is_valid; + prop_is_valid = ast_ari_validate_string( + ast_json_object_iter_value(iter)); + if (!prop_is_valid) { + ast_log(LOG_ERROR, "ARI Playback field next_media_uri failed validation\n"); + res = 0; + } + } else if (strcmp("state", ast_json_object_iter_key(iter)) == 0) { int prop_is_valid; has_state = 1; @@ -4741,6 +4750,9 @@ int ast_ari_validate_event(struct ast_json *json) if (strcmp("PeerStatusChange", discriminator) == 0) { return ast_ari_validate_peer_status_change(json); } else + if (strcmp("PlaybackContinuing", discriminator) == 0) { + return ast_ari_validate_playback_continuing(json); + } else if (strcmp("PlaybackFinished", discriminator) == 0) { return ast_ari_validate_playback_finished(json); } else @@ -4930,6 +4942,9 @@ int ast_ari_validate_message(struct ast_json *json) if (strcmp("PeerStatusChange", discriminator) == 0) { return ast_ari_validate_peer_status_change(json); } else + if (strcmp("PlaybackContinuing", discriminator) == 0) { + return ast_ari_validate_playback_continuing(json); + } else if (strcmp("PlaybackFinished", discriminator) == 0) { return ast_ari_validate_playback_finished(json); } else @@ -5216,6 +5231,85 @@ ari_validator ast_ari_validate_peer_status_change_fn(void) return ast_ari_validate_peer_status_change; } +int ast_ari_validate_playback_continuing(struct ast_json *json) +{ + int res = 1; + struct ast_json_iter *iter; + int has_type = 0; + int has_application = 0; + int has_playback = 0; + + for (iter = ast_json_object_iter(json); iter; iter = ast_json_object_iter_next(json, iter)) { + if (strcmp("type", ast_json_object_iter_key(iter)) == 0) { + int prop_is_valid; + has_type = 1; + prop_is_valid = ast_ari_validate_string( + ast_json_object_iter_value(iter)); + if (!prop_is_valid) { + ast_log(LOG_ERROR, "ARI PlaybackContinuing field type failed validation\n"); + res = 0; + } + } else + if (strcmp("application", ast_json_object_iter_key(iter)) == 0) { + int prop_is_valid; + has_application = 1; + prop_is_valid = ast_ari_validate_string( + ast_json_object_iter_value(iter)); + if (!prop_is_valid) { + ast_log(LOG_ERROR, "ARI PlaybackContinuing field application failed validation\n"); + res = 0; + } + } else + if (strcmp("timestamp", ast_json_object_iter_key(iter)) == 0) { + int prop_is_valid; + prop_is_valid = ast_ari_validate_date( + ast_json_object_iter_value(iter)); + if (!prop_is_valid) { + ast_log(LOG_ERROR, "ARI PlaybackContinuing field timestamp failed validation\n"); + res = 0; + } + } else + if (strcmp("playback", ast_json_object_iter_key(iter)) == 0) { + int prop_is_valid; + has_playback = 1; + prop_is_valid = ast_ari_validate_playback( + ast_json_object_iter_value(iter)); + if (!prop_is_valid) { + ast_log(LOG_ERROR, "ARI PlaybackContinuing field playback failed validation\n"); + res = 0; + } + } else + { + ast_log(LOG_ERROR, + "ARI PlaybackContinuing has undocumented field %s\n", + ast_json_object_iter_key(iter)); + res = 0; + } + } + + if (!has_type) { + ast_log(LOG_ERROR, "ARI PlaybackContinuing missing required field type\n"); + res = 0; + } + + if (!has_application) { + ast_log(LOG_ERROR, "ARI PlaybackContinuing missing required field application\n"); + res = 0; + } + + if (!has_playback) { + ast_log(LOG_ERROR, "ARI PlaybackContinuing missing required field playback\n"); + res = 0; + } + + return res; +} + +ari_validator ast_ari_validate_playback_continuing_fn(void) +{ + return ast_ari_validate_playback_continuing; +} + int ast_ari_validate_playback_finished(struct ast_json *json) { int res = 1; diff --git a/res/ari/ari_model_validators.h b/res/ari/ari_model_validators.h index 0bcdb0fa2..2634528ba 100644 --- a/res/ari/ari_model_validators.h +++ b/res/ari/ari_model_validators.h @@ -1187,6 +1187,24 @@ int ast_ari_validate_peer_status_change(struct ast_json *json); ari_validator ast_ari_validate_peer_status_change_fn(void); /*! + * \brief Validator for PlaybackContinuing. + * + * Event showing the continuation of a media playback operation from one media URI to the next in the list. + * + * \param json JSON object to validate. + * \returns True (non-zero) if valid. + * \returns False (zero) if invalid. + */ +int ast_ari_validate_playback_continuing(struct ast_json *json); + +/*! + * \brief Function pointer to ast_ari_validate_playback_continuing(). + * + * See \ref ast_ari_model_validators.h for more details. + */ +ari_validator ast_ari_validate_playback_continuing_fn(void); + +/*! * \brief Validator for PlaybackFinished. * * Event showing the completion of a media playback operation. @@ -1457,6 +1475,7 @@ ari_validator ast_ari_validate_application_fn(void); * - id: string (required) * - language: string * - media_uri: string (required) + * - next_media_uri: string * - state: string (required) * - target_uri: string (required) * DeviceState @@ -1670,6 +1689,11 @@ ari_validator ast_ari_validate_application_fn(void); * - timestamp: Date * - endpoint: Endpoint (required) * - peer: Peer (required) + * PlaybackContinuing + * - type: string (required) + * - application: string (required) + * - timestamp: Date + * - playback: Playback (required) * PlaybackFinished * - type: string (required) * - application: string (required) diff --git a/res/ari/resource_bridges.c b/res/ari/resource_bridges.c index 57c1c2738..cec443dba 100644 --- a/res/ari/resource_bridges.c +++ b/res/ari/resource_bridges.c @@ -332,7 +332,8 @@ static struct ast_channel *prepare_bridge_media_channel(const char *type) * \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_media medias to play + * \param args_media_count number of media items in \c media * \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 @@ -346,7 +347,8 @@ static struct ast_channel *prepare_bridge_media_channel(const char *type) * \retval -1 operation failed * \retval operation was successful */ -static int ari_bridges_play_helper(const char *args_media, +static int ari_bridges_play_helper(const char **args_media, + size_t args_media_count, const char *args_lang, int args_offset_ms, int args_skipms, @@ -371,8 +373,8 @@ static int ari_bridges_play_helper(const char *args_media, 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, + playback = stasis_app_control_play_uri(control, args_media, args_media_count, + language, bridge->uniqueid, STASIS_PLAYBACK_TARGET_BRIDGE, args_skipms, args_offset_ms, args_playback_id); if (!playback) { @@ -396,7 +398,8 @@ static int ari_bridges_play_helper(const char *args_media, return 0; } -static void ari_bridges_play_new(const char *args_media, +static void ari_bridges_play_new(const char **args_media, + size_t args_media_count, const char *args_lang, int args_offset_ms, int args_skipms, @@ -449,9 +452,9 @@ static void ari_bridges_play_new(const char *args_media, } 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)) { + if (ari_bridges_play_helper(args_media, args_media_count, args_lang, + args_offset_ms, args_skipms, args_playback_id, response, bridge, + control, &json, &playback_url)) { ao2_unlock(control); return; } @@ -497,7 +500,8 @@ enum play_found_result { * \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_media medias to play + * \param args_media_count number of media items in \c media * \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 @@ -511,7 +515,8 @@ enum play_found_result { * \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, +static enum play_found_result ari_bridges_play_found(const char **args_media, + size_t args_media_count, const char *args_lang, int args_offset_ms, int args_skipms, @@ -537,9 +542,9 @@ static enum play_found_result ari_bridges_play_found(const char *args_media, 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)) { + if (ari_bridges_play_helper(args_media, args_media_count, + args_lang, args_offset_ms, args_skipms, args_playback_id, + response, bridge, control, &json, &playback_url)) { ao2_unlock(control); return PLAY_FOUND_FAILURE; } @@ -551,7 +556,8 @@ static enum play_found_result ari_bridges_play_found(const char *args_media, static void ari_bridges_handle_play( const char *args_bridge_id, - const char *args_media, + const char **args_media, + size_t args_media_count, const char *args_lang, int args_offset_ms, int args_skipms, @@ -574,15 +580,15 @@ static void ari_bridges_handle_play( * 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, + if (ari_bridges_play_found(args_media, args_media_count, 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, + ari_bridges_play_new(args_media, args_media_count, args_lang, args_offset_ms, args_skipms, args_playback_id, response, bridge); } @@ -593,6 +599,7 @@ void ast_ari_bridges_play(struct ast_variable *headers, { ari_bridges_handle_play(args->bridge_id, args->media, + args->media_count, args->lang, args->offsetms, args->skipms, @@ -606,6 +613,7 @@ void ast_ari_bridges_play_with_id(struct ast_variable *headers, { ari_bridges_handle_play(args->bridge_id, args->media, + args->media_count, args->lang, args->offsetms, args->skipms, diff --git a/res/ari/resource_bridges.h b/res/ari/resource_bridges.h index 36ff6a017..17a3b8365 100644 --- a/res/ari/resource_bridges.h +++ b/res/ari/resource_bridges.h @@ -245,11 +245,15 @@ void ast_ari_bridges_stop_moh(struct ast_variable *headers, struct ast_ari_bridg struct ast_ari_bridges_play_args { /*! Bridge's id */ const char *bridge_id; - /*! Media's URI to play. */ - const char *media; + /*! Array of Media URIs to play. */ + const char **media; + /*! Length of media array. */ + size_t media_count; + /*! Parsing context for media. */ + char *media_parse; /*! For sounds, selects language for sound. */ const char *lang; - /*! Number of media to skip before playing. */ + /*! Number of milliseconds to skip before playing. Only applies to the first URI if multiple media URIs are specified. */ int offsetms; /*! Number of milliseconds to skip for forward/reverse operations. */ int skipms; @@ -283,11 +287,15 @@ struct ast_ari_bridges_play_with_id_args { const char *bridge_id; /*! Playback ID. */ const char *playback_id; - /*! Media's URI to play. */ - const char *media; + /*! Array of Media URIs to play. */ + const char **media; + /*! Length of media array. */ + size_t media_count; + /*! Parsing context for media. */ + char *media_parse; /*! For sounds, selects language for sound. */ const char *lang; - /*! Number of media to skip before playing. */ + /*! Number of milliseconds to skip before playing. Only applies to the first URI if multiple media URIs are specified. */ int offsetms; /*! Number of milliseconds to skip for forward/reverse operations. */ int skipms; diff --git a/res/ari/resource_channels.c b/res/ari/resource_channels.c index edf1a20e6..b42581c84 100644 --- a/res/ari/resource_channels.c +++ b/res/ari/resource_channels.c @@ -469,7 +469,8 @@ void ast_ari_channels_stop_silence(struct ast_variable *headers, static void ari_channels_handle_play( const char *args_channel_id, - const char *args_media, + const char **args_media, + size_t args_media_count, const char *args_lang, int args_offsetms, int args_skipms, @@ -515,7 +516,7 @@ static void ari_channels_handle_play( language = S_OR(args_lang, snapshot->language); - playback = stasis_app_control_play_uri(control, args_media, language, + playback = stasis_app_control_play_uri(control, args_media, args_media_count, language, args_channel_id, STASIS_PLAYBACK_TARGET_CHANNEL, args_skipms, args_offsetms, args_playback_id); if (!playback) { ast_ari_response_error( @@ -551,6 +552,7 @@ void ast_ari_channels_play(struct ast_variable *headers, ari_channels_handle_play( args->channel_id, args->media, + args->media_count, args->lang, args->offsetms, args->skipms, @@ -565,6 +567,7 @@ void ast_ari_channels_play_with_id(struct ast_variable *headers, ari_channels_handle_play( args->channel_id, args->media, + args->media_count, args->lang, args->offsetms, args->skipms, diff --git a/res/ari/resource_channels.h b/res/ari/resource_channels.h index 89b466d00..c690d70c8 100644 --- a/res/ari/resource_channels.h +++ b/res/ari/resource_channels.h @@ -505,11 +505,15 @@ void ast_ari_channels_stop_silence(struct ast_variable *headers, struct ast_ari_ struct ast_ari_channels_play_args { /*! Channel's id */ const char *channel_id; - /*! Media's URI to play. */ - const char *media; + /*! Array of Media URIs to play. */ + const char **media; + /*! Length of media array. */ + size_t media_count; + /*! Parsing context for media. */ + char *media_parse; /*! For sounds, selects language for sound. */ const char *lang; - /*! Number of media to skip before playing. */ + /*! Number of milliseconds to skip before playing. Only applies to the first URI if multiple media URIs are specified. */ int offsetms; /*! Number of milliseconds to skip for forward/reverse operations. */ int skipms; @@ -543,11 +547,15 @@ struct ast_ari_channels_play_with_id_args { const char *channel_id; /*! Playback ID. */ const char *playback_id; - /*! Media's URI to play. */ - const char *media; + /*! Array of Media URIs to play. */ + const char **media; + /*! Length of media array. */ + size_t media_count; + /*! Parsing context for media. */ + char *media_parse; /*! For sounds, selects language for sound. */ const char *lang; - /*! Number of media to skip before playing. */ + /*! Number of milliseconds to skip before playing. Only applies to the first URI if multiple media URIs are specified. */ int offsetms; /*! Number of milliseconds to skip for forward/reverse operations. */ int skipms; diff --git a/res/res_ari_bridges.c b/res/res_ari_bridges.c index 633dc94eb..119687999 100644 --- a/res/res_ari_bridges.c +++ b/res/res_ari_bridges.c @@ -935,7 +935,32 @@ int ast_ari_bridges_play_parse_body( /* Parse query parameters out of it */ field = ast_json_object_get(body, "media"); if (field) { - args->media = ast_json_string_get(field); + /* If they were silly enough to both pass in a query param and a + * JSON body, free up the query value. + */ + ast_free(args->media); + if (ast_json_typeof(field) == AST_JSON_ARRAY) { + /* Multiple param passed as array */ + size_t i; + args->media_count = ast_json_array_size(field); + args->media = ast_malloc(sizeof(*args->media) * args->media_count); + + if (!args->media) { + return -1; + } + + for (i = 0; i < args->media_count; ++i) { + args->media[i] = ast_json_string_get(ast_json_array_get(field, i)); + } + } else { + /* Multiple param passed as single value */ + args->media_count = 1; + args->media = ast_malloc(sizeof(*args->media) * args->media_count); + if (!args->media) { + return -1; + } + args->media[0] = ast_json_string_get(field); + } } field = ast_json_object_get(body, "lang"); if (field) { @@ -978,7 +1003,47 @@ static void ast_ari_bridges_play_cb( for (i = get_params; i; i = i->next) { if (strcmp(i->name, "media") == 0) { - args.media = (i->value); + /* Parse comma separated list */ + char *vals[MAX_VALS]; + size_t j; + + args.media_parse = ast_strdup(i->value); + if (!args.media_parse) { + ast_ari_response_alloc_failed(response); + goto fin; + } + + if (strlen(args.media_parse) == 0) { + /* ast_app_separate_args can't handle "" */ + args.media_count = 1; + vals[0] = args.media_parse; + } else { + args.media_count = ast_app_separate_args( + args.media_parse, ',', vals, + ARRAY_LEN(vals)); + } + + if (args.media_count == 0) { + ast_ari_response_alloc_failed(response); + goto fin; + } + + if (args.media_count >= MAX_VALS) { + ast_ari_response_error(response, 400, + "Bad Request", + "Too many values for media"); + goto fin; + } + + args.media = ast_malloc(sizeof(*args.media) * args.media_count); + if (!args.media) { + ast_ari_response_alloc_failed(response); + goto fin; + } + + for (j = 0; j < args.media_count; ++j) { + args.media[j] = (vals[j]); + } } else if (strcmp(i->name, "lang") == 0) { args.lang = (i->value); @@ -1051,6 +1116,8 @@ static void ast_ari_bridges_play_cb( #endif /* AST_DEVMODE */ fin: __attribute__((unused)) + ast_free(args.media_parse); + ast_free(args.media); return; } int ast_ari_bridges_play_with_id_parse_body( @@ -1061,7 +1128,32 @@ int ast_ari_bridges_play_with_id_parse_body( /* Parse query parameters out of it */ field = ast_json_object_get(body, "media"); if (field) { - args->media = ast_json_string_get(field); + /* If they were silly enough to both pass in a query param and a + * JSON body, free up the query value. + */ + ast_free(args->media); + if (ast_json_typeof(field) == AST_JSON_ARRAY) { + /* Multiple param passed as array */ + size_t i; + args->media_count = ast_json_array_size(field); + args->media = ast_malloc(sizeof(*args->media) * args->media_count); + + if (!args->media) { + return -1; + } + + for (i = 0; i < args->media_count; ++i) { + args->media[i] = ast_json_string_get(ast_json_array_get(field, i)); + } + } else { + /* Multiple param passed as single value */ + args->media_count = 1; + args->media = ast_malloc(sizeof(*args->media) * args->media_count); + if (!args->media) { + return -1; + } + args->media[0] = ast_json_string_get(field); + } } field = ast_json_object_get(body, "lang"); if (field) { @@ -1100,7 +1192,47 @@ static void ast_ari_bridges_play_with_id_cb( for (i = get_params; i; i = i->next) { if (strcmp(i->name, "media") == 0) { - args.media = (i->value); + /* Parse comma separated list */ + char *vals[MAX_VALS]; + size_t j; + + args.media_parse = ast_strdup(i->value); + if (!args.media_parse) { + ast_ari_response_alloc_failed(response); + goto fin; + } + + if (strlen(args.media_parse) == 0) { + /* ast_app_separate_args can't handle "" */ + args.media_count = 1; + vals[0] = args.media_parse; + } else { + args.media_count = ast_app_separate_args( + args.media_parse, ',', vals, + ARRAY_LEN(vals)); + } + + if (args.media_count == 0) { + ast_ari_response_alloc_failed(response); + goto fin; + } + + if (args.media_count >= MAX_VALS) { + ast_ari_response_error(response, 400, + "Bad Request", + "Too many values for media"); + goto fin; + } + + args.media = ast_malloc(sizeof(*args.media) * args.media_count); + if (!args.media) { + ast_ari_response_alloc_failed(response); + goto fin; + } + + for (j = 0; j < args.media_count; ++j) { + args.media[j] = (vals[j]); + } } else if (strcmp(i->name, "lang") == 0) { args.lang = (i->value); @@ -1173,6 +1305,8 @@ static void ast_ari_bridges_play_with_id_cb( #endif /* AST_DEVMODE */ fin: __attribute__((unused)) + ast_free(args.media_parse); + ast_free(args.media); return; } int ast_ari_bridges_record_parse_body( diff --git a/res/res_ari_channels.c b/res/res_ari_channels.c index 1f0818170..951a5475b 100644 --- a/res/res_ari_channels.c +++ b/res/res_ari_channels.c @@ -1842,7 +1842,32 @@ int ast_ari_channels_play_parse_body( /* Parse query parameters out of it */ field = ast_json_object_get(body, "media"); if (field) { - args->media = ast_json_string_get(field); + /* If they were silly enough to both pass in a query param and a + * JSON body, free up the query value. + */ + ast_free(args->media); + if (ast_json_typeof(field) == AST_JSON_ARRAY) { + /* Multiple param passed as array */ + size_t i; + args->media_count = ast_json_array_size(field); + args->media = ast_malloc(sizeof(*args->media) * args->media_count); + + if (!args->media) { + return -1; + } + + for (i = 0; i < args->media_count; ++i) { + args->media[i] = ast_json_string_get(ast_json_array_get(field, i)); + } + } else { + /* Multiple param passed as single value */ + args->media_count = 1; + args->media = ast_malloc(sizeof(*args->media) * args->media_count); + if (!args->media) { + return -1; + } + args->media[0] = ast_json_string_get(field); + } } field = ast_json_object_get(body, "lang"); if (field) { @@ -1885,7 +1910,47 @@ static void ast_ari_channels_play_cb( for (i = get_params; i; i = i->next) { if (strcmp(i->name, "media") == 0) { - args.media = (i->value); + /* Parse comma separated list */ + char *vals[MAX_VALS]; + size_t j; + + args.media_parse = ast_strdup(i->value); + if (!args.media_parse) { + ast_ari_response_alloc_failed(response); + goto fin; + } + + if (strlen(args.media_parse) == 0) { + /* ast_app_separate_args can't handle "" */ + args.media_count = 1; + vals[0] = args.media_parse; + } else { + args.media_count = ast_app_separate_args( + args.media_parse, ',', vals, + ARRAY_LEN(vals)); + } + + if (args.media_count == 0) { + ast_ari_response_alloc_failed(response); + goto fin; + } + + if (args.media_count >= MAX_VALS) { + ast_ari_response_error(response, 400, + "Bad Request", + "Too many values for media"); + goto fin; + } + + args.media = ast_malloc(sizeof(*args.media) * args.media_count); + if (!args.media) { + ast_ari_response_alloc_failed(response); + goto fin; + } + + for (j = 0; j < args.media_count; ++j) { + args.media[j] = (vals[j]); + } } else if (strcmp(i->name, "lang") == 0) { args.lang = (i->value); @@ -1958,6 +2023,8 @@ static void ast_ari_channels_play_cb( #endif /* AST_DEVMODE */ fin: __attribute__((unused)) + ast_free(args.media_parse); + ast_free(args.media); return; } int ast_ari_channels_play_with_id_parse_body( @@ -1968,7 +2035,32 @@ int ast_ari_channels_play_with_id_parse_body( /* Parse query parameters out of it */ field = ast_json_object_get(body, "media"); if (field) { - args->media = ast_json_string_get(field); + /* If they were silly enough to both pass in a query param and a + * JSON body, free up the query value. + */ + ast_free(args->media); + if (ast_json_typeof(field) == AST_JSON_ARRAY) { + /* Multiple param passed as array */ + size_t i; + args->media_count = ast_json_array_size(field); + args->media = ast_malloc(sizeof(*args->media) * args->media_count); + + if (!args->media) { + return -1; + } + + for (i = 0; i < args->media_count; ++i) { + args->media[i] = ast_json_string_get(ast_json_array_get(field, i)); + } + } else { + /* Multiple param passed as single value */ + args->media_count = 1; + args->media = ast_malloc(sizeof(*args->media) * args->media_count); + if (!args->media) { + return -1; + } + args->media[0] = ast_json_string_get(field); + } } field = ast_json_object_get(body, "lang"); if (field) { @@ -2007,7 +2099,47 @@ static void ast_ari_channels_play_with_id_cb( for (i = get_params; i; i = i->next) { if (strcmp(i->name, "media") == 0) { - args.media = (i->value); + /* Parse comma separated list */ + char *vals[MAX_VALS]; + size_t j; + + args.media_parse = ast_strdup(i->value); + if (!args.media_parse) { + ast_ari_response_alloc_failed(response); + goto fin; + } + + if (strlen(args.media_parse) == 0) { + /* ast_app_separate_args can't handle "" */ + args.media_count = 1; + vals[0] = args.media_parse; + } else { + args.media_count = ast_app_separate_args( + args.media_parse, ',', vals, + ARRAY_LEN(vals)); + } + + if (args.media_count == 0) { + ast_ari_response_alloc_failed(response); + goto fin; + } + + if (args.media_count >= MAX_VALS) { + ast_ari_response_error(response, 400, + "Bad Request", + "Too many values for media"); + goto fin; + } + + args.media = ast_malloc(sizeof(*args.media) * args.media_count); + if (!args.media) { + ast_ari_response_alloc_failed(response); + goto fin; + } + + for (j = 0; j < args.media_count; ++j) { + args.media[j] = (vals[j]); + } } else if (strcmp(i->name, "lang") == 0) { args.lang = (i->value); @@ -2080,6 +2212,8 @@ static void ast_ari_channels_play_with_id_cb( #endif /* AST_DEVMODE */ fin: __attribute__((unused)) + ast_free(args.media_parse); + ast_free(args.media); return; } int ast_ari_channels_record_parse_body( diff --git a/res/res_stasis_playback.c b/res/res_stasis_playback.c index 97191c26d..a64ecffa7 100644 --- a/res/res_stasis_playback.c +++ b/res/res_stasis_playback.c @@ -70,10 +70,16 @@ static struct ao2_container *playbacks; struct stasis_app_playback { AST_DECLARE_STRING_FIELDS( AST_STRING_FIELD(id); /*!< Playback unique id */ - AST_STRING_FIELD(media); /*!< Playback media uri */ + AST_STRING_FIELD(media); /*!< The current media playing */ AST_STRING_FIELD(language); /*!< Preferred language */ AST_STRING_FIELD(target); /*!< Playback device uri */ - ); + ); + /*! The list of medias to play back */ + AST_VECTOR(, char *) medias; + + /*! The current index in \c medias we're playing */ + size_t media_index; + /*! Control object for the channel we're playing back to */ struct stasis_app_control *control; /*! Number of milliseconds to skip before playing */ @@ -99,6 +105,8 @@ static struct ast_json *playback_to_json(struct stasis_message *message, if (!strcmp(state, "playing")) { type = "PlaybackStarted"; + } else if (!strcmp(state, "continuing")) { + type = "PlaybackContinuing"; } else if (!strcmp(state, "done")) { type = "PlaybackFinished"; } else { @@ -117,6 +125,14 @@ STASIS_MESSAGE_TYPE_DEFN(stasis_app_playback_snapshot_type, static void playback_dtor(void *obj) { struct stasis_app_playback *playback = obj; + int i; + + for (i = 0; i < AST_VECTOR_SIZE(&playback->medias); i++) { + char *media = AST_VECTOR_GET(&playback->medias, i); + + ast_free(media); + } + AST_VECTOR_FREE(&playback->medias); ao2_cleanup(playback->control); ast_string_field_free_memory(playback); @@ -137,6 +153,11 @@ static struct stasis_app_playback *playback_create( return NULL; } + if (AST_VECTOR_INIT(&playback->medias, 8)) { + ao2_ref(playback, -1); + return NULL; + } + if (!ast_strlen_zero(id)) { ast_string_field_set(playback, id, id); } else { @@ -180,6 +201,8 @@ static const char *state_to_string(enum stasis_app_playback_state state) return "playing"; case STASIS_PLAYBACK_STATE_PAUSED: return "paused"; + case STASIS_PLAYBACK_STATE_CONTINUING: + return "continuing"; case STASIS_PLAYBACK_STATE_STOPPED: case STASIS_PLAYBACK_STATE_COMPLETE: case STASIS_PLAYBACK_STATE_CANCELED: @@ -241,7 +264,11 @@ static void playback_final_update(struct stasis_app_playback *playback, playback->playedms = playedms; if (res == 0) { - playback->state = STASIS_PLAYBACK_STATE_COMPLETE; + if (playback->media_index == AST_VECTOR_SIZE(&playback->medias) - 1) { + playback->state = STASIS_PLAYBACK_STATE_COMPLETE; + } else { + playback->state = STASIS_PLAYBACK_STATE_CONTINUING; + } } else { if (playback->state == STASIS_PLAYBACK_STATE_STOPPED) { ast_log(LOG_NOTICE, "%s: Playback stopped for %s\n", @@ -262,7 +289,7 @@ static void play_on_channel(struct stasis_app_playback *playback, int res; long offsetms; - /* Even though these local variables look fairly pointless, the avoid + /* Even though these local variables look fairly pointless, they avoid * having a bunch of NULL's passed directly into * ast_control_streamfile() */ const char *fwd = NULL; @@ -273,73 +300,80 @@ static void play_on_channel(struct stasis_app_playback *playback, ast_assert(playback != NULL); - offsetms = playback->offsetms; - - res = playback_first_update(playback, ast_channel_uniqueid(chan)); - - if (res != 0) { - return; - } - if (ast_channel_state(chan) != AST_STATE_UP) { ast_indicate(chan, AST_CONTROL_PROGRESS); } - if (ast_begins_with(playback->media, SOUND_URI_SCHEME)) { - playback->controllable = 1; - - /* Play sound */ - res = ast_control_streamfile_lang(chan, playback->media + strlen(SOUND_URI_SCHEME), - fwd, rev, stop, pause, restart, playback->skipms, playback->language, - &offsetms); - } else if (ast_begins_with(playback->media, RECORDING_URI_SCHEME)) { - /* Play recording */ - RAII_VAR(struct stasis_app_stored_recording *, recording, NULL, - ao2_cleanup); - const char *relname = - playback->media + strlen(RECORDING_URI_SCHEME); - recording = stasis_app_stored_recording_find_by_name(relname); - - if (!recording) { - ast_log(LOG_ERROR, "Attempted to play recording '%s' on channel '%s' but recording does not exist", - relname, ast_channel_name(chan)); - return; - } + offsetms = playback->offsetms; - playback->controllable = 1; + for (; playback->media_index < AST_VECTOR_SIZE(&playback->medias); playback->media_index++) { - res = ast_control_streamfile_lang(chan, - stasis_app_stored_recording_get_file(recording), fwd, rev, stop, pause, - restart, playback->skipms, playback->language, &offsetms); - } else if (ast_begins_with(playback->media, NUMBER_URI_SCHEME)) { - int number; + /* Set the current media to play */ + ast_string_field_set(playback, media, AST_VECTOR_GET(&playback->medias, playback->media_index)); - if (sscanf(playback->media + strlen(NUMBER_URI_SCHEME), "%30d", &number) != 1) { - ast_log(LOG_ERROR, "Attempted to play number '%s' on channel '%s' but number is invalid", - playback->media + strlen(NUMBER_URI_SCHEME), ast_channel_name(chan)); + res = playback_first_update(playback, ast_channel_uniqueid(chan)); + if (res != 0) { return; } - res = ast_say_number(chan, number, stop, playback->language, NULL); - } else if (ast_begins_with(playback->media, DIGITS_URI_SCHEME)) { - res = ast_say_digit_str(chan, playback->media + strlen(DIGITS_URI_SCHEME), - stop, playback->language); - } else if (ast_begins_with(playback->media, CHARACTERS_URI_SCHEME)) { - res = ast_say_character_str(chan, playback->media + strlen(CHARACTERS_URI_SCHEME), - stop, playback->language, AST_SAY_CASE_NONE); - } else if (ast_begins_with(playback->media, TONE_URI_SCHEME)) { - playback->controllable = 1; - 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\n", - playback->media, ast_channel_name(chan)); - return; - } + if (ast_begins_with(playback->media, SOUND_URI_SCHEME)) { + playback->controllable = 1; + + /* Play sound */ + res = ast_control_streamfile_lang(chan, playback->media + strlen(SOUND_URI_SCHEME), + fwd, rev, stop, pause, restart, playback->skipms, playback->language, + &offsetms); + } else if (ast_begins_with(playback->media, RECORDING_URI_SCHEME)) { + /* Play recording */ + RAII_VAR(struct stasis_app_stored_recording *, recording, NULL, + ao2_cleanup); + const char *relname = + playback->media + strlen(RECORDING_URI_SCHEME); + recording = stasis_app_stored_recording_find_by_name(relname); + + if (!recording) { + ast_log(LOG_ERROR, "Attempted to play recording '%s' on channel '%s' but recording does not exist", + relname, ast_channel_name(chan)); + continue; + } + + playback->controllable = 1; + + res = ast_control_streamfile_lang(chan, + stasis_app_stored_recording_get_file(recording), fwd, rev, stop, pause, + restart, playback->skipms, playback->language, &offsetms); + } else if (ast_begins_with(playback->media, NUMBER_URI_SCHEME)) { + int number; + + if (sscanf(playback->media + strlen(NUMBER_URI_SCHEME), "%30d", &number) != 1) { + ast_log(LOG_ERROR, "Attempted to play number '%s' on channel '%s' but number is invalid", + playback->media + strlen(NUMBER_URI_SCHEME), ast_channel_name(chan)); + continue; + } + + res = ast_say_number(chan, number, stop, playback->language, NULL); + } else if (ast_begins_with(playback->media, DIGITS_URI_SCHEME)) { + res = ast_say_digit_str(chan, playback->media + strlen(DIGITS_URI_SCHEME), + stop, playback->language); + } else if (ast_begins_with(playback->media, CHARACTERS_URI_SCHEME)) { + res = ast_say_character_str(chan, playback->media + strlen(CHARACTERS_URI_SCHEME), + stop, playback->language, AST_SAY_CASE_NONE); + } else if (ast_begins_with(playback->media, TONE_URI_SCHEME)) { + playback->controllable = 1; + 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\n", + playback->media, ast_channel_name(chan)); + continue; + } - playback_final_update(playback, offsetms, res, - ast_channel_uniqueid(chan)); + playback_final_update(playback, offsetms, res, + ast_channel_uniqueid(chan)); + /* Reset offset for any subsequent media */ + offsetms = 0; + } return; } @@ -431,30 +465,45 @@ static void set_target_uri( } struct stasis_app_playback *stasis_app_control_play_uri( - struct stasis_app_control *control, const char *uri, - const char *language, const char *target_id, + struct stasis_app_control *control, const char **media, + size_t media_count, const char *language, const char *target_id, enum stasis_app_playback_target_type target_type, int skipms, long offsetms, const char *id) { struct stasis_app_playback *playback; + size_t i; - if (skipms < 0 || offsetms < 0) { + if (skipms < 0 || offsetms < 0 || media_count == 0) { return NULL; } - ast_debug(3, "%s: Sending play(%s) command\n", - stasis_app_control_get_channel_id(control), uri); - playback = playback_create(control, id); if (!playback) { return NULL; } + for (i = 0; i < media_count; i++) { + char *media_uri; + + media_uri = ast_malloc(strlen(media[i]) + 1); + if (!media_uri) { + ao2_ref(playback, -1); + return NULL; + } + + ast_debug(3, "%s: Sending play(%s) command\n", + stasis_app_control_get_channel_id(control), media[i]); + + /* safe */ + strcpy(media_uri, media[i]); + AST_VECTOR_APPEND(&playback->medias, media_uri); + } + if (skipms == 0) { skipms = PLAYBACK_DEFAULT_SKIPMS; } - ast_string_field_set(playback, media, uri); + ast_string_field_set(playback, media, AST_VECTOR_GET(&playback->medias, 0)); ast_string_field_set(playback, language, language); set_target_uri(playback, target_type, target_id); playback->skipms = skipms; @@ -497,12 +546,22 @@ struct ast_json *stasis_app_playback_to_json( return NULL; } - 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)); + if (playback->media_index == AST_VECTOR_SIZE(&playback->medias) - 1) { + 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)); + } else { + json = ast_json_pack("{s: s, s: s, s: s, s: s, s: s, s: s}", + "id", playback->id, + "media_uri", playback->media, + "next_media_uri", AST_VECTOR_GET(&playback->medias, playback->media_index + 1), + "target_uri", playback->target, + "language", playback->language, + "state", state_to_string(playback->state)); + } return ast_json_ref(json); } @@ -615,6 +674,13 @@ playback_opreation_cb operations[STASIS_PLAYBACK_STATE_MAX][STASIS_PLAYBACK_MEDI [STASIS_PLAYBACK_STATE_PLAYING][STASIS_PLAYBACK_REVERSE] = playback_reverse, [STASIS_PLAYBACK_STATE_PLAYING][STASIS_PLAYBACK_FORWARD] = playback_forward, + [STASIS_PLAYBACK_STATE_CONTINUING][STASIS_PLAYBACK_STOP] = playback_stop, + [STASIS_PLAYBACK_STATE_CONTINUING][STASIS_PLAYBACK_RESTART] = playback_restart, + [STASIS_PLAYBACK_STATE_CONTINUING][STASIS_PLAYBACK_PAUSE] = playback_pause, + [STASIS_PLAYBACK_STATE_CONTINUING][STASIS_PLAYBACK_UNPAUSE] = playback_noop, + [STASIS_PLAYBACK_STATE_CONTINUING][STASIS_PLAYBACK_REVERSE] = playback_reverse, + [STASIS_PLAYBACK_STATE_CONTINUING][STASIS_PLAYBACK_FORWARD] = playback_forward, + [STASIS_PLAYBACK_STATE_PAUSED][STASIS_PLAYBACK_STOP] = playback_stop, [STASIS_PLAYBACK_STATE_PAUSED][STASIS_PLAYBACK_PAUSE] = playback_noop, [STASIS_PLAYBACK_STATE_PAUSED][STASIS_PLAYBACK_UNPAUSE] = playback_unpause, diff --git a/rest-api/api-docs/bridges.json b/rest-api/api-docs/bridges.json index b608be6d6..ab2c6c2d5 100644 --- a/rest-api/api-docs/bridges.json +++ b/rest-api/api-docs/bridges.json @@ -328,10 +328,10 @@ }, { "name": "media", - "description": "Media's URI to play.", + "description": "Media URIs to play.", "paramType": "query", "required": true, - "allowMultiple": false, + "allowMultiple": true, "dataType": "string" }, { @@ -344,7 +344,7 @@ }, { "name": "offsetms", - "description": "Number of media to skip before playing.", + "description": "Number of milliseconds to skip before playing. Only applies to the first URI if multiple media URIs are specified.", "paramType": "query", "required": false, "allowMultiple": false, @@ -420,10 +420,10 @@ }, { "name": "media", - "description": "Media's URI to play.", + "description": "Media URIs to play.", "paramType": "query", "required": true, - "allowMultiple": false, + "allowMultiple": true, "dataType": "string" }, { @@ -436,7 +436,7 @@ }, { "name": "offsetms", - "description": "Number of media to skip before playing.", + "description": "Number of milliseconds to skip before playing. Only applies to the first URI if multiple media URIs are specified.", "paramType": "query", "required": false, "allowMultiple": false, diff --git a/rest-api/api-docs/channels.json b/rest-api/api-docs/channels.json index 2389f7cb9..aafd231a1 100644 --- a/rest-api/api-docs/channels.json +++ b/rest-api/api-docs/channels.json @@ -973,10 +973,10 @@ }, { "name": "media", - "description": "Media's URI to play.", + "description": "Media URIs to play.", "paramType": "query", "required": true, - "allowMultiple": false, + "allowMultiple": true, "dataType": "string" }, { @@ -989,7 +989,7 @@ }, { "name": "offsetms", - "description": "Number of media to skip before playing.", + "description": "Number of milliseconds to skip before playing. Only applies to the first URI if multiple media URIs are specified.", "paramType": "query", "required": false, "allowMultiple": false, @@ -1055,10 +1055,10 @@ }, { "name": "media", - "description": "Media's URI to play.", + "description": "Media URIs to play.", "paramType": "query", "required": true, - "allowMultiple": false, + "allowMultiple": true, "dataType": "string" }, { @@ -1071,7 +1071,7 @@ }, { "name": "offsetms", - "description": "Number of media to skip before playing.", + "description": "Number of milliseconds to skip before playing. Only applies to the first URI if multiple media URIs are specified.", "paramType": "query", "required": false, "allowMultiple": false, diff --git a/rest-api/api-docs/events.json b/rest-api/api-docs/events.json index dee7c2db9..ca2616101 100644 --- a/rest-api/api-docs/events.json +++ b/rest-api/api-docs/events.json @@ -146,6 +146,7 @@ "subTypes": [ "DeviceStateChanged", "PlaybackStarted", + "PlaybackContinuing", "PlaybackFinished", "RecordingStarted", "RecordingFinished", @@ -270,6 +271,17 @@ } } }, + "PlaybackContinuing": { + "id": "PlaybackContinuing", + "description": "Event showing the continuation of a media playback operation from one media URI to the next in the list.", + "properties": { + "playback": { + "type": "Playback", + "description": "Playback control object", + "required": true + } + } + }, "PlaybackFinished": { "id": "PlaybackFinished", "description": "Event showing the completion of a media playback operation.", diff --git a/rest-api/api-docs/playbacks.json b/rest-api/api-docs/playbacks.json index 63df3f24b..9f9003558 100644 --- a/rest-api/api-docs/playbacks.json +++ b/rest-api/api-docs/playbacks.json @@ -124,9 +124,14 @@ }, "media_uri": { "type": "string", - "description": "URI for the media to play back.", + "description": "The URI for the media currently being played back.", "required": true }, + "next_media_uri": { + "type": "string", + "description": "If a list of URIs is being played, the next media URI to be played back.", + "required": false + }, "target_uri": { "type": "string", "description": "URI for the channel or bridge to play the media on", @@ -145,7 +150,8 @@ "values": [ "queued", "playing", - "complete" + "continuing", + "done" ] } } |