You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
rtpengine/daemon/t38.c

807 lines
20 KiB

#include "t38.h"
#ifdef WITH_TRANSCODING
#include <assert.h>
#include <spandsp/t30.h>
#include <spandsp/logging.h>
#include "spandsp_logging.h"
#include "codec.h"
#include "call.h"
#include "log.h"
#include "str.h"
#include "media_player.h"
#include "log_funcs.h"
#include "sdp.h"
struct udptl_packet {
seq_packet_t p;
str *s;
};
static void __add_udptl_len(GString *s, const void *buf, unsigned int len) {
if (len < 0x80) {
g_string_append_c(s, len);
if (len)
g_string_append_len(s, buf, len);
return;
}
if (len < 0x4000) {
uint16_t enc_len = htons(0x8000 | len);
g_string_append_len(s, (void *) &enc_len, 2);
g_string_append_len(s, buf, len);
return;
}
// fragmented - we don't support more than 65535 bytes
unsigned int mult = len >> 14;
g_string_append_c(s, 0xc0 | mult);
mult <<= 14;
// one portion
g_string_append_len(s, buf, mult);
// remainder - may be zero length
__add_udptl_len(s, buf + mult, len - mult);
}
static void __add_udptl_raw(GString *s, const char *buf, size_t len) {
assert(len < 0x10000);
if (len == 0) {
// add a single zero byte, length 1
__add_udptl_len(s, "\x00", 1);
return;
}
__add_udptl_len(s, buf, len);
}
static void __add_udptl(GString *s, const str *buf) {
__add_udptl_raw(s, buf->s, buf->len);
}
static void g_string_null_extend(GString *s, size_t len) {
if (s->len >= len)
return;
size_t oldb = s->len;
size_t newb = len - s->len;
g_string_set_size(s, len);
memset(s->str + oldb, 0, newb);
}
static void spandsp_logging_func(SPAN_LOG_ARGS) {
if (level <= SPAN_LOG_PROTOCOL_ERROR)
level = LOG_ERR;
else if (level <= SPAN_LOG_PROTOCOL_WARNING)
level = LOG_WARN;
else
level = LOG_DEBUG;
ilogs(spandsp, level, "SpanDSP: %s", text);
}
// call is locked in R or W
static int t38_gateway_handler(t38_core_state_t *stat, void *user_data, const uint8_t *b, int len, int count) {
struct t38_gateway *tg = user_data;
// cap the max length of packet we can handle
if (len < 0 || len >= 0x10000) {
ilog(LOG_ERR, "Received %i bytes from T.38 encoder - discarding", len);
return -1;
}
ilog(LOG_DEBUG, "Received %i bytes from T.38 encoder", len);
// build udptl packet: use a conservative guess for required buffer
GString *s = g_string_sized_new(512);
// add seqnum
uint16_t seq = htons(tg->seqnum);
g_string_append_len(s, (void *) &seq, 2);
// add primary IFP packet
str buf = STR_LEN(b, len);
__add_udptl(s, &buf);
// add error correction packets
if (tg->options.fec_span > 1) {
// forward error correction
g_string_append_c(s, 0x80);
// figure out how many packets we have and which span to use
unsigned int packets = tg->options.fec_span * tg->options.max_ec_entries;
if (packets > tg->udptl_ec_out.length)
packets = tg->udptl_ec_out.length;
unsigned int span = packets / tg->options.max_ec_entries;
if (!span)
span = 1;
packets = span * tg->options.max_ec_entries; // our own packets we use
unsigned int entries = packets / span; // FEC entries in the output
if (entries > tg->udptl_ec_out.length)
entries = tg->udptl_ec_out.length;
packets = entries * span;
assert(span < 0x80);
assert(entries < 0x80);
g_string_append_c(s, 0x01);
g_string_append_c(s, span);
// create needed number of FEC packet entries
GQueue fec = G_QUEUE_INIT;
for (int i = 0; i < entries; i++)
g_queue_push_tail(&fec, g_string_new(""));
// take each input packet, going backwards in time, and XOR it into
// the respective output FEC packet
GList *inp = tg->udptl_ec_out.head;
for (int i = 0; i < packets; i++) {
assert(inp != NULL);
str *ip = inp->data;
// just keep shifting the list around
GString *outp = g_queue_pop_head(&fec);
// extend string as needed
g_string_null_extend(outp, ip->len);
for (size_t j = 0; j < ip->len; j++)
outp->str[j] ^= ip->s[j];
g_queue_push_tail(&fec, outp);
inp = inp->next;
}
// output list is now complete, but in reverse. append it to output buffer
GString *ec = g_string_sized_new(512);
entries = 0;
int going = 1;
while (fec.length) {
GString *outp = g_queue_pop_tail(&fec);
if (going) {
if (s->len + ec->len + outp->len > tg->options.max_datagram)
going = 0;
else {
__add_udptl_raw(ec, outp->str, outp->len);
entries++;
}
}
g_string_free(outp, TRUE);
}
g_string_append_c(s, entries);
g_string_append_len(s, ec->str, ec->len);
g_string_free(ec, TRUE);
}
else {
// redundancy error correction
g_string_append_c(s, 0x00);
GString *ec = g_string_sized_new(512);
int entries = 0;
for (GList *l = tg->udptl_ec_out.head; l; l = l->next) {
str *ec_s = l->data;
// stop when we exceed max datagram length
if (s->len + ec->len + ec_s->len > tg->options.max_datagram)
break;
// add redundancy packet
__add_udptl(ec, ec_s);
entries++;
}
// number of entries - must be <0x80 as verified in settings
g_string_append_c(s, entries);
g_string_append_len(s, ec->str, ec->len);
g_string_free(ec, TRUE);
}
// done building our packet - add primary to our error correction buffer
tg->seqnum++;
unsigned int q_entries = tg->options.max_ec_entries * tg->options.fec_span;
if (q_entries) {
while (tg->udptl_ec_out.length >= q_entries) {
str *ec_s = g_queue_pop_tail(&tg->udptl_ec_out);
free(ec_s);
}
g_queue_push_head(&tg->udptl_ec_out, str_dup(&buf));
}
// send our packet if we can
struct packet_stream *ps = NULL;
if (tg->t38_media && tg->t38_media->streams.head)
ps = tg->t38_media->streams.head->data;
if (ps)
mutex_lock(&ps->out_lock);
stream_fd *sfd = NULL;
if (ps)
sfd = ps->selected_sfd;
if (sfd && sfd->socket.fd != -1 && ps->endpoint.address.family != NULL) {
for (int i = 0; i < count; i++) {
ilog(LOG_DEBUG, "Sending %u UDPTL bytes", (unsigned int) s->len);
socket_sendto(&sfd->socket, s->str, s->len, &ps->endpoint);
}
}
else
ilog(LOG_WARN | LOG_FLAG_LIMIT, "Unable to send T.38 UDPTL packet due to lack of "
"socket or stream");
if (ps)
mutex_unlock(&ps->out_lock);
g_string_free(s, TRUE);
return 0;
}
void __t38_gateway_free(struct t38_gateway *tg) {
ilog(LOG_DEBUG, "Destroying T.38 gateway");
if (tg->gw)
t38_gateway_free(tg->gw);
if (tg->pcm_player) {
media_player_stop(tg->pcm_player);
media_player_put(&tg->pcm_player);
}
if (tg->udptl_fec)
g_hash_table_destroy(tg->udptl_fec);
g_queue_clear_full(&tg->udptl_ec_out, free);
packet_sequencer_destroy(&tg->sequencer);
}
// call is locked in R and mp is locked
static bool t38_pcm_player(struct media_player *mp) {
if (!mp || !mp->media)
return true;
struct t38_gateway *tg = mp->media->t38_gateway;
if (!tg)
return true;
if (tg->pcm_media && tg->pcm_media->streams.head
&& ((struct packet_stream *) tg->pcm_media->streams.head->data)->selected_sfd)
log_info_stream_fd(((struct packet_stream *) tg->pcm_media->streams.head->data)->selected_sfd);
ilog(LOG_DEBUG, "Generating T.38 PCM samples");
mutex_lock(&tg->lock);
int16_t smp[80];
int num = t38_gateway_tx(tg->gw, smp, 80);
if (num <= 0) {
ilog(LOG_DEBUG, "No T.38 PCM samples generated");
// use a fixed interval of 10 ms
mp->next_run += 10000;
timerthread_obj_schedule_abs(&mp->tt_obj, mp->next_run);
mutex_unlock(&tg->lock);
return false;
}
ilog(LOG_DEBUG, "Generated %i T.38 PCM samples", num);
// release gateway lock as the media player may trigger a lock on the SSRC objects
// and this is the wrong lock order
struct media_player *pcm_player = media_player_get(tg->pcm_player);
unsigned long long pts = tg->pts;
tg->pts += num;
// handle fill-in
if (rtpe_now - tg->last_rx_ts > 30000) {
ilog(LOG_DEBUG, "Adding T.38 fill-in samples");
t38_gateway_rx_fillin(tg->gw, 80);
}
mutex_unlock(&tg->lock);
// this reschedules our player as well
media_player_add_packet(pcm_player, (char *) smp, num * 2, num * 1000000 / 8000, pts);
media_player_put(&pcm_player);
return false;
}
static void __udptl_packet_free(struct udptl_packet *p) {
if (p->s)
free(p->s);
g_free(p);
}
static void __t38_options_normalise(struct t38_options *opts) {
if (opts->version < 0)
opts->version = 0;
if (opts->fec_span < 1)
opts->fec_span = 1;
if (opts->min_ec_entries < 0)
opts->min_ec_entries = 0;
if (opts->min_ec_entries >= 0x80)
opts->min_ec_entries = 0x7f;
if (opts->max_ec_entries < 0)
opts->max_ec_entries = 0;
if (opts->max_ec_entries >= 0x80)
opts->max_ec_entries = 0x7f;
if (opts->max_ifp <= 0 || opts->max_ifp >= 0x4000)
opts->max_ifp = 0x3fff;
if (opts->max_datagram <= 0 || opts->max_datagram >= 0x4000)
opts->max_datagram = 0x3fff;
}
static int span_log_level_map(int level) {
if (level <= LOG_ERR)
level = SPAN_LOG_PROTOCOL_ERROR;
else if (level < LOG_DEBUG)
level = SPAN_LOG_PROTOCOL_WARNING;
else
level = SPAN_LOG_DEBUG_3;
return level;
}
static void t38_insert_media_attributes(GString *gs, struct call_media *media, struct call_media *source_media,
const sdp_ng_flags *flags)
{
struct t38_gateway *tg = media->t38_gateway;
if (!tg)
return;
sdp_append_attr(gs, flags, MT_IMAGE, "T38FaxVersion", "%i", tg->options.version);
sdp_append_attr(gs, flags, MT_IMAGE, "T38MaxBitRate", "14400");
sdp_append_attr(gs, flags, MT_IMAGE, "T38FaxRateManagement", "%s",
tg->options.local_tcf ? "localTFC" : "transferredTCF");
sdp_append_attr(gs, flags, MT_IMAGE, "T38FaxMaxBuffer", "1800");
sdp_append_attr(gs, flags, MT_IMAGE, "T38FaxMaxDatagram", "512");
if (tg->options.max_ec_entries == 0)
sdp_append_attr(gs, flags, MT_IMAGE, "T38FaxUdpEC", "t38UDPNoEC");
else if (tg->options.fec_span > 1)
sdp_append_attr(gs, flags, MT_IMAGE, "T38FaxUdpEC", "t38UDPFEC");
else
sdp_append_attr(gs, flags, MT_IMAGE, "T38FaxUdpEC", "t38UDPRedundancy");
// XXX more options possible here
}
// call is locked in W
int t38_gateway_pair(struct call_media *t38_media, struct call_media *pcm_media,
const struct t38_options *options)
{
const char *err = NULL;
if (!t38_media || !pcm_media || !options)
return -1;
struct t38_options opts = *options;
__t38_options_normalise(&opts);
// do we have one yet?
if (t38_media->t38_gateway
&& t38_media->t38_gateway == pcm_media->t38_gateway)
{
// XXX check options here?
return 0;
}
// release old structs, if any
t38_gateway_put(&t38_media->t38_gateway);
t38_gateway_put(&pcm_media->t38_gateway);
ilog(LOG_DEBUG, "Creating new T.38 gateway");
// create and init new
__auto_type tg = obj_alloc0(struct t38_gateway, __t38_gateway_free);
tg->t38_media = t38_media;
tg->pcm_media = pcm_media;
mutex_init(&tg->lock);
tg->udptl_fec = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL,
(GDestroyNotify) __udptl_packet_free);
tg->options = opts;
tg->pcm_pt.payload_type = -1;
tg->pcm_pt.encoding = STR("X-L16");
tg->pcm_pt.encoding_with_params = tg->pcm_pt.encoding;
tg->pcm_pt.clock_rate = 8000;
tg->pcm_pt.channels = 1;
tg->pcm_pt.ptime = 20;
err = "Failed to init PCM codec";
ensure_codec_def(&tg->pcm_pt, pcm_media);
if (!codec_def_supported(tg->pcm_pt.codec_def))
goto err;
err = "Failed to create spandsp T.38 gateway";
if (!(tg->gw = t38_gateway_init(NULL, t38_gateway_handler, tg)))
goto err;
media_player_new(&tg->pcm_player, pcm_media->monologue,
call_get_first_ssrc(&pcm_media->ssrc_hash_out),
NULL);
// even though we call media_player_set_media() here, we need to call it again in
// t38_gateway_start because our sink might not have any streams added here yet,
// leaving the media_player setup incomplete
media_player_set_media(tg->pcm_player, pcm_media);
tg->pcm_player->run_func = t38_pcm_player;
// set options
t38_core_state_t *t38 = t38_gateway_get_t38_core_state(tg->gw);
t38_gateway_set_ecm_capability(tg->gw, opts.no_ecm ? FALSE : TRUE);
t38_gateway_set_transmit_on_idle(tg->gw, TRUE);
t38_gateway_set_supported_modems(tg->gw,
(opts.no_v17 ? 0 : T30_SUPPORT_V17)
| (opts.no_v27ter ? 0 : T30_SUPPORT_V27TER)
| (opts.no_v29 ? 0 : T30_SUPPORT_V29)
| (opts.no_v34 ? 0 : T30_SUPPORT_V34HDX)
| (opts.no_iaf ? 0 : T30_SUPPORT_IAF));
t38_set_t38_version(t38, opts.version);
t38_set_data_rate_management_method(t38,
opts.local_tcf ? 1 : 2);
t38_set_fill_bit_removal(t38, opts.fill_bit_removal);
t38_set_mmr_transcoding(t38, opts.transcoding_mmr);
t38_set_jbig_transcoding(t38, opts.transcoding_jbig);
t38_set_max_datagram_size(t38, opts.max_ifp);
logging_state_t *ls = t38_gateway_get_logging_state(tg->gw);
my_span_set_log(ls, spandsp_logging_func);
span_log_set_level(ls, span_log_level_map(get_log_level(spandsp)));
ls = t38_core_get_logging_state(t38);
my_span_set_log(ls, spandsp_logging_func);
span_log_set_level(ls, span_log_level_map(get_log_level(spandsp)));
packet_sequencer_init(&tg->sequencer, (GDestroyNotify) __udptl_packet_free);
tg->sequencer.seq = 0;
// done - add references to media structs
t38_media->t38_gateway = tg;
pcm_media->t38_gateway = obj_get(tg);
// add SDP options for T38
t38_media->sdp_attr_print = t38_insert_media_attributes;
return 0;
err:
if (err)
ilog(LOG_ERR, "Failed to create T.38 gateway: %s", err);
t38_gateway_put(&tg);
return -1;
}
// call is locked in W
void t38_gateway_start(struct t38_gateway *tg, str_case_value_ht codec_set) {
if (!tg)
return;
// set up our player first
media_player_set_media(tg->pcm_player, tg->pcm_media);
if (media_player_setup(tg->pcm_player, &tg->pcm_pt, NULL, codec_set))
return;
// now start our player if we can or should
// already running?
if (tg->pcm_player->next_run)
return;
// only start our player only if we can send both ways
if (!tg->pcm_media->codecs.codec_prefs.length)
return;
if (!tg->pcm_media->streams.length)
return;
if (!tg->t38_media->streams.length)
return;
struct packet_stream *ps;
ps = tg->pcm_media->streams.head->data;
if (!PS_ISSET(ps, FILLED))
return;
ps = tg->t38_media->streams.head->data;
if (!PS_ISSET(ps, FILLED))
return;
ilog(LOG_DEBUG, "Starting T.38 PCM player");
// start off PCM player
tg->pcm_player->next_run = rtpe_now;
timerthread_obj_schedule_abs(&tg->pcm_player->tt_obj, tg->pcm_player->next_run);
}
// call is locked in R
int t38_gateway_input_samples(struct t38_gateway *tg, int16_t amp[], int len) {
if (!tg)
return 0;
if (len <= 0)
return 0;
ilog(LOG_DEBUG, "Adding %i samples to T.38 encoder", len);
mutex_lock(&tg->lock);
int left = t38_gateway_rx(tg->gw, amp, len);
if (left)
ilog(LOG_WARN | LOG_FLAG_LIMIT, "%i PCM samples were not processed by the T.38 gateway",
left);
tg->last_rx_ts = rtpe_now;
mutex_unlock(&tg->lock);
return 0;
}
static ssize_t __get_udptl_len(str *s) {
ssize_t ret;
if (s->len < 1)
return -1;
if (!(s->s[0] & 0x80)) {
ret = s->s[0];
str_shift(s, 1);
return ret;
}
if (s->len < 2)
return -1;
if (!(s->s[0] & 0x40)) {
ret = ntohs(*((uint16_t *) s->s)) & 0x3fff;
str_shift(s, 2);
return ret;
}
ilog(LOG_INFO | LOG_FLAG_LIMIT, "Decoding UDPTL fragments is not supported");
return -1;
}
static int __get_udptl(str *piece, str *s) {
ssize_t len = __get_udptl_len(s);
if (len < 0)
return -1;
return str_shift_ret(s, len, piece);
}
static struct udptl_packet *__make_udptl_packet(const str *piece, uint16_t seq) {
struct udptl_packet *up = g_new0(__typeof(*up), 1);
up->p.seq = seq;
up->s = str_dup(piece);
return up;
}
static void __fec_save(struct t38_gateway *tg, const str *piece, uint16_t seq) {
struct udptl_packet *up = __make_udptl_packet(piece, seq);
g_hash_table_insert(tg->udptl_fec, GUINT_TO_POINTER(seq), up);
}
int t38_gateway_input_udptl(struct t38_gateway *tg, const str *buf) {
const char *err = NULL;
struct udptl_packet *up = NULL;
if (!tg)
return 0;
if (!buf || !buf->len)
return 0;
if (buf->len < 4) {
ilog(LOG_INFO | LOG_FLAG_LIMIT, "Ignoring short UDPTL packet (%zu bytes)", buf->len);
return 0;
}
ilog(LOG_DEBUG, "Processing %zu UDPTL bytes", buf->len);
str s = *buf;
str piece;
// get seq num
uint16_t seq;
if (str_shift_ret(&s, 2, &piece))
goto err_nolock;
seq = ntohs(*((uint16_t *) piece.s));
err = "Invalid primary UDPTL packet";
if (__get_udptl(&piece, &s))
goto err;
ilog(LOG_DEBUG, "Received primary IFP packet, len %zu, seq %i", piece.len, seq);
str primary = piece;
up = __make_udptl_packet(&primary, seq);
err = "Error correction mode byte missing";
if (str_shift_ret(&s, 1, &piece))
goto err_nolock;
char fec = piece.s[0];
mutex_lock(&tg->lock);
long diff = seq - up->p.seq;
if (diff > 100 || diff < -100) {
ilog(LOG_INFO | LOG_FLAG_LIMIT, "Ignoring UDPTL packet with wildly off seq (%u <> %u)",
(unsigned int) seq, (unsigned int) up->p.seq);
err = NULL;
goto err;
}
// XXX possible short path here without going through the sequencer
int ret = packet_sequencer_insert(&tg->sequencer, &up->p);
if (ret < 0) {
// main seq is dupe - everything else must be dupe too
__udptl_packet_free(up);
goto out;
}
up = NULL;
if (!(fec & 0x80)) {
// packet redundancy
if (packet_sequencer_next_ok(&tg->sequencer))
goto seq_ok;
// process EC packets as well as something's wrong
ssize_t num_packets = __get_udptl_len(&s);
err = "Invalid number of EC packets";
if (num_packets < 0 || num_packets > 100)
goto err;
for (int i = 0; i < num_packets; i++) {
if (__get_udptl(&piece, &s)) {
ilog(LOG_WARN | LOG_FLAG_LIMIT,
"Invalid UDPTL error correction packet at index %i",
i);
break;
}
// ignore zero-length packets
if (!piece.len)
continue;
ilog(LOG_DEBUG, "Received secondary IFP packet, len %zu, seq %i", piece.len,
seq - 1 - i);
up = __make_udptl_packet(&piece, seq - 1 - i);
packet_sequencer_insert(&tg->sequencer, &up->p);
up = NULL;
// can we stop here?
if (packet_sequencer_next_ok(&tg->sequencer))
break;
}
}
else {
// FEC
// start by saving the new packet
__fec_save(tg, &primary, seq);
if (packet_sequencer_next_ok(&tg->sequencer))
goto seq_ok;
// process all FEC packets
err = "Invalid number of FEC packets";
if (str_shift_ret(&s, 2, &piece))
goto err;
if (piece.s[0] != 0x01)
goto err;
unsigned int span = piece.s[1];
if (span <= 0 || span >= 0x80)
goto err;
ssize_t entries = __get_udptl_len(&s);
if (entries < 0 || entries > 100)
goto err;
// first seq we can possibly recover
uint16_t seq_start = seq - span * entries;
while (entries) {
// get our entry
if (__get_udptl(&piece, &s)) {
ilog(LOG_WARN | LOG_FLAG_LIMIT,
"Invalid UDPTL error correction packet at index %i",
seq_start);
break;
}
// check each of the entries covered by `span`
for (int i = 0; i < span; i++) {
uint16_t seq_fec = seq_start + i * span;
// skip if we already know this packet
if (g_hash_table_lookup(tg->udptl_fec, GUINT_TO_POINTER(seq_fec)))
continue;
// can we recover it? we need all other packets from the series
GString *rec_s = g_string_new("");
int complete = 1;
for (int j = 0; j < span; j++) {
uint16_t seq_rec = seq_start + i * span;
if (seq_rec == seq_fec)
continue;
struct udptl_packet *recp =
g_hash_table_lookup(tg->udptl_fec, GUINT_TO_POINTER(seq_rec));
if (!recp) {
ilog(LOG_WARN | LOG_FLAG_LIMIT, "Unable to recover UDPTL FEC "
"packet with seq %i due to missing seq %i",
seq_fec, seq_rec);
complete = 0;
break;
}
// XOR in packet
for (size_t k = 0; k < recp->s->len; k++)
rec_s->str[k] ^= recp->s->s[k];
}
if (complete) {
ilog(LOG_WARN | LOG_FLAG_LIMIT, "Recovered UDPTL "
"packet with seq %i from FEC",
seq_fec);
str rec_str = STR_GS(rec_s);
__fec_save(tg, &rec_str, seq_fec);
up = __make_udptl_packet(&rec_str, seq_fec);
packet_sequencer_insert(&tg->sequencer, &up->p);
up = NULL;
}
g_string_free(rec_s, TRUE);
// no point in continuing further: one packet was missing, which means
// that no other packet in this span can be recovered
break;
}
// proceed to next entry
entries--;
seq_start++;
}
}
seq_ok:;
t38_core_state_t *t38 = t38_gateway_get_t38_core_state(tg->gw);
// process any packets that we can
while (1) {
up = packet_sequencer_next_packet(&tg->sequencer);
if (!up)
break;
ilog(LOG_DEBUG, "Processing %zu IFP bytes, seq %i", up->s->len, up->p.seq);
t38_core_rx_ifp_packet(t38, (uint8_t *) up->s->s, up->s->len, up->p.seq);
__udptl_packet_free(up);
}
out:
mutex_unlock(&tg->lock);
return 0;
err:
mutex_unlock(&tg->lock);
err_nolock:
if (err)
ilog(LOG_ERR | LOG_FLAG_LIMIT, "Failed to process UDPTL/T.38/IFP packet: %s", err);
if (up)
__udptl_packet_free(up);
return -1;
}
void t38_gateway_stop(struct t38_gateway *tg) {
if (!tg)
return;
if (tg->pcm_player)
media_player_stop(tg->pcm_player);
if (tg->t38_media)
tg->t38_media->sdp_attr_print = sdp_insert_media_attributes;
}
void t38_init(void) {
my_span_mh(NULL);
}
#endif