diff --git a/channels/chan_pjsip.c b/channels/chan_pjsip.c index 13d1c66b06..da46f374ec 100644 --- a/channels/chan_pjsip.c +++ b/channels/chan_pjsip.c @@ -121,6 +121,7 @@ struct ast_channel_tech chan_pjsip_tech = { .read_stream = chan_pjsip_read_stream, .write = chan_pjsip_write, .write_stream = chan_pjsip_write_stream, + .write_text = chan_pjsip_write, .exception = chan_pjsip_read_stream, .indicate = chan_pjsip_indicate, .transfer = chan_pjsip_transfer, @@ -1021,6 +1022,18 @@ static int chan_pjsip_write_stream(struct ast_channel *ast, int stream_num, stru break; case AST_FRAME_CNG: break; + case AST_FRAME_TEXT: + if (!media) { + return 0; + } else if (media->type != AST_MEDIA_TYPE_TEXT) { + ast_debug(3, "Channel %s stream %d is of type '%s', not text!\n", + ast_channel_name(ast), stream_num, ast_codec_media_type2str(media->type)); + return 0; + } else if (media->write_callback) { + ast_debug(3, "Text data to write %.*s\n",frame->datalen,(char*)frame->data.ptr); + res = media->write_callback(session, media, frame); + } + break; case AST_FRAME_RTCP: /* We only support writing out feedback */ if (frame->subclass.integer != AST_RTP_RTCP_PSFB || !media) { @@ -1043,6 +1056,9 @@ static int chan_pjsip_write_stream(struct ast_channel *ast, int stream_num, stru static int chan_pjsip_write(struct ast_channel *ast, struct ast_frame *frame) { + if (frame->frametype == AST_FRAME_TEXT) { + return chan_pjsip_write_stream(ast, frame->stream_num, frame); + } return chan_pjsip_write_stream(ast, -1, frame); } diff --git a/channels/pjsip/dialplan_functions.c b/channels/pjsip/dialplan_functions.c index 594a56601b..d3c44fcdec 100644 --- a/channels/pjsip/dialplan_functions.c +++ b/channels/pjsip/dialplan_functions.c @@ -91,6 +91,8 @@ static int channel_read_rtp(struct ast_channel *chan, const char *type, const ch media = session->active_media_state->default_session[AST_MEDIA_TYPE_AUDIO]; } else if (!strcmp(field, "video")) { media = session->active_media_state->default_session[AST_MEDIA_TYPE_VIDEO]; + } else if (!strcmp(field, "text")) { + media = session->active_media_state->default_session[AST_MEDIA_TYPE_TEXT]; } else { ast_log(AST_LOG_WARNING, "Unknown media type field '%s' for 'rtp' information\n", field); return -1; diff --git a/configs/samples/pjsip.conf.sample b/configs/samples/pjsip.conf.sample index c399ba8a24..6a20288dbe 100644 --- a/configs/samples/pjsip.conf.sample +++ b/configs/samples/pjsip.conf.sample @@ -799,8 +799,10 @@ ;sdp_session=Asterisk ; String used for the SDP session s line (default: ; "Asterisk") ;tos_audio=0 ; DSCP TOS bits for audio streams (default: "0") +;tos_text=0 ; DSCP TOS bits for text streams (default: "0") ;tos_video=0 ; DSCP TOS bits for video streams (default: "0") ;cos_audio=0 ; Priority for audio streams (default: "0") +;cos_text=0 ; Priority for text streams (default: "0") ;cos_video=0 ; Priority for video streams (default: "0") ;allow_subscribe=yes ; Determines if endpoint is allowed to initiate ; subscriptions with Asterisk (default: "yes") @@ -903,6 +905,8 @@ ; ringing devices. ;max_audio_streams= ; The maximum number of allowed negotiated audio streams ; (default: 1) +;max_text_streams= ; The maximum number of allowed negotiated text streams + ; (default: 1) ;max_video_streams= ; The maximum number of allowed negotiated video streams ; (default: 1) ;webrtc= ; When set to "yes" this also enables the following values that are needed diff --git a/contrib/ast-db-manage/config/versions/65b5ac60e54a_rtt_options.py b/contrib/ast-db-manage/config/versions/65b5ac60e54a_rtt_options.py new file mode 100644 index 0000000000..a46cec3fb2 --- /dev/null +++ b/contrib/ast-db-manage/config/versions/65b5ac60e54a_rtt_options.py @@ -0,0 +1,25 @@ +"""Add rtt options + +Revision ID: 65b5ac60e54a +Revises: abdc9ede147d +Create Date: 2025-02-21 12:35:43.615049 + +""" + +# revision identifiers, used by Alembic. +revision = '65b5ac60e54a' +down_revision = 'abdc9ede147d' + +from alembic import op +import sqlalchemy as sa + +def upgrade(): + op.add_column('ps_endpoints', sa.Column('tos_text', sa.String(10))) + op.add_column('ps_endpoints', sa.Column('cos_text', sa.Integer)) + op.add_column('ps_endpoints', sa.Column('max_text_streams', sa.Integer)) + +def downgrade(): + op.drop_column('ps_endpoints', 'tos_text') + op.drop_column('ps_endpoints', 'cos_text') + op.drop_column('ps_endpoints', 'max_text_streams') + \ No newline at end of file diff --git a/include/asterisk/res_pjsip.h b/include/asterisk/res_pjsip.h index 2fafd5790a..f73017cee7 100644 --- a/include/asterisk/res_pjsip.h +++ b/include/asterisk/res_pjsip.h @@ -1043,6 +1043,12 @@ struct ast_sip_endpoint_media_configuration { struct ast_stream_codec_negotiation_prefs codec_prefs_incoming_answer; /*! Codec negotiation prefs for outgoing answers */ struct ast_stream_codec_negotiation_prefs codec_prefs_outgoing_answer; + /*! DSCP TOS bits for text streams */ + unsigned int tos_text; + /*! Priority for text streams */ + unsigned int cos_text; + /*! Maximum number of text streams to offer/accept */ + unsigned int max_text_streams; }; /*! diff --git a/include/asterisk/rtp_engine.h b/include/asterisk/rtp_engine.h index 0a38e43cd5..a9dae2d13d 100644 --- a/include/asterisk/rtp_engine.h +++ b/include/asterisk/rtp_engine.h @@ -1029,7 +1029,7 @@ void ast_rtp_instance_set_data(struct ast_rtp_instance *instance, void *data); * * \code * struct *blob = ast_rtp_instance_get_data(instance); - ( \endcode + * \endcode * * This gets the data pointer on the RTP instance pointed to by 'instance'. * diff --git a/main/channel.c b/main/channel.c index 03e631ca0a..7d0dee879c 100644 --- a/main/channel.c +++ b/main/channel.c @@ -4793,6 +4793,7 @@ int ast_sendtext_data(struct ast_channel *chan, struct ast_msg_data *msg) f.frametype = AST_FRAME_TEXT; f.datalen = body_len; f.mallocd = AST_MALLOCD_DATA; + f.stream_num = ast_stream_get_position(ast_channel_get_default_stream(chan, AST_MEDIA_TYPE_TEXT)); f.data.ptr = ast_strdup(body); if (f.data.ptr) { res = ast_channel_tech(chan)->write_text(chan, &f); diff --git a/res/res_format_attr_red.c b/res/res_format_attr_red.c new file mode 100644 index 0000000000..365e640624 --- /dev/null +++ b/res/res_format_attr_red.c @@ -0,0 +1,281 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2025, GILAWA Ltd + * + * Henning Westerholt + * + * 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 RED format attribute interface + * + * \author Henning Westerholt + * + * \note https://www.rfc-editor.org/rfc/rfc4103.html + */ + +/*** MODULEINFO + core + ***/ + +#include "asterisk.h" + +#include /* for tolower */ + +#include "asterisk/module.h" +#include "asterisk/format.h" +#include "asterisk/logger.h" /* for ast_log, LOG_WARNING */ +#include "asterisk/strings.h" /* for ast_str_append */ +#include "asterisk/utils.h" /* for ast_free */ +#include "asterisk/rtp_engine.h" /* for AST_RED_MAX_GENERATION */ + +/* + * From RFC 4103: "Therefore, text/t140 is RECOMMENDED + * to be the only payload type in the RTP stream." + * For this reason we only support uniform payload types. + * + * If in the future this attribute should be used also for other redundant rtp streams, + * it needs to be adapted together with the other infrastructure supporting it. + */ +struct red_attr { + int red_num_gen; /* Number of generations */ + int red_payload; /* Payload for each generation (actually related to t140) */ + int cps; /* Characters per second */ +}; + +static struct red_attr default_red_attr = { + .red_num_gen = 2, + .red_payload = 98, /* default type that two test clients used */ + .cps = 30 +}; + +static void red_destroy(struct ast_format *format) +{ + struct red_attr *attr = ast_format_get_attribute_data(format); + ast_free(attr); +} + +static int red_clone(const struct ast_format *src, struct ast_format *dst) +{ + struct red_attr *original = ast_format_get_attribute_data(src); + struct red_attr *attr = ast_malloc(sizeof(*attr)); + + if (!attr) { + return -1; + } + + if (original) { + *attr = *original; + } else { + *attr = default_red_attr; + } + + ast_format_set_attribute_data(dst, attr); + return 0; +} + +static struct ast_format *red_parse_sdp_fmtp(const struct ast_format *format, const char *attributes) +{ + char *attribs = ast_strdupa(attributes); + char *rest = NULL; + struct ast_format *cloned; + struct red_attr *attr; + int red_num_gen = -1; + int red_payload = -1; + + cloned = ast_format_clone(format); + if (!cloned) { + return NULL; + } + attr = ast_format_get_attribute_data(cloned); + + attribs = strtok_r(attribs, "/", &rest); + /* number of redundant generations is one less of the attributes */ + while (attribs && red_num_gen++ < AST_RED_MAX_GENERATION-1) { + if (sscanf(attribs, "%30u", &red_payload) == 1) { + attr->red_payload = red_payload; + } + attribs = strtok_r(NULL, "/", &rest); + } + + attr->red_num_gen = red_num_gen; + + return cloned; +} + +static void red_generate_sdp_fmtp(const struct ast_format *format, unsigned int payload, struct ast_str **str) +{ + struct red_attr *attr = ast_format_get_attribute_data(format); + int base_fmtp_size; + int original_size; + if (!attr) { + ast_log(LOG_ERROR, "Invalid RED attributes\n"); + return; + } + + /* + * Generate the SDP fmtp line for RED + * Assuming attr->red_payload holds the payload types for redundancy + * and attr->red_num_gen holds the number of generations + */ + original_size = ast_str_strlen(*str); + base_fmtp_size = ast_str_append(str, 0, "a=fmtp:%u ", payload); + + if (attr->red_num_gen > 0) { + /* attributes are one more as the number of generations */ + for (int i = 0; i < attr->red_num_gen + 1; i++) { + ast_str_append(str, 0, "%d/", attr->red_payload); + } + } + + if (base_fmtp_size == ast_str_strlen(*str) - original_size) { + ast_str_truncate(*str, original_size); + } else { + ast_str_truncate(*str, -1); + ast_str_append(str, 0, "\r\n"); + } + + ast_debug(3, "RED sdp written: %s\n", ast_str_buffer(*str)); +} + + +static const void *red_attribute_get(const struct ast_format *format, const char *name) +{ + struct red_attr *attr = ast_format_get_attribute_data(format); + int *val; + + if (!strcasecmp(name, "red_num_gen")) { + val = &attr->red_num_gen; + } else if (!strcasecmp(name, "red_payload")) { + val = &attr->red_payload; + } else if (!strcasecmp(name, "cps")) { + val = &attr->cps; + } else { + ast_log(LOG_WARNING, "unknown attribute type %s\n", name); + return NULL; + } + + return val; +} + + +static struct ast_format *red_attribute_set(const struct ast_format *format, const char *name, const char *value) +{ + struct ast_format *cloned; + struct red_attr *attr; + unsigned int val; + + if (sscanf(value, "%30u", &val) != 1) { + ast_log(LOG_WARNING, "Unknown value '%s' for attribute type '%s'\n", + value, name); + return NULL; + } + + cloned = ast_format_clone(format); + if (!cloned) { + return NULL; + } + attr = ast_format_get_attribute_data(cloned); + + if (!strcasecmp(name, "red_num_gen")) { + attr->red_num_gen = val; + } else if (!strcasecmp(name, "red_payload")) { + attr->red_payload = val; + } else if (!strcasecmp(name, "cps")) { + attr->cps = val; + } else { + ast_log(LOG_WARNING, "unknown attribute type %s\n", name); + } + + return cloned; +} + + +static struct ast_format *red_getjoint(const struct ast_format *format1, const struct ast_format *format2) +{ + struct red_attr *attr1 = ast_format_get_attribute_data(format1); + struct red_attr *attr2 = ast_format_get_attribute_data(format2); + struct ast_format *jointformat; + struct red_attr *attr_res; + /* To re-use offered RED payload types */ + unsigned int use_side = 0; + + if (!attr1) { + attr1 = &default_red_attr; + use_side = 2; + } + + if (!attr2) { + attr2 = &default_red_attr; + use_side = 1; + } + + jointformat = ast_format_clone(use_side == 2 ? format2 : format1); + if (!jointformat) { + return NULL; + } + + /* Get the resulting attributes for the joint format */ + attr_res = ast_format_get_attribute_data(jointformat); + + /* Determine joint attributes */ + if (attr1->red_num_gen == 0 || attr2->red_num_gen == 0) { + /* If either format has no redundancy, set joint to no redundancy */ + attr_res->red_num_gen = 0; + } else { + /* + * If both formats have redundancy, take the minimum number of generations. + * Some tested clients only support e.g. 2 generations and it would be a waste + * of bandwith or could even lead to incompatibilities. + */ + attr_res->red_num_gen = MIN(attr1->red_num_gen, attr2->red_num_gen); + attr_res->red_payload = (use_side == 2) ? attr2->red_payload : attr1->red_payload; + } + + ast_debug(3, "RED final joint: generations %d, payload %d\n", attr_res->red_num_gen, + attr_res->red_payload); + + return jointformat; +} + +static struct ast_format_interface red_interface = { + .format_destroy = red_destroy, + .format_clone = red_clone, + .format_cmp = NULL, + .format_get_joint = red_getjoint, + .format_attribute_set = red_attribute_set, + .format_attribute_get = red_attribute_get, + .format_parse_sdp_fmtp = red_parse_sdp_fmtp, + .format_generate_sdp_fmtp = red_generate_sdp_fmtp, +}; + +static int load_module(void) +{ + if (ast_format_interface_register("red", &red_interface)) { + return AST_MODULE_LOAD_DECLINE; + } + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "RED Format Attribute Module", + .support_level = AST_MODULE_SUPPORT_CORE, + .load = load_module, + .unload = unload_module, + .load_pri = AST_MODPRI_CHANNEL_DEPEND, +); \ No newline at end of file diff --git a/res/res_pjsip/pjsip_config.xml b/res/res_pjsip/pjsip_config.xml index cef061b84c..d3a7c62d95 100644 --- a/res/res_pjsip/pjsip_config.xml +++ b/res/res_pjsip/pjsip_config.xml @@ -448,6 +448,45 @@ + + + 22.4.0 + 21.9.0 + 20.14.0 + + + Maximum number of text streams + + + + Maximum number of text streams to accept. This is useful in situations + where you want to limit the number of text streams that can be sent to + the client. + + + + + + 22.4.0 + 21.9.0 + 20.14.0 + + DSCP TOS bits for text streams + + See https://docs.asterisk.org/Configuration/Channel-Drivers/IP-Quality-of-Service for more information about QoS settings + + + + + 22.4.0 + 21.9.0 + 20.14.0 + + Priority for text streams + + See https://docs.asterisk.org/Configuration/Channel-Drivers/IP-Quality-of-Service for more information about QoS settings + + 12.2.0 diff --git a/res/res_pjsip/pjsip_configuration.c b/res/res_pjsip/pjsip_configuration.c index c2d78dfa34..b30240fdaa 100644 --- a/res/res_pjsip/pjsip_configuration.c +++ b/res/res_pjsip/pjsip_configuration.c @@ -1082,6 +1082,8 @@ static int tos_handler(const struct aco_option *opt, endpoint->media.tos_audio = value; } else if (!strcmp(var->name, "tos_video")) { endpoint->media.tos_video = value; + } else if (!strcmp(var->name, "tos_text")) { + endpoint->media.tos_text = value; } else { /* If we reach this point, someone called the tos_handler when they shouldn't have. */ ast_assert(0); @@ -1110,6 +1112,16 @@ static int tos_video_to_str(const void *obj, const intptr_t *args, char **buf) return 0; } +static int tos_text_to_str(const void *obj, const intptr_t *args, char **buf) +{ + const struct ast_sip_endpoint *endpoint = obj; + + if (ast_asprintf(buf, "%u", endpoint->media.tos_text) == -1) { + return -1; + } + return 0; +} + static int from_user_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) { @@ -2244,8 +2256,10 @@ int ast_res_pjsip_initialize_configuration(void) ast_sorcery_object_field_register(sip_sorcery, "endpoint", "sdp_session", "Asterisk", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_endpoint, media.sdpsession)); ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "tos_audio", "0", tos_handler, tos_audio_to_str, NULL, 0, 0); ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "tos_video", "0", tos_handler, tos_video_to_str, NULL, 0, 0); + ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "tos_text", "0", tos_handler, tos_text_to_str, NULL, 0, 0); ast_sorcery_object_field_register(sip_sorcery, "endpoint", "cos_audio", "0", OPT_UINT_T, 0, FLDSET(struct ast_sip_endpoint, media.cos_audio)); ast_sorcery_object_field_register(sip_sorcery, "endpoint", "cos_video", "0", OPT_UINT_T, 0, FLDSET(struct ast_sip_endpoint, media.cos_video)); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "cos_text", "0", OPT_UINT_T, 0, FLDSET(struct ast_sip_endpoint, media.cos_text)); ast_sorcery_object_field_register(sip_sorcery, "endpoint", "allow_subscribe", "yes", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, subscription.allow)); ast_sorcery_object_field_register(sip_sorcery, "endpoint", "sub_min_expiry", "0", OPT_UINT_T, 0, FLDSET(struct ast_sip_endpoint, subscription.minexpiry)); ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "from_user", "", from_user_handler, from_user_to_str, NULL, 0, 0); @@ -2286,6 +2300,7 @@ int ast_res_pjsip_initialize_configuration(void) ast_sorcery_object_field_register(sip_sorcery, "endpoint", "notify_early_inuse_ringing", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, notify_early_inuse_ringing)); ast_sorcery_object_field_register(sip_sorcery, "endpoint", "max_audio_streams", "1", OPT_UINT_T, 0, FLDSET(struct ast_sip_endpoint, media.max_audio_streams)); ast_sorcery_object_field_register(sip_sorcery, "endpoint", "max_video_streams", "1", OPT_UINT_T, 0, FLDSET(struct ast_sip_endpoint, media.max_video_streams)); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "max_text_streams", "1", OPT_UINT_T, 0, FLDSET(struct ast_sip_endpoint, media.max_text_streams)); ast_sorcery_object_field_register(sip_sorcery, "endpoint", "bundle", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, media.bundle)); ast_sorcery_object_field_register(sip_sorcery, "endpoint", "webrtc", "no", OPT_YESNO_T, 1, FLDSET(struct ast_sip_endpoint, media.webrtc)); ast_sorcery_object_field_register(sip_sorcery, "endpoint", "incoming_mwi_mailbox", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_endpoint, incoming_mwi_mailbox)); diff --git a/res/res_pjsip/pjsip_manager.xml b/res/res_pjsip/pjsip_manager.xml index 9f2cf634c5..b7d37c38b7 100644 --- a/res/res_pjsip/pjsip_manager.xml +++ b/res/res_pjsip/pjsip_manager.xml @@ -500,12 +500,18 @@ + + + + + + @@ -647,6 +653,9 @@ + + + diff --git a/res/res_pjsip_sdp_rtp.c b/res/res_pjsip_sdp_rtp.c index 7d2a5e7c4a..5113183c11 100644 --- a/res/res_pjsip_sdp_rtp.c +++ b/res/res_pjsip_sdp_rtp.c @@ -67,6 +67,7 @@ static struct ast_sockaddr address_rtp; static const char STR_AUDIO[] = "audio"; static const char STR_VIDEO[] = "video"; +static const char STR_TEXT[] = "text"; static int send_keepalive(const void *data) { @@ -290,6 +291,10 @@ static int create_rtp(struct ast_sip_session *session, struct ast_sip_session_me (session->endpoint->media.tos_audio || session->endpoint->media.cos_audio)) { ast_rtp_instance_set_qos(session_media->rtp, session->endpoint->media.tos_audio, session->endpoint->media.cos_audio, "SIP RTP Audio"); + } else if (session_media->type == AST_MEDIA_TYPE_TEXT && + (session->endpoint->media.tos_text || session->endpoint->media.cos_text)) { + ast_rtp_instance_set_qos(session_media->rtp, session->endpoint->media.tos_text, + session->endpoint->media.cos_text, "SIP RTP Text"); } else if (session_media->type == AST_MEDIA_TYPE_VIDEO) { ast_rtp_instance_set_prop(session_media->rtp, AST_RTP_PROPERTY_RETRANS_RECV, session->endpoint->media.webrtc); ast_rtp_instance_set_prop(session_media->rtp, AST_RTP_PROPERTY_RETRANS_SEND, session->endpoint->media.webrtc); @@ -373,7 +378,6 @@ static void get_codecs(struct ast_sip_session *session, const struct pjmedia_sdp struct ast_format *format_parsed; ast_copy_pj_str(fmt_param, &fmtp.fmt_param, sizeof(fmt_param)); - format_parsed = ast_format_parse_sdp_fmtp(format, fmt_param); if (format_parsed) { ast_rtp_codecs_payload_replace_format(codecs, num, format_parsed); @@ -1539,6 +1543,45 @@ static void set_session_media_remotely_held(struct ast_sip_session_media *sessio } } + +/*! \brief Function which checks for RED in a session and initalize it if needed */ +static void check_red_support(struct ast_sip_session *session, struct ast_sip_session_media *session_media, + struct ast_format *format_parsed) +{ + /* + * To keep API compatibility, we are using an array here, even if the red codec + * is using only an integer internally, similar as the other codec attributes. + * The rtp red support from asterisk expect the primary payload also added to + * the array, therefore we need one more element. + */ + int red_pt_array[AST_RED_MAX_GENERATION+1]; + memset(red_pt_array, 0, sizeof(red_pt_array)); + + if (session == NULL || session_media == NULL || format_parsed == NULL) { + ast_log(LOG_ERROR, "Invalid parameter for initializing RED\n"); + return; + } + + if (ast_format_cap_has_type(session->endpoint->media.codecs, AST_MEDIA_TYPE_TEXT)) { + if (ast_format_attribute_get(format_parsed, "red_payload") != NULL && + strcasecmp(ast_format_get_name(format_parsed), "RED") == 0) { + int red_payload = *(int*)ast_format_attribute_get(format_parsed, "red_payload"); + int red_num_gen = *(int*)ast_format_attribute_get(format_parsed, "red_num_gen"); + if (red_payload != -1 && red_num_gen != -1) { + for (int x = 0; x < red_num_gen+1; x++) { + red_pt_array[x] = red_payload; + } + if (ast_rtp_red_init(session_media->rtp, 300, red_pt_array, red_num_gen) < 0) { + ast_log(LOG_ERROR, "Failed to initialize RED on rtp instance for stream %s\n", + ast_sip_session_get_name(session)); + } + } + ast_debug(3, "RED init success for stream %s with %d payload and %d generations\n", + ast_sip_session_get_name(session), red_payload, red_num_gen); + } + } +} + /*! \brief Function which negotiates an incoming media stream */ static int negotiate_incoming_sdp_stream(struct ast_sip_session *session, struct ast_sip_session_media *session_media, const pjmedia_sdp_session *sdp, @@ -1809,6 +1852,8 @@ static int create_outgoing_sdp_stream(struct ast_sip_session *session, struct as AST_VECTOR(, int) sample_rates; /* In case we can't init the sample rates, still try to do the rest. */ int build_dtmf_sample_rates = 1; + /* Temporary store the t140 payload for redundant text payload adaptions */ + int t140_payload = 0; SCOPE_ENTER(1, "%s Type: %s %s\n", ast_sip_session_get_name(session), ast_codec_media_type2str(media_type), ast_str_tmp(128, ast_stream_to_str(stream, &STR_TMP))); @@ -1967,6 +2012,28 @@ static int create_outgoing_sdp_stream(struct ast_sip_session *session, struct as build_dtmf_sample_rates = 0; } + /* if we are using text with rtp redundancy, we need to match the t140 payload inside the RED sdp */ + for (index = 0; index < ast_format_cap_count(caps); ++index) { + struct ast_format *format = ast_format_cap_get_format(caps, index); + if (ast_format_get_type(format) == AST_MEDIA_TYPE_TEXT && strcasecmp(ast_format_get_name(format), "T140") == 0) { + if (session_media_transport != session_media) { + if ((rtp_code = ast_rtp_codecs_payload_code(ast_rtp_instance_get_codecs(session_media_transport->rtp), 1, format, 0)) == -1) { + ast_log(LOG_WARNING, "Unable to get rtp codec payload code for %s\n", ast_format_get_name(format)); + ao2_ref(format, -1); + continue; + } + } else { + if ((rtp_code = ast_rtp_codecs_payload_code(ast_rtp_instance_get_codecs(session_media->rtp), 1, format, 0)) == -1) { + ast_log(LOG_WARNING, "Unable to get rtp codec payload code for %s\n", ast_format_get_name(format)); + ao2_ref(format, -1); + continue; + } + } + t140_payload = rtp_code; + ast_debug(3, "Got t140 payload %d\n", t140_payload); + } + } + for (index = 0; index < ast_format_cap_count(caps); ++index) { struct ast_format *format = ast_format_cap_get_format(caps, index); @@ -2004,6 +2071,16 @@ static int create_outgoing_sdp_stream(struct ast_sip_session *session, struct as } } + /* if we are using text with rtp redundancy, we need to match the t140 payload inside the RED sdp */ + if (ast_format_get_type(format) == AST_MEDIA_TYPE_TEXT && strncasecmp(ast_format_get_name(format), "RED", 3) == 0) { + if (ast_format_attribute_get(format, "red_payload") != NULL && + (*(int *)ast_format_attribute_get(format, "red_payload") != t140_payload)) { + char tmp[10]; + snprintf(tmp, 10, "%d", t140_payload); + format = ast_format_attribute_set(format, "red_payload", tmp); + ast_debug(3, "Set RED payload to %s\n", tmp); + } + } if ((attr = generate_rtpmap_attr(session, media, pool, rtp_code, 1, format, 0))) { int i, added = 0; int newrate = ast_rtp_lookup_sample_rate2(1, format, 0); @@ -2224,6 +2301,8 @@ static int apply_negotiated_sdp_stream(struct ast_sip_session *session, int res; int rtp_timeout; struct ast_sip_session_media *session_media_transport; + struct ast_format_cap *joint; + struct ast_format *format_parsed; SCOPE_ENTER(1, "%s Stream: %s\n", ast_sip_session_get_name(session), ast_str_tmp(128, ast_stream_to_str(asterisk_stream, &STR_TMP))); @@ -2298,6 +2377,14 @@ static int apply_negotiated_sdp_stream(struct ast_sip_session *session, SCOPE_EXIT_RTN_VALUE(-1, "set_caps failed\n"); } + joint = set_incoming_call_offer_cap(session, session_media, remote_stream); + + /* If RED support is enabled we need to set it up */ + format_parsed = ast_format_cap_get_compatible_format(joint, ast_format_t140_red); + if (format_parsed) { + check_red_support(session, session_media, format_parsed); + } + /* Set the channel uniqueid on the RTP instance now that it is becoming active */ ast_channel_lock(session->channel); ast_rtp_instance_set_channel_id(session_media->rtp, ast_channel_uniqueid(session->channel)); @@ -2452,6 +2539,17 @@ static struct ast_sip_session_sdp_handler video_sdp_handler = { .stream_destroy = stream_destroy, }; +/*! \brief SDP handler for 'text' media stream */ +static struct ast_sip_session_sdp_handler text_sdp_handler = { + .id = STR_TEXT, + .negotiate_incoming_sdp_stream = negotiate_incoming_sdp_stream, + .create_outgoing_sdp_stream = create_outgoing_sdp_stream, + .apply_negotiated_sdp_stream = apply_negotiated_sdp_stream, + .change_outgoing_sdp_stream_media_address = change_outgoing_sdp_stream_media_address, + .stream_stop = stream_stop, + .stream_destroy = stream_destroy, +}; + static int video_info_incoming_request(struct ast_sip_session *session, struct pjsip_rx_data *rdata) { struct pjsip_transaction *tsx; @@ -2485,6 +2583,7 @@ static int unload_module(void) ast_sip_session_unregister_supplement(&video_info_supplement); ast_sip_session_unregister_sdp_handler(&video_sdp_handler, STR_VIDEO); ast_sip_session_unregister_sdp_handler(&audio_sdp_handler, STR_AUDIO); + ast_sip_session_unregister_sdp_handler(&text_sdp_handler, STR_TEXT); if (sched) { ast_sched_context_destroy(sched); @@ -2531,6 +2630,11 @@ static int load_module(void) goto end; } + if (ast_sip_session_register_sdp_handler(&text_sdp_handler, STR_TEXT)) { + ast_log(LOG_ERROR, "Unable to register SDP handler for %s stream type\n", STR_TEXT); + goto end; + } + ast_sip_session_register_supplement(&video_info_supplement); return AST_MODULE_LOAD_SUCCESS; diff --git a/res/res_pjsip_session.c b/res/res_pjsip_session.c index db5bdd1202..e2aadcdcfa 100644 --- a/res/res_pjsip_session.c +++ b/res/res_pjsip_session.c @@ -608,8 +608,9 @@ static int is_stream_limitation_reached(enum ast_media_type type, const struct a case AST_MEDIA_TYPE_IMAGE: /* We don't have an option for image (T.38) streams so cap it to one. */ return (type_streams[type] > 0); - case AST_MEDIA_TYPE_UNKNOWN: case AST_MEDIA_TYPE_TEXT: + return !(type_streams[type] < endpoint->media.max_text_streams); + case AST_MEDIA_TYPE_UNKNOWN: default: /* We don't want any unknown or "other" streams on our endpoint, * so always just say we've reached the limit diff --git a/res/res_pjsip_session/pjsip_session_caps.c b/res/res_pjsip_session/pjsip_session_caps.c index d44f1a6bb0..3eddaca156 100644 --- a/res/res_pjsip_session/pjsip_session_caps.c +++ b/res/res_pjsip_session/pjsip_session_caps.c @@ -127,14 +127,40 @@ struct ast_format_cap *ast_sip_create_joint_call_cap(const struct ast_format_cap return joint; } +/* + * Fix preferences for real time text calls when we are supporting t140 and red codecs + * If the the remote party only supports t140, we should not offer redundancy + */ +static void fix_t140_red_pref(const struct ast_sip_session *session, enum ast_media_type media_type) { + int res = 0; + struct ast_flags *pref = &session->endpoint->media.outgoing_call_offer_pref; + + if (media_type == AST_MEDIA_TYPE_TEXT && session->call_direction == AST_SIP_SESSION_OUTGOING_CALL) { + if (strcmp(ast_sip_call_codec_pref_to_str(*pref), "local_merge") == 0) { + ast_debug(3, "text call with local_merge preference, change to local\n"); + res = ast_sip_call_codec_str_to_pref(pref, "local", 1); + } else { + if (strcmp(ast_sip_call_codec_pref_to_str(*pref), "remote_merge") == 0) { + ast_debug(3, "text call with remote_merge preference, change to remote\n"); + res = ast_sip_call_codec_str_to_pref(pref, "remote", 1); + } + } + } + if (res != 0) { + ast_log(LOG_ERROR, "Could not fix preferences for real time text calls\n"); + } +} + struct ast_stream *ast_sip_session_create_joint_call_stream(const struct ast_sip_session *session, struct ast_stream *remote_stream) { struct ast_stream *joint_stream = ast_stream_clone(remote_stream, NULL); const struct ast_format_cap *remote = ast_stream_get_formats(remote_stream); enum ast_media_type media_type = ast_stream_get_type(remote_stream); + struct ast_format_cap *joint; - struct ast_format_cap *joint = ast_sip_create_joint_call_cap(remote, + fix_t140_red_pref(session, media_type); + joint = ast_sip_create_joint_call_cap(remote, session->endpoint->media.codecs, media_type, session->call_direction == AST_SIP_SESSION_OUTGOING_CALL ? session->endpoint->media.outgoing_call_offer_pref @@ -152,7 +178,10 @@ struct ast_format_cap *ast_sip_session_create_joint_call_cap( const struct ast_sip_session *session, enum ast_media_type media_type, const struct ast_format_cap *remote) { - struct ast_format_cap *joint = ast_sip_create_joint_call_cap(remote, + struct ast_format_cap *joint; + + fix_t140_red_pref(session, media_type); + joint = ast_sip_create_joint_call_cap(remote, session->endpoint->media.codecs, media_type, session->call_direction == AST_SIP_SESSION_OUTGOING_CALL ? session->endpoint->media.outgoing_call_offer_pref diff --git a/res/res_rtp_asterisk.c b/res/res_rtp_asterisk.c index b94a0fb40e..84f33c5ec6 100644 --- a/res/res_rtp_asterisk.c +++ b/res/res_rtp_asterisk.c @@ -618,9 +618,10 @@ struct ast_rtcp { struct rtp_red { struct ast_frame t140; /*!< Primary data */ struct ast_frame t140red; /*!< Redundant t140*/ - unsigned char pt[AST_RED_MAX_GENERATION]; /*!< Payload types for redundancy data */ - unsigned char ts[AST_RED_MAX_GENERATION]; /*!< Time stamps */ - unsigned char len[AST_RED_MAX_GENERATION]; /*!< length of each generation */ + /* We are encoding the primary payload type also in this array, so we need one more element */ + unsigned char pt[AST_RED_MAX_GENERATION+1]; /*!< Payload types for redundancy data */ + unsigned char ts[AST_RED_MAX_GENERATION+1]; /*!< Time stamps */ + unsigned char len[AST_RED_MAX_GENERATION+1]; /*!< length of each generation */ int num_gen; /*!< Number of generations */ int schedid; /*!< Timer id */ unsigned char t140red_data[64000]; @@ -5579,7 +5580,7 @@ static int ast_rtp_write(struct ast_rtp_instance *instance, struct ast_frame *fr } if (rtp->red) { - /* return 0; */ + ast_rtp_red_buffer(instance, frame); /* no primary data or generations to send */ if ((frame = red_t140_to_red(rtp->red)) == NULL) return 0; @@ -8008,9 +8009,11 @@ static struct ast_frame *ast_rtp_interpret(struct ast_rtp_instance *instance, st *data = 0xBD; } - if (ast_format_cmp(rtp->f.subclass.format, ast_format_t140_red) == AST_FORMAT_CMP_EQUAL) { - unsigned char *data = rtp->f.data.ptr; - unsigned char *header_end; + if (rtp->f.datalen <= 0) { + return AST_LIST_FIRST(&frames) ? AST_LIST_FIRST(&frames) : &ast_null_frame; + } else if (ast_format_cmp(rtp->f.subclass.format, ast_format_t140_red) == AST_FORMAT_CMP_EQUAL) { + unsigned char* data = rtp->f.data.ptr; + unsigned char* header_end; int num_generations; int header_length; int len; @@ -8053,6 +8056,9 @@ static struct ast_frame *ast_rtp_interpret(struct ast_rtp_instance *instance, st rtp->f.data.ptr += len; rtp->f.datalen -= len; } + if (rtp->f.datalen < 0) { + return AST_LIST_FIRST(&frames) ? AST_LIST_FIRST(&frames) : &ast_null_frame; + } } if (ast_format_get_type(rtp->f.subclass.format) == AST_MEDIA_TYPE_AUDIO) { @@ -9135,13 +9141,17 @@ static void ast_rtp_remote_address_set(struct ast_rtp_instance *instance, struct */ static int red_write(const void *data) { - struct ast_rtp_instance *instance = (struct ast_rtp_instance*) data; - struct ast_rtp *rtp = ast_rtp_instance_get_data(instance); + struct ast_rtp_instance *instance = (struct ast_rtp_instance *) data; + struct ast_rtp *rtp; ao2_lock(instance); - if (rtp->red->t140.datalen > 0) { - ast_rtp_write(instance, &rtp->red->t140); + rtp = ast_rtp_instance_get_data(instance); + if (!rtp || !rtp->red) { + ao2_unlock(instance); + ast_log(LOG_ERROR, "Could not get rtp from instance or RED handler\n"); + return 0; } + ao2_unlock(instance); return 1; @@ -9174,7 +9184,14 @@ static int rtp_red_init(struct ast_rtp_instance *instance, int buffer_time, int rtp->red->t140red_data[x*4] = rtp->red->pt[x]; } rtp->red->t140red_data[x*4] = rtp->red->pt[x] = payloads[x]; /* primary pt */ + ao2_ref(instance, +1); rtp->red->schedid = ast_sched_add(rtp->sched, buffer_time, red_write, instance); + if (rtp->red->schedid < 0) { + ao2_ref(instance, -1); + ast_log(LOG_WARNING, "scheduling red_write failed.\n"); + } + ast_debug(3, "Init RED for stream %s, primary payload %d with %d generations\n", + ast_rtp_instance_get_cname(instance), payloads[x], generations); return 0; } @@ -9191,6 +9208,14 @@ static int rtp_red_buffer(struct ast_rtp_instance *instance, struct ast_frame *f if (frame->datalen > 0) { if (red->t140.datalen > 0) { + /* + * Avoid merging command and regular T.140 text packets by flushing the + * previous T.140 packets. When realtime text packets are to be sent, the + * text is accumulated in a buffer and sent regularly by a timer. It can + * happen that commands such as a backspace, CR, or LF get merged with + * regular text. This breaks some UAs. In recent tests it could not be + * reproduced, but its probably better to keep it. + */ const unsigned char *primary = red->buf_data; /* There is something already in the T.140 buffer */ @@ -9379,7 +9404,9 @@ static void ast_rtp_stop(struct ast_rtp_instance *instance) if (rtp->red) { ao2_unlock(instance); - AST_SCHED_DEL(rtp->sched, rtp->red->schedid); + if (!ast_sched_del(rtp->sched, rtp->red->schedid)) { + ao2_ref(instance, -1); + } ao2_lock(instance); ast_free(rtp->red); rtp->red = NULL;