From 2ca64340e1d1459677d4f176f9d28c6e1d1ebb1f Mon Sep 17 00:00:00 2001 From: Richard Fuchs Date: Mon, 1 Nov 2021 09:44:34 -0400 Subject: [PATCH] TT#136957 support DTMF triggers for blocking Change-Id: I32e0d02e739ceb5a34aaf187b1ea086482e6d73e --- README.md | 34 ++++++++++- daemon/call_interfaces.c | 100 ++++++++++++++++++++++++-------- daemon/codec.c | 46 ++++++++++++++- daemon/dtmf.c | 117 +++++++++++++++++++++++++++++++++++++- daemon/main.c | 2 + include/call.h | 7 +++ include/call_interfaces.h | 6 ++ include/codec.h | 1 + include/main.h | 1 + utils/rtpengine-ng-client | 10 +++- 10 files changed, 291 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index d92dd0db4..413aca5d3 100644 --- a/README.md +++ b/README.md @@ -1529,7 +1529,39 @@ Optionally included keys are: in-band DTMF audio tones and RFC event packets); `zero` which is similar to `random` except that a zero event is always used; `DTMF` which is similar to `zero` except that a different DTMF digit can be - specified. + specified; `off` to disable DTMF blocking. + +* `DTMF-security-trigger` + + Blocking mode to enable when the DTMF `trigger` (see below) is detected. + +* `DTMF-security-trigger-end` + + Blocking mode to enable when the DTMF `end trigger` (see below) is + detected. + +* `trigger` + + A string of DTMF digits that enable a DTMF blocking mode when detected. + +* `end trigger` or `trigger-end` + + A string of DTMF digits that disable DTMF blocking or enable a + different DTMF blocking mode when detected, but only after the initial + enabling `trigger` has been detected. + +* `trigger-end-time` + + Time in milliseconds that a DTMF blocking mode enabled by the `trigger` + should remain active the most. After the time has expired, the blocking + mode is switched to the `trigger-end` mode. + +* `trigger-end-digits` + + Number of DTMF digits that a DTMF blocking mode enabled by the + `trigger` should remain active the most. After this number of DTMF + digits has been detected, the blocking mode is switched to the + `trigger-end` mode. * `frequency` diff --git a/daemon/call_interfaces.c b/daemon/call_interfaces.c index a40c52ef5..79f1a6ba4 100644 --- a/daemon/call_interfaces.c +++ b/daemon/call_interfaces.c @@ -1018,6 +1018,35 @@ static void call_ng_codec_flags(struct sdp_ng_flags *out, str *key, bencode_item } #endif } +static void call_ng_parse_block_mode(str *s, enum block_dtmf_mode *output) { + switch (__csh_lookup(s)) { + case CSH_LOOKUP("off"): + *output = BLOCK_DTMF_OFF; + break; + case CSH_LOOKUP("drop"): + *output = BLOCK_DTMF_DROP; + break; + case CSH_LOOKUP("silence"): + *output = BLOCK_DTMF_SILENCE; + break; + case CSH_LOOKUP("tone"): + *output = BLOCK_DTMF_TONE; + break; + case CSH_LOOKUP("random"): + *output = BLOCK_DTMF_RANDOM; + break; + case CSH_LOOKUP("zero"): + *output = BLOCK_DTMF_ZERO; + break; + case CSH_LOOKUP("DTMF"): + case CSH_LOOKUP("dtmf"): + *output = BLOCK_DTMF_DTMF; + break; + default: + ilog(LOG_WARN, "Unknown DTMF block mode encountered: '" STR_FORMAT "'", + STR_FMT(s)); + } +} static void call_ng_main_flags(struct sdp_ng_flags *out, str *key, bencode_item_t *value) { str s = STR_NULL; bencode_item_t *it; @@ -1328,6 +1357,27 @@ static void call_ng_main_flags(struct sdp_ng_flags *out, str *key, bencode_item_ if (s.len == 1) out->digit = s.s[0]; break; + case CSH_LOOKUP("trigger"): + out->trigger = s; + break; + case CSH_LOOKUP("trigger-end"): + case CSH_LOOKUP("trigger end"): + case CSH_LOOKUP("end trigger"): + case CSH_LOOKUP("end-trigger"): + out->trigger_end = s; + break; + case CSH_LOOKUP("trigger-end-time"): + case CSH_LOOKUP("trigger end time"): + case CSH_LOOKUP("end-trigger-time"): + case CSH_LOOKUP("end trigger time"): + out->trigger_end_ms = bencode_get_integer_str(value, out->trigger_end_ms); + break; + case CSH_LOOKUP("trigger-end-digits"): + case CSH_LOOKUP("trigger end digits"): + case CSH_LOOKUP("end-trigger-digits"): + case CSH_LOOKUP("end trigger digits"): + out->trigger_end_digits = bencode_get_integer_str(value, out->trigger_end_digits); + break; #ifdef WITH_TRANSCODING case CSH_LOOKUP("T38"): case CSH_LOOKUP("T.38"): @@ -1339,30 +1389,19 @@ static void call_ng_main_flags(struct sdp_ng_flags *out, str *key, bencode_item_ case CSH_LOOKUP("dtmf-security"): case CSH_LOOKUP("DTMF security"): case CSH_LOOKUP("dtmf security"): - switch (__csh_lookup(&s)) { - case CSH_LOOKUP("drop"): - out->block_dtmf_mode = BLOCK_DTMF_DROP; - break; - case CSH_LOOKUP("silence"): - out->block_dtmf_mode = BLOCK_DTMF_SILENCE; - break; - case CSH_LOOKUP("tone"): - out->block_dtmf_mode = BLOCK_DTMF_TONE; - break; - case CSH_LOOKUP("random"): - out->block_dtmf_mode = BLOCK_DTMF_RANDOM; - break; - case CSH_LOOKUP("zero"): - out->block_dtmf_mode = BLOCK_DTMF_ZERO; - break; - case CSH_LOOKUP("DTMF"): - case CSH_LOOKUP("dtmf"): - out->block_dtmf_mode = BLOCK_DTMF_DTMF; - break; - default: - ilog(LOG_WARN, "Unknown 'DTMF-security' flag encountered: '" STR_FORMAT "'", - STR_FMT(&s)); - } + call_ng_parse_block_mode(&s, &out->block_dtmf_mode); + break; + case CSH_LOOKUP("DTMF-security-trigger"): + case CSH_LOOKUP("dtmf-security-trigger"): + case CSH_LOOKUP("DTMF security trigger"): + case CSH_LOOKUP("dtmf security trigger"): + call_ng_parse_block_mode(&s, &out->block_dtmf_mode_trigger); + break; + case CSH_LOOKUP("DTMF-security-trigger-end"): + case CSH_LOOKUP("dtmf-security-trigger-end"): + case CSH_LOOKUP("DTMF security trigger end"): + case CSH_LOOKUP("dtmf security trigger end"): + call_ng_parse_block_mode(&s, &out->block_dtmf_mode_trigger_end); break; case CSH_LOOKUP("delay-buffer"): case CSH_LOOKUP("delay buffer"): @@ -1371,6 +1410,7 @@ static void call_ng_main_flags(struct sdp_ng_flags *out, str *key, bencode_item_ #endif } } + static void call_ng_process_flags(struct sdp_ng_flags *out, bencode_item_t *input, enum call_opmode opmode) { call_ng_flags_init(out, opmode); call_ng_dict_iter(out, input, call_ng_main_flags); @@ -2394,6 +2434,13 @@ static void call_monologue_set_block_mode(struct call_monologue *ml, struct sdp_ ml->dtmf_digit = flags->digit; } + call_str_cpy(ml->call, &ml->dtmf_trigger, &flags->trigger); + call_str_cpy(ml->call, &ml->dtmf_trigger_end, &flags->trigger_end); + ml->block_dtmf_trigger = flags->block_dtmf_mode_trigger; + ml->dtmf_trigger_match = 0; + ml->dtmf_trigger_digits = flags->trigger_end_digits; + ml->block_dtmf_trigger_end_ms = flags->trigger_end_ms; + codec_update_all_handlers(ml); } const char *call_block_dtmf_ng(bencode_item_t *input, bencode_item_t *output) { @@ -2407,6 +2454,9 @@ const char *call_block_dtmf_ng(bencode_item_t *input, bencode_item_t *output) { return errstr; enum block_dtmf_mode mode = BLOCK_DTMF_DROP; + // special case default: if there's a trigger, default block mode is none + if (flags.block_dtmf_mode_trigger || flags.trigger.len) + mode = BLOCK_DTMF_OFF; if (flags.block_dtmf_mode) mode = flags.block_dtmf_mode; @@ -2420,7 +2470,7 @@ const char *call_block_dtmf_ng(bencode_item_t *input, bencode_item_t *output) { call->block_dtmf = mode; } - if (is_dtmf_replace_mode(mode) || flags.delay_buffer >= 0) { + if (is_dtmf_replace_mode(mode) || flags.delay_buffer >= 0 || flags.trigger.len) { if (monologue) call_monologue_set_block_mode(monologue, &flags); else { diff --git a/daemon/codec.c b/daemon/codec.c index b79d6f548..96ca7b6ee 100644 --- a/daemon/codec.c +++ b/daemon/codec.c @@ -32,6 +32,12 @@ struct mqtt_timer { struct call *call; struct call_media *media; }; +struct timer_callback { + struct codec_timer ct; + void (*func)(struct call *, void *); + struct call *call; + void *ptr; +}; typedef void (*raw_input_func_t)(struct media_packet *mp, unsigned int); @@ -1006,6 +1012,10 @@ void codec_handlers_update(struct call_media *receiver, struct call_media *sink, bool do_pcm_dtmf_blocking = is_pcm_dtmf_block_mode(dtmf_block_mode); bool do_dtmf_blocking = is_dtmf_replace_mode(dtmf_block_mode); + bool do_dtmf_detect = false; + if (receiver->monologue->dtmf_trigger.len) + do_dtmf_detect = true; + // do we have to force everything through the transcoding engine even if codecs match? bool force_transcoding = do_pcm_dtmf_blocking || do_dtmf_blocking; if (sink->monologue->inject_dtmf) @@ -1136,13 +1146,19 @@ void codec_handlers_update(struct call_media *receiver, struct call_media *sink, if (!recv_dtmf_pt) pcm_dtmf_detect = true; } - else if (do_dtmf_blocking) { + else if (do_dtmf_blocking && !pcm_dtmf_detect) { // we only need the DSP if there's no DTMF payload present, as otherwise // we expect DTMF event packets if (!recv_dtmf_pt) pcm_dtmf_detect = true; } + // same logic if we need to detect DTMF + if (do_dtmf_detect && !pcm_dtmf_detect) { + if (!recv_dtmf_pt) + pcm_dtmf_detect = true; + } + if (pcm_dtmf_detect) { if (sink_dtmf_pt) ilogs(codec, LOG_DEBUG, "Enabling PCM DTMF detection from " STR_FORMAT @@ -1906,7 +1922,7 @@ static int packet_dtmf_event(struct codec_ssrc_handler *ch, struct codec_ssrc_ha LOCK(&mp->media->dtmf_lock); if (mp->media->dtmf_ts != packet->ts) { // ignore already processed events - int ret = dtmf_event_packet(mp, packet->payload, ch->encoder_format.clockrate, packet->ts); + int ret = dtmf_event_packet(mp, packet->payload, ch->handler->source_pt.clock_rate, packet->ts); if (G_UNLIKELY(ret == -1)) // error return -1; if (ret == 1) { @@ -4640,6 +4656,32 @@ bool codec_store_is_full_answer(const struct codec_store *src, const struct code return true; } + + +static void __codec_timer_callback_free(void *p) { + struct timer_callback *cb = p; + if (cb->call) + obj_put(cb->call); +} +static void __codec_timer_callback_fire(struct codec_timer *ct) { + struct timer_callback *cb = (void *) ct; + log_info_call(cb->call); + cb->func(cb->call, cb->ptr); + codec_timer_stop(&ct); + log_info_clear(); +} +void codec_timer_callback(struct call *c, void (*func)(struct call *, void *), void *p, uint64_t delay) { + struct timer_callback *cb = obj_alloc0("codec_timer_callback", sizeof(*cb), __codec_timer_callback_free); + cb->ct.tt_obj.tt = &codec_timers_thread; + cb->call = obj_get(c); + cb->func = func; + cb->ptr = p; + cb->ct.func = __codec_timer_callback_fire; + cb->ct.next = rtpe_now; + timeval_add_usec(&cb->ct.next, delay); + timerthread_obj_schedule_abs(&cb->ct.tt_obj, &cb->ct.next); +} + static void codec_timers_run(void *p) { struct codec_timer *ct = p; ct->func(ct); diff --git a/daemon/dtmf.c b/daemon/dtmf.c index 24fe4e4a8..e3be480e4 100644 --- a/daemon/dtmf.c +++ b/daemon/dtmf.c @@ -151,12 +151,123 @@ static void dtmf_end_event(struct call_media *media, unsigned int event, unsigne g_string_free(buf, TRUE); } -static void dtmf_code_event(struct call_media *media, char event, uint64_t ts) { +static void dtmf_trigger_set(struct call *c, void *mlp) { + struct call_monologue *ml = mlp; + + rwlock_lock_w(&c->master_lock); + + ilog(LOG_INFO, "Setting DTMF block mode to %i and setting new trigger to '" STR_FORMAT "'", + ml->block_dtmf_trigger, STR_FMT(&ml->dtmf_trigger_end)); + + ml->block_dtmf = ml->block_dtmf_trigger; + + // switch trigger to end trigger + ml->block_dtmf_trigger = ml->block_dtmf_trigger_end; + ml->dtmf_trigger = ml->dtmf_trigger_end; + ml->dtmf_trigger_end = STR_NULL; + ml->dtmf_trigger_digits *= -1; // negative means it's active + + codec_update_all_handlers(ml); + + rwlock_unlock_w(&c->master_lock); +} +static void dtmf_trigger_unset(struct call *c, void *mlp) { + struct call_monologue *ml = mlp; + + ilog(LOG_INFO, "Setting DTMF block mode to %i", ml->block_dtmf_trigger_end); + + rwlock_lock_w(&c->master_lock); + + ml->block_dtmf = ml->block_dtmf_trigger_end; + ml->dtmf_trigger = STR_NULL; + + codec_update_all_handlers(ml); + + rwlock_unlock_w(&c->master_lock); +} + +static void dtmf_check_trigger(struct call_media *media, char event, uint64_t ts, int clockrate) { + struct call_monologue *ml = media->monologue; + + if (!clockrate) + clockrate = 8000; + + if (!ml->dtmf_trigger.len) // do we have a trigger? + return; + if (ml->dtmf_trigger_match >= ml->dtmf_trigger.len) // is the trigger done already? + return; + + // check delay from previous event + if (media->dtmf_start) { + uint32_t ts_diff = ts - media->dtmf_start; + uint64_t ts_diff_ms = ts_diff * 1000 / clockrate; + if (ts_diff_ms > rtpe_config.dtmf_digit_delay) { + // delay too long: restart event trigger + ml->dtmf_trigger_match = 0; + } + } + + if (ml->dtmf_trigger_digits < 0) { + // end trigger is active + ml->dtmf_trigger_digits++; + if (ml->dtmf_trigger_digits == 0) { + // got all digits + codec_timer_callback(ml->call, dtmf_trigger_unset, ml, 0); + } + } + + // is the new event a match? + if (ml->dtmf_trigger.s[ml->dtmf_trigger_match] == event) { + ml->dtmf_trigger_match++; + if (ml->dtmf_trigger_match == ml->dtmf_trigger.len) { + // trigger is finished + ml->dtmf_trigger_match = 0; // reset + + ilog(LOG_INFO, "DTMF trigger ('" STR_FORMAT "') matched, setting block mode to %i", + STR_FMT(&ml->dtmf_trigger), ml->block_dtmf_trigger); + + // We only hold a read-lock on the call here and cannot switch to a write-lock + // easily, which is needed to reset the codec handlers. Therefore we do this + // asynchronously: + codec_timer_callback(ml->call, dtmf_trigger_set, ml, 0); + + // set up unblock triggers + if (ml->block_dtmf_trigger_end_ms) + codec_timer_callback(ml->call, dtmf_trigger_unset, ml, + ml->block_dtmf_trigger_end_ms * 1000); + } + return; + } + + // can we do a partial match? + for (size_t off = 1; off < ml->dtmf_trigger_match; off++) { + // look for repeating prefix: trigger "ABCABD", matched 5, prefix at offset 3: [AB]C[AB] + if (memcmp(ml->dtmf_trigger.s + off, ml->dtmf_trigger.s, ml->dtmf_trigger_match - off)) + continue; + // is the new event a match? + unsigned int next_match_idx = ml->dtmf_trigger.len - off; + if (ml->dtmf_trigger.s[next_match_idx] == event) { + // got a partial match + ml->dtmf_trigger_match = next_match_idx; + return; + } + } + // no partial match... reset completely + if (event == ml->dtmf_trigger.s[0]) + ml->dtmf_trigger_match = 1; + else + ml->dtmf_trigger_match = 0; +} + +static void dtmf_code_event(struct call_media *media, char event, uint64_t ts, int clockrate) { if (media->dtmf_code == event) // old/ongoing event return; // start of new event + // check trigger before setting new dtmf_start + dtmf_check_trigger(media, event, ts, clockrate); + media->dtmf_code = event; media->dtmf_start = ts; media->dtmf_end = 0; @@ -204,7 +315,7 @@ int dtmf_event_packet(struct media_packet *mp, str *payload, int clockrate, uint dtmf->event, dtmf->volume, dtmf->end, ntohs(dtmf->duration)); if (!dtmf->end) { - dtmf_code_event(mp->media, dtmf_code_to_char(dtmf->event), ts); + dtmf_code_event(mp->media, dtmf_code_to_char(dtmf->event), ts, clockrate); return 0; } @@ -251,7 +362,7 @@ void dtmf_dsp_event(const struct dtmf_event *new_event, struct dtmf_event *cur_e new_event->code, new_event->volume, duration); int code = dtmf_code_from_char(new_event->code); // for validation if (code != -1) - dtmf_code_event(media, (char) new_event->code, ts); + dtmf_code_event(media, (char) new_event->code, ts, clockrate); } } diff --git a/daemon/main.c b/daemon/main.c index 36939dc68..2f6394d66 100644 --- a/daemon/main.c +++ b/daemon/main.c @@ -97,6 +97,7 @@ struct rtpengine_config rtpe_config = { .mqtt_port = 1883, .mqtt_keepalive = 30, .mqtt_publish_interval = 5000, + .dtmf_digit_delay = 2500, .common = { .log_levels = { [log_level_index_internals] = -1, @@ -491,6 +492,7 @@ static void options(int *argc, char ***argv) { { "dtmf-log-dest", 0,0, G_OPTION_ARG_STRING, &dtmf_udp_ep, "Destination address for DTMF logging via UDP", "IP46|HOSTNAME:PORT" }, { "dtmf-log-ng-tcp", 0,0, G_OPTION_ARG_NONE, &rtpe_config.dtmf_via_ng, "DTMF logging via TCP NG protocol", NULL }, { "dtmf-no-suppress", 0,0,G_OPTION_ARG_NONE, &rtpe_config.dtmf_no_suppress, "Disable audio suppression during DTMF events", NULL }, + { "dtmf-digit-delay", 0,0,G_OPTION_ARG_INT, &rtpe_config.dtmf_digit_delay, "Delay in ms between DTMF digit for trigger detection", NULL }, #endif { "log-format", 0, 0, G_OPTION_ARG_STRING, &log_format, "Log prefix format", "default|parsable"}, { "xmlrpc-format",'x', 0, G_OPTION_ARG_INT, &rtpe_config.fmt, "XMLRPC timeout request format to use. 0: SEMS DI, 1: call-id only, 2: Kamailio", "INT" }, diff --git a/include/call.h b/include/call.h index 3039cf6e5..4ed25d60f 100644 --- a/include/call.h +++ b/include/call.h @@ -470,6 +470,13 @@ struct call_monologue { unsigned int tone_freq; unsigned int tone_vol; char dtmf_digit; + str dtmf_trigger; + unsigned int dtmf_trigger_match; + enum block_dtmf_mode block_dtmf_trigger; + str dtmf_trigger_end; + int dtmf_trigger_digits; + enum block_dtmf_mode block_dtmf_trigger_end; + unsigned int block_dtmf_trigger_end_ms; unsigned int block_media:1; unsigned int silence_media:1; diff --git a/include/call_interfaces.h b/include/call_interfaces.h index 105fa19e7..e346c717c 100644 --- a/include/call_interfaces.h +++ b/include/call_interfaces.h @@ -81,6 +81,12 @@ struct sdp_ng_flags { int frequency; int volume; char digit; + str trigger; + enum block_dtmf_mode block_dtmf_mode_trigger; + str trigger_end; + enum block_dtmf_mode block_dtmf_mode_trigger_end; + int trigger_end_digits; + int trigger_end_ms; unsigned int asymmetric:1, protocol_accept:1, no_redis_update:1, diff --git a/include/codec.h b/include/codec.h index 6feb27a32..a4501d41b 100644 --- a/include/codec.h +++ b/include/codec.h @@ -79,6 +79,7 @@ void codecs_init(void); void codecs_cleanup(void); void codec_timers_loop(void *); void rtcp_timer_stop(struct rtcp_timer **); +void codec_timer_callback(struct call *, void (*)(struct call *, void *), void *, uint64_t delay); void mqtt_timer_stop(struct mqtt_timer **); void mqtt_timer_start(struct mqtt_timer **mqtp, struct call *call, struct call_media *media); diff --git a/include/main.h b/include/main.h index 0d323fef3..cd8f5af03 100644 --- a/include/main.h +++ b/include/main.h @@ -100,6 +100,7 @@ struct rtpengine_config { endpoint_t dtmf_udp_ep; int dtmf_via_ng; int dtmf_no_suppress; + int dtmf_digit_delay; enum endpoint_learning endpoint_learning; int jb_length; int jb_clock_drift; diff --git a/utils/rtpengine-ng-client b/utils/rtpengine-ng-client index a265bbd3c..3873355d5 100755 --- a/utils/rtpengine-ng-client +++ b/utils/rtpengine-ng-client @@ -105,17 +105,23 @@ GetOptions( 'to-label=s' => \$options{'to-label'}, 'from-tags=s@' => \$options{'from-tags'}, 'DTMF-security=s' => \$options{'DTMF-security'}, + 'DTMF-security-trigger=s' => \$options{'DTMF-security-trigger'}, + 'DTMF-security-trigger-end=s' => \$options{'DTMF-security-trigger-end'}, 'delay-buffer=i' => \$options{'delay-buffer'}, 'frequency=i' => \$options{'frequency'}, 'volume=i' => \$options{'volume'}, 'digit=s' => \$options{'digit'}, + 'trigger=s' => \$options{'trigger'}, + 'trigger-end=s' => \$options{'trigger-end'}, + 'trigger-end-digits=i' => \$options{'trigger-end-digits'}, + 'trigger-end-time=i' => \$options{'trigger-end-time'}, ) 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,set-label,from-label,to-label,DTMF-security,digit')) { +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,DTMF-security,digit,DTMF-security-trigger,DTMF-security-trigger-end,trigger,trigger-end')) { if (defined($options{$x})) { if (!$options{json}) { $packet{$x} = \$options{$x}; @@ -125,7 +131,7 @@ for my $x (split(/,/, 'from-tag,to-tag,call-id,transport protocol,media address, } } } -for my $x (split(/,/, 'TOS,delete-delay,delay-buffer,volume,frequency')) { +for my $x (split(/,/, 'TOS,delete-delay,delay-buffer,volume,frequency,trigger-end-time,trigger-end-digits')) { 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,SIPREC,pad crypto,generate mid,fragment,original sendrecv,symmetric codecs,asymmetric codecs,inject DTMF,detect DTMF,generate RTCP,single codec,reorder codecs,pierce NAT,SIP-source-address,allow transcoding')) {