diff --git a/daemon/call.c b/daemon/call.c index ee6e22043..9239d3da5 100644 --- a/daemon/call.c +++ b/daemon/call.c @@ -3165,6 +3165,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 ab657b0f5..d1026eda0 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 4c46bfa4a..ba3eb0318 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 89d3cd301..d7565fd12 100644 --- a/daemon/media_socket.c +++ b/daemon/media_socket.c @@ -1859,7 +1859,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); @@ -3049,15 +3050,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 b5861a4d6..4f792a03e 100644 --- a/include/call.h +++ b/include/call.h @@ -550,6 +550,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 { diff --git a/perl/NGCP/Rtpengine/AutoTest.pm b/perl/NGCP/Rtpengine/AutoTest.pm index 7a201ec76..5d0353280 100644 --- a/perl/NGCP/Rtpengine/AutoTest.pm +++ b/perl/NGCP/Rtpengine/AutoTest.pm @@ -26,10 +26,15 @@ BEGIN { our @EXPORT = qw(autotest_start new_call new_call_nc offer answer ft tt cid snd snd_no srtp_snd rtp rcv srtp_rcv rcv_no rcv_maybe srtp_dec escape rtpm rtpmre reverse_tags new_ft new_tt crlf sdp_split rtpe_req offer_answer autotest_init subscribe_request subscribe_answer publish create create_answer - use_json rtpe_raw_req); + use_json rtpe_raw_req last_resp); }; +my $last_response; +sub last_resp { + return $last_response; +} + my $rtpe_stdout; my $rtpe_stderr; my $rtpe_pid; @@ -183,6 +188,7 @@ sub offer_answer { $req->{'from-tag'} //= $ft; $req->{sdp} = $sdp_in; my $resp = rtpe_req($cmd, $name, $req); + $last_response = $resp; return sdp_match($cmd, $name, $resp->{sdp}, $exp_sdp_out); } sub offer { diff --git a/t/Makefile b/t/Makefile index f706673b8..d0b4268bd 100644 --- a/t/Makefile +++ b/t/Makefile @@ -86,7 +86,7 @@ include ../lib/common.Makefile daemon-tests-measure-rtp daemon-tests-mos-legacy daemon-tests-mos-fullband daemon-tests-config-file \ daemon-tests-templ-def daemon-tests-templ-def-offer daemon-tests-t38 daemon-tests-evs-dtx \ daemon-tests-transform daemon-tests-http daemon-tests-heuristic daemon-tests-asymmetric \ - daemon-tests-dtx-no-shift daemon-tests-rtcp daemon-tests-redis-subscribe daemon-tests-rtp-ext \ + daemon-tests-dtx-no-shift daemon-tests-rtcp daemon-tests-redis-subscribe daemon-tests-rtp-ext daemon-tests-ssrc \ daemon-tests-bundle daemon-tests-dtls daemon-tests-rollback \ daemon-tests-rollback-redis \ daemon-tests-recording daemon-tests-create daemon-tests-alias \ @@ -139,7 +139,7 @@ daemon-tests: daemon-tests-main daemon-tests-jb daemon-tests-pubsub daemon-tests daemon-tests-sdp-manipulations daemon-tests-sdes-manipulations \ daemon-tests-sdp-orig-replacements daemon-tests-moh daemon-tests-evs-dtx daemon-tests-transform \ daemon-tests-transcode-config daemon-tests-codec-prefs daemon-tests-http daemon-tests-heuristic \ - daemon-tests-asymmetric daemon-tests-rtcp daemon-tests-redis-subscribe daemon-tests-rtp-ext \ + daemon-tests-asymmetric daemon-tests-rtcp daemon-tests-redis-subscribe daemon-tests-rtp-ext daemon-tests-ssrc \ daemon-tests-bundle daemon-tests-dtls daemon-tests-rollback daemon-tests-rollback-redis \ daemon-tests-recording daemon-tests-create \ daemon-tests-dtx daemon-tests-dtx-cn daemon-tests-dtx-no-shift \ @@ -295,6 +295,9 @@ daemon-tests-rtcp: daemon-test-deps daemon-tests-rtp-ext: daemon-test-deps ./auto-test-helper "$@" perl -I../perl auto-daemon-tests-rtp-ext.pl +daemon-tests-ssrc: daemon-test-deps + ./auto-test-helper "$@" perl -I../perl auto-daemon-tests-ssrc.pl + daemon-tests-bundle: daemon-test-deps ./auto-test-helper "$@" perl -I../perl auto-daemon-tests-bundle.pl diff --git a/t/auto-daemon-tests-ssrc.pl b/t/auto-daemon-tests-ssrc.pl new file mode 100755 index 000000000..ec7d70a9e --- /dev/null +++ b/t/auto-daemon-tests-ssrc.pl @@ -0,0 +1,97 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use NGCP::Rtpengine::Test; +use NGCP::Rtpengine::AutoTest; +use Test::More; +use POSIX; + + +autotest_start(qw(--config-file=none -t -1 -i 203.0.113.1 -n 2223 -f -L 7 -E --log-level-internals=7)) + or die; + + +my ($sock_a, $sock_ax, $sock_b, $sock_bx, $port_a, $port_ax, $port_b, $port_bx, $resp, $ssrcs); + + +# the chosen egress SSRC is reported per media in the offer response + +($sock_a, $sock_ax, $sock_b, $sock_bx) = new_call( + [qw(198.51.100.1 7000)], + [qw(198.51.100.1 7001)], + [qw(198.51.100.3 7002)], + [qw(198.51.100.3 7003)], +); + +($port_a, $port_ax) = offer('egress SSRC reported', { flags => ['fixed egress SSRC'] }, <{'egress SSRC'}; + +is ref($ssrcs), 'ARRAY', 'egress SSRC list present in offer response'; +is scalar(@{$ssrcs // []}), 1, 'one egress SSRC entry per m= section'; +is $ssrcs->[0]->{index}, 1, 'egress SSRC entry carries media index'; +is $ssrcs->[0]->{type}, 'audio', 'egress SSRC entry carries media type'; +ok(($ssrcs->[0]->{SSRC} // 0) > 0, 'egress SSRC entry carries a non-zero SSRC'); + + +my $egress = $ssrcs->[0]->{SSRC}; + +($port_b, $port_bx) = answer('egress SSRC reported', { }, <