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
This commit is contained in:
George Joseph 2019-08-05 05:59:59 -06:00
parent c00a010fe8
commit d566314e38
7 changed files with 579 additions and 24 deletions

View File

@ -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.

View File

@ -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;

View File

@ -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

View File

@ -1056,7 +1056,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,
@ -1094,19 +1094,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);
@ -1118,7 +1118,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)) {
@ -1126,7 +1126,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);
@ -1137,7 +1137,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));
@ -1145,7 +1145,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));
@ -1160,7 +1160,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");
@ -1170,7 +1170,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 */
@ -1184,14 +1184,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);
@ -1199,7 +1199,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) {
@ -1227,7 +1227,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;
}
}
@ -1240,7 +1240,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, ",")))) {
@ -1259,7 +1259,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);
}
@ -1275,7 +1275,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);
@ -1286,7 +1286,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)) {
@ -1351,8 +1351,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;
}
/*!
@ -1393,6 +1392,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) {
@ -1406,7 +1406,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,
@ -1422,6 +1422,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);
}
@ -1430,6 +1431,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) {
@ -1443,7 +1445,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,
@ -1459,6 +1461,7 @@ void ast_ari_channels_originate(struct ast_variable *headers,
args->originator,
args->formats,
response);
ast_channel_cleanup(chan);
ast_variables_destroy(variables);
}
@ -2049,3 +2052,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 <host>:<port>");
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");
}
}

View File

@ -822,5 +822,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 */

View File

@ -2804,6 +2804,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;
}
@ -3000,14 +3122,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)

View File

@ -1748,6 +1748,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": {
@ -2034,6 +2159,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"
}
}
}
}
}