From 2ae1a22e0e71fc705ffd93d4ac061c48b0bb593c Mon Sep 17 00:00:00 2001 From: George Joseph Date: Mon, 5 Aug 2019 05:59:59 -0600 Subject: [PATCH] ARI: External Media The Channel resource has a new sub-resource "externalMedia". This allows an application to create a channel for the sole purpose of exchanging media with an external server. Once created, this channel could be placed into a bridge with existing channels to allow the external server to inject audio into the bridge or receive audio from the bridge. See https://wiki.asterisk.org/wiki/display/AST/External+Media+and+ARI for more information. Change-Id: I9618899198880b4c650354581b50c0401b58bc46 --- doc/CHANGES-staging/ARI.txt | 10 ++ res/ari/ari_model_validators.c | 56 ++++++++++ res/ari/ari_model_validators.h | 22 ++++ res/ari/resource_channels.c | 192 ++++++++++++++++++++++++++++---- res/ari/resource_channels.h | 42 +++++++ res/res_ari_channels.c | 135 +++++++++++++++++++++- rest-api/api-docs/channels.json | 146 ++++++++++++++++++++++++ 7 files changed, 579 insertions(+), 24 deletions(-) create mode 100644 doc/CHANGES-staging/ARI.txt diff --git a/doc/CHANGES-staging/ARI.txt b/doc/CHANGES-staging/ARI.txt new file mode 100644 index 0000000000..06ac4ab0fe --- /dev/null +++ b/doc/CHANGES-staging/ARI.txt @@ -0,0 +1,10 @@ +Subject: ARI Channels + +The Channel resource has a new sub-resource "externalMedia". +This allows an application to create a channel for the sole purpose +of exchanging media with an external server. Once created, this +channel could be placed into a bridge with existing channels to +allow the external server to inject audio into the bridge or +receive audio from the bridge. +See https://wiki.asterisk.org/wiki/display/AST/External+Media+and+ARI +for more information. \ No newline at end of file diff --git a/res/ari/ari_model_validators.c b/res/ari/ari_model_validators.c index 8910bbb94a..3d63f5317f 100644 --- a/res/ari/ari_model_validators.c +++ b/res/ari/ari_model_validators.c @@ -1385,6 +1385,62 @@ ari_validator ast_ari_validate_dialplan_cep_fn(void) return ast_ari_validate_dialplan_cep; } +int ast_ari_validate_external_media(struct ast_json *json) +{ + int res = 1; + struct ast_json_iter *iter; + int has_channel = 0; + + for (iter = ast_json_object_iter(json); iter; iter = ast_json_object_iter_next(json, iter)) { + if (strcmp("channel", ast_json_object_iter_key(iter)) == 0) { + int prop_is_valid; + has_channel = 1; + prop_is_valid = ast_ari_validate_channel( + ast_json_object_iter_value(iter)); + if (!prop_is_valid) { + ast_log(LOG_ERROR, "ARI ExternalMedia field channel failed validation\n"); + res = 0; + } + } else + if (strcmp("local_address", 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 ExternalMedia field local_address failed validation\n"); + res = 0; + } + } else + if (strcmp("local_port", ast_json_object_iter_key(iter)) == 0) { + int prop_is_valid; + prop_is_valid = ast_ari_validate_int( + ast_json_object_iter_value(iter)); + if (!prop_is_valid) { + ast_log(LOG_ERROR, "ARI ExternalMedia field local_port failed validation\n"); + res = 0; + } + } else + { + ast_log(LOG_ERROR, + "ARI ExternalMedia has undocumented field %s\n", + ast_json_object_iter_key(iter)); + res = 0; + } + } + + if (!has_channel) { + ast_log(LOG_ERROR, "ARI ExternalMedia missing required field channel\n"); + res = 0; + } + + return res; +} + +ari_validator ast_ari_validate_external_media_fn(void) +{ + return ast_ari_validate_external_media; +} + int ast_ari_validate_rtpstat(struct ast_json *json) { int res = 1; diff --git a/res/ari/ari_model_validators.h b/res/ari/ari_model_validators.h index f9285b43ce..53a85739cb 100644 --- a/res/ari/ari_model_validators.h +++ b/res/ari/ari_model_validators.h @@ -477,6 +477,24 @@ int ast_ari_validate_dialplan_cep(struct ast_json *json); */ ari_validator ast_ari_validate_dialplan_cep_fn(void); +/*! + * \brief Validator for ExternalMedia. + * + * ExternalMedia session. + * + * \param json JSON object to validate. + * \returns True (non-zero) if valid. + * \returns False (zero) if invalid. + */ +int ast_ari_validate_external_media(struct ast_json *json); + +/*! + * \brief Function pointer to ast_ari_validate_external_media(). + * + * See \ref ast_ari_model_validators.h for more details. + */ +ari_validator ast_ari_validate_external_media_fn(void); + /*! * \brief Validator for RTPstat. * @@ -1522,6 +1540,10 @@ ari_validator ast_ari_validate_application_fn(void); * - context: string (required) * - exten: string (required) * - priority: long (required) + * ExternalMedia + * - channel: Channel (required) + * - local_address: string + * - local_port: int * RTPstat * - channel_uniqueid: string (required) * - local_maxjitter: double diff --git a/res/ari/resource_channels.c b/res/ari/resource_channels.c index 1164ab10ed..0fd2d691af 100644 --- a/res/ari/resource_channels.c +++ b/res/ari/resource_channels.c @@ -1060,7 +1060,7 @@ end: return NULL; } -static void ari_channels_handle_originate_with_id(const char *args_endpoint, +static struct ast_channel *ari_channels_handle_originate_with_id(const char *args_endpoint, const char *args_extension, const char *args_context, long args_priority, @@ -1098,19 +1098,19 @@ static void ari_channels_handle_originate_with_id(const char *args_endpoint, || (assignedids.uniqueid2 && AST_MAX_PUBLIC_UNIQUEID < strlen(assignedids.uniqueid2))) { ast_ari_response_error(response, 400, "Bad Request", "Uniqueid length exceeds maximum of %d", AST_MAX_PUBLIC_UNIQUEID); - return; + return NULL; } if (ast_strlen_zero(args_endpoint)) { ast_ari_response_error(response, 400, "Bad Request", "Endpoint must be specified"); - return; + return NULL; } if (!ast_strlen_zero(args_originator) && !ast_strlen_zero(args_formats)) { ast_ari_response_error(response, 400, "Bad Request", "Originator and formats can't both be specified"); - return; + return NULL; } dialtech = ast_strdupa(args_endpoint); @@ -1122,7 +1122,7 @@ static void ari_channels_handle_originate_with_id(const char *args_endpoint, if (ast_strlen_zero(dialtech) || ast_strlen_zero(dialdevice)) { ast_ari_response_error(response, 400, "Bad Request", "Invalid endpoint specified"); - return; + return NULL; } if (!ast_strlen_zero(args_app)) { @@ -1130,7 +1130,7 @@ static void ari_channels_handle_originate_with_id(const char *args_endpoint, if (!appdata) { ast_ari_response_alloc_failed(response); - return; + return NULL; } ast_str_set(&appdata, 0, "%s", args_app); @@ -1141,7 +1141,7 @@ static void ari_channels_handle_originate_with_id(const char *args_endpoint, origination = ast_calloc(1, sizeof(*origination) + ast_str_size(appdata) + 1); if (!origination) { ast_ari_response_alloc_failed(response); - return; + return NULL; } strcpy(origination->appdata, ast_str_buffer(appdata)); @@ -1149,7 +1149,7 @@ static void ari_channels_handle_originate_with_id(const char *args_endpoint, origination = ast_calloc(1, sizeof(*origination) + 1); if (!origination) { ast_ari_response_alloc_failed(response); - return; + return NULL; } ast_copy_string(origination->context, S_OR(args_context, "default"), sizeof(origination->context)); @@ -1164,7 +1164,7 @@ static void ari_channels_handle_originate_with_id(const char *args_endpoint, if (ipri == -1) { ast_log(AST_LOG_ERROR, "Requested label: %s can not be found in context: %s\n", args_label, args_context); ast_ari_response_error(response, 404, "Not Found", "Requested label can not be found"); - return; + return NULL; } } else { ast_debug(3, "Numeric value provided for label, jumping to that priority\n"); @@ -1174,7 +1174,7 @@ static void ari_channels_handle_originate_with_id(const char *args_endpoint, ast_log(AST_LOG_ERROR, "Invalid priority label '%s' specified for extension %s in context: %s\n", args_label, args_extension, args_context); ast_ari_response_error(response, 400, "Bad Request", "Requested priority is illegal"); - return; + return NULL; } /* Our priority was provided by a label */ @@ -1188,14 +1188,14 @@ static void ari_channels_handle_originate_with_id(const char *args_endpoint, } else { ast_ari_response_error(response, 400, "Bad Request", "Application or extension must be specified"); - return; + return NULL; } dial = ast_dial_create(); if (!dial) { ast_ari_response_alloc_failed(response); ast_free(origination); - return; + return NULL; } ast_dial_set_user_data(dial, origination); @@ -1203,7 +1203,7 @@ static void ari_channels_handle_originate_with_id(const char *args_endpoint, ast_ari_response_alloc_failed(response); ast_dial_destroy(dial); ast_free(origination); - return; + return NULL; } if (args_timeout > 0) { @@ -1231,7 +1231,7 @@ static void ari_channels_handle_originate_with_id(const char *args_endpoint, "Provided originator channel was not found"); ast_dial_destroy(dial); ast_free(origination); - return; + return NULL; } } @@ -1244,7 +1244,7 @@ static void ari_channels_handle_originate_with_id(const char *args_endpoint, ast_dial_destroy(dial); ast_free(origination); ast_channel_cleanup(other); - return; + return NULL; } while ((format_name = ast_strip(strsep(&formats_copy, ",")))) { @@ -1263,7 +1263,7 @@ static void ari_channels_handle_originate_with_id(const char *args_endpoint, ast_channel_cleanup(other); ao2_ref(format_cap, -1); ao2_cleanup(fmt); - return; + return NULL; } ao2_ref(fmt, -1); } @@ -1279,7 +1279,7 @@ static void ari_channels_handle_originate_with_id(const char *args_endpoint, ast_dial_destroy(dial); ast_free(origination); ast_channel_cleanup(other); - return; + return NULL; } ast_channel_cleanup(other); @@ -1290,7 +1290,7 @@ static void ari_channels_handle_originate_with_id(const char *args_endpoint, ast_ari_response_alloc_failed(response); ast_dial_destroy(dial); ast_free(origination); - return; + return NULL; } if (!ast_strlen_zero(cid_num) || !ast_strlen_zero(cid_name)) { @@ -1355,8 +1355,7 @@ static void ari_channels_handle_originate_with_id(const char *args_endpoint, ast_ari_response_ok(response, ast_channel_snapshot_to_json(snapshot, NULL)); } - ast_channel_unref(chan); - return; + return chan; } /*! @@ -1397,6 +1396,7 @@ void ast_ari_channels_originate_with_id(struct ast_variable *headers, struct ast_ari_response *response) { struct ast_variable *variables = NULL; + struct ast_channel *chan; /* Parse any query parameters out of the body parameter */ if (args->variables) { @@ -1410,7 +1410,7 @@ void ast_ari_channels_originate_with_id(struct ast_variable *headers, } } - ari_channels_handle_originate_with_id( + chan = ari_channels_handle_originate_with_id( args->endpoint, args->extension, args->context, @@ -1426,6 +1426,7 @@ void ast_ari_channels_originate_with_id(struct ast_variable *headers, args->originator, args->formats, response); + ast_channel_cleanup(chan); ast_variables_destroy(variables); } @@ -1434,6 +1435,7 @@ void ast_ari_channels_originate(struct ast_variable *headers, struct ast_ari_response *response) { struct ast_variable *variables = NULL; + struct ast_channel *chan; /* Parse any query parameters out of the body parameter */ if (args->variables) { @@ -1447,7 +1449,7 @@ void ast_ari_channels_originate(struct ast_variable *headers, } } - ari_channels_handle_originate_with_id( + chan = ari_channels_handle_originate_with_id( args->endpoint, args->extension, args->context, @@ -1463,6 +1465,7 @@ void ast_ari_channels_originate(struct ast_variable *headers, args->originator, args->formats, response); + ast_channel_cleanup(chan); ast_variables_destroy(variables); } @@ -2053,3 +2056,148 @@ void ast_ari_channels_rtpstatistics(struct ast_variable *headers, return; } + +static void external_media_rtp_udp(struct ast_ari_channels_external_media_args *args, + struct ast_variable *variables, + struct ast_ari_response *response) +{ + size_t endpoint_len; + char *endpoint; + struct ast_channel *chan; + struct ast_json *json_chan; + struct varshead *vars; + + endpoint_len = strlen("UnicastRTP/") + strlen(args->external_host) + 1; + endpoint = ast_alloca(endpoint_len); + snprintf(endpoint, endpoint_len, "UnicastRTP/%s", args->external_host); + + chan = ari_channels_handle_originate_with_id( + endpoint, + NULL, + NULL, + 0, + NULL, + args->app, + NULL, + NULL, + 0, + variables, + args->channel_id, + NULL, + NULL, + args->format, + response); + ast_variables_destroy(variables); + + if (!chan) { + return; + } + + /* + * At this point, response->message contains a channel object so we + * need to save it then create a new ExternalMedia object and put the + * channel in it. + */ + json_chan = response->message; + response->message = ast_json_object_create(); + if (!response->message) { + ast_channel_unref(chan); + ast_json_unref(json_chan); + ast_ari_response_alloc_failed(response); + return; + } + + ast_json_object_set(response->message, "channel", json_chan); + /* + * At the time the channel snapshot was taken the channel variables might + * not have been set so we try to grab them directly from the channel. + */ + ast_channel_lock(chan); + vars = ast_channel_varshead(chan); + if (vars && !AST_LIST_EMPTY(vars)) { + struct ast_var_t *variables; + + /* Put them all on the channel object */ + ast_json_object_set(json_chan, "channelvars", ast_json_channel_vars(vars)); + /* Grab out the local address and port */ + AST_LIST_TRAVERSE(vars, variables, entries) { + if (!strcmp("UNICASTRTP_LOCAL_ADDRESS", ast_var_name(variables))) { + ast_json_object_set(response->message, "local_address", + ast_json_string_create(ast_var_value(variables))); + } + else if (!strcmp("UNICASTRTP_LOCAL_PORT", ast_var_name(variables))) { + ast_json_object_set(response->message, "local_port", + ast_json_integer_create(strtol(ast_var_value(variables), NULL, 10))); + } + } + } + ast_channel_unlock(chan); + ast_channel_unref(chan); +} + +#include "asterisk/config.h" +#include "asterisk/netsock2.h" + +void ast_ari_channels_external_media(struct ast_variable *headers, + struct ast_ari_channels_external_media_args *args, struct ast_ari_response *response) +{ + struct ast_variable *variables = NULL; + char *external_host; + char *host = NULL; + char *port = NULL; + + ast_assert(response != NULL); + + if (ast_strlen_zero(args->app)) { + ast_ari_response_error(response, 400, "Bad Request", "app cannot be empty"); + return; + } + + if (ast_strlen_zero(args->external_host)) { + ast_ari_response_error(response, 400, "Bad Request", "external_host cannot be empty"); + return; + } + + external_host = ast_strdupa(args->external_host); + if (!ast_sockaddr_split_hostport(external_host, &host, &port, PARSE_PORT_REQUIRE)) { + ast_ari_response_error(response, 400, "Bad Request", "external_host must be :"); + return; + } + + if (ast_strlen_zero(args->format)) { + ast_ari_response_error(response, 400, "Bad Request", "format cannot be empty"); + return; + } + + if (ast_strlen_zero(args->encapsulation)) { + args->encapsulation = "rtp"; + } + if (ast_strlen_zero(args->transport)) { + args->transport = "udp"; + } + if (ast_strlen_zero(args->connection_type)) { + args->connection_type = "client"; + } + if (ast_strlen_zero(args->direction)) { + args->direction = "both"; + } + + if (args->variables) { + struct ast_json *json_variables; + + ast_ari_channels_external_media_parse_body(args->variables, args); + json_variables = ast_json_object_get(args->variables, "variables"); + if (json_variables + && json_to_ast_variables(response, json_variables, &variables)) { + return; + } + } + + if (strcasecmp(args->encapsulation, "rtp") == 0 && strcasecmp(args->transport, "udp") == 0) { + external_media_rtp_udp(args, variables, response); + } else { + ast_ari_response_error( + response, 501, "Not Implemented", + "The encapsulation and/or transport is not supported"); + } +} diff --git a/res/ari/resource_channels.h b/res/ari/resource_channels.h index 8aefb40479..49a38829ca 100644 --- a/res/ari/resource_channels.h +++ b/res/ari/resource_channels.h @@ -824,5 +824,47 @@ struct ast_ari_channels_rtpstatistics_args { * \param[out] response HTTP response */ void ast_ari_channels_rtpstatistics(struct ast_variable *headers, struct ast_ari_channels_rtpstatistics_args *args, struct ast_ari_response *response); +/*! Argument struct for ast_ari_channels_external_media() */ +struct ast_ari_channels_external_media_args { + /*! The unique id to assign the channel on creation. */ + const char *channel_id; + /*! Stasis Application to place channel into */ + const char *app; + /*! The "variables" key in the body object holds variable key/value pairs to set on the channel on creation. Other keys in the body object are interpreted as query parameters. Ex. { "endpoint": "SIP/Alice", "variables": { "CALLERID(name)": "Alice" } } */ + struct ast_json *variables; + /*! Hostname/ip:port of external host */ + const char *external_host; + /*! Payload encapsulation protocol */ + const char *encapsulation; + /*! Transport protocol */ + const char *transport; + /*! Connection type (client/server) */ + const char *connection_type; + /*! Format to encode audio in */ + const char *format; + /*! External media direction */ + const char *direction; +}; +/*! + * \brief Body parsing function for /channels/externalMedia. + * \param body The JSON body from which to parse parameters. + * \param[out] args The args structure to parse into. + * \retval zero on success + * \retval non-zero on failure + */ +int ast_ari_channels_external_media_parse_body( + struct ast_json *body, + struct ast_ari_channels_external_media_args *args); + +/*! + * \brief Start an External Media session. + * + * Create a channel to an External Media source/sink. + * + * \param headers HTTP headers + * \param args Swagger parameters + * \param[out] response HTTP response + */ +void ast_ari_channels_external_media(struct ast_variable *headers, struct ast_ari_channels_external_media_args *args, struct ast_ari_response *response); #endif /* _ASTERISK_RESOURCE_CHANNELS_H */ diff --git a/res/res_ari_channels.c b/res/res_ari_channels.c index 825d4c2704..27d9deb8cf 100644 --- a/res/res_ari_channels.c +++ b/res/res_ari_channels.c @@ -2811,6 +2811,128 @@ static void ast_ari_channels_rtpstatistics_cb( } #endif /* AST_DEVMODE */ +fin: __attribute__((unused)) + return; +} +int ast_ari_channels_external_media_parse_body( + struct ast_json *body, + struct ast_ari_channels_external_media_args *args) +{ + struct ast_json *field; + /* Parse query parameters out of it */ + field = ast_json_object_get(body, "channelId"); + if (field) { + args->channel_id = ast_json_string_get(field); + } + field = ast_json_object_get(body, "app"); + if (field) { + args->app = ast_json_string_get(field); + } + field = ast_json_object_get(body, "external_host"); + if (field) { + args->external_host = ast_json_string_get(field); + } + field = ast_json_object_get(body, "encapsulation"); + if (field) { + args->encapsulation = ast_json_string_get(field); + } + field = ast_json_object_get(body, "transport"); + if (field) { + args->transport = ast_json_string_get(field); + } + field = ast_json_object_get(body, "connection_type"); + if (field) { + args->connection_type = ast_json_string_get(field); + } + field = ast_json_object_get(body, "format"); + if (field) { + args->format = ast_json_string_get(field); + } + field = ast_json_object_get(body, "direction"); + if (field) { + args->direction = ast_json_string_get(field); + } + return 0; +} + +/*! + * \brief Parameter parsing callback for /channels/externalMedia. + * \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 ast_ari_channels_external_media_cb( + struct ast_tcptls_session_instance *ser, + struct ast_variable *get_params, struct ast_variable *path_vars, + struct ast_variable *headers, struct ast_json *body, struct ast_ari_response *response) +{ + struct ast_ari_channels_external_media_args args = {}; + struct ast_variable *i; +#if defined(AST_DEVMODE) + int is_valid; + int code; +#endif /* AST_DEVMODE */ + + for (i = get_params; i; i = i->next) { + if (strcmp(i->name, "channelId") == 0) { + args.channel_id = (i->value); + } else + if (strcmp(i->name, "app") == 0) { + args.app = (i->value); + } else + if (strcmp(i->name, "external_host") == 0) { + args.external_host = (i->value); + } else + if (strcmp(i->name, "encapsulation") == 0) { + args.encapsulation = (i->value); + } else + if (strcmp(i->name, "transport") == 0) { + args.transport = (i->value); + } else + if (strcmp(i->name, "connection_type") == 0) { + args.connection_type = (i->value); + } else + if (strcmp(i->name, "format") == 0) { + args.format = (i->value); + } else + if (strcmp(i->name, "direction") == 0) { + args.direction = (i->value); + } else + {} + } + args.variables = body; + ast_ari_channels_external_media(headers, &args, response); +#if defined(AST_DEVMODE) + code = response->response_code; + + switch (code) { + case 0: /* Implementation is still a stub, or the code wasn't set */ + is_valid = response->message == NULL; + break; + case 500: /* Internal Server Error */ + case 501: /* Not Implemented */ + case 400: /* Invalid parameters */ + case 409: /* Channel is not in a Stasis application; Channel is already bridged */ + is_valid = 1; + break; + default: + if (200 <= code && code <= 299) { + is_valid = ast_ari_validate_external_media( + response->message); + } else { + ast_log(LOG_ERROR, "Invalid error response %d for /channels/externalMedia\n", code); + is_valid = 0; + } + } + + if (!is_valid) { + ast_log(LOG_ERROR, "Response validation failed for /channels/externalMedia\n"); + ast_ari_response_error(response, 500, + "Internal Server Error", "Response validation failed"); + } +#endif /* AST_DEVMODE */ + fin: __attribute__((unused)) return; } @@ -3007,14 +3129,23 @@ static struct stasis_rest_handlers channels_channelId = { .children = { &channels_channelId_continue,&channels_channelId_move,&channels_channelId_redirect,&channels_channelId_answer,&channels_channelId_ring,&channels_channelId_dtmf,&channels_channelId_mute,&channels_channelId_hold,&channels_channelId_moh,&channels_channelId_silence,&channels_channelId_play,&channels_channelId_record,&channels_channelId_variable,&channels_channelId_snoop,&channels_channelId_dial,&channels_channelId_rtp_statistics, } }; /*! \brief REST handler for /api-docs/channels.json */ +static struct stasis_rest_handlers channels_externalMedia = { + .path_segment = "externalMedia", + .callbacks = { + [AST_HTTP_POST] = ast_ari_channels_external_media_cb, + }, + .num_children = 0, + .children = { } +}; +/*! \brief REST handler for /api-docs/channels.json */ static struct stasis_rest_handlers channels = { .path_segment = "channels", .callbacks = { [AST_HTTP_GET] = ast_ari_channels_list_cb, [AST_HTTP_POST] = ast_ari_channels_originate_cb, }, - .num_children = 2, - .children = { &channels_create,&channels_channelId, } + .num_children = 3, + .children = { &channels_create,&channels_channelId,&channels_externalMedia, } }; static int unload_module(void) diff --git a/rest-api/api-docs/channels.json b/rest-api/api-docs/channels.json index 77412695bd..53e6e9de3f 100644 --- a/rest-api/api-docs/channels.json +++ b/rest-api/api-docs/channels.json @@ -1755,6 +1755,131 @@ ] } ] + }, + { + "path": "/channels/externalMedia", + "description": "Create a channel to an External Media source/sink.", + "operations": [ + { + "httpMethod": "POST", + "summary": "Start an External Media session.", + "notes": "Create a channel to an External Media source/sink.", + "nickname": "externalMedia", + "responseClass": "ExternalMedia", + "parameters": [ + { + "name": "channelId", + "description": "The unique id to assign the channel on creation.", + "paramType": "query", + "required": false, + "allowMultiple": false, + "dataType": "string" + }, + { + "name": "app", + "description": "Stasis Application to place channel into", + "paramType": "query", + "required": true, + "allowMultiple": false, + "dataType": "string" + }, + { + "name": "variables", + "description": "The \"variables\" key in the body object holds variable key/value pairs to set on the channel on creation. Other keys in the body object are interpreted as query parameters. Ex. { \"endpoint\": \"SIP/Alice\", \"variables\": { \"CALLERID(name)\": \"Alice\" } }", + "paramType": "body", + "required": false, + "dataType": "containers", + "allowMultiple": false + }, + { + "name": "external_host", + "description": "Hostname/ip:port of external host", + "paramType": "query", + "required": true, + "allowMultiple": false, + "dataType": "string" + }, + { + "name": "encapsulation", + "description": "Payload encapsulation protocol", + "paramType": "query", + "required": false, + "allowMultiple": false, + "dataType": "string", + "defaultValue": "rtp", + "allowableValues": { + "valueType": "LIST", + "values": [ + "rtp" + ] + } + }, + { + "name": "transport", + "description": "Transport protocol", + "paramType": "query", + "required": false, + "allowMultiple": false, + "dataType": "string", + "defaultValue": "udp", + "allowableValues": { + "valueType": "LIST", + "values": [ + "udp" + ] + } + }, + { + "name": "connection_type", + "description": "Connection type (client/server)", + "paramType": "query", + "required": false, + "allowMultiple": false, + "dataType": "string", + "defaultValue": "client", + "allowableValues": { + "valueType": "LIST", + "values": [ + "client" + ] + } + }, + { + "name": "format", + "description": "Format to encode audio in", + "paramType": "query", + "required": true, + "allowMultiple": false, + "dataType": "string" + }, + { + "name": "direction", + "description": "External media direction", + "paramType": "query", + "required": false, + "allowMultiple": false, + "dataType": "string", + "defaultValue": "both", + "allowableValues": { + "valueType": "LIST", + "values": [ + "both" + ] + } + } + ], + "errorResponses": [ + { + "code": 400, + "reason": "Invalid parameters" + }, + { + "code": 409, + "reason": "Channel is not in a Stasis application; Channel is already bridged" + } + ] + } + ] } ], "models": { @@ -2041,6 +2166,27 @@ "description": "Channel variables" } } + }, + "ExternalMedia": { + "id": "ExternalMedia", + "description": "ExternalMedia session.", + "properties": { + "channel": { + "required": true, + "type": "Channel", + "description": "The Asterisk channel representing the external media" + }, + "local_address": { + "required": false, + "type": "string", + "description": "The local ip address used" + }, + "local_port": { + "required": false, + "type": "int", + "description": "The local ip port used" + } + } } } }