| /* $Id: jbsim.c 3664 2011-07-19 03:42:28Z nanang $ */ |
| /* |
| * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 2 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| */ |
| |
| /* jbsim: |
| |
| This program emulates various system and network impairment |
| conditions as well as application parameters and apply it to |
| an input WAV file. The output is another WAV file as well as |
| a detailed log file (in CSV format) for troubleshooting. |
| */ |
| |
| |
| /* Include PJMEDIA and PJLIB */ |
| #include <pjmedia.h> |
| #include <pjmedia-codec.h> |
| #include <pjlib.h> |
| #include <pjlib-util.h> |
| |
| #define THIS_FILE "jbsim.c" |
| |
| /* Timer resolution in ms (must be NONZERO!) */ |
| #define WALL_CLOCK_TICK 1 |
| |
| /* Defaults settings */ |
| #define CODEC "PCMU" |
| #define LOG_FILE "jbsim.csv" |
| #define WAV_REF "../../tests/pjsua/wavs/input.8.wav" |
| #define WAV_OUT "jbsim.wav" |
| #define DURATION 60 |
| #define DTX PJ_TRUE |
| #define PLC PJ_TRUE |
| #define MIN_LOST_BURST 0 |
| #define MAX_LOST_BURST 20 |
| #define LOSS_CORR 0 |
| #define LOSS_EXTRA 2 |
| #define SILENT 1 |
| |
| /* |
| Test setup: |
| |
| Input WAV --> TX Stream --> Loop transport --> RX Stream --> Out WAV |
| */ |
| |
| /* Stream settings */ |
| struct stream_cfg |
| { |
| const char *name; /* for logging purposes */ |
| pjmedia_dir dir; /* stream direction */ |
| pj_str_t codec; /* codec name */ |
| unsigned ptime; /* zero for default */ |
| pj_bool_t dtx; /* DTX enabled? */ |
| pj_bool_t plc; /* PLC enabled? */ |
| }; |
| |
| /* Stream instance. We will instantiate two streams, TX and RX */ |
| struct stream |
| { |
| pj_pool_t *pool; |
| pjmedia_stream *strm; |
| pjmedia_port *port; |
| |
| /* |
| * Running states: |
| */ |
| union { |
| /* TX stream state */ |
| struct { |
| pj_time_val next_schedule; /* Time to send next packet */ |
| unsigned total_tx; /* # of TX packets so far */ |
| int total_lost; /* # of dropped pkts so far */ |
| unsigned cur_lost_burst; /* current # of lost bursts */ |
| unsigned drop_prob; /* drop probability value */ |
| |
| } tx; |
| |
| /* RX stream state */ |
| struct { |
| pj_time_val next_schedule; /* Time to fetch next pkt */ |
| } rx; |
| } state; |
| }; |
| |
| /* |
| * Logging |
| */ |
| |
| /* Events names */ |
| #define EVENT_LOG "" |
| #define EVENT_TX "TX/PUT" |
| #define EVENT_TX_DROP "*** LOSS ***" |
| #define EVENT_GET_PRE "GET (pre)" |
| #define EVENT_GET_POST "GET (post)" |
| |
| |
| /* Logging entry */ |
| struct log_entry |
| { |
| pj_time_val wall_clock; /* Wall clock time */ |
| const char *event; /* Event name */ |
| pjmedia_jb_state *jb_state; /* JB state, optional */ |
| pjmedia_rtcp_stat *stat; /* Stream stat, optional */ |
| const char *log; /* Log message, optional */ |
| }; |
| |
| /* Test settings, taken from command line */ |
| struct test_cfg |
| { |
| /* General options */ |
| pj_bool_t silent; /* Write little to stdout */ |
| const char *log_file; /* The output log file */ |
| |
| /* Test settings */ |
| pj_str_t codec; /* Codec to be used */ |
| unsigned duration_msec; /* Test duration */ |
| |
| /* Transmitter setting */ |
| const char *tx_wav_in; /* Input/reference WAV */ |
| unsigned tx_ptime; /* TX stream ptime */ |
| unsigned tx_min_jitter; /* Minimum jitter in ms */ |
| unsigned tx_max_jitter; /* Max jitter in ms */ |
| unsigned tx_dtx; /* DTX enabled? */ |
| unsigned tx_pct_avg_lost; /* Average loss in percent */ |
| unsigned tx_min_lost_burst; /* Min lost burst in #pkt */ |
| unsigned tx_max_lost_burst; /* Max lost burst in #pkt */ |
| unsigned tx_pct_loss_corr; /* Loss correlation in pct */ |
| |
| /* Receiver setting */ |
| const char *rx_wav_out; /* Output WAV file */ |
| unsigned rx_ptime; /* RX stream ptime */ |
| unsigned rx_snd_burst; /* RX sound burst */ |
| pj_bool_t rx_plc; /* RX PLC enabled? */ |
| int rx_jb_init; /* if > 0 will enable prefetch (ms) */ |
| int rx_jb_min_pre; /* JB minimum prefetch (ms) */ |
| int rx_jb_max_pre; /* JB maximum prefetch (ms) */ |
| int rx_jb_max; /* JB maximum size (ms) */ |
| }; |
| |
| /* |
| * Global var |
| */ |
| struct global_app |
| { |
| pj_caching_pool cp; |
| pj_pool_t *pool; |
| pj_int16_t *framebuf; |
| pjmedia_endpt *endpt; |
| pjmedia_transport *loop; |
| |
| pj_oshandle_t log_fd; |
| |
| struct test_cfg cfg; |
| |
| struct stream *tx; |
| pjmedia_port *tx_wav; |
| |
| struct stream *rx; |
| pjmedia_port *rx_wav; |
| |
| pj_time_val wall_clock; |
| }; |
| |
| static struct global_app g_app; |
| |
| |
| #ifndef MAX |
| # define MAX(a,b) (a<b ? b : a) |
| #endif |
| |
| #ifndef MIN |
| # define MIN(a,b) (a<b ? a : b) |
| #endif |
| |
| /***************************************************************************** |
| * Logging |
| */ |
| static void write_log(struct log_entry *entry, pj_bool_t to_stdout) |
| { |
| /* Format (CSV): */ |
| const char *format = "TIME;EVENT;#RX packets;#packets lost;#JB prefetch;#JB size;#JBDISCARD;#JBEMPTY;Log Message"; |
| static char log[2000]; |
| enum { D = 20 }; |
| char s_jbprefetch[D], |
| s_jbsize[D], |
| s_rxpkt[D], |
| s_losspkt[D], |
| s_jbdiscard[D], |
| s_jbempty[D]; |
| static pj_bool_t header_written; |
| |
| if (!header_written) { |
| pj_ansi_snprintf(log, sizeof(log), |
| "%s\n", format); |
| if (g_app.log_fd != NULL) { |
| pj_ssize_t size = strlen(log); |
| pj_file_write(g_app.log_fd, log, &size); |
| } |
| if (to_stdout && !g_app.cfg.silent) |
| printf("%s", log); |
| header_written = PJ_TRUE; |
| } |
| |
| if (entry->jb_state) { |
| sprintf(s_jbprefetch, "%d", entry->jb_state->prefetch); |
| sprintf(s_jbsize, "%d", entry->jb_state->size); |
| sprintf(s_jbdiscard, "%d", entry->jb_state->discard); |
| sprintf(s_jbempty, "%d", entry->jb_state->empty); |
| } else { |
| strcpy(s_jbprefetch, ""); |
| strcpy(s_jbsize, ""); |
| strcpy(s_jbdiscard, ""); |
| strcpy(s_jbempty, ""); |
| } |
| |
| if (entry->stat) { |
| sprintf(s_rxpkt, "%d", entry->stat->rx.pkt); |
| sprintf(s_losspkt, "%d", entry->stat->rx.loss); |
| } else { |
| strcpy(s_rxpkt, ""); |
| strcpy(s_losspkt, ""); |
| } |
| |
| if (entry->log == NULL) |
| entry->log = ""; |
| |
| pj_ansi_snprintf(log, sizeof(log), |
| "'%d.%03d;" /* time */ |
| "%s;" /* event */ |
| "%s;" /* rxpkt */ |
| "%s;" /* jb prefetch */ |
| "%s;" /* jbsize */ |
| "%s;" /* losspkt */ |
| "%s;" /* jbdiscard */ |
| "%s;" /* jbempty */ |
| "%s\n" /* logmsg */, |
| |
| (int)entry->wall_clock.sec, (int)entry->wall_clock.msec, /* time */ |
| entry->event, |
| s_rxpkt, |
| s_losspkt, |
| s_jbprefetch, |
| s_jbsize, |
| s_jbdiscard, |
| s_jbempty, |
| entry->log |
| ); |
| if (g_app.log_fd != NULL) { |
| pj_ssize_t size = strlen(log); |
| pj_file_write(g_app.log_fd, log, &size); |
| } |
| |
| if (to_stdout && !g_app.cfg.silent) |
| printf("%s", log); |
| } |
| |
| static void log_cb(int level, const char *data, int len) |
| { |
| struct log_entry entry; |
| |
| /* Write to stdout */ |
| pj_log_write(level, data, len); |
| puts(""); |
| |
| /* Also add to CSV file */ |
| pj_bzero(&entry, sizeof(entry)); |
| entry.event = EVENT_LOG; |
| entry.log = data; |
| entry.wall_clock = g_app.wall_clock; |
| write_log(&entry, PJ_FALSE); |
| } |
| |
| static void jbsim_perror(const char *title, pj_status_t status) |
| { |
| char errmsg[PJ_ERR_MSG_SIZE]; |
| |
| pj_strerror(status, errmsg, sizeof(errmsg)); |
| PJ_LOG(1,(THIS_FILE, "%s: %s", title, errmsg)); |
| } |
| |
| /***************************************************************************** |
| * stream |
| */ |
| |
| static void stream_destroy(struct stream *stream) |
| { |
| if (stream->strm) |
| pjmedia_stream_destroy(stream->strm); |
| if (stream->pool) |
| pj_pool_release(stream->pool); |
| } |
| |
| static pj_status_t stream_init(const struct stream_cfg *cfg, struct stream **p_stream) |
| { |
| pj_pool_t *pool = NULL; |
| struct stream *stream = NULL; |
| pjmedia_codec_mgr *cm; |
| unsigned count; |
| const pjmedia_codec_info *ci; |
| pjmedia_stream_info si; |
| pj_status_t status; |
| |
| /* Create instance */ |
| pool = pj_pool_create(&g_app.cp.factory, cfg->name, 512, 512, NULL); |
| stream = PJ_POOL_ZALLOC_T(pool, struct stream); |
| stream->pool = pool; |
| |
| /* Create stream info */ |
| pj_bzero(&si, sizeof(si)); |
| si.type = PJMEDIA_TYPE_AUDIO; |
| si.proto = PJMEDIA_TP_PROTO_RTP_AVP; |
| si.dir = cfg->dir; |
| pj_sockaddr_in_init(&si.rem_addr.ipv4, NULL, 4000); /* dummy */ |
| pj_sockaddr_in_init(&si.rem_rtcp.ipv4, NULL, 4001); /* dummy */ |
| |
| /* Apply JB settings if this is RX direction */ |
| if (cfg->dir == PJMEDIA_DIR_DECODING) { |
| si.jb_init = g_app.cfg.rx_jb_init; |
| si.jb_min_pre = g_app.cfg.rx_jb_min_pre; |
| si.jb_max_pre = g_app.cfg.rx_jb_max_pre; |
| si.jb_max = g_app.cfg.rx_jb_max; |
| } |
| |
| /* Get the codec info and param */ |
| cm = pjmedia_endpt_get_codec_mgr(g_app.endpt); |
| count = 1; |
| status = pjmedia_codec_mgr_find_codecs_by_id(cm, &cfg->codec, &count, &ci, NULL); |
| if (status != PJ_SUCCESS) { |
| jbsim_perror("Unable to find codec", status); |
| goto on_error; |
| } |
| |
| pj_memcpy(&si.fmt, ci, sizeof(*ci)); |
| |
| si.param = PJ_POOL_ALLOC_T(pool, struct pjmedia_codec_param); |
| status = pjmedia_codec_mgr_get_default_param(cm, &si.fmt, si.param); |
| if (status != PJ_SUCCESS) { |
| jbsim_perror("Unable to get codec defaults", status); |
| goto on_error; |
| } |
| |
| si.tx_pt = si.fmt.pt; |
| |
| /* Apply ptime setting */ |
| if (cfg->ptime) { |
| si.param->setting.frm_per_pkt = (pj_uint8_t) |
| ((cfg->ptime + si.param->info.frm_ptime - 1) / |
| si.param->info.frm_ptime); |
| } |
| /* Apply DTX setting */ |
| si.param->setting.vad = cfg->dtx; |
| |
| /* Apply PLC setting */ |
| si.param->setting.plc = cfg->plc; |
| |
| /* Create stream */ |
| status = pjmedia_stream_create(g_app.endpt, pool, &si, g_app.loop, NULL, &stream->strm); |
| if (status != PJ_SUCCESS) { |
| jbsim_perror("Error creating stream", status); |
| goto on_error; |
| } |
| |
| status = pjmedia_stream_get_port(stream->strm, &stream->port); |
| if (status != PJ_SUCCESS) { |
| jbsim_perror("Error retrieving stream", status); |
| goto on_error; |
| } |
| |
| /* Start stream */ |
| status = pjmedia_stream_start(stream->strm); |
| if (status != PJ_SUCCESS) { |
| jbsim_perror("Error starting stream", status); |
| goto on_error; |
| } |
| |
| /* Done */ |
| *p_stream = stream; |
| return PJ_SUCCESS; |
| |
| on_error: |
| if (stream) { |
| stream_destroy(stream); |
| } else { |
| if (pool) |
| pj_pool_release(pool); |
| } |
| return status; |
| } |
| |
| |
| /***************************************************************************** |
| * The test session |
| */ |
| static void test_destroy(void) |
| { |
| if (g_app.tx) |
| stream_destroy(g_app.tx); |
| if (g_app.tx_wav) |
| pjmedia_port_destroy(g_app.tx_wav); |
| if (g_app.rx) |
| stream_destroy(g_app.rx); |
| if (g_app.rx_wav) |
| pjmedia_port_destroy(g_app.rx_wav); |
| if (g_app.loop) |
| pjmedia_transport_close(g_app.loop); |
| if (g_app.endpt) |
| pjmedia_endpt_destroy( g_app.endpt ); |
| if (g_app.log_fd) { |
| pj_log_set_log_func(&pj_log_write); |
| pj_log_set_decor(pj_log_get_decor() | PJ_LOG_HAS_NEWLINE); |
| pj_file_close(g_app.log_fd); |
| g_app.log_fd = NULL; |
| } |
| if (g_app.pool) |
| pj_pool_release(g_app.pool); |
| pj_caching_pool_destroy( &g_app.cp ); |
| pj_shutdown(); |
| } |
| |
| |
| static pj_status_t test_init(void) |
| { |
| struct stream_cfg strm_cfg; |
| pj_status_t status; |
| |
| /* Must init PJLIB first: */ |
| status = pj_init(); |
| PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); |
| |
| /* Must create a pool factory before we can allocate any memory. */ |
| pj_caching_pool_init(&g_app.cp, &pj_pool_factory_default_policy, 0); |
| |
| /* Pool */ |
| g_app.pool = pj_pool_create(&g_app.cp.factory, "g_app", 512, 512, NULL); |
| |
| /* Log file */ |
| if (g_app.cfg.log_file) { |
| status = pj_file_open(g_app.pool, g_app.cfg.log_file, |
| PJ_O_WRONLY, |
| &g_app.log_fd); |
| if (status != PJ_SUCCESS) { |
| jbsim_perror("Error writing output file", status); |
| goto on_error; |
| } |
| |
| pj_log_set_decor(PJ_LOG_HAS_SENDER | PJ_LOG_HAS_COLOR | PJ_LOG_HAS_LEVEL_TEXT); |
| pj_log_set_log_func(&log_cb); |
| } |
| |
| /* |
| * Initialize media endpoint. |
| * This will implicitly initialize PJMEDIA too. |
| */ |
| status = pjmedia_endpt_create(&g_app.cp.factory, NULL, 0, &g_app.endpt); |
| if (status != PJ_SUCCESS) { |
| jbsim_perror("Error creating media endpoint", status); |
| goto on_error; |
| } |
| |
| /* Register codecs */ |
| pjmedia_codec_register_audio_codecs(g_app.endpt, NULL); |
| |
| /* Create the loop transport */ |
| status = pjmedia_transport_loop_create(g_app.endpt, &g_app.loop); |
| if (status != PJ_SUCCESS) { |
| jbsim_perror("Error creating loop transport", status); |
| goto on_error; |
| } |
| |
| /* Create transmitter stream */ |
| pj_bzero(&strm_cfg, sizeof(strm_cfg)); |
| strm_cfg.name = "tx"; |
| strm_cfg.dir = PJMEDIA_DIR_ENCODING; |
| strm_cfg.codec = g_app.cfg.codec; |
| strm_cfg.ptime = g_app.cfg.tx_ptime; |
| strm_cfg.dtx = g_app.cfg.tx_dtx; |
| strm_cfg.plc = PJ_TRUE; |
| status = stream_init(&strm_cfg, &g_app.tx); |
| if (status != PJ_SUCCESS) |
| goto on_error; |
| |
| /* Create transmitter WAV */ |
| status = pjmedia_wav_player_port_create(g_app.pool, |
| g_app.cfg.tx_wav_in, |
| g_app.cfg.tx_ptime, |
| 0, |
| 0, |
| &g_app.tx_wav); |
| if (status != PJ_SUCCESS) { |
| jbsim_perror("Error reading input WAV file", status); |
| goto on_error; |
| } |
| |
| /* Make sure stream and WAV parameters match */ |
| if (PJMEDIA_PIA_SRATE(&g_app.tx_wav->info) != PJMEDIA_PIA_SRATE(&g_app.tx->port->info) || |
| PJMEDIA_PIA_CCNT(&g_app.tx_wav->info) != PJMEDIA_PIA_CCNT(&g_app.tx->port->info)) |
| { |
| jbsim_perror("Error: Input WAV file has different clock rate " |
| "or number of channels than the codec", PJ_SUCCESS); |
| goto on_error; |
| } |
| |
| |
| /* Create receiver */ |
| pj_bzero(&strm_cfg, sizeof(strm_cfg)); |
| strm_cfg.name = "rx"; |
| strm_cfg.dir = PJMEDIA_DIR_DECODING; |
| strm_cfg.codec = g_app.cfg.codec; |
| strm_cfg.ptime = g_app.cfg.rx_ptime; |
| strm_cfg.dtx = PJ_TRUE; |
| strm_cfg.plc = g_app.cfg.rx_plc; |
| status = stream_init(&strm_cfg, &g_app.rx); |
| if (status != PJ_SUCCESS) |
| goto on_error; |
| |
| /* Create receiver WAV */ |
| status = pjmedia_wav_writer_port_create(g_app.pool, |
| g_app.cfg.rx_wav_out, |
| PJMEDIA_PIA_SRATE(&g_app.rx->port->info), |
| PJMEDIA_PIA_CCNT(&g_app.rx->port->info), |
| PJMEDIA_PIA_SPF(&g_app.rx->port->info), |
| PJMEDIA_PIA_BITS(&g_app.rx->port->info), |
| 0, |
| 0, |
| &g_app.rx_wav); |
| if (status != PJ_SUCCESS) { |
| jbsim_perror("Error creating output WAV file", status); |
| goto on_error; |
| } |
| |
| |
| /* Frame buffer */ |
| g_app.framebuf = (pj_int16_t*) |
| pj_pool_alloc(g_app.pool, |
| MAX(PJMEDIA_PIA_SPF(&g_app.rx->port->info), |
| PJMEDIA_PIA_SPF(&g_app.tx->port->info)) * sizeof(pj_int16_t)); |
| |
| |
| /* Set the receiver in the loop transport */ |
| pjmedia_transport_loop_disable_rx(g_app.loop, g_app.tx->strm, PJ_TRUE); |
| |
| /* Done */ |
| return PJ_SUCCESS; |
| |
| on_error: |
| test_destroy(); |
| return status; |
| } |
| |
| static void run_one_frame(pjmedia_port *src, pjmedia_port *dst, |
| pj_bool_t *has_frame) |
| { |
| pjmedia_frame frame; |
| pj_status_t status; |
| |
| pj_bzero(&frame, sizeof(frame)); |
| frame.type = PJMEDIA_FRAME_TYPE_AUDIO; |
| frame.buf = g_app.framebuf; |
| frame.size = PJMEDIA_PIA_SPF(&dst->info) * 2; |
| |
| status = pjmedia_port_get_frame(src, &frame); |
| pj_assert(status == PJ_SUCCESS); |
| |
| if (status!= PJ_SUCCESS || frame.type != PJMEDIA_FRAME_TYPE_AUDIO) { |
| frame.buf = g_app.framebuf; |
| pjmedia_zero_samples(g_app.framebuf, PJMEDIA_PIA_SPF(&src->info)); |
| frame.size = PJMEDIA_PIA_SPF(&src->info) * 2; |
| if (has_frame) |
| *has_frame = PJ_FALSE; |
| } else { |
| if (has_frame) |
| *has_frame = PJ_TRUE; |
| } |
| |
| |
| status = pjmedia_port_put_frame(dst, &frame); |
| pj_assert(status == PJ_SUCCESS); |
| } |
| |
| |
| /* This is the transmission "tick". |
| * This function is called periodically every "tick" milliseconds, and |
| * it will determine whether to transmit packet(s) (or to drop it). |
| */ |
| static void tx_tick(const pj_time_val *t) |
| { |
| struct stream *strm = g_app.tx; |
| static char log_msg[120]; |
| pjmedia_port *port = g_app.tx->port; |
| long pkt_interval; |
| |
| /* packet interval, without jitter */ |
| pkt_interval = PJMEDIA_PIA_SPF(&port->info) * 1000 / |
| PJMEDIA_PIA_SRATE(&port->info); |
| |
| while (PJ_TIME_VAL_GTE(*t, strm->state.tx.next_schedule)) { |
| struct log_entry entry; |
| pj_bool_t drop_this_pkt = PJ_FALSE; |
| int jitter; |
| |
| /* Init log entry */ |
| pj_bzero(&entry, sizeof(entry)); |
| entry.wall_clock = *t; |
| |
| /* |
| * Determine whether to drop this packet |
| */ |
| if (strm->state.tx.cur_lost_burst) { |
| /* We are currently dropping packet */ |
| |
| /* Make it comply to minimum lost burst */ |
| if (strm->state.tx.cur_lost_burst < g_app.cfg.tx_min_lost_burst) { |
| drop_this_pkt = PJ_TRUE; |
| } |
| |
| /* Correlate the next packet loss */ |
| if (!drop_this_pkt && |
| strm->state.tx.cur_lost_burst < g_app.cfg.tx_max_lost_burst && |
| MAX(strm->state.tx.total_lost-LOSS_EXTRA,0) * 100 / MAX(strm->state.tx.total_tx,1) < g_app.cfg.tx_pct_avg_lost |
| ) |
| { |
| strm->state.tx.drop_prob = ((g_app.cfg.tx_pct_loss_corr * strm->state.tx.drop_prob) + |
| ((100-g_app.cfg.tx_pct_loss_corr) * (pj_rand()%100)) |
| ) / 100; |
| if (strm->state.tx.drop_prob >= 100) |
| strm->state.tx.drop_prob = 99; |
| |
| if (strm->state.tx.drop_prob >= 100 - g_app.cfg.tx_pct_avg_lost) |
| drop_this_pkt = PJ_TRUE; |
| } |
| } |
| |
| /* If we're not dropping packet then use randomly distributed loss */ |
| if (!drop_this_pkt && |
| MAX(strm->state.tx.total_lost-LOSS_EXTRA,0) * 100 / MAX(strm->state.tx.total_tx,1) < g_app.cfg.tx_pct_avg_lost) |
| { |
| strm->state.tx.drop_prob = pj_rand() % 100; |
| |
| if (strm->state.tx.drop_prob >= 100 - g_app.cfg.tx_pct_avg_lost) |
| drop_this_pkt = PJ_TRUE; |
| } |
| |
| if (drop_this_pkt) { |
| /* Drop the frame */ |
| pjmedia_transport_simulate_lost(g_app.loop, PJMEDIA_DIR_ENCODING, 100); |
| run_one_frame(g_app.tx_wav, g_app.tx->port, NULL); |
| pjmedia_transport_simulate_lost(g_app.loop, PJMEDIA_DIR_ENCODING, 0); |
| |
| entry.event = EVENT_TX_DROP; |
| entry.log = "** This packet was lost **"; |
| |
| ++strm->state.tx.total_lost; |
| ++strm->state.tx.cur_lost_burst; |
| |
| } else { |
| pjmedia_rtcp_stat stat; |
| pjmedia_jb_state jstate; |
| unsigned last_discard; |
| |
| pjmedia_stream_get_stat_jbuf(g_app.rx->strm, &jstate); |
| last_discard = jstate.discard; |
| |
| run_one_frame(g_app.tx_wav, g_app.tx->port, NULL); |
| |
| pjmedia_stream_get_stat(g_app.rx->strm, &stat); |
| pjmedia_stream_get_stat_jbuf(g_app.rx->strm, &jstate); |
| |
| entry.event = EVENT_TX; |
| entry.jb_state = &jstate; |
| entry.stat = &stat; |
| entry.log = log_msg; |
| |
| if (jstate.discard > last_discard) |
| strcat(log_msg, "** Note: packet was discarded by jitter buffer **"); |
| |
| strm->state.tx.cur_lost_burst = 0; |
| } |
| |
| write_log(&entry, PJ_TRUE); |
| |
| ++strm->state.tx.total_tx; |
| |
| /* Calculate next schedule */ |
| strm->state.tx.next_schedule.sec = 0; |
| strm->state.tx.next_schedule.msec = (strm->state.tx.total_tx + 1) * pkt_interval; |
| |
| /* Apply jitter */ |
| if (g_app.cfg.tx_max_jitter || g_app.cfg.tx_min_jitter) { |
| |
| if (g_app.cfg.tx_max_jitter == g_app.cfg.tx_min_jitter) { |
| /* Fixed jitter */ |
| switch (pj_rand() % 3) { |
| case 0: |
| jitter = 0 - g_app.cfg.tx_min_jitter; |
| break; |
| case 2: |
| jitter = g_app.cfg.tx_min_jitter; |
| break; |
| default: |
| jitter = 0; |
| break; |
| } |
| } else { |
| int jitter_range; |
| jitter_range = (g_app.cfg.tx_max_jitter-g_app.cfg.tx_min_jitter)*2; |
| jitter = pj_rand() % jitter_range; |
| if (jitter < jitter_range/2) { |
| jitter = 0 - g_app.cfg.tx_min_jitter - (jitter/2); |
| } else { |
| jitter = g_app.cfg.tx_min_jitter + (jitter/2); |
| } |
| } |
| |
| } else { |
| jitter = 0; |
| } |
| |
| pj_time_val_normalize(&strm->state.tx.next_schedule); |
| |
| sprintf(log_msg, "** Packet #%u tick is at %d.%03d, %d ms jitter applied **", |
| strm->state.tx.total_tx+1, |
| (int)strm->state.tx.next_schedule.sec, (int)strm->state.tx.next_schedule.msec, |
| jitter); |
| |
| strm->state.tx.next_schedule.msec += jitter; |
| pj_time_val_normalize(&strm->state.tx.next_schedule); |
| |
| } /* while */ |
| } |
| |
| |
| /* This is the RX "tick". |
| * This function is called periodically every "tick" milliseconds, and |
| * it will determine whether to call get_frame() from the RX stream. |
| */ |
| static void rx_tick(const pj_time_val *t) |
| { |
| struct stream *strm = g_app.rx; |
| pjmedia_port *port = g_app.rx->port; |
| long pkt_interval; |
| |
| pkt_interval = PJMEDIA_PIA_SPF(&port->info) * 1000 / |
| PJMEDIA_PIA_SRATE(&port->info) * |
| g_app.cfg.rx_snd_burst; |
| |
| if (PJ_TIME_VAL_GTE(*t, strm->state.rx.next_schedule)) { |
| unsigned i; |
| for (i=0; i<g_app.cfg.rx_snd_burst; ++i) { |
| struct log_entry entry; |
| pjmedia_rtcp_stat stat; |
| pjmedia_jb_state jstate; |
| pj_bool_t has_frame; |
| char msg[120]; |
| unsigned last_empty; |
| |
| pjmedia_stream_get_stat(g_app.rx->strm, &stat); |
| pjmedia_stream_get_stat_jbuf(g_app.rx->strm, &jstate); |
| last_empty = jstate.empty; |
| |
| /* Pre GET event */ |
| pj_bzero(&entry, sizeof(entry)); |
| entry.event = EVENT_GET_PRE; |
| entry.wall_clock = *t; |
| entry.stat = &stat; |
| entry.jb_state = &jstate; |
| |
| write_log(&entry, PJ_TRUE); |
| |
| /* GET */ |
| run_one_frame(g_app.rx->port, g_app.rx_wav, &has_frame); |
| |
| /* Post GET event */ |
| pjmedia_stream_get_stat(g_app.rx->strm, &stat); |
| pjmedia_stream_get_stat_jbuf(g_app.rx->strm, &jstate); |
| |
| pj_bzero(&entry, sizeof(entry)); |
| entry.event = EVENT_GET_POST; |
| entry.wall_clock = *t; |
| entry.stat = &stat; |
| entry.jb_state = &jstate; |
| |
| msg[0] = '\0'; |
| entry.log = msg; |
| |
| if (jstate.empty > last_empty) |
| strcat(msg, "** JBUF was empty **"); |
| if (!has_frame) |
| strcat(msg, "** NULL frame was returned **"); |
| |
| write_log(&entry, PJ_TRUE); |
| |
| } |
| |
| |
| strm->state.rx.next_schedule.msec += pkt_interval; |
| pj_time_val_normalize(&strm->state.rx.next_schedule); |
| } |
| |
| } |
| |
| static void test_loop(long duration) |
| { |
| g_app.wall_clock.sec = 0; |
| g_app.wall_clock.msec = 0; |
| |
| while (PJ_TIME_VAL_MSEC(g_app.wall_clock) <= duration) { |
| |
| /* Run TX tick */ |
| tx_tick(&g_app.wall_clock); |
| |
| /* Run RX tick */ |
| rx_tick(&g_app.wall_clock); |
| |
| /* Increment tick */ |
| g_app.wall_clock.msec += WALL_CLOCK_TICK; |
| pj_time_val_normalize(&g_app.wall_clock); |
| } |
| } |
| |
| |
| /***************************************************************************** |
| * usage() |
| */ |
| enum { |
| OPT_CODEC = 'c', |
| OPT_INPUT = 'i', |
| OPT_OUTPUT = 'o', |
| OPT_DURATION = 'd', |
| OPT_LOG_FILE = 'l', |
| OPT_LOSS = 'x', |
| OPT_MIN_JITTER = 'j', |
| OPT_MAX_JITTER = 'J', |
| OPT_SND_BURST = 'b', |
| OPT_TX_PTIME = 't', |
| OPT_RX_PTIME = 'r', |
| OPT_NO_VAD = 'U', |
| OPT_NO_PLC = 'p', |
| OPT_JB_PREFETCH = 'P', |
| OPT_JB_MIN_PRE = 'm', |
| OPT_JB_MAX_PRE = 'M', |
| OPT_JB_MAX = 'X', |
| OPT_HELP = 'h', |
| OPT_MIN_LOST_BURST = 1, |
| OPT_MAX_LOST_BURST, |
| OPT_LOSS_CORR, |
| }; |
| |
| |
| static void usage(void) |
| { |
| printf("jbsim - System and network impairments simulator\n"); |
| printf("Copyright (C) 2008-2009 Teluu Inc. (http://www.teluu.com)\n"); |
| printf("\n"); |
| printf("This program emulates various system and network impairment\n"); |
| printf("conditions as well as application parameters and apply it to\n"); |
| printf("an input WAV file. The output is another WAV file as well as\n"); |
| printf("a detailed log file (in CSV format) for troubleshooting.\n"); |
| printf("\n"); |
| printf("Usage:\n"); |
| printf(" jbsim [OPTIONS]\n"); |
| printf("\n"); |
| printf("General OPTIONS:\n"); |
| printf(" --codec, -%c NAME Set the audio codec\n", OPT_CODEC); |
| printf(" Default: %s\n", CODEC); |
| printf(" --input, -%c FILE Set WAV reference file to FILE\n", OPT_INPUT); |
| printf(" Default: " WAV_REF "\n"); |
| printf(" --output, -%c FILE Set WAV output file to FILE\n", OPT_OUTPUT); |
| printf(" Default: " WAV_OUT "\n"); |
| printf(" --duration, -%c SEC Set test duration to SEC seconds\n", OPT_DURATION); |
| printf(" Default: %d\n", DURATION); |
| printf(" --log-file, -%c FILE Save simulation log file to FILE\n", OPT_LOG_FILE); |
| printf(" Note: FILE will be in CSV format with semicolon separator\n"); |
| printf(" Default: %s\n", LOG_FILE); |
| printf(" --help, -h Display this screen\n"); |
| printf("\n"); |
| printf("Simulation OPTIONS:\n"); |
| printf(" --loss, -%c PCT Set packet average loss to PCT percent\n", OPT_LOSS); |
| printf(" Default: 0\n"); |
| printf(" --loss-corr PCT Set the loss correlation to PCT percent. Default: 0\n"); |
| printf(" --min-lost-burst N Set minimum packet lost burst (default:%d)\n", MIN_LOST_BURST); |
| printf(" --max-lost-burst N Set maximum packet lost burst (default:%d)\n", MAX_LOST_BURST); |
| printf(" --min-jitter, -%c MSEC Set minimum network jitter to MSEC\n", OPT_MIN_JITTER); |
| printf(" Default: 0\n"); |
| printf(" --max-jitter, -%c MSEC Set maximum network jitter to MSEC\n", OPT_MAX_JITTER); |
| printf(" Default: 0\n"); |
| printf(" --snd-burst, -%c VAL Set RX sound burst value to VAL frames.\n", OPT_SND_BURST); |
| printf(" Default: 1\n"); |
| printf(" --tx-ptime, -%c MSEC Set transmitter ptime to MSEC\n", OPT_TX_PTIME); |
| printf(" Default: 0 (not set, use default)\n"); |
| printf(" --rx-ptime, -%c MSEC Set receiver ptime to MSEC\n", OPT_RX_PTIME); |
| printf(" Default: 0 (not set, use default)\n"); |
| printf(" --no-vad, -%c Disable VAD/DTX in transmitter\n", OPT_NO_VAD); |
| printf(" --no-plc, -%c Disable PLC in receiver\n", OPT_NO_PLC); |
| printf(" --jb-prefetch, -%c Enable prefetch bufferring in jitter buffer\n", OPT_JB_PREFETCH); |
| printf(" --jb-min-pre, -%c MSEC Jitter buffer minimum prefetch delay in msec\n", OPT_JB_MIN_PRE); |
| printf(" --jb-max-pre, -%c MSEC Jitter buffer maximum prefetch delay in msec\n", OPT_JB_MAX_PRE); |
| printf(" --jb-max, -%c MSEC Set maximum delay that can be accomodated by the\n", OPT_JB_MAX); |
| printf(" jitter buffer msec.\n"); |
| } |
| |
| |
| static int init_options(int argc, char *argv[]) |
| { |
| struct pj_getopt_option long_options[] = { |
| { "codec", 1, 0, OPT_CODEC }, |
| { "input", 1, 0, OPT_INPUT }, |
| { "output", 1, 0, OPT_OUTPUT }, |
| { "duration", 1, 0, OPT_DURATION }, |
| { "log-file", 1, 0, OPT_LOG_FILE}, |
| { "loss", 1, 0, OPT_LOSS }, |
| { "min-lost-burst", 1, 0, OPT_MIN_LOST_BURST}, |
| { "max-lost-burst", 1, 0, OPT_MAX_LOST_BURST}, |
| { "loss-corr", 1, 0, OPT_LOSS_CORR}, |
| { "min-jitter", 1, 0, OPT_MIN_JITTER }, |
| { "max-jitter", 1, 0, OPT_MAX_JITTER }, |
| { "snd-burst", 1, 0, OPT_SND_BURST }, |
| { "tx-ptime", 1, 0, OPT_TX_PTIME }, |
| { "rx-ptime", 1, 0, OPT_RX_PTIME }, |
| { "no-vad", 0, 0, OPT_NO_VAD }, |
| { "no-plc", 0, 0, OPT_NO_PLC }, |
| { "jb-prefetch", 0, 0, OPT_JB_PREFETCH }, |
| { "jb-min-pre", 1, 0, OPT_JB_MIN_PRE }, |
| { "jb-max-pre", 1, 0, OPT_JB_MAX_PRE }, |
| { "jb-max", 1, 0, OPT_JB_MAX }, |
| { "help", 0, 0, OPT_HELP}, |
| { NULL, 0, 0, 0 }, |
| }; |
| int c; |
| int option_index; |
| char format[128]; |
| |
| /* Init default config */ |
| g_app.cfg.codec = pj_str(CODEC); |
| g_app.cfg.duration_msec = DURATION * 1000; |
| g_app.cfg.silent = SILENT; |
| g_app.cfg.log_file = LOG_FILE; |
| g_app.cfg.tx_wav_in = WAV_REF; |
| g_app.cfg.tx_ptime = 0; |
| g_app.cfg.tx_min_jitter = 0; |
| g_app.cfg.tx_max_jitter = 0; |
| g_app.cfg.tx_dtx = DTX; |
| g_app.cfg.tx_pct_avg_lost = 0; |
| g_app.cfg.tx_min_lost_burst = MIN_LOST_BURST; |
| g_app.cfg.tx_max_lost_burst = MAX_LOST_BURST; |
| g_app.cfg.tx_pct_loss_corr = LOSS_CORR; |
| |
| g_app.cfg.rx_wav_out = WAV_OUT; |
| g_app.cfg.rx_ptime = 0; |
| g_app.cfg.rx_plc = PLC; |
| g_app.cfg.rx_snd_burst = 1; |
| g_app.cfg.rx_jb_init = -1; |
| g_app.cfg.rx_jb_min_pre = -1; |
| g_app.cfg.rx_jb_max_pre = -1; |
| g_app.cfg.rx_jb_max = -1; |
| |
| /* Build format */ |
| format[0] = '\0'; |
| for (c=0; c<PJ_ARRAY_SIZE(long_options)-1; ++c) { |
| if (long_options[c].has_arg) { |
| char cmd[10]; |
| pj_ansi_snprintf(cmd, sizeof(cmd), "%c:", long_options[c].val); |
| pj_ansi_strcat(format, cmd); |
| } |
| } |
| for (c=0; c<PJ_ARRAY_SIZE(long_options)-1; ++c) { |
| if (long_options[c].has_arg == 0) { |
| char cmd[10]; |
| pj_ansi_snprintf(cmd, sizeof(cmd), "%c", long_options[c].val); |
| pj_ansi_strcat(format, cmd); |
| } |
| } |
| |
| /* Parse options */ |
| pj_optind = 0; |
| while((c=pj_getopt_long(argc,argv, format, |
| long_options, &option_index))!=-1) |
| { |
| switch (c) { |
| case OPT_CODEC: |
| g_app.cfg.codec = pj_str(pj_optarg); |
| break; |
| case OPT_INPUT: |
| g_app.cfg.tx_wav_in = pj_optarg; |
| break; |
| case OPT_OUTPUT: |
| g_app.cfg.rx_wav_out = pj_optarg; |
| break; |
| case OPT_DURATION: |
| g_app.cfg.duration_msec = atoi(pj_optarg) * 1000; |
| break; |
| case OPT_LOG_FILE: |
| g_app.cfg.log_file = pj_optarg; |
| break; |
| case OPT_LOSS: |
| g_app.cfg.tx_pct_avg_lost = atoi(pj_optarg); |
| if (g_app.cfg.tx_pct_avg_lost > 100) { |
| puts("Error: Invalid loss value?"); |
| return 1; |
| } |
| break; |
| case OPT_MIN_LOST_BURST: |
| g_app.cfg.tx_min_lost_burst = atoi(pj_optarg); |
| break; |
| case OPT_MAX_LOST_BURST: |
| g_app.cfg.tx_max_lost_burst = atoi(pj_optarg); |
| break; |
| case OPT_LOSS_CORR: |
| g_app.cfg.tx_pct_loss_corr = atoi(pj_optarg); |
| if (g_app.cfg.tx_pct_avg_lost > 100) { |
| puts("Error: Loss correlation is in percentage, value is not valid?"); |
| return 1; |
| } |
| break; |
| case OPT_MIN_JITTER: |
| g_app.cfg.tx_min_jitter = atoi(pj_optarg); |
| break; |
| case OPT_MAX_JITTER: |
| g_app.cfg.tx_max_jitter = atoi(pj_optarg); |
| break; |
| case OPT_SND_BURST: |
| g_app.cfg.rx_snd_burst = atoi(pj_optarg); |
| break; |
| case OPT_TX_PTIME: |
| g_app.cfg.tx_ptime = atoi(pj_optarg); |
| break; |
| case OPT_RX_PTIME: |
| g_app.cfg.rx_ptime = atoi(pj_optarg); |
| break; |
| case OPT_NO_VAD: |
| g_app.cfg.tx_dtx = PJ_FALSE; |
| break; |
| case OPT_NO_PLC: |
| g_app.cfg.rx_plc = PJ_FALSE; |
| break; |
| case OPT_JB_PREFETCH: |
| g_app.cfg.rx_jb_init = 1; |
| break; |
| case OPT_JB_MIN_PRE: |
| g_app.cfg.rx_jb_min_pre = atoi(pj_optarg); |
| break; |
| case OPT_JB_MAX_PRE: |
| g_app.cfg.rx_jb_max_pre = atoi(pj_optarg); |
| break; |
| case OPT_JB_MAX: |
| g_app.cfg.rx_jb_max = atoi(pj_optarg); |
| break; |
| case OPT_HELP: |
| usage(); |
| return 1; |
| default: |
| usage(); |
| return 1; |
| } |
| } |
| |
| /* Check for orphaned params */ |
| if (pj_optind < argc) { |
| usage(); |
| return 1; |
| } |
| |
| /* Normalize options */ |
| if (g_app.cfg.rx_jb_init < g_app.cfg.rx_jb_min_pre) |
| g_app.cfg.rx_jb_init = g_app.cfg.rx_jb_min_pre; |
| else if (g_app.cfg.rx_jb_init > g_app.cfg.rx_jb_max_pre) |
| g_app.cfg.rx_jb_init = g_app.cfg.rx_jb_max_pre; |
| |
| if (g_app.cfg.tx_max_jitter < g_app.cfg.tx_min_jitter) |
| g_app.cfg.tx_max_jitter = g_app.cfg.tx_min_jitter; |
| return 0; |
| } |
| |
| /***************************************************************************** |
| * main() |
| */ |
| int main(int argc, char *argv[]) |
| { |
| pj_status_t status; |
| |
| if (init_options(argc, argv) != 0) |
| return 1; |
| |
| |
| /* Init */ |
| status = test_init(); |
| if (status != PJ_SUCCESS) |
| return 1; |
| |
| /* Print parameters */ |
| PJ_LOG(3,(THIS_FILE, "Starting simulation. Parameters: ")); |
| PJ_LOG(3,(THIS_FILE, " Codec=%.*s, tx_ptime=%d, rx_ptime=%d", |
| (int)g_app.cfg.codec.slen, |
| g_app.cfg.codec.ptr, |
| g_app.cfg.tx_ptime, |
| g_app.cfg.rx_ptime)); |
| PJ_LOG(3,(THIS_FILE, " Loss avg=%d%%, min_burst=%d, max_burst=%d", |
| g_app.cfg.tx_pct_avg_lost, |
| g_app.cfg.tx_min_lost_burst, |
| g_app.cfg.tx_max_lost_burst)); |
| PJ_LOG(3,(THIS_FILE, " TX jitter min=%dms, max=%dms", |
| g_app.cfg.tx_min_jitter, |
| g_app.cfg.tx_max_jitter)); |
| PJ_LOG(3,(THIS_FILE, " RX jb init:%dms, min_pre=%dms, max_pre=%dms, max=%dms", |
| g_app.cfg.rx_jb_init, |
| g_app.cfg.rx_jb_min_pre, |
| g_app.cfg.rx_jb_max_pre, |
| g_app.cfg.rx_jb_max)); |
| PJ_LOG(3,(THIS_FILE, " RX sound burst:%d frames", |
| g_app.cfg.rx_snd_burst)); |
| PJ_LOG(3,(THIS_FILE, " DTX=%d, PLC=%d", |
| g_app.cfg.tx_dtx, g_app.cfg.rx_plc)); |
| |
| /* Run test loop */ |
| test_loop(g_app.cfg.duration_msec); |
| |
| /* Print statistics */ |
| PJ_LOG(3,(THIS_FILE, "Simulation done")); |
| PJ_LOG(3,(THIS_FILE, " TX packets=%u, dropped=%u/%5.1f%%", |
| g_app.tx->state.tx.total_tx, |
| g_app.tx->state.tx.total_lost, |
| (float)(g_app.tx->state.tx.total_lost * 100.0 / g_app.tx->state.tx.total_tx))); |
| |
| /* Done */ |
| test_destroy(); |
| |
| return 0; |
| } |