summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES9
-rw-r--r--include/asterisk/stasis_app_playback.h17
-rw-r--r--res/ari/ari_model_validators.c94
-rw-r--r--res/ari/ari_model_validators.h24
-rw-r--r--res/ari/resource_bridges.c42
-rw-r--r--res/ari/resource_bridges.h20
-rw-r--r--res/ari/resource_channels.c7
-rw-r--r--res/ari/resource_channels.h20
-rw-r--r--res/res_ari_bridges.c142
-rw-r--r--res/res_ari_channels.c142
-rw-r--r--res/res_stasis_playback.c210
-rw-r--r--rest-api/api-docs/bridges.json12
-rw-r--r--rest-api/api-docs/channels.json12
-rw-r--r--rest-api/api-docs/events.json12
-rw-r--r--rest-api/api-docs/playbacks.json10
15 files changed, 645 insertions, 128 deletions
diff --git a/CHANGES b/CHANGES
index ec44e30fc..15b4d0c67 100644
--- a/CHANGES
+++ b/CHANGES
@@ -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"
]
}
}