blob: 9ab6ce54ea8bb174a675bddcca961cc56cfb1a17 [file] [log] [blame]
Benny Prijono1f47f3f2009-07-29 12:28:31 +00001/* $Id$ */
2/*
3 * Copyright (C) 2008-2009 Teluu Inc. (http://www.teluu.com)
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 */
19
20/* jbsim:
21
22 This program emulates various system and network impairment
23 conditions as well as application parameters and apply it to
24 an input WAV file. The output is another WAV file as well as
25 a detailed log file (in CSV format) for troubleshooting.
26 */
27
28
29/* Include PJMEDIA and PJLIB */
30#include <pjmedia.h>
31#include <pjmedia-codec.h>
32#include <pjlib.h>
33#include <pjlib-util.h>
34
35#define THIS_FILE "jbsim.c"
36
37/* Timer resolution in ms (must be NONZERO!) */
38#define WALL_CLOCK_TICK 1
39
40/* Defaults settings */
41#define CODEC "PCMU"
Benny Prijono7e8e0c12009-08-05 17:10:35 +000042#define LOG_FILE "jbsim.csv"
Benny Prijono1f47f3f2009-07-29 12:28:31 +000043#define WAV_REF "../../tests/pjsua/wavs/input.8.wav"
44#define WAV_OUT "jbsim.wav"
Benny Prijono7e8e0c12009-08-05 17:10:35 +000045#define DURATION 60
Benny Prijono1f47f3f2009-07-29 12:28:31 +000046#define DTX PJ_TRUE
47#define PLC PJ_TRUE
48#define MIN_LOST_BURST 0
49#define MAX_LOST_BURST 20
50#define LOSS_CORR 0
51#define LOSS_EXTRA 2
Benny Prijono7e8e0c12009-08-05 17:10:35 +000052#define SILENT 1
Benny Prijono1f47f3f2009-07-29 12:28:31 +000053
54/*
55 Test setup:
56
57 Input WAV --> TX Stream --> Loop transport --> RX Stream --> Out WAV
58 */
59
60/* Stream settings */
61struct stream_cfg
62{
63 const char *name; /* for logging purposes */
64 pjmedia_dir dir; /* stream direction */
65 pj_str_t codec; /* codec name */
66 unsigned ptime; /* zero for default */
67 pj_bool_t dtx; /* DTX enabled? */
68 pj_bool_t plc; /* PLC enabled? */
69};
70
71/* Stream instance. We will instantiate two streams, TX and RX */
72struct stream
73{
74 pj_pool_t *pool;
75 pjmedia_stream *strm;
76 pjmedia_port *port;
77
78 /*
79 * Running states:
80 */
81 union {
82 /* TX stream state */
83 struct {
84 pj_time_val next_schedule; /* Time to send next packet */
85 unsigned total_tx; /* # of TX packets so far */
86 int total_lost; /* # of dropped pkts so far */
87 unsigned cur_lost_burst; /* current # of lost bursts */
88 unsigned drop_prob; /* drop probability value */
89
90 } tx;
91
92 /* RX stream state */
93 struct {
94 pj_time_val next_schedule; /* Time to fetch next pkt */
95 } rx;
96 } state;
97};
98
99/*
100 * Logging
101 */
102
103/* Events names */
104#define EVENT_LOG ""
105#define EVENT_TX "TX/PUT"
106#define EVENT_TX_DROP "*** LOSS ***"
107#define EVENT_GET_PRE "GET (pre)"
108#define EVENT_GET_POST "GET (post)"
109
110
111/* Logging entry */
112struct log_entry
113{
114 pj_time_val wall_clock; /* Wall clock time */
115 const char *event; /* Event name */
116 pjmedia_jb_state *jb_state; /* JB state, optional */
117 pjmedia_rtcp_stat *stat; /* Stream stat, optional */
118 const char *log; /* Log message, optional */
119};
120
121/* Test settings, taken from command line */
122struct test_cfg
123{
124 /* General options */
Benny Prijono7e8e0c12009-08-05 17:10:35 +0000125 pj_bool_t silent; /* Write little to stdout */
Benny Prijono1f47f3f2009-07-29 12:28:31 +0000126 const char *log_file; /* The output log file */
127
128 /* Test settings */
129 pj_str_t codec; /* Codec to be used */
130 unsigned duration_msec; /* Test duration */
131
132 /* Transmitter setting */
133 const char *tx_wav_in; /* Input/reference WAV */
134 unsigned tx_ptime; /* TX stream ptime */
135 unsigned tx_min_jitter; /* Minimum jitter in ms */
136 unsigned tx_max_jitter; /* Max jitter in ms */
137 unsigned tx_dtx; /* DTX enabled? */
138 unsigned tx_pct_avg_lost; /* Average loss in percent */
139 unsigned tx_min_lost_burst; /* Min lost burst in #pkt */
140 unsigned tx_max_lost_burst; /* Max lost burst in #pkt */
141 unsigned tx_pct_loss_corr; /* Loss correlation in pct */
142
143 /* Receiver setting */
144 const char *rx_wav_out; /* Output WAV file */
145 unsigned rx_ptime; /* RX stream ptime */
146 unsigned rx_snd_burst; /* RX sound burst */
147 pj_bool_t rx_plc; /* RX PLC enabled? */
148 int rx_jb_init; /* if > 0 will enable prefetch (ms) */
149 int rx_jb_min_pre; /* JB minimum prefetch (ms) */
150 int rx_jb_max_pre; /* JB maximum prefetch (ms) */
151 int rx_jb_max; /* JB maximum size (ms) */
152};
153
154/*
155 * Global var
156 */
157struct global_app
158{
159 pj_caching_pool cp;
160 pj_pool_t *pool;
161 pj_int16_t *framebuf;
162 pjmedia_endpt *endpt;
163 pjmedia_transport *loop;
164
165 pj_oshandle_t log_fd;
166
167 struct test_cfg cfg;
168
169 struct stream *tx;
170 pjmedia_port *tx_wav;
171
172 struct stream *rx;
173 pjmedia_port *rx_wav;
174
175 pj_time_val wall_clock;
176};
177
178static struct global_app g_app;
179
180
181#ifndef MAX
182# define MAX(a,b) (a<b ? b : a)
183#endif
184
185#ifndef MIN
186# define MIN(a,b) (a<b ? a : b)
187#endif
188
189/*****************************************************************************
190 * Logging
191 */
Benny Prijono7e8e0c12009-08-05 17:10:35 +0000192static void write_log(struct log_entry *entry, pj_bool_t to_stdout)
Benny Prijono1f47f3f2009-07-29 12:28:31 +0000193{
194 /* Format (CSV): */
195 const char *format = "TIME;EVENT;#RX packets;#packets lost;#JB prefetch;#JB size;#JBDISCARD;#JBEMPTY;Log Message";
196 static char log[2000];
197 enum { D = 20 };
198 char s_jbprefetch[D],
199 s_jbsize[D],
200 s_rxpkt[D],
201 s_losspkt[D],
202 s_jbdiscard[D],
203 s_jbempty[D];
204 static pj_bool_t header_written;
205
206 if (!header_written) {
207 pj_ansi_snprintf(log, sizeof(log),
208 "%s\n", format);
209 if (g_app.log_fd != NULL) {
210 pj_ssize_t size = strlen(log);
211 pj_file_write(g_app.log_fd, log, &size);
212 }
Benny Prijono7e8e0c12009-08-05 17:10:35 +0000213 if (to_stdout && !g_app.cfg.silent)
214 printf("%s", log);
Benny Prijono1f47f3f2009-07-29 12:28:31 +0000215 header_written = PJ_TRUE;
216 }
217
218 if (entry->jb_state) {
219 sprintf(s_jbprefetch, "%d", entry->jb_state->prefetch);
220 sprintf(s_jbsize, "%d", entry->jb_state->size);
221 sprintf(s_jbdiscard, "%d", entry->jb_state->discard);
222 sprintf(s_jbempty, "%d", entry->jb_state->empty);
223 } else {
224 strcpy(s_jbprefetch, "");
225 strcpy(s_jbsize, "");
226 strcpy(s_jbdiscard, "");
227 strcpy(s_jbempty, "");
228 }
229
230 if (entry->stat) {
231 sprintf(s_rxpkt, "%d", entry->stat->rx.pkt);
232 sprintf(s_losspkt, "%d", entry->stat->rx.loss);
233 } else {
234 strcpy(s_rxpkt, "");
235 strcpy(s_losspkt, "");
236 }
237
238 if (entry->log == NULL)
239 entry->log = "";
240
241 pj_ansi_snprintf(log, sizeof(log),
242 "'%d.%03d;" /* time */
243 "%s;" /* event */
244 "%s;" /* rxpkt */
245 "%s;" /* jb prefetch */
246 "%s;" /* jbsize */
247 "%s;" /* losspkt */
248 "%s;" /* jbdiscard */
249 "%s;" /* jbempty */
250 "%s\n" /* logmsg */,
251
252 (int)entry->wall_clock.sec, (int)entry->wall_clock.msec, /* time */
253 entry->event,
254 s_rxpkt,
255 s_losspkt,
256 s_jbprefetch,
257 s_jbsize,
258 s_jbdiscard,
259 s_jbempty,
260 entry->log
261 );
262 if (g_app.log_fd != NULL) {
263 pj_ssize_t size = strlen(log);
264 pj_file_write(g_app.log_fd, log, &size);
265 }
Benny Prijono7e8e0c12009-08-05 17:10:35 +0000266
267 if (to_stdout && !g_app.cfg.silent)
268 printf("%s", log);
Benny Prijono1f47f3f2009-07-29 12:28:31 +0000269}
270
271static void log_cb(int level, const char *data, int len)
272{
273 struct log_entry entry;
274
275 /* Write to stdout */
276 pj_log_write(level, data, len);
277 puts("");
278
279 /* Also add to CSV file */
280 pj_bzero(&entry, sizeof(entry));
281 entry.event = EVENT_LOG;
282 entry.log = data;
283 entry.wall_clock = g_app.wall_clock;
Benny Prijono7e8e0c12009-08-05 17:10:35 +0000284 write_log(&entry, PJ_FALSE);
Benny Prijono1f47f3f2009-07-29 12:28:31 +0000285}
286
287static void jbsim_perror(const char *title, pj_status_t status)
288{
289 char errmsg[PJ_ERR_MSG_SIZE];
290
291 pj_strerror(status, errmsg, sizeof(errmsg));
292 PJ_LOG(1,(THIS_FILE, "%s: %s", title, errmsg));
293}
294
295/*****************************************************************************
296 * stream
297 */
298
299static void stream_destroy(struct stream *stream)
300{
301 if (stream->strm)
302 pjmedia_stream_destroy(stream->strm);
303 if (stream->pool)
304 pj_pool_release(stream->pool);
305}
306
307static pj_status_t stream_init(const struct stream_cfg *cfg, struct stream **p_stream)
308{
309 pj_pool_t *pool = NULL;
310 struct stream *stream = NULL;
311 pjmedia_codec_mgr *cm;
312 unsigned count;
Benny Prijono140beae2009-10-11 05:06:43 +0000313 const pjmedia_codec_info *ci;
Benny Prijono1f47f3f2009-07-29 12:28:31 +0000314 pjmedia_stream_info si;
315 pj_status_t status;
316
317 /* Create instance */
318 pool = pj_pool_create(&g_app.cp.factory, cfg->name, 512, 512, NULL);
319 stream = PJ_POOL_ZALLOC_T(pool, struct stream);
320 stream->pool = pool;
321
322 /* Create stream info */
323 pj_bzero(&si, sizeof(si));
324 si.type = PJMEDIA_TYPE_AUDIO;
325 si.proto = PJMEDIA_TP_PROTO_RTP_AVP;
326 si.dir = cfg->dir;
327 pj_sockaddr_in_init(&si.rem_addr.ipv4, NULL, 4000); /* dummy */
328 pj_sockaddr_in_init(&si.rem_rtcp.ipv4, NULL, 4001); /* dummy */
329
330 /* Apply JB settings if this is RX direction */
331 if (cfg->dir == PJMEDIA_DIR_DECODING) {
332 si.jb_init = g_app.cfg.rx_jb_init;
333 si.jb_min_pre = g_app.cfg.rx_jb_min_pre;
334 si.jb_max_pre = g_app.cfg.rx_jb_max_pre;
335 si.jb_max = g_app.cfg.rx_jb_max;
336 }
337
338 /* Get the codec info and param */
339 cm = pjmedia_endpt_get_codec_mgr(g_app.endpt);
340 count = 1;
341 status = pjmedia_codec_mgr_find_codecs_by_id(cm, &cfg->codec, &count, &ci, NULL);
342 if (status != PJ_SUCCESS) {
343 jbsim_perror("Unable to find codec", status);
344 goto on_error;
345 }
346
347 pj_memcpy(&si.fmt, ci, sizeof(*ci));
348
349 si.param = PJ_POOL_ALLOC_T(pool, struct pjmedia_codec_param);
350 status = pjmedia_codec_mgr_get_default_param(cm, &si.fmt, si.param);
351 if (status != PJ_SUCCESS) {
352 jbsim_perror("Unable to get codec defaults", status);
353 goto on_error;
354 }
355
356 si.tx_pt = si.fmt.pt;
357
358 /* Apply ptime setting */
359 if (cfg->ptime) {
360 si.param->setting.frm_per_pkt = (pj_uint8_t)
361 ((cfg->ptime + si.param->info.frm_ptime - 1) /
362 si.param->info.frm_ptime);
363 }
364 /* Apply DTX setting */
365 si.param->setting.vad = cfg->dtx;
366
367 /* Apply PLC setting */
368 si.param->setting.plc = cfg->plc;
369
370 /* Create stream */
371 status = pjmedia_stream_create(g_app.endpt, pool, &si, g_app.loop, NULL, &stream->strm);
372 if (status != PJ_SUCCESS) {
373 jbsim_perror("Error creating stream", status);
374 goto on_error;
375 }
376
377 status = pjmedia_stream_get_port(stream->strm, &stream->port);
378 if (status != PJ_SUCCESS) {
379 jbsim_perror("Error retrieving stream", status);
380 goto on_error;
381 }
382
383 /* Start stream */
384 status = pjmedia_stream_start(stream->strm);
385 if (status != PJ_SUCCESS) {
386 jbsim_perror("Error starting stream", status);
387 goto on_error;
388 }
389
390 /* Done */
391 *p_stream = stream;
392 return PJ_SUCCESS;
393
394on_error:
395 if (stream) {
396 stream_destroy(stream);
397 } else {
398 if (pool)
399 pj_pool_release(pool);
400 }
401 return status;
402}
403
404
405/*****************************************************************************
406 * The test session
407 */
408static void test_destroy(void)
409{
410 if (g_app.tx)
411 stream_destroy(g_app.tx);
412 if (g_app.tx_wav)
413 pjmedia_port_destroy(g_app.tx_wav);
414 if (g_app.rx)
415 stream_destroy(g_app.rx);
416 if (g_app.rx_wav)
417 pjmedia_port_destroy(g_app.rx_wav);
418 if (g_app.loop)
419 pjmedia_transport_close(g_app.loop);
420 if (g_app.endpt)
421 pjmedia_endpt_destroy( g_app.endpt );
422 if (g_app.log_fd) {
423 pj_log_set_log_func(&pj_log_write);
424 pj_log_set_decor(pj_log_get_decor() | PJ_LOG_HAS_NEWLINE);
425 pj_file_close(g_app.log_fd);
426 g_app.log_fd = NULL;
427 }
428 if (g_app.pool)
429 pj_pool_release(g_app.pool);
430 pj_caching_pool_destroy( &g_app.cp );
431 pj_shutdown();
432}
433
434
435static pj_status_t test_init(void)
436{
437 struct stream_cfg strm_cfg;
438 pj_status_t status;
439
440 /* Must init PJLIB first: */
441 status = pj_init();
442 PJ_ASSERT_RETURN(status == PJ_SUCCESS, status);
443
444 /* Must create a pool factory before we can allocate any memory. */
445 pj_caching_pool_init(&g_app.cp, &pj_pool_factory_default_policy, 0);
446
447 /* Pool */
448 g_app.pool = pj_pool_create(&g_app.cp.factory, "g_app", 512, 512, NULL);
449
450 /* Log file */
451 if (g_app.cfg.log_file) {
452 status = pj_file_open(g_app.pool, g_app.cfg.log_file,
453 PJ_O_WRONLY,
454 &g_app.log_fd);
455 if (status != PJ_SUCCESS) {
456 jbsim_perror("Error writing output file", status);
457 goto on_error;
458 }
459
460 pj_log_set_decor(PJ_LOG_HAS_SENDER | PJ_LOG_HAS_COLOR | PJ_LOG_HAS_LEVEL_TEXT);
461 pj_log_set_log_func(&log_cb);
462 }
463
464 /*
465 * Initialize media endpoint.
466 * This will implicitly initialize PJMEDIA too.
467 */
468 status = pjmedia_endpt_create(&g_app.cp.factory, NULL, 0, &g_app.endpt);
469 if (status != PJ_SUCCESS) {
470 jbsim_perror("Error creating media endpoint", status);
471 goto on_error;
472 }
473
474 /* Register codecs */
475#if defined(PJMEDIA_HAS_GSM_CODEC) && PJMEDIA_HAS_GSM_CODEC != 0
476 pjmedia_codec_gsm_init(g_app.endpt);
477#endif
478#if defined(PJMEDIA_HAS_G711_CODEC) && PJMEDIA_HAS_G711_CODEC!=0
479 pjmedia_codec_g711_init(g_app.endpt);
480#endif
481#if defined(PJMEDIA_HAS_SPEEX_CODEC) && PJMEDIA_HAS_SPEEX_CODEC!=0
482 pjmedia_codec_speex_init(g_app.endpt, 0, PJMEDIA_CODEC_SPEEX_DEFAULT_QUALITY,
483 PJMEDIA_CODEC_SPEEX_DEFAULT_COMPLEXITY);
484#endif
485#if defined(PJMEDIA_HAS_G722_CODEC) && (PJMEDIA_HAS_G722_CODEC != 0)
486 pjmedia_codec_g722_init(g_app.endpt);
487#endif
488#if defined(PJMEDIA_HAS_ILBC_CODEC) && PJMEDIA_HAS_ILBC_CODEC != 0
Benny Prijono7e8e0c12009-08-05 17:10:35 +0000489 /* Init ILBC with mode=20 to make the losts occur at the same
490 * places as other codecs.
491 */
492 pjmedia_codec_ilbc_init(g_app.endpt, 20);
Benny Prijono1f47f3f2009-07-29 12:28:31 +0000493#endif
494#if defined(PJMEDIA_HAS_INTEL_IPP) && PJMEDIA_HAS_INTEL_IPP != 0
495 pjmedia_codec_ipp_init(g_app.endpt);
496#endif
497#if defined(PJMEDIA_HAS_L16_CODEC) && PJMEDIA_HAS_L16_CODEC != 0
498 pjmedia_codec_l16_init(g_app.endpt, 0);
499#endif
500
501 /* Create the loop transport */
502 status = pjmedia_transport_loop_create(g_app.endpt, &g_app.loop);
503 if (status != PJ_SUCCESS) {
504 jbsim_perror("Error creating loop transport", status);
505 goto on_error;
506 }
507
508 /* Create transmitter stream */
509 pj_bzero(&strm_cfg, sizeof(strm_cfg));
510 strm_cfg.name = "tx";
511 strm_cfg.dir = PJMEDIA_DIR_ENCODING;
512 strm_cfg.codec = g_app.cfg.codec;
513 strm_cfg.ptime = g_app.cfg.tx_ptime;
514 strm_cfg.dtx = g_app.cfg.tx_dtx;
515 strm_cfg.plc = PJ_TRUE;
516 status = stream_init(&strm_cfg, &g_app.tx);
517 if (status != PJ_SUCCESS)
518 goto on_error;
519
520 /* Create transmitter WAV */
521 status = pjmedia_wav_player_port_create(g_app.pool,
522 g_app.cfg.tx_wav_in,
523 g_app.cfg.tx_ptime,
524 0,
525 0,
526 &g_app.tx_wav);
527 if (status != PJ_SUCCESS) {
528 jbsim_perror("Error reading input WAV file", status);
529 goto on_error;
530 }
531
532 /* Make sure stream and WAV parameters match */
Benny Prijonoc45d9512010-12-10 11:04:30 +0000533 if (PJMEDIA_PIA_SRATE(&g_app.tx_wav->info) != PJMEDIA_PIA_SRATE(&g_app.tx->port->info) ||
534 PJMEDIA_PIA_CCNT(&g_app.tx_wav->info) != PJMEDIA_PIA_CCNT(&g_app.tx->port->info))
Benny Prijono1f47f3f2009-07-29 12:28:31 +0000535 {
536 jbsim_perror("Error: Input WAV file has different clock rate "
537 "or number of channels than the codec", PJ_SUCCESS);
538 goto on_error;
539 }
540
541
542 /* Create receiver */
543 pj_bzero(&strm_cfg, sizeof(strm_cfg));
544 strm_cfg.name = "rx";
545 strm_cfg.dir = PJMEDIA_DIR_DECODING;
546 strm_cfg.codec = g_app.cfg.codec;
547 strm_cfg.ptime = g_app.cfg.rx_ptime;
548 strm_cfg.dtx = PJ_TRUE;
549 strm_cfg.plc = g_app.cfg.rx_plc;
550 status = stream_init(&strm_cfg, &g_app.rx);
551 if (status != PJ_SUCCESS)
552 goto on_error;
553
554 /* Create receiver WAV */
555 status = pjmedia_wav_writer_port_create(g_app.pool,
556 g_app.cfg.rx_wav_out,
Benny Prijonoc45d9512010-12-10 11:04:30 +0000557 PJMEDIA_PIA_SRATE(&g_app.rx->port->info),
558 PJMEDIA_PIA_CCNT(&g_app.rx->port->info),
559 PJMEDIA_PIA_SPF(&g_app.rx->port->info),
560 PJMEDIA_PIA_BITS(&g_app.rx->port->info),
Benny Prijono1f47f3f2009-07-29 12:28:31 +0000561 0,
562 0,
563 &g_app.rx_wav);
564 if (status != PJ_SUCCESS) {
565 jbsim_perror("Error creating output WAV file", status);
566 goto on_error;
567 }
568
569
570 /* Frame buffer */
571 g_app.framebuf = (pj_int16_t*)
572 pj_pool_alloc(g_app.pool,
Benny Prijonoc45d9512010-12-10 11:04:30 +0000573 MAX(PJMEDIA_PIA_SPF(&g_app.rx->port->info),
574 PJMEDIA_PIA_SPF(&g_app.tx->port->info)) * sizeof(pj_int16_t));
Benny Prijono1f47f3f2009-07-29 12:28:31 +0000575
576
577 /* Set the receiver in the loop transport */
578 pjmedia_transport_loop_disable_rx(g_app.loop, g_app.tx->strm, PJ_TRUE);
579
580 /* Done */
581 return PJ_SUCCESS;
582
583on_error:
584 test_destroy();
585 return status;
586}
587
588static void run_one_frame(pjmedia_port *src, pjmedia_port *dst,
589 pj_bool_t *has_frame)
590{
591 pjmedia_frame frame;
592 pj_status_t status;
593
594 pj_bzero(&frame, sizeof(frame));
595 frame.type = PJMEDIA_FRAME_TYPE_AUDIO;
596 frame.buf = g_app.framebuf;
Benny Prijonoc45d9512010-12-10 11:04:30 +0000597 frame.size = PJMEDIA_PIA_SPF(&dst->info) * 2;
Benny Prijono1f47f3f2009-07-29 12:28:31 +0000598
599 status = pjmedia_port_get_frame(src, &frame);
600 pj_assert(status == PJ_SUCCESS);
601
602 if (status!= PJ_SUCCESS || frame.type != PJMEDIA_FRAME_TYPE_AUDIO) {
603 frame.buf = g_app.framebuf;
Benny Prijonoc45d9512010-12-10 11:04:30 +0000604 pjmedia_zero_samples(g_app.framebuf, PJMEDIA_PIA_SPF(&src->info));
605 frame.size = PJMEDIA_PIA_SPF(&src->info) * 2;
Benny Prijono1f47f3f2009-07-29 12:28:31 +0000606 if (has_frame)
607 *has_frame = PJ_FALSE;
608 } else {
609 if (has_frame)
610 *has_frame = PJ_TRUE;
611 }
612
613
614 status = pjmedia_port_put_frame(dst, &frame);
615 pj_assert(status == PJ_SUCCESS);
616}
617
618
619/* This is the transmission "tick".
620 * This function is called periodically every "tick" milliseconds, and
621 * it will determine whether to transmit packet(s) (or to drop it).
622 */
623static void tx_tick(const pj_time_val *t)
624{
625 struct stream *strm = g_app.tx;
626 static char log_msg[120];
627 pjmedia_port *port = g_app.tx->port;
628 long pkt_interval;
629
630 /* packet interval, without jitter */
Benny Prijonoc45d9512010-12-10 11:04:30 +0000631 pkt_interval = PJMEDIA_PIA_SPF(&port->info) * 1000 /
632 PJMEDIA_PIA_SRATE(&port->info);
Benny Prijono1f47f3f2009-07-29 12:28:31 +0000633
634 while (PJ_TIME_VAL_GTE(*t, strm->state.tx.next_schedule)) {
635 struct log_entry entry;
636 pj_bool_t drop_this_pkt = PJ_FALSE;
637 int jitter;
638
639 /* Init log entry */
640 pj_bzero(&entry, sizeof(entry));
641 entry.wall_clock = *t;
642
643 /*
644 * Determine whether to drop this packet
645 */
646 if (strm->state.tx.cur_lost_burst) {
647 /* We are currently dropping packet */
648
649 /* Make it comply to minimum lost burst */
650 if (strm->state.tx.cur_lost_burst < g_app.cfg.tx_min_lost_burst) {
651 drop_this_pkt = PJ_TRUE;
652 }
653
654 /* Correlate the next packet loss */
655 if (!drop_this_pkt &&
656 strm->state.tx.cur_lost_burst < g_app.cfg.tx_max_lost_burst &&
657 MAX(strm->state.tx.total_lost-LOSS_EXTRA,0) * 100 / MAX(strm->state.tx.total_tx,1) < g_app.cfg.tx_pct_avg_lost
658 )
659 {
660 strm->state.tx.drop_prob = ((g_app.cfg.tx_pct_loss_corr * strm->state.tx.drop_prob) +
661 ((100-g_app.cfg.tx_pct_loss_corr) * (pj_rand()%100))
662 ) / 100;
663 if (strm->state.tx.drop_prob >= 100)
664 strm->state.tx.drop_prob = 99;
665
666 if (strm->state.tx.drop_prob >= 100 - g_app.cfg.tx_pct_avg_lost)
667 drop_this_pkt = PJ_TRUE;
668 }
669 }
670
671 /* If we're not dropping packet then use randomly distributed loss */
672 if (!drop_this_pkt &&
673 MAX(strm->state.tx.total_lost-LOSS_EXTRA,0) * 100 / MAX(strm->state.tx.total_tx,1) < g_app.cfg.tx_pct_avg_lost)
674 {
675 strm->state.tx.drop_prob = pj_rand() % 100;
676
677 if (strm->state.tx.drop_prob >= 100 - g_app.cfg.tx_pct_avg_lost)
678 drop_this_pkt = PJ_TRUE;
679 }
680
681 if (drop_this_pkt) {
682 /* Drop the frame */
683 pjmedia_transport_simulate_lost(g_app.loop, PJMEDIA_DIR_ENCODING, 100);
684 run_one_frame(g_app.tx_wav, g_app.tx->port, NULL);
685 pjmedia_transport_simulate_lost(g_app.loop, PJMEDIA_DIR_ENCODING, 0);
686
687 entry.event = EVENT_TX_DROP;
688 entry.log = "** This packet was lost **";
689
690 ++strm->state.tx.total_lost;
691 ++strm->state.tx.cur_lost_burst;
692
693 } else {
694 pjmedia_rtcp_stat stat;
695 pjmedia_jb_state jstate;
696 unsigned last_discard;
697
698 pjmedia_stream_get_stat_jbuf(g_app.rx->strm, &jstate);
699 last_discard = jstate.discard;
700
701 run_one_frame(g_app.tx_wav, g_app.tx->port, NULL);
702
703 pjmedia_stream_get_stat(g_app.rx->strm, &stat);
704 pjmedia_stream_get_stat_jbuf(g_app.rx->strm, &jstate);
705
706 entry.event = EVENT_TX;
707 entry.jb_state = &jstate;
708 entry.stat = &stat;
709 entry.log = log_msg;
710
711 if (jstate.discard > last_discard)
712 strcat(log_msg, "** Note: packet was discarded by jitter buffer **");
713
714 strm->state.tx.cur_lost_burst = 0;
715 }
716
Benny Prijono7e8e0c12009-08-05 17:10:35 +0000717 write_log(&entry, PJ_TRUE);
Benny Prijono1f47f3f2009-07-29 12:28:31 +0000718
719 ++strm->state.tx.total_tx;
720
721 /* Calculate next schedule */
722 strm->state.tx.next_schedule.sec = 0;
723 strm->state.tx.next_schedule.msec = (strm->state.tx.total_tx + 1) * pkt_interval;
724
725 /* Apply jitter */
726 if (g_app.cfg.tx_max_jitter || g_app.cfg.tx_min_jitter) {
727
728 if (g_app.cfg.tx_max_jitter == g_app.cfg.tx_min_jitter) {
729 /* Fixed jitter */
730 switch (pj_rand() % 3) {
731 case 0:
732 jitter = 0 - g_app.cfg.tx_min_jitter;
733 break;
734 case 2:
735 jitter = g_app.cfg.tx_min_jitter;
736 break;
737 default:
738 jitter = 0;
739 break;
740 }
741 } else {
742 int jitter_range;
743 jitter_range = (g_app.cfg.tx_max_jitter-g_app.cfg.tx_min_jitter)*2;
744 jitter = pj_rand() % jitter_range;
745 if (jitter < jitter_range/2) {
746 jitter = 0 - g_app.cfg.tx_min_jitter - (jitter/2);
747 } else {
748 jitter = g_app.cfg.tx_min_jitter + (jitter/2);
749 }
750 }
751
752 } else {
753 jitter = 0;
754 }
755
756 pj_time_val_normalize(&strm->state.tx.next_schedule);
757
758 sprintf(log_msg, "** Packet #%u tick is at %d.%03d, %d ms jitter applied **",
759 strm->state.tx.total_tx+1,
760 (int)strm->state.tx.next_schedule.sec, (int)strm->state.tx.next_schedule.msec,
761 jitter);
762
763 strm->state.tx.next_schedule.msec += jitter;
764 pj_time_val_normalize(&strm->state.tx.next_schedule);
765
766 } /* while */
767}
768
769
770/* This is the RX "tick".
771 * This function is called periodically every "tick" milliseconds, and
772 * it will determine whether to call get_frame() from the RX stream.
773 */
774static void rx_tick(const pj_time_val *t)
775{
776 struct stream *strm = g_app.rx;
777 pjmedia_port *port = g_app.rx->port;
778 long pkt_interval;
779
Benny Prijonoc45d9512010-12-10 11:04:30 +0000780 pkt_interval = PJMEDIA_PIA_SPF(&port->info) * 1000 /
781 PJMEDIA_PIA_SRATE(&port->info) *
Benny Prijono1f47f3f2009-07-29 12:28:31 +0000782 g_app.cfg.rx_snd_burst;
783
784 if (PJ_TIME_VAL_GTE(*t, strm->state.rx.next_schedule)) {
785 unsigned i;
786 for (i=0; i<g_app.cfg.rx_snd_burst; ++i) {
787 struct log_entry entry;
788 pjmedia_rtcp_stat stat;
789 pjmedia_jb_state jstate;
790 pj_bool_t has_frame;
791 char msg[120];
792 unsigned last_empty;
793
794 pjmedia_stream_get_stat(g_app.rx->strm, &stat);
795 pjmedia_stream_get_stat_jbuf(g_app.rx->strm, &jstate);
796 last_empty = jstate.empty;
797
798 /* Pre GET event */
799 pj_bzero(&entry, sizeof(entry));
800 entry.event = EVENT_GET_PRE;
801 entry.wall_clock = *t;
802 entry.stat = &stat;
803 entry.jb_state = &jstate;
804
Benny Prijono7e8e0c12009-08-05 17:10:35 +0000805 write_log(&entry, PJ_TRUE);
Benny Prijono1f47f3f2009-07-29 12:28:31 +0000806
807 /* GET */
808 run_one_frame(g_app.rx->port, g_app.rx_wav, &has_frame);
809
810 /* Post GET event */
811 pjmedia_stream_get_stat(g_app.rx->strm, &stat);
812 pjmedia_stream_get_stat_jbuf(g_app.rx->strm, &jstate);
813
814 pj_bzero(&entry, sizeof(entry));
815 entry.event = EVENT_GET_POST;
816 entry.wall_clock = *t;
817 entry.stat = &stat;
818 entry.jb_state = &jstate;
819
820 msg[0] = '\0';
821 entry.log = msg;
822
823 if (jstate.empty > last_empty)
824 strcat(msg, "** JBUF was empty **");
825 if (!has_frame)
826 strcat(msg, "** NULL frame was returned **");
827
Benny Prijono7e8e0c12009-08-05 17:10:35 +0000828 write_log(&entry, PJ_TRUE);
Benny Prijono1f47f3f2009-07-29 12:28:31 +0000829
830 }
831
832
833 strm->state.rx.next_schedule.msec += pkt_interval;
834 pj_time_val_normalize(&strm->state.rx.next_schedule);
835 }
836
837}
838
839static void test_loop(long duration)
840{
841 g_app.wall_clock.sec = 0;
842 g_app.wall_clock.msec = 0;
843
844 while (PJ_TIME_VAL_MSEC(g_app.wall_clock) <= duration) {
845
846 /* Run TX tick */
847 tx_tick(&g_app.wall_clock);
848
849 /* Run RX tick */
850 rx_tick(&g_app.wall_clock);
851
852 /* Increment tick */
853 g_app.wall_clock.msec += WALL_CLOCK_TICK;
854 pj_time_val_normalize(&g_app.wall_clock);
855 }
856}
857
858
859/*****************************************************************************
860 * usage()
861 */
862enum {
863 OPT_CODEC = 'c',
864 OPT_INPUT = 'i',
865 OPT_OUTPUT = 'o',
866 OPT_DURATION = 'd',
867 OPT_LOG_FILE = 'l',
868 OPT_LOSS = 'x',
869 OPT_MIN_JITTER = 'j',
870 OPT_MAX_JITTER = 'J',
871 OPT_SND_BURST = 'b',
872 OPT_TX_PTIME = 't',
873 OPT_RX_PTIME = 'r',
874 OPT_NO_VAD = 'U',
875 OPT_NO_PLC = 'p',
876 OPT_JB_PREFETCH = 'P',
877 OPT_JB_MIN_PRE = 'm',
878 OPT_JB_MAX_PRE = 'M',
879 OPT_JB_MAX = 'X',
880 OPT_HELP = 'h',
881 OPT_MIN_LOST_BURST = 1,
882 OPT_MAX_LOST_BURST,
883 OPT_LOSS_CORR,
884};
885
886
887static void usage(void)
888{
889 printf("jbsim - System and network impairments simulator\n");
890 printf("Copyright (C) 2008-2009 Teluu Inc. (http://www.teluu.com)\n");
891 printf("\n");
892 printf("This program emulates various system and network impairment\n");
893 printf("conditions as well as application parameters and apply it to\n");
894 printf("an input WAV file. The output is another WAV file as well as\n");
895 printf("a detailed log file (in CSV format) for troubleshooting.\n");
896 printf("\n");
897 printf("Usage:\n");
898 printf(" jbsim [OPTIONS]\n");
899 printf("\n");
900 printf("General OPTIONS:\n");
901 printf(" --codec, -%c NAME Set the audio codec\n", OPT_CODEC);
902 printf(" Default: %s\n", CODEC);
903 printf(" --input, -%c FILE Set WAV reference file to FILE\n", OPT_INPUT);
904 printf(" Default: " WAV_REF "\n");
905 printf(" --output, -%c FILE Set WAV output file to FILE\n", OPT_OUTPUT);
906 printf(" Default: " WAV_OUT "\n");
907 printf(" --duration, -%c SEC Set test duration to SEC seconds\n", OPT_DURATION);
908 printf(" Default: %d\n", DURATION);
909 printf(" --log-file, -%c FILE Save simulation log file to FILE\n", OPT_LOG_FILE);
910 printf(" Note: FILE will be in CSV format with semicolon separator\n");
911 printf(" Default: %s\n", LOG_FILE);
912 printf(" --help, -h Display this screen\n");
913 printf("\n");
914 printf("Simulation OPTIONS:\n");
915 printf(" --loss, -%c PCT Set packet average loss to PCT percent\n", OPT_LOSS);
916 printf(" Default: 0\n");
917 printf(" --loss-corr PCT Set the loss correlation to PCT percent. Default: 0\n");
918 printf(" --min-lost-burst N Set minimum packet lost burst (default:%d)\n", MIN_LOST_BURST);
919 printf(" --max-lost-burst N Set maximum packet lost burst (default:%d)\n", MAX_LOST_BURST);
920 printf(" --min-jitter, -%c MSEC Set minimum network jitter to MSEC\n", OPT_MIN_JITTER);
921 printf(" Default: 0\n");
922 printf(" --max-jitter, -%c MSEC Set maximum network jitter to MSEC\n", OPT_MAX_JITTER);
923 printf(" Default: 0\n");
924 printf(" --snd-burst, -%c VAL Set RX sound burst value to VAL frames.\n", OPT_SND_BURST);
925 printf(" Default: 1\n");
926 printf(" --tx-ptime, -%c MSEC Set transmitter ptime to MSEC\n", OPT_TX_PTIME);
927 printf(" Default: 0 (not set, use default)\n");
928 printf(" --rx-ptime, -%c MSEC Set receiver ptime to MSEC\n", OPT_RX_PTIME);
929 printf(" Default: 0 (not set, use default)\n");
930 printf(" --no-vad, -%c Disable VAD/DTX in transmitter\n", OPT_NO_VAD);
931 printf(" --no-plc, -%c Disable PLC in receiver\n", OPT_NO_PLC);
932 printf(" --jb-prefetch, -%c Enable prefetch bufferring in jitter buffer\n", OPT_JB_PREFETCH);
933 printf(" --jb-min-pre, -%c MSEC Jitter buffer minimum prefetch delay in msec\n", OPT_JB_MIN_PRE);
934 printf(" --jb-max-pre, -%c MSEC Jitter buffer maximum prefetch delay in msec\n", OPT_JB_MAX_PRE);
935 printf(" --jb-max, -%c MSEC Set maximum delay that can be accomodated by the\n", OPT_JB_MAX);
936 printf(" jitter buffer msec.\n");
937}
938
939
940static int init_options(int argc, char *argv[])
941{
942 struct pj_getopt_option long_options[] = {
943 { "codec", 1, 0, OPT_CODEC },
944 { "input", 1, 0, OPT_INPUT },
945 { "output", 1, 0, OPT_OUTPUT },
946 { "duration", 1, 0, OPT_DURATION },
947 { "log-file", 1, 0, OPT_LOG_FILE},
948 { "loss", 1, 0, OPT_LOSS },
949 { "min-lost-burst", 1, 0, OPT_MIN_LOST_BURST},
950 { "max-lost-burst", 1, 0, OPT_MAX_LOST_BURST},
951 { "loss-corr", 1, 0, OPT_LOSS_CORR},
952 { "min-jitter", 1, 0, OPT_MIN_JITTER },
953 { "max-jitter", 1, 0, OPT_MAX_JITTER },
954 { "snd-burst", 1, 0, OPT_SND_BURST },
955 { "tx-ptime", 1, 0, OPT_TX_PTIME },
956 { "rx-ptime", 1, 0, OPT_RX_PTIME },
957 { "no-vad", 0, 0, OPT_NO_VAD },
958 { "no-plc", 0, 0, OPT_NO_PLC },
959 { "jb-prefetch", 0, 0, OPT_JB_PREFETCH },
960 { "jb-min-pre", 1, 0, OPT_JB_MIN_PRE },
961 { "jb-max-pre", 1, 0, OPT_JB_MAX_PRE },
962 { "jb-max", 1, 0, OPT_JB_MAX },
963 { "help", 0, 0, OPT_HELP},
964 { NULL, 0, 0, 0 },
965 };
966 int c;
967 int option_index;
968 char format[128];
969
970 /* Init default config */
971 g_app.cfg.codec = pj_str(CODEC);
972 g_app.cfg.duration_msec = DURATION * 1000;
Benny Prijono7e8e0c12009-08-05 17:10:35 +0000973 g_app.cfg.silent = SILENT;
Benny Prijono1f47f3f2009-07-29 12:28:31 +0000974 g_app.cfg.log_file = LOG_FILE;
975 g_app.cfg.tx_wav_in = WAV_REF;
976 g_app.cfg.tx_ptime = 0;
977 g_app.cfg.tx_min_jitter = 0;
978 g_app.cfg.tx_max_jitter = 0;
979 g_app.cfg.tx_dtx = DTX;
980 g_app.cfg.tx_pct_avg_lost = 0;
981 g_app.cfg.tx_min_lost_burst = MIN_LOST_BURST;
982 g_app.cfg.tx_max_lost_burst = MAX_LOST_BURST;
983 g_app.cfg.tx_pct_loss_corr = LOSS_CORR;
984
985 g_app.cfg.rx_wav_out = WAV_OUT;
986 g_app.cfg.rx_ptime = 0;
987 g_app.cfg.rx_plc = PLC;
988 g_app.cfg.rx_snd_burst = 1;
989 g_app.cfg.rx_jb_init = -1;
990 g_app.cfg.rx_jb_min_pre = -1;
991 g_app.cfg.rx_jb_max_pre = -1;
992 g_app.cfg.rx_jb_max = -1;
993
994 /* Build format */
995 format[0] = '\0';
996 for (c=0; c<PJ_ARRAY_SIZE(long_options)-1; ++c) {
997 if (long_options[c].has_arg) {
998 char cmd[10];
999 pj_ansi_snprintf(cmd, sizeof(cmd), "%c:", long_options[c].val);
1000 pj_ansi_strcat(format, cmd);
1001 }
1002 }
1003 for (c=0; c<PJ_ARRAY_SIZE(long_options)-1; ++c) {
1004 if (long_options[c].has_arg == 0) {
1005 char cmd[10];
1006 pj_ansi_snprintf(cmd, sizeof(cmd), "%c", long_options[c].val);
1007 pj_ansi_strcat(format, cmd);
1008 }
1009 }
1010
1011 /* Parse options */
1012 pj_optind = 0;
1013 while((c=pj_getopt_long(argc,argv, format,
1014 long_options, &option_index))!=-1)
1015 {
1016 switch (c) {
1017 case OPT_CODEC:
1018 g_app.cfg.codec = pj_str(pj_optarg);
1019 break;
1020 case OPT_INPUT:
1021 g_app.cfg.tx_wav_in = pj_optarg;
1022 break;
1023 case OPT_OUTPUT:
1024 g_app.cfg.rx_wav_out = pj_optarg;
1025 break;
1026 case OPT_DURATION:
1027 g_app.cfg.duration_msec = atoi(pj_optarg) * 1000;
1028 break;
1029 case OPT_LOG_FILE:
1030 g_app.cfg.log_file = pj_optarg;
1031 break;
1032 case OPT_LOSS:
1033 g_app.cfg.tx_pct_avg_lost = atoi(pj_optarg);
1034 if (g_app.cfg.tx_pct_avg_lost > 100) {
1035 puts("Error: Invalid loss value?");
1036 return 1;
1037 }
1038 break;
1039 case OPT_MIN_LOST_BURST:
1040 g_app.cfg.tx_min_lost_burst = atoi(pj_optarg);
1041 break;
1042 case OPT_MAX_LOST_BURST:
1043 g_app.cfg.tx_max_lost_burst = atoi(pj_optarg);
1044 break;
1045 case OPT_LOSS_CORR:
1046 g_app.cfg.tx_pct_loss_corr = atoi(pj_optarg);
1047 if (g_app.cfg.tx_pct_avg_lost > 100) {
1048 puts("Error: Loss correlation is in percentage, value is not valid?");
1049 return 1;
1050 }
1051 break;
1052 case OPT_MIN_JITTER:
1053 g_app.cfg.tx_min_jitter = atoi(pj_optarg);
1054 break;
1055 case OPT_MAX_JITTER:
1056 g_app.cfg.tx_max_jitter = atoi(pj_optarg);
1057 break;
1058 case OPT_SND_BURST:
1059 g_app.cfg.rx_snd_burst = atoi(pj_optarg);
1060 break;
1061 case OPT_TX_PTIME:
1062 g_app.cfg.tx_ptime = atoi(pj_optarg);
1063 break;
1064 case OPT_RX_PTIME:
1065 g_app.cfg.rx_ptime = atoi(pj_optarg);
1066 break;
1067 case OPT_NO_VAD:
1068 g_app.cfg.tx_dtx = PJ_FALSE;
1069 break;
1070 case OPT_NO_PLC:
1071 g_app.cfg.rx_plc = PJ_FALSE;
1072 break;
1073 case OPT_JB_PREFETCH:
1074 g_app.cfg.rx_jb_init = 1;
1075 break;
1076 case OPT_JB_MIN_PRE:
1077 g_app.cfg.rx_jb_min_pre = atoi(pj_optarg);
1078 break;
1079 case OPT_JB_MAX_PRE:
1080 g_app.cfg.rx_jb_max_pre = atoi(pj_optarg);
1081 break;
1082 case OPT_JB_MAX:
1083 g_app.cfg.rx_jb_max = atoi(pj_optarg);
1084 break;
1085 case OPT_HELP:
1086 usage();
1087 return 1;
1088 default:
1089 usage();
1090 return 1;
1091 }
1092 }
1093
1094 /* Check for orphaned params */
1095 if (pj_optind < argc) {
1096 usage();
1097 return 1;
1098 }
1099
1100 /* Normalize options */
1101 if (g_app.cfg.rx_jb_init < g_app.cfg.rx_jb_min_pre)
1102 g_app.cfg.rx_jb_init = g_app.cfg.rx_jb_min_pre;
1103 else if (g_app.cfg.rx_jb_init > g_app.cfg.rx_jb_max_pre)
1104 g_app.cfg.rx_jb_init = g_app.cfg.rx_jb_max_pre;
1105
1106 if (g_app.cfg.tx_max_jitter < g_app.cfg.tx_min_jitter)
1107 g_app.cfg.tx_max_jitter = g_app.cfg.tx_min_jitter;
1108 return 0;
1109}
1110
1111/*****************************************************************************
1112 * main()
1113 */
1114int main(int argc, char *argv[])
1115{
1116 pj_status_t status;
1117
1118 if (init_options(argc, argv) != 0)
1119 return 1;
1120
1121
1122 /* Init */
1123 status = test_init();
1124 if (status != PJ_SUCCESS)
1125 return 1;
1126
1127 /* Print parameters */
1128 PJ_LOG(3,(THIS_FILE, "Starting simulation. Parameters: "));
1129 PJ_LOG(3,(THIS_FILE, " Codec=%.*s, tx_ptime=%d, rx_ptime=%d",
1130 (int)g_app.cfg.codec.slen,
1131 g_app.cfg.codec.ptr,
1132 g_app.cfg.tx_ptime,
1133 g_app.cfg.rx_ptime));
1134 PJ_LOG(3,(THIS_FILE, " Loss avg=%d%%, min_burst=%d, max_burst=%d",
1135 g_app.cfg.tx_pct_avg_lost,
1136 g_app.cfg.tx_min_lost_burst,
1137 g_app.cfg.tx_max_lost_burst));
1138 PJ_LOG(3,(THIS_FILE, " TX jitter min=%dms, max=%dms",
1139 g_app.cfg.tx_min_jitter,
1140 g_app.cfg.tx_max_jitter));
1141 PJ_LOG(3,(THIS_FILE, " RX jb init:%dms, min_pre=%dms, max_pre=%dms, max=%dms",
1142 g_app.cfg.rx_jb_init,
1143 g_app.cfg.rx_jb_min_pre,
1144 g_app.cfg.rx_jb_max_pre,
1145 g_app.cfg.rx_jb_max));
1146 PJ_LOG(3,(THIS_FILE, " RX sound burst:%d frames",
1147 g_app.cfg.rx_snd_burst));
1148 PJ_LOG(3,(THIS_FILE, " DTX=%d, PLC=%d",
1149 g_app.cfg.tx_dtx, g_app.cfg.rx_plc));
1150
1151 /* Run test loop */
1152 test_loop(g_app.cfg.duration_msec);
1153
1154 /* Print statistics */
1155 PJ_LOG(3,(THIS_FILE, "Simulation done"));
1156 PJ_LOG(3,(THIS_FILE, " TX packets=%u, dropped=%u/%5.1f%%",
1157 g_app.tx->state.tx.total_tx,
1158 g_app.tx->state.tx.total_lost,
1159 (float)(g_app.tx->state.tx.total_lost * 100.0 / g_app.tx->state.tx.total_tx)));
1160
1161 /* Done */
1162 test_destroy();
1163
1164 return 0;
1165}