diff --git a/bridges/bridge_native_rtp.c b/bridges/bridge_native_rtp.c index efe476ecb22..b6702da7514 100644 --- a/bridges/bridge_native_rtp.c +++ b/bridges/bridge_native_rtp.c @@ -643,7 +643,7 @@ static int native_rtp_bridge_compatible_check(struct ast_bridge *bridge, struct RAII_VAR(struct rtp_glue_data *, glue0, NULL, rtp_glue_data_destroy); RAII_VAR(struct rtp_glue_data *, glue1, NULL, rtp_glue_data_destroy); - ast_debug(1, "Bridge '%s'. Checking compatability for channels '%s' and '%s'\n", + ast_debug(1, "Bridge '%s'. Checking compatibility for channels '%s' and '%s'\n", bridge->uniqueid, ast_channel_name(bc0->chan), ast_channel_name(bc1->chan)); if (!native_rtp_bridge_capable(bc0->chan)) { diff --git a/configure b/configure index 9bab0e6a73d..af5b9b79378 100755 --- a/configure +++ b/configure @@ -714,7 +714,7 @@ AST_NO_STRINGOP_TRUNCATION AST_NO_FORMAT_Y2K AST_NO_FORMAT_TRUNCATION AST_NO_STRICT_OVERFLOW -AST_FORTIFY_SOURCE +# AST_FORTIFY_SOURCE AST_TRAMPOLINES AST_DECLARATION_AFTER_STATEMENT AST_UNDEFINED_SANITIZER diff --git a/include/asterisk/res_pjsip.h b/include/asterisk/res_pjsip.h index 6234098749d..0910a7db84e 100644 --- a/include/asterisk/res_pjsip.h +++ b/include/asterisk/res_pjsip.h @@ -887,6 +887,26 @@ struct ast_sip_t38_configuration { unsigned int bind_udptl_to_media_address; }; +/*! + * \brief WebRTC Datachannel configuration for SIP endpoints + */ +struct ast_sip_webrtc_datachannel_configuration +{ + /*! Whether WebRTC Datachannel support is enabled or not */ + unsigned int enabled; + /*! Whether to use IPv6 for WebRTC Datachannel or not */ + unsigned int ipv6; +}; + +/*! + * \brief Websocket text configuration for SIP endpoints + */ +struct ast_sip_websocket_text_configuration +{ + /*! Whether websocket text support is enabled or not */ + unsigned int enabled; +}; + /*! * \brief Media configuration for SIP endpoints */ @@ -905,6 +925,10 @@ struct ast_sip_endpoint_media_configuration { struct ast_sip_direct_media_configuration direct_media; /*! T.38 (FoIP) options */ struct ast_sip_t38_configuration t38; + /*! WebRTC Datachannel configuration options */ + struct ast_sip_webrtc_datachannel_configuration webrtc_datachannel_configuration; + /*! Websocket text configuration options */ + struct ast_sip_websocket_text_configuration websocket_text_configuration; /*! Configured codecs */ struct ast_format_cap *codecs; /*! Capabilities in topology form */ @@ -921,7 +945,7 @@ struct ast_sip_endpoint_media_configuration { unsigned int tos_text; /*! Priority for text streams */ unsigned int cos_text; - /*! Indicate if text stream supports RED */ + /*! Flag indicate if text stream supports RED */ unsigned int red_enabled; /*! Is g.726 packed in a non standard way */ unsigned int g726_non_standard; diff --git a/include/asterisk/res_pjsip_session.h b/include/asterisk/res_pjsip_session.h index d287e968654..2afe5622512 100644 --- a/include/asterisk/res_pjsip_session.h +++ b/include/asterisk/res_pjsip_session.h @@ -51,6 +51,7 @@ struct pjmedia_sdp_media; struct pjmedia_sdp_session; struct ast_dsp; struct ast_udptl; +struct ast_websocket_session_text; /*! \brief T.38 states for a session */ enum ast_sip_session_t38state { @@ -127,6 +128,9 @@ struct ast_sip_session_media { char *remote_label; /*! \brief Stream name */ char *stream_name; + + /*! \brief Websocket session text instance itself */ + struct ast_websocket_session_text *websocket_session_text; }; /*! diff --git a/include/asterisk/strings.h b/include/asterisk/strings.h index 935c7e9236f..55ff22b3c2b 100644 --- a/include/asterisk/strings.h +++ b/include/asterisk/strings.h @@ -94,7 +94,7 @@ static force_inline int attribute_pure ast_strlen_zero(const char *s) \retval 1 if \a str begins with \a prefix \retval 0 otherwise. */ -static int force_inline attribute_pure ast_begins_with(const char *str, const char *prefix) +static force_inline int attribute_pure ast_begins_with(const char *str, const char *prefix) { ast_assert(str != NULL); ast_assert(prefix != NULL); @@ -113,7 +113,7 @@ static int force_inline attribute_pure ast_begins_with(const char *str, const ch \retval 1 if \a str ends with \a suffix \retval 0 otherwise. */ -static int force_inline attribute_pure ast_ends_with(const char *str, const char *suffix) +static force_inline int attribute_pure ast_ends_with(const char *str, const char *suffix) { size_t str_len; size_t suffix_len; diff --git a/main/channel.c b/main/channel.c index 796b07a259d..b305282eaf6 100644 --- a/main/channel.c +++ b/main/channel.c @@ -4818,6 +4818,7 @@ int ast_sendtext_data(struct ast_channel *chan, struct ast_msg_data *msg) f.datalen = body_len; f.mallocd = AST_MALLOCD_DATA; f.data.ptr = ast_strdup(body); + f.stream_num = ast_stream_get_position(ast_channel_get_default_stream(chan, AST_MEDIA_TYPE_TEXT)); if (f.data.ptr) { res = ast_channel_tech(chan)->write_text(chan, &f); } else { diff --git a/res/res_pjsip/pjsip_config.xml b/res/res_pjsip/pjsip_config.xml index d512f1f02db..dcfaee6cb4a 100644 --- a/res/res_pjsip/pjsip_config.xml +++ b/res/res_pjsip/pjsip_config.xml @@ -998,7 +998,7 @@ DSCP TOS bits for text streams - See https://wiki.asterisk.org/wiki/display/AST/IP+Quality+of+Service for more information about QoS settings + See https://docs.asterisk.org/Configuration/Channel-Drivers/IP-Quality-of-Service for more information about QoS settings @@ -1016,9 +1016,12 @@ Priority for text streams - See https://wiki.asterisk.org/wiki/display/AST/IP+Quality+of+Service for more information about QoS settings + See https://docs.asterisk.org/Configuration/Channel-Drivers/IP-Quality-of-Service for more information about QoS settings + + Allow use of websocket text for WebRTC traffic + Determines if endpoint is allowed to initiate subscriptions with Asterisk. diff --git a/res/res_pjsip/pjsip_configuration.c b/res/res_pjsip/pjsip_configuration.c index 655c23bf3d7..77033884525 100644 --- a/res/res_pjsip/pjsip_configuration.c +++ b/res/res_pjsip/pjsip_configuration.c @@ -2217,6 +2217,9 @@ int ast_res_pjsip_initialize_configuration(void) ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "named_call_group", "", named_groups_handler, named_callgroups_to_str, NULL, 0, 0); ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "named_pickup_group", "", named_groups_handler, named_pickupgroups_to_str, NULL, 0, 0); ast_sorcery_object_field_register(sip_sorcery, "endpoint", "device_state_busy_at", "0", OPT_UINT_T, 0, FLDSET(struct ast_sip_endpoint, devicestate_busy_at)); + + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "ws_text", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, media.websocket_text_configuration.enabled)); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "t38_udptl", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, media.t38.enabled)); ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "t38_udptl_ec", "none", t38udptl_ec_handler, t38udptl_ec_to_str, NULL, 0, 0); ast_sorcery_object_field_register(sip_sorcery, "endpoint", "t38_udptl_maxdatagram", "0", OPT_UINT_T, 0, FLDSET(struct ast_sip_endpoint, media.t38.maxdatagram)); diff --git a/res/res_pjsip/pjsip_manager.xml b/res/res_pjsip/pjsip_manager.xml index a66e7faaa28..2984ce2c490 100644 --- a/res/res_pjsip/pjsip_manager.xml +++ b/res/res_pjsip/pjsip_manager.xml @@ -252,6 +252,9 @@ + + + diff --git a/res/res_pjsip_sdp_rtp.c b/res/res_pjsip_sdp_rtp.c index 604c927cf94..aa7f91f32e4 100644 --- a/res/res_pjsip_sdp_rtp.c +++ b/res/res_pjsip_sdp_rtp.c @@ -384,7 +384,7 @@ static void get_codecs(struct ast_sip_session *session, const struct pjmedia_sdp red_cp = strtok_r(NULL, "/", &rest); } - if (++red_num_gen > 0) { + if (red_num_gen > 0) { ast_log(AST_LOG_NOTICE, "T.140/RED enabled (pt=%d) with %d generations\n", num, red_num_gen); session->endpoint->media.red_enabled = 1; ast_rtp_red_init(session_media->rtp, 300, red_data_pt, red_num_gen); @@ -975,7 +975,7 @@ static enum ast_sip_session_media_encryption get_media_encryption_type(pj_str_t *optimistic = 0; - if (!transport_str) { + if (!transport_str || !strstr(transport_str, "AVP")) { return AST_SIP_MEDIA_TRANSPORT_INVALID; } if (strstr(transport_str, "UDP/TLS")) { @@ -1536,6 +1536,12 @@ static int negotiate_incoming_sdp_stream(struct ast_sip_session *session, SCOPE_EXIT_RTN_VALUE(0, "Endpoint has no codecs\n"); } + RAII_VAR(char *, transport_str, ast_strndup(stream->desc.transport.ptr, stream->desc.transport.slen), ast_free); + + if (!transport_str || !strstr(transport_str, "AVP")) { + SCOPE_EXIT_RTN_VALUE(0, "Incompatible transport\n"); + } + /* Ensure incoming transport is compatible with the endpoint's configuration */ if (!session->endpoint->media.rtp.use_received_transport) { encryption = check_endpoint_media_transport(session->endpoint, stream); @@ -1974,6 +1980,10 @@ static int create_outgoing_sdp_stream(struct ast_sip_session *session, struct as media->attr[media->attr_count++] = attr; } + if (media_type == AST_MEDIA_TYPE_TEXT) { + ast_debug(3, "SDP generate RTP frame text %d %s\n", rtp_code, ast_format_get_codec_name(format)); + } + if (media_type == AST_MEDIA_TYPE_TEXT && !strcasecmp(ast_format_get_codec_name(format), "red")) { // TODO jpb: En attendant de faire mieux. if (rtp_code == 105 || rtp_code == 96) { @@ -2150,6 +2160,12 @@ static int apply_negotiated_sdp_stream(struct ast_sip_session *session, SCOPE_EXIT_RTN_VALUE(1, "No channel\n"); } + RAII_VAR(char *, transport_str, ast_strndup(remote_stream->desc.transport.ptr, remote_stream->desc.transport.slen), ast_free); + + if (!transport_str || !strstr(transport_str, "AVP")) { + SCOPE_EXIT_RTN_VALUE(0, "Incompatible transport\n"); + } + /* Ensure incoming transport is compatible with the endpoint's configuration */ if (!session->endpoint->media.rtp.use_received_transport && check_endpoint_media_transport(session->endpoint, remote_stream) == AST_SIP_MEDIA_TRANSPORT_INVALID) { @@ -2347,7 +2363,7 @@ static void stream_destroy(struct ast_sip_session_media *session_media) /*! \brief SDP handler for 'audio' media stream */ static struct ast_sip_session_sdp_handler audio_sdp_handler = { - .id = STR_AUDIO, + .id = "audio_rtp", //STR_AUDIO, .negotiate_incoming_sdp_stream = negotiate_incoming_sdp_stream, .create_outgoing_sdp_stream = create_outgoing_sdp_stream, .apply_negotiated_sdp_stream = apply_negotiated_sdp_stream, @@ -2358,7 +2374,7 @@ static struct ast_sip_session_sdp_handler audio_sdp_handler = { /*! \brief SDP handler for 'video' media stream */ static struct ast_sip_session_sdp_handler video_sdp_handler = { - .id = STR_VIDEO, + .id = "video_rtp", //STR_VIDEO, .negotiate_incoming_sdp_stream = negotiate_incoming_sdp_stream, .create_outgoing_sdp_stream = create_outgoing_sdp_stream, .apply_negotiated_sdp_stream = apply_negotiated_sdp_stream, @@ -2370,7 +2386,7 @@ static struct ast_sip_session_sdp_handler video_sdp_handler = { /*! \brief SDP handler for 'text' media stream */ static struct ast_sip_session_sdp_handler text_sdp_handler = { - .id = STR_TEXT, + .id = "test_rtp", //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, @@ -2458,7 +2474,7 @@ static int load_module(void) ast_log(LOG_ERROR, "Unable to register SDP handler for %s stream type\n", STR_VIDEO); 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; diff --git a/res/res_pjsip_session.c b/res/res_pjsip_session.c index 66e492dc1a8..ff3c4d182da 100644 --- a/res/res_pjsip_session.c +++ b/res/res_pjsip_session.c @@ -59,8 +59,8 @@ #define MOD_DATA_ON_RESPONSE "on_response" #define MOD_DATA_NAT_HOOK "nat_hook" -/* Most common case is one audio and one video stream */ -#define DEFAULT_NUM_SESSION_MEDIA 2 +/* Most common case is one audio, one video stream and one text stream */ +#define DEFAULT_NUM_SESSION_MEDIA 3 /* Some forward declarations */ static void handle_session_begin(struct ast_sip_session *session); diff --git a/res/res_pjsip_websocket_text.c b/res/res_pjsip_websocket_text.c new file mode 100644 index 00000000000..f36704af1f0 --- /dev/null +++ b/res/res_pjsip_websocket_text.c @@ -0,0 +1,913 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2024, IVèS. + * + * Jean-Pierre BROCHET + * + * 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 + * + * \author Jean-Pierre BROCHET + * + * \brief Websocket Text handling + */ + +/*** MODULEINFO + pjproject + res_pjsip + res_pjsip_session + res_pjsip_sdp_rtp + res_http_websocket + core + ***/ + +#include "asterisk.h" + +#include +#include +#include +#include + +#include "asterisk/module.h" +#include "asterisk/astobj2.h" +#include "asterisk/strings.h" +#include "asterisk/file.h" + +#include "asterisk/channel.h" +#include "asterisk/stream.h" +#include "asterisk/format_cache.h" +#include "asterisk/http.h" +#include "asterisk/http_websocket.h" + +#include "asterisk/res_pjsip.h" +#include "asterisk/res_pjsip_session.h" +#include "asterisk/res_pjsip_session_caps.h" + +static const char STR_TEXT[] = "text"; + +/*! \brief Websocket Text information */ +struct ast_websocket_session_text +{ + char id[256]; + int pipe_fds[2]; + struct ast_websocket *websocket; + + AST_LIST_ENTRY(ast_websocket_session_text) entry; +}; + +static AST_LIST_HEAD(websocket_session_text_list, ast_websocket_session_text) websocket_session_text_list; + +#define WEBSOCKET_HOSTNAME_MAX_LENGTH 255 + +// Structure pour stocker les valeurs de configuration +struct websocket_session_text_config +{ + int port; + char hostname[WEBSOCKET_HOSTNAME_MAX_LENGTH + 1]; +}; + +static int get_websocket_tls_port(void) +{ + struct ast_config *cfg; + struct ast_flags config_flags = {0}; + int websocket_tls_port = 8089; + + // Charger le fichier de configuration http.conf + cfg = ast_config_load("http.conf", config_flags); + if (cfg) { + const char *port_str = ast_variable_retrieve(cfg, "general", "tlsbindaddr"); + if (port_str) { + const char *port_start = strrchr(port_str, ':'); + if (port_start) { + websocket_tls_port = atoi(port_start + 1); + } + } + + ast_config_destroy(cfg); + } else { + ast_log(LOG_ERROR, "Failed to load http.conf\n"); + } + + return websocket_tls_port; +} + +// Function to load the module configuration +// 'config' is passed as a pointer to avoid dynamic memory allocation +static int load_websocket_session_text_config(struct websocket_session_text_config *config) { + struct ast_config *cfg; + const char *port_str, *hostname; + struct ast_flags config_flags = {0}; + + // Ensure the config pointer is not NULL to prevent crashes + if (config == NULL) { + ast_log(LOG_ERROR, "Null pointer passed to load res_pjsip_websocket_text.conf\n"); + return -2; // Return error if the pointer is NULL + } else { + const pj_str_t *pj_hostname = pj_gethostname(); + + // Default to "localhost" if no hostname is specified + strncpy(config->hostname, pj_hostname ? pj_hostname->ptr : "localhost", WEBSOCKET_HOSTNAME_MAX_LENGTH); + config->hostname[WEBSOCKET_HOSTNAME_MAX_LENGTH] = '\0'; // Ensure null termination + + config->port = get_websocket_tls_port(); // Default port if not specified + } + + // Load the configuration file res_pjsip_websocket_text.conf + cfg = ast_config_load("res_pjsip_websocket_text.conf", config_flags); + if (!cfg || cfg == CONFIG_STATUS_FILEINVALID) { + ast_log(LOG_ERROR, "Failed to load res_pjsip_websocket_text.conf\n"); + return 1; // Return warning if file loading fails + } + + // Retrieve the 'port' value from the [general] section of the config + if ((port_str = ast_variable_retrieve(cfg, "general", "port")) != NULL) { + config->port = atoi(port_str); + } + + // Retrieve the 'hostname' value from the [general] section of the config + if ((hostname = ast_variable_retrieve(cfg, "general", "hostname")) != NULL) { + // Copy the hostname into the structure, ensuring no buffer overflow + strncpy(config->hostname, hostname, WEBSOCKET_HOSTNAME_MAX_LENGTH); + config->hostname[WEBSOCKET_HOSTNAME_MAX_LENGTH] = '\0'; // Ensure null termination + } + + // Destroy the config structure after we're done processing + ast_config_destroy(cfg); + + return 0; // Return success +} + +/*! \brief Supplement for adding framehook to sip_session channel */ +static struct ast_sip_session_supplement websocket_session_text_supplement = { + .method = "INVITE", + .priority = AST_SIP_SUPPLEMENT_PRIORITY_CHANNEL + 1, +}; + +static int ast_websocket_session_text_fd(const struct ast_websocket_session_text *ws_text) +{ + return ws_text->pipe_fds[0]; +} + +/*! \brief Destructor for T.38 state information */ +static void ast_websocket_session_text_destroy(void *obj) +{ + ast_free(obj); +} + +static struct ast_format_cap *set_incoming_call_offer_cap( + struct ast_sip_session *session, struct ast_sip_session_media *session_media, + const struct pjmedia_sdp_media *stream) +{ + struct ast_format_cap *incoming_call_offer_cap; + struct ast_format_cap *remote; + SCOPE_ENTER(1, "%s\n", ast_sip_session_get_name(session)); + + remote = ast_format_cap_alloc(AST_FORMAT_CAP_FLAG_DEFAULT); + if (!remote) { + ast_log(LOG_ERROR, "Failed to allocate %s incoming remote capabilities\n", + ast_codec_media_type2str(session_media->type)); + SCOPE_EXIT_RTN_VALUE(NULL, "Couldn't allocate caps\n"); + } + + // Ajouter le format T.140/RED + if (ast_format_cap_append(remote, ast_format_t140_red, 0) != 0) { + ast_log(LOG_ERROR, "Failed to add T.140/RED format\n"); + SCOPE_EXIT_RTN_VALUE(NULL, "Impossible to add T.140/RED in call offer caps\n"); + } + + // Ajouter le format T.140 + if (ast_format_cap_append(remote, ast_format_t140, 0) != 0) { + ast_log(LOG_ERROR, "Failed to add T.140 format\n"); + SCOPE_EXIT_RTN_VALUE(NULL, "Impossible to add t140 in call offer caps\n"); + } + + incoming_call_offer_cap = ast_sip_session_create_joint_call_cap( + session, session_media->type, remote); + + ao2_ref(remote, -1); + + if (!incoming_call_offer_cap || ast_format_cap_empty(incoming_call_offer_cap)) { + ao2_cleanup(incoming_call_offer_cap); + SCOPE_EXIT_RTN_VALUE(NULL, "No incoming call offer caps\n"); + } + + SCOPE_EXIT_RTN_VALUE(incoming_call_offer_cap); +} + +/*! \brief Function which negotiates an incoming media stream */ +static int negotiate_incoming_sdp_stream(struct ast_sip_session *sip_session, + struct ast_sip_session_media *sip_session_media, const struct pjmedia_sdp_session *sdp, + int index, struct ast_stream *asterisk_stream) +{ + char host[NI_MAXHOST]; + pjmedia_sdp_media *stream = sdp->media[index]; + struct ast_format_cap *joint; + RAII_VAR(struct ast_sockaddr *, addrs, NULL, ast_free); + SCOPE_ENTER(1, "%s\n", ast_sip_session_get_name(sip_session)); + + if (!sip_session->endpoint->media.websocket_text_configuration.enabled) { + SCOPE_EXIT_RTN_VALUE(0, "Declining: websocket text configuration not enabled on sip_session\n"); + } + + ast_copy_pj_str(host, stream->conn ? &stream->conn->addr : &sdp->conn->addr, sizeof(host)); + + /* Ensure that the address provided is valid */ + if (ast_sockaddr_resolve(&addrs, host, PARSE_PORT_FORBID, AST_AF_UNSPEC) <= 0) { + /* The provided host was actually invalid so we error out this negotiation */ + SCOPE_EXIT_RTN_VALUE(0, "Declining: provided host is invalid\n"); + } + + /* If no type formats have been configured reject this stream */ + if (!ast_format_cap_has_type(sip_session->endpoint->media.codecs, sip_session_media->type)) { + ast_debug(3, "Endpoint has no codecs for media type '%s', declining stream\n", + ast_codec_media_type2str(sip_session_media->type)); + SCOPE_EXIT_RTN_VALUE(0, "Endpoint has no codecs\n"); + } + + RAII_VAR(char *, transport_str, ast_strndup(stream->desc.transport.ptr, stream->desc.transport.slen), ast_free); + + if (!transport_str || !strstr(transport_str, "TCP/WSS")) { + SCOPE_EXIT_RTN_VALUE(0, "Incompatible transport\n"); + } + + if (sip_session->inv_session) { + if (!sip_session_media->websocket_session_text) { + sip_session_media->websocket_session_text = ast_calloc(1, sizeof(*sip_session_media->websocket_session_text)); + if (!sip_session_media->websocket_session_text) { + SCOPE_EXIT_RTN_VALUE(-1, "Failed to allocate memory for websocket session\n"); + } + + strcpy(sip_session_media->websocket_session_text->id, sip_session->inv_session->obj_name + strlen("inv0x")); + + AST_LIST_LOCK(&websocket_session_text_list); + AST_LIST_INSERT_HEAD(&websocket_session_text_list, sip_session_media->websocket_session_text, entry); + AST_LIST_UNLOCK(&websocket_session_text_list); + + ast_debug(3, "websocket negotiate_incoming_sdp_stream created '%s' with id %s\n", ast_codec_media_type2str(sip_session_media->type), sip_session_media->websocket_session_text->id); + } else { + ast_debug(3, "websocket negotiate_incoming_sdp_stream already created '%s' with id %s\n", ast_codec_media_type2str(sip_session_media->type), sip_session_media->websocket_session_text->id); + } + } else { + SCOPE_EXIT_RTN_VALUE(-1, "Failed to generate id for websocket session\n"); + } + + joint = set_incoming_call_offer_cap(sip_session, sip_session_media, stream); + ast_stream_set_formats(asterisk_stream, joint); + ao2_cleanup(joint); + + ast_stream_set_state(asterisk_stream, AST_STREAM_STATE_SENDRECV); + + SCOPE_EXIT_RTN_VALUE(1); +} + +/*! \brief Function which creates an outgoing stream */ +static int create_outgoing_sdp_stream(struct ast_sip_session *sip_session, struct ast_sip_session_media *sip_session_media, + struct pjmedia_sdp_session *sdp, const struct pjmedia_sdp_session *remote, struct ast_stream *asterisk_stream) +{ + pj_pool_t *pool = sip_session->inv_session->pool_prov; + static const pj_str_t STR_TCP_WSS = {"TCP/WSS", 7}; + static const pj_str_t STR_T140 = {"t140", 4}; + pjmedia_sdp_media *media; + char tmp[512]; + + SCOPE_ENTER(1, "%s Type: %s %s\n", ast_sip_session_get_name(sip_session), + ast_codec_media_type2str(sip_session_media->type), ast_str_tmp(128, ast_stream_to_str(asterisk_stream, &STR_TMP))); + + if (!sip_session->endpoint->media.websocket_text_configuration.enabled) { + SCOPE_EXIT_RTN_VALUE(1, "Not creating outgoing SDP stream: websocket text not enabled\n"); + } + + if (sip_session->inv_session) { + if (!sip_session_media->websocket_session_text) { + sip_session_media->websocket_session_text = ast_calloc(1, sizeof(*sip_session_media->websocket_session_text)); + if (!sip_session_media->websocket_session_text) { + SCOPE_EXIT_RTN_VALUE(-1, "Failed to allocate memory for websocket session\n"); + } + + strcpy(sip_session_media->websocket_session_text->id, sip_session->inv_session->obj_name + strlen("inv0x")); + + AST_LIST_LOCK(&websocket_session_text_list); + AST_LIST_INSERT_HEAD(&websocket_session_text_list, sip_session_media->websocket_session_text, entry); + AST_LIST_UNLOCK(&websocket_session_text_list); + + ast_debug(3, "websocket create_outgoing_sdp_stream created '%s' with id %s\n", ast_codec_media_type2str(sip_session_media->type), sip_session_media->websocket_session_text->id); + } else { + ast_debug(3, "websocket create_outgoing_sdp_stream already created '%s' with id %s\n", ast_codec_media_type2str(sip_session_media->type), sip_session_media->websocket_session_text->id); + } + } else { + SCOPE_EXIT_RTN_VALUE(-1, "Failed to generate id for websocket session\n"); + } + + pjmedia_sdp_attr *attr; + struct websocket_session_text_config config; + + // Load the configuration into the 'config' structure + load_websocket_session_text_config(&config); + + if (!(media = pj_pool_zalloc(pool, sizeof(struct pjmedia_sdp_media)))) { + SCOPE_EXIT_RTN_VALUE(-1, "Pool alloc failure\n"); + } + + pj_strdup2(pool, &media->desc.media, ast_codec_media_type2str(sip_session_media->type)); + + media->desc.transport = STR_TCP_WSS; + media->desc.port = config.port; + media->desc.port_count = 1; + + media->desc.fmt[media->desc.fmt_count++] = STR_T140; + + if (sip_session->inv_session) { + snprintf(tmp, sizeof(tmp), "wss://%s:%d/ws_text/%s", config.hostname, media->desc.port, sip_session->inv_session->obj_name + strlen( "inv0x")); + } else { + snprintf(tmp, sizeof(tmp), "wss://%s:%d/ws_text/%s", config.hostname, media->desc.port, "channel_demo"); + } + + attr = pjmedia_sdp_attr_create(pool, tmp, NULL); + media->attr[media->attr_count++] = attr; + + sdp->media[sdp->media_count++] = media; + + SCOPE_EXIT_RTN_VALUE(1, "RC: 1\n"); +} + +static struct ast_frame *media_sip_session_websocket_session_text_read_callback(struct ast_sip_session *sip_session, struct ast_sip_session_media *sip_session_media) +{ + struct ast_frame *frame = NULL; + + if (!sip_session_media->websocket_session_text) { + return &ast_null_frame; + } + + ssize_t bytes_read; + size_t buffer_capacity = 396; + char *final_buffer = NULL; + size_t total_length = 0; + int done = 0; + + final_buffer = (char *)ast_malloc(buffer_capacity); + if (final_buffer == NULL) { + ast_log(LOG_ERROR, "websocket text malloc failed\n"); + } + + while (!done) { + if (total_length >= buffer_capacity) { + buffer_capacity *= 2; + final_buffer = (char *)ast_realloc(final_buffer, buffer_capacity); + if (final_buffer == NULL) { + ast_log(LOG_ERROR, "websocket text realloc failed\n"); + total_length = 0; + done = 1; + } + } + + bytes_read = read(ast_websocket_session_text_fd(sip_session_media->websocket_session_text), final_buffer + total_length, buffer_capacity - total_length); + if (bytes_read > 0) { + ast_debug(3, "Reading websocket text string, length %" PRIu64 "\n", bytes_read); + total_length += bytes_read; + if (bytes_read < (ssize_t)(buffer_capacity - total_length)) { + done = 1; + } + done = 1; + + } else if (bytes_read == 0) { + ast_log(LOG_WARNING, "Reading websocket text EOF\n"); + done = 1; + } else { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + ast_log(LOG_WARNING, "websocket text read no additional data available\n"); + done = 1; + } else { + ast_log(LOG_ERROR, "websocket text read failed: %s\n", strerror(errno)); + done = 1; + } + } + } + + if (total_length > 0) { + struct ast_websocket_session_text *ws_session = NULL; + + AST_LIST_LOCK(&websocket_session_text_list); + AST_LIST_TRAVERSE(&websocket_session_text_list, ws_session, entry) + { + if (!strcmp(ws_session->id, sip_session->inv_session->obj_name + strlen("inv0x"))) { + struct ast_frame f_in; + + memset(&f_in, 0, sizeof(f_in)); + f_in.frametype = AST_FRAME_TEXT; + //f_in.subclass.format = ast_format_none; + //f_in.subclass.format = ast_format_t140; + f_in.subclass.format = ast_format_t140_red; + f_in.datalen = total_length; + f_in.data.ptr = (void *)final_buffer; + + frame = ast_frdup(&f_in); + + ast_debug(3, "Reading websocket text string, total length %" PRIu64 "\n", total_length); + + break; + } + } + AST_LIST_UNLOCK(&websocket_session_text_list); + + ast_free(final_buffer); + } + + if (!frame) { + ast_log(LOG_ERROR, "websocket text session or frame not found, length %" PRIu64 " loss\n", total_length); + return NULL; // Error. + } + + frame->stream_num = sip_session_media->stream_num; + + return frame; +} + +static int media_sip_session_websocket_session_text_write_callback(struct ast_sip_session *sip_session, struct ast_sip_session_media *sip_session_media, struct ast_frame *frame) +{ + if (!sip_session_media->websocket_session_text) { + return 0; + } + + if (frame && frame->frametype == AST_FRAME_TEXT) { + struct ast_websocket *websocket = NULL; + struct ast_websocket_session_text *ws_session = NULL; + + // Searching for the websocket session using the asterisk channel name retrieved from the sip session. + AST_LIST_LOCK(&websocket_session_text_list); + AST_LIST_TRAVERSE(&websocket_session_text_list, ws_session, entry) + { + if (!strcmp(ws_session->id, sip_session->inv_session->obj_name + strlen("inv0x"))) { + websocket = ws_session->websocket; + break; + } + } + AST_LIST_UNLOCK(&websocket_session_text_list); + + if (frame->datalen > 0) { + char *text = frame->data.ptr; + + if (text[frame->datalen - 1] != '\0') { + /* Not zero terminated, we need to allocate */ + text = ast_strndup(text, frame->datalen); + } + + if (text) { + uint64_t text_len = strlen(text); + if (websocket) { + enum ast_websocket_opcode opcode = AST_WEBSOCKET_OPCODE_TEXT; + + ast_websocket_write(websocket, opcode, text, text_len); + } else { + ast_log(LOG_ERROR, "websocket text session %s not found, length %" PRIu64 " loss\n", sip_session->inv_session->obj_name + strlen("inv0x"), text_len); + } + + if (text != frame->data.ptr) { + /* Only free if we allocated */ + ast_free(text); + } + } + } + } + + return 0; +} + +static int set_caps(struct ast_sip_session *session, + struct ast_sip_session_media *session_media, + const struct pjmedia_sdp_media *stream, + int is_offer, struct ast_stream *asterisk_stream) +{ + RAII_VAR(struct ast_format_cap *, caps, NULL, ao2_cleanup); + RAII_VAR(struct ast_format_cap *, peer, NULL, ao2_cleanup); + RAII_VAR(struct ast_format_cap *, joint, NULL, ao2_cleanup); + enum ast_media_type media_type = session_media->type; + int direct_media_enabled = !ast_sockaddr_isnull(&session_media->direct_media_addr) && + ast_format_cap_count(session->direct_media_cap); + SCOPE_ENTER(1, "%s %s\n", ast_sip_session_get_name(session), is_offer ? "OFFER" : "ANSWER"); + + if (!(caps = ast_format_cap_alloc(AST_FORMAT_CAP_FLAG_DEFAULT)) || + !(peer = ast_format_cap_alloc(AST_FORMAT_CAP_FLAG_DEFAULT)) || + !(joint = ast_format_cap_alloc(AST_FORMAT_CAP_FLAG_DEFAULT))) { + ast_log(LOG_ERROR, "Failed to allocate %s capabilities\n", + ast_codec_media_type2str(session_media->type)); + SCOPE_EXIT_RTN_VALUE(-1, "Couldn't create %s capabilities\n", + ast_codec_media_type2str(session_media->type)); + } + + /* get the endpoint capabilities */ + if (direct_media_enabled) { + ast_format_cap_get_compatible(session->endpoint->media.codecs, session->direct_media_cap, caps); + } else { + ast_format_cap_append_from_cap(caps, session->endpoint->media.codecs, media_type); + } + + // Add T.140/RED format + if (ast_format_cap_append(peer, ast_format_t140_red, 0) != 0) { + ast_log(LOG_ERROR, "Failed to add T.140/RED format\n"); + SCOPE_EXIT_RTN_VALUE(-1, "Impossible to add T.140/RED in call offer caps\n"); + } + + // Add T.140 format + if (ast_format_cap_append(peer, ast_format_t140, 0) != 0) { + ast_log(LOG_ERROR, "Failed to add T.140 format\n"); + SCOPE_EXIT_RTN_VALUE(-1, "Impossible to add t140 in call offer caps\n"); + } + + /* get the joint capabilities between peer and endpoint */ + ast_format_cap_get_compatible(caps, peer, joint); + + ast_stream_set_formats(asterisk_stream, joint); + + if (session->channel && ast_sip_session_is_pending_stream_default(session, asterisk_stream)) { + ast_channel_lock(session->channel); + + ast_format_cap_remove_by_type(caps, AST_MEDIA_TYPE_UNKNOWN); + ast_format_cap_append_from_cap(caps, ast_channel_nativeformats(session->channel), + AST_MEDIA_TYPE_UNKNOWN); + ast_format_cap_remove_by_type(caps, media_type); + + ast_format_cap_append_from_cap(caps, joint, media_type); + + /* + * Apply the new formats to the channel, potentially changing + * raw read/write formats and translation path while doing so. + */ + ast_channel_nativeformats_set(session->channel, caps); + + if (ast_channel_is_bridged(session->channel)) { + ast_channel_set_unbridged_nolock(session->channel, 1); + } + + ast_channel_unlock(session->channel); + } + + SCOPE_EXIT_RTN_VALUE(0); +} + +/*! \brief Function which applies a negotiated stream */ +static int apply_negotiated_sdp_stream(struct ast_sip_session *sip_session, + struct ast_sip_session_media *sip_session_media, const struct pjmedia_sdp_session *local, + const struct pjmedia_sdp_session *remote, int index, struct ast_stream *asterisk_stream) +{ + RAII_VAR(struct ast_sockaddr *, addrs, NULL, ast_free); + pjmedia_sdp_media *remote_stream = remote->media[index]; + char host[NI_MAXHOST]; + SCOPE_ENTER(1, "%s Stream: %s\n", ast_sip_session_get_name(sip_session), + ast_str_tmp(128, ast_stream_to_str(asterisk_stream, &STR_TMP))); + + if (!sip_session->channel) { + SCOPE_EXIT_RTN_VALUE(1, "No channel\n"); + } + + RAII_VAR(char *, transport_str, ast_strndup(remote_stream->desc.transport.ptr, remote_stream->desc.transport.slen), ast_free); + + if (!transport_str || !strstr(transport_str, "TCP/WSS")) { + SCOPE_EXIT_RTN_VALUE(0, "Incompatible transport\n"); + } + + ast_copy_pj_str(host, remote_stream->conn ? &remote_stream->conn->addr : &remote->conn->addr, sizeof(host)); + + /* Ensure that the address provided is valid */ + if (ast_sockaddr_resolve(&addrs, host, PARSE_PORT_FORBID, AST_AF_UNSPEC) <= 0) { + /* The provided host was actually invalid so we error out this negotiation */ + SCOPE_EXIT_RTN_VALUE(-1, "Not applying negotiated SDP stream: failed to resolve remote stream host\n"); + } + + if (sip_session->inv_session) { + if (!sip_session_media->websocket_session_text) { + sip_session_media->websocket_session_text = ast_calloc(1, sizeof(*sip_session_media->websocket_session_text)); + if (!sip_session_media->websocket_session_text) { + SCOPE_EXIT_RTN_VALUE(-1, "Failed to allocate memory for websocket session\n"); + } + + strcpy(sip_session_media->websocket_session_text->id, sip_session->inv_session->obj_name + strlen("inv0x")); + + AST_LIST_LOCK(&websocket_session_text_list); + AST_LIST_INSERT_HEAD(&websocket_session_text_list, sip_session_media->websocket_session_text, entry); + AST_LIST_UNLOCK(&websocket_session_text_list); + + ast_debug(3, "websocket apply_negotiated_sdp_stream created '%s' with id %s\n", ast_codec_media_type2str(sip_session_media->type), sip_session_media->websocket_session_text->id); + } else { + ast_debug(3, "websocket apply_negotiated_sdp_stream already created '%s' with id %s\n", ast_codec_media_type2str(sip_session_media->type), sip_session_media->websocket_session_text->id); + } + } else { + SCOPE_EXIT_RTN_VALUE(-1, "Failed to generate id for websocket session\n"); + } + + ast_sip_session_media_set_write_callback(sip_session, sip_session_media, media_sip_session_websocket_session_text_write_callback); + + if (pipe(sip_session_media->websocket_session_text->pipe_fds) == -1 || sip_session_media->websocket_session_text == NULL) { + SCOPE_EXIT_RTN_VALUE(-1, "pipe create to exchange frames failed\n"); + } else { + ast_fd_set_flags(ast_websocket_session_text_fd(sip_session_media->websocket_session_text), O_NONBLOCK); + + ast_sip_session_media_add_read_callback(sip_session + , sip_session_media + , ast_websocket_session_text_fd(sip_session_media->websocket_session_text) + , media_sip_session_websocket_session_text_read_callback + ); + } + + if (set_caps(sip_session, sip_session_media, remote_stream, 0, asterisk_stream)) { + SCOPE_EXIT_RTN_VALUE(-1, "set_caps failed\n"); + } + + SCOPE_EXIT_RTN_VALUE(1, "Handled\n"); +} + +/*! \brief Function which destroys the Websocket Text instance when sip_session ends */ +static void stream_destroy(struct ast_sip_session_media *sip_session_media) +{ + if (sip_session_media->websocket_session_text) { + struct ast_websocket_session_text *ws_session = NULL; + + AST_LIST_LOCK(&websocket_session_text_list); + AST_LIST_TRAVERSE_SAFE_BEGIN(&websocket_session_text_list, ws_session, entry) + { + if (sip_session_media->websocket_session_text == ws_session) { + AST_LIST_REMOVE_CURRENT(entry); + + if (ws_session->websocket) { + ast_debug(3, "websocket text unref websocket with id %s\n", sip_session_media->websocket_session_text->id); + + ast_websocket_unref(ws_session->websocket); + ws_session->websocket = NULL; + } + ast_debug(3, "websocket text deleted with id %s\n", sip_session_media->websocket_session_text->id); + break; + } + } + AST_LIST_TRAVERSE_SAFE_END; + AST_LIST_UNLOCK(&websocket_session_text_list); + + if (sip_session_media->websocket_session_text->pipe_fds[0] > 0) { + close(sip_session_media->websocket_session_text->pipe_fds[0]); + sip_session_media->websocket_session_text->pipe_fds[0] = -1; + } + if (sip_session_media->websocket_session_text->pipe_fds[1] > 0) { + close(sip_session_media->websocket_session_text->pipe_fds[1]); + sip_session_media->websocket_session_text->pipe_fds[1] = -1; + } + + ast_websocket_session_text_destroy(sip_session_media->websocket_session_text); + } + + sip_session_media->websocket_session_text = NULL; +} + +/*! \brief SDP handler for 'application' media stream */ +static struct ast_sip_session_sdp_handler text_sdp_handler = { + .id = "test_ws", //STR_TEXT, + .defer_incoming_sdp_stream = NULL, + .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 = NULL, + .stream_stop = NULL, + .stream_destroy = stream_destroy, +}; + +//========== WEBSOCKET SERVER ========== +//====================================== + +/* Function to add a variable to a list */ +static void add_variable_to_list(struct ast_variable **head, const char *name, const char *value) +{ + struct ast_variable *new_var, *last; + + new_var = ast_variable_new(name, value, ""); + if (!new_var) { + ast_log(LOG_ERROR, "Failed to create new variable\n"); + return; + } + + if (*head == NULL) { + *head = new_var; + } else { + last = *head; + while (last->next) { + last = last->next; + } + last->next = new_var; + } + + ast_log(LOG_NOTICE, "Variable added: %s=%s\n", name, value); +} + +static int websocket_text_t140_uri_cb(struct ast_tcptls_session_instance *ser + , const struct ast_http_uri *urih + , const char *uri + , enum ast_http_method method + , struct ast_variable *get_params + , struct ast_variable *headers + ) +{ + struct ast_websocket_session_text *ws_session = NULL; + + ast_debug(1, "Entering websocket text t140 loop method %s uri %s\n", ast_get_http_method(method), uri); + + // Searching for the websocket session using the asterisk channel name retrieved from the sip session. + AST_LIST_LOCK(&websocket_session_text_list); + AST_LIST_TRAVERSE(&websocket_session_text_list, ws_session, entry) + { + if (!strcmp(ws_session->id, uri)) { + break; + } + } + AST_LIST_UNLOCK(&websocket_session_text_list); + + if (!ws_session) { + ast_http_error(ser, 403, "Access Denied", "You do not have permission to access the requested URL."); + return 0; + } + + add_variable_to_list(&get_params, "uri", uri); + + return ast_websocket_uri_cb(ser, urih, uri, method, get_params, headers); +} + +static struct ast_http_uri websocket_text_t140_uri = { + .callback = websocket_text_t140_uri_cb, + .description = "Asterisk HTTP WebSocket text t140", + .uri = "ws_text", + .has_subtree = 1, + .data = NULL, + .key = __FILE__, +}; + +/*! \brief Simple echo implementation which echoes received text and binary frames */ +static void websocket_session_text_t140_callback(struct ast_websocket *websocket, struct ast_variable *parameters, struct ast_variable *headers) +{ + int res; + struct ast_websocket_session_text *ws_session = NULL; + + ast_debug(1, "Entering websocket text t140 loop %s, addr remote %s and local %s\n" + , ast_websocket_session_id(websocket) + , ast_sockaddr_stringify(ast_websocket_remote_address(websocket)) + , ast_sockaddr_stringify(ast_websocket_local_address(websocket)) + ); + + struct ast_variable *i; + for (i = parameters; i; i = i->next) { + if (!strcmp(i->name, "uri")) { + AST_LIST_LOCK(&websocket_session_text_list); + AST_LIST_TRAVERSE(&websocket_session_text_list, ws_session, entry) + { + if (!strcmp(ws_session->id, i->value)) { + break; + } + } + AST_LIST_UNLOCK(&websocket_session_text_list); + break; + } + } + if (!ws_session) { + ast_log(LOG_ERROR, "Failed to find uri or allocate memory for websocket client\n"); + goto end; + } + + //if (ast_fd_set_flags(ast_websocket_fd(websocket), O_NONBLOCK)) { + if(ast_websocket_set_nonblock(websocket)) { + goto end; + } + + if (!ws_session->websocket) { + ast_websocket_ref(websocket); + ws_session->websocket = websocket; + } + + while ((res = ast_websocket_wait_for_input(websocket, -1)) > 0) { + char *payload; + uint64_t payload_len; + enum ast_websocket_opcode opcode; + int fragmented; + + if (ast_websocket_read(websocket, &payload, &payload_len, &opcode, &fragmented)) { + // We err on the side of caution and terminate the ws_session if any error occurs. + ast_log(LOG_WARNING, "Read failure during websocket t140 loop\n"); + break; + } + + if (opcode == AST_WEBSOCKET_OPCODE_TEXT && payload_len > 0) { + //write(ws_session->pipe_fds[1], payload, payload_len); + size_t offset = 0; + while (offset < payload_len) { + size_t bytes_to_write = (payload_len - offset >= 396) ? 396 : (payload_len - offset); + + write(ws_session->pipe_fds[1], payload + offset, bytes_to_write); + + offset += bytes_to_write; + } + } else if (opcode == AST_WEBSOCKET_OPCODE_CLOSE) { + break; + } else { + ast_debug(1, "Ignored websocket text t140 opcode %u\n", opcode ); + } + } + +end: + ast_debug(1, "Exiting websocket text t140 loop %s\n", ast_websocket_session_id(websocket)); + + AST_LIST_LOCK(&websocket_session_text_list); + AST_LIST_TRAVERSE_SAFE_BEGIN(&websocket_session_text_list, ws_session, entry) + { + if (ws_session->websocket == websocket) { + ast_debug(3, "websocket text unref websocket (callback) with id %s\n", ws_session->id); + + ast_websocket_unref(ws_session->websocket); + ws_session->websocket = NULL; + break; + } + } + AST_LIST_TRAVERSE_SAFE_END; + AST_LIST_UNLOCK(&websocket_session_text_list); +} + +/*! \brief Unloads the SIP Websocket Text module from Asterisk */ +static int unload_module(void) +{ + struct ast_websocket_session_text *ws_session = NULL; + + ast_sip_session_unregister_sdp_handler(&text_sdp_handler, STR_TEXT); + ast_sip_session_unregister_supplement(&websocket_session_text_supplement); + + if (websocket_text_t140_uri.data) { + ast_websocket_server_remove_protocol(websocket_text_t140_uri.data, "t140", websocket_session_text_t140_callback); + ast_http_uri_unlink(&websocket_text_t140_uri); + ao2_ref(websocket_text_t140_uri.data, -1); + websocket_text_t140_uri.data = NULL; + } + + AST_LIST_LOCK(&websocket_session_text_list); + AST_LIST_TRAVERSE_SAFE_BEGIN(&websocket_session_text_list, ws_session, entry) + { + AST_LIST_REMOVE_CURRENT(entry); + if (ws_session->websocket) { + ast_debug(3, "websocket text unref websocket (unload) with id %s\n", ws_session->id); + + ast_websocket_unref(ws_session->websocket); + ws_session->websocket = NULL; + } + + ast_debug(3, "websocket text deleted (unload) with id %s\n", ws_session->id); + + ast_free(ws_session); + } + AST_LIST_TRAVERSE_SAFE_END; + AST_LIST_UNLOCK(&websocket_session_text_list); + + return 0; +} + +/*! + * \brief Load the module + * + * Module loading including tests for configuration or dependencies. + * This function can return AST_MODULE_LOAD_FAILURE, AST_MODULE_LOAD_DECLINE, + * or AST_MODULE_LOAD_SUCCESS. If a dependency or environment variable fails + * tests return AST_MODULE_LOAD_FAILURE. If the module can not load the + * configuration file or other non-critical problem return + * AST_MODULE_LOAD_DECLINE. On success return AST_MODULE_LOAD_SUCCESS. + */ +static int load_module(void) +{ + websocket_text_t140_uri.data = ast_websocket_server_create(); + if (!websocket_text_t140_uri.data) { + return AST_MODULE_LOAD_DECLINE; + } + ast_http_uri_link(&websocket_text_t140_uri); + ast_websocket_server_add_protocol(websocket_text_t140_uri.data, "t140", websocket_session_text_t140_callback); + + ast_sip_session_register_supplement(&websocket_session_text_supplement); + + 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; + } + + return AST_MODULE_LOAD_SUCCESS; +end: + unload_module(); + + return AST_MODULE_LOAD_DECLINE; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "PJSIP Websocket Text Support", + .support_level = AST_MODULE_SUPPORT_CORE, + .load = load_module, + .unload = unload_module, + .load_pri = AST_MODPRI_CHANNEL_DRIVER, + .requires = "res_pjsip,res_pjsip_session,res_pjsip_sdp_rtp,res_http_websocket", + ); + diff --git a/res/res_rtp_asterisk.c b/res/res_rtp_asterisk.c index ce1183aa072..74fdd78de6c 100644 --- a/res/res_rtp_asterisk.c +++ b/res/res_rtp_asterisk.c @@ -7924,7 +7924,7 @@ static struct ast_frame *ast_rtp_interpret(struct ast_rtp_instance *instance, st /* format ast_format_t140_red became ast_format_t140 */ ao2_replace(rtp->f.subclass.format, ast_format_t140); - /* RFC 2198 - §3 ) |F| block PT | timestamp offset | block length | + /* RFC 2198 - �3 ) |F| block PT | timestamp offset | block length | * Bit F is zero for the last header block, search F==0 : */ while (header_end < data_end && (*header_end & 0x80)) {