diff --git a/include/asterisk/app.h b/include/asterisk/app.h index 75d1fbb7ec..2089bced9d 100644 --- a/include/asterisk/app.h +++ b/include/asterisk/app.h @@ -636,14 +636,19 @@ int ast_linear_stream(struct ast_channel *chan, const char *filename, int fd, in /*! * \brief Stream a file with fast forward, pause, reverse, restart. - * \param chan - * \param file filename - * \param fwd, rev, stop, pause, restart, skipms, offsetms + * \param chan Channel + * \param file File to play. + * \param fwd, rev, stop, pause, restart DTMF keys for media control + * \param skipms Number of milliseconds to skip for fwd/rev. + * \param offsetms Number of milliseconds to skip when starting the media. * * Before calling this function, set this to be the number * of ms to start from the beginning of the file. When the function * returns, it will be the number of ms from the beginning where the * playback stopped. Pass NULL if you don't care. + * + * \retval 0 on success + * \retval Non-zero on failure */ int ast_control_streamfile(struct ast_channel *chan, const char *file, const char *fwd, const char *rev, const char *stop, const char *pause, const char *restart, int skipms, long *offsetms); diff --git a/include/asterisk/stasis_app_playback.h b/include/asterisk/stasis_app_playback.h new file mode 100644 index 0000000000..598c6be598 --- /dev/null +++ b/include/asterisk/stasis_app_playback.h @@ -0,0 +1,117 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * David M. Lee, II + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +#ifndef _ASTERISK_STASIS_APP_PLAYBACK_H +#define _ASTERISK_STASIS_APP_PLAYBACK_H + +/*! \file + * + * \brief Stasis Application Playback API. See \ref res_stasis "Stasis + * Application API" for detailed documentation. + * + * \author David M. Lee, II + * \since 12 + */ + +#include "asterisk/stasis_app.h" + +/*! Opaque struct for handling the playback of a single file */ +struct stasis_app_playback; + +/*! State of a playback operation */ +enum stasis_app_playback_state { + /*! The playback has not started yet */ + STASIS_PLAYBACK_STATE_QUEUED, + /*! The media is currently playing */ + STASIS_PLAYBACK_STATE_PLAYING, + /*! The media has stopped playing */ + STASIS_PLAYBACK_STATE_COMPLETE, +}; + +enum stasis_app_playback_media_control { + STASIS_PLAYBACK_STOP, + STASIS_PLAYBACK_PAUSE, + STASIS_PLAYBACK_PLAY, + STASIS_PLAYBACK_REWIND, + STASIS_PLAYBACK_FAST_FORWARD, + STASIS_PLAYBACK_SPEED_UP, + STASIS_PLAYBACK_SLOW_DOWN, +}; + +/*! + * \brief Play a file to the control's channel. + * + * Note that the file isn't the full path to the file. Asterisk's internal + * playback mechanism will automagically select the best format based on the + * available codecs for the channel. + * + * \param control Control for \c res_stasis. + * \param file Base filename for the file to play. + * \return Playback control object. + * \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); + +/*! + * \brief Gets the current state of a playback operation. + * + * \param playback Playback control object. + * \return The state of the \a playback object. + */ +enum stasis_app_playback_state stasis_app_playback_get_state( + struct stasis_app_playback *playback); + +/*! + * \brief Gets the unique id of a playback object. + * + * \param playback Playback control object. + * \return \a playback's id. + * \return \c NULL if \a playback ic \c NULL + */ +const char *stasis_app_playback_get_id( + struct stasis_app_playback *playback); + +/*! + * \brief Finds the playback object with the given id. + * + * \param id Id of the playback object to find. + * \return Associated \ref stasis_app_playback object. + * \return \c NULL if \a id not found. + */ +struct ast_json *stasis_app_playback_find_by_id(const char *id); + +/*! + * \brief Controls the media for a given playback operation. + * + * \param playback Playback control object. + * \param control Media control operation. + * \return 0 on success + * \return non-zero on error. + */ +int stasis_app_playback_control(struct stasis_app_playback *playback, + enum stasis_app_playback_media_control control); + +/*! + * \brief Message type for playback updates. The data is an + * \ref ast_channel_blob. + */ +struct stasis_message_type *stasis_app_playback_snapshot_type(void); + +#endif /* _ASTERISK_STASIS_APP_PLAYBACK_H */ diff --git a/include/asterisk/stasis_channels.h b/include/asterisk/stasis_channels.h index 5ead93d3a5..dace99af57 100644 --- a/include/asterisk/stasis_channels.h +++ b/include/asterisk/stasis_channels.h @@ -54,6 +54,7 @@ struct ast_channel_snapshot { AST_STRING_FIELD(caller_number); /*!< Caller ID Number */ AST_STRING_FIELD(connected_name); /*!< Connected Line Name */ AST_STRING_FIELD(connected_number); /*!< Connected Line Number */ + AST_STRING_FIELD(language); /*!< The default spoken language for the channel */ ); struct timeval creationtime; /*!< The time of channel creation */ @@ -122,6 +123,17 @@ struct stasis_message_type *ast_channel_snapshot_type(void); struct ast_channel_snapshot *ast_channel_snapshot_create( struct ast_channel *chan); +/*! + * \since 12 + * \brief Get the most recent snapshot for channel with the given \a uniqueid. + * + * \param uniqueid Uniqueid of the snapshot to fetch. + * \return Most recent channel snapshot + * \return \c NULL on error + */ +struct ast_channel_snapshot *ast_channel_snapshot_get_latest( + const char *uniqueid); + /*! * \since 12 * \brief Creates a \ref ast_channel_blob message. @@ -140,6 +152,23 @@ struct ast_channel_snapshot *ast_channel_snapshot_create( struct stasis_message *ast_channel_blob_create(struct ast_channel *chan, struct stasis_message_type *type, struct ast_json *blob); +/*! + * \since 12 + * \brief Create a \ref ast_channel_blob message, pulling channel state from + * the cache. + * + * \param uniqueid Uniqueid of the channel. + * \param type Message type for this blob. + * \param blob JSON object representing the data, or \c NULL for no data. If + * \c NULL, ast_json_null() is put into the object. + * + * \return \ref ast_channel_blob message. + * \return \c NULL on error + */ +struct stasis_message *ast_channel_blob_create_from_cache( + const char *uniqueid, struct stasis_message_type *type, + struct ast_json *blob); + /*! * \since 12 * \brief Create a \ref ast_multi_channel_blob suitable for a \ref stasis_message. @@ -221,6 +250,14 @@ struct ast_json *ast_multi_channel_blob_get_json(struct ast_multi_channel_blob * void ast_multi_channel_blob_add_channel(struct ast_multi_channel_blob *obj, const char *role, struct ast_channel_snapshot *snapshot); +/*! + * \since 12 + * \brief Publish a \ref ast_channel_snapshot for a channel. + * + * \param chan Channel to publish. + */ +void ast_channel_publish_snapshot(struct ast_channel *chan); + /*! * \since 12 * \brief Publish a \ref ast_channel_varset for a channel. diff --git a/include/asterisk/stasis_http.h b/include/asterisk/stasis_http.h index cc0ceeee4d..f81a206e67 100644 --- a/include/asterisk/stasis_http.h +++ b/include/asterisk/stasis_http.h @@ -162,6 +162,12 @@ void stasis_http_response_ok(struct stasis_http_response *response, */ void stasis_http_response_no_content(struct stasis_http_response *response); +/*! + * \brief Fill in a Created (201) \a stasis_http_response. + */ +void stasis_http_response_created(struct stasis_http_response *response, + const char *url); + /*! * \brief Fill in \a response with a 500 message for allocation failures. * \param response Response to fill in. diff --git a/main/channel_internal_api.c b/main/channel_internal_api.c index 42aaada6d3..87551f72f3 100644 --- a/main/channel_internal_api.c +++ b/main/channel_internal_api.c @@ -414,15 +414,17 @@ int ast_channel_data_cmp_structure(const struct ast_data_search *tree, /* ACCESSORS */ -#define DEFINE_STRINGFIELD_SETTERS_FOR(field) \ +#define DEFINE_STRINGFIELD_SETTERS_FOR(field, publish) \ void ast_channel_##field##_set(struct ast_channel *chan, const char *value) \ { \ ast_string_field_set(chan, field, value); \ + if (publish) ast_channel_publish_snapshot(chan); \ } \ \ void ast_channel_##field##_build_va(struct ast_channel *chan, const char *fmt, va_list ap) \ { \ ast_string_field_build_va(chan, field, fmt, ap); \ + if (publish) ast_channel_publish_snapshot(chan); \ } \ void ast_channel_##field##_build(struct ast_channel *chan, const char *fmt, ...) \ { \ @@ -430,19 +432,20 @@ void ast_channel_##field##_build(struct ast_channel *chan, const char *fmt, ...) va_start(ap, fmt); \ ast_channel_##field##_build_va(chan, fmt, ap); \ va_end(ap); \ + if (publish) ast_channel_publish_snapshot(chan); \ } -DEFINE_STRINGFIELD_SETTERS_FOR(name); -DEFINE_STRINGFIELD_SETTERS_FOR(language); -DEFINE_STRINGFIELD_SETTERS_FOR(musicclass); -DEFINE_STRINGFIELD_SETTERS_FOR(accountcode); -DEFINE_STRINGFIELD_SETTERS_FOR(peeraccount); -DEFINE_STRINGFIELD_SETTERS_FOR(userfield); -DEFINE_STRINGFIELD_SETTERS_FOR(call_forward); -DEFINE_STRINGFIELD_SETTERS_FOR(uniqueid); -DEFINE_STRINGFIELD_SETTERS_FOR(parkinglot); -DEFINE_STRINGFIELD_SETTERS_FOR(hangupsource); -DEFINE_STRINGFIELD_SETTERS_FOR(dialcontext); +DEFINE_STRINGFIELD_SETTERS_FOR(name, 0); +DEFINE_STRINGFIELD_SETTERS_FOR(language, 1); +DEFINE_STRINGFIELD_SETTERS_FOR(musicclass, 0); +DEFINE_STRINGFIELD_SETTERS_FOR(accountcode, 0); +DEFINE_STRINGFIELD_SETTERS_FOR(peeraccount, 0); +DEFINE_STRINGFIELD_SETTERS_FOR(userfield, 0); +DEFINE_STRINGFIELD_SETTERS_FOR(call_forward, 0); +DEFINE_STRINGFIELD_SETTERS_FOR(uniqueid, 0); +DEFINE_STRINGFIELD_SETTERS_FOR(parkinglot, 0); +DEFINE_STRINGFIELD_SETTERS_FOR(hangupsource, 0); +DEFINE_STRINGFIELD_SETTERS_FOR(dialcontext, 0); #define DEFINE_STRINGFIELD_GETTER_FOR(field) const char *ast_channel_##field(const struct ast_channel *chan) \ { \ diff --git a/main/stasis_channels.c b/main/stasis_channels.c index 0b2ccbc1fd..f8c9be327d 100644 --- a/main/stasis_channels.c +++ b/main/stasis_channels.c @@ -134,6 +134,7 @@ struct ast_channel_snapshot *ast_channel_snapshot_create(struct ast_channel *cha S_COR(ast_channel_connected(chan)->id.name.valid, ast_channel_connected(chan)->id.name.str, "")); ast_string_field_set(snapshot, connected_number, S_COR(ast_channel_connected(chan)->id.number.valid, ast_channel_connected(chan)->id.number.str, "")); + ast_string_field_set(snapshot, language, ast_channel_language(chan)); snapshot->creationtime = ast_channel_creationtime(chan); snapshot->state = ast_channel_state(chan); @@ -149,6 +150,28 @@ struct ast_channel_snapshot *ast_channel_snapshot_create(struct ast_channel *cha return snapshot; } +struct ast_channel_snapshot *ast_channel_snapshot_get_latest( + const char *uniqueid) +{ + RAII_VAR(struct stasis_message *, msg, NULL, ao2_cleanup); + struct ast_channel_snapshot *snapshot; + + msg = stasis_cache_get(ast_channel_topic_all_cached(), + ast_channel_snapshot_type(), uniqueid); + + if (!msg) { + return NULL; + } + + snapshot = stasis_message_data(msg); + if (!snapshot) { + return NULL; + } + + ao2_ref(snapshot, +1); + return snapshot; +} + static void publish_message_for_channel_topics(struct stasis_message *message, struct ast_channel *chan) { if (chan) { @@ -207,7 +230,8 @@ void ast_channel_publish_dial(struct ast_channel *caller, struct ast_channel *pe publish_message_for_channel_topics(msg, caller); } -struct stasis_message *ast_channel_blob_create(struct ast_channel *chan, +static struct stasis_message *channel_blob_create( + struct ast_channel_snapshot *snapshot, struct stasis_message_type *type, struct ast_json *blob) { RAII_VAR(struct ast_channel_blob *, obj, NULL, ao2_cleanup); @@ -222,11 +246,9 @@ struct stasis_message *ast_channel_blob_create(struct ast_channel *chan, return NULL; } - if (chan) { - obj->snapshot = ast_channel_snapshot_create(chan); - if (obj->snapshot == NULL) { - return NULL; - } + if (snapshot) { + ao2_ref(snapshot, +1); + obj->snapshot = snapshot; } obj->blob = ast_json_ref(blob); @@ -240,6 +262,35 @@ struct stasis_message *ast_channel_blob_create(struct ast_channel *chan, return msg; } +struct stasis_message *ast_channel_blob_create(struct ast_channel *chan, + struct stasis_message_type *type, struct ast_json *blob) +{ + RAII_VAR(struct ast_channel_snapshot *, snapshot, NULL, ao2_cleanup); + + if (chan != NULL) { + snapshot = ast_channel_snapshot_create(chan); + if (snapshot == NULL) { + return NULL; + } + } + + return channel_blob_create(snapshot, type, blob); +} + +struct stasis_message *ast_channel_blob_create_from_cache( + const char *uniqueid, struct stasis_message_type *type, + struct ast_json *blob) +{ + RAII_VAR(struct ast_channel_snapshot *, snapshot, NULL, ao2_cleanup); + + snapshot = ast_channel_snapshot_get_latest(uniqueid); + if (snapshot == NULL) { + return NULL; + } + + return channel_blob_create(snapshot, type, blob); +} + /*! \brief A channel snapshot wrapper object used in \ref ast_multi_channel_blob objects */ struct channel_role_snapshot { struct ast_channel_snapshot *snapshot; /*!< A channel snapshot */ @@ -389,6 +440,26 @@ struct ast_json *ast_multi_channel_blob_get_json(struct ast_multi_channel_blob * return obj->blob; } +void ast_channel_publish_snapshot(struct ast_channel *chan) +{ + RAII_VAR(struct ast_channel_snapshot *, snapshot, NULL, ao2_cleanup); + RAII_VAR(struct stasis_message *, message, NULL, ao2_cleanup); + + snapshot = ast_channel_snapshot_create(chan); + if (!snapshot) { + return; + } + + message = stasis_message_create(ast_channel_snapshot_type(), snapshot); + if (!message) { + return; + } + + ast_assert(ast_channel_topic(chan) != NULL); + stasis_publish(ast_channel_topic(chan), message); +} + + void ast_channel_publish_varset(struct ast_channel *chan, const char *name, const char *value) { RAII_VAR(struct stasis_message *, msg, NULL, ao2_cleanup); diff --git a/res/res_stasis_http.c b/res/res_stasis_http.c index 75a13284e4..63e2601e87 100644 --- a/res/res_stasis_http.c +++ b/res/res_stasis_http.c @@ -330,6 +330,15 @@ void stasis_http_response_alloc_failed(struct stasis_http_response *response) response->response_text = "Internal Server Error"; } +void stasis_http_response_created(struct stasis_http_response *response, + const char *url) +{ + response->message = ast_json_null(); + response->response_code = 201; + response->response_text = "Created"; + ast_str_append(&response->headers, 0, "Location: %s\r\n", url); +} + static void add_allow_header(struct stasis_rest_handlers *handler, struct stasis_http_response *response) { diff --git a/res/res_stasis_http_channels.c b/res/res_stasis_http_channels.c index aa44819034..89c0cf09c5 100644 --- a/res/res_stasis_http_channels.c +++ b/res/res_stasis_http_channels.c @@ -327,6 +327,9 @@ static void stasis_http_play_on_channel_cb( if (strcmp(i->name, "media") == 0) { args.media = (i->value); } else + if (strcmp(i->name, "lang") == 0) { + args.lang = (i->value); + } else {} } for (i = path_vars; i; i = i->next) { diff --git a/res/res_stasis_json_events.c b/res/res_stasis_json_events.c index 10d36be424..e96d84e4ff 100644 --- a/res/res_stasis_json_events.c +++ b/res/res_stasis_json_events.c @@ -116,30 +116,18 @@ struct ast_json *stasis_json_event_bridge_created_create( return ast_json_ref(message); } -struct ast_json *stasis_json_event_channel_destroyed_create( - struct ast_channel_snapshot *channel_snapshot, +struct ast_json *stasis_json_event_playback_finished_create( struct ast_json *blob ) { RAII_VAR(struct ast_json *, message, NULL, ast_json_unref); RAII_VAR(struct ast_json *, event, NULL, ast_json_unref); struct ast_json *validator; - int ret; - ast_assert(channel_snapshot != NULL); ast_assert(blob != NULL); - ast_assert(ast_json_object_get(blob, "channel") == NULL); ast_assert(ast_json_object_get(blob, "type") == NULL); - validator = ast_json_object_get(blob, "cause"); - if (validator) { - /* do validation? XXX */ - } else { - /* fail message generation if the required parameter doesn't exist */ - return NULL; - } - - validator = ast_json_object_get(blob, "cause_txt"); + validator = ast_json_object_get(blob, "playback"); if (validator) { /* do validation? XXX */ } else { @@ -152,13 +140,7 @@ struct ast_json *stasis_json_event_channel_destroyed_create( return NULL; } - ret = ast_json_object_set(event, - "channel", ast_channel_snapshot_to_json(channel_snapshot)); - if (ret) { - return NULL; - } - - message = ast_json_pack("{s: o}", "channel_destroyed", ast_json_ref(event)); + message = ast_json_pack("{s: o}", "playback_finished", ast_json_ref(event)); if (!message) { return NULL; } @@ -245,29 +227,23 @@ struct ast_json *stasis_json_event_channel_caller_id_create( return ast_json_ref(message); } -struct ast_json *stasis_json_event_channel_hangup_request_create( - struct ast_channel_snapshot *channel_snapshot, +struct ast_json *stasis_json_event_playback_started_create( struct ast_json *blob ) { RAII_VAR(struct ast_json *, message, NULL, ast_json_unref); RAII_VAR(struct ast_json *, event, NULL, ast_json_unref); struct ast_json *validator; - int ret; - ast_assert(channel_snapshot != NULL); ast_assert(blob != NULL); - ast_assert(ast_json_object_get(blob, "channel") == NULL); ast_assert(ast_json_object_get(blob, "type") == NULL); - validator = ast_json_object_get(blob, "soft"); - if (validator) { - /* do validation? XXX */ - } - - validator = ast_json_object_get(blob, "cause"); + validator = ast_json_object_get(blob, "playback"); if (validator) { /* do validation? XXX */ + } else { + /* fail message generation if the required parameter doesn't exist */ + return NULL; } event = ast_json_deep_copy(blob); @@ -275,13 +251,7 @@ struct ast_json *stasis_json_event_channel_hangup_request_create( return NULL; } - ret = ast_json_object_set(event, - "channel", ast_channel_snapshot_to_json(channel_snapshot)); - if (ret) { - return NULL; - } - - message = ast_json_pack("{s: o}", "channel_hangup_request", ast_json_ref(event)); + message = ast_json_pack("{s: o}", "playback_started", ast_json_ref(event)); if (!message) { return NULL; } @@ -350,6 +320,56 @@ struct ast_json *stasis_json_event_application_replaced_create( return ast_json_ref(message); } +struct ast_json *stasis_json_event_channel_destroyed_create( + struct ast_channel_snapshot *channel_snapshot, + struct ast_json *blob + ) +{ + RAII_VAR(struct ast_json *, message, NULL, ast_json_unref); + RAII_VAR(struct ast_json *, event, NULL, ast_json_unref); + struct ast_json *validator; + int ret; + + ast_assert(channel_snapshot != NULL); + ast_assert(blob != NULL); + ast_assert(ast_json_object_get(blob, "channel") == NULL); + ast_assert(ast_json_object_get(blob, "type") == NULL); + + validator = ast_json_object_get(blob, "cause"); + if (validator) { + /* do validation? XXX */ + } else { + /* fail message generation if the required parameter doesn't exist */ + return NULL; + } + + validator = ast_json_object_get(blob, "cause_txt"); + if (validator) { + /* do validation? XXX */ + } else { + /* fail message generation if the required parameter doesn't exist */ + return NULL; + } + + event = ast_json_deep_copy(blob); + if (!event) { + return NULL; + } + + ret = ast_json_object_set(event, + "channel", ast_channel_snapshot_to_json(channel_snapshot)); + if (ret) { + return NULL; + } + + message = ast_json_pack("{s: o}", "channel_destroyed", ast_json_ref(event)); + if (!message) { + return NULL; + } + + return ast_json_ref(message); +} + struct ast_json *stasis_json_event_channel_varset_create( struct ast_channel_snapshot *channel_snapshot, struct ast_json *blob @@ -587,6 +607,50 @@ struct ast_json *stasis_json_event_channel_state_change_create( return ast_json_ref(message); } +struct ast_json *stasis_json_event_channel_hangup_request_create( + struct ast_channel_snapshot *channel_snapshot, + struct ast_json *blob + ) +{ + RAII_VAR(struct ast_json *, message, NULL, ast_json_unref); + RAII_VAR(struct ast_json *, event, NULL, ast_json_unref); + struct ast_json *validator; + int ret; + + ast_assert(channel_snapshot != NULL); + ast_assert(blob != NULL); + ast_assert(ast_json_object_get(blob, "channel") == NULL); + ast_assert(ast_json_object_get(blob, "type") == NULL); + + validator = ast_json_object_get(blob, "soft"); + if (validator) { + /* do validation? XXX */ + } + + validator = ast_json_object_get(blob, "cause"); + if (validator) { + /* do validation? XXX */ + } + + event = ast_json_deep_copy(blob); + if (!event) { + return NULL; + } + + ret = ast_json_object_set(event, + "channel", ast_channel_snapshot_to_json(channel_snapshot)); + if (ret) { + return NULL; + } + + message = ast_json_pack("{s: o}", "channel_hangup_request", ast_json_ref(event)); + if (!message) { + return NULL; + } + + return ast_json_ref(message); +} + struct ast_json *stasis_json_event_channel_entered_bridge_create( struct ast_bridge_snapshot *bridge_snapshot, struct ast_channel_snapshot *channel_snapshot diff --git a/res/res_stasis_json_events.exports.in b/res/res_stasis_json_events.exports.in index e3f59ca763..8be4c849b3 100644 --- a/res/res_stasis_json_events.exports.in +++ b/res/res_stasis_json_events.exports.in @@ -2,18 +2,20 @@ global: LINKER_SYMBOL_PREFIXstasis_json_event_channel_userevent_create; LINKER_SYMBOL_PREFIXstasis_json_event_bridge_created_create; - LINKER_SYMBOL_PREFIXstasis_json_event_channel_destroyed_create; + LINKER_SYMBOL_PREFIXstasis_json_event_playback_finished_create; LINKER_SYMBOL_PREFIXstasis_json_event_channel_snapshot_create; LINKER_SYMBOL_PREFIXstasis_json_event_channel_caller_id_create; - LINKER_SYMBOL_PREFIXstasis_json_event_channel_hangup_request_create; + LINKER_SYMBOL_PREFIXstasis_json_event_playback_started_create; LINKER_SYMBOL_PREFIXstasis_json_event_bridge_destroyed_create; LINKER_SYMBOL_PREFIXstasis_json_event_application_replaced_create; + LINKER_SYMBOL_PREFIXstasis_json_event_channel_destroyed_create; LINKER_SYMBOL_PREFIXstasis_json_event_channel_varset_create; LINKER_SYMBOL_PREFIXstasis_json_event_channel_left_bridge_create; LINKER_SYMBOL_PREFIXstasis_json_event_channel_created_create; LINKER_SYMBOL_PREFIXstasis_json_event_stasis_start_create; LINKER_SYMBOL_PREFIXstasis_json_event_channel_dialplan_create; LINKER_SYMBOL_PREFIXstasis_json_event_channel_state_change_create; + LINKER_SYMBOL_PREFIXstasis_json_event_channel_hangup_request_create; LINKER_SYMBOL_PREFIXstasis_json_event_channel_entered_bridge_create; LINKER_SYMBOL_PREFIXstasis_json_event_channel_dtmf_received_create; LINKER_SYMBOL_PREFIXstasis_json_event_stasis_end_create; diff --git a/res/res_stasis_playback.c b/res/res_stasis_playback.c new file mode 100644 index 0000000000..5f54a14b45 --- /dev/null +++ b/res/res_stasis_playback.c @@ -0,0 +1,320 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * David M. Lee, II + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * + * \brief res_stasis playback support. + * + * \author David M. Lee, II + */ + +/*** MODULEINFO + res_stasis + core + ***/ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "$Revision$") + +#include "asterisk/app.h" +#include "asterisk/astobj2.h" +#include "asterisk/file.h" +#include "asterisk/logger.h" +#include "asterisk/module.h" +#include "asterisk/stasis_app_impl.h" +#include "asterisk/stasis_app_playback.h" +#include "asterisk/stasis_channels.h" +#include "asterisk/stringfields.h" +#include "asterisk/uuid.h" + +/*! Number of hash buckets for playback container. Keep it prime! */ +#define PLAYBACK_BUCKETS 127 + +/*! Number of milliseconds of media to skip */ +#define PLAYBACK_SKIPMS 250 + +#define SOUND_URI_SCHEME "sound:" +#define RECORDING_URI_SCHEME "recording:" + +STASIS_MESSAGE_TYPE_DEFN(stasis_app_playback_snapshot_type); + +/*! Container of all current playbacks */ +static struct ao2_container *playbacks; + +/*! Playback control object for res_stasis */ +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(language); /*!< Preferred language */ + ); + /*! Current playback state */ + enum stasis_app_playback_state state; + /*! Control object for the channel we're playing back to */ + struct stasis_app_control *control; +}; + +static int playback_hash(const void *obj, int flags) +{ + const struct stasis_app_playback *playback = obj; + const char *id = flags & OBJ_KEY ? obj : playback->id; + return ast_str_hash(id); +} + +static int playback_cmp(void *obj, void *arg, int flags) +{ + struct stasis_app_playback *lhs = obj; + struct stasis_app_playback *rhs = arg; + const char *rhs_id = flags & OBJ_KEY ? arg : rhs->id; + + if (strcmp(lhs->id, rhs_id) == 0) { + return CMP_MATCH | CMP_STOP; + } else { + return 0; + } +} + +static const char *state_to_string(enum stasis_app_playback_state state) +{ + switch (state) { + case STASIS_PLAYBACK_STATE_QUEUED: + return "queued"; + case STASIS_PLAYBACK_STATE_PLAYING: + return "playing"; + case STASIS_PLAYBACK_STATE_COMPLETE: + return "done"; + } + + return "?"; +} + +static struct ast_json *playback_to_json(struct stasis_app_playback *playback) +{ + RAII_VAR(struct ast_json *, json, NULL, ast_json_unref); + + if (playback == NULL) { + return NULL; + } + + json = ast_json_pack("{s: s, s: s, s: s, s: s}", + "id", playback->id, + "media_uri", playback->media, + "language", playback->language, + "state", state_to_string(playback->state)); + + return ast_json_ref(json); +} + +static void playback_publish(struct stasis_app_playback *playback) +{ + RAII_VAR(struct ast_json *, json, NULL, ast_json_unref); + RAII_VAR(struct ast_channel_snapshot *, snapshot, NULL, ao2_cleanup); + RAII_VAR(struct stasis_message *, message, NULL, ao2_cleanup); + + ast_assert(playback != NULL); + + json = playback_to_json(playback); + if (json == NULL) { + return; + } + + message = ast_channel_blob_create_from_cache( + stasis_app_control_get_channel_id(playback->control), + stasis_app_playback_snapshot_type(), json); + if (message == NULL) { + return; + } + + stasis_app_control_publish(playback->control, message); +} + +static void playback_set_state(struct stasis_app_playback *playback, + enum stasis_app_playback_state state) +{ + SCOPED_AO2LOCK(lock, playback); + + playback->state = state; + playback_publish(playback); +} + +static void playback_cleanup(struct stasis_app_playback *playback) +{ + playback_set_state(playback, STASIS_PLAYBACK_STATE_COMPLETE); + + ao2_unlink_flags(playbacks, playback, + OBJ_POINTER | OBJ_UNLINK | OBJ_NODATA); +} + +static void *__app_control_play_uri(struct stasis_app_control *control, + struct ast_channel *chan, void *data) +{ + RAII_VAR(struct stasis_app_playback *, playback, NULL, + playback_cleanup); + RAII_VAR(struct ast_json *, json, NULL, ast_json_unref); + const char *file; + int res; + /* Even though these local variables look fairly pointless, the avoid + * having a bunch of NULL's passed directly into + * ast_control_streamfile() */ + const char *fwd = NULL; + const char *rev = NULL; + const char *stop = NULL; + const char *pause = NULL; + const char *restart = NULL; + int skipms = PLAYBACK_SKIPMS; + long offsetms = 0; + + playback = data; + ast_assert(playback != NULL); + + playback_set_state(playback, STASIS_PLAYBACK_STATE_PLAYING); + + if (ast_channel_state(chan) != AST_STATE_UP) { + ast_answer(chan); + } + + if (ast_begins_with(playback->media, SOUND_URI_SCHEME)) { + /* Play sound */ + file = playback->media + strlen(SOUND_URI_SCHEME); + } else if (ast_begins_with(playback->media, RECORDING_URI_SCHEME)) { + /* Play recording */ + file = playback->media + strlen(RECORDING_URI_SCHEME); + } else { + /* Play URL */ + ast_log(LOG_ERROR, "Unimplemented\n"); + return NULL; + } + + res = ast_control_streamfile(chan, file, fwd, rev, stop, pause, + restart, skipms, &offsetms); + + if (res != 0) { + ast_log(LOG_WARNING, "%s: Playback failed for %s", + ast_channel_uniqueid(chan), playback->media); + } + + return NULL; +} + +static void playback_dtor(void *obj) +{ + struct stasis_app_playback *playback = obj; + + ast_string_field_free_memory(playback); +} + +struct stasis_app_playback *stasis_app_control_play_uri( + struct stasis_app_control *control, const char *uri, + const char *language) +{ + RAII_VAR(struct stasis_app_playback *, playback, NULL, ao2_cleanup); + char id[AST_UUID_STR_LEN]; + + ast_debug(3, "%s: Sending play(%s) command\n", + stasis_app_control_get_channel_id(control), uri); + + playback = ao2_alloc(sizeof(*playback), playback_dtor); + if (!playback || ast_string_field_init(playback, 128) ){ + return NULL; + } + + ast_uuid_generate_str(id, sizeof(id)); + ast_string_field_set(playback, id, id); + ast_string_field_set(playback, media, uri); + ast_string_field_set(playback, language, language); + playback->control = control; + ao2_link(playbacks, playback); + + playback_set_state(playback, STASIS_PLAYBACK_STATE_QUEUED); + + ao2_ref(playback, +1); + stasis_app_send_command_async( + control, __app_control_play_uri, playback); + + + ao2_ref(playback, +1); + return playback; +} + +enum stasis_app_playback_state stasis_app_playback_get_state( + struct stasis_app_playback *control) +{ + SCOPED_AO2LOCK(lock, control); + return control->state; +} + +const char *stasis_app_playback_get_id( + struct stasis_app_playback *control) +{ + /* id is immutable; no lock needed */ + return control->id; +} + +struct ast_json *stasis_app_playback_find_by_id(const char *id) +{ + RAII_VAR(struct stasis_app_playback *, playback, NULL, ao2_cleanup); + RAII_VAR(struct ast_json *, json, NULL, ast_json_unref); + + playback = ao2_find(playbacks, id, OBJ_KEY); + if (playback == NULL) { + return NULL; + } + + json = playback_to_json(playback); + return ast_json_ref(json); +} + +int stasis_app_playback_control(struct stasis_app_playback *playback, + enum stasis_app_playback_media_control control) +{ + SCOPED_AO2LOCK(lock, playback); + ast_assert(0); /* TODO */ + return -1; +} + +static int load_module(void) +{ + int r; + + r = STASIS_MESSAGE_TYPE_INIT(stasis_app_playback_snapshot_type); + if (r != 0) { + return AST_MODULE_LOAD_FAILURE; + } + + playbacks = ao2_container_alloc(PLAYBACK_BUCKETS, playback_hash, + playback_cmp); + if (!playbacks) { + return AST_MODULE_LOAD_FAILURE; + } + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + ao2_cleanup(playbacks); + playbacks = NULL; + STASIS_MESSAGE_TYPE_CLEANUP(stasis_app_playback_snapshot_type); + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS, + "Stasis application playback support", + .load = load_module, + .unload = unload_module, + .nonoptreq = "res_stasis"); diff --git a/res/res_stasis_playback.exports.in b/res/res_stasis_playback.exports.in new file mode 100644 index 0000000000..0ad493c49e --- /dev/null +++ b/res/res_stasis_playback.exports.in @@ -0,0 +1,6 @@ +{ + global: + LINKER_SYMBOL_PREFIXstasis_app_*; + local: + *; +}; diff --git a/res/stasis/control.c b/res/stasis/control.c index e32781b5fa..1cc818616f 100644 --- a/res/stasis/control.c +++ b/res/stasis/control.c @@ -56,7 +56,8 @@ struct stasis_app_control *control_create(struct ast_channel *channel) return NULL; } - control->command_queue = ao2_container_alloc_list(0, 0, NULL, NULL); + control->command_queue = ao2_container_alloc_list( + AO2_ALLOC_OPT_LOCK_MUTEX, 0, NULL, NULL); control->channel = channel; @@ -75,10 +76,8 @@ static struct stasis_app_command *exec_command( return NULL; } - ao2_lock(control); - ao2_ref(command, +1); + /* command_queue is a thread safe list; no lock needed */ ao2_link(control->command_queue, command); - ao2_unlock(control); ao2_ref(command, +1); return command; @@ -182,8 +181,6 @@ int control_dispatch_all(struct stasis_app_control *control, struct ao2_iterator i; void *obj; - SCOPED_AO2LOCK(lock, control); - ast_assert(control->channel == chan); i = ao2_iterator_init(control->command_queue, AO2_ITERATOR_UNLINK); diff --git a/res/stasis_http/resource_channels.c b/res/stasis_http/resource_channels.c index 3cc97c5110..4e404dc1d4 100644 --- a/res/stasis_http/resource_channels.c +++ b/res/stasis_http/resource_channels.c @@ -24,6 +24,7 @@ */ /*** MODULEINFO + res_stasis_app_playback core ***/ @@ -31,10 +32,14 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$") +#include "asterisk/file.h" #include "asterisk/stasis_app.h" +#include "asterisk/stasis_app_playback.h" #include "asterisk/stasis_channels.h" #include "resource_channels.h" +#include + /*! * \brief Finds the control object for a channel, filling the response with an * error, if appropriate. @@ -131,9 +136,53 @@ void stasis_http_unhold_channel(struct ast_variable *headers, struct ast_unhold_ { ast_log(LOG_ERROR, "TODO: stasis_http_unhold_channel\n"); } -void stasis_http_play_on_channel(struct ast_variable *headers, struct ast_play_on_channel_args *args, struct stasis_http_response *response) + +void stasis_http_play_on_channel(struct ast_variable *headers, + struct ast_play_on_channel_args *args, + struct stasis_http_response *response) { - ast_log(LOG_ERROR, "TODO: stasis_http_play_on_channel\n"); + RAII_VAR(struct stasis_app_control *, control, NULL, ao2_cleanup); + RAII_VAR(struct ast_channel_snapshot *, snapshot, NULL, ao2_cleanup); + RAII_VAR(struct stasis_app_playback *, playback, NULL, ao2_cleanup); + RAII_VAR(char *, playback_url, NULL, ast_free); + const char *language; + + ast_assert(response != NULL); + + control = find_control(response, args->channel_id); + if (control == NULL) { + /* Response filled in by find_control */ + return; + } + + snapshot = stasis_app_control_get_snapshot(control); + if (!snapshot) { + stasis_http_response_error( + response, 404, "Not Found", + "Channel not found"); + return; + } + + language = S_OR(args->lang, snapshot->language); + + playback = stasis_app_control_play_uri(control, args->media, language); + if (!playback) { + stasis_http_response_error( + response, 500, "Internal Server Error", + "Failed to answer channel"); + return; + } + + ast_asprintf(&playback_url, "/playback/%s", + stasis_app_playback_get_id(playback)); + if (!playback_url) { + stasis_http_response_error( + response, 500, "Internal Server Error", + "Out of memory"); + return; + } + + stasis_http_response_created(response, playback_url); } void stasis_http_record_channel(struct ast_variable *headers, struct ast_record_channel_args *args, struct stasis_http_response *response) { @@ -143,8 +192,8 @@ void stasis_http_get_channel(struct ast_variable *headers, struct ast_get_channel_args *args, struct stasis_http_response *response) { - RAII_VAR(struct stasis_caching_topic *, caching_topic, NULL, ao2_cleanup); RAII_VAR(struct stasis_message *, msg, NULL, ao2_cleanup); + struct stasis_caching_topic *caching_topic; struct ast_channel_snapshot *snapshot; caching_topic = ast_channel_topic_all_cached(); @@ -154,7 +203,6 @@ void stasis_http_get_channel(struct ast_variable *headers, "Message bus not initialized"); return; } - ao2_ref(caching_topic, +1); msg = stasis_cache_get(caching_topic, ast_channel_snapshot_type(), args->channel_id); diff --git a/res/stasis_http/resource_channels.h b/res/stasis_http/resource_channels.h index 2c78589b7c..8a35072f02 100644 --- a/res/stasis_http/resource_channels.h +++ b/res/stasis_http/resource_channels.h @@ -200,6 +200,8 @@ struct ast_play_on_channel_args { const char *channel_id; /*! \brief Media's URI to play. */ const char *media; + /*! \brief For sounds, selects language for sound */ + const char *lang; }; /*! * \brief Start playback of media. diff --git a/res/stasis_http/resource_playback.c b/res/stasis_http/resource_playback.c index 99f2e09eb6..f016a0095c 100644 --- a/res/stasis_http/resource_playback.c +++ b/res/stasis_http/resource_playback.c @@ -27,11 +27,22 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$") +#include "asterisk/stasis_app_playback.h" #include "resource_playback.h" -void stasis_http_get_playback(struct ast_variable *headers, struct ast_get_playback_args *args, struct stasis_http_response *response) +void stasis_http_get_playback(struct ast_variable *headers, + struct ast_get_playback_args *args, + struct stasis_http_response *response) { - ast_log(LOG_ERROR, "TODO: stasis_http_get_playback\n"); + RAII_VAR(struct ast_json *, playback, NULL, ast_json_unref); + playback = stasis_app_playback_find_by_id(args->playback_id); + if (playback == NULL) { + stasis_http_response_error(response, 404, "Not Found", + "Playback not found"); + return; + } + + stasis_http_response_ok(response, ast_json_ref(playback)); } void stasis_http_stop_playback(struct ast_variable *headers, struct ast_stop_playback_args *args, struct stasis_http_response *response) { diff --git a/res/stasis_json/resource_channels.h b/res/stasis_json/resource_channels.h index 45e1031fc1..c561d306af 100644 --- a/res/stasis_json/resource_channels.h +++ b/res/stasis_json/resource_channels.h @@ -40,7 +40,17 @@ /* * JSON models * + * CallerID + * - name: string (required) + * - number: string (required) + * Dialed * Originated + * Playback + * - language: string + * - media_uri: string (required) + * - id: string (required) + * - target_uri: string (required) + * - state: string (required) * DialplanCEP * - priority: long (required) * - exten: string (required) @@ -61,10 +71,6 @@ * - hangupsource: string (required) * - dialplan: DialplanCEP (required) * - data: string (required) - * CallerID - * - name: string (required) - * - number: string (required) - * Dialed */ #endif /* _ASTERISK_RESOURCE_CHANNELS_H */ diff --git a/res/stasis_json/resource_events.h b/res/stasis_json/resource_events.h index 63abe0f85d..d631817881 100644 --- a/res/stasis_json/resource_events.h +++ b/res/stasis_json/resource_events.h @@ -68,18 +68,15 @@ struct ast_json *stasis_json_event_bridge_created_create( ); /*! - * \brief Notification that a channel has been destroyed. + * \brief Event showing the completion of a media playback operation. * - * \param channel The channel to be used to generate this event * \param blob JSON blob containing the following parameters: - * - cause: integer - Integer representation of the cause of the hangup (required) - * - cause_txt: string - Text representation of the cause of the hangup (required) + * - playback: Playback - Playback control object (required) * * \retval NULL on error * \retval JSON (ast_json) describing the event */ -struct ast_json *stasis_json_event_channel_destroyed_create( - struct ast_channel_snapshot *channel_snapshot, +struct ast_json *stasis_json_event_playback_finished_create( struct ast_json *blob ); @@ -112,18 +109,15 @@ struct ast_json *stasis_json_event_channel_caller_id_create( ); /*! - * \brief A hangup was requested on the channel. + * \brief Event showing the start of a media playback operation. * - * \param channel The channel on which the hangup was requested. * \param blob JSON blob containing the following parameters: - * - soft: boolean - Whether the hangup request was a soft hangup request. - * - cause: integer - Integer representation of the cause of the hangup. + * - playback: Playback - Playback control object (required) * * \retval NULL on error * \retval JSON (ast_json) describing the event */ -struct ast_json *stasis_json_event_channel_hangup_request_create( - struct ast_channel_snapshot *channel_snapshot, +struct ast_json *stasis_json_event_playback_started_create( struct ast_json *blob ); @@ -152,6 +146,22 @@ struct ast_json *stasis_json_event_application_replaced_create( struct ast_json *blob ); +/*! + * \brief Notification that a channel has been destroyed. + * + * \param channel The channel to be used to generate this event + * \param blob JSON blob containing the following parameters: + * - cause: integer - Integer representation of the cause of the hangup (required) + * - cause_txt: string - Text representation of the cause of the hangup (required) + * + * \retval NULL on error + * \retval JSON (ast_json) describing the event + */ +struct ast_json *stasis_json_event_channel_destroyed_create( + struct ast_channel_snapshot *channel_snapshot, + struct ast_json *blob + ); + /*! * \brief Channel variable changed. * @@ -237,6 +247,22 @@ struct ast_json *stasis_json_event_channel_state_change_create( struct ast_channel_snapshot *channel_snapshot ); +/*! + * \brief A hangup was requested on the channel. + * + * \param channel The channel on which the hangup was requested. + * \param blob JSON blob containing the following parameters: + * - soft: boolean - Whether the hangup request was a soft hangup request. + * - cause: integer - Integer representation of the cause of the hangup. + * + * \retval NULL on error + * \retval JSON (ast_json) describing the event + */ +struct ast_json *stasis_json_event_channel_hangup_request_create( + struct ast_channel_snapshot *channel_snapshot, + struct ast_json *blob + ); + /*! * \brief Notification that a channel has entered a bridge. * @@ -284,19 +310,20 @@ struct ast_json *stasis_json_event_stasis_end_create( * ChannelUserevent * - eventname: string (required) * BridgeCreated - * ChannelDestroyed - * - cause: integer (required) - * - cause_txt: string (required) + * PlaybackFinished + * - playback: Playback (required) * ChannelSnapshot * ChannelCallerId * - caller_presentation_txt: string (required) * - caller_presentation: integer (required) - * ChannelHangupRequest - * - soft: boolean - * - cause: integer + * PlaybackStarted + * - playback: Playback (required) * BridgeDestroyed * ApplicationReplaced * - application: string (required) + * ChannelDestroyed + * - cause: integer (required) + * - cause_txt: string (required) * ChannelVarset * - variable: string (required) * - value: string (required) @@ -308,6 +335,9 @@ struct ast_json *stasis_json_event_stasis_end_create( * - application: string (required) * - application_data: string (required) * ChannelStateChange + * ChannelHangupRequest + * - soft: boolean + * - cause: integer * ChannelEnteredBridge * ChannelDtmfReceived * - digit: string (required) @@ -325,10 +355,12 @@ struct ast_json *stasis_json_event_stasis_end_create( * - application: string (required) * - channel_hangup_request: ChannelHangupRequest * - channel_userevent: ChannelUserevent + * - playback_started: PlaybackStarted * - channel_snapshot: ChannelSnapshot * - channel_dtmf_received: ChannelDtmfReceived * - channel_caller_id: ChannelCallerId * - bridge_destroyed: BridgeDestroyed + * - playback_finished: PlaybackFinished * - stasis_end: StasisEnd * StasisEnd */ diff --git a/rest-api/api-docs/channels.json b/rest-api/api-docs/channels.json index c2d77b22c3..3b4d4d486c 100644 --- a/rest-api/api-docs/channels.json +++ b/rest-api/api-docs/channels.json @@ -405,6 +405,14 @@ "required": true, "allowMultiple": false, "dataType": "string" + }, + { + "name": "lang", + "description": "For sounds, selects language for sound", + "paramType": "query", + "required": false, + "allowMultiple": false, + "dataType": "string" } ], "errorResponses": [ @@ -640,6 +648,44 @@ "description": "Timestamp when channel was created" } } + }, + "Playback": { + "id": "Playback", + "description": "Object representing the playback of media to a channel", + "properties": { + "id": { + "type": "string", + "description": "ID for this playback operation", + "required": true + }, + "media_uri": { + "type": "string", + "description": "URI for the media to play back.", + "required": true + }, + "target_uri": { + "type": "string", + "description": "URI for the channel or bridge to play the media on", + "required": true + }, + "language": { + "type": "string", + "description": "For media types that support multiple languages, the language requested for playback." + }, + "state": { + "type": "string", + "description": "Current state of the playback operation.", + "required": true, + "allowableValues": { + "valueType": "LIST", + "values": [ + "queued", + "playing", + "complete" + ] + } + } + } } } } diff --git a/rest-api/api-docs/events.json b/rest-api/api-docs/events.json index 4a36da3b8d..0e0a822cf0 100644 --- a/rest-api/api-docs/events.json +++ b/rest-api/api-docs/events.json @@ -69,7 +69,31 @@ "channel_hangup_request": { "type": "ChannelHangupRequest" }, "channel_varset": { "type": "ChannelVarset" }, "stasis_end": { "type": "StasisEnd" }, - "stasis_start": { "type": "StasisStart" } + "stasis_start": { "type": "StasisStart" }, + "playback_started": { "type": "PlaybackStarted" }, + "playback_finished": { "type": "PlaybackFinished" } + } + }, + "PlaybackStarted": { + "id": "PlaybackStarted", + "description": "Event showing the start of a media playback operation.", + "properties": { + "playback": { + "type": "Playback", + "description": "Playback control object", + "required": true + } + } + }, + "PlaybackFinished": { + "id": "PlaybackFinished", + "description": "Event showing the completion of a media playback operation.", + "properties": { + "playback": { + "type": "Playback", + "description": "Playback control object", + "required": true + } } }, "ApplicationReplaced": {