Initial commit re #1263 (PJSUA-LIB Video API):
 - API designed and reviewed (pjsua.h)
 - Implemented these APIs and added to pjsua sample application:
   - video device enums API
   - video capture preview API
 - refactoring in PJSUA-LIB:
   - video stuffs go to pjsua_vid.c
   - call dump goes to pjsua_dump.c

We're still missing:
 - video call API implementation
 - media info and statistic API implementation



git-svn-id: https://svn.pjsip.org/repos/pjproject/branches/projects/2.0-dev@3609 74dad513-b988-da41-8d7b-12977e46ad98
diff --git a/pjsip/src/pjsua-lib/pjsua_dump.c b/pjsip/src/pjsua-lib/pjsua_dump.c
new file mode 100644
index 0000000..f94e036
--- /dev/null
+++ b/pjsip/src/pjsua-lib/pjsua_dump.c
@@ -0,0 +1,944 @@
+/* $Id$ */
+/* 
+ * Copyright (C) 2011-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 
+ */
+#include <pjsua-lib/pjsua.h>
+#include <pjsua-lib/pjsua_internal.h>
+
+const char *good_number(char *buf, pj_int32_t val)
+{
+    if (val < 1000) {
+	pj_ansi_sprintf(buf, "%d", val);
+    } else if (val < 1000000) {
+	pj_ansi_sprintf(buf, "%d.%dK",
+			val / 1000,
+			(val % 1000) / 100);
+    } else {
+	pj_ansi_sprintf(buf, "%d.%02dM",
+			val / 1000000,
+			(val % 1000000) / 10000);
+    }
+
+    return buf;
+}
+
+static unsigned dump_media_stat(const char *indent,
+				char *buf, unsigned maxlen,
+				const pjmedia_rtcp_stat *stat,
+				const char *rx_info, const char *tx_info)
+{
+    char last_update[64];
+    char packets[32], bytes[32], ipbytes[32], avg_bps[32], avg_ipbps[32];
+    pj_time_val media_duration, now;
+    char *p = buf, *end = buf+maxlen;
+    int len;
+
+    if (stat->rx.update_cnt == 0)
+	strcpy(last_update, "never");
+    else {
+	pj_gettimeofday(&now);
+	PJ_TIME_VAL_SUB(now, stat->rx.update);
+	sprintf(last_update, "%02ldh:%02ldm:%02ld.%03lds ago",
+		now.sec / 3600,
+		(now.sec % 3600) / 60,
+		now.sec % 60,
+		now.msec);
+    }
+
+    pj_gettimeofday(&media_duration);
+    PJ_TIME_VAL_SUB(media_duration, stat->start);
+    if (PJ_TIME_VAL_MSEC(media_duration) == 0)
+	media_duration.msec = 1;
+
+    len = pj_ansi_snprintf(p, end-p,
+	   "%s     RX %s last update:%s\n"
+	   "%s        total %spkt %sB (%sB +IP hdr) @avg=%sbps/%sbps\n"
+	   "%s        pkt loss=%d (%3.1f%%), discrd=%d (%3.1f%%), dup=%d (%2.1f%%), reord=%d (%3.1f%%)\n"
+	   "%s              (msec)    min     avg     max     last    dev\n"
+	   "%s        loss period: %7.3f %7.3f %7.3f %7.3f %7.3f\n"
+	   "%s        jitter     : %7.3f %7.3f %7.3f %7.3f %7.3f\n"
+#if defined(PJMEDIA_RTCP_STAT_HAS_RAW_JITTER) && PJMEDIA_RTCP_STAT_HAS_RAW_JITTER!=0
+	   "%s        raw jitter : %7.3f %7.3f %7.3f %7.3f %7.3f\n"
+#endif
+#if defined(PJMEDIA_RTCP_STAT_HAS_IPDV) && PJMEDIA_RTCP_STAT_HAS_IPDV!=0
+	   "%s        IPDV       : %7.3f %7.3f %7.3f %7.3f %7.3f\n"
+#endif
+	   "%s",
+	   indent,
+	   rx_info? rx_info : "",
+	   last_update,
+
+	   indent,
+	   good_number(packets, stat->rx.pkt),
+	   good_number(bytes, stat->rx.bytes),
+	   good_number(ipbytes, stat->rx.bytes + stat->rx.pkt * 40),
+	   good_number(avg_bps, (pj_int32_t)((pj_int64_t)stat->rx.bytes * 8 * 1000 / PJ_TIME_VAL_MSEC(media_duration))),
+	   good_number(avg_ipbps, (pj_int32_t)(((pj_int64_t)stat->rx.bytes + stat->rx.pkt * 40) * 8 * 1000 / PJ_TIME_VAL_MSEC(media_duration))),
+	   indent,
+	   stat->rx.loss,
+	   (stat->rx.loss? stat->rx.loss * 100.0 / (stat->rx.pkt + stat->rx.loss) : 0),
+	   stat->rx.discard,
+	   (stat->rx.discard? stat->rx.discard * 100.0 / (stat->rx.pkt + stat->rx.loss) : 0),
+	   stat->rx.dup,
+	   (stat->rx.dup? stat->rx.dup * 100.0 / (stat->rx.pkt + stat->rx.loss) : 0),
+	   stat->rx.reorder,
+	   (stat->rx.reorder? stat->rx.reorder * 100.0 / (stat->rx.pkt + stat->rx.loss) : 0),
+	   indent, indent,
+	   stat->rx.loss_period.min / 1000.0,
+	   stat->rx.loss_period.mean / 1000.0,
+	   stat->rx.loss_period.max / 1000.0,
+	   stat->rx.loss_period.last / 1000.0,
+	   pj_math_stat_get_stddev(&stat->rx.loss_period) / 1000.0,
+	   indent,
+	   stat->rx.jitter.min / 1000.0,
+	   stat->rx.jitter.mean / 1000.0,
+	   stat->rx.jitter.max / 1000.0,
+	   stat->rx.jitter.last / 1000.0,
+	   pj_math_stat_get_stddev(&stat->rx.jitter) / 1000.0,
+#if defined(PJMEDIA_RTCP_STAT_HAS_RAW_JITTER) && PJMEDIA_RTCP_STAT_HAS_RAW_JITTER!=0
+	   indent,
+	   stat->rx_raw_jitter.min / 1000.0,
+	   stat->rx_raw_jitter.mean / 1000.0,
+	   stat->rx_raw_jitter.max / 1000.0,
+	   stat->rx_raw_jitter.last / 1000.0,
+	   pj_math_stat_get_stddev(&stat->rx_raw_jitter) / 1000.0,
+#endif
+#if defined(PJMEDIA_RTCP_STAT_HAS_IPDV) && PJMEDIA_RTCP_STAT_HAS_IPDV!=0
+	   indent,
+	   stat->rx_ipdv.min / 1000.0,
+	   stat->rx_ipdv.mean / 1000.0,
+	   stat->rx_ipdv.max / 1000.0,
+	   stat->rx_ipdv.last / 1000.0,
+	   pj_math_stat_get_stddev(&stat->rx_ipdv) / 1000.0,
+#endif
+	   ""
+	   );
+
+    if (len < 1 || len > end-p) {
+	*p = '\0';
+	return (p-buf);
+    }
+    p += len;
+
+    if (stat->tx.update_cnt == 0)
+	strcpy(last_update, "never");
+    else {
+	pj_gettimeofday(&now);
+	PJ_TIME_VAL_SUB(now, stat->tx.update);
+	sprintf(last_update, "%02ldh:%02ldm:%02ld.%03lds ago",
+		now.sec / 3600,
+		(now.sec % 3600) / 60,
+		now.sec % 60,
+		now.msec);
+    }
+
+    len = pj_ansi_snprintf(p, end-p,
+	   "%s     TX %s last update:%s\n"
+	   "%s        total %spkt %sB (%sB +IP hdr) @avg %sbps/%sbps\n"
+	   "%s        pkt loss=%d (%3.1f%%), dup=%d (%3.1f%%), reorder=%d (%3.1f%%)\n"
+	   "%s              (msec)    min     avg     max     last    dev \n"
+	   "%s        loss period: %7.3f %7.3f %7.3f %7.3f %7.3f\n"
+	   "%s        jitter     : %7.3f %7.3f %7.3f %7.3f %7.3f\n",
+	   indent,
+	   tx_info,
+	   last_update,
+
+	   indent,
+	   good_number(packets, stat->tx.pkt),
+	   good_number(bytes, stat->tx.bytes),
+	   good_number(ipbytes, stat->tx.bytes + stat->tx.pkt * 40),
+	   good_number(avg_bps, (pj_int32_t)((pj_int64_t)stat->tx.bytes * 8 * 1000 / PJ_TIME_VAL_MSEC(media_duration))),
+	   good_number(avg_ipbps, (pj_int32_t)(((pj_int64_t)stat->tx.bytes + stat->tx.pkt * 40) * 8 * 1000 / PJ_TIME_VAL_MSEC(media_duration))),
+
+	   indent,
+	   stat->tx.loss,
+	   (stat->tx.loss? stat->tx.loss * 100.0 / (stat->tx.pkt + stat->tx.loss) : 0),
+	   stat->tx.dup,
+	   (stat->tx.dup? stat->tx.dup * 100.0 / (stat->tx.pkt + stat->tx.loss) : 0),
+	   stat->tx.reorder,
+	   (stat->tx.reorder? stat->tx.reorder * 100.0 / (stat->tx.pkt + stat->tx.loss) : 0),
+
+	   indent, indent,
+	   stat->tx.loss_period.min / 1000.0,
+	   stat->tx.loss_period.mean / 1000.0,
+	   stat->tx.loss_period.max / 1000.0,
+	   stat->tx.loss_period.last / 1000.0,
+	   pj_math_stat_get_stddev(&stat->tx.loss_period) / 1000.0,
+	   indent,
+	   stat->tx.jitter.min / 1000.0,
+	   stat->tx.jitter.mean / 1000.0,
+	   stat->tx.jitter.max / 1000.0,
+	   stat->tx.jitter.last / 1000.0,
+	   pj_math_stat_get_stddev(&stat->tx.jitter) / 1000.0
+	   );
+
+    if (len < 1 || len > end-p) {
+	*p = '\0';
+	return (p-buf);
+    }
+    p += len;
+
+    len = pj_ansi_snprintf(p, end-p,
+	   "%s     RTT msec      : %7.3f %7.3f %7.3f %7.3f %7.3f\n",
+	   indent,
+	   stat->rtt.min / 1000.0,
+	   stat->rtt.mean / 1000.0,
+	   stat->rtt.max / 1000.0,
+	   stat->rtt.last / 1000.0,
+	   pj_math_stat_get_stddev(&stat->rtt) / 1000.0
+	   );
+    if (len < 1 || len > end-p) {
+	*p = '\0';
+	return (p-buf);
+    }
+    p += len;
+
+    return (p-buf);
+}
+
+
+/* Dump media session */
+static void dump_media_session(const char *indent,
+			       char *buf, unsigned maxlen,
+			       pjsua_call *call)
+{
+    unsigned i;
+    char *p = buf, *end = buf+maxlen;
+    int len;
+
+    for (i=0; i<call->med_cnt; ++i) {
+	pjsua_call_media *call_med = &call->media[i];
+	pjmedia_rtcp_stat stat;
+	pj_bool_t has_stat;
+	pjmedia_transport_info tp_info;
+	char rem_addr_buf[80];
+	char codec_info[32] = {'0'};
+	char rx_info[80] = {'\0'};
+	char tx_info[80] = {'\0'};
+	const char *rem_addr;
+	const char *dir_str;
+	const char *media_type_str;
+
+	switch (call_med->type) {
+	case PJMEDIA_TYPE_AUDIO:
+	    media_type_str = "audio";
+	    break;
+	case PJMEDIA_TYPE_VIDEO:
+	    media_type_str = "video";
+	    break;
+	case PJMEDIA_TYPE_APPLICATION:
+	    media_type_str = "application";
+	    break;
+	default:
+	    media_type_str = "unknown";
+	    break;
+	}
+
+	/* Check if the stream is deactivated */
+	if (call_med->tp == NULL ||
+	    (!call_med->strm.a.stream && !call_med->strm.v.stream))
+	{
+	    len = pj_ansi_snprintf(p, end-p,
+		      "%s #%d %s deactivated\n",
+		      indent, i, media_type_str);
+	    if (len < 1 || len > end-p) {
+		*p = '\0';
+		return;
+	    }
+
+	    p += len;
+	    continue;
+	}
+
+	pjmedia_transport_info_init(&tp_info);
+	pjmedia_transport_get_info(call_med->tp, &tp_info);
+
+	// rem_addr will contain actual address of RTP originator, instead of
+	// remote RTP address specified by stream which is fetched from the SDP.
+	// Please note that we are assuming only one stream per call.
+	//rem_addr = pj_sockaddr_print(&info.stream_info[i].rem_addr,
+	//			     rem_addr_buf, sizeof(rem_addr_buf), 3);
+	if (pj_sockaddr_has_addr(&tp_info.src_rtp_name)) {
+	    rem_addr = pj_sockaddr_print(&tp_info.src_rtp_name, rem_addr_buf,
+					 sizeof(rem_addr_buf), 3);
+	} else {
+	    pj_ansi_snprintf(rem_addr_buf, sizeof(rem_addr_buf), "-");
+	    rem_addr = rem_addr_buf;
+	}
+
+	if (call_med->dir == PJMEDIA_DIR_NONE) {
+	    /* To handle when the stream that is currently being paused
+	     * (http://trac.pjsip.org/repos/ticket/1079)
+	     */
+	    dir_str = "inactive";
+	} else if (call_med->dir == PJMEDIA_DIR_ENCODING)
+	    dir_str = "sendonly";
+	else if (call_med->dir == PJMEDIA_DIR_DECODING)
+	    dir_str = "recvonly";
+	else if (call_med->dir == PJMEDIA_DIR_ENCODING_DECODING)
+	    dir_str = "sendrecv";
+	else
+	    dir_str = "inactive";
+
+	if (call_med->type == PJMEDIA_TYPE_AUDIO) {
+	    pjmedia_stream *stream = call_med->strm.a.stream;
+	    pjmedia_stream_info info;
+
+	    pjmedia_stream_get_stat(stream, &stat);
+	    has_stat = PJ_TRUE;
+
+	    pjmedia_stream_get_info(stream, &info);
+	    pj_ansi_snprintf(codec_info, sizeof(codec_info), " %.*s @%dkHz",
+			     (int)info.fmt.encoding_name.slen,
+			     info.fmt.encoding_name.ptr,
+			     info.fmt.clock_rate / 1000);
+	    pj_ansi_snprintf(rx_info, sizeof(rx_info), "pt=%d,",
+			     info.fmt.pt);
+	    pj_ansi_snprintf(tx_info, sizeof(tx_info), "pt=%d, ptime=%d,",
+			     info.tx_pt,
+			     info.param->setting.frm_per_pkt*
+			     info.param->info.frm_ptime);
+	} else if (call_med->type == PJMEDIA_TYPE_VIDEO) {
+	    pjmedia_vid_stream *stream = call_med->strm.v.stream;
+	    pjmedia_vid_stream_info info;
+
+	    pjmedia_vid_stream_get_stat(stream, &stat);
+	    has_stat = PJ_TRUE;
+
+	    pjmedia_vid_stream_get_info(stream, &info);
+	    pj_ansi_snprintf(codec_info, sizeof(codec_info), " %.*s",
+	                     (int)info.codec_info.encoding_name.slen,
+			     info.codec_info.encoding_name.ptr);
+	    if (call_med->dir & PJMEDIA_DIR_DECODING) {
+		pjmedia_video_format_detail *vfd;
+		vfd = pjmedia_format_get_video_format_detail(
+					&info.codec_param->dec_fmt, PJ_TRUE);
+		pj_ansi_snprintf(rx_info, sizeof(rx_info),
+				 "pt=%d, size=%dx%d, fps=%.2f,",
+				 info.rx_pt,
+				 vfd->size.w, vfd->size.h,
+				 vfd->fps.num*1.0/vfd->fps.denum);
+	    }
+	    if (call_med->dir & PJMEDIA_DIR_ENCODING) {
+		pjmedia_video_format_detail *vfd;
+		vfd = pjmedia_format_get_video_format_detail(
+					&info.codec_param->enc_fmt, PJ_TRUE);
+		pj_ansi_snprintf(tx_info, sizeof(tx_info),
+				 "pt=%d, size=%dx%d, fps=%.2f,",
+				 info.tx_pt,
+				 vfd->size.w, vfd->size.h,
+				 vfd->fps.num*1.0/vfd->fps.denum);
+	    }
+	} else {
+	    has_stat = PJ_FALSE;
+	}
+
+	len = pj_ansi_snprintf(p, end-p,
+		  "%s  #%d %s%s, %s, peer=%s\n",
+		  indent,
+		  call_med->idx,
+		  media_type_str,
+		  codec_info,
+		  dir_str,
+		  rem_addr);
+	if (len < 1 || len > end-p) {
+	    *p = '\0';
+	    return;
+	}
+	p += len;
+
+	/* Get and ICE SRTP status */
+	if (call_med->tp) {
+	    pjmedia_transport_info tp_info;
+
+	    pjmedia_transport_info_init(&tp_info);
+	    pjmedia_transport_get_info(call_med->tp, &tp_info);
+	    if (tp_info.specific_info_cnt > 0) {
+		unsigned j;
+		for (j = 0; j < tp_info.specific_info_cnt; ++j) {
+		    if (tp_info.spc_info[j].type == PJMEDIA_TRANSPORT_TYPE_SRTP)
+		    {
+			pjmedia_srtp_info *srtp_info =
+				    (pjmedia_srtp_info*) tp_info.spc_info[j].buffer;
+
+			len = pj_ansi_snprintf(p, end-p,
+					       "   %s  SRTP status: %s Crypto-suite: %s",
+					       indent,
+					       (srtp_info->active?"Active":"Not active"),
+					       srtp_info->tx_policy.name.ptr);
+			if (len > 0 && len < end-p) {
+			    p += len;
+			    *p++ = '\n';
+			    *p = '\0';
+			}
+		    } else if (tp_info.spc_info[j].type==PJMEDIA_TRANSPORT_TYPE_ICE) {
+			const pjmedia_ice_transport_info *ii;
+
+			ii = (const pjmedia_ice_transport_info*)
+			     tp_info.spc_info[j].buffer;
+
+			len = pj_ansi_snprintf(p, end-p,
+					       "   %s  ICE role: %s, state: %s, comp_cnt: %u",
+					       indent,
+					       pj_ice_sess_role_name(ii->role),
+					       pj_ice_strans_state_name(ii->sess_state),
+					       ii->comp_cnt);
+			if (len > 0 && len < end-p) {
+			    p += len;
+			    *p++ = '\n';
+			    *p = '\0';
+			}
+		    }
+		}
+	    }
+	}
+
+
+	if (has_stat) {
+	    len = dump_media_stat(indent, p, end-p, &stat,
+				  rx_info, tx_info);
+	    p += len;
+	}
+
+#if defined(PJMEDIA_HAS_RTCP_XR) && (PJMEDIA_HAS_RTCP_XR != 0)
+#   define SAMPLES_TO_USEC(usec, samples, clock_rate) \
+	do { \
+	    if (samples <= 4294) \
+		usec = samples * 1000000 / clock_rate; \
+	    else { \
+		usec = samples * 1000 / clock_rate; \
+		usec *= 1000; \
+	    } \
+	} while(0)
+
+#   define PRINT_VOIP_MTC_VAL(s, v) \
+	if (v == 127) \
+	    sprintf(s, "(na)"); \
+	else \
+	    sprintf(s, "%d", v)
+
+#   define VALIDATE_PRINT_BUF() \
+	if (len < 1 || len > end-p) { *p = '\0'; return; } \
+	p += len; *p++ = '\n'; *p = '\0'
+
+
+	if (call_med->type == PJMEDIA_TYPE_AUDIO) {
+	    pjmedia_stream_info info;
+	    char last_update[64];
+	    char loss[16], dup[16];
+	    char jitter[80];
+	    char toh[80];
+	    char plc[16], jba[16], jbr[16];
+	    char signal_lvl[16], noise_lvl[16], rerl[16];
+	    char r_factor[16], ext_r_factor[16], mos_lq[16], mos_cq[16];
+	    pjmedia_rtcp_xr_stat xr_stat;
+	    unsigned clock_rate;
+	    pj_time_val now;
+
+	    if (pjmedia_stream_get_stat_xr(call_med->strm.a.stream,
+	                                   &xr_stat) != PJ_SUCCESS)
+	    {
+		continue;
+	    }
+
+	    if (pjmedia_stream_get_info(call_med->strm.a.stream, &info)
+		    != PJ_SUCCESS)
+	    {
+		continue;
+	    }
+
+	    clock_rate = info.fmt.clock_rate;
+	    pj_gettimeofday(&now);
+
+	    len = pj_ansi_snprintf(p, end-p, "\n%s  Extended reports:", indent);
+	    VALIDATE_PRINT_BUF();
+
+	    /* Statistics Summary */
+	    len = pj_ansi_snprintf(p, end-p, "%s   Statistics Summary", indent);
+	    VALIDATE_PRINT_BUF();
+
+	    if (xr_stat.rx.stat_sum.l)
+		sprintf(loss, "%d", xr_stat.rx.stat_sum.lost);
+	    else
+		sprintf(loss, "(na)");
+
+	    if (xr_stat.rx.stat_sum.d)
+		sprintf(dup, "%d", xr_stat.rx.stat_sum.dup);
+	    else
+		sprintf(dup, "(na)");
+
+	    if (xr_stat.rx.stat_sum.j) {
+		unsigned jmin, jmax, jmean, jdev;
+
+		SAMPLES_TO_USEC(jmin, xr_stat.rx.stat_sum.jitter.min,
+				clock_rate);
+		SAMPLES_TO_USEC(jmax, xr_stat.rx.stat_sum.jitter.max,
+				clock_rate);
+		SAMPLES_TO_USEC(jmean, xr_stat.rx.stat_sum.jitter.mean,
+				clock_rate);
+		SAMPLES_TO_USEC(jdev,
+			       pj_math_stat_get_stddev(&xr_stat.rx.stat_sum.jitter),
+			       clock_rate);
+		sprintf(jitter, "%7.3f %7.3f %7.3f %7.3f",
+			jmin/1000.0, jmean/1000.0, jmax/1000.0, jdev/1000.0);
+	    } else
+		sprintf(jitter, "(report not available)");
+
+	    if (xr_stat.rx.stat_sum.t) {
+		sprintf(toh, "%11d %11d %11d %11d",
+			xr_stat.rx.stat_sum.toh.min,
+			xr_stat.rx.stat_sum.toh.mean,
+			xr_stat.rx.stat_sum.toh.max,
+			pj_math_stat_get_stddev(&xr_stat.rx.stat_sum.toh));
+	    } else
+		sprintf(toh, "(report not available)");
+
+	    if (xr_stat.rx.stat_sum.update.sec == 0)
+		strcpy(last_update, "never");
+	    else {
+		pj_gettimeofday(&now);
+		PJ_TIME_VAL_SUB(now, xr_stat.rx.stat_sum.update);
+		sprintf(last_update, "%02ldh:%02ldm:%02ld.%03lds ago",
+			now.sec / 3600,
+			(now.sec % 3600) / 60,
+			now.sec % 60,
+			now.msec);
+	    }
+
+	    len = pj_ansi_snprintf(p, end-p,
+		    "%s     RX last update: %s\n"
+		    "%s        begin seq=%d, end seq=%d\n"
+		    "%s        pkt loss=%s, dup=%s\n"
+		    "%s              (msec)    min     avg     max     dev\n"
+		    "%s        jitter     : %s\n"
+		    "%s        toh        : %s",
+		    indent, last_update,
+		    indent,
+		    xr_stat.rx.stat_sum.begin_seq, xr_stat.rx.stat_sum.end_seq,
+		    indent, loss, dup,
+		    indent,
+		    indent, jitter,
+		    indent, toh
+		    );
+	    VALIDATE_PRINT_BUF();
+
+	    if (xr_stat.tx.stat_sum.l)
+		sprintf(loss, "%d", xr_stat.tx.stat_sum.lost);
+	    else
+		sprintf(loss, "(na)");
+
+	    if (xr_stat.tx.stat_sum.d)
+		sprintf(dup, "%d", xr_stat.tx.stat_sum.dup);
+	    else
+		sprintf(dup, "(na)");
+
+	    if (xr_stat.tx.stat_sum.j) {
+		unsigned jmin, jmax, jmean, jdev;
+
+		SAMPLES_TO_USEC(jmin, xr_stat.tx.stat_sum.jitter.min,
+				clock_rate);
+		SAMPLES_TO_USEC(jmax, xr_stat.tx.stat_sum.jitter.max,
+				clock_rate);
+		SAMPLES_TO_USEC(jmean, xr_stat.tx.stat_sum.jitter.mean,
+				clock_rate);
+		SAMPLES_TO_USEC(jdev,
+			       pj_math_stat_get_stddev(&xr_stat.tx.stat_sum.jitter),
+			       clock_rate);
+		sprintf(jitter, "%7.3f %7.3f %7.3f %7.3f",
+			jmin/1000.0, jmean/1000.0, jmax/1000.0, jdev/1000.0);
+	    } else
+		sprintf(jitter, "(report not available)");
+
+	    if (xr_stat.tx.stat_sum.t) {
+		sprintf(toh, "%11d %11d %11d %11d",
+			xr_stat.tx.stat_sum.toh.min,
+			xr_stat.tx.stat_sum.toh.mean,
+			xr_stat.tx.stat_sum.toh.max,
+			pj_math_stat_get_stddev(&xr_stat.rx.stat_sum.toh));
+	    } else
+		sprintf(toh,    "(report not available)");
+
+	    if (xr_stat.tx.stat_sum.update.sec == 0)
+		strcpy(last_update, "never");
+	    else {
+		pj_gettimeofday(&now);
+		PJ_TIME_VAL_SUB(now, xr_stat.tx.stat_sum.update);
+		sprintf(last_update, "%02ldh:%02ldm:%02ld.%03lds ago",
+			now.sec / 3600,
+			(now.sec % 3600) / 60,
+			now.sec % 60,
+			now.msec);
+	    }
+
+	    len = pj_ansi_snprintf(p, end-p,
+		    "%s     TX last update: %s\n"
+		    "%s        begin seq=%d, end seq=%d\n"
+		    "%s        pkt loss=%s, dup=%s\n"
+		    "%s              (msec)    min     avg     max     dev\n"
+		    "%s        jitter     : %s\n"
+		    "%s        toh        : %s",
+		    indent, last_update,
+		    indent,
+		    xr_stat.tx.stat_sum.begin_seq, xr_stat.tx.stat_sum.end_seq,
+		    indent, loss, dup,
+		    indent,
+		    indent, jitter,
+		    indent, toh
+		    );
+	    VALIDATE_PRINT_BUF();
+
+
+	    /* VoIP Metrics */
+	    len = pj_ansi_snprintf(p, end-p, "%s   VoIP Metrics", indent);
+	    VALIDATE_PRINT_BUF();
+
+	    PRINT_VOIP_MTC_VAL(signal_lvl, xr_stat.rx.voip_mtc.signal_lvl);
+	    PRINT_VOIP_MTC_VAL(noise_lvl, xr_stat.rx.voip_mtc.noise_lvl);
+	    PRINT_VOIP_MTC_VAL(rerl, xr_stat.rx.voip_mtc.rerl);
+	    PRINT_VOIP_MTC_VAL(r_factor, xr_stat.rx.voip_mtc.r_factor);
+	    PRINT_VOIP_MTC_VAL(ext_r_factor, xr_stat.rx.voip_mtc.ext_r_factor);
+	    PRINT_VOIP_MTC_VAL(mos_lq, xr_stat.rx.voip_mtc.mos_lq);
+	    PRINT_VOIP_MTC_VAL(mos_cq, xr_stat.rx.voip_mtc.mos_cq);
+
+	    switch ((xr_stat.rx.voip_mtc.rx_config>>6) & 3) {
+		case PJMEDIA_RTCP_XR_PLC_DIS:
+		    sprintf(plc, "DISABLED");
+		    break;
+		case PJMEDIA_RTCP_XR_PLC_ENH:
+		    sprintf(plc, "ENHANCED");
+		    break;
+		case PJMEDIA_RTCP_XR_PLC_STD:
+		    sprintf(plc, "STANDARD");
+		    break;
+		case PJMEDIA_RTCP_XR_PLC_UNK:
+		default:
+		    sprintf(plc, "UNKNOWN");
+		    break;
+	    }
+
+	    switch ((xr_stat.rx.voip_mtc.rx_config>>4) & 3) {
+		case PJMEDIA_RTCP_XR_JB_FIXED:
+		    sprintf(jba, "FIXED");
+		    break;
+		case PJMEDIA_RTCP_XR_JB_ADAPTIVE:
+		    sprintf(jba, "ADAPTIVE");
+		    break;
+		default:
+		    sprintf(jba, "UNKNOWN");
+		    break;
+	    }
+
+	    sprintf(jbr, "%d", xr_stat.rx.voip_mtc.rx_config & 0x0F);
+
+	    if (xr_stat.rx.voip_mtc.update.sec == 0)
+		strcpy(last_update, "never");
+	    else {
+		pj_gettimeofday(&now);
+		PJ_TIME_VAL_SUB(now, xr_stat.rx.voip_mtc.update);
+		sprintf(last_update, "%02ldh:%02ldm:%02ld.%03lds ago",
+			now.sec / 3600,
+			(now.sec % 3600) / 60,
+			now.sec % 60,
+			now.msec);
+	    }
+
+	    len = pj_ansi_snprintf(p, end-p,
+		    "%s     RX last update: %s\n"
+		    "%s        packets    : loss rate=%d (%.2f%%), discard rate=%d (%.2f%%)\n"
+		    "%s        burst      : density=%d (%.2f%%), duration=%d%s\n"
+		    "%s        gap        : density=%d (%.2f%%), duration=%d%s\n"
+		    "%s        delay      : round trip=%d%s, end system=%d%s\n"
+		    "%s        level      : signal=%s%s, noise=%s%s, RERL=%s%s\n"
+		    "%s        quality    : R factor=%s, ext R factor=%s\n"
+		    "%s                     MOS LQ=%s, MOS CQ=%s\n"
+		    "%s        config     : PLC=%s, JB=%s, JB rate=%s, Gmin=%d\n"
+		    "%s        JB delay   : cur=%d%s, max=%d%s, abs max=%d%s",
+		    indent,
+		    last_update,
+		    /* packets */
+		    indent,
+		    xr_stat.rx.voip_mtc.loss_rate, xr_stat.rx.voip_mtc.loss_rate*100.0/256,
+		    xr_stat.rx.voip_mtc.discard_rate, xr_stat.rx.voip_mtc.discard_rate*100.0/256,
+		    /* burst */
+		    indent,
+		    xr_stat.rx.voip_mtc.burst_den, xr_stat.rx.voip_mtc.burst_den*100.0/256,
+		    xr_stat.rx.voip_mtc.burst_dur, "ms",
+		    /* gap */
+		    indent,
+		    xr_stat.rx.voip_mtc.gap_den, xr_stat.rx.voip_mtc.gap_den*100.0/256,
+		    xr_stat.rx.voip_mtc.gap_dur, "ms",
+		    /* delay */
+		    indent,
+		    xr_stat.rx.voip_mtc.rnd_trip_delay, "ms",
+		    xr_stat.rx.voip_mtc.end_sys_delay, "ms",
+		    /* level */
+		    indent,
+		    signal_lvl, "dB",
+		    noise_lvl, "dB",
+		    rerl, "",
+		    /* quality */
+		    indent,
+		    r_factor, ext_r_factor,
+		    indent,
+		    mos_lq, mos_cq,
+		    /* config */
+		    indent,
+		    plc, jba, jbr, xr_stat.rx.voip_mtc.gmin,
+		    /* JB delay */
+		    indent,
+		    xr_stat.rx.voip_mtc.jb_nom, "ms",
+		    xr_stat.rx.voip_mtc.jb_max, "ms",
+		    xr_stat.rx.voip_mtc.jb_abs_max, "ms"
+		    );
+	    VALIDATE_PRINT_BUF();
+
+	    PRINT_VOIP_MTC_VAL(signal_lvl, xr_stat.tx.voip_mtc.signal_lvl);
+	    PRINT_VOIP_MTC_VAL(noise_lvl, xr_stat.tx.voip_mtc.noise_lvl);
+	    PRINT_VOIP_MTC_VAL(rerl, xr_stat.tx.voip_mtc.rerl);
+	    PRINT_VOIP_MTC_VAL(r_factor, xr_stat.tx.voip_mtc.r_factor);
+	    PRINT_VOIP_MTC_VAL(ext_r_factor, xr_stat.tx.voip_mtc.ext_r_factor);
+	    PRINT_VOIP_MTC_VAL(mos_lq, xr_stat.tx.voip_mtc.mos_lq);
+	    PRINT_VOIP_MTC_VAL(mos_cq, xr_stat.tx.voip_mtc.mos_cq);
+
+	    switch ((xr_stat.tx.voip_mtc.rx_config>>6) & 3) {
+		case PJMEDIA_RTCP_XR_PLC_DIS:
+		    sprintf(plc, "DISABLED");
+		    break;
+		case PJMEDIA_RTCP_XR_PLC_ENH:
+		    sprintf(plc, "ENHANCED");
+		    break;
+		case PJMEDIA_RTCP_XR_PLC_STD:
+		    sprintf(plc, "STANDARD");
+		    break;
+		case PJMEDIA_RTCP_XR_PLC_UNK:
+		default:
+		    sprintf(plc, "unknown");
+		    break;
+	    }
+
+	    switch ((xr_stat.tx.voip_mtc.rx_config>>4) & 3) {
+		case PJMEDIA_RTCP_XR_JB_FIXED:
+		    sprintf(jba, "FIXED");
+		    break;
+		case PJMEDIA_RTCP_XR_JB_ADAPTIVE:
+		    sprintf(jba, "ADAPTIVE");
+		    break;
+		default:
+		    sprintf(jba, "unknown");
+		    break;
+	    }
+
+	    sprintf(jbr, "%d", xr_stat.tx.voip_mtc.rx_config & 0x0F);
+
+	    if (xr_stat.tx.voip_mtc.update.sec == 0)
+		strcpy(last_update, "never");
+	    else {
+		pj_gettimeofday(&now);
+		PJ_TIME_VAL_SUB(now, xr_stat.tx.voip_mtc.update);
+		sprintf(last_update, "%02ldh:%02ldm:%02ld.%03lds ago",
+			now.sec / 3600,
+			(now.sec % 3600) / 60,
+			now.sec % 60,
+			now.msec);
+	    }
+
+	    len = pj_ansi_snprintf(p, end-p,
+		    "%s     TX last update: %s\n"
+		    "%s        packets    : loss rate=%d (%.2f%%), discard rate=%d (%.2f%%)\n"
+		    "%s        burst      : density=%d (%.2f%%), duration=%d%s\n"
+		    "%s        gap        : density=%d (%.2f%%), duration=%d%s\n"
+		    "%s        delay      : round trip=%d%s, end system=%d%s\n"
+		    "%s        level      : signal=%s%s, noise=%s%s, RERL=%s%s\n"
+		    "%s        quality    : R factor=%s, ext R factor=%s\n"
+		    "%s                     MOS LQ=%s, MOS CQ=%s\n"
+		    "%s        config     : PLC=%s, JB=%s, JB rate=%s, Gmin=%d\n"
+		    "%s        JB delay   : cur=%d%s, max=%d%s, abs max=%d%s",
+		    indent,
+		    last_update,
+		    /* pakcets */
+		    indent,
+		    xr_stat.tx.voip_mtc.loss_rate, xr_stat.tx.voip_mtc.loss_rate*100.0/256,
+		    xr_stat.tx.voip_mtc.discard_rate, xr_stat.tx.voip_mtc.discard_rate*100.0/256,
+		    /* burst */
+		    indent,
+		    xr_stat.tx.voip_mtc.burst_den, xr_stat.tx.voip_mtc.burst_den*100.0/256,
+		    xr_stat.tx.voip_mtc.burst_dur, "ms",
+		    /* gap */
+		    indent,
+		    xr_stat.tx.voip_mtc.gap_den, xr_stat.tx.voip_mtc.gap_den*100.0/256,
+		    xr_stat.tx.voip_mtc.gap_dur, "ms",
+		    /* delay */
+		    indent,
+		    xr_stat.tx.voip_mtc.rnd_trip_delay, "ms",
+		    xr_stat.tx.voip_mtc.end_sys_delay, "ms",
+		    /* level */
+		    indent,
+		    signal_lvl, "dB",
+		    noise_lvl, "dB",
+		    rerl, "",
+		    /* quality */
+		    indent,
+		    r_factor, ext_r_factor,
+		    indent,
+		    mos_lq, mos_cq,
+		    /* config */
+		    indent,
+		    plc, jba, jbr, xr_stat.tx.voip_mtc.gmin,
+		    /* JB delay */
+		    indent,
+		    xr_stat.tx.voip_mtc.jb_nom, "ms",
+		    xr_stat.tx.voip_mtc.jb_max, "ms",
+		    xr_stat.tx.voip_mtc.jb_abs_max, "ms"
+		    );
+	    VALIDATE_PRINT_BUF();
+
+
+	    /* RTT delay (by receiver side) */
+	    len = pj_ansi_snprintf(p, end-p,
+		    "%s   RTT (from recv)      min     avg     max     last    dev",
+		    indent);
+	    VALIDATE_PRINT_BUF();
+	    len = pj_ansi_snprintf(p, end-p,
+		    "%s     RTT msec      : %7.3f %7.3f %7.3f %7.3f %7.3f",
+		    indent,
+		    xr_stat.rtt.min / 1000.0,
+		    xr_stat.rtt.mean / 1000.0,
+		    xr_stat.rtt.max / 1000.0,
+		    xr_stat.rtt.last / 1000.0,
+		    pj_math_stat_get_stddev(&xr_stat.rtt) / 1000.0
+		   );
+	    VALIDATE_PRINT_BUF();
+	} /* if audio */;
+#endif
+
+    }
+}
+
+
+/* Print call info */
+void print_call(const char *title,
+	        int call_id,
+	        char *buf, pj_size_t size)
+{
+    int len;
+    pjsip_inv_session *inv = pjsua_var.calls[call_id].inv;
+    pjsip_dialog *dlg = inv->dlg;
+    char userinfo[128];
+
+    /* Dump invite sesion info. */
+
+    len = pjsip_hdr_print_on(dlg->remote.info, userinfo, sizeof(userinfo));
+    if (len < 0)
+	pj_ansi_strcpy(userinfo, "<--uri too long-->");
+    else
+	userinfo[len] = '\0';
+
+    len = pj_ansi_snprintf(buf, size, "%s[%s] %s",
+			   title,
+			   pjsip_inv_state_name(inv->state),
+			   userinfo);
+    if (len < 1 || len >= (int)size) {
+	pj_ansi_strcpy(buf, "<--uri too long-->");
+	len = 18;
+    } else
+	buf[len] = '\0';
+}
+
+
+/*
+ * Dump call and media statistics to string.
+ */
+PJ_DEF(pj_status_t) pjsua_call_dump( pjsua_call_id call_id,
+				     pj_bool_t with_media,
+				     char *buffer,
+				     unsigned maxlen,
+				     const char *indent)
+{
+    pjsua_call *call;
+    pjsip_dialog *dlg;
+    pj_time_val duration, res_delay, con_delay;
+    char tmp[128];
+    char *p, *end;
+    pj_status_t status;
+    int len;
+
+    PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls,
+		     PJ_EINVAL);
+
+    status = acquire_call("pjsua_call_dump()", call_id, &call, &dlg);
+    if (status != PJ_SUCCESS)
+	return status;
+
+    *buffer = '\0';
+    p = buffer;
+    end = buffer + maxlen;
+    len = 0;
+
+    print_call(indent, call_id, tmp, sizeof(tmp));
+
+    len = pj_ansi_strlen(tmp);
+    pj_ansi_strcpy(buffer, tmp);
+
+    p += len;
+    *p++ = '\r';
+    *p++ = '\n';
+
+    /* Calculate call duration */
+    if (call->conn_time.sec != 0) {
+	pj_gettimeofday(&duration);
+	PJ_TIME_VAL_SUB(duration, call->conn_time);
+	con_delay = call->conn_time;
+	PJ_TIME_VAL_SUB(con_delay, call->start_time);
+    } else {
+	duration.sec = duration.msec = 0;
+	con_delay.sec = con_delay.msec = 0;
+    }
+
+    /* Calculate first response delay */
+    if (call->res_time.sec != 0) {
+	res_delay = call->res_time;
+	PJ_TIME_VAL_SUB(res_delay, call->start_time);
+    } else {
+	res_delay.sec = res_delay.msec = 0;
+    }
+
+    /* Print duration */
+    len = pj_ansi_snprintf(p, end-p,
+		           "%s  Call time: %02dh:%02dm:%02ds, "
+		           "1st res in %d ms, conn in %dms",
+			   indent,
+		           (int)(duration.sec / 3600),
+		           (int)((duration.sec % 3600)/60),
+		           (int)(duration.sec % 60),
+		           (int)PJ_TIME_VAL_MSEC(res_delay),
+		           (int)PJ_TIME_VAL_MSEC(con_delay));
+
+    if (len > 0 && len < end-p) {
+	p += len;
+	*p++ = '\n';
+	*p = '\0';
+    }
+
+    /* Dump session statistics */
+    if (with_media && pjsua_call_has_media(call_id))
+	dump_media_session(indent, p, end-p, call);
+
+    pjsip_dlg_dec_lock(dlg);
+
+    return PJ_SUCCESS;
+}
+