MT#55283 fix: retain SDP state for created and subscribed endpoints

Copy create-answer and subscribe-answer SDP into call-owned storage before parsing, matching offer/answer and publish. Retained stream parameters otherwise reference released NG request buffers and later subscriptions can lose their media type and codecs.

Initialize created-answer monologue session state so generated subscriptions retain a valid SDP origin and session name. Add request-buffer-churn and decoded-audio regressions for created and subscribed endpoints.

Validation on standalone Sipwise master: daemon build, all 89 retained-SDP assertions, all 652 create assertions, and all 932 publish/subscribe assertions pass. The cumulative 26.2.1 release also has isolated browser Opus decoding, privacy, recording, and mixer regression coverage.

Closes #2194

Change-Id: Id8ca0e0ff691356d905ee94c0f41090f973fc70f
(cherry picked from commit 12ecdbc2cf)
(cherry picked from commit 30f1a2559a)
mr26.2.1
Dragos Vingarzan 1 month ago committed by Richard Fuchs
parent 4f795733dc
commit 1b6dcd0806

@ -6829,6 +6829,8 @@ bool monologue_call_create(struct call_monologue *ml, sdp_ng_flags *flags) {
bool monologue_call_create_answer(struct call_monologue *ml, sdp_ng_flags *flags, sdp_streams_q *streams) {
__call_monologue_init_from_flags(ml, NULL, flags);
g_auto(str_ht) mid_tracker = str_ht_new();
for (__auto_type l = streams->head; l; l = l->next)

@ -2297,7 +2297,9 @@ const char *call_subscribe_answer_ng(ng_command_ctx_t *ctx) {
if (!flags.sdp.len)
return "No SDP body in message";
if (!sdp_parse(&flags.sdp, &parsed, &flags))
// Stream parameters retain slices of this SDP after the NG request is released.
str sdp_in = call_str_cpy(&flags.sdp);
if (!sdp_parse(&sdp_in, &parsed, &flags))
return "Failed to parse SDP";
if (!sdp_streams(&parsed, &streams, &flags))
@ -2587,7 +2589,9 @@ const char *call_create_answer_ng(ng_command_ctx_t *ctx) {
if (!call)
return "unknown call-ID";
if (!sdp_parse(&flags.sdp, &parsed, &flags))
// Stream parameters retain slices of this SDP after the NG request is released.
str sdp_in = call_str_cpy(&flags.sdp);
if (!sdp_parse(&sdp_in, &parsed, &flags))
return "Failed to parse SDP";
if (!sdp_streams(&parsed, &streams, &flags))

@ -84,6 +84,7 @@ include ../lib/common.Makefile
daemon-tests-bundle daemon-tests-dtls \
daemon-tests-redis-srtp-index \
daemon-tests-recording daemon-tests-create daemon-tests-alias \
daemon-tests-created-subscriptions \
daemon-tests-kernel-api \
daemon-tests-dtx-dtmf
@ -136,7 +137,7 @@ daemon-tests: daemon-tests-main daemon-tests-jb daemon-tests-pubsub daemon-tests
daemon-tests-asymmetric daemon-tests-rtcp daemon-tests-redis-subscribe daemon-tests-rtp-ext \
daemon-tests-bundle daemon-tests-dtls \
daemon-tests-redis-srtp-index \
daemon-tests-recording daemon-tests-create \
daemon-tests-recording daemon-tests-create daemon-tests-created-subscriptions \
daemon-tests-dtx daemon-tests-dtx-cn daemon-tests-dtx-no-shift daemon-tests-dtx-dtmf \
daemon-tests-alias \
daemon-tests-kernel-api
@ -180,6 +181,9 @@ daemon-tests-pubsub: daemon-test-deps
daemon-tests-create: daemon-test-deps
./auto-test-helper "$@" perl -I../perl auto-daemon-tests-create.pl
daemon-tests-created-subscriptions: daemon-test-deps
./auto-test-helper "$@" perl -I../perl auto-daemon-tests-created-subscriptions.pl
daemon-tests-alias: daemon-test-deps
./auto-test-helper "$@" perl -I../perl auto-daemon-tests-alias.pl

@ -0,0 +1,99 @@
#!/usr/bin/perl
# Retained SDP ownership for created and subscribed endpoints.
use strict;
use warnings;
use NGCP::Rtpengine::Test;
use NGCP::Rtpclient::SRTP;
use NGCP::Rtpengine::AutoTest;
use Test::More;
use Time::HiRes qw(sleep);
use IO::Select;
autotest_start(qw(--config-file=none -t -1 -i 203.0.113.1 -n 2223 -f -L 7 -E)) or die;
# Generate independently owned request bodies so later commands reuse the parser buffers.
sub endpoint_sdp {
my ($port, $name) = @_;
return "v=0\r\no=- 123456 7 IN IP4 198.51.100.10\r\ns=$name\r\nt=0 0\r\n" .
"c=IN IP4 198.51.100.10\r\nm=audio $port RTP/AVP 0\r\n" .
"a=rtpmap:0 PCMU/8000\r\na=sendrecv\r\n";
}
# Churn unrelated SDP requests in the same call without changing the source endpoint.
sub churn_requests {
for my $n (1 .. 32) {
rtpe_req('publish', 'request buffer churn', {
'from-tag' => 'churn',
sdp => endpoint_sdp(5010, "unrelated-$n") . "a=x-padding:" . ('x' x (1000 + $n)) . "\r\n",
});
}
}
for my $kind ('created', 'subscribed') {
my ($source, $listener) = new_call([qw(198.51.100.10 5000)], [qw(198.51.100.10 5002)]);
my $response;
if ($kind eq 'created') {
$response = rtpe_req('create', 'created source', {
'from-tag' => ft(),
media => [{type => 'audio', codecs => ['PCMU/8000']}],
});
rtpe_req('create answer', 'created source answer', {
'from-tag' => ft(), sdp => endpoint_sdp(5000, 'retained-session'),
});
}
else {
rtpe_req('publish', 'subscription parent', {
'from-tag' => 'parent', sdp => endpoint_sdp(5004, 'parent-session'),
});
$response = rtpe_req('subscribe request', 'subscribed source', {
'from-tag' => 'parent', 'to-tag' => ft(),
});
rtpe_req('subscribe answer', 'subscribed source answer', {
'to-tag' => ft(), sdp => endpoint_sdp(5000, 'retained-session'),
});
}
my ($input_port) = $response->{sdp} =~ /m=audio (\d+)/;
ok($input_port, "$kind source has a media port");
churn_requests();
my $offer = rtpe_req('subscribe request', "$kind WebRTC listener", {
'from-tag' => ft(), 'to-tag' => 'webrtc-listener', flags => ['WebRTC', 'block DTMF'],
codec => {mask => ['all'], transcode => ['opus']},
});
like($offer->{sdp}, qr/^m=audio \d+ UDP\/TLS\/RTP\/SAVPF 96\r?$/m, "$kind retains audio type and Opus payload");
like($offer->{sdp}, qr/^a=rtpmap:96 opus\/48000\/2\r?$/m, "$kind retains requested Opus codec");
if ($kind eq 'created') {
like($offer->{sdp}, qr/^o=- 123456 \d+ IN IP4 \S+\r?$/m, 'created answer retains a valid origin');
like($offer->{sdp}, qr/^s=retained-session\r?$/m, 'created answer retains session name');
my $plain = rtpe_req('subscribe request', 'created decoded audio listener', {
'from-tag' => ft(), 'to-tag' => tt(),
codec => {mask => ['all'], transcode => ['L16/16000/1']},
});
my ($payload_type) = $plain->{sdp} =~ /a=rtpmap:(\d+) L16\/16000/;
ok(defined $payload_type, 'decoded audio listener negotiates L16');
$payload_type //= 96;
rtpe_req('subscribe answer', 'created decoded audio answer', {
'to-tag' => tt(),
flags => ['allow transcoding'],
sdp => "v=0\r\no=- 8 1 IN IP4 198.51.100.10\r\ns=listener\r\nt=0 0\r\n" .
"c=IN IP4 198.51.100.10\r\nm=audio 5002 RTP/AVP $payload_type\r\n" .
"a=rtpmap:$payload_type L16/16000/1\r\na=recvonly\r\n",
});
my $select = IO::Select->new($listener);
my $audible = 0;
for my $seq (1 .. 20) {
snd($source, $input_port, rtp(0, $seq, $seq * 160, 0x5678, "\x40" x 160));
sleep(0.02);
while ($select->can_read(0)) {
my $packet;
$listener->recv($packet, 65535);
next if length($packet) < 12 || (ord(substr($packet, 1, 1)) & 0x7f) != $payload_type;
$audible++ if grep { abs($_) > 500 } unpack('s>*', substr($packet, 12));
}
}
ok($audible >= 5, 'created source delivers decoded non-silent audio after request churn');
}
rtpe_req('delete', "$kind cleanup", {});
}
done_testing();
Loading…
Cancel
Save