ARI: Bridge Playback, Bridge Record

Adds a new channel driver for creating channels for specific purposes
in bridges, primarily to act as either recorders or announcers. Adds
ARI commands for playing announcements to ever participant in a bridge
as well as for recording a bridge. This patch also includes some
documentation/reponse fixes to related ARI models such as playback
controls.

(closes issue ASTERISK-21592)
Reported by: Matt Jordan

(closes issue ASTERISK-21593)
Reported by: Matt Jordan

Review: https://reviewboard.asterisk.org/r/2670/


git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@394809 65c4cc65-6c06-0410-ace0-fbb531ad65f3
changes/78/78/1
Jonathan Rose 12 years ago
parent 5a8f32703c
commit 17c546173f

@ -0,0 +1,218 @@
/*
* Asterisk -- An open source telephony toolkit.
*
* Copyright (C) 2013 Digium, Inc.
*
* Jonathan Rose <jrose@digium.com>
*
* 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 Bridge Media Channels driver
*
* \author Jonathan Rose <jrose@digium.com>
* \author Richard Mudgett <rmudgett@digium.com>
*
* \brief Bridge Media Channels
*
* \ingroup channel_drivers
*/
/*** MODULEINFO
<support_level>core</support_level>
***/
#include "asterisk.h"
ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
#include "asterisk/channel.h"
#include "asterisk/bridging.h"
#include "asterisk/core_unreal.h"
#include "asterisk/module.h"
static int media_call(struct ast_channel *chan, const char *addr, int timeout)
{
/* ast_call() will fail unconditionally against channels provided by this driver */
return -1;
}
static int media_hangup(struct ast_channel *ast)
{
struct ast_unreal_pvt *p = ast_channel_tech_pvt(ast);
int res;
if (!p) {
return -1;
}
/* Give the pvt a ref to fulfill calling requirements. */
ao2_ref(p, +1);
res = ast_unreal_hangup(p, ast);
ao2_ref(p, -1);
return res;
}
static struct ast_channel *announce_request(const char *type, struct ast_format_cap *cap,
const struct ast_channel *requestor, const char *data, int *cause);
static struct ast_channel *record_request(const char *type, struct ast_format_cap *cap,
const struct ast_channel *requestor, const char *data, int *cause);
static struct ast_channel_tech announce_tech = {
.type = "Announcer",
.description = "Bridge Media Announcing Channel Driver",
.requester = announce_request,
.call = media_call,
.hangup = media_hangup,
.send_digit_begin = ast_unreal_digit_begin,
.send_digit_end = ast_unreal_digit_end,
.read = ast_unreal_read,
.write = ast_unreal_write,
.write_video = ast_unreal_write,
.exception = ast_unreal_read,
.indicate = ast_unreal_indicate,
.fixup = ast_unreal_fixup,
.send_html = ast_unreal_sendhtml,
.send_text = ast_unreal_sendtext,
.queryoption = ast_unreal_queryoption,
.setoption = ast_unreal_setoption,
.properties = AST_CHAN_TP_ANNOUNCER,
};
static struct ast_channel_tech record_tech = {
.type = "Recorder",
.description = "Bridge Media Recording Channel Driver",
.requester = record_request,
.call = media_call,
.hangup = media_hangup,
.send_digit_begin = ast_unreal_digit_begin,
.send_digit_end = ast_unreal_digit_end,
.read = ast_unreal_read,
.write = ast_unreal_write,
.write_video = ast_unreal_write,
.exception = ast_unreal_read,
.indicate = ast_unreal_indicate,
.fixup = ast_unreal_fixup,
.send_html = ast_unreal_sendhtml,
.send_text = ast_unreal_sendtext,
.queryoption = ast_unreal_queryoption,
.setoption = ast_unreal_setoption,
.properties = AST_CHAN_TP_RECORDER,
};
static struct ast_channel *media_request_helper(struct ast_format_cap *cap,
const struct ast_channel *requestor, const char *data, struct ast_channel_tech *tech, const char *role)
{
struct ast_channel *chan;
RAII_VAR(struct ast_callid *, callid, NULL, ast_callid_cleanup);
RAII_VAR(struct ast_unreal_pvt *, pvt, NULL, ao2_cleanup);
if (!(pvt = ast_unreal_alloc(sizeof(*pvt), ast_unreal_destructor, cap))) {
return NULL;
}
ast_copy_string(pvt->name, data, sizeof(pvt->name));
ast_set_flag(pvt, AST_UNREAL_NO_OPTIMIZATION);
callid = ast_read_threadstorage_callid();
chan = ast_unreal_new_channels(pvt, tech,
AST_STATE_UP, AST_STATE_UP, NULL, NULL, requestor, callid);
if (!chan) {
return NULL;
}
ast_answer(pvt->owner);
ast_answer(pvt->chan);
if (ast_channel_add_bridge_role(pvt->chan, role)) {
ast_hangup(chan);
return NULL;
}
return chan;
}
static struct ast_channel *announce_request(const char *type, struct ast_format_cap *cap,
const struct ast_channel *requestor, const char *data, int *cause)
{
return media_request_helper(cap, requestor, data, &announce_tech, "announcer");
}
static struct ast_channel *record_request(const char *type, struct ast_format_cap *cap,
const struct ast_channel *requestor, const char *data, int *cause)
{
return media_request_helper(cap, requestor, data, &record_tech, "recorder");
}
static void cleanup_capabilities(void)
{
if (announce_tech.capabilities) {
announce_tech.capabilities = ast_format_cap_destroy(announce_tech.capabilities);
}
if (record_tech.capabilities) {
record_tech.capabilities = ast_format_cap_destroy(record_tech.capabilities);
}
}
static int unload_module(void)
{
ast_channel_unregister(&announce_tech);
ast_channel_unregister(&record_tech);
cleanup_capabilities();
return 0;
}
static int load_module(void)
{
announce_tech.capabilities = ast_format_cap_alloc();
if (!announce_tech.capabilities) {
return AST_MODULE_LOAD_DECLINE;
}
record_tech.capabilities = ast_format_cap_alloc();
if (!record_tech.capabilities) {
return AST_MODULE_LOAD_DECLINE;
}
ast_format_cap_add_all(announce_tech.capabilities);
ast_format_cap_add_all(record_tech.capabilities);
if (ast_channel_register(&announce_tech)) {
ast_log(LOG_ERROR, "Unable to register channel technology %s(%s).\n",
announce_tech.type, announce_tech.description);
cleanup_capabilities();
return AST_MODULE_LOAD_DECLINE;
}
if (ast_channel_register(&record_tech)) {
ast_log(LOG_ERROR, "Unable to register channel technology %s(%s).\n",
record_tech.type, record_tech.description);
cleanup_capabilities();
return AST_MODULE_LOAD_DECLINE;
}
return AST_MODULE_LOAD_SUCCESS;
}
AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "Bridge Media Channel Driver",
.load = load_module,
.unload = unload_module,
);

@ -31,6 +31,7 @@
#include "asterisk/astobj2.h"
#include "asterisk/channel.h"
#include "asterisk/bridging.h"
#include "asterisk/abstract_jb.h"
#if defined(__cplusplus) || defined(c_plusplus)
@ -208,6 +209,20 @@ struct ast_channel *ast_unreal_new_channels(struct ast_unreal_pvt *p,
*/
void ast_unreal_call_setup(struct ast_channel *semi1, struct ast_channel *semi2);
/*!
* \brief Push the semi2 unreal channel into a bridge from either member of the unreal pair
* \since 12.0.0
*
* \param ast A member of the unreal channel being pushed
* \param bridge Which bridge we want to push the channel to
*
* \retval 0 if the channel is successfully imparted onto the bridge
* \retval -1 on failure
*
* \note This is equivalent to ast_call() on unreal based channel drivers that are designed to use it instead.
*/
int ast_unreal_channel_push_to_bridge(struct ast_channel *ast, struct ast_bridge *bridge);
/* ------------------------------------------------------------------- */
#if defined(__cplusplus) || defined(c_plusplus)

@ -281,7 +281,16 @@ struct ast_callid *ast_read_threadstorage_callid(void);
*
* \retval NULL always
*/
#define ast_callid_unref(c) ({ ao2_ref(c, -1); (NULL); })
#define ast_callid_unref(c) ({ ao2_ref(c, -1); (struct ast_callid *) (NULL); })
/*!
* \brief Cleanup a callid reference (NULL safe ao2 unreference)
*
* \param c the ast_callid
*
* \retval NULL always
*/
#define ast_callid_cleanup(c) ({ ao2_cleanup(c); (struct ast_callid *) (NULL); })
/*!
* \brief Sets what is stored in the thread storage to the given

@ -126,6 +126,29 @@ struct stasis_app_control *stasis_app_control_find_by_channel(
struct stasis_app_control *stasis_app_control_find_by_channel_id(
const char *channel_id);
/*!
* \brief Creates a control handler for a channel that isn't in a stasis app.
* \since 12.0.0
*
* \param chan Channel to create controller handle for
*
* \return NULL on failure to create the handle
* \return Pointer to \c res_stasis handler.
*/
struct stasis_app_control *stasis_app_control_create(
struct ast_channel *chan);
/*!
* \brief Act on a stasis app control queue until it is empty
* \since 12.0.0
*
* \param chan Channel to handle
* \param control Control object to execute
*/
void stasis_app_control_execute_until_exhausted(
struct ast_channel *chan,
struct stasis_app_control *control);
/*!
* \brief Returns the uniqueid of the channel associated with this control
*

@ -69,6 +69,13 @@ enum stasis_app_playback_media_operation {
STASIS_PLAYBACK_MEDIA_OP_MAX,
};
enum stasis_app_playback_target_type {
/*! The target is a channel */
STASIS_PLAYBACK_TARGET_CHANNEL = 0,
/*! The target is a bridge */
STASIS_PLAYBACK_TARGET_BRIDGE,
};
/*!
* \brief Play a file to the control's channel.
*
@ -79,6 +86,8 @@ enum stasis_app_playback_media_operation {
* \param control Control for \c res_stasis.
* \param file Base filename for the file to play.
* \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
* \param skipms Number of milliseconds to skip for forward/reverse operations.
* \param offsetms Number of milliseconds to skip before playing.
* \return Playback control object.
@ -86,7 +95,9 @@ enum stasis_app_playback_media_operation {
*/
struct stasis_app_playback *stasis_app_control_play_uri(
struct stasis_app_control *control, const char *file,
const char *language, int skipms, long offsetms);
const char *language, const char *target_id,
enum stasis_app_playback_target_type target_type,
int skipms, long offsetms);
/*!
* \brief Gets the current state of a playback operation.

@ -668,6 +668,96 @@ void ast_unreal_call_setup(struct ast_channel *semi1, struct ast_channel *semi2)
ast_channel_datastore_inherit(semi1, semi2);
}
int ast_unreal_channel_push_to_bridge(struct ast_channel *ast, struct ast_bridge *bridge)
{
struct ast_bridge_features *features;
struct ast_channel *chan;
struct ast_channel *owner;
RAII_VAR(struct ast_unreal_pvt *, p, NULL, ao2_cleanup);
RAII_VAR(struct ast_callid *, bridge_callid, NULL, ast_callid_cleanup);
ast_bridge_lock(bridge);
bridge_callid = bridge->callid ? ast_callid_ref(bridge->callid) : NULL;
ast_bridge_unlock(bridge);
{
SCOPED_CHANNELLOCK(lock, ast);
p = ast_channel_tech_pvt(ast);
if (!p) {
return -1;
}
ao2_ref(p, +1);
}
{
SCOPED_AO2LOCK(lock, p);
chan = p->chan;
if (!chan) {
return -1;
}
owner = p->owner;
if (!owner) {
return -1;
}
ast_channel_ref(chan);
ast_channel_ref(owner);
}
if (bridge_callid) {
struct ast_callid *chan_callid;
struct ast_callid *owner_callid;
/* chan side call ID setting */
ast_channel_lock(chan);
chan_callid = ast_channel_callid(chan);
if (!chan_callid) {
ast_channel_callid_set(chan, bridge_callid);
}
ast_channel_unlock(chan);
ast_callid_cleanup(chan_callid);
/* owner side call ID setting */
ast_channel_lock(owner);
owner_callid = ast_channel_callid(owner);
if (!owner_callid) {
ast_channel_callid_set(owner, bridge_callid);
}
ast_channel_unlock(owner);
ast_callid_cleanup(owner_callid);
}
/* We are done with the owner now that its call ID matches the bridge */
ast_channel_unref(owner);
owner = NULL;
features = ast_bridge_features_new();
if (!features) {
ast_channel_unref(chan);
return -1;
}
ast_set_flag(&features->feature_flags, AST_BRIDGE_CHANNEL_FLAG_IMMOVABLE);
/* Impart the semi2 channel into the bridge */
if (ast_bridge_impart(bridge, chan, NULL, features, 1)) {
ast_bridge_features_destroy(features);
ast_channel_unref(chan);
return -1;
}
ao2_lock(p);
ast_set_flag(p, AST_UNREAL_CARETAKER_THREAD);
ao2_unlock(p);
ast_channel_unref(chan);
return 0;
}
int ast_unreal_hangup(struct ast_unreal_pvt *p, struct ast_channel *ast)
{
int hangup_chan = 0;

@ -146,6 +146,11 @@ static int control_compare(void *lhs, void *rhs, int flags)
}
}
struct stasis_app_control *stasis_app_control_create(struct ast_channel *chan)
{
return control_create(chan);
}
struct stasis_app_control *stasis_app_control_find_by_channel(
const struct ast_channel *chan)
{
@ -531,6 +536,16 @@ int app_send_end_msg(struct app *app, struct ast_channel *chan)
return 0;
}
void stasis_app_control_execute_until_exhausted(struct ast_channel *chan, struct stasis_app_control *control)
{
while (!control_is_done(control)) {
int command_count = control_dispatch_all(control, chan);
if (command_count == 0 || ast_channel_fdno(chan) == -1) {
break;
}
}
}
/*! /brief Stasis dialplan application callback */
int stasis_app_exec(struct ast_channel *chan, const char *app_name, int argc,
char *argv[])
@ -750,7 +765,7 @@ static struct ast_json *simple_bridge_channel_event(
struct ast_channel_snapshot *channel_snapshot,
const struct timeval *tv)
{
return ast_json_pack("{s: s, s: o, s: o}",
return ast_json_pack("{s: s, s: o, s: o, s: o}",
"type", type,
"timestamp", ast_json_timeval(*tv, NULL),
"bridge", ast_bridge_snapshot_to_json(bridge_snapshot),

@ -357,6 +357,73 @@ static void stasis_http_remove_channel_from_bridge_cb(
}
#endif /* AST_DEVMODE */
}
/*!
* \brief Parameter parsing callback for /bridges/{bridgeId}/play.
* \param get_params GET parameters in the HTTP request.
* \param path_vars Path variables extracted from the request.
* \param headers HTTP headers.
* \param[out] response Response to the HTTP request.
*/
static void stasis_http_play_on_bridge_cb(
struct ast_variable *get_params, struct ast_variable *path_vars,
struct ast_variable *headers, struct stasis_http_response *response)
{
#if defined(AST_DEVMODE)
int is_valid;
int code;
#endif /* AST_DEVMODE */
struct ast_play_on_bridge_args args = {};
struct ast_variable *i;
for (i = get_params; i; i = i->next) {
if (strcmp(i->name, "media") == 0) {
args.media = (i->value);
} else
if (strcmp(i->name, "lang") == 0) {
args.lang = (i->value);
} else
if (strcmp(i->name, "offsetms") == 0) {
args.offsetms = atoi(i->value);
} else
if (strcmp(i->name, "skipms") == 0) {
args.skipms = atoi(i->value);
} else
{}
}
for (i = path_vars; i; i = i->next) {
if (strcmp(i->name, "bridgeId") == 0) {
args.bridge_id = (i->value);
} else
{}
}
stasis_http_play_on_bridge(headers, &args, response);
#if defined(AST_DEVMODE)
code = response->response_code;
switch (code) {
case 500: /* Internal server error */
case 404: /* Bridge not found */
case 409: /* Bridge not in a Stasis application */
is_valid = 1;
break;
default:
if (200 <= code && code <= 299) {
is_valid = ari_validate_playback(
response->message);
} else {
ast_log(LOG_ERROR, "Invalid error response %d for /bridges/{bridgeId}/play\n", code);
is_valid = 0;
}
}
if (!is_valid) {
ast_log(LOG_ERROR, "Response validation failed for /bridges/{bridgeId}/play\n");
stasis_http_response_error(response, 500,
"Internal Server Error", "Response validation failed");
}
#endif /* AST_DEVMODE */
}
/*!
* \brief Parameter parsing callback for /bridges/{bridgeId}/record.
* \param get_params GET parameters in the HTTP request.
@ -380,14 +447,17 @@ static void stasis_http_record_bridge_cb(
if (strcmp(i->name, "name") == 0) {
args.name = (i->value);
} else
if (strcmp(i->name, "format") == 0) {
args.format = (i->value);
} else
if (strcmp(i->name, "maxDurationSeconds") == 0) {
args.max_duration_seconds = atoi(i->value);
} else
if (strcmp(i->name, "maxSilenceSeconds") == 0) {
args.max_silence_seconds = atoi(i->value);
} else
if (strcmp(i->name, "append") == 0) {
args.append = ast_true(i->value);
if (strcmp(i->name, "ifExists") == 0) {
args.if_exists = (i->value);
} else
if (strcmp(i->name, "beep") == 0) {
args.beep = ast_true(i->value);
@ -448,6 +518,15 @@ static struct stasis_rest_handlers bridges_bridgeId_removeChannel = {
.children = { }
};
/*! \brief REST handler for /api-docs/bridges.{format} */
static struct stasis_rest_handlers bridges_bridgeId_play = {
.path_segment = "play",
.callbacks = {
[AST_HTTP_POST] = stasis_http_play_on_bridge_cb,
},
.num_children = 0,
.children = { }
};
/*! \brief REST handler for /api-docs/bridges.{format} */
static struct stasis_rest_handlers bridges_bridgeId_record = {
.path_segment = "record",
.callbacks = {
@ -464,8 +543,8 @@ static struct stasis_rest_handlers bridges_bridgeId = {
[AST_HTTP_GET] = stasis_http_get_bridge_cb,
[AST_HTTP_DELETE] = stasis_http_delete_bridge_cb,
},
.num_children = 3,
.children = { &bridges_bridgeId_addChannel,&bridges_bridgeId_removeChannel,&bridges_bridgeId_record, }
.num_children = 4,
.children = { &bridges_bridgeId_addChannel,&bridges_bridgeId_removeChannel,&bridges_bridgeId_play,&bridges_bridgeId_record, }
};
/*! \brief REST handler for /api-docs/bridges.{format} */
static struct stasis_rest_handlers bridges = {

@ -796,7 +796,7 @@ static void stasis_http_record_channel_cb(
break;
default:
if (200 <= code && code <= 299) {
is_valid = ari_validate_void(
is_valid = ari_validate_live_recording(
response->message);
} else {
ast_log(LOG_ERROR, "Invalid error response %d for /channels/{channelId}/record\n", code);

@ -192,7 +192,7 @@ static void stasis_http_control_playback_cb(
break;
default:
if (200 <= code && code <= 299) {
is_valid = ari_validate_playback(
is_valid = ari_validate_void(
response->message);
} else {
ast_log(LOG_ERROR, "Invalid error response %d for /playback/{playbackId}/control\n", code);

@ -64,6 +64,7 @@ struct stasis_app_playback {
AST_STRING_FIELD(id); /*!< Playback unique id */
AST_STRING_FIELD(media); /*!< Playback media uri */
AST_STRING_FIELD(language); /*!< Preferred language */
AST_STRING_FIELD(target); /*!< Playback device uri */
);
/*! Control object for the channel we're playing back to */
struct stasis_app_control *control;
@ -263,9 +264,31 @@ static void playback_dtor(void *obj)
ast_string_field_free_memory(playback);
}
static void set_target_uri(
struct stasis_app_playback *playback,
enum stasis_app_playback_target_type target_type,
const char *target_id)
{
const char *type = NULL;
switch (target_type) {
case STASIS_PLAYBACK_TARGET_CHANNEL:
type = "channel";
break;
case STASIS_PLAYBACK_TARGET_BRIDGE:
type = "bridge";
break;
}
ast_assert(type != NULL);
ast_string_field_build(playback, target, "%s:%s", type, target_id);
}
struct stasis_app_playback *stasis_app_control_play_uri(
struct stasis_app_control *control, const char *uri,
const char *language, int skipms, long offsetms)
const char *language, const char *target_id,
enum stasis_app_playback_target_type target_type,
int skipms, long offsetms)
{
RAII_VAR(struct stasis_app_playback *, playback, NULL, ao2_cleanup);
char id[AST_UUID_STR_LEN];
@ -290,6 +313,7 @@ struct stasis_app_playback *stasis_app_control_play_uri(
ast_string_field_set(playback, id, id);
ast_string_field_set(playback, media, uri);
ast_string_field_set(playback, language, language);
set_target_uri(playback, target_type, target_id);
playback->control = control;
playback->skipms = skipms;
playback->offsetms = offsetms;
@ -342,9 +366,10 @@ struct ast_json *stasis_app_playback_to_json(
return NULL;
}
json = ast_json_pack("{s: s, s: s, s: s, s: s}",
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));

@ -65,6 +65,11 @@ struct stasis_app_control *control_create(struct ast_channel *channel)
control->command_queue = ao2_container_alloc_list(
AO2_ALLOC_OPT_LOCK_MUTEX, 0, NULL, NULL);
if (!control->command_queue) {
ao2_cleanup(control);
return NULL;
}
control->channel = channel;
return control;

@ -578,16 +578,38 @@ int ari_validate_live_recording(struct ast_json *json)
{
int res = 1;
struct ast_json_iter *iter;
int has_id = 0;
int has_format = 0;
int has_name = 0;
int has_state = 0;
for (iter = ast_json_object_iter(json); iter; iter = ast_json_object_iter_next(json, iter)) {
if (strcmp("id", ast_json_object_iter_key(iter)) == 0) {
if (strcmp("format", ast_json_object_iter_key(iter)) == 0) {
int prop_is_valid;
has_id = 1;
has_format = 1;
prop_is_valid = ari_validate_string(
ast_json_object_iter_value(iter));
if (!prop_is_valid) {
ast_log(LOG_ERROR, "ARI LiveRecording field format failed validation\n");
res = 0;
}
} else
if (strcmp("name", ast_json_object_iter_key(iter)) == 0) {
int prop_is_valid;
has_name = 1;
prop_is_valid = ari_validate_string(
ast_json_object_iter_value(iter));
if (!prop_is_valid) {
ast_log(LOG_ERROR, "ARI LiveRecording field name failed validation\n");
res = 0;
}
} else
if (strcmp("state", ast_json_object_iter_key(iter)) == 0) {
int prop_is_valid;
has_state = 1;
prop_is_valid = ari_validate_string(
ast_json_object_iter_value(iter));
if (!prop_is_valid) {
ast_log(LOG_ERROR, "ARI LiveRecording field id failed validation\n");
ast_log(LOG_ERROR, "ARI LiveRecording field state failed validation\n");
res = 0;
}
} else
@ -599,8 +621,18 @@ int ari_validate_live_recording(struct ast_json *json)
}
}
if (!has_id) {
ast_log(LOG_ERROR, "ARI LiveRecording missing required field id\n");
if (!has_format) {
ast_log(LOG_ERROR, "ARI LiveRecording missing required field format\n");
res = 0;
}
if (!has_name) {
ast_log(LOG_ERROR, "ARI LiveRecording missing required field name\n");
res = 0;
}
if (!has_state) {
ast_log(LOG_ERROR, "ARI LiveRecording missing required field state\n");
res = 0;
}

@ -816,7 +816,9 @@ ari_validator ari_validate_stasis_start_fn(void);
* - id: string (required)
* - technology: string (required)
* LiveRecording
* - id: string (required)
* - format: string (required)
* - name: string (required)
* - state: string (required)
* StoredRecording
* - duration_seconds: int
* - formats: List[string] (required)

@ -35,8 +35,14 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
#include "asterisk/stasis.h"
#include "asterisk/stasis_bridging.h"
#include "asterisk/stasis_app.h"
#include "asterisk/stasis_app_playback.h"
#include "asterisk/stasis_app_recording.h"
#include "asterisk/stasis_channels.h"
#include "asterisk/core_unreal.h"
#include "asterisk/channel.h"
#include "asterisk/bridging.h"
#include "asterisk/format_cap.h"
#include "asterisk/file.h"
/*!
* \brief Finds a bridge, filling the response with an error, if appropriate.
@ -144,9 +150,275 @@ void stasis_http_remove_channel_from_bridge(struct ast_variable *headers, struct
stasis_http_response_no_content(response);
}
struct bridge_channel_control_thread_data {
struct ast_channel *bridge_channel;
struct stasis_app_control *control;
};
static void *bridge_channel_control_thread(void *data)
{
struct bridge_channel_control_thread_data *thread_data = data;
struct ast_channel *bridge_channel = thread_data->bridge_channel;
struct stasis_app_control *control = thread_data->control;
RAII_VAR(struct ast_callid *, callid, ast_channel_callid(bridge_channel), ast_callid_cleanup);
if (callid) {
ast_callid_threadassoc_add(callid);
}
ast_free(thread_data);
thread_data = NULL;
stasis_app_control_execute_until_exhausted(bridge_channel, control);
ast_hangup(bridge_channel);
ao2_cleanup(control);
return NULL;
}
static struct ast_channel *prepare_bridge_media_channel(const char *type)
{
RAII_VAR(struct ast_format_cap *, cap, NULL, ast_format_cap_destroy);
struct ast_format format;
cap = ast_format_cap_alloc_nolock();
if (!cap) {
return NULL;
}
ast_format_cap_add(cap, ast_format_set(&format, AST_FORMAT_SLINEAR, 0));
if (!cap) {
return NULL;
}
return ast_request(type, cap, NULL, "ARI", NULL);
}
void stasis_http_play_on_bridge(struct ast_variable *headers, struct ast_play_on_bridge_args *args, struct stasis_http_response *response)
{
RAII_VAR(struct ast_bridge *, bridge, find_bridge(response, args->bridge_id), ao2_cleanup);
RAII_VAR(struct ast_channel *, play_channel, NULL, ast_hangup);
RAII_VAR(struct stasis_app_control *, control, NULL, ao2_cleanup);
RAII_VAR(struct ast_channel_snapshot *, snapshot, NULL, ao2_cleanup);
RAII_VAR(struct stasis_app_playback *, playback, NULL, ao2_cleanup);
RAII_VAR(char *, playback_url, NULL, ast_free);
RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
struct bridge_channel_control_thread_data *thread_data;
const char *language;
pthread_t threadid;
ast_assert(response != NULL);
if (!bridge) {
return;
}
if (!(play_channel = prepare_bridge_media_channel("Announcer"))) {
stasis_http_response_error(
response, 500, "Internal Error", "Could not create playback channel");
return;
}
ast_debug(1, "Created announcer channel '%s'\n", ast_channel_name(play_channel));
if (ast_unreal_channel_push_to_bridge(play_channel, bridge)) {
stasis_http_response_error(
response, 500, "Internal Error", "Failed to put playback channel into the bridge");
return;
}
control = stasis_app_control_create(play_channel);
if (control == NULL) {
stasis_http_response_alloc_failed(response);
return;
}
snapshot = stasis_app_control_get_snapshot(control);
if (!snapshot) {
stasis_http_response_error(
response, 500, "Internal Error", "Failed to get control snapshot");
return;
}
language = S_OR(args->lang, snapshot->language);
playback = stasis_app_control_play_uri(control, args->media, language,
args->bridge_id, STASIS_PLAYBACK_TARGET_BRIDGE, args->skipms,
args->offsetms);
if (!playback) {
stasis_http_response_alloc_failed(response);
return;
}
ast_asprintf(&playback_url, "/playback/%s",
stasis_app_playback_get_id(playback));
if (!playback_url) {
stasis_http_response_alloc_failed(response);
return;
}
json = stasis_app_playback_to_json(playback);
if (!json) {
stasis_http_response_alloc_failed(response);
return;
}
/* Give play_channel and control reference to the thread data */
thread_data = ast_calloc(1, sizeof(*thread_data));
if (!thread_data) {
stasis_http_response_alloc_failed(response);
return;
}
thread_data->bridge_channel = play_channel;
thread_data->control = control;
if (ast_pthread_create_detached(&threadid, NULL, bridge_channel_control_thread, thread_data)) {
stasis_http_response_alloc_failed(response);
ast_free(thread_data);
return;
}
/* These are owned by the other thread now, so we don't want RAII_VAR disposing of them. */
play_channel = NULL;
control = NULL;
stasis_http_response_created(response, playback_url, json);
}
void stasis_http_record_bridge(struct ast_variable *headers, struct ast_record_bridge_args *args, struct stasis_http_response *response)
{
ast_log(LOG_ERROR, "TODO: stasis_http_record_bridge\n");
RAII_VAR(struct ast_bridge *, bridge, find_bridge(response, args->bridge_id), ao2_cleanup);
RAII_VAR(struct ast_channel *, record_channel, NULL, ast_hangup);
RAII_VAR(struct stasis_app_control *, control, NULL, ao2_cleanup);
RAII_VAR(struct stasis_app_recording *, recording, NULL, ao2_cleanup);
RAII_VAR(char *, recording_url, NULL, ast_free);
RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
RAII_VAR(struct stasis_app_recording_options *, options, NULL, ao2_cleanup);
RAII_VAR(char *, uri_encoded_name, NULL, ast_free);
size_t uri_name_maxlen;
struct bridge_channel_control_thread_data *thread_data;
pthread_t threadid;
ast_assert(response != NULL);
if (bridge == NULL) {
return;
}
if (!(record_channel = prepare_bridge_media_channel("Recorder"))) {
stasis_http_response_error(
response, 500, "Internal Server Error", "Failed to create recording channel");
return;
}
if (ast_unreal_channel_push_to_bridge(record_channel, bridge)) {
stasis_http_response_error(
response, 500, "Internal Error", "Failed to put recording channel into the bridge");
return;
}
control = stasis_app_control_create(record_channel);
if (control == NULL) {
stasis_http_response_alloc_failed(response);
return;
}
options = stasis_app_recording_options_create(args->name, args->format);
if (options == NULL) {
stasis_http_response_alloc_failed(response);
return;
}
options->max_silence_seconds = args->max_silence_seconds;
options->max_duration_seconds = args->max_duration_seconds;
options->terminate_on =
stasis_app_recording_termination_parse(args->terminate_on);
options->if_exists =
stasis_app_recording_if_exists_parse(args->if_exists);
options->beep = args->beep;
recording = stasis_app_control_record(control, options);
if (recording == NULL) {
switch(errno) {
case EINVAL:
/* While the arguments are invalid, we should have
* caught them prior to calling record.
*/
stasis_http_response_error(
response, 500, "Internal Server Error",
"Error parsing request");
break;
case EEXIST:
stasis_http_response_error(response, 409, "Conflict",
"Recording '%s' already in progress",
args->name);
break;
case ENOMEM:
stasis_http_response_alloc_failed(response);
break;
case EPERM:
stasis_http_response_error(
response, 400, "Bad Request",
"Recording name invalid");
break;
default:
ast_log(LOG_WARNING,
"Unrecognized recording error: %s\n",
strerror(errno));
stasis_http_response_error(
response, 500, "Internal Server Error",
"Internal Server Error");
break;
}
return;
}
uri_name_maxlen = strlen(args->name) * 3;
uri_encoded_name = ast_malloc(uri_name_maxlen);
if (!uri_encoded_name) {
stasis_http_response_alloc_failed(response);
return;
}
ast_uri_encode(args->name, uri_encoded_name, uri_name_maxlen, ast_uri_http);
ast_asprintf(&recording_url, "/recordings/live/%s", uri_encoded_name);
if (!recording_url) {
stasis_http_response_alloc_failed(response);
return;
}
json = stasis_app_recording_to_json(recording);
if (!json) {
stasis_http_response_alloc_failed(response);
return;
}
thread_data = ast_calloc(1, sizeof(*thread_data));
if (!thread_data) {
stasis_http_response_alloc_failed(response);
return;
}
thread_data->bridge_channel = record_channel;
thread_data->control = control;
if (ast_pthread_create_detached(&threadid, NULL, bridge_channel_control_thread, thread_data)) {
stasis_http_response_alloc_failed(response);
ast_free(thread_data);
return;
}
/* These are owned by the other thread now, so we don't want RAII_VAR disposing of them. */
record_channel = NULL;
control = NULL;
stasis_http_response_created(response, recording_url, json);
}
void stasis_http_get_bridge(struct ast_variable *headers, struct ast_get_bridge_args *args, struct stasis_http_response *response)

@ -123,18 +123,43 @@ struct ast_remove_channel_from_bridge_args {
* \param[out] response HTTP response
*/
void stasis_http_remove_channel_from_bridge(struct ast_variable *headers, struct ast_remove_channel_from_bridge_args *args, struct stasis_http_response *response);
/*! \brief Argument struct for stasis_http_play_on_bridge() */
struct ast_play_on_bridge_args {
/*! \brief Bridge's id */
const char *bridge_id;
/*! \brief Media's URI to play. */
const char *media;
/*! \brief For sounds, selects language for sound. */
const char *lang;
/*! \brief Number of media to skip before playing. */
int offsetms;
/*! \brief Number of milliseconds to skip for forward/reverse operations. */
int skipms;
};
/*!
* \brief Start playback of media on a bridge.
*
* The media URI may be any of a number of URI's. You may use http: and https: URI's, as well as sound: and recording: URI's. This operation creates a playback resource that can be used to control the playback of media (pause, rewind, fast forward, etc.)
*
* \param headers HTTP headers
* \param args Swagger parameters
* \param[out] response HTTP response
*/
void stasis_http_play_on_bridge(struct ast_variable *headers, struct ast_play_on_bridge_args *args, struct stasis_http_response *response);
/*! \brief Argument struct for stasis_http_record_bridge() */
struct ast_record_bridge_args {
/*! \brief Bridge's id */
const char *bridge_id;
/*! \brief Recording's filename */
const char *name;
/*! \brief Format to encode audio in */
const char *format;
/*! \brief Maximum duration of the recording, in seconds. 0 for no limit. */
int max_duration_seconds;
/*! \brief Maximum duration of silence, in seconds. 0 for no limit. */
int max_silence_seconds;
/*! \brief If true, and recording already exists, append to recording. */
int append;
/*! \brief Action to take if a recording with the same name already exists. */
const char *if_exists;
/*! \brief Play beep when recording begins */
int beep;
/*! \brief DTMF input to terminate recording. */

@ -273,7 +273,7 @@ void stasis_http_play_on_channel(struct ast_variable *headers,
language = S_OR(args->lang, snapshot->language);
playback = stasis_app_control_play_uri(control, args->media, language,
args->skipms, args->offsetms);
args->channel_id, STASIS_PLAYBACK_TARGET_CHANNEL, args->skipms, args->offsetms);
if (!playback) {
stasis_http_response_error(
response, 500, "Internal Server Error",

@ -168,6 +168,83 @@
}
]
},
{
"path": "/bridges/{bridgeId}/play",
"description": "Play media to the participants of a bridge",
"operations": [
{
"httpMethod": "POST",
"summary": "Start playback of media on a bridge.",
"notes": "The media URI may be any of a number of URI's. You may use http: and https: URI's, as well as sound: and recording: URI's. This operation creates a playback resource that can be used to control the playback of media (pause, rewind, fast forward, etc.)",
"nickname": "playOnBridge",
"responseClass": "Playback",
"parameters": [
{
"name": "bridgeId",
"description": "Bridge's id",
"paramType": "path",
"required": true,
"allowMultiple": false,
"dataType": "string"
},
{
"name": "media",
"description": "Media's URI to play.",
"paramType": "query",
"required": true,
"allowMultiple": false,
"dataType": "string"
},
{
"name": "lang",
"description": "For sounds, selects language for sound.",
"paramType": "query",
"required": false,
"allowMultiple": false,
"dataType": "string"
},
{
"name": "offsetms",
"description": "Number of media to skip before playing.",
"paramType": "query",
"required": false,
"allowMultiple": false,
"dataType": "int",
"defaultValue": 0,
"allowableValues": {
"valueType": "RANGE",
"min": 0
}
},
{
"name": "skipms",
"description": "Number of milliseconds to skip for forward/reverse operations.",
"paramType": "query",
"required": false,
"allowMultiple": false,
"dataType": "int",
"defaultValue": 3000,
"allowableValues": {
"valueType": "RANGE",
"min": 0
}
}
],
"errorResponses": [
{
"code": 404,
"reason": "Bridge not found"
},
{
"code": 409,
"reason": "Bridge not in a Stasis application"
}
]
}
]
},
{
"path": "/bridges/{bridgeId}/record",
"description": "Record audio on a bridge",
@ -195,6 +272,14 @@
"allowMultiple": false,
"dataType": "string"
},
{
"name": "format",
"description": "Format to encode audio in",
"paramType": "query",
"required": true,
"allowMultiple": true,
"dataType": "string"
},
{
"name": "maxDurationSeconds",
"description": "Maximum duration of the recording, in seconds. 0 for no limit.",
@ -202,7 +287,11 @@
"required": false,
"allowMultiple": false,
"dataType": "int",
"defaultValue": 0
"defaultValue": 0,
"allowableValues": {
"valueType": "RANGE",
"min": 0
}
},
{
"name": "maxSilenceSeconds",
@ -211,16 +300,28 @@
"required": false,
"allowMultiple": false,
"dataType": "int",
"defaultValue": 0
"defaultValue": 0,
"allowableValues": {
"valueType": "RANGE",
"min": 0
}
},
{
"name": "append",
"description": "If true, and recording already exists, append to recording.",
"name": "ifExists",
"description": "Action to take if a recording with the same name already exists.",
"paramType": "query",
"required": false,
"allowMultiple": false,
"dataType": "boolean",
"defaultValue": false
"dataType": "string",
"defaultValue": "fail",
"allowableValues": {
"valueType": "LIST",
"values": [
"fail",
"overwrite",
"append"
]
}
},
{
"name": "beep",

@ -538,7 +538,7 @@
"summary": "Start a recording.",
"notes": "Record audio from a channel. Note that this will not capture audio sent to the channel. The bridge itself has a record feature if that's what you want.",
"nickname": "recordChannel",
"responseClass": "void",
"responseClass": "LiveRecording",
"parameters": [
{
"name": "channelId",

@ -53,7 +53,7 @@
"httpMethod": "POST",
"summary": "Get a playback's details.",
"nickname": "controlPlayback",
"responseClass": "Playback",
"responseClass": "void",
"parameters": [
{
"name": "playbackId",

@ -243,7 +243,15 @@
"id": "LiveRecording",
"description": "A recording that is in progress",
"properties": {
"id": {
"name": {
"required": true,
"type": "string"
},
"state": {
"required": true,
"type": "string"
},
"format": {
"required": true,
"type": "string"
}

Loading…
Cancel
Save