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)) {