]> git.ipfire.org Git - thirdparty/asterisk.git/commitdiff
ARI: Add the ability to play multiple media URIs in a single operation 41/2841/2
authorMatt Jordan <mjordan@digium.com>
Mon, 18 Apr 2016 23:17:08 +0000 (18:17 -0500)
committerJoshua Colp <jcolp@digium.com>
Tue, 17 May 2016 17:01:22 +0000 (14:01 -0300)
Many ARI applications will want to play multiple media files in a row to
a resource. The most common use case is when building long-ish IVR prompts
made up of multiple, smaller sound files. Today, that requires building a
small state machine, listening for each PlaybackFinished event, and triggering
the next sound file to play. While not especially challenging, it is tedious
work. Since requiring developers to write tedious code to do normal activities
stinks, this patch adds the ability to play back a list of media files to a
resource.

Each of the 'play' operations on supported resources (channels and bridges)
now accepts a comma delineated list of media URIs to play. A single Playback
resource is created as a handle to the entire list. The operation of playing
a list is identical to playing a single media URI, save that a new event,
PlaybackContinuing, is raised instead of a PlaybackFinished for each non-final
media URI. When the entire list is finished being played, a PlaybackFinished
event is raised.

In order to help inform applications where they are in the list playback, the
Playback resource now includes a new, optional attribute, 'next_media_uri',
that contains the next URI in the list to be played.

It's important to note the following:
 - If an offset is provided to the 'play' operations, it only applies to the
   first media URI, as it would be weird to skip n seconds forward in every
   media resource.
 - Operations that control the position of the media only affect the current
   media being played. For example, once a media resource in the list
   completes, a 'reverse' operation on a subsequent media resource will not
   start a previously completed media resource at the appropiate offset.
 - This patch does not add any new operations to control the list. Hopefully,
   user feedback and/or future patches would add that if people want it.

ASTERISK-26022 #close

Change-Id: Ie1ea5356573447b8f51f2e7964915ea01792f16f

15 files changed:
CHANGES
include/asterisk/stasis_app_playback.h
res/ari/ari_model_validators.c
res/ari/ari_model_validators.h
res/ari/resource_bridges.c
res/ari/resource_bridges.h
res/ari/resource_channels.c
res/ari/resource_channels.h
res/res_ari_bridges.c
res/res_ari_channels.c
res/res_stasis_playback.c
rest-api/api-docs/bridges.json
rest-api/api-docs/channels.json
rest-api/api-docs/events.json
rest-api/api-docs/playbacks.json

diff --git a/CHANGES b/CHANGES
index ec44e30fc4b3c0580306368d20ea322d222d912b..15b4d0c670d0509819900f921db9443412b28ef1 100644 (file)
--- 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
 ------------------
 
index b35299581cfe53824b20a225cd4858c569b21f26..0038fd6d0810bbead8249570d16e6ae523ae49fd 100644 (file)
@@ -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);
 
index 623d5b54162be5f3c637bf0b84c60c79112633b2..8f05db03561aacbc4cab17c38a479a6faf2fbf81 100644 (file)
@@ -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;
index 0bcdb0fa211a13426913427c7a53532900ac290d..2634528bacec7349c874b781fc725382f3bd8cb0 100644 (file)
@@ -1186,6 +1186,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.
  *
@@ -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)
index 57c1c273889a739d925e26ae4ec4b928ef8ee786..cec443dbab4db23d79f13938a5905725c1f1bf57 100644 (file)
@@ -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,
index 36ff6a017843aebed0d4c487d2d55eacbf7745fe..17a3b8365f0974631e7ea38ba372c71594c5ffd8 100644 (file)
@@ -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;
index edf1a20e6cb99554af0bb9545960143b9d3ef5e6..b42581c8463511891aaf13ec43a03c88e23fd4cf 100644 (file)
@@ -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,
index 89b466d00a07c2edd52ea9e922b4114d54da5752..c690d70c81367ca639f76d74a7b579c10cbab391 100644 (file)
@@ -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;
index 633dc94eb2a2c8023a83e938c4604a64a63e5456..119687999fde2c2e0b0f7caf7fbffaab0f118c86 100644 (file)
@@ -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(
index 1f0818170216340ead10808d38383d5fdbe88d25..951a5475bc030ec776d9b47557c55f5fb32d9f22 100644 (file)
@@ -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(
index 97191c26dd1153d4fdb19227447b9c798681314a..a64ecffa706b52cf385ba10ca638fea459eb4494 100644 (file)
@@ -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,
index b608be6d6b5e13b52841cb55a2dfa830c7b88705..ab2c6c2d54532d047fd2422b518d21572b998f38 100644 (file)
                                                },
                                                {
                                                        "name": "media",
-                                                       "description": "Media's URI to play.",
+                                                       "description": "Media URIs to play.",
                                                        "paramType": "query",
                                                        "required": true,
-                                                       "allowMultiple": false,
+                                                       "allowMultiple": true,
                                                        "dataType": "string"
                                                },
                                                {
                                                },
                                                {
                                                        "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,
                                                },
                                                {
                                                        "name": "media",
-                                                       "description": "Media's URI to play.",
+                                                       "description": "Media URIs to play.",
                                                        "paramType": "query",
                                                        "required": true,
-                                                       "allowMultiple": false,
+                                                       "allowMultiple": true,
                                                        "dataType": "string"
                                                },
                                                {
                                                },
                                                {
                                                        "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,
index 2389f7cb9812d956a8250cde87fb5f0519c8f1bc..aafd231a1f31a09a0eca8dbe53e2835707fcb9e4 100644 (file)
                                                },
                                                {
                                                        "name": "media",
-                                                       "description": "Media's URI to play.",
+                                                       "description": "Media URIs to play.",
                                                        "paramType": "query",
                                                        "required": true,
-                                                       "allowMultiple": false,
+                                                       "allowMultiple": true,
                                                        "dataType": "string"
                                                },
                                                {
                                                },
                                                {
                                                        "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,
                                                },
                                                {
                                                        "name": "media",
-                                                       "description": "Media's URI to play.",
+                                                       "description": "Media URIs to play.",
                                                        "paramType": "query",
                                                        "required": true,
-                                                       "allowMultiple": false,
+                                                       "allowMultiple": true,
                                                        "dataType": "string"
                                                },
                                                {
                                                },
                                                {
                                                        "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,
index dee7c2db934c6fe0fbd6afea41b9da517bc1377d..ca26161017a6b982a8bba8efd2376b5335d6f7c4 100644 (file)
                        "subTypes": [
                                "DeviceStateChanged",
                                "PlaybackStarted",
+                               "PlaybackContinuing",
                                "PlaybackFinished",
                                "RecordingStarted",
                                "RecordingFinished",
                                }
                        }
                },
+               "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.",
index 63df3f24b48530ce51fe8c9d4ee7740711fa8e8e..9f90035588cc7a49eb9a7efff8b43abe2e6258b6 100644 (file)
                                },
                                "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",
                                                "values": [
                                                        "queued",
                                                        "playing",
-                                                       "complete"
+                                                       "continuing",
+                                                       "done"
                                                ]
                                        }
                                }