From dcf8e04c7cfc0eaea2b0475ce93cd0e621d3733b Mon Sep 17 00:00:00 2001 From: bgoRelatel Date: Thu, 1 Oct 2026 12:00:00 +0200 Subject: [PATCH 1/3] [core] Parse the direction attribute on the text m-line The audio and video branches of switch_core_media_negotiate_sdp() derive their remote media flow from m->m_mode. The text branch never did, so the text engine kept the SWITCH_MEDIA_FLOW_INACTIVE it is reset to before parsing, whatever the far end actually offered. That value is copied onto the other leg's send mode when the answer is processed, after which switch_core_session_write_text_frame() discards every frame written toward that leg. Seen on a bridged call between two RTT-capable handsets: text from the callee reached the switch and was dropped inside it, logging "Writing text to RECVONLY/INACTIVE session" once per frame while nothing was sent on the wire. --- src/switch_core_media.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/switch_core_media.c b/src/switch_core_media.c index 6a8e06342ce..43002d6a989 100644 --- a/src/switch_core_media.c +++ b/src/switch_core_media.c @@ -5897,6 +5897,8 @@ SWITCH_DECLARE(uint8_t) switch_core_media_negotiate_sdp(switch_core_session_t *s sdp_rtpmap_t *map; payload_map_t *red_pmap = NULL; + switch_core_media_set_rmode(smh->session, SWITCH_MEDIA_TYPE_TEXT, sdp_media_flow(m->m_mode), sdp_type); + switch_channel_set_flag(session->channel, CF_RTT); connection = sdp->sdp_connection; From 793728a5c259d601d027b3d5f35af47509fdead0 Mon Sep 17 00:00:00 2001 From: bgoRelatel Date: Thu, 1 Oct 2026 12:00:00 +0200 Subject: [PATCH 2/3] [core] Take the RED redundancy level from the negotiated fmtp red_max is fixed at 5, so we always transmit four redundant generations plus the primary. RFC 4103 negotiates the level through the fmtp of the red payload type, where "96/96/96" asks for two generations plus the primary. Sending more blocks than were negotiated was enough for the receiving handsets to discard the packets: the text arrived correctly encoded, with the right payload types, in order and without loss, and was never displayed. Packet captures either side of this change show the block count dropping from five to three and the text appearing. Taking the level from the fmtp also stops the ring walking slots that were never written, whose timestamp offsets are emitted relative to zero. --- src/switch_core_media.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/switch_core_media.c b/src/switch_core_media.c index 43002d6a989..18403f01acf 100644 --- a/src/switch_core_media.c +++ b/src/switch_core_media.c @@ -198,6 +198,7 @@ struct switch_rtp_engine_s { uint8_t reject_avp; int t140_pt; int red_pt; + int red_generations; switch_rtp_text_factory_t *tf; switch_engine_function_t engine_function; @@ -5952,6 +5953,20 @@ SWITCH_DECLARE(uint8_t) switch_core_media_negotiate_sdp(switch_core_session_t *s t_engine->cur_payload_map = red_pmap; } + /* red fmtp lists one payload type per generation: "96/96/96" is two redundant plus the primary */ + if (red_pmap && !zstr(red_pmap->rm_fmtp)) { + const char *fp = red_pmap->rm_fmtp; + int generations = 1; + + while (*fp) { + if (*fp++ == '/') generations++; + } + + if (generations > 1 && generations <= MAX_RED_FRAMES) { + t_engine->red_generations = generations; + } + } + for (attr = m->m_attributes; attr; attr = attr->a_next) { if (!strcasecmp(attr->a_name, "rtcp") && attr->a_value) { switch_channel_set_variable(session->channel, "sip_remote_text_rtcp_port", attr->a_value); @@ -9199,6 +9214,11 @@ SWITCH_DECLARE(switch_status_t) switch_core_media_activate_rtp(switch_core_sessi if (!t_engine->tf) { switch_rtp_text_factory_create(&t_engine->tf, switch_core_session_get_pool(session)); + + /* only lower it, the factory allocated red_max buffers */ + if (t_engine->tf && t_engine->red_generations > 0 && t_engine->red_generations < t_engine->tf->red_max) { + t_engine->tf->red_max = t_engine->red_generations; + } } switch_rtp_set_video_buffer_size(t_engine->rtp_session, 2, 2048); From 1f1d261a3ac8df5b1c8f6942412a29e1b2e20a65 Mon Sep 17 00:00:00 2001 From: bgoRelatel Date: Thu, 1 Oct 2026 12:00:00 +0200 Subject: [PATCH 3/3] [core] Set up text when a re-INVITE adds it mid-call Switching RTT on during an established call sends a re-INVITE adding an m=text line. Three things stopped that working. The text branch did not set the local send mode from the offer, which the audio and video branches do immediately after setting the remote mode. That call is also the only one that reaches switch_core_media_set_smode() with SDP_OFFER, which is a condition of CF_STREAM_CHANGED and therefore of propagating the new stream to the other leg at all. The propagated re-offer was then built without the text stream, because the far leg had never parsed an m=text line and so held neither CF_WANT_RTT nor CF_RTT, which gate the text branch of the SDP generator. Finally switch_core_media_activate_rtp() jumps from the audio section straight to video: on a re-INVITE whose audio endpoint is unchanged, skipping the text section, so the text RTP session and its factory were never created. The non-re-INVITE path already guards that jump; the re-INVITE path did not. Every relay write then failed with "text engine not available for processing". --- src/switch_core_media.c | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/switch_core_media.c b/src/switch_core_media.c index 18403f01acf..fb254f1de18 100644 --- a/src/switch_core_media.c +++ b/src/switch_core_media.c @@ -4743,6 +4743,11 @@ SWITCH_DECLARE(void) switch_core_media_set_smode(switch_core_session_t *session, if (switch_core_session_get_partner(session, &other_session) == SWITCH_STATUS_SUCCESS) { switch_core_media_set_smode(other_session, type, opp_smode, SDP_OFFER); + + if (type == SWITCH_MEDIA_TYPE_TEXT) { + /* the partner has no text m-line yet so its re-offer would not carry one */ + switch_channel_set_flag(other_session->channel, CF_WANT_RTT); + } switch_channel_set_flag(session->channel, CF_STREAM_CHANGED); switch_core_session_rwunlock(other_session); } @@ -5900,6 +5905,23 @@ SWITCH_DECLARE(uint8_t) switch_core_media_negotiate_sdp(switch_core_session_t *s switch_core_media_set_rmode(smh->session, SWITCH_MEDIA_TYPE_TEXT, sdp_media_flow(m->m_mode), sdp_type); + if (sdp_type == SDP_OFFER) { + switch(t_engine->rmode) { + case SWITCH_MEDIA_FLOW_RECVONLY: + switch_core_media_set_smode(smh->session, SWITCH_MEDIA_TYPE_TEXT, SWITCH_MEDIA_FLOW_SENDONLY, sdp_type); + break; + case SWITCH_MEDIA_FLOW_SENDONLY: + switch_core_media_set_smode(smh->session, SWITCH_MEDIA_TYPE_TEXT, SWITCH_MEDIA_FLOW_RECVONLY, sdp_type); + break; + case SWITCH_MEDIA_FLOW_INACTIVE: + switch_core_media_set_smode(smh->session, SWITCH_MEDIA_TYPE_TEXT, SWITCH_MEDIA_FLOW_INACTIVE, sdp_type); + break; + default: + switch_core_media_set_smode(smh->session, SWITCH_MEDIA_TYPE_TEXT, SWITCH_MEDIA_FLOW_SENDRECV, sdp_type); + break; + } + } + switch_channel_set_flag(session->channel, CF_RTT); connection = sdp->sdp_connection; @@ -8682,6 +8704,10 @@ SWITCH_DECLARE(switch_status_t) switch_core_media_activate_rtp(switch_core_sessi switch_channel_get_name(session->channel)); a_engine->cur_payload_map->negotiated = 1; //XX + if (switch_channel_test_flag(session->channel, CF_TEXT_POSSIBLE) && !switch_rtp_ready(t_engine->rtp_session)) { + goto text; + } + goto video; } else { switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "Audio params changed for %s from %s:%d to %s:%d\n", @@ -8745,6 +8771,10 @@ SWITCH_DECLARE(switch_status_t) switch_core_media_activate_rtp(switch_core_sessi check_dtls_reinvite(session, a_engine); } + if (switch_channel_test_flag(session->channel, CF_TEXT_POSSIBLE) && !switch_rtp_ready(t_engine->rtp_session)) { + goto text; + } + goto video; }