diff --git a/daemon/call.c b/daemon/call.c index a1a6caa2d..3d63c7289 100644 --- a/daemon/call.c +++ b/daemon/call.c @@ -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) diff --git a/daemon/call_interfaces.c b/daemon/call_interfaces.c index 6b2b6c448..ec155acb1 100644 --- a/daemon/call_interfaces.c +++ b/daemon/call_interfaces.c @@ -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)) diff --git a/t/Makefile b/t/Makefile index 7f6708c70..d0a9f3be7 100644 --- a/t/Makefile +++ b/t/Makefile @@ -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 diff --git a/t/auto-daemon-tests-created-subscriptions.pl b/t/auto-daemon-tests-created-subscriptions.pl new file mode 100755 index 000000000..b150fc683 --- /dev/null +++ b/t/auto-daemon-tests-created-subscriptions.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();