From 7674d8529ce31a64c436a1684ca061e65362d7a6 Mon Sep 17 00:00:00 2001 From: dnygate <64208008+dnygate@users.noreply.github.com> Date: Fri, 11 Sep 2026 07:45:56 +0100 Subject: [PATCH] MT#55283 Add a fixed egress SSRC per media A controlling agent feeding media into a WebRTC SFU needs to know the SSRC of the incoming stream before it can bind a receiver to it, and the SFU needs that SSRC to stay put for the life of the call. With `fixed egress SSRC` set, rtpengine picks an SSRC for each media at signalling time and returns the chosen values in the offer or answer response, one entry per m= section along with its index and media type. The value is recorded as the ingress entry's `ssrc_map_out`, which is already the mapping every consumer reads: the egress entry is keyed off it, RTCP rewriting uses it, and the kernel module is handed it as `ssrc_out`. Because one egress entry then stands in for every ingress SSRC, it outlives a change of source, so `seq_diff` carries across on its own. All that is left is re-basing it once at the changeover, taking the last sequence number from the counter shared with the kernel module so that it is still right while the stream is offloaded. The substitution itself goes in the plain passthrough handler rather than the SSRC passthrough one, so that it applies to builds without transcoding support as well. Those builds compile the kernel side regardless, so leaving the userspace half out would have meant the original SSRC being forwarded until the stream was offloaded and the fixed one afterwards. Change-Id: Iad5508c53d29d1e7d54c825da1ab56069cdd89b7 --- daemon/call.c | 5 +++++ daemon/call_flags.c | 4 ++++ daemon/call_interfaces.c | 15 +++++++++++++++ daemon/codec.c | 7 +++++++ daemon/media_socket.c | 31 ++++++++++++++++++++++++++++--- docs/ng_control_protocol.md | 16 ++++++++++++++++ include/call.h | 3 +++ include/call_flags.h | 4 +++- include/ssrc.h | 5 +++++ 9 files changed, 86 insertions(+), 4 deletions(-) diff --git a/daemon/call.c b/daemon/call.c index 6b7311a41..f7e487ac2 100644 --- a/daemon/call.c +++ b/daemon/call.c @@ -3137,6 +3137,11 @@ static void media_init_from_flags(struct call_media *media, sdp_ng_flags *flags) if (flags->recrypt) MEDIA_SET(media, RECRYPT); + + if (flags->fixed_egress_ssrc) { + while (!media->fixed_egress_ssrc) + media->fixed_egress_ssrc = ssl_random(); + } } __attribute__((nonnull(1, 2))) diff --git a/daemon/call_flags.c b/daemon/call_flags.c index 1bda1580d..75851bde0 100644 --- a/daemon/call_flags.c +++ b/daemon/call_flags.c @@ -973,6 +973,10 @@ const char *call_ng_flags_flags(str *s, unsigned int idx, helper_arg arg) { case CSH_LOOKUP("strict source"): out->strict_source = true; break; + case CSH_LOOKUP("fixed-egress-SSRC"): + case CSH_LOOKUP("fixed egress SSRC"): + out->fixed_egress_ssrc = true; + break; case CSH_LOOKUP("strip-extmap"): case CSH_LOOKUP("strip extmap"): return call_ng_flags_str_ht(STR_PTR("all"), 0, &out->rtpext_strip); diff --git a/daemon/call_interfaces.c b/daemon/call_interfaces.c index 8fb16a402..393598a99 100644 --- a/daemon/call_interfaces.c +++ b/daemon/call_interfaces.c @@ -689,6 +689,21 @@ static const char *call_offer_answer_ng(ng_command_ctx_t *ctx, const char *addr) /* place return output SDP */ ctx->ngbuf->sdp_out = sdp_out.s; ctx->parser_ctx.parser->dict_add_str(output, "sdp", &sdp_out); + + /* report the SSRCs picked for media sent towards the recipient of this SDP, + * one entry per m= section */ + if (flags.fixed_egress_ssrc && to_ml->medias) { + parser_arg ssrcs = parser->dict_add_list(output, "egress SSRC"); + for (unsigned int i = 0; i < to_ml->medias->len; i++) { + struct call_media *media = to_ml->medias->pdata[i]; + if (!media || !media->fixed_egress_ssrc) + continue; + parser_arg ent = parser->list_add_dict(ssrcs); + parser->dict_add_int(ent, "index", media->index); + parser->dict_add_str(ent, "type", &media->type); + parser->dict_add_int(ent, "SSRC", media->fixed_egress_ssrc); + } + } if (flags.supports_rollback) { parser_arg supported = parser->dict_add_list(output, "supported"); parser->list_add_string(supported, "rollback"); diff --git a/daemon/codec.c b/daemon/codec.c index 055c18c7e..c4a794075 100644 --- a/daemon/codec.c +++ b/daemon/codec.c @@ -2210,6 +2210,13 @@ static int handler_func_passthrough(struct codec_handler *h, struct media_packet ML_CLEAR(mp->media->monologue, DTMF_INJECTION_ACTIVE); + // substitute a fixed egress SSRC. done here rather than in the SSRC + // passthrough handler as that one is only built with transcoding support. + if (mp->rtp && mp->media_out->fixed_egress_ssrc && mp->ssrc_out) { + mp->rtp->ssrc = htonl(mp->ssrc_out->h.ssrc); + mp->rtp->seq_num = htons(ntohs(mp->rtp->seq_num) + mp->ssrc_out->seq_diff); + } + __buffer_delay_raw(h->delay_buffer, h, codec_add_raw_packet, mp, h->source_pt.clock_rate); return 0; diff --git a/daemon/media_socket.c b/daemon/media_socket.c index db5898291..508ea3581 100644 --- a/daemon/media_socket.c +++ b/daemon/media_socket.c @@ -1858,7 +1858,8 @@ static const char *kernelize_one(kernelize_state *s, } - if (MEDIA_ISSET(media, ECHO) || sink_handler->attrs.transcoding) + if (MEDIA_ISSET(media, ECHO) || sink_handler->attrs.transcoding + || sink->media->fixed_egress_ssrc) redi->output.ssrc_subst = 1; __re_address_translate_ep(&redi->output.dst_addr, &sink->endpoint); @@ -3048,15 +3049,39 @@ static void media_packet_rtp_out(struct packet_handler_ctx *phc, struct sink_han const char *unkern = NULL; + uint32_t fixed_ssrc = phc->mp.media_out->fixed_egress_ssrc; + + /* A fixed egress SSRC is just a mapping from the ingress SSRC, so record it + * where every consumer already looks for one. */ + if (fixed_ssrc && phc->mp.ssrc_in) + phc->mp.ssrc_in->ssrc_map_out = fixed_ssrc; + if (G_LIKELY(!phc->rtcp && phc->mp.rtp)) { unkern = __stream_ssrc_out(phc->out_srtp, phc->mp.rtp->ssrc, phc->mp.ssrc_in, &phc->mp.ssrc_out, &phc->mp.media_out->ssrc_hash_out, - sh->attrs.transcoding ? true : false); + sh->attrs.transcoding || fixed_ssrc); + + /* a fixed egress SSRC outlives the ingress SSRC, so carry the + * sequence numbering across a change of source */ + if (fixed_ssrc && phc->mp.ssrc_out) { + struct ssrc_entry_call *so = phc->mp.ssrc_out; + uint32_t in_ssrc = ntohl(phc->mp.rtp->ssrc); + uint16_t seq = ntohs(phc->mp.rtp->seq_num); + if (so->fixed_in_ssrc_set && so->fixed_in_ssrc != in_ssrc) { + /* the egress sequence counter lives in memory shared with + * the kernel module, so it stays current even while the + * stream is offloaded and userspace sees no packets */ + uint16_t last = atomic_get_na(&so->stats->ext_seq); + so->seq_diff = last + 1 - seq; + } + so->fixed_in_ssrc = in_ssrc; + so->fixed_in_ssrc_set = true; + } } else if (phc->rtcp && phc->mp.rtcp) { unkern = __stream_ssrc_out(phc->out_srtp, phc->mp.rtcp->ssrc, phc->mp.ssrc_in, &phc->mp.ssrc_out, &phc->mp.media_out->ssrc_hash_out, - sh->attrs.transcoding ? true : false); + sh->attrs.transcoding || fixed_ssrc); } if (unkern) diff --git a/docs/ng_control_protocol.md b/docs/ng_control_protocol.md index 470c9532f..7a7afbfc0 100644 --- a/docs/ng_control_protocol.md +++ b/docs/ng_control_protocol.md @@ -1376,6 +1376,18 @@ Spaces in each string may be replaced by hyphens. negotiated are removed from forwarded RTP. Once set, the flag remains in effect for the lifetime of the call. +* `fixed egress SSRC` + + Pick an SSRC for each media at signalling time and use it for all RTP sent + towards that media, instead of passing through the SSRC that the other side + happens to be sending. The chosen values are returned in the response, and + they stay in place for the lifetime of the call, so a source that changes + its own SSRC mid-call is still forwarded under the same one, with the + sequence numbering carried across the change. + + Intended for feeding media into a system that has to bind a receiver to an + SSRC up front, such as a WebRTC selective forwarding unit. + * `strict source` Normally, *rtpengine* attempts to learn the correct endpoint address for every stream during @@ -1821,6 +1833,10 @@ SDP body that the SIP proxy should insert into the SIP message. If `supports` requested a supported extension, the response can also contain a `supported` list. +With `fixed egress SSRC` set, the response also contains `egress SSRC`, a list with +one entry per `m=` section of the returned SDP, in the same order. Each entry gives +the `index` and `type` of the media along with the `SSRC` chosen for it. + Example response: { "result": "ok", "sdp": "v=0\r\no=..." } diff --git a/include/call.h b/include/call.h index b044c4514..b2d6fecff 100644 --- a/include/call.h +++ b/include/call.h @@ -562,6 +562,9 @@ struct call_media { struct ssrc_hash ssrc_hash_in; struct ssrc_hash ssrc_hash_out; + /* fixed SSRC to use for RTP sent towards this media, 0 if unset */ + uint32_t fixed_egress_ssrc; + struct codec_store codecs; struct codec_store offered_codecs; sdp_attr_q generic_attributes; /* sdp_attr_new() */ diff --git a/include/call_flags.h b/include/call_flags.h index 09ab29d37..afbeae753 100644 --- a/include/call_flags.h +++ b/include/call_flags.h @@ -327,7 +327,9 @@ RTPE_NG_FLAGS_STR_CASE_HT_PARAMS /* prevents double MoH holds */ moh_double_hold:1, /* process RTP header extensions even if none were negotiated */ - force_strip_extmap:1; + force_strip_extmap:1, + /* pick a fixed egress SSRC per media and report it */ + fixed_egress_ssrc:1; }; diff --git a/include/ssrc.h b/include/ssrc.h index 1ad5876d0..66087ba1b 100644 --- a/include/ssrc.h +++ b/include/ssrc.h @@ -93,6 +93,11 @@ struct ssrc_entry_call { uint32_t jitter, transit; // output only uint16_t seq_diff; + + /* for a fixed egress SSRC: the last ingress SSRC seen, so a change of + * source can be spotted and the sequence numbering carried across it */ + uint32_t fixed_in_ssrc; + bool fixed_in_ssrc_set; }; struct ssrc_time_item {