TT#91151 add publish/subscribe commands

Change-Id: I1842b89efea7fa3af0bd4d045e49da31285cd0e1
pull/1346/head
Richard Fuchs 4 years ago
parent 413798e43f
commit f04332915b

@ -57,6 +57,8 @@ the following additional features are available:
- Playback of pre-recorded streams/announcements
- Transcoding between T.38 and PCM (G.711 or other audio codecs)
- Silence detection and comfort noise (RFC 3389) payloads
* Media forking
* Publish/subscribe mechanism for N-to-N media forwarding
*Rtpengine* does not (yet) support:
@ -579,6 +581,10 @@ a string and determines the type of message. Currently the following commands ar
* stop media
* play DTMF
* statistics
* publish
* subscribe request
* subscribe answer
* unsubscribe
The response dictionary must contain at least one key called `result`. The value can be either `ok` or `error`.
For the `ping` command, the additional value `pong` is allowed. If the result is `error`, then another key
@ -632,10 +638,19 @@ Optionally included keys are:
The SIP `Via` branch as string. Used to additionally refine the matching logic between media streams
and calls and call branches.
* `label`
* `label` or `from-label`
A custom free-form string which *rtpengine* remembers for this participating endpoint and reports
back in logs and statistics output.
back in logs and statistics output. For some commands (e.g. `block media`) the given label is not
used to set the label of the call participant, but rather to select an existing call participant.
* `set-label` or `to-label`
Some commands (e.g. `block media`) use the given `label` to select
an existing call participant. For these commands, `set-label` instead
of `label` can be used to set the label at the same time, either for
the selected call participant (if selected via `from-tag`) or for the
newly created participant (e.g. for `subscribe request`).
* `flags`
@ -751,6 +766,14 @@ Optionally included keys are:
even if the re-offer lists other codecs as preferred, or in a different order. Recommended
to be combined with `single codec`.
- `allow transcoding`
This flag is only useful in commands that provide an explicit answer SDP to *rtpengine*
(e.g. `subscribe answer`). For these commands, if the answer SDP does not accept all
codecs that were offered, the default behaviour is to reject the answer. With this flag
given, the answer will be accepted even if some codecs were rejected, and codecs will be
transcoded as required.
- `all`
Only relevant to the `unblock media` and `unsilence media`
@ -876,6 +899,12 @@ Optionally included keys are:
This special keyword is provided only for legacy support and should be considered obsolete.
It will be removed in future versions.
* `interface`
Contains a single string naming one of the configured interfaces, just like `direction` does. The
`interface` option is used instead of `direction` where only one interface is required (e.g. outside
of an offer/answer scenario), for example in the `publish` or `subscribe request` commands.
* `received from`
Contains a list of exactly two elements. The first element denotes the address family and the second
@ -2003,6 +2032,75 @@ command. Sample return dictionary:
"result": "ok"
}
`publish` Message
-----------------
Similar to an `offer` message except that it is used outside of an offer/answer
scenario. The media described by the SDP is published to *rtpengine* directly,
and other peer can then subscribe to the published media to receive a copy.
The message must include the key `sdp` which should describe `sendonly` media;
and the key `call-id` and `from-tag` to identify the publisher. Most other keys
and options supported by `offer` are also supported for `publish`.
The reply message will contain an answer SDP in `sdp`, but unlike with `offer`
this is not a rewritten version of the received SDP, but rather a `recvonly`
answer SDP generated by *rtpengine* locally. Only one codec for each media
section will be listed, and by default this will be the first supported codec
from the published media. This can be influenced with the `codec` options
described above.
`subscribe request` Message
---------------------------
This message is used to request subscription (i.e. receiving a copy of the
media) to an existing call participant, which must have been created either
through the offer/answer mechanism, or through the publish mechanism.
The call participant is selected in the same way as described under `block
DTMF` except that one call participant must be selected (i.e. the `all` keyword
cannot be used). This message then creates a new call participant, which
corresponds to the subscription. This new call participant will be identified
by a newly generated unique tag, or by the tag given in the `to-tag` key. If a
label is to be set for the newly created subscription, it can be set through
`set-label`.
The reply message will contain a sendonly offer SDP in `sdp` which by default
will mirror the SDP of the call participant being subscribed to. This offer SDP
can be manipulated with the same flags as used in an `offer` message, including
the option to manipulate the codecs. The reply message will also contain the
`from-tag` (corresponding to the call participant being subscribed to) and the
`to-tag` (corresponding to the subscription, either generated or taken from the
received message).
`subscribe answer` Message
--------------------------
This message is expected to be received after responding to a `subscribe
request` message. The message should contain the same `from-tag` and `to-tag`
is the reply to the `subscribe request` (although `label` etc can also be used
instead of the `from-tag`), as well as the answer SDP in `sdp`.
By default, the answer SDP must accept all codecs that were presented in the
offer SDP (given in the reply to `subscribe request`). If not all codecs were
accepted, then the `subscribe answer` will be rejected. This behavious can be
changed by including the `allow transcoding` flag in the message. If this flag
is present, then the answer SDP will be accepted as long as at least one valid
codec is present, and the media will be transcoded as required. This also holds
true if some codecs were added for transcoding in the `subscribe request`
message, which means that `allow transcoding` must always be included in
`subscribe answer` if any transcoding is to be allowed.
The reply message will simply indicate success or failure. If successful, media
forwarding will start to the endpoint given in the answer SDP.
`unsubscribe` Message
---------------------
This message is a counterpart to `subsscribe answer` to stop an established
subscription. The subscription to be stopped is identified by `from-tag` and
`to`tag`.
The *tcp-ng* Control Protocol
=========================

File diff suppressed because it is too large Load Diff

@ -871,6 +871,9 @@ static void call_ng_flags_flags(struct sdp_ng_flags *out, str *s, void *dummy) {
case CSH_LOOKUP("single-codec"):
out->single_codec = 1;
break;
case CSH_LOOKUP("allow-transcoding"):
out->allow_transcoding = 1;
break;
case CSH_LOOKUP("inject-DTMF"):
out->inject_dtmf = 1;
break;
@ -907,7 +910,7 @@ static void call_ng_flags_flags(struct sdp_ng_flags *out, str *s, void *dummy) {
&out->codec_except))
return;
#ifdef WITH_TRANSCODING
if (out->opmode == OP_OFFER) {
if (out->opmode == OP_OFFER || out->opmode == OP_REQUEST || out->opmode == OP_PUBLISH) {
if (call_ng_flags_prefix(out, s, "transcode-", call_ng_flags_codec_list,
&out->codec_transcode))
return;
@ -957,9 +960,11 @@ static void call_ng_process_flags(struct sdp_ng_flags *out, bencode_item_t *inpu
bencode_dictionary_get_str(input, "from-tag", &out->from_tag);
bencode_dictionary_get_str(input, "to-tag", &out->to_tag);
bencode_dictionary_get_str(input, "via-branch", &out->via_branch);
bencode_dictionary_get_str(input, "label", &out->label);
bencode_get_alt(input, "label", "from-label", &out->label);
bencode_get_alt(input, "to-label", "set-label", &out->set_label);
bencode_dictionary_get_str(input, "address", &out->address);
bencode_get_alt(input, "sdp", "SDP", &out->sdp);
bencode_dictionary_get_str(input, "interface", &out->interface);
diridx = 0;
if ((list = bencode_dictionary_get_expect(input, "direction", BENCODE_LIST))) {
@ -1145,7 +1150,7 @@ static void call_ng_process_flags(struct sdp_ng_flags *out, bencode_item_t *inpu
call_ng_flags_list(out, dict, "offer", call_ng_flags_codec_list, &out->codec_offer);
call_ng_flags_list(out, dict, "except", call_ng_flags_str_ht, &out->codec_except);
#ifdef WITH_TRANSCODING
if (opmode == OP_OFFER) {
if (opmode == OP_OFFER || opmode == OP_REQUEST || opmode == OP_PUBLISH) {
call_ng_flags_list(out, dict, "transcode", call_ng_flags_codec_list, &out->codec_transcode);
call_ng_flags_list(out, dict, "mask", call_ng_flags_codec_list, &out->codec_mask);
call_ng_flags_list(out, dict, "set", call_ng_flags_str_ht_split, &out->codec_set);
@ -2044,6 +2049,12 @@ found:
__monologue_unkernelize(*monologue);
}
// for generic ops, handle set-label here if given
if (opmode == OP_OTHER && flags->set_label.len && *monologue) {
call_str_cpy(*call, &(*monologue)->label, &flags->set_label);
g_hash_table_replace((*call)->labels, &(*monologue)->label, *monologue);
}
return NULL;
}
@ -2460,6 +2471,174 @@ found_sink:
#endif
}
const char *call_publish_ng(bencode_item_t *input, bencode_item_t *output) {
AUTO_CLEANUP(struct sdp_ng_flags flags, call_ng_free_flags);
AUTO_CLEANUP(GQueue parsed, sdp_free) = G_QUEUE_INIT;
AUTO_CLEANUP(GQueue streams, sdp_streams_free) = G_QUEUE_INIT;
AUTO_CLEANUP(str sdp_in, str_free_dup) = STR_NULL;
AUTO_CLEANUP(str sdp_out, str_free_dup) = STR_NULL;
call_ng_process_flags(&flags, input, OP_PUBLISH);
if (!flags.sdp.s)
return "No SDP body in message";
if (!flags.call_id.s)
return "No call-id in message";
if (!flags.from_tag.s)
return "No from-tag in message";
str_init_dup_str(&sdp_in, &flags.sdp);
if (sdp_parse(&sdp_in, &parsed, &flags))
return "Failed to parse SDP";
if (sdp_streams(&parsed, &streams, &flags))
return "Incomplete SDP specification";
struct call *call = call_get_or_create(&flags.call_id, false, false);
struct call_monologue *ml = call_get_or_create_monologue(call, &flags.from_tag);
int ret = monologue_publish(ml, &streams, &flags);
if (ret)
ilog(LOG_ERR, "Publish error"); // XXX close call? handle errors?
ret = sdp_create(&sdp_out, ml, &flags);
if (!ret) {
save_last_sdp(ml, &sdp_in, &parsed, &streams);
bencode_buffer_destroy_add(output->buffer, g_free, sdp_out.s);
bencode_dictionary_add_str(output, "sdp", &sdp_out);
sdp_out = STR_NULL; // ownership passed to output
}
rwlock_unlock_w(&call->master_lock);
obj_put(call);
if (!ret)
return NULL;
return "Failed to create SDP";
}
const char *call_subscribe_request_ng(bencode_item_t *input, bencode_item_t *output) {
const char *err = NULL;
AUTO_CLEANUP(struct sdp_ng_flags flags, call_ng_free_flags);
char rand_buf[65];
AUTO_CLEANUP_NULL(struct call *call, call_unlock_release);
struct call_monologue *source_ml;
// get source monologue
err = media_block_match(&call, &source_ml, &flags, input, OP_REQUEST);
if (err)
return err;
if (flags.sdp.len)
ilog(LOG_INFO, "Subscribe-request with SDP received - ignoring SDP");
if (!source_ml)
return "No call participant specified";
if (!source_ml->last_in_sdp.len || !source_ml->last_in_sdp_parsed.length)
return "No SDP known for this from-tag";
// the `label=` option was possibly used above to select the from-tag --
// switch it out with `to-label=` or `set-label=` for monologue_subscribe_request
// below which sets the label based on `label` for a newly created monologue
flags.label = flags.set_label;
// get destination monologue
if (!flags.to_tag.len) {
// generate one
flags.to_tag = STR_CONST_INIT(rand_buf);
rand_hex_str(flags.to_tag.s, flags.to_tag.len / 2);
}
struct call_monologue *dest_ml = call_get_or_create_monologue(call, &flags.to_tag);
struct sdp_chopper *chopper = sdp_chopper_new(&source_ml->last_in_sdp);
bencode_buffer_destroy_add(output->buffer, (free_func_t) sdp_chopper_destroy, chopper);
int ret = monologue_subscribe_request(source_ml, dest_ml, &flags);
if (ret)
return "Failed to request subscription";
ret = sdp_replace(chopper, &source_ml->last_in_sdp_parsed, dest_ml, &flags);
if (ret)
return "Failed to rewrite SDP";
if (chopper->output->len)
bencode_dictionary_add_string_len(output, "sdp", chopper->output->str, chopper->output->len);
bencode_dictionary_add_str_dup(output, "from-tag", &source_ml->tag);
bencode_dictionary_add_str_dup(output, "to-tag", &dest_ml->tag);
return NULL;
}
const char *call_subscribe_answer_ng(bencode_item_t *input, bencode_item_t *output) {
const char *err = NULL;
AUTO_CLEANUP(struct sdp_ng_flags flags, call_ng_free_flags);
AUTO_CLEANUP(GQueue parsed, sdp_free) = G_QUEUE_INIT;
AUTO_CLEANUP(GQueue streams, sdp_streams_free) = G_QUEUE_INIT;
AUTO_CLEANUP_NULL(struct call *call, call_unlock_release);
struct call_monologue *source_ml;
// get source monologue
err = media_block_match(&call, &source_ml, &flags, input, OP_REQ_ANSWER);
if (err)
return err;
if (!source_ml)
return "No call participant specified";
if (!flags.to_tag.s)
return "No to-tag in message";
if (!flags.sdp.len)
return "No SDP body in message";
// get destination monologue
struct call_monologue *dest_ml = call_get_monologue(call, &flags.to_tag);
if (!dest_ml)
return "To-tag not found";
if (sdp_parse(&flags.sdp, &parsed, &flags))
return "Failed to parse SDP";
if (sdp_streams(&parsed, &streams, &flags))
return "Incomplete SDP specification";
int ret = monologue_subscribe_answer(source_ml, dest_ml, &flags, &streams);
if (ret)
return "Failed to process subscription answer";
return NULL;
}
const char *call_unsubscribe_ng(bencode_item_t *input, bencode_item_t *output) {
const char *err = NULL;
AUTO_CLEANUP(struct sdp_ng_flags flags, call_ng_free_flags);
AUTO_CLEANUP_NULL(struct call *call, call_unlock_release);
struct call_monologue *source_ml;
// get source monologue
err = media_block_match(&call, &source_ml, &flags, input, OP_OTHER);
if (err)
return err;
if (!source_ml)
return "No call participant specified";
if (!flags.to_tag.s)
return "No to-tag in message";
// get destination monologue
struct call_monologue *dest_ml = call_get_or_create_monologue(call, &flags.to_tag);
if (!dest_ml)
return "To-tag not found";
int ret = monologue_unsubscribe(source_ml, dest_ml, &flags);
if (ret)
return "Failed to unsubscribe";
return NULL;
}
void call_interfaces_free() {
if (info_re) {
pcre_free(info_re);

@ -3849,6 +3849,21 @@ void codec_store_synthesise(struct codec_store *dst, struct codec_store *opposit
}
}
// check all codecs listed in the source are also be present in the answer (dst)
bool codec_store_is_full_answer(const struct codec_store *src, const struct codec_store *dst) {
for (GList *l = src->codec_prefs.head; l; l = l->next) {
const struct rtp_payload_type *src_pt = l->data;
const struct rtp_payload_type *dst_pt = g_hash_table_lookup(dst->codecs,
GINT_TO_POINTER(src_pt->payload_type));
if (!dst_pt || rtp_payload_type_cmp(src_pt, dst_pt)) {
ilogs(codec, LOG_DEBUG, "Source codec " STR_FORMAT " is not present in the answer",
STR_FMT(&src_pt->encoding_with_params));
return false;
}
}
return true;
}
static void codec_timers_run(void *p) {
struct codec_timer *ct = p;
ct->func(ct);

@ -39,12 +39,15 @@ const char *ng_command_strings[NGC_COUNT] = {
"stop recording", "start forwarding", "stop forwarding", "block DTMF",
"unblock DTMF", "block media", "unblock media", "play media", "stop media",
"play DTMF", "statistics", "silence media", "unsilence media",
"publish", "subscribe request",
"subscribe answer", "unsubscribe",
};
const char *ng_command_strings_short[NGC_COUNT] = {
"Ping", "Offer", "Answer", "Delete", "Query", "List", "StartRec",
"StopRec", "StartFwd", "StopFwd", "BlkDTMF",
"UnblkDTMF", "BlkMedia", "UnblkMedia", "PlayMedia", "StopMedia",
"PlayDTMF", "Stats", "SlnMedia", "UnslnMedia",
"Pub", "SubReq", "SubAns", "Unsub",
};
static void timeval_update_request_time(struct request_time *request, const struct timeval *offer_diff) {
@ -306,6 +309,22 @@ int control_ng_process(str *buf, const endpoint_t *sin, char *addr,
errstr = statistics_ng(dict, resp);
command = NGC_STATISTICS;
break;
case CSH_LOOKUP("publish"):
errstr = call_publish_ng(dict, resp);
command = NGC_PUBLISH;
break;
case CSH_LOOKUP("subscribe request"):
errstr = call_subscribe_request_ng(dict, resp);
command = NGC_SUBSCRIBE_REQ;
break;
case CSH_LOOKUP("subscribe answer"):
errstr = call_subscribe_answer_ng(dict, resp);
command = NGC_SUBSCRIBE_ANS;
break;
case CSH_LOOKUP("unsubscribe"):
errstr = call_unsubscribe_ng(dict, resp);
command = NGC_UNSUBSCRIBE;
break;
default:
errstr = "Unrecognized command";
}

@ -2496,7 +2496,8 @@ struct packet_stream *print_rtcp(GString *s, struct call_media *media, GList *rt
if (proto_is_rtp(media->protocol)) {
if (MEDIA_ISSET(media, RTCP_MUX)
&& (flags->opmode == OP_ANSWER || flags->opmode == OP_OTHER
|| (flags->opmode == OP_OFFER
|| flags->opmode == OP_PUBLISH
|| ((flags->opmode == OP_OFFER || flags->opmode == OP_REQUEST)
&& flags->rtcp_mux_require)))
{
insert_rtcp_attr(s, ps, flags);

@ -50,6 +50,9 @@ enum stream_address_format {
enum call_opmode {
OP_OFFER = 0,
OP_ANSWER = 1,
OP_REQUEST,
OP_REQ_ANSWER,
OP_PUBLISH,
OP_OTHER,
};
@ -562,10 +565,16 @@ int call_get_mono_dialogue(struct call_monologue *dialogue[2], struct call *call
const str *totag,
const str *viabranch);
struct call_monologue *call_get_monologue(struct call *call, const str *fromtag);
struct call_monologue *call_get_or_create_monologue(struct call *call, const str *fromtag);
struct call *call_get(const str *callid);
int monologue_offer_answer(struct call_monologue *dialogue[2], GQueue *streams, struct sdp_ng_flags *flags);
void codecs_offer_answer(struct call_media *media, struct call_media *other_media,
struct stream_params *sp, struct sdp_ng_flags *flags);
int monologue_publish(struct call_monologue *ml, GQueue *streams, struct sdp_ng_flags *flags);
int monologue_subscribe_request(struct call_monologue *src, struct call_monologue *dst, struct sdp_ng_flags *);
int monologue_subscribe_answer(struct call_monologue *src, struct call_monologue *dst, struct sdp_ng_flags *,
GQueue *);
int monologue_unsubscribe(struct call_monologue *src, struct call_monologue *dst, struct sdp_ng_flags *);
int call_delete_branch(const str *callid, const str *branch,
const str *fromtag, const str *totag, bencode_item_t *output, int delete_delay);
void call_destroy(struct call *);

@ -32,11 +32,13 @@ struct sdp_ng_flags {
sockaddr_t parsed_received_from;
sockaddr_t parsed_media_address;
str direction[2];
str interface;
sockfamily_t *address_family;
int tos;
str record_call_str;
str metadata;
str label;
str set_label;
str address;
sockaddr_t xmlrpc_callback;
GQueue codec_strip;
@ -110,7 +112,8 @@ struct sdp_ng_flags {
loop_protect:1,
original_sendrecv:1,
single_codec:1,
reuse_codec:1,
reuse_codec:1,
allow_transcoding:1,
inject_dtmf:1,
t38_decode:1,
t38_force:1,
@ -177,6 +180,10 @@ const char *call_stop_media_ng(bencode_item_t *, bencode_item_t *);
const char *call_play_dtmf_ng(bencode_item_t *, bencode_item_t *);
void ng_call_stats(struct call *call, const str *fromtag, const str *totag, bencode_item_t *output,
struct call_stats *totals);
const char *call_publish_ng(bencode_item_t *, bencode_item_t *);
const char *call_subscribe_request_ng(bencode_item_t *, bencode_item_t *);
const char *call_subscribe_answer_ng(bencode_item_t *, bencode_item_t *);
const char *call_unsubscribe_ng(bencode_item_t *, bencode_item_t *);
int call_interfaces_init(void);
void call_interfaces_free(void);

@ -4,6 +4,7 @@
#include <glib.h>
#include <sys/time.h>
#include <stdbool.h>
#include "str.h"
#include "codeclib.h"
#include "aux.h"
@ -98,6 +99,7 @@ void codec_store_track(struct codec_store *, GQueue *);
void codec_store_transcode(struct codec_store *, GQueue *, struct codec_store *);
void codec_store_answer(struct codec_store *dst, struct codec_store *src, struct sdp_ng_flags *flags);
void codec_store_synthesise(struct codec_store *dst, struct codec_store *opposite);
bool codec_store_is_full_answer(const struct codec_store *src, const struct codec_store *dst);
void codec_add_raw_packet(struct media_packet *mp, unsigned int clockrate);
void codec_packet_free(void *);

@ -31,6 +31,10 @@ enum ng_command {
NGC_STATISTICS,
NGC_SILENCE_MEDIA,
NGC_UNSILENCE_MEDIA,
NGC_PUBLISH,
NGC_SUBSCRIBE_REQ,
NGC_SUBSCRIBE_ANS,
NGC_UNSUBSCRIBE,
NGC_COUNT // last, number of elements
};

@ -301,6 +301,9 @@ INLINE void g_tree_clear(GTree *t) {
g_tree_remove(t, k);
}
}
INLINE void g_string_free_true(GString *s) {
g_string_free(s, TRUE);
}
INLINE void __g_string_free(GString **s) {
g_string_free(*s, TRUE);
}

@ -91,23 +91,27 @@ GetOptions(
'no-jitter-buffer' => \$options{'no jitter buffer'},
'generate-RTCP' => \$options{'generate RTCP'},
'single-codec' => \$options{'single codec'},
'allow-transcoding' => \$options{'allow transcoding'},
'reorder-codecs' => \$options{'reorder codecs'},
'media-echo=s' => \$options{'media echo'},
'pierce-NAT' => \$options{'pierce NAT'},
'label=s' => \$options{'label'},
'set-label=s' => \$options{'set-label'},
'from-label=s' => \$options{'from-label'},
'to-label=s' => \$options{'to-label'},
) or die;
my $cmd = shift(@ARGV) or die;
my %packet = (command => $cmd);
for my $x (split(/,/, 'from-tag,to-tag,call-id,transport protocol,media address,ICE,address family,DTLS,via-branch,media address,ptime,xmlrpc-callback,metadata,address,file,db-id,code,DTLS-fingerprint,ICE-lite,media echo,label')) {
for my $x (split(/,/, 'from-tag,to-tag,call-id,transport protocol,media address,ICE,address family,DTLS,via-branch,media address,ptime,xmlrpc-callback,metadata,address,file,db-id,code,DTLS-fingerprint,ICE-lite,media echo,label,set-label,from-label,to-label')) {
defined($options{$x}) and $packet{$x} = \$options{$x};
}
for my $x (split(/,/, 'TOS,delete-delay')) {
defined($options{$x}) and $packet{$x} = $options{$x};
}
for my $x (split(/,/, 'trust address,symmetric,asymmetric,unidirectional,force,strict source,media handover,sip source address,reset,port latching,no rtcp attribute,full rtcp attribute,loop protect,record call,always transcode,all,pad crypto,generate mid,fragment,original sendrecv,symmetric codecs,asymmetric codecs,inject DTMF,generate RTCP,single codec,reorder codecs,pierce NAT,SIP-source-address')) {
for my $x (split(/,/, 'trust address,symmetric,asymmetric,unidirectional,force,strict source,media handover,sip source address,reset,port latching,no rtcp attribute,full rtcp attribute,loop protect,record call,always transcode,all,pad crypto,generate mid,fragment,original sendrecv,symmetric codecs,asymmetric codecs,inject DTMF,generate RTCP,single codec,reorder codecs,pierce NAT,SIP-source-address,allow transcoding')) {
defined($options{$x}) and push(@{$packet{flags}}, $x);
}
for my $x (split(/,/, 'origin,session connection,sdp version,username,session-name,zero-address')) {
@ -188,9 +192,19 @@ my $resp = $engine->req(\%packet);
#print Dumper $resp;
#exit;
if (exists($$resp{result}) && $$resp{result} eq 'ok') {
delete $$resp{result};
}
if (defined($$resp{sdp})) {
print("New SDP:\n-----8<-----8<-----8<-----8<-----8<-----\n$$resp{sdp}\n".
"----->8----->8----->8----->8----->8-----\n");
delete $$resp{sdp};
if (%$resp) {
print("Result dictionary:\n-----8<-----8<-----8<-----8<-----8<-----\n"
. Dumper($resp)
. "----->8----->8----->8----->8----->8-----\n");
}
}
else {
local $Data::Dumper::Indent = 1;

Loading…
Cancel
Save