diff options
Diffstat (limited to 'pjsip-apps/src/samples')
37 files changed, 19126 insertions, 0 deletions
diff --git a/pjsip-apps/src/samples/aectest.c b/pjsip-apps/src/samples/aectest.c new file mode 100644 index 0000000..ff95db4 --- /dev/null +++ b/pjsip-apps/src/samples/aectest.c @@ -0,0 +1,304 @@ +/* $Id: aectest.c 3664 2011-07-19 03:42:28Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 + */ + + +/** + * \page page_pjmedia_samples_aectest_c Samples: AEC Test (aectest.c) + * + * Play a file to speaker, run AEC, and record the microphone input + * to see if echo is coming. + * + * This file is pjsip-apps/src/samples/aectest.c + * + * \includelineno aectest.c + */ +#include <pjmedia.h> +#include <pjlib-util.h> /* pj_getopt */ +#include <pjlib.h> + +#define THIS_FILE "aectest.c" +#define PTIME 20 +#define TAIL_LENGTH 200 + +static const char *desc = +" FILE \n" +" \n" +" aectest.c \n" +" \n" +" PURPOSE \n" +" \n" +" Test the AEC effectiveness. \n" +" \n" +" USAGE \n" +" \n" +" aectest [options] <PLAY.WAV> <REC.WAV> <OUTPUT.WAV> \n" +" \n" +" <PLAY.WAV> is the signal played to the speaker. \n" +" <REC.WAV> is the signal captured from the microphone. \n" +" <OUTPUT.WAV> is the output file to store the test result \n" +"\n" +" options:\n" +" -d The delay between playback and capture in ms, at least 25 ms.\n" +" Default is 25 ms. See note below. \n" +" -l Set the echo tail length in ms. Default is 200 ms \n" +" -r Set repeat count (default=1) \n" +" -a Algorithm: 0=default, 1=speex, 3=echo suppress \n" +" -i Interactive \n" +"\n" +" Note that for the AEC internal buffering mechanism, it is required\n" +" that the echoed signal (in REC.WAV) is delayed from the \n" +" corresponding reference signal (in PLAY.WAV) at least as much as \n" +" frame time + PJMEDIA_WSOLA_DELAY_MSEC. In this application, frame \n" +" time is 20 ms and default PJMEDIA_WSOLA_DELAY_MSEC is 5 ms, hence \n" +" 25 ms delay is the minimum value. \n"; + +/* + * Sample session: + * + * -d 100 -a 1 ../bin/orig8.wav ../bin/echo8.wav ../bin/result8.wav + */ + +static void app_perror(const char *sender, const char *title, pj_status_t st) +{ + char errmsg[PJ_ERR_MSG_SIZE]; + + pj_strerror(st, errmsg, sizeof(errmsg)); + PJ_LOG(3,(sender, "%s: %s", title, errmsg)); +} + + +/* + * main() + */ +int main(int argc, char *argv[]) +{ + pj_caching_pool cp; + pjmedia_endpt *med_endpt; + pj_pool_t *pool; + pjmedia_port *wav_play; + pjmedia_port *wav_rec; + pjmedia_port *wav_out; + pj_status_t status; + pjmedia_echo_state *ec; + pjmedia_frame play_frame, rec_frame; + unsigned opt = 0; + unsigned latency_ms = 25; + unsigned tail_ms = TAIL_LENGTH; + pj_timestamp t0, t1; + int i, repeat=1, interactive=0, c; + + pj_optind = 0; + while ((c=pj_getopt(argc, argv, "d:l:a:r:i")) !=-1) { + switch (c) { + case 'd': + latency_ms = atoi(pj_optarg); + if (latency_ms < 25) { + puts("Invalid delay"); + puts(desc); + } + break; + case 'l': + tail_ms = atoi(pj_optarg); + break; + case 'a': + { + int alg = atoi(pj_optarg); + switch (alg) { + case 0: + opt = 0; + case 1: + opt = PJMEDIA_ECHO_SPEEX; + break; + case 3: + opt = PJMEDIA_ECHO_SIMPLE; + break; + default: + puts("Invalid algorithm"); + puts(desc); + return 1; + } + } + break; + case 'r': + repeat = atoi(pj_optarg); + if (repeat < 1) { + puts("Invalid repeat count"); + puts(desc); + return 1; + } + break; + case 'i': + interactive = 1; + break; + } + } + + if (argc - pj_optind != 3) { + puts("Error: missing argument(s)"); + puts(desc); + return 1; + } + + /* Must init PJLIB first: */ + status = pj_init(); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* Must create a pool factory before we can allocate any memory. */ + pj_caching_pool_init(&cp, &pj_pool_factory_default_policy, 0); + + /* + * Initialize media endpoint. + * This will implicitly initialize PJMEDIA too. + */ + status = pjmedia_endpt_create(&cp.factory, NULL, 1, &med_endpt); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* Create memory pool for our file player */ + pool = pj_pool_create( &cp.factory, /* pool factory */ + "wav", /* pool name. */ + 4000, /* init size */ + 4000, /* increment size */ + NULL /* callback on error */ + ); + + /* Open wav_play */ + status = pjmedia_wav_player_port_create(pool, argv[pj_optind], PTIME, + PJMEDIA_FILE_NO_LOOP, 0, + &wav_play); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Error opening playback WAV file", status); + return 1; + } + + /* Open recorded wav */ + status = pjmedia_wav_player_port_create(pool, argv[pj_optind+1], PTIME, + PJMEDIA_FILE_NO_LOOP, 0, + &wav_rec); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Error opening recorded WAV file", status); + return 1; + } + + /* play and rec WAVs must have the same clock rate */ + if (PJMEDIA_PIA_SRATE(&wav_play->info) != PJMEDIA_PIA_SRATE(&wav_rec->info)) { + puts("Error: clock rate mismatch in the WAV files"); + return 1; + } + + /* .. and channel count */ + if (PJMEDIA_PIA_CCNT(&wav_play->info) != PJMEDIA_PIA_CCNT(&wav_rec->info)) { + puts("Error: clock rate mismatch in the WAV files"); + return 1; + } + + /* Create output wav */ + status = pjmedia_wav_writer_port_create(pool, argv[pj_optind+2], + PJMEDIA_PIA_SRATE(&wav_play->info), + PJMEDIA_PIA_CCNT(&wav_play->info), + PJMEDIA_PIA_SPF(&wav_play->info), + PJMEDIA_PIA_BITS(&wav_play->info), + 0, 0, &wav_out); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Error opening output WAV file", status); + return 1; + } + + /* Create echo canceller */ + status = pjmedia_echo_create2(pool, PJMEDIA_PIA_SRATE(&wav_play->info), + PJMEDIA_PIA_CCNT(&wav_play->info), + PJMEDIA_PIA_SPF(&wav_play->info), + tail_ms, latency_ms, + opt, &ec); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Error creating EC", status); + return 1; + } + + + /* Processing loop */ + play_frame.buf = pj_pool_alloc(pool, PJMEDIA_PIA_SPF(&wav_play->info)<<1); + rec_frame.buf = pj_pool_alloc(pool, PJMEDIA_PIA_SPF(&wav_play->info)<<1); + pj_get_timestamp(&t0); + for (i=0; i < repeat; ++i) { + for (;;) { + play_frame.size = PJMEDIA_PIA_SPF(&wav_play->info) << 1; + status = pjmedia_port_get_frame(wav_play, &play_frame); + if (status != PJ_SUCCESS) + break; + + status = pjmedia_echo_playback(ec, (short*)play_frame.buf); + + rec_frame.size = PJMEDIA_PIA_SPF(&wav_play->info) << 1; + status = pjmedia_port_get_frame(wav_rec, &rec_frame); + if (status != PJ_SUCCESS) + break; + + status = pjmedia_echo_capture(ec, (short*)rec_frame.buf, 0); + + //status = pjmedia_echo_cancel(ec, (short*)rec_frame.buf, + // (short*)play_frame.buf, 0, NULL); + + pjmedia_port_put_frame(wav_out, &rec_frame); + } + + pjmedia_wav_player_port_set_pos(wav_play, 0); + pjmedia_wav_player_port_set_pos(wav_rec, 0); + } + pj_get_timestamp(&t1); + + i = pjmedia_wav_writer_port_get_pos(wav_out) / sizeof(pj_int16_t) * 1000 / + (PJMEDIA_PIA_SRATE(&wav_out->info) * PJMEDIA_PIA_CCNT(&wav_out->info)); + PJ_LOG(3,(THIS_FILE, "Processed %3d.%03ds audio", + i / 1000, i % 1000)); + PJ_LOG(3,(THIS_FILE, "Completed in %u msec\n", pj_elapsed_msec(&t0, &t1))); + + /* Destroy file port(s) */ + status = pjmedia_port_destroy( wav_play ); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + status = pjmedia_port_destroy( wav_rec ); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + status = pjmedia_port_destroy( wav_out ); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* Destroy ec */ + pjmedia_echo_destroy(ec); + + /* Release application pool */ + pj_pool_release( pool ); + + /* Destroy media endpoint. */ + pjmedia_endpt_destroy( med_endpt ); + + /* Destroy pool factory */ + pj_caching_pool_destroy( &cp ); + + /* Shutdown PJLIB */ + pj_shutdown(); + + if (interactive) { + char s[10], *dummy; + puts("ENTER to quit"); + dummy = fgets(s, sizeof(s), stdin); + } + + /* Done. */ + return 0; +} + diff --git a/pjsip-apps/src/samples/auddemo.c b/pjsip-apps/src/samples/auddemo.c new file mode 100644 index 0000000..c4e3562 --- /dev/null +++ b/pjsip-apps/src/samples/auddemo.c @@ -0,0 +1,582 @@ +/* $Id: auddemo.c 3664 2011-07-19 03:42:28Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 <pjmedia-audiodev/audiodev.h> +#include <pjmedia-audiodev/audiotest.h> +#include <pjmedia.h> +#include <pjlib.h> +#include <pjlib-util.h> + +#define THIS_FILE "auddemo.c" +#define MAX_DEVICES 64 +#define WAV_FILE "auddemo.wav" + + +static unsigned dev_count; +static unsigned playback_lat = PJMEDIA_SND_DEFAULT_PLAY_LATENCY; +static unsigned capture_lat = PJMEDIA_SND_DEFAULT_REC_LATENCY; + +static void app_perror(const char *title, pj_status_t status) +{ + char errmsg[PJ_ERR_MSG_SIZE]; + + pj_strerror(status, errmsg, sizeof(errmsg)); + printf( "%s: %s (err=%d)\n", + title, errmsg, status); +} + +static void list_devices(void) +{ + unsigned i; + pj_status_t status; + + dev_count = pjmedia_aud_dev_count(); + if (dev_count == 0) { + PJ_LOG(3,(THIS_FILE, "No devices found")); + return; + } + + PJ_LOG(3,(THIS_FILE, "Found %d devices:", dev_count)); + + for (i=0; i<dev_count; ++i) { + pjmedia_aud_dev_info info; + + status = pjmedia_aud_dev_get_info(i, &info); + if (status != PJ_SUCCESS) + continue; + + PJ_LOG(3,(THIS_FILE," %2d: %s [%s] (%d/%d)", + i, info.driver, info.name, info.input_count, info.output_count)); + } +} + +static const char *decode_caps(unsigned caps) +{ + static char text[200]; + unsigned i; + + text[0] = '\0'; + + for (i=0; i<31; ++i) { + if ((1 << i) & caps) { + const char *capname; + capname = pjmedia_aud_dev_cap_name((pjmedia_aud_dev_cap)(1 << i), + NULL); + strcat(text, capname); + strcat(text, " "); + } + } + + return text; +} + +static void show_dev_info(unsigned index) +{ +#define H "%-20s" + pjmedia_aud_dev_info info; + char formats[200]; + pj_status_t status; + + if (index >= dev_count) { + PJ_LOG(1,(THIS_FILE, "Error: invalid index %u", index)); + return; + } + + status = pjmedia_aud_dev_get_info(index, &info); + if (status != PJ_SUCCESS) { + app_perror("pjmedia_aud_dev_get_info() error", status); + return; + } + + PJ_LOG(3, (THIS_FILE, "Device at index %u:", index)); + PJ_LOG(3, (THIS_FILE, "-------------------------")); + + PJ_LOG(3, (THIS_FILE, H": %u (0x%x)", "ID", index, index)); + PJ_LOG(3, (THIS_FILE, H": %s", "Name", info.name)); + PJ_LOG(3, (THIS_FILE, H": %s", "Driver", info.driver)); + PJ_LOG(3, (THIS_FILE, H": %u", "Input channels", info.input_count)); + PJ_LOG(3, (THIS_FILE, H": %u", "Output channels", info.output_count)); + PJ_LOG(3, (THIS_FILE, H": %s", "Capabilities", decode_caps(info.caps))); + + formats[0] = '\0'; + if (info.caps & PJMEDIA_AUD_DEV_CAP_EXT_FORMAT) { + unsigned i; + + for (i=0; i<info.ext_fmt_cnt; ++i) { + char bitrate[32]; + + switch (info.ext_fmt[i].id) { + case PJMEDIA_FORMAT_L16: + strcat(formats, "L16/"); + break; + case PJMEDIA_FORMAT_PCMA: + strcat(formats, "PCMA/"); + break; + case PJMEDIA_FORMAT_PCMU: + strcat(formats, "PCMU/"); + break; + case PJMEDIA_FORMAT_AMR: + strcat(formats, "AMR/"); + break; + case PJMEDIA_FORMAT_G729: + strcat(formats, "G729/"); + break; + case PJMEDIA_FORMAT_ILBC: + strcat(formats, "ILBC/"); + break; + default: + strcat(formats, "unknown/"); + break; + } + sprintf(bitrate, "%u", info.ext_fmt[i].det.aud.avg_bps); + strcat(formats, bitrate); + strcat(formats, " "); + } + } + PJ_LOG(3, (THIS_FILE, H": %s", "Extended formats", formats)); + +#undef H +} + +static void test_device(pjmedia_dir dir, unsigned rec_id, unsigned play_id, + unsigned clock_rate, unsigned ptime, + unsigned chnum) +{ + pjmedia_aud_param param; + pjmedia_aud_test_results result; + pj_status_t status; + + if (dir & PJMEDIA_DIR_CAPTURE) { + status = pjmedia_aud_dev_default_param(rec_id, ¶m); + } else { + status = pjmedia_aud_dev_default_param(play_id, ¶m); + } + + if (status != PJ_SUCCESS) { + app_perror("pjmedia_aud_dev_default_param()", status); + return; + } + + param.dir = dir; + param.rec_id = rec_id; + param.play_id = play_id; + param.clock_rate = clock_rate; + param.channel_count = chnum; + param.samples_per_frame = clock_rate * chnum * ptime / 1000; + + /* Latency settings */ + param.flags |= (PJMEDIA_AUD_DEV_CAP_INPUT_LATENCY | + PJMEDIA_AUD_DEV_CAP_OUTPUT_LATENCY); + param.input_latency_ms = capture_lat; + param.output_latency_ms = playback_lat; + + PJ_LOG(3,(THIS_FILE, "Performing test..")); + + status = pjmedia_aud_test(¶m, &result); + if (status != PJ_SUCCESS) { + app_perror("Test has completed with error", status); + return; + } + + PJ_LOG(3,(THIS_FILE, "Done. Result:")); + + if (dir & PJMEDIA_DIR_CAPTURE) { + if (result.rec.frame_cnt==0) { + PJ_LOG(1,(THIS_FILE, "Error: no frames captured!")); + } else { + PJ_LOG(3,(THIS_FILE, " %-20s: interval (min/max/avg/dev)=%u/%u/%u/%u, burst=%u", + "Recording result", + result.rec.min_interval, + result.rec.max_interval, + result.rec.avg_interval, + result.rec.dev_interval, + result.rec.max_burst)); + } + } + + if (dir & PJMEDIA_DIR_PLAYBACK) { + if (result.play.frame_cnt==0) { + PJ_LOG(1,(THIS_FILE, "Error: no playback!")); + } else { + PJ_LOG(3,(THIS_FILE, " %-20s: interval (min/max/avg/dev)=%u/%u/%u/%u, burst=%u", + "Playback result", + result.play.min_interval, + result.play.max_interval, + result.play.avg_interval, + result.play.dev_interval, + result.play.max_burst)); + } + } + + if (dir==PJMEDIA_DIR_CAPTURE_PLAYBACK) { + if (result.rec_drift_per_sec == 0) { + PJ_LOG(3,(THIS_FILE, " No clock drift detected")); + } else { + const char *which = result.rec_drift_per_sec>=0 ? "faster" : "slower"; + unsigned drift = result.rec_drift_per_sec>=0 ? + result.rec_drift_per_sec : + -result.rec_drift_per_sec; + + PJ_LOG(3,(THIS_FILE, " Clock drifts detected. Capture device " + "is running %d samples per second %s " + "than the playback device", + drift, which)); + } + } +} + + +static pj_status_t wav_rec_cb(void *user_data, pjmedia_frame *frame) +{ + return pjmedia_port_put_frame((pjmedia_port*)user_data, frame); +} + +static void record(unsigned rec_index, const char *filename) +{ + pj_pool_t *pool = NULL; + pjmedia_port *wav = NULL; + pjmedia_aud_param param; + pjmedia_aud_stream *strm = NULL; + char line[10], *dummy; + pj_status_t status; + + if (filename == NULL) + filename = WAV_FILE; + + pool = pj_pool_create(pjmedia_aud_subsys_get_pool_factory(), "wav", + 1000, 1000, NULL); + + status = pjmedia_wav_writer_port_create(pool, filename, 16000, + 1, 320, 16, 0, 0, &wav); + if (status != PJ_SUCCESS) { + app_perror("Error creating WAV file", status); + goto on_return; + } + + status = pjmedia_aud_dev_default_param(rec_index, ¶m); + if (status != PJ_SUCCESS) { + app_perror("pjmedia_aud_dev_default_param()", status); + goto on_return; + } + + param.dir = PJMEDIA_DIR_CAPTURE; + param.clock_rate = PJMEDIA_PIA_SRATE(&wav->info); + param.samples_per_frame = PJMEDIA_PIA_SPF(&wav->info); + param.channel_count = PJMEDIA_PIA_CCNT(&wav->info); + param.bits_per_sample = PJMEDIA_PIA_BITS(&wav->info); + + status = pjmedia_aud_stream_create(¶m, &wav_rec_cb, NULL, wav, + &strm); + if (status != PJ_SUCCESS) { + app_perror("Error opening the sound device", status); + goto on_return; + } + + status = pjmedia_aud_stream_start(strm); + if (status != PJ_SUCCESS) { + app_perror("Error starting the sound device", status); + goto on_return; + } + + PJ_LOG(3,(THIS_FILE, "Recording started, press ENTER to stop")); + dummy = fgets(line, sizeof(line), stdin); + +on_return: + if (strm) { + pjmedia_aud_stream_stop(strm); + pjmedia_aud_stream_destroy(strm); + } + if (wav) + pjmedia_port_destroy(wav); + if (pool) + pj_pool_release(pool); +} + + +static pj_status_t wav_play_cb(void *user_data, pjmedia_frame *frame) +{ + return pjmedia_port_get_frame((pjmedia_port*)user_data, frame); +} + + +static void play_file(unsigned play_index, const char *filename) +{ + pj_pool_t *pool = NULL; + pjmedia_port *wav = NULL; + pjmedia_aud_param param; + pjmedia_aud_stream *strm = NULL; + char line[10], *dummy; + pj_status_t status; + + if (filename == NULL) + filename = WAV_FILE; + + pool = pj_pool_create(pjmedia_aud_subsys_get_pool_factory(), "wav", + 1000, 1000, NULL); + + status = pjmedia_wav_player_port_create(pool, filename, 20, 0, 0, &wav); + if (status != PJ_SUCCESS) { + app_perror("Error opening WAV file", status); + goto on_return; + } + + status = pjmedia_aud_dev_default_param(play_index, ¶m); + if (status != PJ_SUCCESS) { + app_perror("pjmedia_aud_dev_default_param()", status); + goto on_return; + } + + param.dir = PJMEDIA_DIR_PLAYBACK; + param.clock_rate = PJMEDIA_PIA_SRATE(&wav->info); + param.samples_per_frame = PJMEDIA_PIA_SPF(&wav->info); + param.channel_count = PJMEDIA_PIA_CCNT(&wav->info); + param.bits_per_sample = PJMEDIA_PIA_BITS(&wav->info); + + status = pjmedia_aud_stream_create(¶m, NULL, &wav_play_cb, wav, + &strm); + if (status != PJ_SUCCESS) { + app_perror("Error opening the sound device", status); + goto on_return; + } + + status = pjmedia_aud_stream_start(strm); + if (status != PJ_SUCCESS) { + app_perror("Error starting the sound device", status); + goto on_return; + } + + PJ_LOG(3,(THIS_FILE, "Playback started, press ENTER to stop")); + dummy = fgets(line, sizeof(line), stdin); + +on_return: + if (strm) { + pjmedia_aud_stream_stop(strm); + pjmedia_aud_stream_destroy(strm); + } + if (wav) + pjmedia_port_destroy(wav); + if (pool) + pj_pool_release(pool); +} + + +static void print_menu(void) +{ + puts(""); + puts("Audio demo menu:"); + puts("-------------------------------"); + puts(" l List devices"); + puts(" R Refresh devices"); + puts(" i ID Show device info for device ID"); + puts(" t RID PID CR PTIM [CH] Perform test on the device:"); + puts(" RID: record device ID (-1 for no)"); + puts(" PID: playback device ID (-1 for no)"); + puts(" CR: clock rate"); + puts(" PTIM: ptime in ms"); + puts(" CH: # of channels"); + puts(" r RID [FILE] Record capture device RID to WAV file"); + puts(" p PID [FILE] Playback WAV file to device ID PID"); + puts(" d [RLAT PLAT] Get/set sound device latencies (in ms):"); + puts(" Specify no param to get current latencies setting"); + puts(" RLAT: record latency (-1 for default)"); + puts(" PLAT: playback latency (-1 for default)"); + puts(" v Toggle log verbosity"); + puts(" q Quit"); + puts(""); + printf("Enter selection: "); + fflush(stdout); +} + +int main() +{ + pj_caching_pool cp; + pj_bool_t done = PJ_FALSE; + pj_status_t status; + + /* Init pjlib */ + status = pj_init(); + PJ_ASSERT_RETURN(status==PJ_SUCCESS, 1); + + pj_log_set_decor(PJ_LOG_HAS_NEWLINE); + + /* Must create a pool factory before we can allocate any memory. */ + pj_caching_pool_init(&cp, &pj_pool_factory_default_policy, 0); + + status = pjmedia_aud_subsys_init(&cp.factory); + if (status != PJ_SUCCESS) { + app_perror("pjmedia_aud_subsys_init()", status); + pj_caching_pool_destroy(&cp); + pj_shutdown(); + return 1; + } + + list_devices(); + + while (!done) { + char line[80]; + + print_menu(); + + if (fgets(line, sizeof(line), stdin)==NULL) + break; + + switch (line[0]) { + case 'l': + list_devices(); + break; + + case 'R': + pjmedia_aud_dev_refresh(); + puts("Audio device list refreshed."); + break; + + case 'i': + { + unsigned dev_index; + if (sscanf(line+2, "%u", &dev_index) != 1) { + puts("error: device ID required"); + break; + } + show_dev_info(dev_index); + } + break; + + case 't': + { + pjmedia_dir dir; + int rec_id, play_id; + unsigned clock_rate, ptime, chnum; + int cnt; + + cnt = sscanf(line+2, "%d %d %u %u %u", &rec_id, &play_id, + &clock_rate, &ptime, &chnum); + if (cnt < 4) { + puts("error: not enough parameters"); + break; + } + if (clock_rate < 8000 || clock_rate > 128000) { + puts("error: invalid clock rate"); + break; + } + if (ptime < 10 || ptime > 500) { + puts("error: invalid ptime"); + break; + } + if (cnt==5) { + if (chnum < 1 || chnum > 4) { + puts("error: invalid number of channels"); + break; + } + } else { + chnum = 1; + } + + if (rec_id >= 0 && rec_id < (int)dev_count) { + if (play_id >= 0 && play_id < (int)dev_count) + dir = PJMEDIA_DIR_CAPTURE_PLAYBACK; + else + dir = PJMEDIA_DIR_CAPTURE; + } else if (play_id >= 0 && play_id < (int)dev_count) { + dir = PJMEDIA_DIR_PLAYBACK; + } else { + puts("error: at least one valid device index required"); + break; + } + + test_device(dir, rec_id, play_id, clock_rate, ptime, chnum); + + } + break; + + case 'r': + /* record */ + { + int index; + char filename[80]; + int count; + + count = sscanf(line+2, "%d %s", &index, filename); + if (count==1) + record(index, NULL); + else if (count==2) + record(index, filename); + else + puts("error: invalid command syntax"); + } + break; + + case 'p': + /* playback */ + { + int index; + char filename[80]; + int count; + + count = sscanf(line+2, "%d %s", &index, filename); + if (count==1) + play_file(index, NULL); + else if (count==2) + play_file(index, filename); + else + puts("error: invalid command syntax"); + } + break; + + case 'd': + /* latencies */ + { + int rec_lat, play_lat; + + if (sscanf(line+2, "%d %d", &rec_lat, &play_lat) == 2) { + capture_lat = (unsigned) + (rec_lat>=0? rec_lat:PJMEDIA_SND_DEFAULT_REC_LATENCY); + playback_lat = (unsigned) + (play_lat >= 0? play_lat : PJMEDIA_SND_DEFAULT_PLAY_LATENCY); + printf("Recording latency=%ums, playback latency=%ums", + capture_lat, playback_lat); + } else { + printf("Current latencies: record=%ums, playback=%ums", + capture_lat, playback_lat); + } + puts(""); + } + break; + + case 'v': + if (pj_log_get_level() <= 3) { + pj_log_set_level(5); + puts("Logging set to detail"); + } else { + pj_log_set_level(3); + puts("Logging set to quiet"); + } + break; + + case 'q': + done = PJ_TRUE; + break; + } + } + + pj_caching_pool_destroy(&cp); + pj_shutdown(); + return 0; +} + + diff --git a/pjsip-apps/src/samples/aviplay.c b/pjsip-apps/src/samples/aviplay.c new file mode 100644 index 0000000..2ac904e --- /dev/null +++ b/pjsip-apps/src/samples/aviplay.c @@ -0,0 +1,560 @@ +/* $Id: aviplay.c 4051 2012-04-13 08:16:30Z ming $ */ +/* + * Copyright (C) 2010-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 <pjmedia.h> +#include <pjmedia/converter.h> +#include <pjmedia-codec.h> +#include <pjlib-util.h> +#include <pjlib.h> +#include <stdio.h> +#include <stdlib.h> + +#include "util.h" + +/** + * \page page_pjmedia_samples_aviplay_c Samples: Playing AVI File to + * Video and Sound Devices + * + * This is a very simple example to use the @ref PJMEDIA_FILE_PLAY, + * @ref PJMED_SND_PORT, and @ref PJMEDIA_VID_PORT. In this example, we + * open the file, video, and sound devices, then connect the file to both + * video and sound devices to play the contents of the file. + * + * + * This file is pjsip-apps/src/samples/aviplay.c + * + * \includelineno aviplay.c + */ + + +/* + * aviplay.c + * + * PURPOSE: + * Play a AVI file to video and sound devices. + * + * USAGE: + * aviplay FILE.AVI + */ + + +#if defined(PJMEDIA_HAS_VIDEO) && (PJMEDIA_HAS_VIDEO != 0) + + +/* For logging purpose. */ +#define THIS_FILE "aviplay.c" + +static const char *desc = +" FILE \n" +" \n" +" aviplay.c \n" +" \n" +" PURPOSE \n" +" \n" +" Demonstrate how to play a AVI file. \n" +" \n" +" USAGE \n" +" \n" +" aviplay FILE.AVI \n"; + +struct codec_fmt { + pj_uint32_t pjmedia_id; + const char *codec_id; + /* Do we need to convert the decoded frame? */ + pj_bool_t need_conversion; + /* If conversion is needed, dst_fmt indicates the destination format */ + pjmedia_format_id dst_fmt; +} codec_fmts[] = {{PJMEDIA_FORMAT_MJPEG, "mjpeg", + PJ_TRUE , PJMEDIA_FORMAT_I420}, + {PJMEDIA_FORMAT_H263 , "h263" , + PJ_FALSE, 0}, + {PJMEDIA_FORMAT_MPEG4, "mp4v"}, + {PJMEDIA_FORMAT_H264 , "h264"} + }; + +typedef struct avi_port_t +{ + pjmedia_vid_port *vid_port; + pjmedia_snd_port *snd_port; + pj_bool_t is_running; + pj_bool_t is_quitting; +} avi_port_t; + +typedef struct codec_port_data_t +{ + pjmedia_vid_codec *codec; + pjmedia_port *src_port; + pj_uint8_t *enc_buf; + pj_size_t enc_buf_size; + + pjmedia_converter *conv; +} codec_port_data_t; + +static pj_status_t avi_event_cb(pjmedia_event *event, + void *user_data) +{ + avi_port_t *ap = (avi_port_t *)user_data; + + switch (event->type) { + case PJMEDIA_EVENT_WND_CLOSED: + ap->is_quitting = PJ_TRUE; + break; + case PJMEDIA_EVENT_MOUSE_BTN_DOWN: + if (ap->is_running) { + pjmedia_vid_port_stop(ap->vid_port); + if (ap->snd_port) + pjmedia_aud_stream_stop( + pjmedia_snd_port_get_snd_stream(ap->snd_port)); + } else { + pjmedia_vid_port_start(ap->vid_port); + if (ap->snd_port) + pjmedia_aud_stream_start( + pjmedia_snd_port_get_snd_stream(ap->snd_port)); + } + ap->is_running = !ap->is_running; + break; + default: + return PJ_SUCCESS; + } + + /* We handled the event on our own, so return non-PJ_SUCCESS here */ + return -1; +} + +static pj_status_t codec_get_frame(pjmedia_port *port, + pjmedia_frame *frame) +{ + codec_port_data_t *port_data = (codec_port_data_t*)port->port_data.pdata; + pjmedia_vid_codec *codec = port_data->codec; + pjmedia_frame enc_frame; + pj_status_t status; + + enc_frame.buf = port_data->enc_buf; + enc_frame.size = port_data->enc_buf_size; + + if (port_data->conv) { + pj_size_t frame_size = frame->size; + + status = pjmedia_port_get_frame(port_data->src_port, frame); + if (status != PJ_SUCCESS) goto on_error; + + status = pjmedia_vid_codec_decode(codec, 1, frame, + frame->size, &enc_frame); + if (status != PJ_SUCCESS) goto on_error; + + frame->size = frame_size; + status = pjmedia_converter_convert(port_data->conv, &enc_frame, frame); + if (status != PJ_SUCCESS) goto on_error; + + return PJ_SUCCESS; + } + + status = pjmedia_port_get_frame(port_data->src_port, &enc_frame); + if (status != PJ_SUCCESS) goto on_error; + + status = pjmedia_vid_codec_decode(codec, 1, &enc_frame, + frame->size, frame); + if (status != PJ_SUCCESS) goto on_error; + + return PJ_SUCCESS; + +on_error: + pj_perror(3, THIS_FILE, status, "codec_get_frame() error"); + return status; +} + +static int aviplay(pj_pool_t *pool, const char *fname) +{ + pjmedia_vid_port *renderer=NULL; + pjmedia_vid_port_param param; + const pjmedia_video_format_info *vfi; + pjmedia_video_format_detail *vfd; + pjmedia_snd_port *snd_port = NULL; + pj_status_t status; + int rc = 0; + pjmedia_avi_streams *avi_streams; + pjmedia_avi_stream *vid_stream, *aud_stream; + pjmedia_port *vid_port = NULL, *aud_port = NULL; + pjmedia_vid_codec *codec=NULL; + avi_port_t avi_port; + + pj_bzero(&avi_port, sizeof(avi_port)); + + status = pjmedia_avi_player_create_streams(pool, fname, 0, &avi_streams); + if (status != PJ_SUCCESS) { + PJ_PERROR(2,("", status, " Error playing %s", fname)); + rc = 210; goto on_return; + } + + vid_stream = pjmedia_avi_streams_get_stream_by_media(avi_streams, + 0, + PJMEDIA_TYPE_VIDEO); + vid_port = pjmedia_avi_stream_get_port(vid_stream); + + if (vid_port) { + pjmedia_vid_port_param_default(¶m); + + status = pjmedia_vid_dev_default_param(pool, + PJMEDIA_VID_DEFAULT_RENDER_DEV, + ¶m.vidparam); + if (status != PJ_SUCCESS) { + rc = 220; goto on_return; + } + + /* Create renderer, set it to active */ + param.active = PJ_TRUE; + param.vidparam.dir = PJMEDIA_DIR_RENDER; + vfd = pjmedia_format_get_video_format_detail(&vid_port->info.fmt, + PJ_TRUE); + pjmedia_format_init_video(¶m.vidparam.fmt, + vid_port->info.fmt.id, + vfd->size.w, vfd->size.h, + vfd->fps.num, vfd->fps.denum); + + vfi = pjmedia_get_video_format_info( + pjmedia_video_format_mgr_instance(), + vid_port->info.fmt.id); + /* Check whether the frame is encoded */ + if (!vfi || vfi->bpp == 0) { + /* Yes, prepare codec */ + pj_str_t codec_id_st; + unsigned info_cnt = 1, i, k; + const pjmedia_vid_codec_info *codec_info; + pj_str_t port_name = {"codec", 5}; + pj_uint8_t *enc_buf = NULL; + pj_size_t enc_buf_size = 0; + pjmedia_vid_dev_info rdr_info; + pjmedia_port codec_port; + codec_port_data_t codec_port_data; + pjmedia_vid_codec_param codec_param; + struct codec_fmt *codecp = NULL; + + /* Lookup codec */ + for (i = 0; i < sizeof(codec_fmts)/sizeof(codec_fmts[0]); i++) { + if (vid_port->info.fmt.id == codec_fmts[i].pjmedia_id) { + codecp = &codec_fmts[i]; + break; + } + } + if (!codecp) { + rc = 242; goto on_return; + } + pj_cstr(&codec_id_st, codecp->codec_id); + status = pjmedia_vid_codec_mgr_find_codecs_by_id(NULL, + &codec_id_st, + &info_cnt, + &codec_info, + NULL); + if (status != PJ_SUCCESS) { + rc = 245; goto on_return; + } + status = pjmedia_vid_codec_mgr_get_default_param(NULL, codec_info, + &codec_param); + if (status != PJ_SUCCESS) { + rc = 246; goto on_return; + } + + pjmedia_format_copy(&codec_param.enc_fmt, ¶m.vidparam.fmt); + + pjmedia_vid_dev_get_info(param.vidparam.rend_id, &rdr_info); + for (i=0; i<codec_info->dec_fmt_id_cnt; ++i) { + for (k=0; k<rdr_info.fmt_cnt; ++k) { + if (codec_info->dec_fmt_id[i]==(int)rdr_info.fmt[k].id) + { + param.vidparam.fmt.id = codec_info->dec_fmt_id[i]; + i = codec_info->dec_fmt_id_cnt; + break; + } + } + } + + /* Open codec */ + status = pjmedia_vid_codec_mgr_alloc_codec(NULL, codec_info, + &codec); + if (status != PJ_SUCCESS) { + rc = 250; goto on_return; + } + + status = pjmedia_vid_codec_init(codec, pool); + if (status != PJ_SUCCESS) { + rc = 251; goto on_return; + } + + pjmedia_format_copy(&codec_param.dec_fmt, ¶m.vidparam.fmt); + codec_param.dir = PJMEDIA_DIR_DECODING; + codec_param.packing = PJMEDIA_VID_PACKING_WHOLE; + status = pjmedia_vid_codec_open(codec, &codec_param); + if (status != PJ_SUCCESS) { + rc = 252; goto on_return; + } + + /* Alloc encoding buffer */ + enc_buf_size = codec_param.dec_fmt.det.vid.size.w * + codec_param.dec_fmt.det.vid.size.h * 4 + + 16; /*< padding, just in case */ + enc_buf = pj_pool_alloc(pool,enc_buf_size); + + /* Init codec port */ + pj_bzero(&codec_port, sizeof(codec_port)); + status = pjmedia_port_info_init2(&codec_port.info, &port_name, + 0x1234, + PJMEDIA_DIR_ENCODING, + &codec_param.dec_fmt); + if (status != PJ_SUCCESS) { + rc = 260; goto on_return; + } + pj_bzero(&codec_port_data, sizeof(codec_port_data)); + codec_port_data.codec = codec; + codec_port_data.src_port = vid_port; + codec_port_data.enc_buf = enc_buf; + codec_port_data.enc_buf_size = enc_buf_size; + + codec_port.get_frame = &codec_get_frame; + codec_port.port_data.pdata = &codec_port_data; + + /* Check whether we need to convert the decoded frame */ + if (codecp->need_conversion) { + pjmedia_conversion_param conv_param; + + pjmedia_format_copy(&conv_param.src, ¶m.vidparam.fmt); + pjmedia_format_copy(&conv_param.dst, ¶m.vidparam.fmt); + conv_param.dst.id = codecp->dst_fmt; + param.vidparam.fmt.id = conv_param.dst.id; + + status = pjmedia_converter_create(NULL, pool, &conv_param, + &codec_port_data.conv); + if (status != PJ_SUCCESS) { + rc = 270; goto on_return; + } + } + + status = pjmedia_vid_port_create(pool, ¶m, &renderer); + if (status != PJ_SUCCESS) { + rc = 230; goto on_return; + } + + status = pjmedia_vid_port_connect(renderer, &codec_port, + PJ_FALSE); + } else { + status = pjmedia_vid_port_create(pool, ¶m, &renderer); + if (status != PJ_SUCCESS) { + rc = 230; goto on_return; + } + + /* Connect avi port to renderer */ + status = pjmedia_vid_port_connect(renderer, vid_port, + PJ_FALSE); + } + + if (status != PJ_SUCCESS) { + rc = 240; goto on_return; + } + } + + aud_stream = pjmedia_avi_streams_get_stream_by_media(avi_streams, + 0, + PJMEDIA_TYPE_AUDIO); + aud_port = pjmedia_avi_stream_get_port(aud_stream); + + if (aud_port) { + /* Create sound player port. */ + status = pjmedia_snd_port_create_player( + pool, /* pool */ + -1, /* use default dev. */ + PJMEDIA_PIA_SRATE(&aud_port->info),/* clock rate. */ + PJMEDIA_PIA_CCNT(&aud_port->info), /* # of channels. */ + PJMEDIA_PIA_SPF(&aud_port->info), /* samples per frame. */ + PJMEDIA_PIA_BITS(&aud_port->info), /* bits per sample. */ + 0, /* options */ + &snd_port /* returned port */ + ); + if (status != PJ_SUCCESS) { + rc = 310; goto on_return; + } + + /* Connect file port to the sound player. + * Stream playing will commence immediately. + */ + status = pjmedia_snd_port_connect(snd_port, aud_port); + if (status != PJ_SUCCESS) { + rc = 330; goto on_return; + } + } + + if (vid_port) { + pjmedia_vid_dev_cb cb; + + pj_bzero(&cb, sizeof(cb)); + avi_port.snd_port = snd_port; + avi_port.vid_port = renderer; + avi_port.is_running = PJ_TRUE; + pjmedia_vid_port_set_cb(renderer, &cb, &avi_port); + + /* subscribe events */ + pjmedia_event_subscribe(NULL, &avi_event_cb, &avi_port, + renderer); + + if (snd_port) { + /* Synchronize video rendering and audio playback */ + pjmedia_vid_port_set_clock_src( + renderer, + pjmedia_snd_port_get_clock_src( + snd_port, PJMEDIA_DIR_PLAYBACK)); + } + + + /* Start video streaming.. */ + status = pjmedia_vid_port_start(renderer); + if (status != PJ_SUCCESS) { + rc = 270; goto on_return; + } + } + + while (!avi_port.is_quitting) { + pj_thread_sleep(100); + } + +on_return: + if (snd_port) { + pjmedia_snd_port_disconnect(snd_port); + /* Without this sleep, Windows/DirectSound will repeteadly + * play the last frame during destroy. + */ + pj_thread_sleep(100); + pjmedia_snd_port_destroy(snd_port); + } + if (renderer) { + pjmedia_event_unsubscribe(NULL, &avi_event_cb, &avi_port, + renderer); + pjmedia_vid_port_destroy(renderer); + } + if (aud_port) + pjmedia_port_destroy(aud_port); + if (vid_port) + pjmedia_port_destroy(vid_port); + if (codec) { + pjmedia_vid_codec_close(codec); + pjmedia_vid_codec_mgr_dealloc_codec(NULL, codec); + } + + return rc; +} + + +static int main_func(int argc, char *argv[]) +{ + pj_caching_pool cp; + pj_pool_t *pool; + int rc = 0; + pj_status_t status = PJ_SUCCESS; + + if (argc != 2) { + puts("Error: filename required"); + puts(desc); + return 1; + } + + + /* Must init PJLIB first: */ + status = pj_init(); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* Must create a pool factory before we can allocate any memory. */ + pj_caching_pool_init(&cp, &pj_pool_factory_default_policy, 0); + + /* Create memory pool for our file player */ + pool = pj_pool_create( &cp.factory, /* pool factory */ + "AVI", /* pool name. */ + 4000, /* init size */ + 4000, /* increment size */ + NULL /* callback on error */ + ); + + pjmedia_video_format_mgr_create(pool, 64, 0, NULL); + pjmedia_converter_mgr_create(pool, NULL); + pjmedia_event_mgr_create(pool, 0, NULL); + pjmedia_vid_codec_mgr_create(pool, NULL); + + status = pjmedia_vid_dev_subsys_init(&cp.factory); + if (status != PJ_SUCCESS) + goto on_return; + + status = pjmedia_aud_subsys_init(&cp.factory); + if (status != PJ_SUCCESS) { + goto on_return; + } + +#if PJMEDIA_HAS_FFMPEG_VID_CODEC + status = pjmedia_codec_ffmpeg_vid_init(NULL, &cp.factory); + if (status != PJ_SUCCESS) + goto on_return; +#endif + + rc = aviplay(pool, argv[1]); + + /* + * File should be playing and looping now + */ + + /* Without this sleep, Windows/DirectSound will repeteadly + * play the last frame during destroy. + */ + pj_thread_sleep(100); + +on_return: +#if PJMEDIA_HAS_FFMPEG_VID_CODEC + pjmedia_codec_ffmpeg_vid_deinit(); +#endif + pjmedia_aud_subsys_shutdown(); + pjmedia_vid_dev_subsys_shutdown(); + + pjmedia_video_format_mgr_destroy(pjmedia_video_format_mgr_instance()); + pjmedia_converter_mgr_destroy(pjmedia_converter_mgr_instance()); + pjmedia_event_mgr_destroy(pjmedia_event_mgr_instance()); + pjmedia_vid_codec_mgr_destroy(pjmedia_vid_codec_mgr_instance()); + + /* Release application pool */ + pj_pool_release( pool ); + + /* Destroy pool factory */ + pj_caching_pool_destroy( &cp ); + + /* Shutdown PJLIB */ + pj_shutdown(); + + /* Done. */ + return 0; +} + +int main(int argc, char *argv[]) +{ + return pj_run_app(&main_func, argc, argv, 0); +} + +#else + +int main(int argc, char *argv[]) +{ + PJ_UNUSED_ARG(argc); + PJ_UNUSED_ARG(argv); + puts("Error: this sample requires video capability (PJMEDIA_HAS_VIDEO == 1)"); + return -1; +} + +#endif /* PJMEDIA_HAS_VIDEO */ diff --git a/pjsip-apps/src/samples/confbench.c b/pjsip-apps/src/samples/confbench.c new file mode 100644 index 0000000..4fe155d --- /dev/null +++ b/pjsip-apps/src/samples/confbench.c @@ -0,0 +1,347 @@ +/* $Id: confbench.c 3664 2011-07-19 03:42:28Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 + */ + + +/** + * \page page_pjmedia_samples_confbench_c Samples: Benchmarking Conference Bridge + * + * Benchmarking pjmedia (conference bridge+resample). For my use only, + * and it only works in Win32. + * + * This file is pjsip-apps/src/samples/confbench.c + * + * \includelineno confbench.c + */ + + +#include <pjmedia.h> +#include <pjlib-util.h> /* pj_getopt */ +#include <pjlib.h> +#include <stdlib.h> /* atoi() */ +#include <stdio.h> +#include <windows.h> + +/* For logging purpose. */ +#define THIS_FILE "confsample.c" + + +/* Configurable: + * LARGE_SET will create in total of about 232 ports. + * HAS_RESAMPLE will activate resampling on about half + * the port. + */ +#define TEST_SET LARGE_SET +#define HAS_RESAMPLE 0 + + +#define SMALL_SET 16 +#define LARGE_SET 100 + + +#define PORT_COUNT 254 +#define CLOCK_RATE 16000 +#define SAMPLES_PER_FRAME (CLOCK_RATE/100) +#if HAS_RESAMPLE +# define SINE_CLOCK 32000 +#else +# define SINE_CLOCK CLOCK_RATE +#endif +#define SINE_PTIME 20 +#define DURATION 10 + +#define SINE_COUNT TEST_SET +#define NULL_COUNT TEST_SET +#define IDLE_COUNT 32 + + +static void app_perror(const char *sender, const char *title, pj_status_t status) +{ + char errmsg[PJ_ERR_MSG_SIZE]; + + pj_strerror(status, errmsg, sizeof(errmsg)); + PJ_LOG(1,(sender, "%s: %s", title, errmsg)); +} + + +struct Times +{ + FILETIME kernel_time; + ULARGE_INTEGER u_kernel_time; + FILETIME user_time; + ULARGE_INTEGER u_user_time; + ULARGE_INTEGER u_total; +}; + +static void process(struct Times *t) +{ + pj_memcpy(&t->u_kernel_time, &t->kernel_time, sizeof(FILETIME)); + pj_memcpy(&t->u_user_time, &t->user_time, sizeof(FILETIME)); + t->u_total.QuadPart = t->u_kernel_time.QuadPart + t->u_user_time.QuadPart; +} + +static void benchmark(void) +{ + FILETIME creation_time, exit_time; + struct Times start, end; + DWORD ts, te; + LARGE_INTEGER elapsed; + BOOL rc; + int i; + double pct; + + puts("Test started!"); fflush(stdout); + + ts = GetTickCount(); + rc = GetProcessTimes(GetCurrentProcess(), &creation_time, &exit_time, + &start.kernel_time, &start.user_time); + for (i=DURATION; i>0; --i) { + printf("\r%d ", i); fflush(stdout); + pj_thread_sleep(1000); + } + rc = GetProcessTimes(GetCurrentProcess(), &creation_time, &exit_time, + &end.kernel_time, &end.user_time); + te = GetTickCount(); + + process(&start); + process(&end); + + elapsed.QuadPart = end.u_total.QuadPart - start.u_total.QuadPart; + + pct = elapsed.QuadPart * 100.0 / ((te-ts)*10000.0); + + printf("CPU usage=%6.4f%%\n", pct); fflush(stdout); +} + + + +/* Struct attached to sine generator */ +typedef struct +{ + pj_int16_t *samples; /* Sine samples. */ +} port_data; + + +/* This callback is called to feed more samples */ +static pj_status_t sine_get_frame( pjmedia_port *port, + pjmedia_frame *frame) +{ + port_data *sine = port->port_data.pdata; + pj_int16_t *samples = frame->buf; + unsigned i, count, left, right; + + /* Get number of samples */ + count = frame->size / 2 / PJMEDIA_PIA_CCNT(&port->info); + + left = 0; + right = 0; + + for (i=0; i<count; ++i) { + *samples++ = sine->samples[left]; + ++left; + + if (PJMEDIA_PIA_CCNT(&port->info) == 2) { + *samples++ = sine->samples[right]; + right += 2; /* higher pitch so we can distinguish left and right. */ + if (right >= count) + right = 0; + } + } + + /* Must set frame->type correctly, otherwise the sound device + * will refuse to play. + */ + frame->type = PJMEDIA_FRAME_TYPE_AUDIO; + + return PJ_SUCCESS; +} + +#ifndef M_PI +#define M_PI (3.14159265) +#endif + +/* + * Create a media port to generate sine wave samples. + */ +static pj_status_t create_sine_port(pj_pool_t *pool, + unsigned sampling_rate, + unsigned channel_count, + pjmedia_port **p_port) +{ + pjmedia_port *port; + unsigned i; + unsigned count; + pj_str_t port_name; + port_data *sine; + + PJ_ASSERT_RETURN(pool && channel_count > 0 && channel_count <= 2, + PJ_EINVAL); + + port = pj_pool_zalloc(pool, sizeof(pjmedia_port)); + PJ_ASSERT_RETURN(port != NULL, PJ_ENOMEM); + + /* Fill in port info. */ + port_name = pj_str("sine generator"); + pjmedia_port_info_init(&port->info, &port_name, + 12345, sampling_rate, channel_count, 16, + sampling_rate * SINE_PTIME / 1000 * channel_count); + + /* Set the function to feed frame */ + port->get_frame = &sine_get_frame; + + /* Create sine port data */ + port->port_data.pdata = sine = pj_pool_zalloc(pool, sizeof(port_data)); + + /* Create samples */ + count = PJMEDIA_PIA_SPF(&port->info) / channel_count; + sine->samples = pj_pool_alloc(pool, count * sizeof(pj_int16_t)); + PJ_ASSERT_RETURN(sine->samples != NULL, PJ_ENOMEM); + + /* initialise sinusoidal wavetable */ + for( i=0; i<count; i++ ) + { + sine->samples[i] = (pj_int16_t) (10000.0 * + sin(((double)i/(double)count) * M_PI * 8.) ); + } + + *p_port = port; + + return PJ_SUCCESS; +} + +int main() +{ + pj_caching_pool cp; + pjmedia_endpt *med_endpt; + pj_pool_t *pool; + pjmedia_conf *conf; + int i; + pjmedia_port *sine_port[SINE_COUNT], *null_port, *conf_port; + pjmedia_port *nulls[NULL_COUNT]; + unsigned null_slots[NULL_COUNT]; + pjmedia_master_port *master_port; + pj_status_t status; + + + pj_log_set_level(3); + + status = pj_init(); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + pj_caching_pool_init(&cp, &pj_pool_factory_default_policy, 0); + pool = pj_pool_create( &cp.factory, /* pool factory */ + "wav", /* pool name. */ + 4000, /* init size */ + 4000, /* increment size */ + NULL /* callback on error */ + ); + + status = pjmedia_endpt_create(&cp.factory, NULL, 1, &med_endpt); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + + status = pjmedia_conf_create( pool, + PORT_COUNT, + CLOCK_RATE, + 1, SAMPLES_PER_FRAME, 16, + PJMEDIA_CONF_NO_DEVICE, + &conf); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to create conference bridge", status); + return 1; + } + + printf("Resampling is %s\n", (HAS_RESAMPLE?"active":"disabled")); + + /* Create Null ports */ + printf("Creating %d null ports..\n", NULL_COUNT); + for (i=0; i<NULL_COUNT; ++i) { + status = pjmedia_null_port_create(pool, CLOCK_RATE, 1, SAMPLES_PER_FRAME*2, 16, &nulls[i]); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + status = pjmedia_conf_add_port(conf, pool, nulls[i], NULL, &null_slots[i]); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + } + + /* Create sine ports. */ + printf("Creating %d sine generator ports..\n", SINE_COUNT); + for (i=0; i<SINE_COUNT; ++i) { + unsigned j, slot; + + /* Load the WAV file to file port. */ + status = create_sine_port(pool, SINE_CLOCK, 1, &sine_port[i]); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* Add the file port to conference bridge */ + status = pjmedia_conf_add_port( conf, /* The bridge */ + pool, /* pool */ + sine_port[i], /* port to connect */ + NULL, /* Use port's name */ + &slot /* ptr for slot # */ + ); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to add conference port", status); + return 1; + } + + status = pjmedia_conf_connect_port(conf, slot, 0, 0); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + for (j=0; j<NULL_COUNT; ++j) { + status = pjmedia_conf_connect_port(conf, slot, null_slots[j], 0); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + } + } + + /* Create idle ports */ + printf("Creating %d idle ports..\n", IDLE_COUNT); + for (i=0; i<IDLE_COUNT; ++i) { + pjmedia_port *dummy; + status = pjmedia_null_port_create(pool, CLOCK_RATE, 1, SAMPLES_PER_FRAME, 16, &dummy); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + status = pjmedia_conf_add_port(conf, pool, dummy, NULL, NULL); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + } + + /* Create null port */ + status = pjmedia_null_port_create(pool, CLOCK_RATE, 1, SAMPLES_PER_FRAME, 16, + &null_port); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + conf_port = pjmedia_conf_get_master_port(conf); + + /* Create master port */ + status = pjmedia_master_port_create(pool, null_port, conf_port, 0, &master_port); + + + pjmedia_master_port_start(master_port); + + puts("Waiting to settle.."); fflush(stdout); + pj_thread_sleep(5000); + + + benchmark(); + + + /* Done. */ + return 0; +} + + diff --git a/pjsip-apps/src/samples/confsample.c b/pjsip-apps/src/samples/confsample.c new file mode 100644 index 0000000..b8eb93f --- /dev/null +++ b/pjsip-apps/src/samples/confsample.c @@ -0,0 +1,609 @@ +/* $Id: confsample.c 3553 2011-05-05 06:14:19Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 <pjmedia.h> +#include <pjlib-util.h> /* pj_getopt */ +#include <pjlib.h> + +#include <stdlib.h> /* atoi() */ +#include <stdio.h> + +#include "util.h" + +/** + * \page page_pjmedia_samples_confsample_c Samples: Using Conference Bridge + * + * Sample to mix multiple files in the conference bridge and play the + * result to sound device. + * + * This file is pjsip-apps/src/samples/confsample.c + * + * \includelineno confsample.c + */ + + +/* For logging purpose. */ +#define THIS_FILE "confsample.c" + + +/* Shall we put recorder in the conference */ +#define RECORDER 1 + + +static const char *desc = + " FILE: \n" + " \n" + " confsample.c \n" + " \n" + " PURPOSE: \n" + " \n" + " Demonstrate how to use conference bridge. \n" + " \n" + " USAGE: \n" + " \n" + " confsample [options] [file1.wav] [file2.wav] ... \n" + " \n" + " options: \n" + SND_USAGE + " \n" + " fileN.wav are optional WAV files to be connected to the conference \n" + " bridge. The WAV files MUST have single channel (mono) and 16 bit PCM \n" + " samples. It can have arbitrary sampling rate. \n" + " \n" + " DESCRIPTION: \n" + " \n" + " Here we create a conference bridge, with at least one port (port zero \n" + " is always created for the sound device). \n" + " \n" + " If WAV files are specified, the WAV file player ports will be connected \n" + " to slot starting from number one in the bridge. The WAV files can have \n" + " arbitrary sampling rate; the bridge will convert it to its clock rate. \n" + " However, the files MUST have a single audio channel only (i.e. mono). \n"; + + + +/* + * Prototypes: + */ + +/* List the ports in the conference bridge */ +static void conf_list(pjmedia_conf *conf, pj_bool_t detail); + +/* Display VU meter */ +static void monitor_level(pjmedia_conf *conf, int slot, int dir, int dur); + + +/* Show usage */ +static void usage(void) +{ + puts(""); + puts(desc); +} + + + +/* Input simple string */ +static pj_bool_t input(const char *title, char *buf, pj_size_t len) +{ + char *p; + + printf("%s (empty to cancel): ", title); fflush(stdout); + if (fgets(buf, len, stdin) == NULL) + return PJ_FALSE; + + /* Remove trailing newlines. */ + for (p=buf; ; ++p) { + if (*p=='\r' || *p=='\n') *p='\0'; + else if (!*p) break; + } + + if (!*buf) + return PJ_FALSE; + + return PJ_TRUE; +} + + +/***************************************************************************** + * main() + */ +int main(int argc, char *argv[]) +{ + int dev_id = -1; + int clock_rate = CLOCK_RATE; + int channel_count = NCHANNELS; + int samples_per_frame = NSAMPLES; + int bits_per_sample = NBITS; + + pj_caching_pool cp; + pjmedia_endpt *med_endpt; + pj_pool_t *pool; + pjmedia_conf *conf; + + int i, port_count, file_count; + pjmedia_port **file_port; /* Array of file ports */ + pjmedia_port *rec_port = NULL; /* Wav writer port */ + + char tmp[10]; + pj_status_t status; + + + /* Must init PJLIB first: */ + status = pj_init(); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* Get command line options. */ + if (get_snd_options(THIS_FILE, argc, argv, &dev_id, &clock_rate, + &channel_count, &samples_per_frame, &bits_per_sample)) + { + usage(); + return 1; + } + + /* Must create a pool factory before we can allocate any memory. */ + pj_caching_pool_init(&cp, &pj_pool_factory_default_policy, 0); + + /* + * Initialize media endpoint. + * This will implicitly initialize PJMEDIA too. + */ + status = pjmedia_endpt_create(&cp.factory, NULL, 1, &med_endpt); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* Create memory pool to allocate memory */ + pool = pj_pool_create( &cp.factory, /* pool factory */ + "wav", /* pool name. */ + 4000, /* init size */ + 4000, /* increment size */ + NULL /* callback on error */ + ); + + + file_count = argc - pj_optind; + port_count = file_count + 1 + RECORDER; + + /* Create the conference bridge. + * With default options (zero), the bridge will create an instance of + * sound capture and playback device and connect them to slot zero. + */ + status = pjmedia_conf_create( pool, /* pool to use */ + port_count,/* number of ports */ + clock_rate, + channel_count, + samples_per_frame, + bits_per_sample, + 0, /* options */ + &conf /* result */ + ); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to create conference bridge", status); + return 1; + } + +#if RECORDER + status = pjmedia_wav_writer_port_create( pool, "confrecord.wav", + clock_rate, channel_count, + samples_per_frame, + bits_per_sample, 0, 0, + &rec_port); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to create WAV writer", status); + return 1; + } + + pjmedia_conf_add_port(conf, pool, rec_port, NULL, NULL); +#endif + + + /* Create file ports. */ + file_port = pj_pool_alloc(pool, file_count * sizeof(pjmedia_port*)); + + for (i=0; i<file_count; ++i) { + + /* Load the WAV file to file port. */ + status = pjmedia_wav_player_port_create( + pool, /* pool. */ + argv[i+pj_optind], /* filename */ + 0, /* use default ptime */ + 0, /* flags */ + 0, /* buf size */ + &file_port[i] /* result */ + ); + if (status != PJ_SUCCESS) { + char title[80]; + pj_ansi_sprintf(title, "Unable to use %s", argv[i+pj_optind]); + app_perror(THIS_FILE, title, status); + usage(); + return 1; + } + + /* Add the file port to conference bridge */ + status = pjmedia_conf_add_port( conf, /* The bridge */ + pool, /* pool */ + file_port[i], /* port to connect */ + NULL, /* Use port's name */ + NULL /* ptr for slot # */ + ); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to add conference port", status); + return 1; + } + } + + + /* + * All ports are set up in the conference bridge. + * But at this point, no media will be flowing since no ports are + * "connected". User must connect the port manually. + */ + + + /* Dump memory usage */ + dump_pool_usage(THIS_FILE, &cp); + + /* Sleep to allow log messages to flush */ + pj_thread_sleep(100); + + + /* + * UI Menu: + */ + for (;;) { + char tmp1[10]; + char tmp2[10]; + char *err; + int src, dst, level, dur; + + puts(""); + conf_list(conf, 0); + puts(""); + puts("Menu:"); + puts(" s Show ports details"); + puts(" c Connect one port to another"); + puts(" d Disconnect port connection"); + puts(" t Adjust signal level transmitted (tx) to a port"); + puts(" r Adjust signal level received (rx) from a port"); + puts(" v Display VU meter for a particular port"); + puts(" q Quit"); + puts(""); + + printf("Enter selection: "); fflush(stdout); + + if (fgets(tmp, sizeof(tmp), stdin) == NULL) + break; + + switch (tmp[0]) { + case 's': + puts(""); + conf_list(conf, 1); + break; + + case 'c': + puts(""); + puts("Connect source port to destination port"); + if (!input("Enter source port number", tmp1, sizeof(tmp1)) ) + continue; + src = strtol(tmp1, &err, 10); + if (*err || src < 0 || src >= port_count) { + puts("Invalid slot number"); + continue; + } + + if (!input("Enter destination port number", tmp2, sizeof(tmp2)) ) + continue; + dst = strtol(tmp2, &err, 10); + if (*err || dst < 0 || dst >= port_count) { + puts("Invalid slot number"); + continue; + } + + status = pjmedia_conf_connect_port(conf, src, dst, 0); + if (status != PJ_SUCCESS) + app_perror(THIS_FILE, "Error connecting port", status); + + break; + + case 'd': + puts(""); + puts("Disconnect port connection"); + if (!input("Enter source port number", tmp1, sizeof(tmp1)) ) + continue; + src = strtol(tmp1, &err, 10); + if (*err || src < 0 || src >= port_count) { + puts("Invalid slot number"); + continue; + } + + if (!input("Enter destination port number", tmp2, sizeof(tmp2)) ) + continue; + dst = strtol(tmp2, &err, 10); + if (*err || dst < 0 || dst >= port_count) { + puts("Invalid slot number"); + continue; + } + + status = pjmedia_conf_disconnect_port(conf, src, dst); + if (status != PJ_SUCCESS) + app_perror(THIS_FILE, "Error connecting port", status); + + + break; + + case 't': + puts(""); + puts("Adjust transmit level of a port"); + if (!input("Enter port number", tmp1, sizeof(tmp1)) ) + continue; + src = strtol(tmp1, &err, 10); + if (*err || src < 0 || src >= port_count) { + puts("Invalid slot number"); + continue; + } + + if (!input("Enter level (-128 to >127, 0 for normal)", + tmp2, sizeof(tmp2)) ) + continue; + level = strtol(tmp2, &err, 10); + if (*err || level < -128) { + puts("Invalid level"); + continue; + } + + status = pjmedia_conf_adjust_tx_level( conf, src, level); + if (status != PJ_SUCCESS) + app_perror(THIS_FILE, "Error adjusting level", status); + break; + + + case 'r': + puts(""); + puts("Adjust receive level of a port"); + if (!input("Enter port number", tmp1, sizeof(tmp1)) ) + continue; + src = strtol(tmp1, &err, 10); + if (*err || src < 0 || src >= port_count) { + puts("Invalid slot number"); + continue; + } + + if (!input("Enter level (-128 to >127, 0 for normal)", + tmp2, sizeof(tmp2)) ) + continue; + level = strtol(tmp2, &err, 10); + if (*err || level < -128) { + puts("Invalid level"); + continue; + } + + status = pjmedia_conf_adjust_rx_level( conf, src, level); + if (status != PJ_SUCCESS) + app_perror(THIS_FILE, "Error adjusting level", status); + break; + + case 'v': + puts(""); + puts("Display VU meter"); + if (!input("Enter port number to monitor", tmp1, sizeof(tmp1)) ) + continue; + src = strtol(tmp1, &err, 10); + if (*err || src < 0 || src >= port_count) { + puts("Invalid slot number"); + continue; + } + + if (!input("Enter r for rx level or t for tx level", tmp2, sizeof(tmp2))) + continue; + if (tmp2[0] != 'r' && tmp2[0] != 't') { + puts("Invalid option"); + continue; + } + + if (!input("Duration to monitor (in seconds)", tmp1, sizeof(tmp1)) ) + continue; + dur = strtol(tmp1, &err, 10); + if (*err) { + puts("Invalid duration number"); + continue; + } + + monitor_level(conf, src, tmp2[0], dur); + break; + + case 'q': + goto on_quit; + + default: + printf("Invalid input character '%c'\n", tmp[0]); + break; + } + } + +on_quit: + + /* Start deinitialization: */ + + /* Destroy conference bridge */ + status = pjmedia_conf_destroy( conf ); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + /* Destroy file ports */ + for (i=0; i<file_count; ++i) { + status = pjmedia_port_destroy( file_port[i]); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + } + + /* Destroy recorder port */ + if (rec_port) + pjmedia_port_destroy(rec_port); + + /* Release application pool */ + pj_pool_release( pool ); + + /* Destroy media endpoint. */ + pjmedia_endpt_destroy( med_endpt ); + + /* Destroy pool factory */ + pj_caching_pool_destroy( &cp ); + + /* Shutdown PJLIB */ + pj_shutdown(); + + /* Done. */ + return 0; +} + + +/* + * List the ports in conference bridge + */ +static void conf_list(pjmedia_conf *conf, int detail) +{ + enum { MAX_PORTS = 32 }; + unsigned i, count; + pjmedia_conf_port_info info[MAX_PORTS]; + + printf("Conference ports:\n"); + + count = PJ_ARRAY_SIZE(info); + pjmedia_conf_get_ports_info(conf, &count, info); + + for (i=0; i<count; ++i) { + char txlist[4*MAX_PORTS]; + unsigned j; + pjmedia_conf_port_info *port_info = &info[i]; + + txlist[0] = '\0'; + for (j=0; j<port_info->listener_cnt; ++j) { + char s[10]; + pj_ansi_sprintf(s, "#%d ", port_info->listener_slots[j]); + pj_ansi_strcat(txlist, s); + + } + + if (txlist[0] == '\0') { + txlist[0] = '-'; + txlist[1] = '\0'; + } + + if (!detail) { + printf("Port #%02d %-25.*s transmitting to: %s\n", + port_info->slot, + (int)port_info->name.slen, + port_info->name.ptr, + txlist); + } else { + unsigned tx_level, rx_level; + + pjmedia_conf_get_signal_level(conf, port_info->slot, + &tx_level, &rx_level); + + printf("Port #%02d:\n" + " Name : %.*s\n" + " Sampling rate : %d Hz\n" + " Samples per frame : %d\n" + " Frame time : %d ms\n" + " Signal level adjustment : tx=%d, rx=%d\n" + " Current signal level : tx=%u, rx=%u\n" + " Transmitting to ports : %s\n\n", + port_info->slot, + (int)port_info->name.slen, + port_info->name.ptr, + port_info->clock_rate, + port_info->samples_per_frame, + port_info->samples_per_frame*1000/port_info->clock_rate, + port_info->tx_adj_level, + port_info->rx_adj_level, + tx_level, + rx_level, + txlist); + } + + } + puts(""); +} + + +/* + * Display VU meter + */ +static void monitor_level(pjmedia_conf *conf, int slot, int dir, int dur) +{ + enum { SLEEP = 20, SAMP_CNT = 2}; + pj_status_t status; + int i, total_count; + unsigned level, samp_cnt; + + + puts(""); + printf("Displaying VU meter for port %d for about %d seconds\n", + slot, dur); + + total_count = dur * 1000 / SLEEP; + + level = 0; + samp_cnt = 0; + + for (i=0; i<total_count; ++i) { + unsigned tx_level, rx_level; + int j, length; + char meter[21]; + + /* Poll the volume every 20 msec */ + status = pjmedia_conf_get_signal_level(conf, slot, + &tx_level, &rx_level); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to read level", status); + return; + } + + level += (dir=='r' ? rx_level : tx_level); + ++samp_cnt; + + /* Accumulate until we have enough samples */ + if (samp_cnt < SAMP_CNT) { + pj_thread_sleep(SLEEP); + continue; + } + + /* Get average */ + level = level / samp_cnt; + + /* Draw bar */ + length = 20 * level / 255; + for (j=0; j<length; ++j) + meter[j] = '#'; + for (; j<20; ++j) + meter[j] = ' '; + meter[20] = '\0'; + + printf("Port #%02d %cx level: [%s] %d \r", + slot, dir, meter, level); + + /* Next.. */ + samp_cnt = 0; + level = 0; + + pj_thread_sleep(SLEEP); + } + + puts(""); +} + diff --git a/pjsip-apps/src/samples/debug.c b/pjsip-apps/src/samples/debug.c new file mode 100644 index 0000000..1d38f9a --- /dev/null +++ b/pjsip-apps/src/samples/debug.c @@ -0,0 +1,32 @@ +/* $Id: debug.c 3553 2011-05-05 06:14:19Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 + */ + +/* + * PURPOSE: + * The purpose of this file is to allow debugging of a sample application + * using MSVC IDE. + */ + +/* To debug a sample application, include the source file here. + * E.g.: + * #include "playfile.c" + */ +#include "icedemo.c" + diff --git a/pjsip-apps/src/samples/encdec.c b/pjsip-apps/src/samples/encdec.c new file mode 100644 index 0000000..9ded990 --- /dev/null +++ b/pjsip-apps/src/samples/encdec.c @@ -0,0 +1,263 @@ +/* $Id: encdec.c 3664 2011-07-19 03:42:28Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 + */ + + /** + * \page page_pjmedia_samples_encdec_c Samples: Encoding and Decoding + * + * This sample shows how to use codec. + * + * This file is pjsip-apps/src/samples/encdec.c + * + * \includelineno encdec.c + */ + +#include <pjlib.h> +#include <pjmedia.h> +#include <pjmedia-codec.h> + +#define THIS_FILE "encdec.c" + +static const char *desc = + " encdec \n" + " \n" + " PURPOSE: \n" + " Encode input WAV with a codec, and decode the result to another WAV \n" + "\n" + "\n" + " USAGE: \n" + " encdec codec input.wav output.wav \n" + "\n" + "\n" + " where:\n" + " codec Set the codec name. \n" + " input.wav Set the input WAV filename. \n" + " output.wav Set the output WAV filename. \n" + + "\n" +; + +//#undef PJ_TRACE +//#define PJ_TRACE 1 + +#ifndef PJ_TRACE +# define PJ_TRACE 0 +#endif + +#if PJ_TRACE +# define TRACE_(expr) PJ_LOG(4,expr) +#else +# define TRACE_(expr) +#endif + + +static void err(const char *op, pj_status_t status) +{ + char errmsg[PJ_ERR_MSG_SIZE]; + pj_strerror(status, errmsg, sizeof(errmsg)); + PJ_LOG(3,("", "%s error: %s", op, errmsg)); +} + +#define CHECK(op) do { \ + status = op; \ + if (status != PJ_SUCCESS) { \ + err(#op, status); \ + return status; \ + } \ + } \ + while (0) + +static pjmedia_endpt *mept; +static unsigned file_msec_duration; + +static pj_status_t enc_dec_test(const char *codec_id, + const char *filein, + const char *fileout) +{ + pj_pool_t *pool; + pjmedia_codec_mgr *cm; + pjmedia_codec *codec; + const pjmedia_codec_info *pci; + pjmedia_codec_param param; + unsigned cnt, samples_per_frame; + pj_str_t tmp; + pjmedia_port *wavin, *wavout; + unsigned lost_pct; + pj_status_t status; + +#define T file_msec_duration/1000, file_msec_duration%1000 + + pool = pjmedia_endpt_create_pool(mept, "encdec", 1000, 1000); + + cm = pjmedia_endpt_get_codec_mgr(mept); + +#ifdef LOST_PCT + lost_pct = LOST_PCT; +#else + lost_pct = 0; +#endif + + cnt = 1; + CHECK( pjmedia_codec_mgr_find_codecs_by_id(cm, pj_cstr(&tmp, codec_id), + &cnt, &pci, NULL) ); + CHECK( pjmedia_codec_mgr_get_default_param(cm, pci, ¶m) ); + + samples_per_frame = param.info.clock_rate * param.info.frm_ptime / 1000; + + /* Control VAD */ + param.setting.vad = 1; + + /* Open wav for reading */ + CHECK( pjmedia_wav_player_port_create(pool, filein, + param.info.frm_ptime, + PJMEDIA_FILE_NO_LOOP, 0, &wavin) ); + + /* Open wav for writing */ + CHECK( pjmedia_wav_writer_port_create(pool, fileout, + param.info.clock_rate, + param.info.channel_cnt, + samples_per_frame, + 16, 0, 0, &wavout) ); + + /* Alloc codec */ + CHECK( pjmedia_codec_mgr_alloc_codec(cm, pci, &codec) ); + CHECK( pjmedia_codec_init(codec, pool) ); + CHECK( pjmedia_codec_open(codec, ¶m) ); + + for (;;) { + pjmedia_frame frm_pcm, frm_bit, out_frm, frames[4]; + pj_int16_t pcmbuf[320]; + pj_timestamp ts; + pj_uint8_t bitstream[160]; + + frm_pcm.buf = (char*)pcmbuf; + frm_pcm.size = samples_per_frame * 2; + + /* Read from WAV */ + if (pjmedia_port_get_frame(wavin, &frm_pcm) != PJ_SUCCESS) + break; + if (frm_pcm.type != PJMEDIA_FRAME_TYPE_AUDIO) + break;; + + /* Update duration */ + file_msec_duration += samples_per_frame * 1000 / + param.info.clock_rate; + + /* Encode */ + frm_bit.buf = bitstream; + frm_bit.size = sizeof(bitstream); + CHECK(pjmedia_codec_encode(codec, &frm_pcm, sizeof(bitstream), + &frm_bit)); + + /* On DTX, write zero frame to wavout to maintain duration */ + if (frm_bit.size == 0 || frm_bit.type != PJMEDIA_FRAME_TYPE_AUDIO) { + out_frm.buf = (char*)pcmbuf; + out_frm.size = 160; + CHECK( pjmedia_port_put_frame(wavout, &out_frm) ); + TRACE_((THIS_FILE, "%d.%03d read: %u, enc: %u", + T, frm_pcm.size, frm_bit.size)); + continue; + } + + /* Parse the bitstream (not really necessary for this case + * since we always decode 1 frame, but it's still good + * for testing) + */ + ts.u64 = 0; + cnt = PJ_ARRAY_SIZE(frames); + CHECK( pjmedia_codec_parse(codec, bitstream, frm_bit.size, &ts, &cnt, + frames) ); + CHECK( (cnt==1 ? PJ_SUCCESS : -1) ); + + /* Decode or simulate packet loss */ + out_frm.buf = (char*)pcmbuf; + out_frm.size = sizeof(pcmbuf); + + if ((pj_rand() % 100) < (int)lost_pct) { + /* Simulate loss */ + CHECK( pjmedia_codec_recover(codec, sizeof(pcmbuf), &out_frm) ); + TRACE_((THIS_FILE, "%d.%03d Packet lost", T)); + } else { + /* Decode */ + CHECK( pjmedia_codec_decode(codec, &frames[0], sizeof(pcmbuf), + &out_frm) ); + } + + /* Write to WAV */ + CHECK( pjmedia_port_put_frame(wavout, &out_frm) ); + + TRACE_((THIS_FILE, "%d.%03d read: %u, enc: %u, dec/write: %u", + T, frm_pcm.size, frm_bit.size, out_frm.size)); + } + + /* Close wavs */ + pjmedia_port_destroy(wavout); + pjmedia_port_destroy(wavin); + + /* Close codec */ + pjmedia_codec_close(codec); + pjmedia_codec_mgr_dealloc_codec(cm, codec); + + /* Release pool */ + pj_pool_release(pool); + + return PJ_SUCCESS; +} + + +int main(int argc, char *argv[]) +{ + pj_caching_pool cp; + pj_time_val t0, t1; + pj_status_t status; + + if (argc != 4) { + puts(desc); + return 1; + } + + CHECK( pj_init() ); + + pj_caching_pool_init(&cp, NULL, 0); + + CHECK( pjmedia_endpt_create(&cp.factory, NULL, 1, &mept) ); + + /* Register all codecs */ + CHECK( pjmedia_codec_register_audio_codecs(mept, NULL) ); + + pj_gettimeofday(&t0); + status = enc_dec_test(argv[1], argv[2], argv[3]); + pj_gettimeofday(&t1); + PJ_TIME_VAL_SUB(t1, t0); + + pjmedia_endpt_destroy(mept); + pj_caching_pool_destroy(&cp); + pj_shutdown(); + + if (status == PJ_SUCCESS) { + puts(""); + puts("Success"); + printf("Duration: %ds.%03d\n", file_msec_duration/1000, + file_msec_duration%1000); + printf("Time: %lds.%03ld\n", t1.sec, t1.msec); + } + + return 0; +} + diff --git a/pjsip-apps/src/samples/footprint.c b/pjsip-apps/src/samples/footprint.c new file mode 100644 index 0000000..241ba1e --- /dev/null +++ b/pjsip-apps/src/samples/footprint.c @@ -0,0 +1,654 @@ +/* $Id: footprint.c 3553 2011-05-05 06:14:19Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 + */ + +/** + * The purpose of this file is to show the typical footprint of + * the application when various PJSIP/PJMEDIA components are used. + * + * This file will not be build as samples, but instead it is build + * by get-footprint.py Python script in pjsip-apps/build directory. + */ + +#include <pjsip_ua.h> +#include <pjsip_simple.h> +#include <pjsip.h> +#include <pjmedia.h> +#include <pjmedia-codec.h> +#include <pjlib-util.h> +#include <pjlib.h> +#include <pjnath.h> +#include <stdlib.h> + +/* All flags: */ +#if 0 +#define HAS_PJLIB + +#define HAS_PJLIB_STUN +#define HAS_PJLIB_GETOPT +#define HAS_PJLIB_XML +#define HAS_PJLIB_SCANNER +#define HAS_PJLIB_DNS +#define HAS_PJLIB_RESOLVER +#define HAS_PJLIB_SRV_RESOLVER + +#define HAS_PJLIB_CRC32 +#define HAS_PJLIB_HMAC_MD5 +#define HAS_PJLIB_HMAC_SHA1 + +#define HAS_PJSIP_CORE_MSG_ELEM +#define HAS_PJSIP_CORE +#define HAS_PJSIP_CORE_MSG_UTIL + +#define HAS_PJSIP_UDP_TRANSPORT +#define HAS_PJSIP_TCP_TRANSPORT +#define HAS_PJSIP_TLS_TRANSPORT +#define HAS_PJSIP_TRANSACTION +#define HAS_PJSIP_UA_LAYER +#define HAS_PJMEDIA_SDP +#define HAS_PJMEDIA_SDP_NEGOTIATOR +#define HAS_PJSIP_AUTH_CLIENT +#define HAS_PJSIP_INV_SESSION +#define HAS_PJSIP_REGC +#define HAS_PJSIP_EVENT_FRAMEWORK +#define HAS_PJSIP_CALL_TRANSFER +#define HAS_PJSIP_PRESENCE +#define HAS_PJSIP_IS_COMPOSING + +#define HAS_PJNATH_STUN +#define HAS_PJNATH_ICE + +#define HAS_PJMEDIA +#define HAS_PJMEDIA_SND_DEV +#define HAS_PJMEDIA_EC +#define HAS_PJMEDIA_SND_PORT +#define HAS_PJMEDIA_RESAMPLE +#define HAS_PJMEDIA_SILENCE_DET +#define HAS_PJMEDIA_PLC +#define HAS_PJMEDIA_CONFERENCE +#define HAS_PJMEDIA_MASTER_PORT +#define HAS_PJMEDIA_RTP +#define HAS_PJMEDIA_RTCP +#define HAS_PJMEDIA_JBUF +#define HAS_PJMEDIA_STREAM +#define HAS_PJMEDIA_TONEGEN +#define HAS_PJMEDIA_UDP_TRANSPORT +#define HAS_PJMEDIA_FILE_PLAYER +#define HAS_PJMEDIA_FILE_CAPTURE +#define HAS_PJMEDIA_MEM_PLAYER +#define HAS_PJMEDIA_MEM_CAPTURE +#define HAS_PJMEDIA_ICE + +#define HAS_PJMEDIA_G711_CODEC +#define HAS_PJMEDIA_GSM_CODEC +#define HAS_PJMEDIA_SPEEX_CODEC +#define HAS_PJMEDIA_ILBC_CODEC +#endif + + +int dummy_function() +{ + pj_caching_pool cp; + + sprintf(NULL, "%d", 0); + rand(); + +#ifdef HAS_PJLIB + pj_init(); + pj_caching_pool_init(&cp, NULL, 0); + pj_array_erase(NULL, 0, 0, 0); + pj_create_unique_string(NULL, NULL); + pj_hash_create(NULL, 0); + pj_hash_get(NULL, NULL, 0, NULL); + pj_hash_set(NULL, NULL, NULL, 0, 0, NULL); + pj_ioqueue_create(NULL, 0, NULL); + pj_ioqueue_register_sock(NULL, NULL, 0, NULL, NULL, NULL); + pj_pool_alloc(NULL, 0); + pj_timer_heap_create(NULL, 0, NULL); +#endif + +#ifdef HAS_PJLIB_STUN + pjstun_get_mapped_addr(&cp.factory, 0, NULL, NULL, 80, NULL, 80, NULL); +#endif + +#ifdef HAS_PJLIB_GETOPT + pj_getopt_long(0, NULL, NULL, NULL, NULL); +#endif + +#ifdef HAS_PJLIB_XML + pj_xml_parse(NULL, NULL, 100); + pj_xml_print(NULL, NULL, 10, PJ_FALSE); + pj_xml_clone(NULL, NULL); + pj_xml_node_new(NULL, NULL); + pj_xml_attr_new(NULL, NULL, NULL); + pj_xml_add_node(NULL, NULL); + pj_xml_add_attr(NULL, NULL); + pj_xml_find_node(NULL, NULL); + pj_xml_find_next_node(NULL, NULL, NULL); + pj_xml_find_attr(NULL, NULL, NULL); + pj_xml_find(NULL, NULL, NULL, NULL); +#endif + +#ifdef HAS_PJLIB_SCANNER + pj_cis_buf_init(NULL); + pj_cis_init(NULL, NULL); + pj_cis_dup(NULL, NULL); + pj_cis_add_alpha(NULL); + pj_cis_add_str(NULL, NULL); + + pj_scan_init(NULL, NULL, 0, 0, NULL); + pj_scan_fini(NULL); + pj_scan_peek(NULL, NULL, NULL); + pj_scan_peek_n(NULL, 0, NULL); + pj_scan_peek_until(NULL, NULL, NULL); + pj_scan_get(NULL, NULL, NULL); + pj_scan_get_unescape(NULL, NULL, NULL); + pj_scan_get_quote(NULL, 0, 0, NULL); + pj_scan_get_n(NULL, 0, NULL); + pj_scan_get_char(NULL); + pj_scan_get_until(NULL, NULL, NULL); + pj_scan_strcmp(NULL, NULL, 0); + pj_scan_stricmp(NULL, NULL, 0); + pj_scan_stricmp_alnum(NULL, NULL, 0); + pj_scan_get_newline(NULL); + pj_scan_restore_state(NULL, NULL); +#endif + +#ifdef HAS_PJLIB_DNS + pj_dns_make_query(NULL, NULL, 0, 0, NULL); + pj_dns_parse_packet(NULL, NULL, 0, NULL); + pj_dns_packet_dup(NULL, NULL, 0, NULL); +#endif + +#ifdef HAS_PJLIB_RESOLVER + pj_dns_resolver_create(NULL, NULL, 0, NULL, NULL, NULL); + pj_dns_resolver_set_ns(NULL, 0, NULL, NULL); + pj_dns_resolver_handle_events(NULL, NULL); + pj_dns_resolver_destroy(NULL, 0); + pj_dns_resolver_start_query(NULL, NULL, 0, 0, NULL, NULL, NULL); + pj_dns_resolver_cancel_query(NULL, 0); + pj_dns_resolver_add_entry(NULL, NULL, 0); +#endif + +#ifdef HAS_PJLIB_SRV_RESOLVER + pj_dns_srv_resolve(NULL, NULL, 0, NULL, NULL, PJ_FALSE, NULL, NULL); +#endif + +#ifdef HAS_PJLIB_CRC32 + pj_crc32_init(NULL); + pj_crc32_update(NULL, NULL, 0); + pj_crc32_final(NULL); +#endif + +#ifdef HAS_PJLIB_HMAC_MD5 + pj_hmac_md5(NULL, 0, NULL, 0, NULL); +#endif + +#ifdef HAS_PJLIB_HMAC_SHA1 + pj_hmac_sha1(NULL, 0, NULL, 0, NULL); +#endif + +#ifdef HAS_PJNATH_STUN + pj_stun_session_create(NULL, NULL, NULL, PJ_FALSE, NULL); + pj_stun_session_destroy(NULL); + pj_stun_session_set_credential(NULL, NULL); + pj_stun_session_create_req(NULL, 0, NULL, NULL); + pj_stun_session_create_ind(NULL, 0, NULL); + pj_stun_session_create_res(NULL, NULL, 0, NULL, NULL); + pj_stun_session_send_msg(NULL, PJ_FALSE, NULL, 0, NULL); +#endif + +#ifdef HAS_PJNATH_ICE + pj_ice_strans_create(NULL, NULL, 0, NULL, NULL, NULL); + pj_ice_strans_set_stun_domain(NULL, NULL, NULL); + pj_ice_strans_create_comp(NULL, 0, 0, NULL); + pj_ice_strans_add_cand(NULL, 0, PJ_ICE_CAND_TYPE_HOST, 0, NULL, PJ_FALSE); + pj_ice_strans_init_ice(NULL, PJ_ICE_SESS_ROLE_CONTROLLED, NULL, NULL); + pj_ice_strans_start_ice(NULL, NULL, NULL, 0, NULL); + pj_ice_strans_stop_ice(NULL); + pj_ice_strans_sendto(NULL, 0, NULL, 0, NULL, 0); +#endif + +#ifdef HAS_PJSIP_CORE_MSG_ELEM + /* Parameter container */ + pjsip_param_find(NULL, NULL); + pjsip_param_print_on(NULL, NULL, 0, NULL, NULL, 0); + + /* SIP URI */ + pjsip_sip_uri_create(NULL, 0); + pjsip_name_addr_create(NULL); + + /* TEL URI */ + pjsip_tel_uri_create(NULL); + + /* Message and headers */ + pjsip_msg_create(NULL, PJSIP_REQUEST_MSG); + pjsip_msg_print(NULL, NULL, 0); + pjsip_accept_hdr_create(NULL); + pjsip_allow_hdr_create(NULL); + pjsip_cid_hdr_create(NULL); + pjsip_clen_hdr_create(NULL); + pjsip_cseq_hdr_create(NULL); + pjsip_contact_hdr_create(NULL); + pjsip_ctype_hdr_create(NULL); + pjsip_expires_hdr_create(NULL, 0); + pjsip_from_hdr_create(NULL); + pjsip_max_fwd_hdr_create(NULL, 0); + pjsip_min_expires_hdr_create(NULL, 0); + pjsip_rr_hdr_create(NULL); + pjsip_require_hdr_create(NULL); + pjsip_retry_after_hdr_create(NULL, 0); + pjsip_supported_hdr_create(NULL); + pjsip_unsupported_hdr_create(NULL); + pjsip_via_hdr_create(NULL); + pjsip_warning_hdr_create(NULL, 0, NULL, NULL); + + pjsip_parse_uri(NULL, NULL, 0, 0); + pjsip_parse_msg(NULL, NULL, 0, NULL); + pjsip_parse_rdata(NULL, 0, NULL); + pjsip_find_msg(NULL, 0, 0, NULL); +#endif + +#ifdef HAS_PJSIP_CORE + pjsip_endpt_create(NULL, NULL, NULL); + + pjsip_tpmgr_create(NULL, NULL, NULL, NULL, NULL); + pjsip_tpmgr_destroy(NULL); + pjsip_transport_send(NULL, NULL, NULL, 0, NULL, NULL); + + +#endif + +#ifdef HAS_PJSIP_CORE_MSG_UTIL + pjsip_endpt_create_request(NULL, NULL, NULL, NULL, NULL, NULL, NULL, + -1, NULL, NULL); + pjsip_endpt_create_request_from_hdr(NULL, NULL, NULL, NULL, NULL, NULL, + NULL, -1, NULL, NULL); + pjsip_endpt_create_response(NULL, NULL, -1, NULL, NULL); + pjsip_endpt_create_ack(NULL, NULL, NULL, NULL); + pjsip_endpt_create_cancel(NULL, NULL, NULL); + pjsip_get_request_dest(NULL, NULL); + pjsip_endpt_send_request_stateless(NULL, NULL, NULL, NULL); + pjsip_get_response_addr(NULL, NULL, NULL); + pjsip_endpt_send_response(NULL, NULL, NULL, NULL, NULL); + pjsip_endpt_respond_stateless(NULL, NULL, -1, NULL, NULL, NULL); +#endif + +#ifdef HAS_PJSIP_UDP_TRANSPORT + pjsip_udp_transport_start(NULL, NULL, NULL, 1, NULL); +#endif + +#ifdef HAS_PJSIP_TCP_TRANSPORT + pjsip_tcp_transport_start(NULL, NULL, 1, NULL); +#endif + +#ifdef HAS_PJSIP_TLS_TRANSPORT + pjsip_tls_transport_start(NULL, NULL, NULL, NULL, 0, NULL); +#endif + +#ifdef HAS_PJSIP_TRANSACTION + pjsip_tsx_layer_init_module(NULL); + + pjsip_tsx_layer_destroy(); + pjsip_tsx_create_uac(NULL, NULL, NULL); + pjsip_tsx_create_uas(NULL, NULL, NULL); + pjsip_tsx_recv_msg(NULL, NULL); + pjsip_tsx_send_msg(NULL, NULL); + pjsip_tsx_terminate(NULL, 200); + + pjsip_endpt_send_request(NULL, NULL, -1, NULL, NULL); + pjsip_endpt_respond(NULL, NULL, NULL, -1, NULL, NULL, NULL, NULL); +#endif + +#ifdef HAS_PJMEDIA_SDP + pjmedia_sdp_parse(NULL, NULL, 1024, NULL); + pjmedia_sdp_print(NULL, NULL, 1024); + pjmedia_sdp_validate(NULL); + pjmedia_sdp_session_clone(NULL, NULL); + pjmedia_sdp_session_cmp(NULL, NULL, 0); + pjmedia_sdp_attr_to_rtpmap(NULL, NULL, NULL); + pjmedia_sdp_attr_get_fmtp(NULL, NULL); + pjmedia_sdp_attr_get_rtcp(NULL, NULL); + pjmedia_sdp_conn_clone(NULL, NULL); + pjmedia_sdp_media_clone(NULL, NULL); + pjmedia_sdp_media_find_attr(NULL, NULL, NULL); +#endif + +#ifdef HAS_PJMEDIA_SDP_NEGOTIATOR + pjmedia_sdp_neg_create_w_local_offer(NULL, NULL, NULL); + pjmedia_sdp_neg_create_w_remote_offer(NULL, NULL, NULL, NULL); + pjmedia_sdp_neg_get_state(NULL); + pjmedia_sdp_neg_negotiate(NULL, NULL, PJ_FALSE); +#endif + +#ifdef HAS_PJSIP_UA_LAYER + pjsip_ua_init_module(NULL, NULL); + pjsip_ua_destroy(); + pjsip_dlg_create_uac(NULL, NULL, NULL, NULL, NULL, NULL); + pjsip_dlg_create_uas(NULL, NULL, NULL, NULL); + pjsip_dlg_terminate(NULL); + pjsip_dlg_set_route_set(NULL, NULL); + pjsip_dlg_create_request(NULL, NULL, -1, NULL); + pjsip_dlg_send_request(NULL, NULL, -1, NULL); + pjsip_dlg_create_response(NULL, NULL, -1, NULL, NULL); + pjsip_dlg_modify_response(NULL, NULL, -1, NULL); + pjsip_dlg_send_response(NULL, NULL, NULL); + pjsip_dlg_respond(NULL, NULL, -1, NULL, NULL, NULL); +#endif + +#ifdef HAS_PJSIP_AUTH_CLIENT + pjsip_auth_clt_init(NULL, NULL, NULL, 0); + pjsip_auth_clt_clone(NULL, NULL, NULL); + pjsip_auth_clt_set_credentials(NULL, 0, NULL); + pjsip_auth_clt_init_req(NULL, NULL); + pjsip_auth_clt_reinit_req(NULL, NULL, NULL, NULL); +#endif + +#ifdef HAS_PJSIP_INV_SESSION + pjsip_inv_usage_init(NULL, NULL); + pjsip_inv_create_uac(NULL, NULL, 0, NULL); + pjsip_inv_verify_request(NULL, NULL, NULL, NULL, NULL, NULL); + pjsip_inv_create_uas(NULL, NULL, NULL, 0, NULL); + pjsip_inv_terminate(NULL, 200, PJ_FALSE); + pjsip_inv_invite(NULL, NULL); + pjsip_inv_initial_answer(NULL, NULL, 200, NULL, NULL, NULL); + pjsip_inv_answer(NULL, 200, NULL, NULL, NULL); + pjsip_inv_end_session(NULL, 200, NULL, NULL); + pjsip_inv_reinvite(NULL, NULL, NULL, NULL); + pjsip_inv_update(NULL, NULL, NULL, NULL); + pjsip_inv_send_msg(NULL, NULL); + pjsip_dlg_get_inv_session(NULL); + //pjsip_tsx_get_inv_session(NULL); + pjsip_inv_state_name(PJSIP_INV_STATE_NULL); +#endif + +#ifdef HAS_PJSIP_REGC + //pjsip_regc_get_module(); + pjsip_regc_create(NULL, NULL, NULL, NULL); + pjsip_regc_destroy(NULL); + pjsip_regc_get_info(NULL, NULL); + pjsip_regc_get_pool(NULL); + pjsip_regc_init(NULL, NULL, NULL, NULL, 0, NULL, 600); + pjsip_regc_set_credentials(NULL, 1, NULL); + pjsip_regc_set_route_set(NULL, NULL); + pjsip_regc_register(NULL, PJ_TRUE, NULL); + pjsip_regc_unregister(NULL, NULL); + pjsip_regc_update_contact(NULL, 10, NULL); + pjsip_regc_update_expires(NULL, 600); + pjsip_regc_send(NULL, NULL); +#endif + +#ifdef HAS_PJSIP_EVENT_FRAMEWORK + pjsip_evsub_init_module(NULL); + pjsip_evsub_instance(); + pjsip_evsub_register_pkg(NULL, NULL, 30, 10, NULL); + pjsip_evsub_create_uac(NULL, NULL, NULL, 10, NULL); + pjsip_evsub_create_uas(NULL, NULL, NULL, 10, NULL); + pjsip_evsub_terminate(NULL, PJ_FALSE); + pjsip_evsub_get_state(NULL); + pjsip_evsub_get_state_name(NULL); + pjsip_evsub_initiate(NULL, NULL, -1, NULL); + pjsip_evsub_accept(NULL, NULL, 200, NULL); + pjsip_evsub_notify(NULL, PJSIP_EVSUB_STATE_ACTIVE, NULL, NULL, NULL); + pjsip_evsub_current_notify(NULL, NULL); + pjsip_evsub_send_request(NULL, NULL); + pjsip_tsx_get_evsub(NULL); + pjsip_evsub_set_mod_data(NULL, 1, NULL); + pjsip_evsub_get_mod_data(NULL, 1); +#endif + +#ifdef HAS_PJSIP_CALL_TRANSFER + pjsip_xfer_init_module(NULL); + pjsip_xfer_create_uac(NULL, NULL, NULL); + pjsip_xfer_create_uas(NULL, NULL, NULL, NULL); + pjsip_xfer_initiate(NULL, NULL, NULL); + pjsip_xfer_accept(NULL, NULL, 200, NULL); + pjsip_xfer_notify(NULL, PJSIP_EVSUB_STATE_ACTIVE, 200, NULL, NULL); + pjsip_xfer_current_notify(NULL, NULL); + pjsip_xfer_send_request(NULL, NULL); +#endif + +#ifdef HAS_PJSIP_PRESENCE + pjsip_pres_init_module(NULL, NULL); + pjsip_pres_instance(); + pjsip_pres_create_uac(NULL, NULL, 0, NULL); + pjsip_pres_create_uas(NULL, NULL, NULL, NULL); + pjsip_pres_terminate(NULL, PJ_FALSE); + pjsip_pres_initiate(NULL, 100, NULL); + pjsip_pres_accept(NULL, NULL, 200, NULL); + pjsip_pres_notify(NULL, PJSIP_EVSUB_STATE_ACTIVE, NULL, NULL, NULL); + pjsip_pres_current_notify(NULL, NULL); + pjsip_pres_send_request(NULL, NULL); + pjsip_pres_get_status(NULL, NULL); + pjsip_pres_set_status(NULL, NULL); +#endif + +#ifdef HAS_PJSIP_IS_COMPOSING + pjsip_iscomposing_create_xml(NULL, PJ_TRUE, NULL, NULL, 0); + pjsip_iscomposing_create_body(NULL, PJ_TRUE, NULL, NULL, 0); + pjsip_iscomposing_parse(NULL, NULL, 0, NULL, NULL, NULL, NULL); +#endif + +#ifdef HAS_PJMEDIA + pjmedia_endpt_create(NULL, NULL, 1, NULL); + pjmedia_endpt_destroy(NULL); + pjmedia_endpt_create_sdp(NULL, NULL, 1, NULL, NULL); +#endif + +#ifdef HAS_PJMEDIA_EC + pjmedia_echo_create(NULL, 0, 0, 0, 0, 0, NULL); + pjmedia_echo_destroy(NULL); + pjmedia_echo_playback(NULL, NULL); + pjmedia_echo_capture(NULL, NULL, 0); + pjmedia_echo_cancel(NULL, NULL, NULL, 0, NULL); +#endif + +#ifdef HAS_PJMEDIA_SND_DEV + pjmedia_snd_init(NULL); + pjmedia_snd_get_dev_count(); + pjmedia_snd_get_dev_info(0); + pjmedia_snd_open(-1, -1, 8000, 1, 80, 16, NULL, NULL, NULL, NULL); + pjmedia_snd_open_rec(-1, 8000, 1, 160, 16, NULL, NULL, NULL); + pjmedia_snd_open_player(-1, 8000, 1, 160, 16, NULL, NULL, NULL); + pjmedia_snd_stream_start(NULL); + pjmedia_snd_stream_stop(NULL); + pjmedia_snd_stream_close(NULL); + pjmedia_snd_deinit(); +#endif + +#ifdef HAS_PJMEDIA_SND_PORT + pjmedia_snd_port_create(NULL, -1, -1, 8000, 1, 180, 16, 0, NULL); + pjmedia_snd_port_create_rec(NULL, -1, 8000, 1, 160, 16, 0, NULL); + pjmedia_snd_port_create_player(NULL, -1, 8000, 1, 160, 16, 0, NULL); + pjmedia_snd_port_destroy(NULL); + pjmedia_snd_port_get_snd_stream(NULL); + pjmedia_snd_port_connect(NULL, NULL); + pjmedia_snd_port_get_port(NULL); + pjmedia_snd_port_disconnect(NULL); +#endif + +#ifdef HAS_PJMEDIA_RESAMPLE + pjmedia_resample_create(NULL, PJ_TRUE, PJ_TRUE, 0, 0, 0, 0, NULL); + pjmedia_resample_run(NULL, NULL, NULL); +#endif + +#ifdef HAS_PJMEDIA_SILENCE_DET + pjmedia_silence_det_create(NULL, 8000, 80, NULL); + pjmedia_silence_det_detect(NULL, NULL, 0, NULL); + pjmedia_silence_det_apply(NULL, 0); +#endif + +#ifdef HAS_PJMEDIA_PLC + pjmedia_plc_create(NULL, 8000, 80, 0, NULL); + pjmedia_plc_save(NULL, NULL); + pjmedia_plc_generate(NULL, NULL); +#endif + +#ifdef HAS_PJMEDIA_CONFERENCE + pjmedia_conf_create(NULL, 10, 8000, 1, 160, 16, 0, NULL); + pjmedia_conf_destroy(NULL); + pjmedia_conf_get_master_port(NULL); + pjmedia_conf_add_port(NULL, NULL, NULL, NULL, NULL); + pjmedia_conf_configure_port(NULL, 1, 0, 0); + pjmedia_conf_connect_port(NULL, 0, 0, 0); + pjmedia_conf_disconnect_port(NULL, 0, 0); + pjmedia_conf_remove_port(NULL, 0); + pjmedia_conf_enum_ports(NULL, NULL, NULL); + pjmedia_conf_get_port_info(NULL, 0, NULL); + pjmedia_conf_get_ports_info(NULL, NULL, NULL); + pjmedia_conf_get_signal_level(NULL, 0, NULL, NULL); + pjmedia_conf_adjust_rx_level(NULL, 0, 0); + pjmedia_conf_adjust_tx_level(NULL, 0, 0); +#endif + +#ifdef HAS_PJMEDIA_MASTER_PORT + pjmedia_master_port_create(NULL, NULL, NULL, 0, NULL); + pjmedia_master_port_start(NULL); + pjmedia_master_port_stop(NULL); + pjmedia_master_port_set_uport(NULL, NULL); + pjmedia_master_port_get_uport(NULL); + pjmedia_master_port_set_dport(NULL, NULL); + pjmedia_master_port_get_dport(NULL); + pjmedia_master_port_destroy(NULL, PJ_FALSE); +#endif + +#ifdef HAS_PJMEDIA_RTP + pjmedia_rtp_session_init(NULL, 0, 0); + pjmedia_rtp_encode_rtp(NULL, 0, 0, 0, 0, NULL, NULL); + pjmedia_rtp_decode_rtp(NULL, NULL, 0, NULL, NULL, NULL); + pjmedia_rtp_session_update(NULL, NULL, NULL); +#endif + +#ifdef HAS_PJMEDIA_RTCP + pjmedia_rtcp_init(NULL, NULL, 0, 0, 0); + pjmedia_rtcp_get_ntp_time(NULL, NULL); + pjmedia_rtcp_fini(NULL); + pjmedia_rtcp_rx_rtp(NULL, 0, 0, 0); + pjmedia_rtcp_tx_rtp(NULL, 0); + pjmedia_rtcp_rx_rtcp(NULL, NULL, 0); + pjmedia_rtcp_build_rtcp(NULL, NULL, NULL); +#endif + +#ifdef HAS_PJMEDIA_JBUF + pjmedia_jbuf_create(NULL, NULL, 0, 0, 0, NULL); + pjmedia_jbuf_set_fixed(NULL, 0); + pjmedia_jbuf_set_adaptive(NULL, 0, 0, 0); + pjmedia_jbuf_destroy(NULL); + pjmedia_jbuf_put_frame(NULL, NULL, 0, 0); + pjmedia_jbuf_get_frame(NULL, NULL, NULL); +#endif + +#ifdef HAS_PJMEDIA_STREAM + pjmedia_stream_create(NULL, NULL, NULL, NULL, NULL, NULL); + pjmedia_stream_destroy(NULL); + pjmedia_stream_get_port(NULL, NULL); + pjmedia_stream_get_transport(NULL); + pjmedia_stream_start(NULL); + pjmedia_stream_get_stat(NULL, NULL); + pjmedia_stream_pause(NULL, PJMEDIA_DIR_ENCODING); + pjmedia_stream_resume(NULL, PJMEDIA_DIR_ENCODING); + pjmedia_stream_dial_dtmf(NULL, NULL); + pjmedia_stream_check_dtmf(NULL); + pjmedia_stream_get_dtmf(NULL, NULL, NULL); +#endif + +#ifdef HAS_PJMEDIA_TONEGEN + pjmedia_tonegen_create(NULL, 0, 0, 0, 0, 0, NULL); + pjmedia_tonegen_is_busy(NULL); + pjmedia_tonegen_stop(NULL); + pjmedia_tonegen_play(NULL, 0, NULL, 0); + pjmedia_tonegen_play_digits(NULL, 0, NULL, 0); + pjmedia_tonegen_get_digit_map(NULL, NULL); + pjmedia_tonegen_set_digit_map(NULL, NULL); +#endif + +#ifdef HAS_PJMEDIA_UDP_TRANSPORT + pjmedia_transport_udp_create(NULL, NULL, 0, 0, NULL); + pjmedia_transport_udp_close(NULL); +#endif + +#ifdef HAS_PJMEDIA_FILE_PLAYER + pjmedia_wav_player_port_create(NULL, NULL, 0, 0, 0, NULL); + pjmedia_wav_player_port_set_pos(NULL, 0); + pjmedia_wav_player_port_get_pos(NULL); + pjmedia_wav_player_set_eof_cb(NULL, NULL, NULL); +#endif + +#ifdef HAS_PJMEDIA_FILE_CAPTURE + pjmedia_wav_writer_port_create(NULL, NULL, 8000, 1, 80, 16, 0, 0, NULL); + pjmedia_wav_writer_port_get_pos(NULL); + pjmedia_wav_writer_port_set_cb(NULL, 0, NULL, NULL); +#endif + +#ifdef HAS_PJMEDIA_MEM_PLAYER + pjmedia_mem_player_create(NULL, NULL, 1000, 8000, 1, 80, 16, 0, NULL); +#endif + +#ifdef HAS_PJMEDIA_MEM_CAPTURE + pjmedia_mem_capture_create(NULL, NULL, 1000, 8000, 1, 80, 16, 0, NULL); +#endif + +#ifdef HAS_PJMEDIA_ICE + pjmedia_ice_create(NULL, NULL, 0, NULL, NULL); + pjmedia_ice_destroy(NULL); + pjmedia_ice_start_init(NULL, 0, NULL, NULL, NULL); + pjmedia_ice_init_ice(NULL, PJ_ICE_SESS_ROLE_CONTROLLED, NULL, NULL); + pjmedia_ice_modify_sdp(NULL, NULL, NULL); + pjmedia_ice_start_ice(NULL, NULL, NULL, 0); + pjmedia_ice_stop_ice(NULL); +#endif + +#ifdef HAS_PJMEDIA_G711_CODEC + pjmedia_codec_g711_init(NULL); + pjmedia_codec_g711_deinit(); +#endif + +#ifdef HAS_PJMEDIA_GSM_CODEC + pjmedia_codec_gsm_init(NULL); + pjmedia_codec_gsm_deinit(); +#endif + +#ifdef HAS_PJMEDIA_SPEEX_CODEC + pjmedia_codec_speex_init(NULL, 0, 0, 0); + pjmedia_codec_speex_deinit(); +#endif + +#ifdef HAS_PJMEDIA_ILBC_CODEC + pjmedia_codec_ilbc_init(NULL, 0); + pjmedia_codec_ilbc_deinit(); +#endif + + return 0; +} + + +int test_main() +{ + return dummy_function(); +} + +#if defined(PJ_RTEMS) && PJ_RTEMS!=0 +# include "../../pjlib/src/pjlib-test/main_rtems.c" +#else +int main() +{ + return test_main(); +} +#endif + diff --git a/pjsip-apps/src/samples/httpdemo.c b/pjsip-apps/src/samples/httpdemo.c new file mode 100644 index 0000000..6b2bf8e --- /dev/null +++ b/pjsip-apps/src/samples/httpdemo.c @@ -0,0 +1,183 @@ +/* $Id: httpdemo.c 3553 2011-05-05 06:14:19Z 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 + */ + +/** + * \page page_httpdemo_c Samples: HTTP Client demo + * + * This file is pjsip-apps/src/samples/httpdemo.c + * + * \includelineno httpdemo.c + */ + +#include <pjlib.h> +#include <pjlib-util.h> +#include <pjlib-util/http_client.h> +#include <pjsip.h> +#include <pjmedia.h> +#include <pjnath.h> +#include <pjsip_simple.h> + +static pj_timer_heap_t *timer_heap; +static pj_ioqueue_t *ioqueue; +static pj_pool_t *pool; +static pj_http_req *http_req; +static pj_pool_factory *mem; +static FILE *f = NULL; + +//#define VERBOSE +#define THIS_FILE "http_demo" + +static void on_response(pj_http_req *http_req, const pj_http_resp *resp) +{ + unsigned i; + + PJ_UNUSED_ARG(http_req); + PJ_LOG(3,(THIS_FILE, "%.*s %d %.*s", (int)resp->version.slen, resp->version.ptr, + resp->status_code, + (int)resp->reason.slen, resp->reason.ptr)); + + for (i=0; i<resp->headers.count; ++i) { + const pj_http_header_elmt *h = &resp->headers.header[i]; + + if (!pj_stricmp2(&h->name, "Content-Length") || + !pj_stricmp2(&h->name, "Content-Type")) + { + PJ_LOG(3,(THIS_FILE, "%.*s: %.*s", + (int)h->name.slen, h->name.ptr, + (int)h->value.slen, h->value.ptr)); + } + } +} + +static void on_send_data(pj_http_req *http_req, void **data, pj_size_t *size) +{ + PJ_UNUSED_ARG(http_req); + PJ_UNUSED_ARG(size); + PJ_UNUSED_ARG(data); +} + +static void on_data_read(pj_http_req *hreq, void *data, pj_size_t size) +{ + PJ_UNUSED_ARG(hreq); + + if (size > 0) { + fwrite(data, 1, size, f); + fflush(f); +#ifdef VERBOSE + PJ_LOG(3, (THIS_FILE, "Data received: %d bytes", size)); + printf("%.*s\n", (int)size, (char *)data); +#endif + } +} + +static void on_complete(pj_http_req *hreq, pj_status_t status, + const pj_http_resp *resp) +{ + PJ_UNUSED_ARG(hreq); + + if (status != PJ_SUCCESS) { + PJ_PERROR(1, (THIS_FILE, status, "HTTP request completed with error")); + return; + } + PJ_LOG(3, (THIS_FILE, "Data completed: %d bytes", resp->size)); + if (resp->size > 0 && resp->data) { +#ifdef VERBOSE + printf("%.*s\n", (int)resp->size, (char *)resp->data); +#endif + } +} + +pj_status_t getURL(const char *curl) +{ + pj_str_t url; + pj_http_req_callback hcb; + pj_status_t status; + + pj_bzero(&hcb, sizeof(hcb)); + hcb.on_complete = &on_complete; + hcb.on_data_read = &on_data_read; + hcb.on_send_data = &on_send_data; + hcb.on_response = &on_response; + + /* Create pool, timer, and ioqueue */ + pool = pj_pool_create(mem, NULL, 8192, 4096, NULL); + if (pj_timer_heap_create(pool, 16, &timer_heap)) + return -31; + if (pj_ioqueue_create(pool, 16, &ioqueue)) + return -32; + + pj_strdup2(pool, &url, curl); + + if ((status = pj_http_req_create(pool, &url, timer_heap, ioqueue, + NULL, &hcb, &http_req)) != PJ_SUCCESS) + return status; + + if ((status = pj_http_req_start(http_req)) != PJ_SUCCESS) + return status; + + while (pj_http_req_is_running(http_req)) { + pj_time_val delay = {0, 50}; + pj_ioqueue_poll(ioqueue, &delay); + pj_timer_heap_poll(timer_heap, NULL); + } + + pj_http_req_destroy(http_req); + pj_ioqueue_destroy(ioqueue); + pj_timer_heap_destroy(timer_heap); + pj_pool_release(pool); + + return PJ_SUCCESS; +} +/* + * main() + */ +int main(int argc, char *argv[]) +{ + pj_caching_pool cp; + pj_status_t status; + + if (argc < 2 || argc > 3) { + puts("Usage: httpdemo URL [output-filename]"); + return 1; + } + + pj_log_set_level(5); + + pj_init(); + pj_caching_pool_init(&cp, NULL, 0); + mem = &cp.factory; + pjlib_util_init(); + + if (argc > 2) + f = fopen(argv[2], "wb"); + else + f = stdout; + + status = getURL(argv[1]); + if (status != PJ_SUCCESS) { + PJ_PERROR(1, (THIS_FILE, status, "Error")); + } + + if (f != stdout) + fclose(f); + + pj_caching_pool_destroy(&cp); + pj_shutdown(); + return 0; +} diff --git a/pjsip-apps/src/samples/icedemo.c b/pjsip-apps/src/samples/icedemo.c new file mode 100644 index 0000000..08292ee --- /dev/null +++ b/pjsip-apps/src/samples/icedemo.c @@ -0,0 +1,1276 @@ +/* $Id: icedemo.c 3841 2011-10-24 09:28:13Z ming $ */ +/* + * 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 + */ +#include <stdio.h> +#include <stdlib.h> +#include <pjlib.h> +#include <pjlib-util.h> +#include <pjnath.h> + + +#define THIS_FILE "icedemo.c" + +/* For this demo app, configure longer STUN keep-alive time + * so that it does't clutter the screen output. + */ +#define KA_INTERVAL 300 + + +/* This is our global variables */ +static struct app_t +{ + /* Command line options are stored here */ + struct options + { + unsigned comp_cnt; + pj_str_t ns; + int max_host; + pj_bool_t regular; + pj_str_t stun_srv; + pj_str_t turn_srv; + pj_bool_t turn_tcp; + pj_str_t turn_username; + pj_str_t turn_password; + pj_bool_t turn_fingerprint; + const char *log_file; + } opt; + + /* Our global variables */ + pj_caching_pool cp; + pj_pool_t *pool; + pj_thread_t *thread; + pj_bool_t thread_quit_flag; + pj_ice_strans_cfg ice_cfg; + pj_ice_strans *icest; + FILE *log_fhnd; + + /* Variables to store parsed remote ICE info */ + struct rem_info + { + char ufrag[80]; + char pwd[80]; + unsigned comp_cnt; + pj_sockaddr def_addr[PJ_ICE_MAX_COMP]; + unsigned cand_cnt; + pj_ice_sess_cand cand[PJ_ICE_ST_MAX_CAND]; + } rem; + +} icedemo; + +/* Utility to display error messages */ +static void icedemo_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)); +} + +/* Utility: display error message and exit application (usually + * because of fatal error. + */ +static void err_exit(const char *title, pj_status_t status) +{ + if (status != PJ_SUCCESS) { + icedemo_perror(title, status); + } + PJ_LOG(3,(THIS_FILE, "Shutting down..")); + + if (icedemo.icest) + pj_ice_strans_destroy(icedemo.icest); + + pj_thread_sleep(500); + + icedemo.thread_quit_flag = PJ_TRUE; + if (icedemo.thread) { + pj_thread_join(icedemo.thread); + pj_thread_destroy(icedemo.thread); + } + + if (icedemo.ice_cfg.stun_cfg.ioqueue) + pj_ioqueue_destroy(icedemo.ice_cfg.stun_cfg.ioqueue); + + if (icedemo.ice_cfg.stun_cfg.timer_heap) + pj_timer_heap_destroy(icedemo.ice_cfg.stun_cfg.timer_heap); + + pj_caching_pool_destroy(&icedemo.cp); + + pj_shutdown(); + + if (icedemo.log_fhnd) { + fclose(icedemo.log_fhnd); + icedemo.log_fhnd = NULL; + } + + exit(status != PJ_SUCCESS); +} + +#define CHECK(expr) status=expr; \ + if (status!=PJ_SUCCESS) { \ + err_exit(#expr, status); \ + } + +/* + * This function checks for events from both timer and ioqueue (for + * network events). It is invoked by the worker thread. + */ +static pj_status_t handle_events(unsigned max_msec, unsigned *p_count) +{ + enum { MAX_NET_EVENTS = 1 }; + pj_time_val max_timeout = {0, 0}; + pj_time_val timeout = { 0, 0}; + unsigned count = 0, net_event_count = 0; + int c; + + max_timeout.msec = max_msec; + + /* Poll the timer to run it and also to retrieve the earliest entry. */ + timeout.sec = timeout.msec = 0; + c = pj_timer_heap_poll( icedemo.ice_cfg.stun_cfg.timer_heap, &timeout ); + if (c > 0) + count += c; + + /* timer_heap_poll should never ever returns negative value, or otherwise + * ioqueue_poll() will block forever! + */ + pj_assert(timeout.sec >= 0 && timeout.msec >= 0); + if (timeout.msec >= 1000) timeout.msec = 999; + + /* compare the value with the timeout to wait from timer, and use the + * minimum value. + */ + if (PJ_TIME_VAL_GT(timeout, max_timeout)) + timeout = max_timeout; + + /* Poll ioqueue. + * Repeat polling the ioqueue while we have immediate events, because + * timer heap may process more than one events, so if we only process + * one network events at a time (such as when IOCP backend is used), + * the ioqueue may have trouble keeping up with the request rate. + * + * For example, for each send() request, one network event will be + * reported by ioqueue for the send() completion. If we don't poll + * the ioqueue often enough, the send() completion will not be + * reported in timely manner. + */ + do { + c = pj_ioqueue_poll( icedemo.ice_cfg.stun_cfg.ioqueue, &timeout); + if (c < 0) { + pj_status_t err = pj_get_netos_error(); + pj_thread_sleep(PJ_TIME_VAL_MSEC(timeout)); + if (p_count) + *p_count = count; + return err; + } else if (c == 0) { + break; + } else { + net_event_count += c; + timeout.sec = timeout.msec = 0; + } + } while (c > 0 && net_event_count < MAX_NET_EVENTS); + + count += net_event_count; + if (p_count) + *p_count = count; + + return PJ_SUCCESS; + +} + +/* + * This is the worker thread that polls event in the background. + */ +static int icedemo_worker_thread(void *unused) +{ + PJ_UNUSED_ARG(unused); + + while (!icedemo.thread_quit_flag) { + handle_events(500, NULL); + } + + return 0; +} + +/* + * This is the callback that is registered to the ICE stream transport to + * receive notification about incoming data. By "data" it means application + * data such as RTP/RTCP, and not packets that belong to ICE signaling (such + * as STUN connectivity checks or TURN signaling). + */ +static void cb_on_rx_data(pj_ice_strans *ice_st, + unsigned comp_id, + void *pkt, pj_size_t size, + const pj_sockaddr_t *src_addr, + unsigned src_addr_len) +{ + char ipstr[PJ_INET6_ADDRSTRLEN+10]; + + PJ_UNUSED_ARG(ice_st); + PJ_UNUSED_ARG(src_addr_len); + PJ_UNUSED_ARG(pkt); + + // Don't do this! It will ruin the packet buffer in case TCP is used! + //((char*)pkt)[size] = '\0'; + + PJ_LOG(3,(THIS_FILE, "Component %d: received %d bytes data from %s: \"%.*s\"", + comp_id, size, + pj_sockaddr_print(src_addr, ipstr, sizeof(ipstr), 3), + (unsigned)size, + (char*)pkt)); +} + +/* + * This is the callback that is registered to the ICE stream transport to + * receive notification about ICE state progression. + */ +static void cb_on_ice_complete(pj_ice_strans *ice_st, + pj_ice_strans_op op, + pj_status_t status) +{ + const char *opname = + (op==PJ_ICE_STRANS_OP_INIT? "initialization" : + (op==PJ_ICE_STRANS_OP_NEGOTIATION ? "negotiation" : "unknown_op")); + + if (status == PJ_SUCCESS) { + PJ_LOG(3,(THIS_FILE, "ICE %s successful", opname)); + } else { + char errmsg[PJ_ERR_MSG_SIZE]; + + pj_strerror(status, errmsg, sizeof(errmsg)); + PJ_LOG(1,(THIS_FILE, "ICE %s failed: %s", opname, errmsg)); + pj_ice_strans_destroy(ice_st); + icedemo.icest = NULL; + } +} + +/* log callback to write to file */ +static void log_func(int level, const char *data, int len) +{ + pj_log_write(level, data, len); + if (icedemo.log_fhnd) { + if (fwrite(data, len, 1, icedemo.log_fhnd) != 1) + return; + } +} + +/* + * This is the main application initialization function. It is called + * once (and only once) during application initialization sequence by + * main(). + */ +static pj_status_t icedemo_init(void) +{ + pj_status_t status; + + if (icedemo.opt.log_file) { + icedemo.log_fhnd = fopen(icedemo.opt.log_file, "a"); + pj_log_set_log_func(&log_func); + } + + /* Initialize the libraries before anything else */ + CHECK( pj_init() ); + CHECK( pjlib_util_init() ); + CHECK( pjnath_init() ); + + /* Must create pool factory, where memory allocations come from */ + pj_caching_pool_init(&icedemo.cp, NULL, 0); + + /* Init our ICE settings with null values */ + pj_ice_strans_cfg_default(&icedemo.ice_cfg); + + icedemo.ice_cfg.stun_cfg.pf = &icedemo.cp.factory; + + /* Create application memory pool */ + icedemo.pool = pj_pool_create(&icedemo.cp.factory, "icedemo", + 512, 512, NULL); + + /* Create timer heap for timer stuff */ + CHECK( pj_timer_heap_create(icedemo.pool, 100, + &icedemo.ice_cfg.stun_cfg.timer_heap) ); + + /* and create ioqueue for network I/O stuff */ + CHECK( pj_ioqueue_create(icedemo.pool, 16, + &icedemo.ice_cfg.stun_cfg.ioqueue) ); + + /* something must poll the timer heap and ioqueue, + * unless we're on Symbian where the timer heap and ioqueue run + * on themselves. + */ + CHECK( pj_thread_create(icedemo.pool, "icedemo", &icedemo_worker_thread, + NULL, 0, 0, &icedemo.thread) ); + + icedemo.ice_cfg.af = pj_AF_INET(); + + /* Create DNS resolver if nameserver is set */ + if (icedemo.opt.ns.slen) { + CHECK( pj_dns_resolver_create(&icedemo.cp.factory, + "resolver", + 0, + icedemo.ice_cfg.stun_cfg.timer_heap, + icedemo.ice_cfg.stun_cfg.ioqueue, + &icedemo.ice_cfg.resolver) ); + + CHECK( pj_dns_resolver_set_ns(icedemo.ice_cfg.resolver, 1, + &icedemo.opt.ns, NULL) ); + } + + /* -= Start initializing ICE stream transport config =- */ + + /* Maximum number of host candidates */ + if (icedemo.opt.max_host != -1) + icedemo.ice_cfg.stun.max_host_cands = icedemo.opt.max_host; + + /* Nomination strategy */ + if (icedemo.opt.regular) + icedemo.ice_cfg.opt.aggressive = PJ_FALSE; + else + icedemo.ice_cfg.opt.aggressive = PJ_TRUE; + + /* Configure STUN/srflx candidate resolution */ + if (icedemo.opt.stun_srv.slen) { + char *pos; + + /* Command line option may contain port number */ + if ((pos=pj_strchr(&icedemo.opt.stun_srv, ':')) != NULL) { + icedemo.ice_cfg.stun.server.ptr = icedemo.opt.stun_srv.ptr; + icedemo.ice_cfg.stun.server.slen = (pos - icedemo.opt.stun_srv.ptr); + + icedemo.ice_cfg.stun.port = (pj_uint16_t)atoi(pos+1); + } else { + icedemo.ice_cfg.stun.server = icedemo.opt.stun_srv; + icedemo.ice_cfg.stun.port = PJ_STUN_PORT; + } + + /* For this demo app, configure longer STUN keep-alive time + * so that it does't clutter the screen output. + */ + icedemo.ice_cfg.stun.cfg.ka_interval = KA_INTERVAL; + } + + /* Configure TURN candidate */ + if (icedemo.opt.turn_srv.slen) { + char *pos; + + /* Command line option may contain port number */ + if ((pos=pj_strchr(&icedemo.opt.turn_srv, ':')) != NULL) { + icedemo.ice_cfg.turn.server.ptr = icedemo.opt.turn_srv.ptr; + icedemo.ice_cfg.turn.server.slen = (pos - icedemo.opt.turn_srv.ptr); + + icedemo.ice_cfg.turn.port = (pj_uint16_t)atoi(pos+1); + } else { + icedemo.ice_cfg.turn.server = icedemo.opt.turn_srv; + icedemo.ice_cfg.turn.port = PJ_STUN_PORT; + } + + /* TURN credential */ + icedemo.ice_cfg.turn.auth_cred.type = PJ_STUN_AUTH_CRED_STATIC; + icedemo.ice_cfg.turn.auth_cred.data.static_cred.username = icedemo.opt.turn_username; + icedemo.ice_cfg.turn.auth_cred.data.static_cred.data_type = PJ_STUN_PASSWD_PLAIN; + icedemo.ice_cfg.turn.auth_cred.data.static_cred.data = icedemo.opt.turn_password; + + /* Connection type to TURN server */ + if (icedemo.opt.turn_tcp) + icedemo.ice_cfg.turn.conn_type = PJ_TURN_TP_TCP; + else + icedemo.ice_cfg.turn.conn_type = PJ_TURN_TP_UDP; + + /* For this demo app, configure longer keep-alive time + * so that it does't clutter the screen output. + */ + icedemo.ice_cfg.turn.alloc_param.ka_interval = KA_INTERVAL; + } + + /* -= That's it for now, initialization is complete =- */ + return PJ_SUCCESS; +} + + +/* + * Create ICE stream transport instance, invoked from the menu. + */ +static void icedemo_create_instance(void) +{ + pj_ice_strans_cb icecb; + pj_status_t status; + + if (icedemo.icest != NULL) { + puts("ICE instance already created, destroy it first"); + return; + } + + /* init the callback */ + pj_bzero(&icecb, sizeof(icecb)); + icecb.on_rx_data = cb_on_rx_data; + icecb.on_ice_complete = cb_on_ice_complete; + + /* create the instance */ + status = pj_ice_strans_create("icedemo", /* object name */ + &icedemo.ice_cfg, /* settings */ + icedemo.opt.comp_cnt, /* comp_cnt */ + NULL, /* user data */ + &icecb, /* callback */ + &icedemo.icest) /* instance ptr */ + ; + if (status != PJ_SUCCESS) + icedemo_perror("error creating ice", status); + else + PJ_LOG(3,(THIS_FILE, "ICE instance successfully created")); +} + +/* Utility to nullify parsed remote info */ +static void reset_rem_info(void) +{ + pj_bzero(&icedemo.rem, sizeof(icedemo.rem)); +} + + +/* + * Destroy ICE stream transport instance, invoked from the menu. + */ +static void icedemo_destroy_instance(void) +{ + if (icedemo.icest == NULL) { + PJ_LOG(1,(THIS_FILE, "Error: No ICE instance, create it first")); + return; + } + + pj_ice_strans_destroy(icedemo.icest); + icedemo.icest = NULL; + + reset_rem_info(); + + PJ_LOG(3,(THIS_FILE, "ICE instance destroyed")); +} + + +/* + * Create ICE session, invoked from the menu. + */ +static void icedemo_init_session(unsigned rolechar) +{ + pj_ice_sess_role role = (pj_tolower((pj_uint8_t)rolechar)=='o' ? + PJ_ICE_SESS_ROLE_CONTROLLING : + PJ_ICE_SESS_ROLE_CONTROLLED); + pj_status_t status; + + if (icedemo.icest == NULL) { + PJ_LOG(1,(THIS_FILE, "Error: No ICE instance, create it first")); + return; + } + + if (pj_ice_strans_has_sess(icedemo.icest)) { + PJ_LOG(1,(THIS_FILE, "Error: Session already created")); + return; + } + + status = pj_ice_strans_init_ice(icedemo.icest, role, NULL, NULL); + if (status != PJ_SUCCESS) + icedemo_perror("error creating session", status); + else + PJ_LOG(3,(THIS_FILE, "ICE session created")); + + reset_rem_info(); +} + + +/* + * Stop/destroy ICE session, invoked from the menu. + */ +static void icedemo_stop_session(void) +{ + pj_status_t status; + + if (icedemo.icest == NULL) { + PJ_LOG(1,(THIS_FILE, "Error: No ICE instance, create it first")); + return; + } + + if (!pj_ice_strans_has_sess(icedemo.icest)) { + PJ_LOG(1,(THIS_FILE, "Error: No ICE session, initialize first")); + return; + } + + status = pj_ice_strans_stop_ice(icedemo.icest); + if (status != PJ_SUCCESS) + icedemo_perror("error stopping session", status); + else + PJ_LOG(3,(THIS_FILE, "ICE session stopped")); + + reset_rem_info(); +} + +#define PRINT(fmt, arg0, arg1, arg2, arg3, arg4, arg5) \ + printed = pj_ansi_snprintf(p, maxlen - (p-buffer), \ + fmt, arg0, arg1, arg2, arg3, arg4, arg5); \ + if (printed <= 0) return -PJ_ETOOSMALL; \ + p += printed + + +/* Utility to create a=candidate SDP attribute */ +static int print_cand(char buffer[], unsigned maxlen, + const pj_ice_sess_cand *cand) +{ + char ipaddr[PJ_INET6_ADDRSTRLEN]; + char *p = buffer; + int printed; + + PRINT("a=candidate:%.*s %u UDP %u %s %u typ ", + (int)cand->foundation.slen, + cand->foundation.ptr, + (unsigned)cand->comp_id, + cand->prio, + pj_sockaddr_print(&cand->addr, ipaddr, + sizeof(ipaddr), 0), + (unsigned)pj_sockaddr_get_port(&cand->addr)); + + PRINT("%s\n", + pj_ice_get_cand_type_name(cand->type), + 0, 0, 0, 0, 0); + + if (p == buffer+maxlen) + return -PJ_ETOOSMALL; + + *p = '\0'; + + return p-buffer; +} + +/* + * Encode ICE information in SDP. + */ +static int encode_session(char buffer[], unsigned maxlen) +{ + char *p = buffer; + unsigned comp; + int printed; + pj_str_t local_ufrag, local_pwd; + pj_status_t status; + + /* Write "dummy" SDP v=, o=, s=, and t= lines */ + PRINT("v=0\no=- 3414953978 3414953978 IN IP4 localhost\ns=ice\nt=0 0\n", + 0, 0, 0, 0, 0, 0); + + /* Get ufrag and pwd from current session */ + pj_ice_strans_get_ufrag_pwd(icedemo.icest, &local_ufrag, &local_pwd, + NULL, NULL); + + /* Write the a=ice-ufrag and a=ice-pwd attributes */ + PRINT("a=ice-ufrag:%.*s\na=ice-pwd:%.*s\n", + (int)local_ufrag.slen, + local_ufrag.ptr, + (int)local_pwd.slen, + local_pwd.ptr, + 0, 0); + + /* Write each component */ + for (comp=0; comp<icedemo.opt.comp_cnt; ++comp) { + unsigned j, cand_cnt; + pj_ice_sess_cand cand[PJ_ICE_ST_MAX_CAND]; + char ipaddr[PJ_INET6_ADDRSTRLEN]; + + /* Get default candidate for the component */ + status = pj_ice_strans_get_def_cand(icedemo.icest, comp+1, &cand[0]); + if (status != PJ_SUCCESS) + return -status; + + /* Write the default address */ + if (comp==0) { + /* For component 1, default address is in m= and c= lines */ + PRINT("m=audio %d RTP/AVP 0\n" + "c=IN IP4 %s\n", + (int)pj_sockaddr_get_port(&cand[0].addr), + pj_sockaddr_print(&cand[0].addr, ipaddr, + sizeof(ipaddr), 0), + 0, 0, 0, 0); + } else if (comp==1) { + /* For component 2, default address is in a=rtcp line */ + PRINT("a=rtcp:%d IN IP4 %s\n", + (int)pj_sockaddr_get_port(&cand[0].addr), + pj_sockaddr_print(&cand[0].addr, ipaddr, + sizeof(ipaddr), 0), + 0, 0, 0, 0); + } else { + /* For other components, we'll just invent this.. */ + PRINT("a=Xice-defcand:%d IN IP4 %s\n", + (int)pj_sockaddr_get_port(&cand[0].addr), + pj_sockaddr_print(&cand[0].addr, ipaddr, + sizeof(ipaddr), 0), + 0, 0, 0, 0); + } + + /* Enumerate all candidates for this component */ + status = pj_ice_strans_enum_cands(icedemo.icest, comp+1, + &cand_cnt, cand); + if (status != PJ_SUCCESS) + return -status; + + /* And encode the candidates as SDP */ + for (j=0; j<cand_cnt; ++j) { + printed = print_cand(p, maxlen - (p-buffer), &cand[j]); + if (printed < 0) + return -PJ_ETOOSMALL; + p += printed; + } + } + + if (p == buffer+maxlen) + return -PJ_ETOOSMALL; + + *p = '\0'; + return p - buffer; +} + + +/* + * Show information contained in the ICE stream transport. This is + * invoked from the menu. + */ +static void icedemo_show_ice(void) +{ + static char buffer[1000]; + int len; + + if (icedemo.icest == NULL) { + PJ_LOG(1,(THIS_FILE, "Error: No ICE instance, create it first")); + return; + } + + puts("General info"); + puts("---------------"); + printf("Component count : %d\n", icedemo.opt.comp_cnt); + printf("Status : "); + if (pj_ice_strans_sess_is_complete(icedemo.icest)) + puts("negotiation complete"); + else if (pj_ice_strans_sess_is_running(icedemo.icest)) + puts("negotiation is in progress"); + else if (pj_ice_strans_has_sess(icedemo.icest)) + puts("session ready"); + else + puts("session not created"); + + if (!pj_ice_strans_has_sess(icedemo.icest)) { + puts("Create the session first to see more info"); + return; + } + + printf("Negotiated comp_cnt: %d\n", + pj_ice_strans_get_running_comp_cnt(icedemo.icest)); + printf("Role : %s\n", + pj_ice_strans_get_role(icedemo.icest)==PJ_ICE_SESS_ROLE_CONTROLLED ? + "controlled" : "controlling"); + + len = encode_session(buffer, sizeof(buffer)); + if (len < 0) + err_exit("not enough buffer to show ICE status", -len); + + puts(""); + printf("Local SDP (paste this to remote host):\n" + "--------------------------------------\n" + "%s\n", buffer); + + + puts(""); + puts("Remote info:\n" + "----------------------"); + if (icedemo.rem.cand_cnt==0) { + puts("No remote info yet"); + } else { + unsigned i; + + printf("Remote ufrag : %s\n", icedemo.rem.ufrag); + printf("Remote password : %s\n", icedemo.rem.pwd); + printf("Remote cand. cnt. : %d\n", icedemo.rem.cand_cnt); + + for (i=0; i<icedemo.rem.cand_cnt; ++i) { + len = print_cand(buffer, sizeof(buffer), &icedemo.rem.cand[i]); + if (len < 0) + err_exit("not enough buffer to show ICE status", -len); + + printf(" %s", buffer); + } + } +} + + +/* + * Input and parse SDP from the remote (containing remote's ICE information) + * and save it to global variables. + */ +static void icedemo_input_remote(void) +{ + char linebuf[80]; + unsigned media_cnt = 0; + unsigned comp0_port = 0; + char comp0_addr[80]; + pj_bool_t done = PJ_FALSE; + + puts("Paste SDP from remote host, end with empty line"); + + reset_rem_info(); + + comp0_addr[0] = '\0'; + + while (!done) { + int len; + char *line; + + printf(">"); + if (stdout) fflush(stdout); + + if (fgets(linebuf, sizeof(linebuf), stdin)==NULL) + break; + + len = strlen(linebuf); + while (len && (linebuf[len-1] == '\r' || linebuf[len-1] == '\n')) + linebuf[--len] = '\0'; + + line = linebuf; + while (len && pj_isspace(*line)) + ++line, --len; + + if (len==0) + break; + + /* Ignore subsequent media descriptors */ + if (media_cnt > 1) + continue; + + switch (line[0]) { + case 'm': + { + int cnt; + char media[32], portstr[32]; + + ++media_cnt; + if (media_cnt > 1) { + puts("Media line ignored"); + break; + } + + cnt = sscanf(line+2, "%s %s RTP/", media, portstr); + if (cnt != 2) { + PJ_LOG(1,(THIS_FILE, "Error parsing media line")); + goto on_error; + } + + comp0_port = atoi(portstr); + + } + break; + case 'c': + { + int cnt; + char c[32], net[32], ip[80]; + + cnt = sscanf(line+2, "%s %s %s", c, net, ip); + if (cnt != 3) { + PJ_LOG(1,(THIS_FILE, "Error parsing connection line")); + goto on_error; + } + + strcpy(comp0_addr, ip); + } + break; + case 'a': + { + char *attr = strtok(line+2, ": \t\r\n"); + if (strcmp(attr, "ice-ufrag")==0) { + strcpy(icedemo.rem.ufrag, attr+strlen(attr)+1); + } else if (strcmp(attr, "ice-pwd")==0) { + strcpy(icedemo.rem.pwd, attr+strlen(attr)+1); + } else if (strcmp(attr, "rtcp")==0) { + char *val = attr+strlen(attr)+1; + int af, cnt; + int port; + char net[32], ip[64]; + pj_str_t tmp_addr; + pj_status_t status; + + cnt = sscanf(val, "%d IN %s %s", &port, net, ip); + if (cnt != 3) { + PJ_LOG(1,(THIS_FILE, "Error parsing rtcp attribute")); + goto on_error; + } + + if (strchr(ip, ':')) + af = pj_AF_INET6(); + else + af = pj_AF_INET(); + + pj_sockaddr_init(af, &icedemo.rem.def_addr[1], NULL, 0); + tmp_addr = pj_str(ip); + status = pj_sockaddr_set_str_addr(af, &icedemo.rem.def_addr[1], + &tmp_addr); + if (status != PJ_SUCCESS) { + PJ_LOG(1,(THIS_FILE, "Invalid IP address")); + goto on_error; + } + pj_sockaddr_set_port(&icedemo.rem.def_addr[1], (pj_uint16_t)port); + + } else if (strcmp(attr, "candidate")==0) { + char *sdpcand = attr+strlen(attr)+1; + int af, cnt; + char foundation[32], transport[12], ipaddr[80], type[32]; + pj_str_t tmpaddr; + int comp_id, prio, port; + pj_ice_sess_cand *cand; + pj_status_t status; + + cnt = sscanf(sdpcand, "%s %d %s %d %s %d typ %s", + foundation, + &comp_id, + transport, + &prio, + ipaddr, + &port, + type); + if (cnt != 7) { + PJ_LOG(1, (THIS_FILE, "error: Invalid ICE candidate line")); + goto on_error; + } + + cand = &icedemo.rem.cand[icedemo.rem.cand_cnt]; + pj_bzero(cand, sizeof(*cand)); + + if (strcmp(type, "host")==0) + cand->type = PJ_ICE_CAND_TYPE_HOST; + else if (strcmp(type, "srflx")==0) + cand->type = PJ_ICE_CAND_TYPE_SRFLX; + else if (strcmp(type, "relay")==0) + cand->type = PJ_ICE_CAND_TYPE_RELAYED; + else { + PJ_LOG(1, (THIS_FILE, "Error: invalid candidate type '%s'", + type)); + goto on_error; + } + + cand->comp_id = (pj_uint8_t)comp_id; + pj_strdup2(icedemo.pool, &cand->foundation, foundation); + cand->prio = prio; + + if (strchr(ipaddr, ':')) + af = pj_AF_INET6(); + else + af = pj_AF_INET(); + + tmpaddr = pj_str(ipaddr); + pj_sockaddr_init(af, &cand->addr, NULL, 0); + status = pj_sockaddr_set_str_addr(af, &cand->addr, &tmpaddr); + if (status != PJ_SUCCESS) { + PJ_LOG(1,(THIS_FILE, "Error: invalid IP address '%s'", + ipaddr)); + goto on_error; + } + + pj_sockaddr_set_port(&cand->addr, (pj_uint16_t)port); + + ++icedemo.rem.cand_cnt; + + if (cand->comp_id > icedemo.rem.comp_cnt) + icedemo.rem.comp_cnt = cand->comp_id; + } + } + break; + } + } + + if (icedemo.rem.cand_cnt==0 || + icedemo.rem.ufrag[0]==0 || + icedemo.rem.pwd[0]==0 || + icedemo.rem.comp_cnt == 0) + { + PJ_LOG(1, (THIS_FILE, "Error: not enough info")); + goto on_error; + } + + if (comp0_port==0 || comp0_addr[0]=='\0') { + PJ_LOG(1, (THIS_FILE, "Error: default address for component 0 not found")); + goto on_error; + } else { + int af; + pj_str_t tmp_addr; + pj_status_t status; + + if (strchr(comp0_addr, ':')) + af = pj_AF_INET6(); + else + af = pj_AF_INET(); + + pj_sockaddr_init(af, &icedemo.rem.def_addr[0], NULL, 0); + tmp_addr = pj_str(comp0_addr); + status = pj_sockaddr_set_str_addr(af, &icedemo.rem.def_addr[0], + &tmp_addr); + if (status != PJ_SUCCESS) { + PJ_LOG(1,(THIS_FILE, "Invalid IP address in c= line")); + goto on_error; + } + pj_sockaddr_set_port(&icedemo.rem.def_addr[0], (pj_uint16_t)comp0_port); + } + + PJ_LOG(3, (THIS_FILE, "Done, %d remote candidate(s) added", + icedemo.rem.cand_cnt)); + return; + +on_error: + reset_rem_info(); +} + + +/* + * Start ICE negotiation! This function is invoked from the menu. + */ +static void icedemo_start_nego(void) +{ + pj_str_t rufrag, rpwd; + pj_status_t status; + + if (icedemo.icest == NULL) { + PJ_LOG(1,(THIS_FILE, "Error: No ICE instance, create it first")); + return; + } + + if (!pj_ice_strans_has_sess(icedemo.icest)) { + PJ_LOG(1,(THIS_FILE, "Error: No ICE session, initialize first")); + return; + } + + if (icedemo.rem.cand_cnt == 0) { + PJ_LOG(1,(THIS_FILE, "Error: No remote info, input remote info first")); + return; + } + + PJ_LOG(3,(THIS_FILE, "Starting ICE negotiation..")); + + status = pj_ice_strans_start_ice(icedemo.icest, + pj_cstr(&rufrag, icedemo.rem.ufrag), + pj_cstr(&rpwd, icedemo.rem.pwd), + icedemo.rem.cand_cnt, + icedemo.rem.cand); + if (status != PJ_SUCCESS) + icedemo_perror("Error starting ICE", status); + else + PJ_LOG(3,(THIS_FILE, "ICE negotiation started")); +} + + +/* + * Send application data to remote agent. + */ +static void icedemo_send_data(unsigned comp_id, const char *data) +{ + pj_status_t status; + + if (icedemo.icest == NULL) { + PJ_LOG(1,(THIS_FILE, "Error: No ICE instance, create it first")); + return; + } + + if (!pj_ice_strans_has_sess(icedemo.icest)) { + PJ_LOG(1,(THIS_FILE, "Error: No ICE session, initialize first")); + return; + } + + /* + if (!pj_ice_strans_sess_is_complete(icedemo.icest)) { + PJ_LOG(1,(THIS_FILE, "Error: ICE negotiation has not been started or is in progress")); + return; + } + */ + + if (comp_id<1||comp_id>pj_ice_strans_get_running_comp_cnt(icedemo.icest)) { + PJ_LOG(1,(THIS_FILE, "Error: invalid component ID")); + return; + } + + status = pj_ice_strans_sendto(icedemo.icest, comp_id, data, strlen(data), + &icedemo.rem.def_addr[comp_id-1], + pj_sockaddr_get_len(&icedemo.rem.def_addr[comp_id-1])); + if (status != PJ_SUCCESS) + icedemo_perror("Error sending data", status); + else + PJ_LOG(3,(THIS_FILE, "Data sent")); +} + + +/* + * Display help for the menu. + */ +static void icedemo_help_menu(void) +{ + puts(""); + puts("-= Help on using ICE and this icedemo program =-"); + puts(""); + puts("This application demonstrates how to use ICE in pjnath without having\n" + "to use the SIP protocol. To use this application, you will need to run\n" + "two instances of this application, to simulate two ICE agents.\n"); + + puts("Basic ICE flow:\n" + " create instance [menu \"c\"]\n" + " repeat these steps as wanted:\n" + " - init session as offerer or answerer [menu \"i\"]\n" + " - display our SDP [menu \"s\"]\n" + " - \"send\" our SDP from the \"show\" output above to remote, by\n" + " copy-pasting the SDP to the other icedemo application\n" + " - parse remote SDP, by pasting SDP generated by the other icedemo\n" + " instance [menu \"r\"]\n" + " - begin ICE negotiation in our end [menu \"b\"], and \n" + " - immediately begin ICE negotiation in the other icedemo instance\n" + " - ICE negotiation will run, and result will be printed to screen\n" + " - send application data to remote [menu \"x\"]\n" + " - end/stop ICE session [menu \"e\"]\n" + " destroy instance [menu \"d\"]\n" + ""); + + puts(""); + puts("This concludes the help screen."); + puts(""); +} + + +/* + * Display console menu + */ +static void icedemo_print_menu(void) +{ + puts(""); + puts("+----------------------------------------------------------------------+"); + puts("| M E N U |"); + puts("+---+------------------------------------------------------------------+"); + puts("| c | create Create the instance |"); + puts("| d | destroy Destroy the instance |"); + puts("| i | init o|a Initialize ICE session as offerer or answerer |"); + puts("| e | stop End/stop ICE session |"); + puts("| s | show Display local ICE info |"); + puts("| r | remote Input remote ICE info |"); + puts("| b | start Begin ICE negotiation |"); + puts("| x | send <compid> .. Send data to remote |"); + puts("+---+------------------------------------------------------------------+"); + puts("| h | help * Help! * |"); + puts("| q | quit Quit |"); + puts("+----------------------------------------------------------------------+"); +} + + +/* + * Main console loop. + */ +static void icedemo_console(void) +{ + pj_bool_t app_quit = PJ_FALSE; + + while (!app_quit) { + char input[80], *cmd; + const char *SEP = " \t\r\n"; + int len; + + icedemo_print_menu(); + + printf("Input: "); + if (stdout) fflush(stdout); + + pj_bzero(input, sizeof(input)); + if (fgets(input, sizeof(input), stdin) == NULL) + break; + + len = strlen(input); + while (len && (input[len-1]=='\r' || input[len-1]=='\n')) + input[--len] = '\0'; + + cmd = strtok(input, SEP); + if (!cmd) + continue; + + if (strcmp(cmd, "create")==0 || strcmp(cmd, "c")==0) { + + icedemo_create_instance(); + + } else if (strcmp(cmd, "destroy")==0 || strcmp(cmd, "d")==0) { + + icedemo_destroy_instance(); + + } else if (strcmp(cmd, "init")==0 || strcmp(cmd, "i")==0) { + + char *role = strtok(NULL, SEP); + if (role) + icedemo_init_session(*role); + else + puts("error: Role required"); + + } else if (strcmp(cmd, "stop")==0 || strcmp(cmd, "e")==0) { + + icedemo_stop_session(); + + } else if (strcmp(cmd, "show")==0 || strcmp(cmd, "s")==0) { + + icedemo_show_ice(); + + } else if (strcmp(cmd, "remote")==0 || strcmp(cmd, "r")==0) { + + icedemo_input_remote(); + + } else if (strcmp(cmd, "start")==0 || strcmp(cmd, "b")==0) { + + icedemo_start_nego(); + + } else if (strcmp(cmd, "send")==0 || strcmp(cmd, "x")==0) { + + char *comp = strtok(NULL, SEP); + + if (!comp) { + PJ_LOG(1,(THIS_FILE, "Error: component ID required")); + } else { + char *data = comp + strlen(comp) + 1; + if (!data) + data = ""; + icedemo_send_data(atoi(comp), data); + } + + } else if (strcmp(cmd, "help")==0 || strcmp(cmd, "h")==0) { + + icedemo_help_menu(); + + } else if (strcmp(cmd, "quit")==0 || strcmp(cmd, "q")==0) { + + app_quit = PJ_TRUE; + + } else { + + printf("Invalid command '%s'\n", cmd); + + } + } +} + + +/* + * Display program usage. + */ +static void icedemo_usage() +{ + puts("Usage: icedemo [optons]"); + printf("icedemo v%s by pjsip.org\n", pj_get_version()); + puts(""); + puts("General options:"); + puts(" --comp-cnt, -c N Component count (default=1)"); + puts(" --nameserver, -n IP Configure nameserver to activate DNS SRV"); + puts(" resolution"); + puts(" --max-host, -H N Set max number of host candidates to N"); + puts(" --regular, -R Use regular nomination (default aggressive)"); + puts(" --log-file, -L FILE Save output to log FILE"); + puts(" --help, -h Display this screen."); + puts(""); + puts("STUN related options:"); + puts(" --stun-srv, -s HOSTDOM Enable srflx candidate by resolving to STUN server."); + puts(" HOSTDOM may be a \"host_or_ip[:port]\" or a domain"); + puts(" name if DNS SRV resolution is used."); + puts(""); + puts("TURN related options:"); + puts(" --turn-srv, -t HOSTDOM Enable relayed candidate by using this TURN server."); + puts(" HOSTDOM may be a \"host_or_ip[:port]\" or a domain"); + puts(" name if DNS SRV resolution is used."); + puts(" --turn-tcp, -T Use TCP to connect to TURN server"); + puts(" --turn-username, -u UID Set TURN username of the credential to UID"); + puts(" --turn-password, -p PWD Set password of the credential to WPWD"); + puts(" --turn-fingerprint, -F Use fingerprint for outgoing TURN requests"); + puts(""); +} + + +/* + * And here's the main() + */ +int main(int argc, char *argv[]) +{ + struct pj_getopt_option long_options[] = { + { "comp-cnt", 1, 0, 'c'}, + { "nameserver", 1, 0, 'n'}, + { "max-host", 1, 0, 'H'}, + { "help", 0, 0, 'h'}, + { "stun-srv", 1, 0, 's'}, + { "turn-srv", 1, 0, 't'}, + { "turn-tcp", 0, 0, 'T'}, + { "turn-username", 1, 0, 'u'}, + { "turn-password", 1, 0, 'p'}, + { "turn-fingerprint", 0, 0, 'F'}, + { "regular", 0, 0, 'R'}, + { "log-file", 1, 0, 'L'}, + }; + int c, opt_id; + pj_status_t status; + + icedemo.opt.comp_cnt = 1; + icedemo.opt.max_host = -1; + + while((c=pj_getopt_long(argc,argv, "c:n:s:t:u:p:H:L:hTFR", long_options, &opt_id))!=-1) { + switch (c) { + case 'c': + icedemo.opt.comp_cnt = atoi(pj_optarg); + if (icedemo.opt.comp_cnt < 1 || icedemo.opt.comp_cnt >= PJ_ICE_MAX_COMP) { + puts("Invalid component count value"); + return 1; + } + break; + case 'n': + icedemo.opt.ns = pj_str(pj_optarg); + break; + case 'H': + icedemo.opt.max_host = atoi(pj_optarg); + break; + case 'h': + icedemo_usage(); + return 0; + case 's': + icedemo.opt.stun_srv = pj_str(pj_optarg); + break; + case 't': + icedemo.opt.turn_srv = pj_str(pj_optarg); + break; + case 'T': + icedemo.opt.turn_tcp = PJ_TRUE; + break; + case 'u': + icedemo.opt.turn_username = pj_str(pj_optarg); + break; + case 'p': + icedemo.opt.turn_password = pj_str(pj_optarg); + break; + case 'F': + icedemo.opt.turn_fingerprint = PJ_TRUE; + break; + case 'R': + icedemo.opt.regular = PJ_TRUE; + break; + case 'L': + icedemo.opt.log_file = pj_optarg; + break; + default: + printf("Argument \"%s\" is not valid. Use -h to see help", + argv[pj_optind]); + return 1; + } + } + + status = icedemo_init(); + if (status != PJ_SUCCESS) + return 1; + + icedemo_console(); + + err_exit("Quitting..", PJ_SUCCESS); + return 0; +} diff --git a/pjsip-apps/src/samples/invtester.c b/pjsip-apps/src/samples/invtester.c new file mode 100644 index 0000000..a1eb413 --- /dev/null +++ b/pjsip-apps/src/samples/invtester.c @@ -0,0 +1,295 @@ +/* $Id: invtester.c 3553 2011-05-05 06:14:19Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 + */ + +/** + * invtester.c + * + * Send INVITE/re-INVITE without SDP. + */ + + +/* Include all headers. */ +#include <pjsip.h> +#include <pjlib-util.h> +#include <pjlib.h> + +#define THIS_FILE "invtester.c" + +#define PORT 50060 +#define PORT_STR ":50060" +#define SAME_BRANCH 0 +#define ACK_HAS_SDP 1 + +static pjsip_endpoint *sip_endpt; +static pj_bool_t quit_flag; +static pjsip_dialog *dlg; + + +/* Callback to handle incoming requests. */ +static void on_tsx_state(pjsip_transaction *tsx, pjsip_event *event); + +static pjsip_module mod_app = +{ + NULL, NULL, /* prev, next. */ + { "mod-app", 7 }, /* Name. */ + -1, /* Id */ + PJSIP_MOD_PRIORITY_APPLICATION, /* Priority */ + NULL, /* load() */ + NULL, /* start() */ + NULL, /* stop() */ + NULL, /* unload() */ + NULL, /* on_rx_request() */ + NULL, /* on_rx_response() */ + NULL, /* on_tx_request. */ + NULL, /* on_tx_response() */ + &on_tsx_state /* on_tsx_state() */ +}; + + +/* Worker thread */ +static int worker_thread(void *arg) +{ + PJ_UNUSED_ARG(arg); + + while (!quit_flag) { + pj_time_val timeout = {0, 500}; + pjsip_endpt_handle_events(sip_endpt, &timeout); + } + + return 0; +} + +/* Send request */ +static void send_request(const pjsip_method *method, + int cseq, + const pj_str_t *branch, + pj_bool_t with_offer) +{ + pjsip_tx_data *tdata; + pj_str_t dummy_sdp_str = + { + "v=0\r\n" + "o=- 3360842071 3360842071 IN IP4 192.168.0.68\r\n" + "s=pjmedia\r\n" + "c=IN IP4 192.168.0.68\r\n" + "t=0 0\r\n" + "m=audio 4000 RTP/AVP 0 101\r\n" + "a=rtcp:4001 IN IP4 192.168.0.68\r\n" + "a=rtpmap:0 PCMU/8000\r\n" + "a=sendrecv\r\n" + "a=rtpmap:101 telephone-event/8000\r\n" + "a=fmtp:101 0-15\r\n", + 0 + }; + pj_status_t status; + + status = pjsip_dlg_create_request(dlg, method, cseq, &tdata); + pj_assert(status == PJ_SUCCESS); + + if (branch) { + pjsip_via_hdr *via; + + via = (pjsip_via_hdr*) pjsip_msg_find_hdr(tdata->msg, PJSIP_H_VIA, NULL); + pj_strdup(tdata->pool, &via->branch_param, branch); + } + + if (with_offer) { + pjsip_msg_body *body; + pj_str_t mime_application = { "application", 11}; + pj_str_t mime_sdp = {"sdp", 3}; + + + dummy_sdp_str.slen = pj_ansi_strlen(dummy_sdp_str.ptr); + body = pjsip_msg_body_create(tdata->pool, + &mime_application, &mime_sdp, + &dummy_sdp_str); + tdata->msg->body = body; + } + + status = pjsip_dlg_send_request(dlg, tdata, -1, NULL); + pj_assert(status == PJ_SUCCESS); +} + +/* Callback to handle incoming requests. */ +static void on_tsx_state(pjsip_transaction *tsx, pjsip_event *event) +{ + if (tsx->role == PJSIP_ROLE_UAC) { + if (tsx->method.id == PJSIP_INVITE_METHOD && tsx->state == PJSIP_TSX_STATE_TERMINATED) { +#if SAME_BRANCH + send_request(&pjsip_ack_method, tsx->cseq, &tsx->branch, ACK_HAS_SDP); +#else + send_request(&pjsip_ack_method, tsx->cseq, NULL, ACK_HAS_SDP); +#endif + } + + } else { + if (event->type == PJSIP_EVENT_RX_MSG && tsx->state == PJSIP_TSX_STATE_TRYING) { + pjsip_tx_data *tdata; + + pjsip_dlg_create_response(dlg, event->body.tsx_state.src.rdata, + 200, NULL, &tdata); + pjsip_dlg_send_response(dlg, tsx, tdata); + } + } +} + +/* make call */ +void make_call(char *uri, pj_bool_t with_offer) +{ + pj_str_t local = pj_str("sip:localhost" PORT_STR); + pj_str_t remote = pj_str(uri); + pj_status_t status; + + status = pjsip_dlg_create_uac(pjsip_ua_instance(), + &local, &local, &remote, &remote, &dlg); + pj_assert(status == PJ_SUCCESS); + + pjsip_dlg_inc_lock(dlg); + + status = pjsip_dlg_add_usage(dlg, &mod_app, NULL); + pj_assert(status == PJ_SUCCESS); + + pjsip_dlg_inc_session(dlg, &mod_app); + + send_request(&pjsip_invite_method, -1, NULL, with_offer); + + pjsip_dlg_dec_lock(dlg); +} + +/* reinvite */ +void reinvite(pj_bool_t with_offer) +{ + send_request(&pjsip_invite_method, -1, NULL, with_offer); +} + +/* hangup call */ +void hangup(void) +{ + send_request(&pjsip_bye_method, -1, NULL, PJ_FALSE); + pjsip_dlg_dec_session(dlg, &mod_app); +} + +/* + * main() + * + */ +int main(int argc, char *argv[]) +{ + pj_caching_pool cp; + pj_thread_t *thread; + pj_pool_t *pool; + pj_status_t status; + + if (argc != 2) { + puts("Error: destination URL needed"); + return 0; + } + + /* Must init PJLIB first: */ + status = pj_init(); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + /* Then init PJLIB-UTIL: */ + status = pjlib_util_init(); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* Must create a pool factory before we can allocate any memory. */ + pj_caching_pool_init(&cp, &pj_pool_factory_default_policy, 0); + + + /* Create the endpoint: */ + status = pjsip_endpt_create(&cp.factory, "sipstateless", + &sip_endpt); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* + * Add UDP transport, with hard-coded port + */ + { + pj_sockaddr_in addr; + + addr.sin_family = pj_AF_INET(); + addr.sin_addr.s_addr = 0; + addr.sin_port = pj_htons(PORT); + + status = pjsip_udp_transport_start( sip_endpt, &addr, NULL, 1, NULL); + if (status != PJ_SUCCESS) { + PJ_LOG(3,(THIS_FILE, + "Error starting UDP transport (port in use?)")); + return 1; + } + } + + status = pjsip_tsx_layer_init_module(sip_endpt); + pj_assert(status == PJ_SUCCESS); + + status = pjsip_ua_init_module(sip_endpt, NULL); + pj_assert(status == PJ_SUCCESS); + + /* + * Register our module to receive incoming requests. + */ + status = pjsip_endpt_register_module( sip_endpt, &mod_app); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + pool = pjsip_endpt_create_pool(sip_endpt, "", 1000, 1000); + + status = pj_thread_create(pool, "", &worker_thread, NULL, 0, 0, &thread); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + printf("Destination URL: %s\n", argv[1]); + + for (;;) { + char line[10]; + + fgets(line, sizeof(line), stdin); + + switch (line[0]) { + case 'm': + make_call(argv[1], PJ_FALSE); + break; + case 'M': + make_call(argv[1], PJ_TRUE); + break; + case 'r': + reinvite(PJ_FALSE); + break; + case 'R': + reinvite(PJ_TRUE); + break; + case 'h': + hangup(); + break; + case 'q': + goto on_quit; + } + } + +on_quit: + quit_flag = 1; + pj_thread_join(thread); + + pjsip_endpt_destroy(sip_endpt); + pj_caching_pool_destroy(&cp); + pj_shutdown(); + return 0; +} + diff --git a/pjsip-apps/src/samples/jbsim.c b/pjsip-apps/src/samples/jbsim.c new file mode 100644 index 0000000..510f67c --- /dev/null +++ b/pjsip-apps/src/samples/jbsim.c @@ -0,0 +1,1141 @@ +/* $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; +} diff --git a/pjsip-apps/src/samples/latency.c b/pjsip-apps/src/samples/latency.c new file mode 100644 index 0000000..6193e61 --- /dev/null +++ b/pjsip-apps/src/samples/latency.c @@ -0,0 +1,202 @@ +/* $Id: latency.c 3664 2011-07-19 03:42:28Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 + */ + +/* See http://trac.pjsip.org/repos/wiki/MeasuringSoundLatency on + * how to use this program. + */ + +#include <pjmedia.h> +#include <pjlib.h> + +#include <stdio.h> + +#define THIS_FILE "lacency.c" + + +/* Util to display the error message for the specified error code */ +static int app_perror( const char *sender, const char *title, + pj_status_t status) +{ + char errmsg[PJ_ERR_MSG_SIZE]; + + PJ_UNUSED_ARG(sender); + + pj_strerror(status, errmsg, sizeof(errmsg)); + + printf("%s: %s [code=%d]\n", title, errmsg, status); + return 1; +} + +/* + * Find out latency + */ +static int calculate_latency(pj_pool_t *pool, pjmedia_port *wav) +{ + pjmedia_frame frm; + short *buf; + unsigned i, samples_per_frame, read, len; + unsigned start_pos; + pj_status_t status; + + unsigned lat_sum = 0, + lat_cnt = 0, + lat_min = 10000, + lat_max = 0; + + samples_per_frame = PJMEDIA_PIA_SPF(&wav->info); + frm.buf = pj_pool_alloc(pool, samples_per_frame * 2); + frm.size = samples_per_frame * 2; + len = pjmedia_wav_player_get_len(wav); + buf = pj_pool_alloc(pool, len + samples_per_frame); + + read = 0; + while (read < len/2) { + status = pjmedia_port_get_frame(wav, &frm); + if (status != PJ_SUCCESS) + break; + + pjmedia_copy_samples(buf+read, (short*)frm.buf, samples_per_frame); + read += samples_per_frame; + } + + if (read < 2 * PJMEDIA_PIA_SRATE(&wav->info)) { + puts("Error: too short"); + return -1; + } + + start_pos = 0; + while (start_pos < len/2 - PJMEDIA_PIA_SRATE(&wav->info)) { + int max_signal = 0; + unsigned max_signal_pos = start_pos; + unsigned max_echo_pos = 0; + unsigned pos; + unsigned lat; + + /* Get the largest signal in the next 0.7s */ + for (i=start_pos; i<start_pos + PJMEDIA_PIA_SRATE(&wav->info) * 700 / 1000; ++i) { + if (abs(buf[i]) > max_signal) { + max_signal = abs(buf[i]); + max_signal_pos = i; + } + } + + /* Advance 10ms from max_signal_pos */ + pos = max_signal_pos + 10 * PJMEDIA_PIA_SRATE(&wav->info) / 1000; + + /* Get the largest signal in the next 500ms */ + max_signal = 0; + max_echo_pos = pos; + for (i=pos; i<pos+PJMEDIA_PIA_SRATE(&wav->info)/2; ++i) { + if (abs(buf[i]) > max_signal) { + max_signal = abs(buf[i]); + max_echo_pos = i; + } + } + + lat = (max_echo_pos - max_signal_pos) * 1000 / PJMEDIA_PIA_SRATE(&wav->info); + +#if 0 + printf("Latency = %u\n", lat); +#endif + + lat_sum += lat; + lat_cnt++; + if (lat < lat_min) + lat_min = lat; + if (lat > lat_max) + lat_max = lat; + + /* Advance next loop */ + start_pos += PJMEDIA_PIA_SRATE(&wav->info); + } + + printf("Latency average = %u\n", lat_sum / lat_cnt); + printf("Latency minimum = %u\n", lat_min); + printf("Latency maximum = %u\n", lat_max); + printf("Number of data = %u\n", lat_cnt); + return 0; +} + + +/* + * main() + */ +int main(int argc, char *argv[]) +{ + enum { NSAMPLES = 160, COUNT=100 }; + pj_caching_pool cp; + pj_pool_t *pool; + pjmedia_port *wav; + pj_status_t status; + + + /* Verify cmd line arguments. */ + if (argc != 2) { + puts("Error: missing argument(s)"); + puts("Usage: latency REV.WAV"); + return 1; + } + + pj_log_set_level(0); + + status = pj_init(); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + pj_caching_pool_init(&cp, &pj_pool_factory_default_policy, 0); + + pool = pj_pool_create( &cp.factory, /* pool factory */ + "wav", /* pool name. */ + 4000, /* init size */ + 4000, /* increment size */ + NULL /* callback on error */ + ); + + status = pj_register_strerror(PJMEDIA_ERRNO_START, PJ_ERRNO_SPACE_SIZE, + &pjmedia_strerror); + pj_assert(status == PJ_SUCCESS); + + /* Wav */ + status = pjmedia_wav_player_port_create( pool, /* memory pool */ + argv[1], /* file to play */ + 0, /* use default ptime*/ + 0, /* flags */ + 0, /* default buffer */ + &wav /* returned port */ + ); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, argv[1], status); + return 1; + } + + status = calculate_latency(pool, wav); + if (status != PJ_SUCCESS) + return 1; + + status = pjmedia_port_destroy( wav ); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + pj_pool_release( pool ); + pj_caching_pool_destroy( &cp ); + pj_shutdown(); + + /* Done. */ + return 0; +} + diff --git a/pjsip-apps/src/samples/level.c b/pjsip-apps/src/samples/level.c new file mode 100644 index 0000000..59bbb96 --- /dev/null +++ b/pjsip-apps/src/samples/level.c @@ -0,0 +1,179 @@ +/* $Id: level.c 3664 2011-07-19 03:42:28Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 + */ + + +/** + * \page page_pjmedia_samples_level_c Samples: Reading from WAV File + * + * This is a very simple example to use the @ref PJMEDIA_FILE_PLAY, to + * directly read the samples from the file. + * + * This file is pjsip-apps/src/samples/level.c + * + * \includelineno level.c + */ + + +static const char *desc = + " FILE: \n" + " level.c \n" + " \n" + " PURPOSE: \n" + " Read PCM WAV file and display the audio level the first 100 frames. \n" + " Each frame is assumed to have 160 samples. \n" + " \n" + " USAGE: \n" + " level file.wav \n" + " \n" + " The WAV file SHOULD have a 16bit mono samples. "; + +#include <pjmedia.h> +#include <pjlib.h> + +#include <stdio.h> + +/* For logging purpose. */ +#define THIS_FILE "level.c" + + +/* Util to display the error message for the specified error code */ +static int app_perror( const char *sender, const char *title, + pj_status_t status) +{ + char errmsg[PJ_ERR_MSG_SIZE]; + + PJ_UNUSED_ARG(sender); + + pj_strerror(status, errmsg, sizeof(errmsg)); + + printf("%s: %s [code=%d]\n", title, errmsg, status); + return 1; +} + + +/* + * main() + */ +int main(int argc, char *argv[]) +{ + enum { NSAMPLES = 640, COUNT=100 }; + pj_caching_pool cp; + pjmedia_endpt *med_endpt; + pj_pool_t *pool; + pjmedia_port *file_port; + int i; + pj_status_t status; + + + /* Verify cmd line arguments. */ + if (argc != 2) { + puts(""); + puts(desc); + return 1; + } + + /* Must init PJLIB first: */ + status = pj_init(); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* Must create a pool factory before we can allocate any memory. */ + pj_caching_pool_init(&cp, &pj_pool_factory_default_policy, 0); + + /* + * Initialize media endpoint. + * This will implicitly initialize PJMEDIA too. + */ + status = pjmedia_endpt_create(&cp.factory, NULL, 1, &med_endpt); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* Create memory pool for our file player */ + pool = pj_pool_create( &cp.factory, /* pool factory */ + "wav", /* pool name. */ + 4000, /* init size */ + 4000, /* increment size */ + NULL /* callback on error */ + ); + + /* Create file media port from the WAV file */ + status = pjmedia_wav_player_port_create( pool, /* memory pool */ + argv[1], /* file to play */ + 0, /* use default ptime*/ + 0, /* flags */ + 0, /* default buffer */ + &file_port/* returned port */ + ); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to use WAV file", status); + return 1; + } + + if (PJMEDIA_PIA_SPF(&file_port->info) > NSAMPLES) { + app_perror(THIS_FILE, "WAV clock rate is too big", PJ_EINVAL); + return 1; + } + + puts("Time\tPCMU\tLinear"); + puts("------------------------"); + + for (i=0; i<COUNT; ++i) { + pj_int16_t framebuf[NSAMPLES]; + pjmedia_frame frm; + pj_int32_t level32; + unsigned ms; + int level; + + frm.buf = framebuf; + frm.size = sizeof(framebuf); + + pjmedia_port_get_frame(file_port, &frm); + + level32 = pjmedia_calc_avg_signal(framebuf, + PJMEDIA_PIA_SPF(&file_port->info)); + level = pjmedia_linear2ulaw(level32) ^ 0xFF; + + ms = i * 1000 * PJMEDIA_PIA_SPF(&file_port->info) / + PJMEDIA_PIA_SRATE(&file_port->info); + printf("%03d.%03d\t%7d\t%7d\n", + ms/1000, ms%1000, level, level32); + } + puts(""); + + + /* Destroy file port */ + status = pjmedia_port_destroy( file_port ); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* Release application pool */ + pj_pool_release( pool ); + + /* Destroy media endpoint. */ + pjmedia_endpt_destroy( med_endpt ); + + /* Destroy pool factory */ + pj_caching_pool_destroy( &cp ); + + /* Shutdown PJLIB */ + pj_shutdown(); + + + /* Done. */ + return 0; +} + diff --git a/pjsip-apps/src/samples/main_rtems.c b/pjsip-apps/src/samples/main_rtems.c new file mode 100644 index 0000000..d26605a --- /dev/null +++ b/pjsip-apps/src/samples/main_rtems.c @@ -0,0 +1,12 @@ + +/* + * !! OIY OIY !! + * + * The purpose of this file is only to get the executable linked. I haven't + * actually tried to run this on RTEMS!! + * + */ + +#include "../../pjlib/src/pjlib-test/main_rtems.c" + + diff --git a/pjsip-apps/src/samples/mix.c b/pjsip-apps/src/samples/mix.c new file mode 100644 index 0000000..c8e224f --- /dev/null +++ b/pjsip-apps/src/samples/mix.c @@ -0,0 +1,237 @@ +/* $Id: mix.c 3664 2011-07-19 03:42:28Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 + */ + + /** + * \page page_pjmedia_samples_mix_c Samples: Mixing WAV files + * + * This file is pjsip-apps/src/samples/mix.c + * + * \includelineno mix.c + */ + +#include <pjlib.h> +#include <pjlib-util.h> +#include <pjmedia.h> + +#define THIS_FILE "mix.c" + +static const char *desc = + " mix\n" + "\n" + " PURPOSE:\n" + " Mix input WAV files and save it to output WAV. Input WAV can have\n" + " different clock rate.\n" + "\n" + "\n" + " USAGE:\n" + " mix [options] output.wav input1.wav [input2.wav] ...\n" + "\n" + " arguments:\n" + " output.wav Set the output WAV filename.\n" + " input1.wav Set the input WAV filename.\n" + " input2.wav Set the input WAV filename.\n" + "\n" + " options:\n" + " -c N Set clock rate to N Hz (default 16000)\n" + " -f Force write (overwrite output without warning\n" +; + +#define MAX_WAV 16 +#define PTIME 20 +#define APPEND 1000 + +struct wav_input +{ + const char *fname; + pjmedia_port *port; + unsigned slot; +}; + +static int err_ret(const char *title, pj_status_t status) +{ + char errmsg[PJ_ERR_MSG_SIZE]; + pj_strerror(status, errmsg, sizeof(errmsg)); + PJ_LOG(3,(THIS_FILE, "%s error: %s", title, errmsg)); + return 1; +} + +static void usage(void) +{ + puts(desc); +} + +int main(int argc, char *argv[]) +{ + pj_caching_pool cp; + pj_pool_t *pool; + pjmedia_endpt *med_ept; + unsigned clock_rate = 16000; + int c, force=0; + const char *out_fname; + pjmedia_conf *conf; + pjmedia_port *wavout; + struct wav_input wav_input[MAX_WAV]; + pj_size_t longest = 0, processed; + unsigned i, input_cnt = 0; + pj_status_t status; + +#define CHECK(op) do { \ + status = op; \ + if (status != PJ_SUCCESS) \ + return err_ret(#op, status); \ + } while (0) + + + /* Parse arguments */ + while ((c=pj_getopt(argc, argv, "c:f")) != -1) { + switch (c) { + case 'c': + clock_rate = atoi(pj_optarg); + if (clock_rate < 1000) { + puts("Error: invalid clock rate"); + usage(); + return -1; + } + break; + case 'f': + force = 1; + break; + } + } + + /* Get output WAV name */ + if (pj_optind == argc) { + puts("Error: no WAV output is specified"); + usage(); + return 1; + } + + out_fname = argv[pj_optind++]; + if (force==0 && pj_file_exists(out_fname)) { + char in[8]; + + printf("File %s exists, overwrite? [Y/N] ", out_fname); + fflush(stdout); + if (fgets(in, sizeof(in), stdin) == NULL) + return 1; + if (pj_tolower(in[0]) != 'y') + return 1; + } + + /* Scan input file names */ + for (input_cnt=0 ; pj_optind<argc && input_cnt<MAX_WAV; + ++pj_optind, ++input_cnt) + { + if (!pj_file_exists(argv[pj_optind])) { + printf("Error: input file %s doesn't exist\n", + argv[pj_optind]); + return 1; + } + wav_input[input_cnt].fname = argv[pj_optind]; + wav_input[input_cnt].port = NULL; + wav_input[input_cnt].slot = 0; + } + + if (input_cnt == 0) { + puts("Error: no input WAV is specified"); + return 0; + } + + /* Initialialize */ + CHECK( pj_init() ); + CHECK( pjlib_util_init() ); + pj_caching_pool_init(&cp, NULL, 0); + CHECK( pjmedia_endpt_create(&cp.factory, NULL, 1, &med_ept) ); + + pool = pj_pool_create(&cp.factory, "mix", 1000, 1000, NULL); + + /* Create the bridge */ + CHECK( pjmedia_conf_create(pool, MAX_WAV+4, clock_rate, 1, + clock_rate * PTIME / 1000, 16, + PJMEDIA_CONF_NO_DEVICE, &conf) ); + + /* Create the WAV output */ + CHECK( pjmedia_wav_writer_port_create(pool, out_fname, clock_rate, 1, + clock_rate * PTIME / 1000, + 16, 0, 0, &wavout) ); + + /* Create and register each WAV input to the bridge */ + for (i=0; i<input_cnt; ++i) { + pj_ssize_t len; + + CHECK( pjmedia_wav_player_port_create(pool, wav_input[i].fname, 20, + PJMEDIA_FILE_NO_LOOP, 0, + &wav_input[i].port) ); + len = pjmedia_wav_player_get_len(wav_input[i].port); + len = (pj_ssize_t)(len * 1.0 * clock_rate / + PJMEDIA_PIA_SRATE(&wav_input[i].port->info)); + if (len > (pj_ssize_t)longest) + longest = len; + + CHECK( pjmedia_conf_add_port(conf, pool, wav_input[i].port, + NULL, &wav_input[i].slot)); + + CHECK( pjmedia_conf_connect_port(conf, wav_input[i].slot, 0, 0) ); + } + + /* Loop reading frame from the bridge and write it to WAV */ + processed = 0; + while (processed < longest + clock_rate * APPEND * 2 / 1000) { + pj_int16_t framebuf[PTIME * 48000 / 1000]; + pjmedia_port *cp = pjmedia_conf_get_master_port(conf); + pjmedia_frame frame; + + frame.buf = framebuf; + frame.size = PJMEDIA_PIA_SPF(&cp->info) * 2; + pj_assert(frame.size <= sizeof(framebuf)); + + CHECK( pjmedia_port_get_frame(cp, &frame) ); + + if (frame.type != PJMEDIA_FRAME_TYPE_AUDIO) { + pj_bzero(frame.buf, frame.size); + frame.type = PJMEDIA_FRAME_TYPE_AUDIO; + } + + CHECK( pjmedia_port_put_frame(wavout, &frame)); + + processed += frame.size; + } + + PJ_LOG(3,(THIS_FILE, "Done. Output duration: %d.%03d", + (processed >> 2)/clock_rate, + ((processed >> 2)*1000/clock_rate) % 1000)); + + /* Shutdown everything */ + CHECK( pjmedia_port_destroy(wavout) ); + for (i=0; i<input_cnt; ++i) { + CHECK( pjmedia_conf_remove_port(conf, wav_input[i].slot) ); + CHECK( pjmedia_port_destroy(wav_input[i].port) ); + } + + CHECK(pjmedia_conf_destroy(conf)); + CHECK(pjmedia_endpt_destroy(med_ept)); + + pj_pool_release(pool); + pj_caching_pool_destroy(&cp); + pj_shutdown(); + + return 0; +} + diff --git a/pjsip-apps/src/samples/pcaputil.c b/pjsip-apps/src/samples/pcaputil.c new file mode 100644 index 0000000..1c26484 --- /dev/null +++ b/pjsip-apps/src/samples/pcaputil.c @@ -0,0 +1,540 @@ +/* $Id: pcaputil.c 3841 2011-10-24 09:28:13Z ming $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 <pjlib.h> +#include <pjlib-util.h> +#include <pjmedia.h> +#include <pjmedia-codec.h> + +static const char *USAGE = +"pcaputil [options] INPUT OUTPUT\n" +"\n" +" Convert captured RTP packets in PCAP file to WAV file or play it\n" +" to audio device.\n" +"\n" +" INPUT is the PCAP file name/path.\n" +" OUTPUT is the WAV file name/path to store the output, or set to \"-\",\n" +" to play the output to audio device. The program will decode\n" +" the RTP contents using codec that is available in PJMEDIA,\n" +" and optionally decrypt the content using the SRTP crypto and\n" +" keys below.\n" +"\n" +"Options to filter packets from PCAP file:\n" +"(you can always select the relevant packets from Wireshark of course!)\n" +" --src-ip=IP Only include packets from this source address\n" +" --dst-ip=IP Only include packets destined to this address\n" +" --src-port=port Only include packets from this source port number\n" +" --dst-port=port Only include packets destined to this port number\n" +"\n" +"Options for RTP packet processing:\n" +"" +" --codec=codec_id The codec ID formatted \"name/clock-rate/channel-count\"\n" +" must be specified for codec with dynamic PT,\n" +" e.g: \"Speex/8000\"\n" +" --srtp-crypto=TAG, -c Set crypto to be used to decrypt SRTP packets. Valid\n" +" tags are: \n" +" AES_CM_128_HMAC_SHA1_80 \n" +" AES_CM_128_HMAC_SHA1_32\n" +" --srtp-key=KEY, -k Set the base64 key to decrypt SRTP packets.\n" +"\n" +"Options for playing to audio device:\n" +"" +" --play-dev-id=dev_id Audio device ID for playback.\n" +"\n" +" Example:\n" +" pcaputil file.pcap output.wav\n" +" pcaputil -c AES_CM_128_HMAC_SHA1_80 \\\n" +" -k VLDONbsbGl2Puqy+0PV7w/uGfpSPKFevDpxGsxN3 \\\n" +" file.pcap output.wav\n" +"\n" +; + +static struct app +{ + pj_caching_pool cp; + pj_pool_t *pool; + pjmedia_endpt *mept; + pj_pcap_file *pcap; + pjmedia_port *wav; + pjmedia_codec *codec; + pjmedia_aud_stream *aud_strm; + unsigned pt; + pjmedia_transport *srtp; + pjmedia_rtp_session rtp_sess; + pj_bool_t rtp_sess_init; +} app; + + +static void cleanup() +{ + if (app.srtp) pjmedia_transport_close(app.srtp); + if (app.wav) { + pj_ssize_t pos = pjmedia_wav_writer_port_get_pos(app.wav); + if (pos >= 0) { + unsigned msec; + msec = pos / 2 * 1000 / PJMEDIA_PIA_SRATE(&app.wav->info); + printf("Written: %dm:%02ds.%03d\n", + msec / 1000 / 60, + (msec / 1000) % 60, + msec % 1000); + } + pjmedia_port_destroy(app.wav); + } + if (app.pcap) pj_pcap_close(app.pcap); + if (app.codec) { + pjmedia_codec_mgr *cmgr; + pjmedia_codec_close(app.codec); + cmgr = pjmedia_endpt_get_codec_mgr(app.mept); + pjmedia_codec_mgr_dealloc_codec(cmgr, app.codec); + } + if (app.aud_strm) { + pjmedia_aud_stream_stop(app.aud_strm); + pjmedia_aud_stream_destroy(app.aud_strm); + } + if (app.mept) pjmedia_endpt_destroy(app.mept); + if (app.pool) pj_pool_release(app.pool); + pj_caching_pool_destroy(&app.cp); + pj_shutdown(); +} + +static void err_exit(const char *title, pj_status_t status) +{ + if (status != PJ_SUCCESS) { + char errmsg[PJ_ERR_MSG_SIZE]; + pj_strerror(status, errmsg, sizeof(errmsg)); + printf("Error: %s: %s\n", title, errmsg); + } else { + printf("Error: %s\n", title); + } + cleanup(); + exit(1); +} + +#define T(op) do { \ + status = op; \ + if (status != PJ_SUCCESS) \ + err_exit(#op, status); \ + } while (0) + + +static void read_rtp(pj_uint8_t *buf, pj_size_t bufsize, + pjmedia_rtp_hdr **rtp, + pj_uint8_t **payload, + unsigned *payload_size, + pj_bool_t check_pt) +{ + pj_status_t status; + + /* Init RTP session */ + if (!app.rtp_sess_init) { + T(pjmedia_rtp_session_init(&app.rtp_sess, 0, 0)); + app.rtp_sess_init = PJ_TRUE; + } + + /* Loop reading until we have a good RTP packet */ + for (;;) { + pj_size_t sz = bufsize; + const pjmedia_rtp_hdr *r; + const void *p; + pjmedia_rtp_status seq_st; + + status = pj_pcap_read_udp(app.pcap, NULL, buf, &sz); + if (status != PJ_SUCCESS) + err_exit("Error reading PCAP file", status); + + /* Decode RTP packet to make sure that this is an RTP packet. + * We will decode it again to get the payload after we do + * SRTP decoding + */ + status = pjmedia_rtp_decode_rtp(&app.rtp_sess, buf, sz, &r, + &p, payload_size); + if (status != PJ_SUCCESS) { + char errmsg[PJ_ERR_MSG_SIZE]; + pj_strerror(status, errmsg, sizeof(errmsg)); + printf("Not RTP packet, skipping packet: %s\n", errmsg); + continue; + } + + /* Decrypt SRTP */ +#if PJMEDIA_HAS_SRTP + if (app.srtp) { + int len = sz; + status = pjmedia_transport_srtp_decrypt_pkt(app.srtp, PJ_TRUE, + buf, &len); + if (status != PJ_SUCCESS) { + char errmsg[PJ_ERR_MSG_SIZE]; + pj_strerror(status, errmsg, sizeof(errmsg)); + printf("SRTP packet decryption failed, skipping packet: %s\n", + errmsg); + continue; + } + sz = len; + + /* Decode RTP packet again */ + status = pjmedia_rtp_decode_rtp(&app.rtp_sess, buf, sz, &r, + &p, payload_size); + if (status != PJ_SUCCESS) { + char errmsg[PJ_ERR_MSG_SIZE]; + pj_strerror(status, errmsg, sizeof(errmsg)); + printf("Not RTP packet, skipping packet: %s\n", errmsg); + continue; + } + } +#endif + + /* Update RTP session */ + pjmedia_rtp_session_update2(&app.rtp_sess, r, &seq_st, PJ_FALSE); + + /* Skip out-of-order packet */ + if (seq_st.diff == 0) { + printf("Skipping out of order packet\n"); + continue; + } + + /* Skip if payload type is different */ + if (check_pt && r->pt != app.pt) { + printf("Skipping RTP packet with bad payload type\n"); + continue; + } + + /* Skip bad packet */ + if (seq_st.status.flag.bad) { + printf("Skipping bad RTP\n"); + continue; + } + + + *rtp = (pjmedia_rtp_hdr*)r; + *payload = (pj_uint8_t*)p; + + /* We have good packet */ + break; + } +} + +pjmedia_frame play_frm; +static pj_bool_t play_frm_copied, play_frm_ready; + +static pj_status_t wait_play(pjmedia_frame *f) +{ + play_frm_copied = PJ_FALSE; + play_frm = *f; + play_frm_ready = PJ_TRUE; + while (!play_frm_copied) { + pj_thread_sleep(1); + } + play_frm_ready = PJ_FALSE; + + return PJ_SUCCESS; +} + +static pj_status_t play_cb(void *user_data, pjmedia_frame *f) +{ + PJ_UNUSED_ARG(user_data); + + if (!play_frm_ready) { + PJ_LOG(3, ("play_cb()", "Warning! Play frame not ready")); + return PJ_SUCCESS; + } + + pj_memcpy(f->buf, play_frm.buf, play_frm.size); + f->size = play_frm.size; + + play_frm_copied = PJ_TRUE; + return PJ_SUCCESS; +} + +static void pcap2wav(const pj_str_t *codec, + const pj_str_t *wav_filename, + pjmedia_aud_dev_index dev_id, + const pj_str_t *srtp_crypto, + const pj_str_t *srtp_key) +{ + const pj_str_t WAV = {".wav", 4}; + struct pkt + { + pj_uint8_t buffer[320]; + pjmedia_rtp_hdr *rtp; + pj_uint8_t *payload; + unsigned payload_len; + } pkt0; + pjmedia_codec_mgr *cmgr; + const pjmedia_codec_info *ci; + pjmedia_codec_param param; + unsigned samples_per_frame; + pj_status_t status; + + /* Initialize all codecs */ + T( pjmedia_codec_register_audio_codecs(app.mept, NULL) ); + + /* Create SRTP transport is needed */ +#if PJMEDIA_HAS_SRTP + if (srtp_crypto->slen) { + pjmedia_srtp_crypto crypto; + + pj_bzero(&crypto, sizeof(crypto)); + crypto.key = *srtp_key; + crypto.name = *srtp_crypto; + T( pjmedia_transport_srtp_create(app.mept, NULL, NULL, &app.srtp) ); + T( pjmedia_transport_srtp_start(app.srtp, &crypto, &crypto) ); + } +#else + PJ_UNUSED_ARG(srtp_crypto); + PJ_UNUSED_ARG(srtp_key); +#endif + + /* Read first packet */ + read_rtp(pkt0.buffer, sizeof(pkt0.buffer), &pkt0.rtp, + &pkt0.payload, &pkt0.payload_len, PJ_FALSE); + + cmgr = pjmedia_endpt_get_codec_mgr(app.mept); + + /* Get codec info and param for the specified payload type */ + app.pt = pkt0.rtp->pt; + if (app.pt >=0 && app.pt < 96) { + T( pjmedia_codec_mgr_get_codec_info(cmgr, pkt0.rtp->pt, &ci) ); + } else { + unsigned cnt = 2; + const pjmedia_codec_info *info[2]; + T( pjmedia_codec_mgr_find_codecs_by_id(cmgr, codec, &cnt, + info, NULL) ); + if (cnt != 1) + err_exit("Codec ID must be specified and unique!", 0); + + ci = info[0]; + } + T( pjmedia_codec_mgr_get_default_param(cmgr, ci, ¶m) ); + + /* Alloc and init codec */ + T( pjmedia_codec_mgr_alloc_codec(cmgr, ci, &app.codec) ); + T( pjmedia_codec_init(app.codec, app.pool) ); + T( pjmedia_codec_open(app.codec, ¶m) ); + + /* Init audio device or WAV file */ + samples_per_frame = ci->clock_rate * param.info.frm_ptime / 1000; + if (pj_strcmp2(wav_filename, "-") == 0) { + pjmedia_aud_param aud_param; + + /* Open audio device */ + T( pjmedia_aud_dev_default_param(dev_id, &aud_param) ); + aud_param.dir = PJMEDIA_DIR_PLAYBACK; + aud_param.channel_count = ci->channel_cnt; + aud_param.clock_rate = ci->clock_rate; + aud_param.samples_per_frame = samples_per_frame; + T( pjmedia_aud_stream_create(&aud_param, NULL, &play_cb, + NULL, &app.aud_strm) ); + T( pjmedia_aud_stream_start(app.aud_strm) ); + } else if (pj_stristr(wav_filename, &WAV)) { + /* Open WAV file */ + T( pjmedia_wav_writer_port_create(app.pool, wav_filename->ptr, + ci->clock_rate, ci->channel_cnt, + samples_per_frame, + param.info.pcm_bits_per_sample, 0, 0, + &app.wav) ); + } else { + err_exit("invalid output file", PJ_EINVAL); + } + + /* Loop reading PCAP and writing WAV file */ + for (;;) { + struct pkt pkt1; + pj_timestamp ts; + pjmedia_frame frames[16], pcm_frame; + short pcm[320]; + unsigned i, frame_cnt; + long samples_cnt, ts_gap; + + pj_assert(sizeof(pcm) >= samples_per_frame); + + /* Parse first packet */ + ts.u64 = 0; + frame_cnt = PJ_ARRAY_SIZE(frames); + T( pjmedia_codec_parse(app.codec, pkt0.payload, pkt0.payload_len, + &ts, &frame_cnt, frames) ); + + /* Decode and write to WAV file */ + samples_cnt = 0; + for (i=0; i<frame_cnt; ++i) { + pjmedia_frame pcm_frame; + + pcm_frame.buf = pcm; + pcm_frame.size = samples_per_frame * 2; + + T( pjmedia_codec_decode(app.codec, &frames[i], pcm_frame.size, + &pcm_frame) ); + if (app.wav) { + T( pjmedia_port_put_frame(app.wav, &pcm_frame) ); + } + if (app.aud_strm) { + T( wait_play(&pcm_frame) ); + } + samples_cnt += samples_per_frame; + } + + /* Read next packet */ + read_rtp(pkt1.buffer, sizeof(pkt1.buffer), &pkt1.rtp, + &pkt1.payload, &pkt1.payload_len, PJ_TRUE); + + /* Fill in the gap (if any) between pkt0 and pkt1 */ + ts_gap = pj_ntohl(pkt1.rtp->ts) - pj_ntohl(pkt0.rtp->ts) - + samples_cnt; + while (ts_gap >= (long)samples_per_frame) { + + pcm_frame.buf = pcm; + pcm_frame.size = samples_per_frame * 2; + + if (app.codec->op->recover) { + T( pjmedia_codec_recover(app.codec, pcm_frame.size, + &pcm_frame) ); + } else { + pj_bzero(pcm_frame.buf, pcm_frame.size); + } + + if (app.wav) { + T( pjmedia_port_put_frame(app.wav, &pcm_frame) ); + } + if (app.aud_strm) { + T( wait_play(&pcm_frame) ); + } + ts_gap -= samples_per_frame; + } + + /* Next */ + pkt0 = pkt1; + pkt0.rtp = (pjmedia_rtp_hdr*)pkt0.buffer; + pkt0.payload = pkt0.buffer + (pkt1.payload - pkt1.buffer); + } +} + + +int main(int argc, char *argv[]) +{ + pj_str_t input, output, srtp_crypto, srtp_key, codec; + pjmedia_aud_dev_index dev_id = PJMEDIA_AUD_DEFAULT_PLAYBACK_DEV; + pj_pcap_filter filter; + pj_status_t status; + + enum { + OPT_SRC_IP = 1, OPT_DST_IP, OPT_SRC_PORT, OPT_DST_PORT, + OPT_CODEC, OPT_PLAY_DEV_ID + }; + struct pj_getopt_option long_options[] = { + { "srtp-crypto", 1, 0, 'c' }, + { "srtp-key", 1, 0, 'k' }, + { "src-ip", 1, 0, OPT_SRC_IP }, + { "dst-ip", 1, 0, OPT_DST_IP }, + { "src-port", 1, 0, OPT_SRC_PORT }, + { "dst-port", 1, 0, OPT_DST_PORT }, + { "codec", 1, 0, OPT_CODEC }, + { "play-dev-id", 1, 0, OPT_PLAY_DEV_ID }, + { NULL, 0, 0, 0} + }; + int c; + int option_index; + char key_bin[32]; + + srtp_crypto.slen = srtp_key.slen = 0; + codec.slen = 0; + + pj_pcap_filter_default(&filter); + filter.link = PJ_PCAP_LINK_TYPE_ETH; + filter.proto = PJ_PCAP_PROTO_TYPE_UDP; + + /* Parse arguments */ + pj_optind = 0; + while((c=pj_getopt_long(argc,argv, "c:k:", long_options, &option_index))!=-1) { + switch (c) { + case 'c': + srtp_crypto = pj_str(pj_optarg); + break; + case 'k': + { + int key_len = sizeof(key_bin); + srtp_key = pj_str(pj_optarg); + if (pj_base64_decode(&srtp_key, (pj_uint8_t*)key_bin, &key_len)) { + puts("Error: invalid key"); + return 1; + } + srtp_key.ptr = key_bin; + srtp_key.slen = key_len; + } + break; + case OPT_SRC_IP: + { + pj_str_t t = pj_str(pj_optarg); + pj_in_addr a = pj_inet_addr(&t); + filter.ip_src = a.s_addr; + } + break; + case OPT_DST_IP: + { + pj_str_t t = pj_str(pj_optarg); + pj_in_addr a = pj_inet_addr(&t); + filter.ip_dst = a.s_addr; + } + break; + case OPT_SRC_PORT: + filter.src_port = pj_htons((pj_uint16_t)atoi(pj_optarg)); + break; + case OPT_DST_PORT: + filter.dst_port = pj_htons((pj_uint16_t)atoi(pj_optarg)); + break; + case OPT_CODEC: + codec = pj_str(pj_optarg); + break; + case OPT_PLAY_DEV_ID: + dev_id = atoi(pj_optarg); + break; + default: + puts("Error: invalid option"); + return 1; + } + } + + if (pj_optind != argc - 2) { + puts(USAGE); + return 1; + } + + if (!(srtp_crypto.slen) != !(srtp_key.slen)) { + puts("Error: both SRTP crypto and key must be specified"); + puts(USAGE); + return 1; + } + + input = pj_str(argv[pj_optind]); + output = pj_str(argv[pj_optind+1]); + + T( pj_init() ); + + pj_caching_pool_init(&app.cp, NULL, 0); + app.pool = pj_pool_create(&app.cp.factory, "pcaputil", 1000, 1000, NULL); + + T( pjlib_util_init() ); + T( pjmedia_endpt_create(&app.cp.factory, NULL, 0, &app.mept) ); + + T( pj_pcap_open(app.pool, input.ptr, &app.pcap) ); + T( pj_pcap_set_filter(app.pcap, &filter) ); + + pcap2wav(&codec, &output, dev_id, &srtp_crypto, &srtp_key); + + cleanup(); + return 0; +} + diff --git a/pjsip-apps/src/samples/pjsip-perf.c b/pjsip-apps/src/samples/pjsip-perf.c new file mode 100644 index 0000000..f1b7cb9 --- /dev/null +++ b/pjsip-apps/src/samples/pjsip-perf.c @@ -0,0 +1,1853 @@ +/* $Id: pjsip-perf.c 3664 2011-07-19 03:42:28Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 + */ + + +/** + * \page page_pjsip_perf_c Samples: SIP Performance Benchmark + * + * <b>pjsip-perf</b> is a complete program to measure the + * performance of PJSIP or other SIP endpoints. It consists of two + * parts: + * - the server, to respond incoming requests, and + * - the client, who actively submits requests and measure the + * performance of the server. + * + * Both server and client part can run simultaneously, to measure the + * performance when both endpoints are co-located in a single program. + * + * The server accepts both INVITE and non-INVITE requests. + * The server exports several different types of URL, which would + * control how the request would be handled by the server: + * - URL with "0" as the user part will be handled statelessly. + * It should not be used with INVITE method. + * - URL with "1" as the user part will be handled statefully. + * If the request is an INVITE request, INVITE transaction will + * be created and 200/OK response will be sent, along with a valid + * SDP body. However, the SDP is just a static text body, and + * is not a proper SDP generated by PJMEDIA. + * - URL with "2" as the user part is only meaningful for INVITE + * requests, as it would be handled <b>call-statefully</b> by the + * server. For this URL, the server also would generate SDP dynamically + * and perform a proper SDP negotiation for the incoming call. + * Also for every call, server will limit the call duration to + * 10 seconds, on which the call will be terminated if the client + * doesn't hangup the call. + * + * + * + * This file is pjsip-apps/src/samples/pjsip-perf.c + * + * \includelineno pjsip-perf.c + */ + +/* Include all headers. */ +#include <pjsip.h> +#include <pjmedia.h> +#include <pjmedia-codec.h> +#include <pjsip_ua.h> +#include <pjsip_simple.h> +#include <pjlib-util.h> +#include <pjlib.h> +#include <stdio.h> + +#if defined(PJ_WIN32) && PJ_WIN32!=0 +# include <windows.h> +#endif + +#define THIS_FILE "pjsip-perf.c" +#define DEFAULT_COUNT (pjsip_cfg()->tsx.max_count/2>10000?10000:pjsip_cfg()->tsx.max_count/2) +#define JOB_WINDOW 1000 +#define TERMINATE_TSX(x,c) + + +#ifndef CACHING_POOL_SIZE +# define CACHING_POOL_SIZE (256*1024*1024) +#endif + + +/* Static message body for INVITE, when stateful processing is + * invoked (instead of call-stateful, where SDP is generated + * dynamically. + */ +static pj_str_t dummy_sdp_str = +{ + "v=0\r\n" + "o=- 3360842071 3360842071 IN IP4 192.168.0.68\r\n" + "s=pjmedia\r\n" + "c=IN IP4 192.168.0.68\r\n" + "t=0 0\r\n" + "m=audio 4000 RTP/AVP 0 8 3 103 102 101\r\n" + "a=rtcp:4001 IN IP4 192.168.0.68\r\n" + "a=rtpmap:103 speex/16000\r\n" + "a=rtpmap:102 speex/8000\r\n" + "a=rtpmap:3 GSM/8000\r\n" + "a=rtpmap:0 PCMU/8000\r\n" + "a=rtpmap:8 PCMA/8000\r\n" + "a=sendrecv\r\n" + "a=rtpmap:101 telephone-event/8000\r\n" + "a=fmtp:101 0-15\r\n", + 0 +}; + +static pj_str_t mime_application = { "application", 11}; +static pj_str_t mime_sdp = {"sdp", 3}; + + +struct srv_state +{ + unsigned stateless_cnt; + unsigned stateful_cnt; + unsigned call_cnt; +}; + + +struct app +{ + pj_caching_pool cp; + pj_pool_t *pool; + pj_bool_t use_tcp; + pj_str_t local_addr; + int local_port; + pjsip_endpoint *sip_endpt; + pjmedia_endpt *med_endpt; + pj_str_t local_uri; + pj_str_t local_contact; + unsigned skinfo_cnt; + pjmedia_sock_info skinfo[8]; + + pj_bool_t thread_quit; + unsigned thread_count; + pj_thread_t *thread[16]; + + pj_bool_t real_sdp; + pjmedia_sdp_session *dummy_sdp; + + int log_level; + + struct { + pjsip_method method; + pj_str_t dst_uri; + pj_bool_t stateless; + unsigned timeout; + unsigned job_count, + job_submitted, + job_finished, + job_window; + unsigned stat_max_window; + pj_time_val first_request; + pj_time_val requests_sent; + pj_time_val last_completion; + unsigned total_responses; + unsigned response_codes[800]; + } client; + + struct { + pj_bool_t send_trying; + pj_bool_t send_ringing; + unsigned delay; + struct srv_state prev_state; + struct srv_state cur_state; + } server; + + +} app; + +struct call +{ + pjsip_inv_session *inv; + pj_timer_entry ans_timer; +}; + + +static void app_perror(const char *sender, const char *title, + pj_status_t status) +{ + char errmsg[PJ_ERR_MSG_SIZE]; + + pj_strerror(status, errmsg, sizeof(errmsg)); + PJ_LOG(1,(sender, "%s: %s [code=%d]", title, errmsg, status)); +} + + +/************************************************************************** + * STATELESS SERVER + */ +static pj_bool_t mod_stateless_on_rx_request(pjsip_rx_data *rdata); + +/* Module to handle incoming requests statelessly. + */ +static pjsip_module mod_stateless_server = +{ + NULL, NULL, /* prev, next. */ + { "mod-stateless-server", 20 }, /* Name. */ + -1, /* Id */ + PJSIP_MOD_PRIORITY_APPLICATION, /* Priority */ + NULL, /* load() */ + NULL, /* start() */ + NULL, /* stop() */ + NULL, /* unload() */ + &mod_stateless_on_rx_request, /* on_rx_request() */ + NULL, /* on_rx_response() */ + NULL, /* on_tx_request. */ + NULL, /* on_tx_response() */ + NULL, /* on_tsx_state() */ +}; + + +static pj_bool_t mod_stateless_on_rx_request(pjsip_rx_data *rdata) +{ + const pj_str_t stateless_user = { "0", 1 }; + pjsip_uri *uri; + pjsip_sip_uri *sip_uri; + + uri = pjsip_uri_get_uri(rdata->msg_info.msg->line.req.uri); + + /* Only want to receive SIP scheme */ + if (!PJSIP_URI_SCHEME_IS_SIP(uri)) + return PJ_FALSE; + + sip_uri = (pjsip_sip_uri*) uri; + + /* Check for matching user part */ + if (pj_strcmp(&sip_uri->user, &stateless_user)!=0) + return PJ_FALSE; + + /* + * Yes, this is for us. + */ + + /* Ignore ACK request */ + if (rdata->msg_info.msg->line.req.method.id == PJSIP_ACK_METHOD) + return PJ_TRUE; + + /* + * Respond statelessly with 200/OK. + */ + pjsip_endpt_respond_stateless(app.sip_endpt, rdata, 200, NULL, + NULL, NULL); + app.server.cur_state.stateless_cnt++; + return PJ_TRUE; +} + + +/************************************************************************** + * STATEFUL SERVER + */ +static pj_bool_t mod_stateful_on_rx_request(pjsip_rx_data *rdata); + +/* Module to handle incoming requests statefully. + */ +static pjsip_module mod_stateful_server = +{ + NULL, NULL, /* prev, next. */ + { "mod-stateful-server", 19 }, /* Name. */ + -1, /* Id */ + PJSIP_MOD_PRIORITY_APPLICATION, /* Priority */ + NULL, /* load() */ + NULL, /* start() */ + NULL, /* stop() */ + NULL, /* unload() */ + &mod_stateful_on_rx_request, /* on_rx_request() */ + NULL, /* on_rx_response() */ + NULL, /* on_tx_request. */ + NULL, /* on_tx_response() */ + NULL, /* on_tsx_state() */ +}; + + +static pj_bool_t mod_stateful_on_rx_request(pjsip_rx_data *rdata) +{ + const pj_str_t stateful_user = { "1", 1 }; + pjsip_uri *uri; + pjsip_sip_uri *sip_uri; + + uri = pjsip_uri_get_uri(rdata->msg_info.msg->line.req.uri); + + /* Only want to receive SIP scheme */ + if (!PJSIP_URI_SCHEME_IS_SIP(uri)) + return PJ_FALSE; + + sip_uri = (pjsip_sip_uri*) uri; + + /* Check for matching user part */ + if (pj_strcmp(&sip_uri->user, &stateful_user)!=0) + return PJ_FALSE; + + /* + * Yes, this is for us. + * Respond statefully with 200/OK. + */ + switch (rdata->msg_info.msg->line.req.method.id) { + case PJSIP_INVITE_METHOD: + { + pjsip_msg_body *body; + + if (dummy_sdp_str.slen == 0) + dummy_sdp_str.slen = pj_ansi_strlen(dummy_sdp_str.ptr); + + body = pjsip_msg_body_create(rdata->tp_info.pool, + &mime_application, &mime_sdp, + &dummy_sdp_str); + pjsip_endpt_respond(app.sip_endpt, &mod_stateful_server, rdata, + 200, NULL, NULL, body, NULL); + } + break; + case PJSIP_ACK_METHOD: + return PJ_TRUE; + default: + pjsip_endpt_respond(app.sip_endpt, &mod_stateful_server, rdata, + 200, NULL, NULL, NULL, NULL); + break; + } + + app.server.cur_state.stateful_cnt++; + return PJ_TRUE; +} + + +/************************************************************************** + * CALL SERVER + */ +static pj_bool_t mod_call_on_rx_request(pjsip_rx_data *rdata); + +/* Module to handle incoming requests callly. + */ +static pjsip_module mod_call_server = +{ + NULL, NULL, /* prev, next. */ + { "mod-call-server", 15 }, /* Name. */ + -1, /* Id */ + PJSIP_MOD_PRIORITY_APPLICATION, /* Priority */ + NULL, /* load() */ + NULL, /* start() */ + NULL, /* stop() */ + NULL, /* unload() */ + &mod_call_on_rx_request, /* on_rx_request() */ + NULL, /* on_rx_response() */ + NULL, /* on_tx_request. */ + NULL, /* on_tx_response() */ + NULL, /* on_tsx_state() */ +}; + + +static pj_status_t send_response(pjsip_inv_session *inv, + pjsip_rx_data *rdata, + int code, + pj_bool_t *has_initial) +{ + pjsip_tx_data *tdata; + pj_status_t status; + + if (*has_initial) { + status = pjsip_inv_answer(inv, code, NULL, NULL, &tdata); + } else { + status = pjsip_inv_initial_answer(inv, rdata, code, + NULL, NULL, &tdata); + } + + if (status != PJ_SUCCESS) { + if (*has_initial) { + status = pjsip_inv_answer(inv, PJSIP_SC_NOT_ACCEPTABLE, + NULL, NULL, &tdata); + } else { + status = pjsip_inv_initial_answer(inv, rdata, + PJSIP_SC_NOT_ACCEPTABLE, + NULL, NULL, &tdata); + } + + if (status == PJ_SUCCESS) { + *has_initial = PJ_TRUE; + pjsip_inv_send_msg(inv, tdata); + } else { + pjsip_inv_terminate(inv, 500, PJ_FALSE); + return -1; + } + } else { + *has_initial = PJ_TRUE; + + status = pjsip_inv_send_msg(inv, tdata); + if (status != PJ_SUCCESS) { + pjsip_tx_data_dec_ref(tdata); + return status; + } + } + + return status; +} + +static void answer_timer_cb(pj_timer_heap_t *h, pj_timer_entry *entry) +{ + struct call *call = entry->user_data; + pj_bool_t has_initial = PJ_TRUE; + + PJ_UNUSED_ARG(h); + + entry->id = 0; + send_response(call->inv, NULL, 200, &has_initial); +} + +static pj_bool_t mod_call_on_rx_request(pjsip_rx_data *rdata) +{ + const pj_str_t call_user = { "2", 1 }; + pjsip_uri *uri; + pjsip_sip_uri *sip_uri; + struct call *call; + pjsip_dialog *dlg; + pjmedia_sdp_session *sdp; + pjsip_tx_data *tdata; + pj_bool_t has_initial = PJ_FALSE; + pj_status_t status; + + uri = pjsip_uri_get_uri(rdata->msg_info.msg->line.req.uri); + + /* Only want to receive SIP scheme */ + if (!PJSIP_URI_SCHEME_IS_SIP(uri)) + return PJ_FALSE; + + sip_uri = (pjsip_sip_uri*) uri; + + /* Only want to handle INVITE requests. */ + if (rdata->msg_info.msg->line.req.method.id != PJSIP_INVITE_METHOD) { + return PJ_FALSE; + } + + + /* Check for matching user part. Incoming requests will be handled + * call-statefully if: + * - user part is "2", or + * - user part is not "0" nor "1" and method is INVITE. + */ + if (pj_strcmp(&sip_uri->user, &call_user) == 0 || + sip_uri->user.slen != 1 || + (*sip_uri->user.ptr != '0' && *sip_uri->user.ptr != '1')) + { + /* Match */ + + } else { + return PJ_FALSE; + } + + + /* Verify that we can handle the request. */ + if (app.real_sdp) { + unsigned options = 0; + status = pjsip_inv_verify_request(rdata, &options, NULL, NULL, + app.sip_endpt, &tdata); + if (status != PJ_SUCCESS) { + + /* + * No we can't handle the incoming INVITE request. + */ + + if (tdata) { + pjsip_response_addr res_addr; + + pjsip_get_response_addr(tdata->pool, rdata, &res_addr); + pjsip_endpt_send_response(app.sip_endpt, &res_addr, tdata, + NULL, NULL); + + } else { + + /* Respond with 500 (Internal Server Error) */ + pjsip_endpt_respond_stateless(app.sip_endpt, rdata, 500, NULL, + NULL, NULL); + } + + return PJ_TRUE; + } + } + + /* Create UAS dialog */ + status = pjsip_dlg_create_uas( pjsip_ua_instance(), rdata, + &app.local_contact, &dlg); + if (status != PJ_SUCCESS) { + const pj_str_t reason = pj_str("Unable to create dialog"); + pjsip_endpt_respond_stateless( app.sip_endpt, rdata, + 500, &reason, + NULL, NULL); + return PJ_TRUE; + } + + /* Alloc call structure. */ + call = pj_pool_zalloc(dlg->pool, sizeof(struct call)); + + /* Create SDP from PJMEDIA */ + if (app.real_sdp) { + status = pjmedia_endpt_create_sdp(app.med_endpt, rdata->tp_info.pool, + app.skinfo_cnt, app.skinfo, + &sdp); + } else { + sdp = app.dummy_sdp; + } + + /* Create UAS invite session */ + status = pjsip_inv_create_uas( dlg, rdata, sdp, 0, &call->inv); + if (status != PJ_SUCCESS) { + pjsip_dlg_create_response(dlg, rdata, 500, NULL, &tdata); + pjsip_dlg_send_response(dlg, pjsip_rdata_get_tsx(rdata), tdata); + return PJ_TRUE; + } + + /* Send 100/Trying if needed */ + if (app.server.send_trying) { + status = send_response(call->inv, rdata, 100, &has_initial); + if (status != PJ_SUCCESS) + return PJ_TRUE; + } + + /* Send 180/Ringing if needed */ + if (app.server.send_ringing) { + status = send_response(call->inv, rdata, 180, &has_initial); + if (status != PJ_SUCCESS) + return PJ_TRUE; + } + + /* Simulate call processing delay */ + if (app.server.delay) { + pj_time_val delay; + + call->ans_timer.id = 1; + call->ans_timer.user_data = call; + call->ans_timer.cb = &answer_timer_cb; + + delay.sec = 0; + delay.msec = app.server.delay; + pj_time_val_normalize(&delay); + + pjsip_endpt_schedule_timer(app.sip_endpt, &call->ans_timer, &delay); + + } else { + /* Send the 200 response immediately . */ + status = send_response(call->inv, rdata, 200, &has_initial); + PJ_ASSERT_ON_FAIL(status == PJ_SUCCESS, return PJ_TRUE); + } + + /* Done */ + app.server.cur_state.call_cnt++; + + return PJ_TRUE; +} + + + +/************************************************************************** + * Default handler when incoming request is not handled by any other + * modules. + */ +static pj_bool_t mod_responder_on_rx_request(pjsip_rx_data *rdata); + +/* Module to handle incoming requests statelessly. + */ +static pjsip_module mod_responder = +{ + NULL, NULL, /* prev, next. */ + { "mod-responder", 13 }, /* Name. */ + -1, /* Id */ + PJSIP_MOD_PRIORITY_APPLICATION+1, /* Priority */ + NULL, /* load() */ + NULL, /* start() */ + NULL, /* stop() */ + NULL, /* unload() */ + &mod_responder_on_rx_request, /* on_rx_request() */ + NULL, /* on_rx_response() */ + NULL, /* on_tx_request. */ + NULL, /* on_tx_response() */ + NULL, /* on_tsx_state() */ +}; + + +static pj_bool_t mod_responder_on_rx_request(pjsip_rx_data *rdata) +{ + const pj_str_t reason = pj_str("Not expecting request at this URI"); + + /* + * Respond any requests (except ACK!) with 500. + */ + if (rdata->msg_info.msg->line.req.method.id != PJSIP_ACK_METHOD) { + pjsip_endpt_respond_stateless(app.sip_endpt, rdata, 500, &reason, + NULL, NULL); + } + + return PJ_TRUE; +} + + + +/***************************************************************************** + * Below is a simple module to log all incoming and outgoing SIP messages + */ + + +/* Notification on incoming messages */ +static pj_bool_t logger_on_rx_msg(pjsip_rx_data *rdata) +{ + PJ_LOG(3,(THIS_FILE, "RX %d bytes %s from %s %s:%d:\n" + "%.*s\n" + "--end msg--", + rdata->msg_info.len, + pjsip_rx_data_get_info(rdata), + rdata->tp_info.transport->type_name, + rdata->pkt_info.src_name, + rdata->pkt_info.src_port, + (int)rdata->msg_info.len, + rdata->msg_info.msg_buf)); + + /* Always return false, otherwise messages will not get processed! */ + return PJ_FALSE; +} + +/* Notification on outgoing messages */ +static pj_status_t logger_on_tx_msg(pjsip_tx_data *tdata) +{ + + /* Important note: + * tp_info field is only valid after outgoing messages has passed + * transport layer. So don't try to access tp_info when the module + * has lower priority than transport layer. + */ + + PJ_LOG(3,(THIS_FILE, "TX %d bytes %s to %s %s:%d:\n" + "%.*s\n" + "--end msg--", + (tdata->buf.cur - tdata->buf.start), + pjsip_tx_data_get_info(tdata), + tdata->tp_info.transport->type_name, + tdata->tp_info.dst_name, + tdata->tp_info.dst_port, + (int)(tdata->buf.cur - tdata->buf.start), + tdata->buf.start)); + + /* Always return success, otherwise message will not get sent! */ + return PJ_SUCCESS; +} + +/* The module instance. */ +static pjsip_module msg_logger = +{ + NULL, NULL, /* prev, next. */ + { "mod-siprtp-log", 14 }, /* Name. */ + -1, /* Id */ + PJSIP_MOD_PRIORITY_TRANSPORT_LAYER-1,/* Priority */ + NULL, /* load() */ + NULL, /* start() */ + NULL, /* stop() */ + NULL, /* unload() */ + &logger_on_rx_msg, /* on_rx_request() */ + &logger_on_rx_msg, /* on_rx_response() */ + &logger_on_tx_msg, /* on_tx_request. */ + &logger_on_tx_msg, /* on_tx_response() */ + NULL, /* on_tsx_state() */ + +}; + + + +/************************************************************************** + * Test Client. + */ + +static pj_bool_t mod_test_on_rx_response(pjsip_rx_data *rdata); + +static void call_on_media_update( pjsip_inv_session *inv, + pj_status_t status); +static void call_on_state_changed( pjsip_inv_session *inv, + pjsip_event *e); +static void call_on_forked(pjsip_inv_session *inv, pjsip_event *e); + + +/* Module to handle incoming requests callly. + */ +static pjsip_module mod_test = +{ + NULL, NULL, /* prev, next. */ + { "mod-test", 8 }, /* Name. */ + -1, /* Id */ + PJSIP_MOD_PRIORITY_APPLICATION, /* Priority */ + NULL, /* load() */ + NULL, /* start() */ + NULL, /* stop() */ + NULL, /* unload() */ + NULL, /* on_rx_request() */ + &mod_test_on_rx_response, /* on_rx_response() */ + NULL, /* on_tx_request. */ + NULL, /* on_tx_response() */ + NULL, /* on_tsx_state() */ +}; + + +static void report_completion(int status_code) +{ + app.client.job_finished++; + if (status_code >= 200 && status_code < 800) + app.client.response_codes[status_code]++; + app.client.total_responses++; + pj_gettimeofday(&app.client.last_completion); +} + + +/* Handler when response is received. */ +static pj_bool_t mod_test_on_rx_response(pjsip_rx_data *rdata) +{ + if (pjsip_rdata_get_tsx(rdata) == NULL) { + report_completion(rdata->msg_info.msg->line.status.code); + } + + return PJ_TRUE; +} + + +/* + * Create app + */ +static pj_status_t create_app(void) +{ + pj_status_t status; + + status = pj_init(); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Error initializing pjlib", status); + return status; + } + + /* init PJLIB-UTIL: */ + status = pjlib_util_init(); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); + + /* Must create a pool factory before we can allocate any memory. */ + pj_caching_pool_init(&app.cp, &pj_pool_factory_default_policy, + CACHING_POOL_SIZE); + + /* Create application pool for misc. */ + app.pool = pj_pool_create(&app.cp.factory, "app", 1000, 1000, NULL); + + /* Create the endpoint: */ + status = pjsip_endpt_create(&app.cp.factory, pj_gethostname()->ptr, + &app.sip_endpt); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); + + + return status; +} + + +/* + * Init SIP stack + */ +static pj_status_t init_sip() +{ + pj_status_t status = -1; + + /* Add UDP/TCP transport. */ + { + pj_sockaddr_in addr; + pjsip_host_port addrname; + const char *transport_type = NULL; + + pj_bzero(&addr, sizeof(addr)); + addr.sin_family = pj_AF_INET(); + addr.sin_addr.s_addr = 0; + addr.sin_port = pj_htons((pj_uint16_t)app.local_port); + + if (app.local_addr.slen) { + addrname.host = app.local_addr; + addrname.port = 5060; + } + if (app.local_port != 0) + addrname.port = app.local_port; + + if (0) { +#if defined(PJ_HAS_TCP) && PJ_HAS_TCP!=0 + } else if (app.use_tcp) { + pj_sockaddr_in local_addr; + pjsip_tpfactory *tpfactory; + + transport_type = "tcp"; + pj_sockaddr_in_init(&local_addr, 0, (pj_uint16_t)app.local_port); + status = pjsip_tcp_transport_start(app.sip_endpt, &local_addr, + app.thread_count, &tpfactory); + if (status == PJ_SUCCESS) { + app.local_addr = tpfactory->addr_name.host; + app.local_port = tpfactory->addr_name.port; + } +#endif + } else { + pjsip_transport *tp; + + transport_type = "udp"; + status = pjsip_udp_transport_start(app.sip_endpt, &addr, + (app.local_addr.slen ? &addrname:NULL), + app.thread_count, &tp); + if (status == PJ_SUCCESS) { + app.local_addr = tp->local_name.host; + app.local_port = tp->local_name.port; + } + + } + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to start transport", status); + return status; + } + + app.local_uri.ptr = pj_pool_alloc(app.pool, 128); + app.local_uri.slen = pj_ansi_sprintf(app.local_uri.ptr, + "<sip:pjsip-perf@%.*s:%d;transport=%s>", + (int)app.local_addr.slen, + app.local_addr.ptr, + app.local_port, + transport_type); + + app.local_contact = app.local_uri; + } + + /* + * Init transaction layer. + * This will create/initialize transaction hash tables etc. + */ + status = pjsip_tsx_layer_init_module(app.sip_endpt); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); + + /* Initialize UA layer. */ + status = pjsip_ua_init_module( app.sip_endpt, NULL ); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); + + /* Initialize 100rel support */ + status = pjsip_100rel_init_module(app.sip_endpt); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); + + /* Init invite session module. */ + { + pjsip_inv_callback inv_cb; + + /* Init the callback for INVITE session: */ + pj_bzero(&inv_cb, sizeof(inv_cb)); + inv_cb.on_state_changed = &call_on_state_changed; + inv_cb.on_new_session = &call_on_forked; + inv_cb.on_media_update = &call_on_media_update; + + /* Initialize invite session module: */ + status = pjsip_inv_usage_init(app.sip_endpt, &inv_cb); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + } + + /* Register our module to receive incoming requests. */ + status = pjsip_endpt_register_module( app.sip_endpt, &mod_test); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); + + + /* Register stateless server module */ + status = pjsip_endpt_register_module( app.sip_endpt, &mod_stateless_server); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); + + /* Register default responder module */ + status = pjsip_endpt_register_module( app.sip_endpt, &mod_responder); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); + + /* Register stateless server module */ + status = pjsip_endpt_register_module( app.sip_endpt, &mod_stateful_server); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); + + + /* Register call server module */ + status = pjsip_endpt_register_module( app.sip_endpt, &mod_call_server); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); + + + /* Done */ + return PJ_SUCCESS; +} + + +/* + * Destroy SIP + */ +static void destroy_app() +{ + unsigned i; + + app.thread_quit = 1; + for (i=0; i<app.thread_count; ++i) { + if (app.thread[i]) { + pj_thread_join(app.thread[i]); + pj_thread_destroy(app.thread[i]); + app.thread[i] = NULL; + } + } + + if (app.sip_endpt) { + pjsip_endpt_destroy(app.sip_endpt); + app.sip_endpt = NULL; + } + + if (app.pool) { + pj_pool_release(app.pool); + app.pool = NULL; + PJ_LOG(3,(THIS_FILE, "Peak memory size: %uMB", + app.cp.peak_used_size / 1000000)); + pj_caching_pool_destroy(&app.cp); + } + + /* Shutdown PJLIB */ + pj_shutdown(); +} + + +/* + * Init media stack. + */ +static pj_status_t init_media() +{ + unsigned i; + pj_uint16_t rtp_port; + pj_status_t status; + + + /* Initialize media endpoint so that at least error subsystem is properly + * initialized. + */ + status = pjmedia_endpt_create(&app.cp.factory, + pjsip_endpt_get_ioqueue(app.sip_endpt), 0, + &app.med_endpt); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); + + + /* Must register all codecs to be supported */ + pjmedia_codec_register_audio_codecs(app.med_endpt, NULL); + + /* Init dummy socket addresses */ + app.skinfo_cnt = 0; + for (i=0, rtp_port=4000; i<PJ_ARRAY_SIZE(app.skinfo); ++i, rtp_port+=2) { + pjmedia_sock_info *skinfo; + + skinfo = &app.skinfo[i]; + + pj_sockaddr_in_init(&skinfo->rtp_addr_name.ipv4, &app.local_addr, + (pj_uint16_t)rtp_port); + pj_sockaddr_in_init(&skinfo->rtp_addr_name.ipv4, &app.local_addr, + (pj_uint16_t)(rtp_port+1)); + app.skinfo_cnt++; + } + + /* Generate dummy SDP */ + dummy_sdp_str.slen = pj_ansi_strlen(dummy_sdp_str.ptr); + status = pjmedia_sdp_parse(app.pool, dummy_sdp_str.ptr, dummy_sdp_str.slen, + &app.dummy_sdp); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Error parsing dummy SDP", status); + return status; + } + + + /* Done */ + return PJ_SUCCESS; +} + + +/* This is notification from the call about media negotiation + * status. This is called for client calls only. + */ +static void call_on_media_update( pjsip_inv_session *inv, + pj_status_t status) +{ + if (status != PJ_SUCCESS) { + pjsip_tx_data *tdata; + pj_status_t status; + + status = pjsip_inv_end_session(inv, PJSIP_SC_UNSUPPORTED_MEDIA_TYPE, + NULL, &tdata); + if (status == PJ_SUCCESS && tdata) + status = pjsip_inv_send_msg(inv, tdata); + } +} + + +/* This is notification from the call when the call state has changed. + * This is called for client calls only. + */ +static void call_on_state_changed( pjsip_inv_session *inv, + pjsip_event *e) +{ + PJ_UNUSED_ARG(e); + + /* Bail out if the session has been counted before */ + if (inv->mod_data[mod_test.id] != NULL) + return; + + /* Bail out if this is not an outgoing call */ + if (inv->role != PJSIP_UAC_ROLE) + return; + + if (inv->state == PJSIP_INV_STATE_CONFIRMED) { + pjsip_tx_data *tdata; + pj_status_t status; + + //report_completion(200); + //inv->mod_data[mod_test.id] = (void*)1; + + status = pjsip_inv_end_session(inv, PJSIP_SC_OK, NULL, &tdata); + if (status == PJ_SUCCESS && tdata) + status = pjsip_inv_send_msg(inv, tdata); + + } else if (inv->state == PJSIP_INV_STATE_DISCONNECTED) { + report_completion(inv->cause); + inv->mod_data[mod_test.id] = (void*)1; + } +} + + +/* Not implemented for now */ +static void call_on_forked(pjsip_inv_session *inv, pjsip_event *e) +{ + /* Do nothing */ + PJ_UNUSED_ARG(inv); + PJ_UNUSED_ARG(e); +} + + +/* + * Make outgoing call. + */ +static pj_status_t make_call(const pj_str_t *dst_uri) +{ + struct call *call; + pjsip_dialog *dlg; + pjmedia_sdp_session *sdp; + pjsip_tx_data *tdata; + pj_status_t status; + + + /* Create UAC dialog */ + status = pjsip_dlg_create_uac( pjsip_ua_instance(), + &app.local_uri, /* local URI */ + &app.local_contact, /* local Contact */ + dst_uri, /* remote URI */ + dst_uri, /* remote target */ + &dlg); /* dialog */ + if (status != PJ_SUCCESS) { + return status; + } + + /* Create call */ + call = pj_pool_zalloc(dlg->pool, sizeof(struct call)); + + /* Create SDP */ + if (app.real_sdp) { + status = pjmedia_endpt_create_sdp(app.med_endpt, dlg->pool, 1, + app.skinfo, &sdp); + if (status != PJ_SUCCESS) { + pjsip_dlg_terminate(dlg); + return status; + } + } else + sdp = app.dummy_sdp; + + /* Create the INVITE session. */ + status = pjsip_inv_create_uac( dlg, sdp, 0, &call->inv); + if (status != PJ_SUCCESS) { + pjsip_dlg_terminate(dlg); + return status; + } + + + /* Create initial INVITE request. + * This INVITE request will contain a perfectly good request and + * an SDP body as well. + */ + status = pjsip_inv_invite(call->inv, &tdata); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); + + + /* Send initial INVITE request. + * From now on, the invite session's state will be reported to us + * via the invite session callbacks. + */ + status = pjsip_inv_send_msg(call->inv, tdata); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); + + + return PJ_SUCCESS; +} + + +/* + * Verify that valid SIP url is given. + */ +static pj_status_t verify_sip_url(const char *c_url) +{ + pjsip_uri *p; + pj_pool_t *pool; + char *url; + int len = (c_url ? pj_ansi_strlen(c_url) : 0); + + if (!len) return -1; + + pool = pj_pool_create(&app.cp.factory, "check%p", 1024, 0, NULL); + if (!pool) return PJ_ENOMEM; + + url = pj_pool_alloc(pool, len+1); + pj_ansi_strcpy(url, c_url); + url[len] = '\0'; + + p = pjsip_parse_uri(pool, url, len, 0); + if (!p || pj_stricmp2(pjsip_uri_get_scheme(p), "sip") != 0) + p = NULL; + + pj_pool_release(pool); + return p ? 0 : -1; +} + + +static void usage(void) +{ + printf( + "Usage:\n" + " pjsip-perf [OPTIONS] -- to start as server\n" + " pjsip-perf [OPTIONS] URL -- to call server (possibly itself)\n" + "\n" + "where:\n" + " URL The SIP URL to be contacted.\n" + "\n" + "Client options:\n" + " --method=METHOD, -m Set test method (set to INVITE for call benchmark)\n" + " [default: OPTIONS]\n" + " --count=N, -n Set total number of requests to initiate\n" + " [default=%d]\n" + " --stateless, -s Set to operate in stateless mode\n" + " [default: stateful]\n" + " --timeout=SEC, -t Set client timeout [default=60 sec]\n" + " --window=COUNT, -w Set maximum outstanding job [default: %d]\n" + "\n" + "SDP options (client and server):\n" + " --real-sdp Generate real SDP from pjmedia, and also perform\n" + " proper SDP negotiation [default: dummy]\n" + "\n" + "Client and Server options:\n" + " --local-port=PORT, -p Set local port [default: 5060]\n" + " --use-tcp, -T Use TCP instead of UDP. Note that when started as\n" + " client, you must add ;transport=tcp parameter to URL\n" + " [default: no]\n" + " --thread-count=N Set number of worker threads [default=1]\n" + " --trying Send 100/Trying response (server, default no)\n" + " --ringing Send 180/Ringing response (server, default no)\n" + " --delay=MS, -d Delay answering call by MS (server, default no)\n" + "\n" + "Misc options:\n" + " --help, -h Display this screen\n" + " --verbose, -v Verbose logging (put more than once for even more)\n" + "\n" + "When started as server, pjsip-perf can be contacted on the following URIs:\n" + " - sip:0@server-addr To handle requests statelessly.\n" + " - sip:1@server-addr To handle requests statefully.\n" + " - sip:2@server-addr To handle INVITE call.\n", + DEFAULT_COUNT, JOB_WINDOW); +} + + +static int my_atoi(const char *s) +{ + pj_str_t ss = pj_str((char*)s); + return pj_strtoul(&ss); +} + + +static pj_status_t init_options(int argc, char *argv[]) +{ + enum { OPT_THREAD_COUNT = 1, OPT_REAL_SDP, OPT_TRYING, OPT_RINGING }; + struct pj_getopt_option long_options[] = { + { "local-port", 1, 0, 'p' }, + { "count", 1, 0, 'c' }, + { "thread-count", 1, 0, OPT_THREAD_COUNT }, + { "method", 1, 0, 'm' }, + { "help", 0, 0, 'h' }, + { "stateless", 0, 0, 's' }, + { "timeout", 1, 0, 't' }, + { "real-sdp", 0, 0, OPT_REAL_SDP }, + { "verbose", 0, 0, 'v' }, + { "use-tcp", 0, 0, 'T' }, + { "window", 1, 0, 'w' }, + { "delay", 1, 0, 'd' }, + { "trying", 0, 0, OPT_TRYING}, + { "ringing", 0, 0, OPT_RINGING}, + { NULL, 0, 0, 0 }, + }; + int c; + int option_index; + + /* Init default application configs */ + app.local_port = 5060; + app.thread_count = 1; + app.client.job_count = DEFAULT_COUNT; + app.client.method = *pjsip_get_options_method(); + app.client.job_window = c = JOB_WINDOW; + app.client.timeout = 60; + app.log_level = 3; + + + /* Parse options */ + pj_optind = 0; + while((c=pj_getopt_long(argc,argv, "p:c:m:t:w:d:hsv", + long_options, &option_index))!=-1) + { + switch (c) { + case 'p': + app.local_port = my_atoi(pj_optarg); + if (app.local_port < 0 || app.local_port > 65535) { + PJ_LOG(3,(THIS_FILE, "Invalid --local-port %s", pj_optarg)); + return -1; + } + break; + + case 'c': + app.client.job_count = my_atoi(pj_optarg); + if (app.client.job_count < 0) { + PJ_LOG(3,(THIS_FILE, "Invalid --local-port %s", pj_optarg)); + return -1; + } + if (app.client.job_count > pjsip_cfg()->tsx.max_count) + PJ_LOG(3,(THIS_FILE, + "Warning: --count value (%d) exceeds maximum " + "transaction count (%d)", app.client.job_count, + pjsip_cfg()->tsx.max_count)); + break; + + case OPT_THREAD_COUNT: + app.thread_count = my_atoi(pj_optarg); + if (app.thread_count < 1 || app.thread_count > 16) { + PJ_LOG(3,(THIS_FILE, "Invalid --thread-count %s", pj_optarg)); + return -1; + } + break; + + case 'm': + { + pj_str_t temp = pj_str((char*)pj_optarg); + pjsip_method_init_np(&app.client.method, &temp); + } + break; + + case 'h': + usage(); + return -1; + + case 's': + app.client.stateless = PJ_TRUE; + break; + + case OPT_REAL_SDP: + app.real_sdp = 1; + break; + + case 'v': + app.log_level++; + break; + + case 't': + app.client.timeout = my_atoi(pj_optarg); + if (app.client.timeout < 0 || app.client.timeout > 600) { + PJ_LOG(3,(THIS_FILE, "Invalid --timeout %s", pj_optarg)); + return -1; + } + break; + + case 'w': + app.client.job_window = my_atoi(pj_optarg); + if (app.client.job_window <= 0) { + PJ_LOG(3,(THIS_FILE, "Invalid --window %s", pj_optarg)); + return -1; + } + break; + + case 'T': + app.use_tcp = PJ_TRUE; + break; + + case 'd': + app.server.delay = my_atoi(pj_optarg); + if (app.server.delay > 3600) { + PJ_LOG(3,(THIS_FILE, "I think --delay %s is too long", + pj_optarg)); + return -1; + } + break; + + case OPT_TRYING: + app.server.send_trying = 1; + break; + + case OPT_RINGING: + app.server.send_ringing = 1; + break; + + default: + PJ_LOG(1,(THIS_FILE, + "Invalid argument. Use --help to see help")); + return -1; + } + } + + if (pj_optind != argc) { + + if (verify_sip_url(argv[pj_optind]) != PJ_SUCCESS) { + PJ_LOG(1,(THIS_FILE, "Invalid SIP URI %s", argv[pj_optind])); + return -1; + } + app.client.dst_uri = pj_str(argv[pj_optind]); + + pj_optind++; + + } + + if (pj_optind != argc) { + PJ_LOG(1,(THIS_FILE, "Error: unknown options %s", argv[pj_optind])); + return -1; + } + + return 0; +} + + +/* Send one stateless request */ +static pj_status_t submit_stateless_job(void) +{ + pjsip_tx_data *tdata; + pj_status_t status; + + status = pjsip_endpt_create_request(app.sip_endpt, &app.client.method, + &app.client.dst_uri, &app.local_uri, + &app.client.dst_uri, &app.local_contact, + NULL, -1, NULL, &tdata); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Error creating request", status); + report_completion(701); + return status; + } + + status = pjsip_endpt_send_request_stateless(app.sip_endpt, tdata, NULL, + NULL); + if (status != PJ_SUCCESS) { + pjsip_tx_data_dec_ref(tdata); + app_perror(THIS_FILE, "Error sending stateless request", status); + report_completion(701); + return status; + } + + return PJ_SUCCESS; +} + + +/* This callback is called when client transaction state has changed */ +static void tsx_completion_cb(void *token, pjsip_event *event) +{ + pjsip_transaction *tsx; + + PJ_UNUSED_ARG(token); + + if (event->type != PJSIP_EVENT_TSX_STATE) + return; + + tsx = event->body.tsx_state.tsx; + + if (tsx->mod_data[mod_test.id] != NULL) { + /* This transaction has been calculated before */ + return; + } + + if (tsx->state==PJSIP_TSX_STATE_TERMINATED) { + report_completion(tsx->status_code); + tsx->mod_data[mod_test.id] = (void*)1; + } + else if (tsx->method.id == PJSIP_INVITE_METHOD && + tsx->state == PJSIP_TSX_STATE_CONFIRMED) { + + report_completion(tsx->status_code); + tsx->mod_data[mod_test.id] = (void*)1; + + } else if (tsx->state == PJSIP_TSX_STATE_COMPLETED) { + + report_completion(tsx->status_code); + tsx->mod_data[mod_test.id] = (void*)1; + + TERMINATE_TSX(tsx, tsx->status_code); + } +} + + +/* Send one stateful request */ +static pj_status_t submit_job(void) +{ + pjsip_tx_data *tdata; + pj_status_t status; + + status = pjsip_endpt_create_request(app.sip_endpt, &app.client.method, + &app.client.dst_uri, &app.local_uri, + &app.client.dst_uri, &app.local_contact, + NULL, -1, NULL, &tdata); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Error creating request", status); + report_completion(701); + return status; + } + + status = pjsip_endpt_send_request(app.sip_endpt, tdata, -1, NULL, + &tsx_completion_cb); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Error sending stateful request", status); + //should have been reported by tsx_completion_cb(). + //report_completion(701); + //No longer necessary (r777) + //pjsip_tx_data_dec_ref(tdata); + } + return status; +} + + +/* Client worker thread */ +static int client_thread(void *arg) +{ + pj_time_val end_time, last_report, now; + unsigned thread_index = (unsigned)(long)arg; + unsigned cycle = 0, last_cycle = 0; + + pj_thread_sleep(100); + + pj_gettimeofday(&end_time); + end_time.sec += app.client.timeout; + + pj_gettimeofday(&last_report); + + if (app.client.first_request.sec == 0) { + pj_gettimeofday(&app.client.first_request); + } + + /* Submit all jobs */ + while (app.client.job_submitted < app.client.job_count && !app.thread_quit){ + pj_time_val timeout = { 0, 1 }; + unsigned i; + int outstanding; + pj_status_t status; + + /* Calculate current outstanding job */ + outstanding = app.client.job_submitted - app.client.job_finished; + + /* Update stats on max outstanding jobs */ + if (outstanding > (int)app.client.stat_max_window) + app.client.stat_max_window = outstanding; + + /* Wait if there are more pending jobs than allowed in the + * window. But spawn a new job anyway if no events are happening + * after we wait for some time. + */ + for (i=0; outstanding > (int)app.client.job_window && i<1000; ++i) { + pj_time_val wait = { 0, 500 }; + unsigned count = 0; + + pjsip_endpt_handle_events2(app.sip_endpt, &wait, &count); + outstanding = app.client.job_submitted - app.client.job_finished; + + if (count == 0) + break; + + ++cycle; + } + + + /* Submit one job */ + if (app.client.method.id == PJSIP_INVITE_METHOD) { + status = make_call(&app.client.dst_uri); + } else if (app.client.stateless) { + status = submit_stateless_job(); + } else { + status = submit_job(); + } + + ++app.client.job_submitted; + ++cycle; + + /* Handle event */ + pjsip_endpt_handle_events2(app.sip_endpt, &timeout, NULL); + + /* Check for time out, also print report */ + if (cycle - last_cycle >= 500) { + pj_gettimeofday(&now); + if (PJ_TIME_VAL_GTE(now, end_time)) { + break; + } + last_cycle = cycle; + + + if (thread_index == 0 && now.sec-last_report.sec >= 2) { + printf("\r%d jobs started, %d completed... ", + app.client.job_submitted, app.client.job_finished); + fflush(stdout); + last_report = now; + } + } + } + + if (app.client.requests_sent.sec == 0) { + pj_gettimeofday(&app.client.requests_sent); + } + + + if (thread_index == 0) { + printf("\r%d jobs started, %d completed%s\n", + app.client.job_submitted, app.client.job_finished, + (app.client.job_submitted!=app.client.job_finished ? + ", waiting..." : ".") ); + fflush(stdout); + } + + /* Wait until all jobs completes, or timed out */ + pj_gettimeofday(&now); + while (PJ_TIME_VAL_LT(now, end_time) && + app.client.job_finished < app.client.job_count && + !app.thread_quit) + { + pj_time_val timeout = { 0, 1 }; + unsigned i; + + for (i=0; i<1000; ++i) { + unsigned count; + count = 0; + pjsip_endpt_handle_events2(app.sip_endpt, &timeout, &count); + if (count == 0) + break; + } + + pj_gettimeofday(&now); + } + + /* Wait couple of seconds to let jobs completes (e.g. ACKs to be sent) */ + pj_gettimeofday(&now); + end_time = now; + end_time.sec += 2; + while (PJ_TIME_VAL_LT(now, end_time)) + { + pj_time_val timeout = { 0, 1 }; + unsigned i; + + for (i=0; i<1000; ++i) { + unsigned count; + count = 0; + pjsip_endpt_handle_events2(app.sip_endpt, &timeout, &count); + if (count == 0) + break; + } + + pj_gettimeofday(&now); + } + + return 0; +} + + +static 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 int server_thread(void *arg) +{ + pj_time_val timeout = { 0, 1 }; + unsigned thread_index = (unsigned)(long)arg; + pj_time_val last_report, next_report; + + pj_gettimeofday(&last_report); + next_report = last_report; + next_report.sec++; + + while (!app.thread_quit) { + pj_time_val now; + unsigned i; + + for (i=0; i<100; ++i) { + unsigned count = 0; + pjsip_endpt_handle_events2(app.sip_endpt, &timeout, &count); + if (count == 0) + break; + } + + if (thread_index == 0) { + pj_gettimeofday(&now); + + if (PJ_TIME_VAL_GTE(now, next_report)) { + pj_time_val tmp; + unsigned msec; + unsigned stateless, stateful, call; + char str_stateless[32], str_stateful[32], str_call[32]; + + tmp = now; + PJ_TIME_VAL_SUB(tmp, last_report); + msec = PJ_TIME_VAL_MSEC(tmp); + + last_report = now; + next_report = last_report; + next_report.sec++; + + stateless = app.server.cur_state.stateless_cnt - app.server.prev_state.stateless_cnt; + stateful = app.server.cur_state.stateful_cnt - app.server.prev_state.stateful_cnt; + call = app.server.cur_state.call_cnt - app.server.prev_state.call_cnt; + + good_number(str_stateless, app.server.cur_state.stateless_cnt); + good_number(str_stateful, app.server.cur_state.stateful_cnt); + good_number(str_call, app.server.cur_state.call_cnt); + + printf("Total(rate): stateless:%s (%d/s), statefull:%s (%d/s), call:%s (%d/s) \r", + str_stateless, stateless*1000/msec, + str_stateful, stateful*1000/msec, + str_call, call*1000/msec); + fflush(stdout); + + app.server.prev_state = app.server.cur_state; + } + } + } + + return 0; +} + +static void write_report(const char *msg) +{ + puts(msg); + +#if defined(PJ_WIN32) && PJ_WIN32!=0 + OutputDebugString(msg); + OutputDebugString("\n"); +#endif +} + + +int main(int argc, char *argv[]) +{ + static char report[1024]; + + printf("PJSIP Performance Measurement Tool v%s\n" + "(c)2006 pjsip.org\n\n", + PJ_VERSION); + + if (create_app() != 0) + return 1; + + if (init_options(argc, argv) != 0) + return 1; + + if (init_sip() != 0) + return 1; + + if (init_media() != 0) + return 1; + + pj_log_set_level(app.log_level); + + if (app.log_level > 4) { + pjsip_endpt_register_module(app.sip_endpt, &msg_logger); + } + + + /* Misc infos */ + if (app.client.dst_uri.slen != 0) { + if (app.client.method.id == PJSIP_INVITE_METHOD) { + if (app.client.stateless) { + PJ_LOG(3,(THIS_FILE, + "Info: --stateless option makes no sense for INVITE," + " ignored.")); + } + } + + } + + + + if (app.client.dst_uri.slen) { + /* Client mode */ + pj_status_t status; + char test_type[64]; + unsigned msec_req, msec_res; + unsigned i; + + /* Get the job name */ + if (app.client.method.id == PJSIP_INVITE_METHOD) { + pj_ansi_strcpy(test_type, "INVITE calls"); + } else if (app.client.stateless) { + pj_ansi_sprintf(test_type, "stateless %.*s requests", + (int)app.client.method.name.slen, + app.client.method.name.ptr); + } else { + pj_ansi_sprintf(test_type, "stateful %.*s requests", + (int)app.client.method.name.slen, + app.client.method.name.ptr); + } + + + printf("Sending %d %s to '%.*s' with %d maximum outstanding jobs, please wait..\n", + app.client.job_count, test_type, + (int)app.client.dst_uri.slen, app.client.dst_uri.ptr, + app.client.job_window); + + for (i=0; i<app.thread_count; ++i) { + status = pj_thread_create(app.pool, NULL, &client_thread, + (void*)(long)i, 0, 0, &app.thread[i]); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to create thread", status); + return 1; + } + } + + for (i=0; i<app.thread_count; ++i) { + pj_thread_join(app.thread[i]); + app.thread[i] = NULL; + } + + if (app.client.last_completion.sec) { + pj_time_val duration; + duration = app.client.last_completion; + PJ_TIME_VAL_SUB(duration, app.client.first_request); + msec_res = PJ_TIME_VAL_MSEC(duration); + } else { + msec_res = app.client.timeout * 1000; + } + + if (msec_res == 0) msec_res = 1; + + if (app.client.requests_sent.sec) { + pj_time_val duration; + duration = app.client.requests_sent; + PJ_TIME_VAL_SUB(duration, app.client.first_request); + msec_req = PJ_TIME_VAL_MSEC(duration); + } else { + msec_req = app.client.timeout * 1000; + } + + if (msec_req == 0) msec_req = 1; + + if (app.client.job_submitted < app.client.job_count) + puts("\ntimed-out!\n"); + else + puts("\ndone.\n"); + + pj_ansi_snprintf( + report, sizeof(report), + "Total %d %s sent in %d ms at rate of %d/sec\n" + "Total %d responses receieved in %d ms at rate of %d/sec:", + app.client.job_submitted, test_type, msec_req, + app.client.job_submitted * 1000 / msec_req, + app.client.total_responses, msec_res, + app.client.total_responses*1000/msec_res); + write_report(report); + + /* Print detailed response code received */ + pj_ansi_sprintf(report, "\nDetailed responses received:"); + write_report(report); + + for (i=0; i<PJ_ARRAY_SIZE(app.client.response_codes); ++i) { + const pj_str_t *reason; + + if (app.client.response_codes[i] == 0) + continue; + + reason = pjsip_get_status_text(i); + pj_ansi_snprintf( report, sizeof(report), + " - %d responses: %7d (%.*s)", + i, app.client.response_codes[i], + (int)reason->slen, reason->ptr); + write_report(report); + } + + /* Total responses and rate */ + pj_ansi_snprintf( report, sizeof(report), + " ------\n" + " TOTAL responses: %7d (rate=%d/sec)\n", + app.client.total_responses, + app.client.total_responses*1000/msec_res); + + write_report(report); + + pj_ansi_sprintf(report, "Maximum outstanding job: %d", + app.client.stat_max_window); + write_report(report); + + + } else { + /* Server mode */ + char s[10], *unused; + pj_status_t status; + unsigned i; + + puts("pjsip-perf started in server-mode"); + + printf("Receiving requests on the following URIs:\n" + " sip:0@%.*s:%d%s for stateless handling\n" + " sip:1@%.*s:%d%s for stateful handling\n" + " sip:2@%.*s:%d%s for call handling\n", + (int)app.local_addr.slen, + app.local_addr.ptr, + app.local_port, + (app.use_tcp ? ";transport=tcp" : ""), + (int)app.local_addr.slen, + app.local_addr.ptr, + app.local_port, + (app.use_tcp ? ";transport=tcp" : ""), + (int)app.local_addr.slen, + app.local_addr.ptr, + app.local_port, + (app.use_tcp ? ";transport=tcp" : "")); + printf("INVITE with non-matching user part will be handled call-statefully\n"); + + for (i=0; i<app.thread_count; ++i) { + status = pj_thread_create(app.pool, NULL, &server_thread, + (void*)(long)i, 0, 0, &app.thread[i]); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to create thread", status); + return 1; + } + } + + puts("\nPress <ENTER> to quit\n"); + fflush(stdout); + unused = fgets(s, sizeof(s), stdin); + PJ_UNUSED_ARG(unused); + + app.thread_quit = PJ_TRUE; + for (i=0; i<app.thread_count; ++i) { + pj_thread_join(app.thread[i]); + app.thread[i] = NULL; + } + + puts(""); + } + + + destroy_app(); + + return 0; +} + diff --git a/pjsip-apps/src/samples/playfile.c b/pjsip-apps/src/samples/playfile.c new file mode 100644 index 0000000..778b3e5 --- /dev/null +++ b/pjsip-apps/src/samples/playfile.c @@ -0,0 +1,217 @@ +/* $Id: playfile.c 3664 2011-07-19 03:42:28Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 <pjmedia.h> +#include <pjlib-util.h> +#include <pjlib.h> +#include <stdio.h> +#include <stdlib.h> + +#include "util.h" + + +/** + * \page page_pjmedia_samples_playfile_c Samples: Playing WAV File to Sound Device + * + * This is a very simple example to use the @ref PJMEDIA_FILE_PLAY and + * @ref PJMED_SND_PORT. In this example, we open both the file and sound + * device, and connect the two of them, and voila! Sound will be playing + * the contents of the file. + * + * @see page_pjmedia_samples_recfile_c + * + * This file is pjsip-apps/src/samples/playfile.c + * + * \includelineno playfile.c + */ + + +/* + * playfile.c + * + * PURPOSE: + * Play a WAV file to sound player device. + * + * USAGE: + * playfile FILE.WAV + * + * The WAV file could have mono or stereo channels with arbitrary + * sampling rate, but MUST contain uncompressed (i.e. 16bit) PCM. + * + */ + + +/* For logging purpose. */ +#define THIS_FILE "playfile.c" + + +static const char *desc = +" FILE \n" +" \n" +" playfile.c \n" +" \n" +" PURPOSE \n" +" \n" +" Demonstrate how to play a WAV file. \n" +" \n" +" USAGE \n" +" \n" +" playfile FILE.WAV \n" +" \n" +" The WAV file could have mono or stereo channels with arbitrary \n" +" sampling rate, but MUST contain uncompressed (i.e. 16bit) PCM. \n"; + + +/* + * main() + */ +int main(int argc, char *argv[]) +{ + pj_caching_pool cp; + pjmedia_endpt *med_endpt; + pj_pool_t *pool; + pjmedia_port *file_port; + pjmedia_snd_port *snd_port; + char tmp[10]; + pj_status_t status; + + + if (argc != 2) { + puts("Error: filename required"); + puts(desc); + return 1; + } + + + /* Must init PJLIB first: */ + status = pj_init(); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* Must create a pool factory before we can allocate any memory. */ + pj_caching_pool_init(&cp, &pj_pool_factory_default_policy, 0); + + /* + * Initialize media endpoint. + * This will implicitly initialize PJMEDIA too. + */ + status = pjmedia_endpt_create(&cp.factory, NULL, 1, &med_endpt); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* Create memory pool for our file player */ + pool = pj_pool_create( &cp.factory, /* pool factory */ + "wav", /* pool name. */ + 4000, /* init size */ + 4000, /* increment size */ + NULL /* callback on error */ + ); + + /* Create file media port from the WAV file */ + status = pjmedia_wav_player_port_create( pool, /* memory pool */ + argv[1], /* file to play */ + 20, /* ptime. */ + 0, /* flags */ + 0, /* default buffer */ + &file_port/* returned port */ + ); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to use WAV file", status); + return 1; + } + + /* Create sound player port. */ + status = pjmedia_snd_port_create_player( + pool, /* pool */ + -1, /* use default dev. */ + PJMEDIA_PIA_SRATE(&file_port->info),/* clock rate. */ + PJMEDIA_PIA_CCNT(&file_port->info),/* # of channels. */ + PJMEDIA_PIA_SPF(&file_port->info), /* samples per frame. */ + PJMEDIA_PIA_BITS(&file_port->info),/* bits per sample. */ + 0, /* options */ + &snd_port /* returned port */ + ); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to open sound device", status); + return 1; + } + + /* Connect file port to the sound player. + * Stream playing will commence immediately. + */ + status = pjmedia_snd_port_connect( snd_port, file_port); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + + /* + * File should be playing and looping now, using sound device's thread. + */ + + + /* Sleep to allow log messages to flush */ + pj_thread_sleep(100); + + + printf("Playing %s..\n", argv[1]); + puts(""); + puts("Press <ENTER> to stop playing and quit"); + + if (fgets(tmp, sizeof(tmp), stdin) == NULL) { + puts("EOF while reading stdin, will quit now.."); + } + + + /* Start deinitialization: */ + + /* Disconnect sound port from file port */ + status = pjmedia_snd_port_disconnect(snd_port); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* Without this sleep, Windows/DirectSound will repeteadly + * play the last frame during destroy. + */ + pj_thread_sleep(100); + + /* Destroy sound device */ + status = pjmedia_snd_port_destroy( snd_port ); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + /* Destroy file port */ + status = pjmedia_port_destroy( file_port ); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + /* Release application pool */ + pj_pool_release( pool ); + + /* Destroy media endpoint. */ + pjmedia_endpt_destroy( med_endpt ); + + /* Destroy pool factory */ + pj_caching_pool_destroy( &cp ); + + /* Shutdown PJLIB */ + pj_shutdown(); + + + /* Done. */ + return 0; +} + diff --git a/pjsip-apps/src/samples/playsine.c b/pjsip-apps/src/samples/playsine.c new file mode 100644 index 0000000..bab0c9b --- /dev/null +++ b/pjsip-apps/src/samples/playsine.c @@ -0,0 +1,317 @@ +/* $Id: playsine.c 3664 2011-07-19 03:42:28Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 + */ + + +/** + * \page page_pjmedia_samples_playsine_c Samples: Using Custom Ports (Sine Wave Generator) + * + * This example demonstrate how to create a custom media port (in this case, a + * sine wave generator) and connect it to the sound device. + * + * This file is pjsip-apps/src/samples/playsine.c + * + * \includelineno playsine.c + */ + +/* + * playsine.c + * + * PURPOSE: + * Demonstrate how to create and use custom media port which + * simply feed a sine wav to the sound player. + * + * USAGE: + * playsine [nchannel] + * + * where: + * nchannel is 1 for mono (this is the default) or 2 for stereo. + */ + +#include <pjmedia.h> +#include <pjlib.h> + +#include <stdlib.h> /* atoi() */ +#include <stdio.h> +#include <math.h> /* sin() */ + +/* For logging purpose. */ +#define THIS_FILE "playsine.c" + + +/* Util to display the error message for the specified error code */ +static int app_perror( const char *sender, const char *title, + pj_status_t status) +{ + char errmsg[PJ_ERR_MSG_SIZE]; + + PJ_UNUSED_ARG(sender); + + pj_strerror(status, errmsg, sizeof(errmsg)); + + printf("%s: %s [code=%d]\n", title, errmsg, status); + return 1; +} + + +/* Struct attached to sine generator */ +typedef struct +{ + pj_int16_t *samples; /* Sine samples. */ +} port_data; + + +/* This callback is called to feed more samples */ +static pj_status_t sine_get_frame( pjmedia_port *port, + pjmedia_frame *frame) +{ + port_data *sine = port->port_data.pdata; + pj_int16_t *samples = frame->buf; + unsigned i, count, left, right; + + /* Get number of samples */ + count = frame->size / 2 / PJMEDIA_PIA_CCNT(&port->info); + + left = 0; + right = 0; + + for (i=0; i<count; ++i) { + *samples++ = sine->samples[left]; + ++left; + + if (PJMEDIA_PIA_CCNT(&port->info) == 2) { + *samples++ = sine->samples[right]; + right += 2; /* higher pitch so we can distinguish left and right. */ + if (right >= count) + right = 0; + } + } + + /* Must set frame->type correctly, otherwise the sound device + * will refuse to play. + */ + frame->type = PJMEDIA_FRAME_TYPE_AUDIO; + + return PJ_SUCCESS; +} + +#ifndef M_PI +#define M_PI (3.14159265) +#endif + +/* + * Create a media port to generate sine wave samples. + */ +static pj_status_t create_sine_port(pj_pool_t *pool, + unsigned sampling_rate, + unsigned channel_count, + pjmedia_port **p_port) +{ + pjmedia_port *port; + unsigned i; + unsigned count; + pj_str_t name; + port_data *sine; + + PJ_ASSERT_RETURN(pool && channel_count > 0 && channel_count <= 2, + PJ_EINVAL); + + port = pj_pool_zalloc(pool, sizeof(pjmedia_port)); + PJ_ASSERT_RETURN(port != NULL, PJ_ENOMEM); + + /* Fill in port info. */ + name = pj_str("sine generator"); + pjmedia_port_info_init(&port->info, &name, + PJMEDIA_SIG_CLASS_PORT_AUD('s', 'i'), + sampling_rate, + channel_count, + 16, sampling_rate * 20 / 1000 * channel_count); + + /* Set the function to feed frame */ + port->get_frame = &sine_get_frame; + + /* Create sine port data */ + port->port_data.pdata = sine = pj_pool_zalloc(pool, sizeof(port_data)); + + /* Create samples */ + count = PJMEDIA_PIA_SPF(&port->info) / channel_count; + sine->samples = pj_pool_alloc(pool, count * sizeof(pj_int16_t)); + PJ_ASSERT_RETURN(sine->samples != NULL, PJ_ENOMEM); + + /* initialise sinusoidal wavetable */ + for( i=0; i<count; i++ ) + { + sine->samples[i] = (pj_int16_t) (10000.0 * + sin(((double)i/(double)count) * M_PI * 8.) ); + } + + *p_port = port; + + return PJ_SUCCESS; +} + + +/* Show usage */ +static void usage(void) +{ + puts(""); + puts("Usage: playsine [nchannel]"); + puts(""); + puts("where"); + puts(" nchannel is number of audio channels (1 for mono, or 2 for stereo)."); + puts(" Default is 1 (mono)."); + puts(""); +} + + +/* + * main() + */ +int main(int argc, char *argv[]) +{ + pj_caching_pool cp; + pjmedia_endpt *med_endpt; + pj_pool_t *pool; + pjmedia_port *sine_port; + pjmedia_snd_port *snd_port; + char tmp[10]; + int channel_count = 1; + pj_status_t status; + + if (argc == 2) { + channel_count = atoi(argv[1]); + if (channel_count < 1 || channel_count > 2) { + puts("Error: invalid arguments"); + usage(); + return 1; + } + } + + /* Must init PJLIB first: */ + status = pj_init(); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* Must create a pool factory before we can allocate any memory. */ + pj_caching_pool_init(&cp, &pj_pool_factory_default_policy, 0); + + /* + * Initialize media endpoint. + * This will implicitly initialize PJMEDIA too. + */ + status = pjmedia_endpt_create(&cp.factory, NULL, 1, &med_endpt); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* Create memory pool for our sine generator */ + pool = pj_pool_create( &cp.factory, /* pool factory */ + "wav", /* pool name. */ + 4000, /* init size */ + 4000, /* increment size */ + NULL /* callback on error */ + ); + + /* Create a media port to generate sine wave samples. */ + status = create_sine_port( pool, /* memory pool */ + 11025, /* sampling rate */ + channel_count,/* # of channels */ + &sine_port /* returned port */ + ); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to create sine port", status); + return 1; + } + + /* Create sound player port. */ + status = pjmedia_snd_port_create_player( + pool, /* pool */ + -1, /* use default dev. */ + PJMEDIA_PIA_SRATE(&sine_port->info),/* clock rate. */ + PJMEDIA_PIA_CCNT(&sine_port->info),/* # of channels. */ + PJMEDIA_PIA_SPF(&sine_port->info), /* samples per frame. */ + PJMEDIA_PIA_BITS(&sine_port->info),/* bits per sample. */ + 0, /* options */ + &snd_port /* returned port */ + ); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to open sound device", status); + return 1; + } + + /* Connect sine generator port to the sound player + * Stream playing will commence immediately. + */ + status = pjmedia_snd_port_connect( snd_port, sine_port); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + + /* + * Audio should be playing in a loop now, using sound device's thread. + */ + + + /* Sleep to allow log messages to flush */ + pj_thread_sleep(100); + + + puts("Playing sine wave.."); + puts(""); + puts("Press <ENTER> to stop playing and quit"); + + if (fgets(tmp, sizeof(tmp), stdin) == NULL) { + puts("EOF while reading stdin, will quit now.."); + } + + + /* Start deinitialization: */ + + /* Disconnect sound port from file port */ + status = pjmedia_snd_port_disconnect(snd_port); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* Without this sleep, Windows/DirectSound will repeteadly + * play the last frame during destroy. + */ + pj_thread_sleep(100); + + /* Destroy sound device */ + status = pjmedia_snd_port_destroy( snd_port ); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + /* Destroy sine generator */ + status = pjmedia_port_destroy( sine_port ); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + /* Release application pool */ + pj_pool_release( pool ); + + /* Destroy media endpoint. */ + pjmedia_endpt_destroy( med_endpt ); + + /* Destroy pool factory */ + pj_caching_pool_destroy( &cp ); + + /* Shutdown PJLIB */ + pj_shutdown(); + + + /* Done. */ + return 0; +} diff --git a/pjsip-apps/src/samples/proxy.h b/pjsip-apps/src/samples/proxy.h new file mode 100644 index 0000000..2e1b383 --- /dev/null +++ b/pjsip-apps/src/samples/proxy.h @@ -0,0 +1,585 @@ +/* $Id: proxy.h 3553 2011-05-05 06:14:19Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 <pjsip.h> +#include <pjlib-util.h> +#include <pjlib.h> + + +/* Options */ +static struct global_struct +{ + pj_caching_pool cp; + pjsip_endpoint *endpt; + int port; + pj_pool_t *pool; + + pj_thread_t *thread; + pj_bool_t quit_flag; + + pj_bool_t record_route; + + unsigned name_cnt; + pjsip_host_port name[16]; +} global; + + + +static void app_perror(const char *msg, pj_status_t status) +{ + char errmsg[PJ_ERR_MSG_SIZE]; + + pj_strerror(status, errmsg, sizeof(errmsg)); + PJ_LOG(1,(THIS_FILE, "%s: %s", msg, errmsg)); +} + + +static void usage(void) +{ + puts("Options:\n" + "\n" + " -p, --port N Set local listener port to N\n" + " -R, --rr Perform record routing\n" + " -L, --log-level N Set log level to N (default: 4)\n" + " -h, --help Show this help screen\n" + ); +} + + +static pj_status_t init_options(int argc, char *argv[]) +{ + struct pj_getopt_option long_opt[] = { + { "port", 1, 0, 'p'}, + { "rr", 0, 0, 'R'}, + { "log-level", 1, 0, 'L'}, + { "help", 0, 0, 'h'}, + { NULL, 0, 0, 0} + }; + int c; + int opt_ind; + + pj_optind = 0; + while((c=pj_getopt_long(argc, argv, "p:L:Rh", long_opt, &opt_ind))!=-1) { + switch (c) { + case 'p': + global.port = atoi(pj_optarg); + printf("Port is set to %d\n", global.port); + break; + + case 'R': + global.record_route = PJ_TRUE; + printf("Using record route mode\n"); + break; + + case 'L': + pj_log_set_level(atoi(pj_optarg)); + break; + + case 'h': + usage(); + return -1; + + default: + puts("Unknown option. Run with --help for help."); + return -1; + } + } + + return PJ_SUCCESS; +} + + +/***************************************************************************** + * This is a very simple PJSIP module, whose sole purpose is to display + * incoming and outgoing messages to log. This module will have priority + * higher than transport layer, which means: + * + * - incoming messages will come to this module first before reaching + * transaction layer. + * + * - outgoing messages will come to this module last, after the message + * has been 'printed' to contiguous buffer by transport layer and + * appropriate transport instance has been decided for this message. + * + */ + +/* Notification on incoming messages */ +static pj_bool_t logging_on_rx_msg(pjsip_rx_data *rdata) +{ + PJ_LOG(5,(THIS_FILE, "RX %d bytes %s from %s %s:%d:\n" + "%.*s\n" + "--end msg--", + rdata->msg_info.len, + pjsip_rx_data_get_info(rdata), + rdata->tp_info.transport->type_name, + rdata->pkt_info.src_name, + rdata->pkt_info.src_port, + (int)rdata->msg_info.len, + rdata->msg_info.msg_buf)); + + /* Always return false, otherwise messages will not get processed! */ + return PJ_FALSE; +} + +/* Notification on outgoing messages */ +static pj_status_t logging_on_tx_msg(pjsip_tx_data *tdata) +{ + + /* Important note: + * tp_info field is only valid after outgoing messages has passed + * transport layer. So don't try to access tp_info when the module + * has lower priority than transport layer. + */ + + PJ_LOG(5,(THIS_FILE, "TX %d bytes %s to %s %s:%d:\n" + "%.*s\n" + "--end msg--", + (tdata->buf.cur - tdata->buf.start), + pjsip_tx_data_get_info(tdata), + tdata->tp_info.transport->type_name, + tdata->tp_info.dst_name, + tdata->tp_info.dst_port, + (int)(tdata->buf.cur - tdata->buf.start), + tdata->buf.start)); + + /* Always return success, otherwise message will not get sent! */ + return PJ_SUCCESS; +} + +/* The module instance. */ +static pjsip_module mod_msg_logger = +{ + NULL, NULL, /* prev, next. */ + { "mod-msg-logger", 14 }, /* Name. */ + -1, /* Id */ + PJSIP_MOD_PRIORITY_TRANSPORT_LAYER-1,/* Priority */ + NULL, /* load() */ + NULL, /* start() */ + NULL, /* stop() */ + NULL, /* unload() */ + &logging_on_rx_msg, /* on_rx_request() */ + &logging_on_rx_msg, /* on_rx_response() */ + &logging_on_tx_msg, /* on_tx_request. */ + &logging_on_tx_msg, /* on_tx_response() */ + NULL, /* on_tsx_state() */ + +}; + + +static pj_status_t init_stack(void) +{ + pj_status_t status; + + /* Must init PJLIB first: */ + status = pj_init(); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); + + + /* Then init PJLIB-UTIL: */ + status = pjlib_util_init(); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); + + + /* Must create a pool factory before we can allocate any memory. */ + pj_caching_pool_init(&global.cp, &pj_pool_factory_default_policy, 0); + + /* Create the endpoint: */ + status = pjsip_endpt_create(&global.cp.factory, NULL, &global.endpt); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); + + /* Init transaction layer for stateful proxy only */ +#if STATEFUL + status = pjsip_tsx_layer_init_module(global.endpt); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); +#endif + + /* Create listening transport */ + { + pj_sockaddr_in addr; + + addr.sin_family = pj_AF_INET(); + addr.sin_addr.s_addr = 0; + addr.sin_port = pj_htons((pj_uint16_t)global.port); + + status = pjsip_udp_transport_start( global.endpt, &addr, + NULL, 1, NULL); + if (status != PJ_SUCCESS) + return status; + } + + /* Create pool for the application */ + global.pool = pj_pool_create(&global.cp.factory, "proxyapp", + 4000, 4000, NULL); + + /* Register the logger module */ + pjsip_endpt_register_module(global.endpt, &mod_msg_logger); + + return PJ_SUCCESS; +} + + +static pj_status_t init_proxy(void) +{ + pj_sockaddr pri_addr; + pj_sockaddr addr_list[16]; + unsigned addr_cnt = PJ_ARRAY_SIZE(addr_list); + unsigned i; + + /* List all names matching local endpoint. + * Note that PJLIB version 0.6 and newer has a function to + * enumerate local IP interface (pj_enum_ip_interface()), so + * by using it would be possible to list all IP interfaces in + * this host. + */ + + /* The first address is important since this would be the one + * to be added in Record-Route. + */ + if (pj_gethostip(pj_AF_INET(), &pri_addr)==PJ_SUCCESS) { + pj_strdup2(global.pool, &global.name[global.name_cnt].host, + pj_inet_ntoa(pri_addr.ipv4.sin_addr)); + global.name[global.name_cnt].port = global.port; + global.name_cnt++; + } + + /* Get the rest of IP interfaces */ + if (pj_enum_ip_interface(pj_AF_INET(), &addr_cnt, addr_list) == PJ_SUCCESS) { + for (i=0; i<addr_cnt; ++i) { + + if (addr_list[i].ipv4.sin_addr.s_addr == pri_addr.ipv4.sin_addr.s_addr) + continue; + + pj_strdup2(global.pool, &global.name[global.name_cnt].host, + pj_inet_ntoa(addr_list[i].ipv4.sin_addr)); + global.name[global.name_cnt].port = global.port; + global.name_cnt++; + } + } + + /* Add loopback address. */ +#if PJ_IP_HELPER_IGNORE_LOOPBACK_IF + global.name[global.name_cnt].host = pj_str("127.0.0.1"); + global.name[global.name_cnt].port = global.port; + global.name_cnt++; +#endif + + global.name[global.name_cnt].host = *pj_gethostname(); + global.name[global.name_cnt].port = global.port; + global.name_cnt++; + + global.name[global.name_cnt].host = pj_str("localhost"); + global.name[global.name_cnt].port = global.port; + global.name_cnt++; + + PJ_LOG(3,(THIS_FILE, "Proxy started, listening on port %d", global.port)); + PJ_LOG(3,(THIS_FILE, "Local host aliases:")); + for (i=0; i<global.name_cnt; ++i) { + PJ_LOG(3,(THIS_FILE, " %.*s:%d", + (int)global.name[i].host.slen, + global.name[i].host.ptr, + global.name[i].port)); + } + + if (global.record_route) { + PJ_LOG(3,(THIS_FILE, "Using Record-Route mode")); + } + + return PJ_SUCCESS; +} + + +#if PJ_HAS_THREADS +static int worker_thread(void *p) +{ + pj_time_val delay = {0, 10}; + + PJ_UNUSED_ARG(p); + + while (!global.quit_flag) { + pjsip_endpt_handle_events(global.endpt, &delay); + } + + return 0; +} +#endif + + +/* Utility to determine if URI is local to this host. */ +static pj_bool_t is_uri_local(const pjsip_sip_uri *uri) +{ + unsigned i; + for (i=0; i<global.name_cnt; ++i) { + if ((uri->port == global.name[i].port || + (uri->port==0 && global.name[i].port==5060)) && + pj_stricmp(&uri->host, &global.name[i].host)==0) + { + /* Match */ + return PJ_TRUE; + } + } + + /* Doesn't match */ + return PJ_FALSE; +} + + +/* Proxy utility to verify incoming requests. + * Return non-zero if verification failed. + */ +static pj_status_t proxy_verify_request(pjsip_rx_data *rdata) +{ + const pj_str_t STR_PROXY_REQUIRE = {"Proxy-Require", 13}; + + /* RFC 3261 Section 16.3 Request Validation */ + + /* Before an element can proxy a request, it MUST verify the message's + * validity. A valid message must pass the following checks: + * + * 1. Reasonable Syntax + * 2. URI scheme + * 3. Max-Forwards + * 4. (Optional) Loop Detection + * 5. Proxy-Require + * 6. Proxy-Authorization + */ + + /* 1. Reasonable Syntax. + * This would have been checked by transport layer. + */ + + /* 2. URI scheme. + * We only want to support "sip:" URI scheme for this simple proxy. + */ + if (!PJSIP_URI_SCHEME_IS_SIP(rdata->msg_info.msg->line.req.uri)) { + pjsip_endpt_respond_stateless(global.endpt, rdata, + PJSIP_SC_UNSUPPORTED_URI_SCHEME, NULL, + NULL, NULL); + return PJSIP_ERRNO_FROM_SIP_STATUS(PJSIP_SC_UNSUPPORTED_URI_SCHEME); + } + + /* 3. Max-Forwards. + * Send error if Max-Forwards is 1 or lower. + */ + if (rdata->msg_info.max_fwd && rdata->msg_info.max_fwd->ivalue <= 1) { + pjsip_endpt_respond_stateless(global.endpt, rdata, + PJSIP_SC_TOO_MANY_HOPS, NULL, + NULL, NULL); + return PJSIP_ERRNO_FROM_SIP_STATUS(PJSIP_SC_TOO_MANY_HOPS); + } + + /* 4. (Optional) Loop Detection. + * Nah, we don't do that with this simple proxy. + */ + + /* 5. Proxy-Require */ + if (pjsip_msg_find_hdr_by_name(rdata->msg_info.msg, &STR_PROXY_REQUIRE, + NULL) != NULL) + { + pjsip_endpt_respond_stateless(global.endpt, rdata, + PJSIP_SC_BAD_EXTENSION, NULL, + NULL, NULL); + return PJSIP_ERRNO_FROM_SIP_STATUS(PJSIP_SC_BAD_EXTENSION); + } + + /* 6. Proxy-Authorization. + * Nah, we don't require any authorization with this sample. + */ + + return PJ_SUCCESS; +} + + +/* Process route information in the reqeust */ +static pj_status_t proxy_process_routing(pjsip_tx_data *tdata) +{ + pjsip_sip_uri *target; + pjsip_route_hdr *hroute; + + /* RFC 3261 Section 16.4 Route Information Preprocessing */ + + target = (pjsip_sip_uri*) tdata->msg->line.req.uri; + + /* The proxy MUST inspect the Request-URI of the request. If the + * Request-URI of the request contains a value this proxy previously + * placed into a Record-Route header field (see Section 16.6 item 4), + * the proxy MUST replace the Request-URI in the request with the last + * value from the Route header field, and remove that value from the + * Route header field. The proxy MUST then proceed as if it received + * this modified request. + */ + if (is_uri_local(target)) { + pjsip_route_hdr *r; + pjsip_sip_uri *uri; + + /* Find the first Route header */ + r = hroute = (pjsip_route_hdr*) + pjsip_msg_find_hdr(tdata->msg, PJSIP_H_ROUTE, NULL); + if (r == NULL) { + /* No Route header. This request is destined for this proxy. */ + return PJ_SUCCESS; + } + + /* Find the last Route header */ + while ( (r=(pjsip_route_hdr*)pjsip_msg_find_hdr(tdata->msg, + PJSIP_H_ROUTE, + r->next)) != NULL ) + { + hroute = r; + } + + /* If the last Route header doesn't have ";lr" parameter, then + * this is a strict-routed request indeed, and we follow the steps + * in processing strict-route requests above. + * + * But if it does contain ";lr" parameter, skip the strict-route + * processing. + */ + uri = (pjsip_sip_uri*) + pjsip_uri_get_uri(&hroute->name_addr); + if (uri->lr_param == 0) { + /* Yes this is strict route, so: + * - replace req URI with the URI in Route header, + * - remove the Route header, + * - proceed as if it received this modified request. + */ + tdata->msg->line.req.uri = hroute->name_addr.uri; + target = (pjsip_sip_uri*) tdata->msg->line.req.uri; + pj_list_erase(hroute); + } + } + + /* If the Request-URI contains a maddr parameter, the proxy MUST check + * to see if its value is in the set of addresses or domains the proxy + * is configured to be responsible for. If the Request-URI has a maddr + * parameter with a value the proxy is responsible for, and the request + * was received using the port and transport indicated (explicitly or by + * default) in the Request-URI, the proxy MUST strip the maddr and any + * non-default port or transport parameter and continue processing as if + * those values had not been present in the request. + */ + if (target->maddr_param.slen != 0) { + pjsip_sip_uri maddr_uri; + + maddr_uri.host = target->maddr_param; + maddr_uri.port = global.port; + + if (is_uri_local(&maddr_uri)) { + target->maddr_param.slen = 0; + target->port = 0; + target->transport_param.slen = 0; + } + } + + /* If the first value in the Route header field indicates this proxy, + * the proxy MUST remove that value from the request. + */ + hroute = (pjsip_route_hdr*) + pjsip_msg_find_hdr(tdata->msg, PJSIP_H_ROUTE, NULL); + if (hroute && is_uri_local((pjsip_sip_uri*)hroute->name_addr.uri)) { + pj_list_erase(hroute); + } + + return PJ_SUCCESS; +} + + +/* Postprocess the request before forwarding it */ +static void proxy_postprocess(pjsip_tx_data *tdata) +{ + /* Optionally record-route */ + if (global.record_route) { + char uribuf[128]; + pj_str_t uri; + const pj_str_t H_RR = { "Record-Route", 12 }; + pjsip_generic_string_hdr *rr; + + pj_ansi_snprintf(uribuf, sizeof(uribuf), "<sip:%.*s:%d;lr>", + (int)global.name[0].host.slen, + global.name[0].host.ptr, + global.name[0].port); + uri = pj_str(uribuf); + rr = pjsip_generic_string_hdr_create(tdata->pool, + &H_RR, &uri); + pjsip_msg_insert_first_hdr(tdata->msg, (pjsip_hdr*)rr); + } +} + + +/* Calculate new target for the request */ +static pj_status_t proxy_calculate_target(pjsip_rx_data *rdata, + pjsip_tx_data *tdata) +{ + pjsip_sip_uri *target; + + /* RFC 3261 Section 16.5 Determining Request Targets */ + + target = (pjsip_sip_uri*) tdata->msg->line.req.uri; + + /* If the Request-URI of the request contains an maddr parameter, the + * Request-URI MUST be placed into the target set as the only target + * URI, and the proxy MUST proceed to Section 16.6. + */ + if (target->maddr_param.slen) { + proxy_postprocess(tdata); + return PJ_SUCCESS; + } + + + /* If the domain of the Request-URI indicates a domain this element is + * not responsible for, the Request-URI MUST be placed into the target + * set as the only target, and the element MUST proceed to the task of + * Request Forwarding (Section 16.6). + */ + if (!is_uri_local(target)) { + proxy_postprocess(tdata); + return PJ_SUCCESS; + } + + /* If the target set for the request has not been predetermined as + * described above, this implies that the element is responsible for the + * domain in the Request-URI, and the element MAY use whatever mechanism + * it desires to determine where to send the request. + */ + + /* We're not interested to receive request destined to us, so + * respond with 404/Not Found (only if request is not ACK!). + */ + if (rdata->msg_info.msg->line.req.method.id != PJSIP_ACK_METHOD) { + pjsip_endpt_respond_stateless(global.endpt, rdata, + PJSIP_SC_NOT_FOUND, NULL, + NULL, NULL); + } + + /* Delete the request since we're not forwarding it */ + pjsip_tx_data_dec_ref(tdata); + + return PJSIP_ERRNO_FROM_SIP_STATUS(PJSIP_SC_NOT_FOUND); +} + + +/* Destroy stack */ +static void destroy_stack(void) +{ + pjsip_endpt_destroy(global.endpt); + pj_pool_release(global.pool); + pj_caching_pool_destroy(&global.cp); + + pj_shutdown(); +} + diff --git a/pjsip-apps/src/samples/recfile.c b/pjsip-apps/src/samples/recfile.c new file mode 100644 index 0000000..fedd5c6 --- /dev/null +++ b/pjsip-apps/src/samples/recfile.c @@ -0,0 +1,202 @@ +/* $Id: recfile.c 3664 2011-07-19 03:42:28Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 + */ + +/** + * \page page_pjmedia_samples_recfile_c Samples: Capturing Audio to WAV File + * + * In this example, we capture audio from the sound device and save it to + * WAVE file. + * + * @see page_pjmedia_samples_playfile_c + * + * This file is pjsip-apps/src/samples/recfile.c + * + * \includelineno recfile.c + */ + +#include <pjmedia.h> +#include <pjlib.h> + +#include <stdio.h> + +/* For logging purpose. */ +#define THIS_FILE "recfile.c" + + +/* Configs */ +#define CLOCK_RATE 44100 +#define NCHANNELS 2 +#define SAMPLES_PER_FRAME (NCHANNELS * (CLOCK_RATE * 10 / 1000)) +#define BITS_PER_SAMPLE 16 + + +static const char *desc = + " FILE \n" + " recfile.c \n" + " \n" + " PURPOSE: \n" + " Record microphone to WAVE file. \n" + " \n" + " USAGE: \n" + " recfile FILE.WAV \n" + ""; + + +/* Util to display the error message for the specified error code */ +static int app_perror( const char *sender, const char *title, + pj_status_t status) +{ + char errmsg[PJ_ERR_MSG_SIZE]; + + PJ_UNUSED_ARG(sender); + + pj_strerror(status, errmsg, sizeof(errmsg)); + + printf("%s: %s [code=%d]\n", title, errmsg, status); + return 1; +} + + +/* + * main() + */ +int main(int argc, char *argv[]) +{ + pj_caching_pool cp; + pjmedia_endpt *med_endpt; + pj_pool_t *pool; + pjmedia_port *file_port; + pjmedia_snd_port *snd_port; + char tmp[10]; + pj_status_t status; + + + /* Verify cmd line arguments. */ + if (argc != 2) { + puts(""); + puts(desc); + return 0; + } + + /* Must init PJLIB first: */ + status = pj_init(); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* Must create a pool factory before we can allocate any memory. */ + pj_caching_pool_init(&cp, &pj_pool_factory_default_policy, 0); + + /* + * Initialize media endpoint. + * This will implicitly initialize PJMEDIA too. + */ + status = pjmedia_endpt_create(&cp.factory, NULL, 1, &med_endpt); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* Create memory pool for our file player */ + pool = pj_pool_create( &cp.factory, /* pool factory */ + "app", /* pool name. */ + 4000, /* init size */ + 4000, /* increment size */ + NULL /* callback on error */ + ); + + /* Create WAVE file writer port. */ + status = pjmedia_wav_writer_port_create( pool, argv[1], + CLOCK_RATE, + NCHANNELS, + SAMPLES_PER_FRAME, + BITS_PER_SAMPLE, + 0, 0, + &file_port); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to open WAV file for writing", status); + return 1; + } + + /* Create sound player port. */ + status = pjmedia_snd_port_create_rec( + pool, /* pool */ + -1, /* use default dev. */ + PJMEDIA_PIA_SRATE(&file_port->info),/* clock rate. */ + PJMEDIA_PIA_CCNT(&file_port->info),/* # of channels. */ + PJMEDIA_PIA_SPF(&file_port->info), /* samples per frame. */ + PJMEDIA_PIA_BITS(&file_port->info),/* bits per sample. */ + 0, /* options */ + &snd_port /* returned port */ + ); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to open sound device", status); + return 1; + } + + /* Connect file port to the sound player. + * Stream playing will commence immediately. + */ + status = pjmedia_snd_port_connect( snd_port, file_port); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + + /* + * Recording should be started now. + */ + + + /* Sleep to allow log messages to flush */ + pj_thread_sleep(10); + + + printf("Recodring %s..\n", argv[1]); + puts(""); + puts("Press <ENTER> to stop recording and quit"); + + if (fgets(tmp, sizeof(tmp), stdin) == NULL) { + puts("EOF while reading stdin, will quit now.."); + } + + + /* Start deinitialization: */ + + /* Destroy sound device */ + status = pjmedia_snd_port_destroy( snd_port ); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + /* Destroy file port */ + status = pjmedia_port_destroy( file_port ); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + /* Release application pool */ + pj_pool_release( pool ); + + /* Destroy media endpoint. */ + pjmedia_endpt_destroy( med_endpt ); + + /* Destroy pool factory */ + pj_caching_pool_destroy( &cp ); + + /* Shutdown PJLIB */ + pj_shutdown(); + + + /* Done. */ + return 0; +} diff --git a/pjsip-apps/src/samples/resampleplay.c b/pjsip-apps/src/samples/resampleplay.c new file mode 100644 index 0000000..c73b2e3 --- /dev/null +++ b/pjsip-apps/src/samples/resampleplay.c @@ -0,0 +1,232 @@ +/* $Id: resampleplay.c 3664 2011-07-19 03:42:28Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 + */ + +/** + * \page page_pjmedia_samples_resampleplay_c Samples: Using Resample Port + * + * This example demonstrates how to use @ref PJMEDIA_RESAMPLE_PORT to + * change the sampling rate of the media streams. + * + * This file is pjsip-apps/src/samples/resampleplay.c + * + * \includelineno resampleplay.c + */ + +#include <pjmedia.h> +#include <pjlib-util.h> +#include <pjlib.h> + +#include <stdio.h> +#include <stdlib.h> + +#include "util.h" + +/* For logging purpose. */ +#define THIS_FILE "resampleplay.c" + + +static const char *desc = +" FILE \n" +" \n" +" resampleplay.c \n" +" \n" +" PURPOSE \n" +" \n" +" Demonstrate how use resample port to play a WAV file to sound \n" +" device using different sampling rate. \n" +" \n" +" USAGE \n" +" \n" +" resampleplay [options] FILE.WAV \n" +" \n" +" where options: \n" +SND_USAGE +" \n" +" The WAV file could have mono or stereo channels with arbitrary \n" +" sampling rate, but MUST contain uncompressed (i.e. 16bit) PCM. \n"; + + +int main(int argc, char *argv[]) +{ + pj_caching_pool cp; + pjmedia_endpt *med_endpt; + pj_pool_t *pool; + pjmedia_port *file_port; + pjmedia_port *resample_port; + pjmedia_snd_port *snd_port; + char tmp[10]; + pj_status_t status; + + int dev_id = -1; + int sampling_rate = CLOCK_RATE; + int channel_count = NCHANNELS; + int samples_per_frame = NSAMPLES; + int bits_per_sample = NBITS; + //int ptime; + //int down_samples; + + /* Must init PJLIB first: */ + status = pj_init(); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + /* Get options */ + if (get_snd_options(THIS_FILE, argc, argv, &dev_id, &sampling_rate, + &channel_count, &samples_per_frame, &bits_per_sample)) + { + puts(""); + puts(desc); + return 1; + } + + if (!argv[pj_optind]) { + puts("Error: no file is specified"); + puts(desc); + return 1; + } + + /* Must create a pool factory before we can allocate any memory. */ + pj_caching_pool_init(&cp, &pj_pool_factory_default_policy, 0); + + /* + * Initialize media endpoint. + * This will implicitly initialize PJMEDIA too. + */ + status = pjmedia_endpt_create(&cp.factory, NULL, 1, &med_endpt); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* Create memory pool for our file player */ + pool = pj_pool_create( &cp.factory, /* pool factory */ + "app", /* pool name. */ + 4000, /* init size */ + 4000, /* increment size */ + NULL /* callback on error */ + ); + + /* Create the file port. */ + status = pjmedia_wav_player_port_create( pool, argv[pj_optind], 0, 0, + 0, &file_port); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to open file", status); + return 1; + } + + /* File must have same number of channels. */ + if (PJMEDIA_PIA_CCNT(&file_port->info) != (unsigned)channel_count) { + PJ_LOG(3,(THIS_FILE, "Error: file has different number of channels. " + "Perhaps you'd need -c option?")); + pjmedia_port_destroy(file_port); + return 1; + } + + /* Calculate number of samples per frame to be taken from file port */ + //ptime = samples_per_frame * 1000 / sampling_rate; + + /* Create the resample port. */ + status = pjmedia_resample_port_create( pool, file_port, + sampling_rate, 0, + &resample_port); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to create resample port", status); + return 1; + } + + /* Create sound player port. */ + status = pjmedia_snd_port_create( + pool, /* pool */ + dev_id, /* device */ + dev_id, /* device */ + sampling_rate, /* clock rate. */ + channel_count, /* # of channels. */ + samples_per_frame, /* samples per frame. */ + bits_per_sample, /* bits per sample. */ + 0, /* options */ + &snd_port /* returned port */ + ); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to open sound device", status); + return 1; + } + + /* Connect resample port to sound device */ + status = pjmedia_snd_port_connect( snd_port, resample_port); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Error connecting sound ports", status); + return 1; + } + + + /* Dump memory usage */ + dump_pool_usage(THIS_FILE, &cp); + + /* + * File should be playing and looping now, using sound device's thread. + */ + + + /* Sleep to allow log messages to flush */ + pj_thread_sleep(100); + + + printf("Playing %s at sampling rate %d (original file sampling rate=%d)\n", + argv[pj_optind], sampling_rate, + PJMEDIA_PIA_SRATE(&file_port->info)); + puts(""); + puts("Press <ENTER> to stop playing and quit"); + + if (fgets(tmp, sizeof(tmp), stdin) == NULL) { + puts("EOF while reading stdin, will quit now.."); + } + + /* Start deinitialization: */ + + + /* Destroy sound device */ + status = pjmedia_snd_port_destroy( snd_port ); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + /* Destroy resample port. + * This will destroy all downstream ports (e.g. the file port) + */ + status = pjmedia_port_destroy( resample_port ); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + /* Release application pool */ + pj_pool_release( pool ); + + /* Destroy media endpoint. */ + pjmedia_endpt_destroy( med_endpt ); + + /* Destroy pool factory */ + pj_caching_pool_destroy( &cp ); + + /* Shutdown PJLIB */ + pj_shutdown(); + + + /* Done. */ + return 0; + +} + + + diff --git a/pjsip-apps/src/samples/simple_pjsua.c b/pjsip-apps/src/samples/simple_pjsua.c new file mode 100644 index 0000000..7716f7f --- /dev/null +++ b/pjsip-apps/src/samples/simple_pjsua.c @@ -0,0 +1,201 @@ +/* $Id: simple_pjsua.c 3553 2011-05-05 06:14:19Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 + */ + +/** + * simple_pjsua.c + * + * This is a very simple but fully featured SIP user agent, with the + * following capabilities: + * - SIP registration + * - Making and receiving call + * - Audio/media to sound device. + * + * Usage: + * - To make outgoing call, start simple_pjsua with the URL of remote + * destination to contact. + * E.g.: + * simpleua sip:user@remote + * + * - Incoming calls will automatically be answered with 200. + * + * This program will quit once it has completed a single call. + */ + +#include <pjsua-lib/pjsua.h> + +#define THIS_FILE "APP" + +#define SIP_DOMAIN "example.com" +#define SIP_USER "alice" +#define SIP_PASSWD "secret" + + +/* Callback called by the library upon receiving incoming call */ +static void on_incoming_call(pjsua_acc_id acc_id, pjsua_call_id call_id, + pjsip_rx_data *rdata) +{ + pjsua_call_info ci; + + PJ_UNUSED_ARG(acc_id); + PJ_UNUSED_ARG(rdata); + + pjsua_call_get_info(call_id, &ci); + + PJ_LOG(3,(THIS_FILE, "Incoming call from %.*s!!", + (int)ci.remote_info.slen, + ci.remote_info.ptr)); + + /* Automatically answer incoming calls with 200/OK */ + pjsua_call_answer(call_id, 200, NULL, NULL); +} + +/* Callback called by the library when call's state has changed */ +static void on_call_state(pjsua_call_id call_id, pjsip_event *e) +{ + pjsua_call_info ci; + + PJ_UNUSED_ARG(e); + + pjsua_call_get_info(call_id, &ci); + PJ_LOG(3,(THIS_FILE, "Call %d state=%.*s", call_id, + (int)ci.state_text.slen, + ci.state_text.ptr)); +} + +/* Callback called by the library when call's media state has changed */ +static void on_call_media_state(pjsua_call_id call_id) +{ + pjsua_call_info ci; + + pjsua_call_get_info(call_id, &ci); + + if (ci.media_status == PJSUA_CALL_MEDIA_ACTIVE) { + // When media is active, connect call to sound device. + pjsua_conf_connect(ci.conf_slot, 0); + pjsua_conf_connect(0, ci.conf_slot); + } +} + +/* Display error and exit application */ +static void error_exit(const char *title, pj_status_t status) +{ + pjsua_perror(THIS_FILE, title, status); + pjsua_destroy(); + exit(1); +} + +/* + * main() + * + * argv[1] may contain URL to call. + */ +int main(int argc, char *argv[]) +{ + pjsua_acc_id acc_id; + pj_status_t status; + + /* Create pjsua first! */ + status = pjsua_create(); + if (status != PJ_SUCCESS) error_exit("Error in pjsua_create()", status); + + /* If argument is specified, it's got to be a valid SIP URL */ + if (argc > 1) { + status = pjsua_verify_url(argv[1]); + if (status != PJ_SUCCESS) error_exit("Invalid URL in argv", status); + } + + /* Init pjsua */ + { + pjsua_config cfg; + pjsua_logging_config log_cfg; + + pjsua_config_default(&cfg); + cfg.cb.on_incoming_call = &on_incoming_call; + cfg.cb.on_call_media_state = &on_call_media_state; + cfg.cb.on_call_state = &on_call_state; + + pjsua_logging_config_default(&log_cfg); + log_cfg.console_level = 4; + + status = pjsua_init(&cfg, &log_cfg, NULL); + if (status != PJ_SUCCESS) error_exit("Error in pjsua_init()", status); + } + + /* Add UDP transport. */ + { + pjsua_transport_config cfg; + + pjsua_transport_config_default(&cfg); + cfg.port = 5060; + status = pjsua_transport_create(PJSIP_TRANSPORT_UDP, &cfg, NULL); + if (status != PJ_SUCCESS) error_exit("Error creating transport", status); + } + + /* Initialization is done, now start pjsua */ + status = pjsua_start(); + if (status != PJ_SUCCESS) error_exit("Error starting pjsua", status); + + /* Register to SIP server by creating SIP account. */ + { + pjsua_acc_config cfg; + + pjsua_acc_config_default(&cfg); + cfg.id = pj_str("sip:" SIP_USER "@" SIP_DOMAIN); + cfg.reg_uri = pj_str("sip:" SIP_DOMAIN); + cfg.cred_count = 1; + cfg.cred_info[0].realm = pj_str(SIP_DOMAIN); + cfg.cred_info[0].scheme = pj_str("digest"); + cfg.cred_info[0].username = pj_str(SIP_USER); + cfg.cred_info[0].data_type = PJSIP_CRED_DATA_PLAIN_PASSWD; + cfg.cred_info[0].data = pj_str(SIP_PASSWD); + + status = pjsua_acc_add(&cfg, PJ_TRUE, &acc_id); + if (status != PJ_SUCCESS) error_exit("Error adding account", status); + } + + /* If URL is specified, make call to the URL. */ + if (argc > 1) { + pj_str_t uri = pj_str(argv[1]); + status = pjsua_call_make_call(acc_id, &uri, 0, NULL, NULL, NULL); + if (status != PJ_SUCCESS) error_exit("Error making call", status); + } + + /* Wait until user press "q" to quit. */ + for (;;) { + char option[10]; + + puts("Press 'h' to hangup all calls, 'q' to quit"); + if (fgets(option, sizeof(option), stdin) == NULL) { + puts("EOF while reading stdin, will quit now.."); + break; + } + + if (option[0] == 'q') + break; + + if (option[0] == 'h') + pjsua_call_hangup_all(); + } + + /* Destroy pjsua */ + pjsua_destroy(); + + return 0; +} diff --git a/pjsip-apps/src/samples/simpleua.c b/pjsip-apps/src/samples/simpleua.c new file mode 100644 index 0000000..2906323 --- /dev/null +++ b/pjsip-apps/src/samples/simpleua.c @@ -0,0 +1,1030 @@ +/* $Id: simpleua.c 4051 2012-04-13 08:16:30Z ming $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 + */ + + +/** + * simpleua.c + * + * This is a very simple SIP user agent complete with media. The user + * agent should do a proper SDP negotiation and start RTP media once + * SDP negotiation has completed. + * + * This program does not register to SIP server. + * + * Capabilities to be demonstrated here: + * - Basic call + * - Should support IPv6 (not tested) + * - UDP transport at port 5060 (hard coded) + * - RTP socket at port 4000 (hard coded) + * - proper SDP negotiation + * - PCMA/PCMU codec only. + * - Audio/media to sound device. + * + * + * Usage: + * - To make outgoing call, start simpleua with the URL of remote + * destination to contact. + * E.g.: + * simpleua sip:user@remote + * + * - Incoming calls will automatically be answered with 180, then 200. + * + * This program does not disconnect call. + * + * This program will quit once it has completed a single call. + */ + +/* Include all headers. */ +#include <pjsip.h> +#include <pjmedia.h> +#include <pjmedia-codec.h> +#include <pjsip_ua.h> +#include <pjsip_simple.h> +#include <pjlib-util.h> +#include <pjlib.h> + +/* For logging purpose. */ +#define THIS_FILE "simpleua.c" + +#include "util.h" + + +/* Settings */ +#define AF pj_AF_INET() /* Change to pj_AF_INET6() for IPv6. + * PJ_HAS_IPV6 must be enabled and + * your system must support IPv6. */ +#if 0 +#define SIP_PORT 5080 /* Listening SIP port */ +#define RTP_PORT 5000 /* RTP port */ +#else +#define SIP_PORT 5060 /* Listening SIP port */ +#define RTP_PORT 4000 /* RTP port */ +#endif + +#define MAX_MEDIA_CNT 2 /* Media count, set to 1 for audio + * only or 2 for audio and video */ + +/* + * Static variables. + */ + +static pj_bool_t g_complete; /* Quit flag. */ +static pjsip_endpoint *g_endpt; /* SIP endpoint. */ +static pj_caching_pool cp; /* Global pool factory. */ + +static pjmedia_endpt *g_med_endpt; /* Media endpoint. */ + +static pjmedia_transport_info g_med_tpinfo[MAX_MEDIA_CNT]; + /* Socket info for media */ +static pjmedia_transport *g_med_transport[MAX_MEDIA_CNT]; + /* Media stream transport */ +static pjmedia_sock_info g_sock_info[MAX_MEDIA_CNT]; + /* Socket info array */ + +/* Call variables: */ +static pjsip_inv_session *g_inv; /* Current invite session. */ +static pjmedia_stream *g_med_stream; /* Call's audio stream. */ +static pjmedia_snd_port *g_snd_port; /* Sound device. */ + +#if defined(PJMEDIA_HAS_VIDEO) && (PJMEDIA_HAS_VIDEO != 0) +static pjmedia_vid_stream *g_med_vstream; /* Call's video stream. */ +static pjmedia_vid_port *g_vid_capturer;/* Call's video capturer. */ +static pjmedia_vid_port *g_vid_renderer;/* Call's video renderer. */ +#endif /* PJMEDIA_HAS_VIDEO */ + +/* + * Prototypes: + */ + +/* Callback to be called when SDP negotiation is done in the call: */ +static void call_on_media_update( pjsip_inv_session *inv, + pj_status_t status); + +/* Callback to be called when invite session's state has changed: */ +static void call_on_state_changed( pjsip_inv_session *inv, + pjsip_event *e); + +/* Callback to be called when dialog has forked: */ +static void call_on_forked(pjsip_inv_session *inv, pjsip_event *e); + +/* Callback to be called to handle incoming requests outside dialogs: */ +static pj_bool_t on_rx_request( pjsip_rx_data *rdata ); + + + + +/* This is a PJSIP module to be registered by application to handle + * incoming requests outside any dialogs/transactions. The main purpose + * here is to handle incoming INVITE request message, where we will + * create a dialog and INVITE session for it. + */ +static pjsip_module mod_simpleua = +{ + NULL, NULL, /* prev, next. */ + { "mod-simpleua", 12 }, /* Name. */ + -1, /* Id */ + PJSIP_MOD_PRIORITY_APPLICATION, /* Priority */ + NULL, /* load() */ + NULL, /* start() */ + NULL, /* stop() */ + NULL, /* unload() */ + &on_rx_request, /* on_rx_request() */ + NULL, /* on_rx_response() */ + NULL, /* on_tx_request. */ + NULL, /* on_tx_response() */ + NULL, /* on_tsx_state() */ +}; + + +/* Notification on incoming messages */ +static pj_bool_t logging_on_rx_msg(pjsip_rx_data *rdata) +{ + PJ_LOG(4,(THIS_FILE, "RX %d bytes %s from %s %s:%d:\n" + "%.*s\n" + "--end msg--", + rdata->msg_info.len, + pjsip_rx_data_get_info(rdata), + rdata->tp_info.transport->type_name, + rdata->pkt_info.src_name, + rdata->pkt_info.src_port, + (int)rdata->msg_info.len, + rdata->msg_info.msg_buf)); + + /* Always return false, otherwise messages will not get processed! */ + return PJ_FALSE; +} + +/* Notification on outgoing messages */ +static pj_status_t logging_on_tx_msg(pjsip_tx_data *tdata) +{ + + /* Important note: + * tp_info field is only valid after outgoing messages has passed + * transport layer. So don't try to access tp_info when the module + * has lower priority than transport layer. + */ + + PJ_LOG(4,(THIS_FILE, "TX %d bytes %s to %s %s:%d:\n" + "%.*s\n" + "--end msg--", + (tdata->buf.cur - tdata->buf.start), + pjsip_tx_data_get_info(tdata), + tdata->tp_info.transport->type_name, + tdata->tp_info.dst_name, + tdata->tp_info.dst_port, + (int)(tdata->buf.cur - tdata->buf.start), + tdata->buf.start)); + + /* Always return success, otherwise message will not get sent! */ + return PJ_SUCCESS; +} + +/* The module instance. */ +static pjsip_module msg_logger = +{ + NULL, NULL, /* prev, next. */ + { "mod-msg-log", 13 }, /* Name. */ + -1, /* Id */ + PJSIP_MOD_PRIORITY_TRANSPORT_LAYER-1,/* Priority */ + NULL, /* load() */ + NULL, /* start() */ + NULL, /* stop() */ + NULL, /* unload() */ + &logging_on_rx_msg, /* on_rx_request() */ + &logging_on_rx_msg, /* on_rx_response() */ + &logging_on_tx_msg, /* on_tx_request. */ + &logging_on_tx_msg, /* on_tx_response() */ + NULL, /* on_tsx_state() */ + +}; + + +/* + * main() + * + * If called with argument, treat argument as SIP URL to be called. + * Otherwise wait for incoming calls. + */ +int main(int argc, char *argv[]) +{ + pj_pool_t *pool = NULL; + pj_status_t status; + unsigned i; + + /* Must init PJLIB first: */ + status = pj_init(); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + pj_log_set_level(5); + + /* Then init PJLIB-UTIL: */ + status = pjlib_util_init(); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + /* Must create a pool factory before we can allocate any memory. */ + pj_caching_pool_init(&cp, &pj_pool_factory_default_policy, 0); + + + /* Create global endpoint: */ + { + const pj_str_t *hostname; + const char *endpt_name; + + /* Endpoint MUST be assigned a globally unique name. + * The name will be used as the hostname in Warning header. + */ + + /* For this implementation, we'll use hostname for simplicity */ + hostname = pj_gethostname(); + endpt_name = hostname->ptr; + + /* Create the endpoint: */ + + status = pjsip_endpt_create(&cp.factory, endpt_name, + &g_endpt); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + } + + + /* + * Add UDP transport, with hard-coded port + * Alternatively, application can use pjsip_udp_transport_attach() to + * start UDP transport, if it already has an UDP socket (e.g. after it + * resolves the address with STUN). + */ + { + pj_sockaddr addr; + + pj_sockaddr_init(AF, &addr, NULL, (pj_uint16_t)SIP_PORT); + + if (AF == pj_AF_INET()) { + status = pjsip_udp_transport_start( g_endpt, &addr.ipv4, NULL, + 1, NULL); + } else if (AF == pj_AF_INET6()) { + status = pjsip_udp_transport_start6(g_endpt, &addr.ipv6, NULL, + 1, NULL); + } else { + status = PJ_EAFNOTSUP; + } + + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to start UDP transport", status); + return 1; + } + } + + + /* + * Init transaction layer. + * This will create/initialize transaction hash tables etc. + */ + status = pjsip_tsx_layer_init_module(g_endpt); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + /* + * Initialize UA layer module. + * This will create/initialize dialog hash tables etc. + */ + status = pjsip_ua_init_module( g_endpt, NULL ); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + /* + * Init invite session module. + * The invite session module initialization takes additional argument, + * i.e. a structure containing callbacks to be called on specific + * occurence of events. + * + * The on_state_changed and on_new_session callbacks are mandatory. + * Application must supply the callback function. + * + * We use on_media_update() callback in this application to start + * media transmission. + */ + { + pjsip_inv_callback inv_cb; + + /* Init the callback for INVITE session: */ + pj_bzero(&inv_cb, sizeof(inv_cb)); + inv_cb.on_state_changed = &call_on_state_changed; + inv_cb.on_new_session = &call_on_forked; + inv_cb.on_media_update = &call_on_media_update; + + /* Initialize invite session module: */ + status = pjsip_inv_usage_init(g_endpt, &inv_cb); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + } + + /* Initialize 100rel support */ + status = pjsip_100rel_init_module(g_endpt); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); + + /* + * Register our module to receive incoming requests. + */ + status = pjsip_endpt_register_module( g_endpt, &mod_simpleua); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* + * Register message logger module. + */ + status = pjsip_endpt_register_module( g_endpt, &msg_logger); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + /* + * Initialize media endpoint. + * This will implicitly initialize PJMEDIA too. + */ +#if PJ_HAS_THREADS + status = pjmedia_endpt_create(&cp.factory, NULL, 1, &g_med_endpt); +#else + status = pjmedia_endpt_create(&cp.factory, + pjsip_endpt_get_ioqueue(g_endpt), + 0, &g_med_endpt); +#endif + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* + * Add PCMA/PCMU codec to the media endpoint. + */ +#if defined(PJMEDIA_HAS_G711_CODEC) && PJMEDIA_HAS_G711_CODEC!=0 + status = pjmedia_codec_g711_init(g_med_endpt); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); +#endif + + +#if defined(PJMEDIA_HAS_VIDEO) && (PJMEDIA_HAS_VIDEO != 0) + /* Init video subsystem */ + pool = pjmedia_endpt_create_pool(g_med_endpt, "Video subsystem", 512, 512); + status = pjmedia_video_format_mgr_create(pool, 64, 0, NULL); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + status = pjmedia_converter_mgr_create(pool, NULL); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + status = pjmedia_vid_codec_mgr_create(pool, NULL); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + status = pjmedia_vid_dev_subsys_init(&cp.factory); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + +# if defined(PJMEDIA_HAS_FFMPEG_VID_CODEC) && PJMEDIA_HAS_FFMPEG_VID_CODEC!=0 + /* Init ffmpeg video codecs */ + status = pjmedia_codec_ffmpeg_vid_init(NULL, &cp.factory); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); +# endif /* PJMEDIA_HAS_FFMPEG_VID_CODEC */ + +#endif /* PJMEDIA_HAS_VIDEO */ + + /* + * Create media transport used to send/receive RTP/RTCP socket. + * One media transport is needed for each call. Application may + * opt to re-use the same media transport for subsequent calls. + */ + for (i = 0; i < PJ_ARRAY_SIZE(g_med_transport); ++i) { + status = pjmedia_transport_udp_create3(g_med_endpt, AF, NULL, NULL, + RTP_PORT + i*2, 0, + &g_med_transport[i]); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to create media transport", status); + return 1; + } + + /* + * Get socket info (address, port) of the media transport. We will + * need this info to create SDP (i.e. the address and port info in + * the SDP). + */ + pjmedia_transport_info_init(&g_med_tpinfo[i]); + pjmedia_transport_get_info(g_med_transport[i], &g_med_tpinfo[i]); + + pj_memcpy(&g_sock_info[i], &g_med_tpinfo[i].sock_info, + sizeof(pjmedia_sock_info)); + } + + /* + * If URL is specified, then make call immediately. + */ + if (argc > 1) { + pj_sockaddr hostaddr; + char hostip[PJ_INET6_ADDRSTRLEN+2]; + char temp[80]; + pj_str_t dst_uri = pj_str(argv[1]); + pj_str_t local_uri; + pjsip_dialog *dlg; + pjmedia_sdp_session *local_sdp; + pjsip_tx_data *tdata; + + if (pj_gethostip(AF, &hostaddr) != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to retrieve local host IP", status); + return 1; + } + pj_sockaddr_print(&hostaddr, hostip, sizeof(hostip), 2); + + pj_ansi_sprintf(temp, "<sip:simpleuac@%s:%d>", + hostip, SIP_PORT); + local_uri = pj_str(temp); + + /* Create UAC dialog */ + status = pjsip_dlg_create_uac( pjsip_ua_instance(), + &local_uri, /* local URI */ + &local_uri, /* local Contact */ + &dst_uri, /* remote URI */ + &dst_uri, /* remote target */ + &dlg); /* dialog */ + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to create UAC dialog", status); + return 1; + } + + /* If we expect the outgoing INVITE to be challenged, then we should + * put the credentials in the dialog here, with something like this: + * + { + pjsip_cred_info cred[1]; + + cred[0].realm = pj_str("sip.server.realm"); + cred[0].scheme = pj_str("digest"); + cred[0].username = pj_str("theuser"); + cred[0].data_type = PJSIP_CRED_DATA_PLAIN_PASSWD; + cred[0].data = pj_str("thepassword"); + + pjsip_auth_clt_set_credentials( &dlg->auth_sess, 1, cred); + } + * + */ + + + /* Get the SDP body to be put in the outgoing INVITE, by asking + * media endpoint to create one for us. + */ + status = pjmedia_endpt_create_sdp( g_med_endpt, /* the media endpt */ + dlg->pool, /* pool. */ + MAX_MEDIA_CNT, /* # of streams */ + g_sock_info, /* RTP sock info */ + &local_sdp); /* the SDP result */ + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + + /* Create the INVITE session, and pass the SDP returned earlier + * as the session's initial capability. + */ + status = pjsip_inv_create_uac( dlg, local_sdp, 0, &g_inv); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* If we want the initial INVITE to travel to specific SIP proxies, + * then we should put the initial dialog's route set here. The final + * route set will be updated once a dialog has been established. + * To set the dialog's initial route set, we do it with something + * like this: + * + { + pjsip_route_hdr route_set; + pjsip_route_hdr *route; + const pj_str_t hname = { "Route", 5 }; + char *uri = "sip:proxy.server;lr"; + + pj_list_init(&route_set); + + route = pjsip_parse_hdr( dlg->pool, &hname, + uri, strlen(uri), + NULL); + PJ_ASSERT_RETURN(route != NULL, 1); + pj_list_push_back(&route_set, route); + + pjsip_dlg_set_route_set(dlg, &route_set); + } + * + * Note that Route URI SHOULD have an ";lr" parameter! + */ + + /* Create initial INVITE request. + * This INVITE request will contain a perfectly good request and + * an SDP body as well. + */ + status = pjsip_inv_invite(g_inv, &tdata); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + + /* Send initial INVITE request. + * From now on, the invite session's state will be reported to us + * via the invite session callbacks. + */ + status = pjsip_inv_send_msg(g_inv, tdata); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + } else { + + /* No URL to make call to */ + + PJ_LOG(3,(THIS_FILE, "Ready to accept incoming calls...")); + } + + + /* Loop until one call is completed */ + for (;!g_complete;) { + pj_time_val timeout = {0, 10}; + pjsip_endpt_handle_events(g_endpt, &timeout); + } + + /* On exit, dump current memory usage: */ + dump_pool_usage(THIS_FILE, &cp); + + /* Destroy audio ports. Destroy the audio port first + * before the stream since the audio port has threads + * that get/put frames to the stream. + */ + if (g_snd_port) + pjmedia_snd_port_destroy(g_snd_port); + +#if defined(PJMEDIA_HAS_VIDEO) && (PJMEDIA_HAS_VIDEO != 0) + /* Destroy video ports */ + if (g_vid_capturer) + pjmedia_vid_port_destroy(g_vid_capturer); + if (g_vid_renderer) + pjmedia_vid_port_destroy(g_vid_renderer); +#endif + + /* Destroy streams */ + if (g_med_stream) + pjmedia_stream_destroy(g_med_stream); +#if defined(PJMEDIA_HAS_VIDEO) && (PJMEDIA_HAS_VIDEO != 0) + if (g_med_vstream) + pjmedia_vid_stream_destroy(g_med_vstream); + + /* Deinit ffmpeg codec */ +# if defined(PJMEDIA_HAS_FFMPEG_VID_CODEC) && PJMEDIA_HAS_FFMPEG_VID_CODEC!=0 + pjmedia_codec_ffmpeg_vid_deinit(); +# endif + +#endif + + /* Destroy media transports */ + for (i = 0; i < MAX_MEDIA_CNT; ++i) { + if (g_med_transport[i]) + pjmedia_transport_close(g_med_transport[i]); + } + + /* Deinit pjmedia endpoint */ + if (g_med_endpt) + pjmedia_endpt_destroy(g_med_endpt); + + /* Deinit pjsip endpoint */ + if (g_endpt) + pjsip_endpt_destroy(g_endpt); + + /* Release pool */ + if (pool) + pj_pool_release(pool); + + return 0; +} + + + +/* + * Callback when INVITE session state has changed. + * This callback is registered when the invite session module is initialized. + * We mostly want to know when the invite session has been disconnected, + * so that we can quit the application. + */ +static void call_on_state_changed( pjsip_inv_session *inv, + pjsip_event *e) +{ + PJ_UNUSED_ARG(e); + + if (inv->state == PJSIP_INV_STATE_DISCONNECTED) { + + PJ_LOG(3,(THIS_FILE, "Call DISCONNECTED [reason=%d (%s)]", + inv->cause, + pjsip_get_status_text(inv->cause)->ptr)); + + PJ_LOG(3,(THIS_FILE, "One call completed, application quitting...")); + g_complete = 1; + + } else { + + PJ_LOG(3,(THIS_FILE, "Call state changed to %s", + pjsip_inv_state_name(inv->state))); + + } +} + + +/* This callback is called when dialog has forked. */ +static void call_on_forked(pjsip_inv_session *inv, pjsip_event *e) +{ + /* To be done... */ + PJ_UNUSED_ARG(inv); + PJ_UNUSED_ARG(e); +} + + +/* + * Callback when incoming requests outside any transactions and any + * dialogs are received. We're only interested to hande incoming INVITE + * request, and we'll reject any other requests with 500 response. + */ +static pj_bool_t on_rx_request( pjsip_rx_data *rdata ) +{ + pj_sockaddr hostaddr; + char temp[80], hostip[PJ_INET6_ADDRSTRLEN]; + pj_str_t local_uri; + pjsip_dialog *dlg; + pjmedia_sdp_session *local_sdp; + pjsip_tx_data *tdata; + unsigned options = 0; + pj_status_t status; + + + /* + * Respond (statelessly) any non-INVITE requests with 500 + */ + if (rdata->msg_info.msg->line.req.method.id != PJSIP_INVITE_METHOD) { + + if (rdata->msg_info.msg->line.req.method.id != PJSIP_ACK_METHOD) { + pj_str_t reason = pj_str("Simple UA unable to handle " + "this request"); + + pjsip_endpt_respond_stateless( g_endpt, rdata, + 500, &reason, + NULL, NULL); + } + return PJ_TRUE; + } + + + /* + * Reject INVITE if we already have an INVITE session in progress. + */ + if (g_inv) { + + pj_str_t reason = pj_str("Another call is in progress"); + + pjsip_endpt_respond_stateless( g_endpt, rdata, + 500, &reason, + NULL, NULL); + return PJ_TRUE; + + } + + /* Verify that we can handle the request. */ + status = pjsip_inv_verify_request(rdata, &options, NULL, NULL, + g_endpt, NULL); + if (status != PJ_SUCCESS) { + + pj_str_t reason = pj_str("Sorry Simple UA can not handle this INVITE"); + + pjsip_endpt_respond_stateless( g_endpt, rdata, + 500, &reason, + NULL, NULL); + return PJ_TRUE; + } + + /* + * Generate Contact URI + */ + if (pj_gethostip(AF, &hostaddr) != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to retrieve local host IP", status); + return PJ_TRUE; + } + pj_sockaddr_print(&hostaddr, hostip, sizeof(hostip), 2); + + pj_ansi_sprintf(temp, "<sip:simpleuas@%s:%d>", + hostip, SIP_PORT); + local_uri = pj_str(temp); + + /* + * Create UAS dialog. + */ + status = pjsip_dlg_create_uas( pjsip_ua_instance(), + rdata, + &local_uri, /* contact */ + &dlg); + if (status != PJ_SUCCESS) { + pjsip_endpt_respond_stateless(g_endpt, rdata, 500, NULL, + NULL, NULL); + return PJ_TRUE; + } + + /* + * Get media capability from media endpoint: + */ + + status = pjmedia_endpt_create_sdp( g_med_endpt, rdata->tp_info.pool, + MAX_MEDIA_CNT, g_sock_info, &local_sdp); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, PJ_TRUE); + + + /* + * Create invite session, and pass both the UAS dialog and the SDP + * capability to the session. + */ + status = pjsip_inv_create_uas( dlg, rdata, local_sdp, 0, &g_inv); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, PJ_TRUE); + + + /* + * Initially send 180 response. + * + * The very first response to an INVITE must be created with + * pjsip_inv_initial_answer(). Subsequent responses to the same + * transaction MUST use pjsip_inv_answer(). + */ + status = pjsip_inv_initial_answer(g_inv, rdata, + 180, + NULL, NULL, &tdata); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, PJ_TRUE); + + + /* Send the 180 response. */ + status = pjsip_inv_send_msg(g_inv, tdata); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, PJ_TRUE); + + + /* + * Now create 200 response. + */ + status = pjsip_inv_answer( g_inv, + 200, NULL, /* st_code and st_text */ + NULL, /* SDP already specified */ + &tdata); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, PJ_TRUE); + + /* + * Send the 200 response. + */ + status = pjsip_inv_send_msg(g_inv, tdata); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, PJ_TRUE); + + + /* Done. + * When the call is disconnected, it will be reported via the callback. + */ + + return PJ_TRUE; +} + + + +/* + * Callback when SDP negotiation has completed. + * We are interested with this callback because we want to start media + * as soon as SDP negotiation is completed. + */ +static void call_on_media_update( pjsip_inv_session *inv, + pj_status_t status) +{ + pjmedia_stream_info stream_info; + const pjmedia_sdp_session *local_sdp; + const pjmedia_sdp_session *remote_sdp; + pjmedia_port *media_port; + + if (status != PJ_SUCCESS) { + + app_perror(THIS_FILE, "SDP negotiation has failed", status); + + /* Here we should disconnect call if we're not in the middle + * of initializing an UAS dialog and if this is not a re-INVITE. + */ + return; + } + + /* Get local and remote SDP. + * We need both SDPs to create a media session. + */ + status = pjmedia_sdp_neg_get_active_local(inv->neg, &local_sdp); + + status = pjmedia_sdp_neg_get_active_remote(inv->neg, &remote_sdp); + + + /* Create stream info based on the media audio SDP. */ + status = pjmedia_stream_info_from_sdp(&stream_info, inv->dlg->pool, + g_med_endpt, + local_sdp, remote_sdp, 0); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE,"Unable to create audio stream info",status); + return; + } + + /* If required, we can also change some settings in the stream info, + * (such as jitter buffer settings, codec settings, etc) before we + * create the stream. + */ + + /* Create new audio media stream, passing the stream info, and also the + * media socket that we created earlier. + */ + status = pjmedia_stream_create(g_med_endpt, inv->dlg->pool, &stream_info, + g_med_transport[0], NULL, &g_med_stream); + if (status != PJ_SUCCESS) { + app_perror( THIS_FILE, "Unable to create audio stream", status); + return; + } + + /* Start the audio stream */ + status = pjmedia_stream_start(g_med_stream); + if (status != PJ_SUCCESS) { + app_perror( THIS_FILE, "Unable to start audio stream", status); + return; + } + + /* Get the media port interface of the audio stream. + * Media port interface is basicly a struct containing get_frame() and + * put_frame() function. With this media port interface, we can attach + * the port interface to conference bridge, or directly to a sound + * player/recorder device. + */ + pjmedia_stream_get_port(g_med_stream, &media_port); + + /* Create sound port */ + pjmedia_snd_port_create(inv->pool, + PJMEDIA_AUD_DEFAULT_CAPTURE_DEV, + PJMEDIA_AUD_DEFAULT_PLAYBACK_DEV, + PJMEDIA_PIA_SRATE(&media_port->info),/* clock rate */ + PJMEDIA_PIA_CCNT(&media_port->info),/* channel count */ + PJMEDIA_PIA_SPF(&media_port->info), /* samples per frame*/ + PJMEDIA_PIA_BITS(&media_port->info),/* bits per sample */ + 0, + &g_snd_port); + + if (status != PJ_SUCCESS) { + app_perror( THIS_FILE, "Unable to create sound port", status); + PJ_LOG(3,(THIS_FILE, "%d %d %d %d", + PJMEDIA_PIA_SRATE(&media_port->info),/* clock rate */ + PJMEDIA_PIA_CCNT(&media_port->info),/* channel count */ + PJMEDIA_PIA_SPF(&media_port->info), /* samples per frame*/ + PJMEDIA_PIA_BITS(&media_port->info) /* bits per sample */ + )); + return; + } + + status = pjmedia_snd_port_connect(g_snd_port, media_port); + + + /* Get the media port interface of the second stream in the session, + * which is video stream. With this media port interface, we can attach + * the port directly to a renderer/capture video device. + */ +#if defined(PJMEDIA_HAS_VIDEO) && (PJMEDIA_HAS_VIDEO != 0) + if (local_sdp->media_count > 1) { + pjmedia_vid_stream_info vstream_info; + pjmedia_vid_port_param vport_param; + + pjmedia_vid_port_param_default(&vport_param); + + /* Create stream info based on the media video SDP. */ + status = pjmedia_vid_stream_info_from_sdp(&vstream_info, + inv->dlg->pool, g_med_endpt, + local_sdp, remote_sdp, 1); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE,"Unable to create video stream info",status); + return; + } + + /* If required, we can also change some settings in the stream info, + * (such as jitter buffer settings, codec settings, etc) before we + * create the video stream. + */ + + /* Create new video media stream, passing the stream info, and also the + * media socket that we created earlier. + */ + status = pjmedia_vid_stream_create(g_med_endpt, NULL, &vstream_info, + g_med_transport[1], NULL, + &g_med_vstream); + if (status != PJ_SUCCESS) { + app_perror( THIS_FILE, "Unable to create video stream", status); + return; + } + + /* Start the video stream */ + status = pjmedia_vid_stream_start(g_med_vstream); + if (status != PJ_SUCCESS) { + app_perror( THIS_FILE, "Unable to start video stream", status); + return; + } + + if (vstream_info.dir & PJMEDIA_DIR_DECODING) { + status = pjmedia_vid_dev_default_param( + inv->pool, PJMEDIA_VID_DEFAULT_RENDER_DEV, + &vport_param.vidparam); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to get default param of video " + "renderer device", status); + return; + } + + /* Get video stream port for decoding direction */ + pjmedia_vid_stream_get_port(g_med_vstream, PJMEDIA_DIR_DECODING, + &media_port); + + /* Set format */ + pjmedia_format_copy(&vport_param.vidparam.fmt, + &media_port->info.fmt); + vport_param.vidparam.dir = PJMEDIA_DIR_RENDER; + vport_param.active = PJ_TRUE; + + /* Create renderer */ + status = pjmedia_vid_port_create(inv->pool, &vport_param, + &g_vid_renderer); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to create video renderer device", + status); + return; + } + + /* Connect renderer to media_port */ + status = pjmedia_vid_port_connect(g_vid_renderer, media_port, + PJ_FALSE); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to connect renderer to stream", + status); + return; + } + } + + /* Create capturer */ + if (vstream_info.dir & PJMEDIA_DIR_ENCODING) { + status = pjmedia_vid_dev_default_param( + inv->pool, PJMEDIA_VID_DEFAULT_CAPTURE_DEV, + &vport_param.vidparam); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to get default param of video " + "capture device", status); + return; + } + + /* Get video stream port for decoding direction */ + pjmedia_vid_stream_get_port(g_med_vstream, PJMEDIA_DIR_ENCODING, + &media_port); + + /* Get capturer format from stream info */ + pjmedia_format_copy(&vport_param.vidparam.fmt, + &media_port->info.fmt); + vport_param.vidparam.dir = PJMEDIA_DIR_CAPTURE; + vport_param.active = PJ_TRUE; + + /* Create capturer */ + status = pjmedia_vid_port_create(inv->pool, &vport_param, + &g_vid_capturer); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to create video capture device", + status); + return; + } + + /* Connect capturer to media_port */ + status = pjmedia_vid_port_connect(g_vid_capturer, media_port, + PJ_FALSE); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to connect capturer to stream", + status); + return; + } + } + + /* Start streaming */ + if (g_vid_renderer) { + status = pjmedia_vid_port_start(g_vid_renderer); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to start video renderer", + status); + return; + } + } + if (g_vid_capturer) { + status = pjmedia_vid_port_start(g_vid_capturer); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to start video capturer", + status); + return; + } + } + } +#endif /* PJMEDIA_HAS_VIDEO */ + + /* Done with media. */ +} + + diff --git a/pjsip-apps/src/samples/sipecho.c b/pjsip-apps/src/samples/sipecho.c new file mode 100644 index 0000000..6d1ab2d --- /dev/null +++ b/pjsip-apps/src/samples/sipecho.c @@ -0,0 +1,688 @@ +/* $Id: sipecho.c 4148 2012-05-31 12:21:59Z bennylp $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 + */ + + +/** + * sipecho.c + * + * - Accepts incoming calls and echoes back SDP and any media. + * - Specify URI in cmdline argument to make call + * - Accepts registration too! + */ + +/* Include all headers. */ +#include <pjsip.h> +#include <pjmedia/sdp.h> +#include <pjsip_ua.h> +#include <pjlib-util.h> +#include <pjlib.h> + +/* For logging purpose. */ +#define THIS_FILE "sipecho.c" + +#include "util.h" + + +/* Settings */ +#define MAX_CALLS 8 + +typedef struct call_t +{ + pjsip_inv_session *inv; +} call_t; + +static struct app_t +{ + pj_caching_pool cp; + pj_pool_t *pool; + + pjsip_endpoint *sip_endpt; + //pjmedia_endpt *med_endpt; + + call_t call[MAX_CALLS]; + + pj_bool_t quit; + pj_thread_t *worker_thread; + + pj_bool_t enable_msg_logging; +} app; + +/* + * Prototypes: + */ + +static void call_on_media_update(pjsip_inv_session *inv, pj_status_t status); +static void call_on_state_changed(pjsip_inv_session *inv, pjsip_event *e); +static void call_on_rx_offer(pjsip_inv_session *inv, const pjmedia_sdp_session *offer); +static void call_on_forked(pjsip_inv_session *inv, pjsip_event *e); +static pj_bool_t on_rx_request( pjsip_rx_data *rdata ); + +/* Globals */ +static int sip_af; +static int sip_port = 5060; +static pj_bool_t sip_tcp; + +/* This is a PJSIP module to be registered by application to handle + * incoming requests outside any dialogs/transactions. The main purpose + * here is to handle incoming INVITE request message, where we will + * create a dialog and INVITE session for it. + */ +static pjsip_module mod_sipecho = +{ + NULL, NULL, /* prev, next. */ + { "mod-sipecho", 11 }, /* Name. */ + -1, /* Id */ + PJSIP_MOD_PRIORITY_APPLICATION, /* Priority */ + NULL, /* load() */ + NULL, /* start() */ + NULL, /* stop() */ + NULL, /* unload() */ + &on_rx_request, /* on_rx_request() */ + NULL, /* on_rx_response() */ + NULL, /* on_tx_request. */ + NULL, /* on_tx_response() */ + NULL, /* on_tsx_state() */ +}; + +/* Notification on incoming messages */ +static pj_bool_t logging_on_rx_msg(pjsip_rx_data *rdata) +{ + if (!app.enable_msg_logging) + return PJ_FALSE; + + PJ_LOG(3,(THIS_FILE, "RX %d bytes %s from %s %s:%d:\n" + "%.*s\n" + "--end msg--", + rdata->msg_info.len, + pjsip_rx_data_get_info(rdata), + rdata->tp_info.transport->type_name, + rdata->pkt_info.src_name, + rdata->pkt_info.src_port, + (int)rdata->msg_info.len, + rdata->msg_info.msg_buf)); + return PJ_FALSE; +} + +/* Notification on outgoing messages */ +static pj_status_t logging_on_tx_msg(pjsip_tx_data *tdata) +{ + if (!app.enable_msg_logging) + return PJ_SUCCESS; + + PJ_LOG(3,(THIS_FILE, "TX %d bytes %s to %s %s:%d:\n" + "%.*s\n" + "--end msg--", + (tdata->buf.cur - tdata->buf.start), + pjsip_tx_data_get_info(tdata), + tdata->tp_info.transport->type_name, + tdata->tp_info.dst_name, + tdata->tp_info.dst_port, + (int)(tdata->buf.cur - tdata->buf.start), + tdata->buf.start)); + return PJ_SUCCESS; +} + +/* The module instance. */ +static pjsip_module msg_logger = +{ + NULL, NULL, /* prev, next. */ + { "mod-msg-log", 13 }, /* Name. */ + -1, /* Id */ + PJSIP_MOD_PRIORITY_TRANSPORT_LAYER-1,/* Priority */ + NULL, /* load() */ + NULL, /* start() */ + NULL, /* stop() */ + NULL, /* unload() */ + &logging_on_rx_msg, /* on_rx_request() */ + &logging_on_rx_msg, /* on_rx_response() */ + &logging_on_tx_msg, /* on_tx_request. */ + &logging_on_tx_msg, /* on_tx_response() */ + NULL, /* on_tsx_state() */ + +}; + +static int worker_proc(void *arg) +{ + PJ_UNUSED_ARG(arg); + + while (!app.quit) { + pj_time_val interval = { 0, 20 }; + pjsip_endpt_handle_events(app.sip_endpt, &interval); + } + + return 0; +} + +static void hangup_all(void) +{ + unsigned i; + for (i=0; i<MAX_CALLS; ++i) { + call_t *call = &app.call[i]; + + if (call->inv && call->inv->state <= PJSIP_INV_STATE_CONFIRMED) { + pj_status_t status; + pjsip_tx_data *tdata; + + status = pjsip_inv_end_session(call->inv, PJSIP_SC_BUSY_HERE, NULL, &tdata); + if (status==PJ_SUCCESS && tdata) + pjsip_inv_send_msg(call->inv, tdata); + } + } +} + +static void destroy_stack(void) +{ + enum { WAIT_CLEAR = 5000, WAIT_INTERVAL = 500 }; + unsigned i; + + PJ_LOG(3,(THIS_FILE, "Shutting down..")); + + /* Wait until all clear */ + hangup_all(); + for (i=0; i<WAIT_CLEAR/WAIT_INTERVAL; ++i) { + unsigned j; + + for (j=0; j<MAX_CALLS; ++j) { + call_t *call = &app.call[j]; + if (call->inv && call->inv->state <= PJSIP_INV_STATE_CONFIRMED) + break; + } + + if (j==MAX_CALLS) + return; + + pj_thread_sleep(WAIT_INTERVAL); + } + + app.quit = PJ_TRUE; + if (app.worker_thread) { + pj_thread_join(app.worker_thread); + app.worker_thread = NULL; + } + + //if (app.med_endpt) + //pjmedia_endpt_destroy(app.med_endpt); + + if (app.sip_endpt) + pjsip_endpt_destroy(app.sip_endpt); + + if (app.pool) + pj_pool_release(app.pool); + + dump_pool_usage(THIS_FILE, &app.cp); + pj_caching_pool_destroy(&app.cp); +} + +#define CHECK_STATUS() do { if (status != PJ_SUCCESS) return status; } while (0) + +static pj_status_t init_stack() +{ + pj_sockaddr addr; + pjsip_inv_callback inv_cb; + pj_status_t status; + + pj_log_set_level(3); + + status = pjlib_util_init(); + CHECK_STATUS(); + + pj_caching_pool_init(&app.cp, NULL, 0); + app.pool = pj_pool_create( &app.cp.factory, "sipecho", 512, 512, 0); + + status = pjsip_endpt_create(&app.cp.factory, NULL, &app.sip_endpt); + CHECK_STATUS(); + + pj_log_set_level(4); + pj_sockaddr_init((pj_uint16_t)sip_af, &addr, NULL, (pj_uint16_t)sip_port); + if (sip_af == pj_AF_INET()) { + if (sip_tcp) { + status = pjsip_tcp_transport_start( app.sip_endpt, &addr.ipv4, 1, + NULL); + } else { + status = pjsip_udp_transport_start( app.sip_endpt, &addr.ipv4, + NULL, 1, NULL); + } + } else if (sip_af == pj_AF_INET6()) { + status = pjsip_udp_transport_start6(app.sip_endpt, &addr.ipv6, + NULL, 1, NULL); + } else { + status = PJ_EAFNOTSUP; + } + + pj_log_set_level(3); + CHECK_STATUS(); + + status = pjsip_tsx_layer_init_module(app.sip_endpt) || + pjsip_ua_init_module( app.sip_endpt, NULL ); + CHECK_STATUS(); + + pj_bzero(&inv_cb, sizeof(inv_cb)); + inv_cb.on_state_changed = &call_on_state_changed; + inv_cb.on_new_session = &call_on_forked; + inv_cb.on_media_update = &call_on_media_update; + inv_cb.on_rx_offer = &call_on_rx_offer; + + status = pjsip_inv_usage_init(app.sip_endpt, &inv_cb) || + pjsip_100rel_init_module(app.sip_endpt) || + pjsip_endpt_register_module( app.sip_endpt, &mod_sipecho) || + pjsip_endpt_register_module( app.sip_endpt, &msg_logger) || + //pjmedia_endpt_create(&app.cp.factory, + // pjsip_endpt_get_ioqueue(app.sip_endpt), + // 0, &app.med_endpt) || + pj_thread_create(app.pool, "sipecho", &worker_proc, NULL, 0, 0, + &app.worker_thread); + CHECK_STATUS(); + + return PJ_SUCCESS; +} + +static void destroy_call(call_t *call) +{ + call->inv = NULL; +} + +static pjmedia_sdp_attr * find_remove_sdp_attrs(unsigned *cnt, + pjmedia_sdp_attr *attr[], + unsigned cnt_attr_to_remove, + const char* attr_to_remove[]) +{ + pjmedia_sdp_attr *found_attr = NULL; + int i; + + for (i=0; i<(int)*cnt; ++i) { + unsigned j; + for (j=0; j<cnt_attr_to_remove; ++j) { + if (pj_strcmp2(&attr[i]->name, attr_to_remove[j])==0) { + if (!found_attr) found_attr = attr[i]; + pj_array_erase(attr, sizeof(attr[0]), *cnt, i); + --(*cnt); + --i; + break; + } + } + } + + return found_attr; +} + +static pjmedia_sdp_session *create_answer(int call_num, pj_pool_t *pool, + const pjmedia_sdp_session *offer) +{ + const char* dir_attrs[] = { "sendrecv", "sendonly", "recvonly", "inactive" }; + const char *ice_attrs[] = {"ice-pwd", "ice-ufrag", "candidate"}; + pjmedia_sdp_session *answer = pjmedia_sdp_session_clone(pool, offer); + pjmedia_sdp_attr *sess_dir_attr = NULL; + unsigned mi; + + PJ_LOG(3,(THIS_FILE, "Call %d: creating answer:", call_num)); + + answer->name = pj_str("sipecho"); + sess_dir_attr = find_remove_sdp_attrs(&answer->attr_count, answer->attr, + PJ_ARRAY_SIZE(dir_attrs), + dir_attrs); + + for (mi=0; mi<answer->media_count; ++mi) { + pjmedia_sdp_media *m = answer->media[mi]; + pjmedia_sdp_attr *m_dir_attr; + pjmedia_sdp_attr *dir_attr; + const char *our_dir = NULL; + pjmedia_sdp_conn *c; + + /* Match direction */ + m_dir_attr = find_remove_sdp_attrs(&m->attr_count, m->attr, + PJ_ARRAY_SIZE(dir_attrs), + dir_attrs); + dir_attr = m_dir_attr ? m_dir_attr : sess_dir_attr; + + if (dir_attr) { + if (pj_strcmp2(&dir_attr->name, "sendonly")==0) + our_dir = "recvonly"; + else if (pj_strcmp2(&dir_attr->name, "inactive")==0) + our_dir = "inactive"; + else if (pj_strcmp2(&dir_attr->name, "recvonly")==0) + our_dir = "inactive"; + + if (our_dir) { + dir_attr = PJ_POOL_ZALLOC_T(pool, pjmedia_sdp_attr); + dir_attr->name = pj_str((char*)our_dir); + m->attr[m->attr_count++] = dir_attr; + } + } + + /* Remove ICE attributes */ + find_remove_sdp_attrs(&m->attr_count, m->attr, PJ_ARRAY_SIZE(ice_attrs), ice_attrs); + + /* Done */ + c = m->conn ? m->conn : answer->conn; + PJ_LOG(3,(THIS_FILE, " Media %d, %.*s: %s <--> %.*s:%d", + mi, (int)m->desc.media.slen, m->desc.media.ptr, + (our_dir ? our_dir : "sendrecv"), + (int)c->addr.slen, c->addr.ptr, m->desc.port)); + } + + return answer; +} + +static void call_on_state_changed( pjsip_inv_session *inv, + pjsip_event *e) +{ + call_t *call = (call_t*)inv->mod_data[mod_sipecho.id]; + if (!call) + return; + + PJ_UNUSED_ARG(e); + if (inv->state == PJSIP_INV_STATE_DISCONNECTED) { + PJ_LOG(3,(THIS_FILE, "Call %d: DISCONNECTED [reason=%d (%s)]", + call - app.call, inv->cause, + pjsip_get_status_text(inv->cause)->ptr)); + destroy_call(call); + } else { + PJ_LOG(3,(THIS_FILE, "Call %d: state changed to %s", + call - app.call, pjsip_inv_state_name(inv->state))); + } +} + +static void call_on_rx_offer(pjsip_inv_session *inv, const pjmedia_sdp_session *offer) +{ + call_t *call = (call_t*) inv->mod_data[mod_sipecho.id]; + pjsip_inv_set_sdp_answer(inv, create_answer(call - app.call, inv->pool_prov, offer)); +} + +static void call_on_forked(pjsip_inv_session *inv, pjsip_event *e) +{ + PJ_UNUSED_ARG(inv); + PJ_UNUSED_ARG(e); +} + +static pj_bool_t on_rx_request( pjsip_rx_data *rdata ) +{ + pj_sockaddr hostaddr; + char temp[80], hostip[PJ_INET6_ADDRSTRLEN]; + pj_str_t local_uri; + pjsip_dialog *dlg; + pjsip_rdata_sdp_info *sdp_info; + pjmedia_sdp_session *answer = NULL; + pjsip_tx_data *tdata = NULL; + call_t *call = NULL; + unsigned i; + pj_status_t status; + + PJ_LOG(3,(THIS_FILE, "RX %.*s from %s", + (int)rdata->msg_info.msg->line.req.method.name.slen, + rdata->msg_info.msg->line.req.method.name.ptr, + rdata->pkt_info.src_name)); + + if (rdata->msg_info.msg->line.req.method.id == PJSIP_REGISTER_METHOD) { + /* Let me be a registrar! */ + pjsip_hdr hdr_list, *h; + pjsip_msg *msg; + int expires = -1; + + pj_list_init(&hdr_list); + msg = rdata->msg_info.msg; + h = (pjsip_hdr*)pjsip_msg_find_hdr(msg, PJSIP_H_EXPIRES, NULL); + if (h) { + expires = ((pjsip_expires_hdr*)h)->ivalue; + pj_list_push_back(&hdr_list, pjsip_hdr_clone(rdata->tp_info.pool, h)); + PJ_LOG(3,(THIS_FILE, " Expires=%d", expires)); + } + if (expires != 0) { + h = (pjsip_hdr*)pjsip_msg_find_hdr(msg, PJSIP_H_CONTACT, NULL); + if (h) + pj_list_push_back(&hdr_list, pjsip_hdr_clone(rdata->tp_info.pool, h)); + } + + pjsip_endpt_respond(app.sip_endpt, &mod_sipecho, rdata, 200, NULL, + &hdr_list, NULL, NULL); + return PJ_TRUE; + } + + if (rdata->msg_info.msg->line.req.method.id != PJSIP_INVITE_METHOD) { + if (rdata->msg_info.msg->line.req.method.id != PJSIP_ACK_METHOD) { + pj_str_t reason = pj_str("Go away"); + pjsip_endpt_respond_stateless( app.sip_endpt, rdata, + 400, &reason, + NULL, NULL); + } + return PJ_TRUE; + } + + sdp_info = pjsip_rdata_get_sdp_info(rdata); + if (!sdp_info || !sdp_info->sdp) { + pj_str_t reason = pj_str("Require valid offer"); + pjsip_endpt_respond_stateless( app.sip_endpt, rdata, + 400, &reason, + NULL, NULL); + } + + for (i=0; i<MAX_CALLS; ++i) { + if (app.call[i].inv == NULL) { + call = &app.call[i]; + break; + } + } + + if (i==MAX_CALLS) { + pj_str_t reason = pj_str("We're full"); + pjsip_endpt_respond_stateless( app.sip_endpt, rdata, + PJSIP_SC_BUSY_HERE, &reason, + NULL, NULL); + return PJ_TRUE; + } + + /* Generate Contact URI */ + status = pj_gethostip(sip_af, &hostaddr); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to retrieve local host IP", status); + return PJ_TRUE; + } + pj_sockaddr_print(&hostaddr, hostip, sizeof(hostip), 2); + pj_ansi_sprintf(temp, "<sip:sipecho@%s:%d>", hostip, sip_port); + local_uri = pj_str(temp); + + status = pjsip_dlg_create_uas( pjsip_ua_instance(), rdata, + &local_uri, &dlg); + + if (status == PJ_SUCCESS) + answer = create_answer(call-app.call, dlg->pool, sdp_info->sdp); + if (status == PJ_SUCCESS) + status = pjsip_inv_create_uas( dlg, rdata, answer, 0, &call->inv); + if (status == PJ_SUCCESS) + status = pjsip_inv_initial_answer(call->inv, rdata, 100, + NULL, NULL, &tdata); + if (status == PJ_SUCCESS) + status = pjsip_inv_send_msg(call->inv, tdata); + + if (status == PJ_SUCCESS) + status = pjsip_inv_answer(call->inv, 180, NULL, + NULL, &tdata); + if (status == PJ_SUCCESS) + status = pjsip_inv_send_msg(call->inv, tdata); + + if (status == PJ_SUCCESS) + status = pjsip_inv_answer(call->inv, 200, NULL, + NULL, &tdata); + if (status == PJ_SUCCESS) + status = pjsip_inv_send_msg(call->inv, tdata); + + if (status != PJ_SUCCESS) { + pjsip_endpt_respond_stateless( app.sip_endpt, rdata, + 500, NULL, NULL, NULL); + destroy_call(call); + } else { + call->inv->mod_data[mod_sipecho.id] = call; + } + + return PJ_TRUE; +} + +static void call_on_media_update( pjsip_inv_session *inv, + pj_status_t status) +{ + PJ_UNUSED_ARG(inv); + PJ_UNUSED_ARG(status); +} + + +static void usage() +{ + printf("\nUsage: sipecho OPTIONS\n"); + printf("\n"); + printf("where OPTIONS:\n"); + printf(" --local-port, -p PORT Bind to port PORT.\n"); + printf(" --tcp, -t Listen to TCP instead.\n"); + printf(" --ipv6, -6 Use IPv6 instead.\n"); + printf(" --help, -h Show this help page.\n"); +} + +/* main() + * + * If called with argument, treat argument as SIP URL to be called. + * Otherwise wait for incoming calls. + */ +int main(int argc, char *argv[]) +{ + struct pj_getopt_option long_options[] = { + { "local-port", 1, 0, 'p' }, + { "tcp", 0, 0, 't' }, + { "ipv6", 0, 0, '6' }, + { "help", 0, 0, 'h' } + }; + int c, option_index; + + pj_log_set_level(5); + + pj_init(); + + sip_af = pj_AF_INET(); + + pj_optind = 0; + while ((c = pj_getopt_long(argc, argv, "p:t6h", long_options, + &option_index)) != -1) + { + switch (c) { + case 'p': + sip_port = atoi(pj_optarg); + break; + case 't': + sip_tcp = PJ_TRUE; + break; + case 'h': + usage(); + return 0; + case '6': + sip_af = pj_AF_INET6(); + break; + default: + PJ_LOG(1,(THIS_FILE, + "Argument \"%s\" is not valid. Use --help to see help", + argv[pj_optind-1])); + return -1; + } + } + + if (init_stack()) + goto on_error; + + /* If URL is specified, then make call immediately. */ + if (pj_optind != argc) { + pj_sockaddr hostaddr; + char hostip[PJ_INET6_ADDRSTRLEN+2]; + char temp[80]; + call_t *call; + pj_str_t dst_uri = pj_str(argv[pj_optind]); + pj_str_t local_uri; + pjsip_dialog *dlg; + pj_status_t status; + pjsip_tx_data *tdata; + + if (pj_gethostip(sip_af, &hostaddr) != PJ_SUCCESS) { + PJ_LOG(1,(THIS_FILE, "Unable to retrieve local host IP")); + goto on_error; + } + pj_sockaddr_print(&hostaddr, hostip, sizeof(hostip), 2); + + pj_ansi_sprintf(temp, "<sip:sipecho@%s:%d>", + hostip, sip_port); + local_uri = pj_str(temp); + + call = &app.call[0]; + + status = pjsip_dlg_create_uac( pjsip_ua_instance(), + &local_uri, /* local URI */ + &local_uri, /* local Contact */ + &dst_uri, /* remote URI */ + &dst_uri, /* remote target */ + &dlg); /* dialog */ + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to create UAC dialog", status); + return 1; + } + + status = pjsip_inv_create_uac( dlg, NULL, 0, &call->inv); + if (status != PJ_SUCCESS) goto on_error; + + call->inv->mod_data[mod_sipecho.id] = call; + + status = pjsip_inv_invite(call->inv, &tdata); + if (status != PJ_SUCCESS) goto on_error; + + status = pjsip_inv_send_msg(call->inv, tdata); + if (status != PJ_SUCCESS) goto on_error; + + puts("Press ENTER to quit..."); + } else { + puts("Ready for incoming calls. Press ENTER to quit..."); + } + + for (;;) { + char s[10]; + + printf("\nMenu:\n" + " h Hangup all calls\n" + " l %s message logging\n" + " q Quit\n", + (app.enable_msg_logging? "Disable" : "Enable")); + + if (fgets(s, sizeof(s), stdin) == NULL) + continue; + + if (s[0]=='q') + break; + switch (s[0]) { + case 'l': + app.enable_msg_logging = !app.enable_msg_logging; + break; + case 'h': + hangup_all(); + break; + } + } + + destroy_stack(); + + puts("Bye bye.."); + return 0; + +on_error: + puts("An error has occurred. run a debugger.."); + return 1; +} + diff --git a/pjsip-apps/src/samples/siprtp.c b/pjsip-apps/src/samples/siprtp.c new file mode 100644 index 0000000..2bd478a --- /dev/null +++ b/pjsip-apps/src/samples/siprtp.c @@ -0,0 +1,2189 @@ +/* $Id: siprtp.c 3553 2011-05-05 06:14:19Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 + */ + + + + + +/* Usage */ +static const char *USAGE = +" PURPOSE: \n" +" This program establishes SIP INVITE session and media, and calculate \n" +" the media quality (packet lost, jitter, rtt, etc.). Unlike normal \n" +" pjmedia applications, this program bypasses all pjmedia stream \n" +" framework and transmit encoded RTP packets manually using own thread. \n" +"\n" +" USAGE:\n" +" siprtp [options] => to start in server mode\n" +" siprtp [options] URL => to start in client mode\n" +"\n" +" Program options:\n" +" --count=N, -c Set number of calls to create (default:1) \n" +" --gap=N -g Set call gapping to N msec (default:0)\n" +" --duration=SEC, -d Set maximum call duration (default:unlimited) \n" +" --auto-quit, -q Quit when calls have been completed (default:no)\n" +" --call-report -R Display report on call termination (default:yes)\n" +"\n" +" Address and ports options:\n" +" --local-port=PORT,-p Set local SIP port (default: 5060)\n" +" --rtp-port=PORT, -r Set start of RTP port (default: 4000)\n" +" --ip-addr=IP, -i Set local IP address to use (otherwise it will\n" +" try to determine local IP address from hostname)\n" +"\n" +" Logging Options:\n" +" --log-level=N, -l Set log verbosity level (default=5)\n" +" --app-log-level=N Set app screen log verbosity (default=3)\n" +" --log-file=FILE Write log to file FILE\n" +" --report-file=FILE Write report to file FILE\n" +"\n" +/* Don't support this anymore, because codec is properly examined in + pjmedia_session_info_from_sdp() function. + +" Codec Options:\n" +" --a-pt=PT Set audio payload type to PT (default=0)\n" +" --a-name=NAME Set audio codec name to NAME (default=pcmu)\n" +" --a-clock=RATE Set audio codec rate to RATE Hz (default=8000Hz)\n" +" --a-bitrate=BPS Set audio codec bitrate to BPS (default=64000bps)\n" +" --a-ptime=MS Set audio frame time to MS msec (default=20ms)\n" +*/ +; + + +/* Include all headers. */ +#include <pjsip.h> +#include <pjmedia.h> +#include <pjmedia-codec.h> +#include <pjsip_ua.h> +#include <pjsip_simple.h> +#include <pjlib-util.h> +#include <pjlib.h> + +#include <stdlib.h> + +/* Uncomment these to disable threads. + * NOTE: + * when threading is disabled, siprtp won't transmit any + * RTP packets. + */ +/* +#undef PJ_HAS_THREADS +#define PJ_HAS_THREADS 0 +*/ + + +#if PJ_HAS_HIGH_RES_TIMER==0 +# error "High resolution timer is needed for this sample" +#endif + +#define THIS_FILE "siprtp.c" +#define MAX_CALLS 1024 +#define RTP_START_PORT 4000 + + +/* Codec descriptor: */ +struct codec +{ + unsigned pt; + char* name; + unsigned clock_rate; + unsigned bit_rate; + unsigned ptime; + char* description; +}; + + +/* A bidirectional media stream created when the call is active. */ +struct media_stream +{ + /* Static: */ + unsigned call_index; /* Call owner. */ + unsigned media_index; /* Media index in call. */ + pjmedia_transport *transport; /* To send/recv RTP/RTCP */ + + /* Active? */ + pj_bool_t active; /* Non-zero if is in call. */ + + /* Current stream info: */ + pjmedia_stream_info si; /* Current stream info. */ + + /* More info: */ + unsigned clock_rate; /* clock rate */ + unsigned samples_per_frame; /* samples per frame */ + unsigned bytes_per_frame; /* frame size. */ + + /* RTP session: */ + pjmedia_rtp_session out_sess; /* outgoing RTP session */ + pjmedia_rtp_session in_sess; /* incoming RTP session */ + + /* RTCP stats: */ + pjmedia_rtcp_session rtcp; /* incoming RTCP session. */ + + /* Thread: */ + pj_bool_t thread_quit_flag; /* Stop media thread. */ + pj_thread_t *thread; /* Media thread. */ +}; + + +/* This is a call structure that is created when the application starts + * and only destroyed when the application quits. + */ +struct call +{ + unsigned index; + pjsip_inv_session *inv; + unsigned media_count; + struct media_stream media[1]; + pj_time_val start_time; + pj_time_val response_time; + pj_time_val connect_time; + + pj_timer_entry d_timer; /**< Disconnect timer. */ +}; + + +/* Application's global variables */ +static struct app +{ + unsigned max_calls; + unsigned call_gap; + pj_bool_t call_report; + unsigned uac_calls; + unsigned duration; + pj_bool_t auto_quit; + unsigned thread_count; + int sip_port; + int rtp_start_port; + pj_str_t local_addr; + pj_str_t local_uri; + pj_str_t local_contact; + + int app_log_level; + int log_level; + char *log_filename; + char *report_filename; + + struct codec audio_codec; + + pj_str_t uri_to_call; + + pj_caching_pool cp; + pj_pool_t *pool; + + pjsip_endpoint *sip_endpt; + pj_bool_t thread_quit; + pj_thread_t *sip_thread[1]; + + pjmedia_endpt *med_endpt; + struct call call[MAX_CALLS]; +} app; + + + +/* + * Prototypes: + */ + +/* Callback to be called when SDP negotiation is done in the call: */ +static void call_on_media_update( pjsip_inv_session *inv, + pj_status_t status); + +/* Callback to be called when invite session's state has changed: */ +static void call_on_state_changed( pjsip_inv_session *inv, + pjsip_event *e); + +/* Callback to be called when dialog has forked: */ +static void call_on_forked(pjsip_inv_session *inv, pjsip_event *e); + +/* Callback to be called to handle incoming requests outside dialogs: */ +static pj_bool_t on_rx_request( pjsip_rx_data *rdata ); + +/* Worker thread prototype */ +static int sip_worker_thread(void *arg); + +/* Create SDP for call */ +static pj_status_t create_sdp( pj_pool_t *pool, + struct call *call, + pjmedia_sdp_session **p_sdp); + +/* Hangup call */ +static void hangup_call(unsigned index); + +/* Destroy the call's media */ +static void destroy_call_media(unsigned call_index); + +/* Destroy media. */ +static void destroy_media(); + +/* This callback is called by media transport on receipt of RTP packet. */ +static void on_rx_rtp(void *user_data, void *pkt, pj_ssize_t size); + +/* This callback is called by media transport on receipt of RTCP packet. */ +static void on_rx_rtcp(void *user_data, void *pkt, pj_ssize_t size); + +/* Display error */ +static void app_perror(const char *sender, const char *title, + pj_status_t status); + +/* Print call */ +static void print_call(int call_index); + + +/* This is a PJSIP module to be registered by application to handle + * incoming requests outside any dialogs/transactions. The main purpose + * here is to handle incoming INVITE request message, where we will + * create a dialog and INVITE session for it. + */ +static pjsip_module mod_siprtp = +{ + NULL, NULL, /* prev, next. */ + { "mod-siprtpapp", 13 }, /* Name. */ + -1, /* Id */ + PJSIP_MOD_PRIORITY_APPLICATION, /* Priority */ + NULL, /* load() */ + NULL, /* start() */ + NULL, /* stop() */ + NULL, /* unload() */ + &on_rx_request, /* on_rx_request() */ + NULL, /* on_rx_response() */ + NULL, /* on_tx_request. */ + NULL, /* on_tx_response() */ + NULL, /* on_tsx_state() */ +}; + + +/* Codec constants */ +struct codec audio_codecs[] = +{ + { 0, "PCMU", 8000, 64000, 20, "G.711 ULaw" }, + { 3, "GSM", 8000, 13200, 20, "GSM" }, + { 4, "G723", 8000, 6400, 30, "G.723.1" }, + { 8, "PCMA", 8000, 64000, 20, "G.711 ALaw" }, + { 18, "G729", 8000, 8000, 20, "G.729" }, +}; + + +/* + * Init SIP stack + */ +static pj_status_t init_sip() +{ + unsigned i; + pj_status_t status; + + /* init PJLIB-UTIL: */ + status = pjlib_util_init(); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); + + /* Must create a pool factory before we can allocate any memory. */ + pj_caching_pool_init(&app.cp, &pj_pool_factory_default_policy, 0); + + /* Create application pool for misc. */ + app.pool = pj_pool_create(&app.cp.factory, "app", 1000, 1000, NULL); + + /* Create the endpoint: */ + status = pjsip_endpt_create(&app.cp.factory, pj_gethostname()->ptr, + &app.sip_endpt); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); + + + /* Add UDP transport. */ + { + pj_sockaddr_in addr; + pjsip_host_port addrname; + pjsip_transport *tp; + + pj_bzero(&addr, sizeof(addr)); + addr.sin_family = pj_AF_INET(); + addr.sin_addr.s_addr = 0; + addr.sin_port = pj_htons((pj_uint16_t)app.sip_port); + + if (app.local_addr.slen) { + + addrname.host = app.local_addr; + addrname.port = app.sip_port; + + status = pj_sockaddr_in_init(&addr, &app.local_addr, + (pj_uint16_t)app.sip_port); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to resolve IP interface", status); + return status; + } + } + + status = pjsip_udp_transport_start( app.sip_endpt, &addr, + (app.local_addr.slen ? &addrname:NULL), + 1, &tp); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to start UDP transport", status); + return status; + } + + PJ_LOG(3,(THIS_FILE, "SIP UDP listening on %.*s:%d", + (int)tp->local_name.host.slen, tp->local_name.host.ptr, + tp->local_name.port)); + } + + /* + * Init transaction layer. + * This will create/initialize transaction hash tables etc. + */ + status = pjsip_tsx_layer_init_module(app.sip_endpt); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); + + /* Initialize UA layer. */ + status = pjsip_ua_init_module( app.sip_endpt, NULL ); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); + + /* Initialize 100rel support */ + status = pjsip_100rel_init_module(app.sip_endpt); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); + + /* Init invite session module. */ + { + pjsip_inv_callback inv_cb; + + /* Init the callback for INVITE session: */ + pj_bzero(&inv_cb, sizeof(inv_cb)); + inv_cb.on_state_changed = &call_on_state_changed; + inv_cb.on_new_session = &call_on_forked; + inv_cb.on_media_update = &call_on_media_update; + + /* Initialize invite session module: */ + status = pjsip_inv_usage_init(app.sip_endpt, &inv_cb); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + } + + /* Register our module to receive incoming requests. */ + status = pjsip_endpt_register_module( app.sip_endpt, &mod_siprtp); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); + + /* Init calls */ + for (i=0; i<app.max_calls; ++i) + app.call[i].index = i; + + /* Done */ + return PJ_SUCCESS; +} + + +/* + * Destroy SIP + */ +static void destroy_sip() +{ + unsigned i; + + app.thread_quit = 1; + for (i=0; i<app.thread_count; ++i) { + if (app.sip_thread[i]) { + pj_thread_join(app.sip_thread[i]); + pj_thread_destroy(app.sip_thread[i]); + app.sip_thread[i] = NULL; + } + } + + if (app.sip_endpt) { + pjsip_endpt_destroy(app.sip_endpt); + app.sip_endpt = NULL; + } + +} + + +/* + * Init media stack. + */ +static pj_status_t init_media() +{ + unsigned i, count; + pj_uint16_t rtp_port; + pj_status_t status; + + + /* Initialize media endpoint so that at least error subsystem is properly + * initialized. + */ +#if PJ_HAS_THREADS + status = pjmedia_endpt_create(&app.cp.factory, NULL, 1, &app.med_endpt); +#else + status = pjmedia_endpt_create(&app.cp.factory, + pjsip_endpt_get_ioqueue(app.sip_endpt), + 0, &app.med_endpt); +#endif + PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); + + + /* Must register codecs to be supported */ +#if defined(PJMEDIA_HAS_G711_CODEC) && PJMEDIA_HAS_G711_CODEC!=0 + pjmedia_codec_g711_init(app.med_endpt); +#endif + + /* RTP port counter */ + rtp_port = (pj_uint16_t)(app.rtp_start_port & 0xFFFE); + + /* Init media transport for all calls. */ + for (i=0, count=0; i<app.max_calls; ++i, ++count) { + + unsigned j; + + /* Create transport for each media in the call */ + for (j=0; j<PJ_ARRAY_SIZE(app.call[0].media); ++j) { + /* Repeat binding media socket to next port when fails to bind + * to current port number. + */ + int retry; + + app.call[i].media[j].call_index = i; + app.call[i].media[j].media_index = j; + + status = -1; + for (retry=0; retry<100; ++retry,rtp_port+=2) { + struct media_stream *m = &app.call[i].media[j]; + + status = pjmedia_transport_udp_create2(app.med_endpt, + "siprtp", + &app.local_addr, + rtp_port, 0, + &m->transport); + if (status == PJ_SUCCESS) { + rtp_port += 2; + break; + } + } + } + + if (status != PJ_SUCCESS) + goto on_error; + } + + /* Done */ + return PJ_SUCCESS; + +on_error: + destroy_media(); + return status; +} + + +/* + * Destroy media. + */ +static void destroy_media() +{ + unsigned i; + + for (i=0; i<app.max_calls; ++i) { + unsigned j; + for (j=0; j<PJ_ARRAY_SIZE(app.call[0].media); ++j) { + struct media_stream *m = &app.call[i].media[j]; + + if (m->transport) { + pjmedia_transport_close(m->transport); + m->transport = NULL; + } + } + } + + if (app.med_endpt) { + pjmedia_endpt_destroy(app.med_endpt); + app.med_endpt = NULL; + } +} + + +/* + * Make outgoing call. + */ +static pj_status_t make_call(const pj_str_t *dst_uri) +{ + unsigned i; + struct call *call; + pjsip_dialog *dlg; + pjmedia_sdp_session *sdp; + pjsip_tx_data *tdata; + pj_status_t status; + + + /* Find unused call slot */ + for (i=0; i<app.max_calls; ++i) { + if (app.call[i].inv == NULL) + break; + } + + if (i == app.max_calls) + return PJ_ETOOMANY; + + call = &app.call[i]; + + /* Create UAC dialog */ + status = pjsip_dlg_create_uac( pjsip_ua_instance(), + &app.local_uri, /* local URI */ + &app.local_contact, /* local Contact */ + dst_uri, /* remote URI */ + dst_uri, /* remote target */ + &dlg); /* dialog */ + if (status != PJ_SUCCESS) { + ++app.uac_calls; + return status; + } + + /* Create SDP */ + create_sdp( dlg->pool, call, &sdp); + + /* Create the INVITE session. */ + status = pjsip_inv_create_uac( dlg, sdp, 0, &call->inv); + if (status != PJ_SUCCESS) { + pjsip_dlg_terminate(dlg); + ++app.uac_calls; + return status; + } + + + /* Attach call data to invite session */ + call->inv->mod_data[mod_siprtp.id] = call; + + /* Mark start of call */ + pj_gettimeofday(&call->start_time); + + + /* Create initial INVITE request. + * This INVITE request will contain a perfectly good request and + * an SDP body as well. + */ + status = pjsip_inv_invite(call->inv, &tdata); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); + + + /* Send initial INVITE request. + * From now on, the invite session's state will be reported to us + * via the invite session callbacks. + */ + status = pjsip_inv_send_msg(call->inv, tdata); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); + + + return PJ_SUCCESS; +} + + +/* + * Receive incoming call + */ +static void process_incoming_call(pjsip_rx_data *rdata) +{ + unsigned i, options; + struct call *call; + pjsip_dialog *dlg; + pjmedia_sdp_session *sdp; + pjsip_tx_data *tdata; + pj_status_t status; + + /* Find free call slot */ + for (i=0; i<app.max_calls; ++i) { + if (app.call[i].inv == NULL) + break; + } + + if (i == app.max_calls) { + const pj_str_t reason = pj_str("Too many calls"); + pjsip_endpt_respond_stateless( app.sip_endpt, rdata, + 500, &reason, + NULL, NULL); + return; + } + + call = &app.call[i]; + + /* Verify that we can handle the request. */ + options = 0; + status = pjsip_inv_verify_request(rdata, &options, NULL, NULL, + app.sip_endpt, &tdata); + if (status != PJ_SUCCESS) { + /* + * No we can't handle the incoming INVITE request. + */ + if (tdata) { + pjsip_response_addr res_addr; + + pjsip_get_response_addr(tdata->pool, rdata, &res_addr); + pjsip_endpt_send_response(app.sip_endpt, &res_addr, tdata, + NULL, NULL); + + } else { + + /* Respond with 500 (Internal Server Error) */ + pjsip_endpt_respond_stateless(app.sip_endpt, rdata, 500, NULL, + NULL, NULL); + } + + return; + } + + /* Create UAS dialog */ + status = pjsip_dlg_create_uas( pjsip_ua_instance(), rdata, + &app.local_contact, &dlg); + if (status != PJ_SUCCESS) { + const pj_str_t reason = pj_str("Unable to create dialog"); + pjsip_endpt_respond_stateless( app.sip_endpt, rdata, + 500, &reason, + NULL, NULL); + return; + } + + /* Create SDP */ + create_sdp( dlg->pool, call, &sdp); + + /* Create UAS invite session */ + status = pjsip_inv_create_uas( dlg, rdata, sdp, 0, &call->inv); + if (status != PJ_SUCCESS) { + pjsip_dlg_create_response(dlg, rdata, 500, NULL, &tdata); + pjsip_dlg_send_response(dlg, pjsip_rdata_get_tsx(rdata), tdata); + return; + } + + + /* Attach call data to invite session */ + call->inv->mod_data[mod_siprtp.id] = call; + + /* Mark start of call */ + pj_gettimeofday(&call->start_time); + + + + /* Create 200 response .*/ + status = pjsip_inv_initial_answer(call->inv, rdata, 200, + NULL, NULL, &tdata); + if (status != PJ_SUCCESS) { + status = pjsip_inv_initial_answer(call->inv, rdata, + PJSIP_SC_NOT_ACCEPTABLE, + NULL, NULL, &tdata); + if (status == PJ_SUCCESS) + pjsip_inv_send_msg(call->inv, tdata); + else + pjsip_inv_terminate(call->inv, 500, PJ_FALSE); + return; + } + + + /* Send the 200 response. */ + status = pjsip_inv_send_msg(call->inv, tdata); + PJ_ASSERT_ON_FAIL(status == PJ_SUCCESS, return); + + + /* Done */ +} + + +/* Callback to be called when dialog has forked: */ +static void call_on_forked(pjsip_inv_session *inv, pjsip_event *e) +{ + PJ_UNUSED_ARG(inv); + PJ_UNUSED_ARG(e); + + PJ_TODO( HANDLE_FORKING ); +} + + +/* Callback to be called to handle incoming requests outside dialogs: */ +static pj_bool_t on_rx_request( pjsip_rx_data *rdata ) +{ + /* Ignore strandled ACKs (must not send respone */ + if (rdata->msg_info.msg->line.req.method.id == PJSIP_ACK_METHOD) + return PJ_FALSE; + + /* Respond (statelessly) any non-INVITE requests with 500 */ + if (rdata->msg_info.msg->line.req.method.id != PJSIP_INVITE_METHOD) { + pj_str_t reason = pj_str("Unsupported Operation"); + pjsip_endpt_respond_stateless( app.sip_endpt, rdata, + 500, &reason, + NULL, NULL); + return PJ_TRUE; + } + + /* Handle incoming INVITE */ + process_incoming_call(rdata); + + /* Done */ + return PJ_TRUE; +} + + +/* Callback timer to disconnect call (limiting call duration) */ +static void timer_disconnect_call( pj_timer_heap_t *timer_heap, + struct pj_timer_entry *entry) +{ + struct call *call = entry->user_data; + + PJ_UNUSED_ARG(timer_heap); + + entry->id = 0; + hangup_call(call->index); +} + + +/* Callback to be called when invite session's state has changed: */ +static void call_on_state_changed( pjsip_inv_session *inv, + pjsip_event *e) +{ + struct call *call = inv->mod_data[mod_siprtp.id]; + + PJ_UNUSED_ARG(e); + + if (!call) + return; + + if (inv->state == PJSIP_INV_STATE_DISCONNECTED) { + + pj_time_val null_time = {0, 0}; + + if (call->d_timer.id != 0) { + pjsip_endpt_cancel_timer(app.sip_endpt, &call->d_timer); + call->d_timer.id = 0; + } + + PJ_LOG(3,(THIS_FILE, "Call #%d disconnected. Reason=%d (%.*s)", + call->index, + inv->cause, + (int)inv->cause_text.slen, + inv->cause_text.ptr)); + + if (app.call_report) { + PJ_LOG(3,(THIS_FILE, "Call #%d statistics:", call->index)); + print_call(call->index); + } + + + call->inv = NULL; + inv->mod_data[mod_siprtp.id] = NULL; + + destroy_call_media(call->index); + + call->start_time = null_time; + call->response_time = null_time; + call->connect_time = null_time; + + ++app.uac_calls; + + } else if (inv->state == PJSIP_INV_STATE_CONFIRMED) { + + pj_time_val t; + + pj_gettimeofday(&call->connect_time); + if (call->response_time.sec == 0) + call->response_time = call->connect_time; + + t = call->connect_time; + PJ_TIME_VAL_SUB(t, call->start_time); + + PJ_LOG(3,(THIS_FILE, "Call #%d connected in %d ms", call->index, + PJ_TIME_VAL_MSEC(t))); + + if (app.duration != 0) { + call->d_timer.id = 1; + call->d_timer.user_data = call; + call->d_timer.cb = &timer_disconnect_call; + + t.sec = app.duration; + t.msec = 0; + + pjsip_endpt_schedule_timer(app.sip_endpt, &call->d_timer, &t); + } + + } else if ( inv->state == PJSIP_INV_STATE_EARLY || + inv->state == PJSIP_INV_STATE_CONNECTING) { + + if (call->response_time.sec == 0) + pj_gettimeofday(&call->response_time); + + } +} + + +/* Utility */ +static void app_perror(const char *sender, const char *title, + pj_status_t status) +{ + char errmsg[PJ_ERR_MSG_SIZE]; + + pj_strerror(status, errmsg, sizeof(errmsg)); + PJ_LOG(3,(sender, "%s: %s [status=%d]", title, errmsg, status)); +} + + +/* Worker thread for SIP */ +static int sip_worker_thread(void *arg) +{ + PJ_UNUSED_ARG(arg); + + while (!app.thread_quit) { + pj_time_val timeout = {0, 10}; + pjsip_endpt_handle_events(app.sip_endpt, &timeout); + } + + return 0; +} + + +/* Init application options */ +static pj_status_t init_options(int argc, char *argv[]) +{ + static char ip_addr[32]; + static char local_uri[64]; + + enum { OPT_START, + OPT_APP_LOG_LEVEL, OPT_LOG_FILE, + OPT_A_PT, OPT_A_NAME, OPT_A_CLOCK, OPT_A_BITRATE, OPT_A_PTIME, + OPT_REPORT_FILE }; + + struct pj_getopt_option long_options[] = { + { "count", 1, 0, 'c' }, + { "gap", 1, 0, 'g' }, + { "call-report", 0, 0, 'R' }, + { "duration", 1, 0, 'd' }, + { "auto-quit", 0, 0, 'q' }, + { "local-port", 1, 0, 'p' }, + { "rtp-port", 1, 0, 'r' }, + { "ip-addr", 1, 0, 'i' }, + + { "log-level", 1, 0, 'l' }, + { "app-log-level", 1, 0, OPT_APP_LOG_LEVEL }, + { "log-file", 1, 0, OPT_LOG_FILE }, + + { "report-file", 1, 0, OPT_REPORT_FILE }, + + /* Don't support this anymore, see comments in USAGE above. + { "a-pt", 1, 0, OPT_A_PT }, + { "a-name", 1, 0, OPT_A_NAME }, + { "a-clock", 1, 0, OPT_A_CLOCK }, + { "a-bitrate", 1, 0, OPT_A_BITRATE }, + { "a-ptime", 1, 0, OPT_A_PTIME }, + */ + + { NULL, 0, 0, 0 }, + }; + int c; + int option_index; + + /* Get local IP address for the default IP address */ + { + const pj_str_t *hostname; + pj_sockaddr_in tmp_addr; + char *addr; + + hostname = pj_gethostname(); + pj_sockaddr_in_init(&tmp_addr, hostname, 0); + addr = pj_inet_ntoa(tmp_addr.sin_addr); + pj_ansi_strcpy(ip_addr, addr); + } + + /* Init defaults */ + app.max_calls = 1; + app.thread_count = 1; + app.sip_port = 5060; + app.rtp_start_port = RTP_START_PORT; + app.local_addr = pj_str(ip_addr); + app.log_level = 5; + app.app_log_level = 3; + app.log_filename = NULL; + + /* Default codecs: */ + app.audio_codec = audio_codecs[0]; + + /* Parse options */ + pj_optind = 0; + while((c=pj_getopt_long(argc,argv, "c:d:p:r:i:l:g:qR", + long_options, &option_index))!=-1) + { + switch (c) { + case 'c': + app.max_calls = atoi(pj_optarg); + if (app.max_calls < 0 || app.max_calls > MAX_CALLS) { + PJ_LOG(3,(THIS_FILE, "Invalid max calls value %s", pj_optarg)); + return 1; + } + break; + case 'g': + app.call_gap = atoi(pj_optarg); + break; + case 'R': + app.call_report = PJ_TRUE; + break; + case 'd': + app.duration = atoi(pj_optarg); + break; + case 'q': + app.auto_quit = 1; + break; + + case 'p': + app.sip_port = atoi(pj_optarg); + break; + case 'r': + app.rtp_start_port = atoi(pj_optarg); + break; + case 'i': + app.local_addr = pj_str(pj_optarg); + break; + + case 'l': + app.log_level = atoi(pj_optarg); + break; + case OPT_APP_LOG_LEVEL: + app.app_log_level = atoi(pj_optarg); + break; + case OPT_LOG_FILE: + app.log_filename = pj_optarg; + break; + + case OPT_A_PT: + app.audio_codec.pt = atoi(pj_optarg); + break; + case OPT_A_NAME: + app.audio_codec.name = pj_optarg; + break; + case OPT_A_CLOCK: + app.audio_codec.clock_rate = atoi(pj_optarg); + break; + case OPT_A_BITRATE: + app.audio_codec.bit_rate = atoi(pj_optarg); + break; + case OPT_A_PTIME: + app.audio_codec.ptime = atoi(pj_optarg); + break; + case OPT_REPORT_FILE: + app.report_filename = pj_optarg; + break; + + default: + puts(USAGE); + return 1; + } + } + + /* Check if URL is specified */ + if (pj_optind < argc) + app.uri_to_call = pj_str(argv[pj_optind]); + + /* Build local URI and contact */ + pj_ansi_sprintf( local_uri, "sip:%s:%d", app.local_addr.ptr, app.sip_port); + app.local_uri = pj_str(local_uri); + app.local_contact = app.local_uri; + + + return PJ_SUCCESS; +} + + +/***************************************************************************** + * MEDIA STUFFS + */ + +/* + * Create SDP session for a call. + */ +static pj_status_t create_sdp( pj_pool_t *pool, + struct call *call, + pjmedia_sdp_session **p_sdp) +{ + pj_time_val tv; + pjmedia_sdp_session *sdp; + pjmedia_sdp_media *m; + pjmedia_sdp_attr *attr; + pjmedia_transport_info tpinfo; + struct media_stream *audio = &call->media[0]; + + PJ_ASSERT_RETURN(pool && p_sdp, PJ_EINVAL); + + + /* Get transport info */ + pjmedia_transport_info_init(&tpinfo); + pjmedia_transport_get_info(audio->transport, &tpinfo); + + /* Create and initialize basic SDP session */ + sdp = pj_pool_zalloc (pool, sizeof(pjmedia_sdp_session)); + + pj_gettimeofday(&tv); + sdp->origin.user = pj_str("pjsip-siprtp"); + sdp->origin.version = sdp->origin.id = tv.sec + 2208988800UL; + sdp->origin.net_type = pj_str("IN"); + sdp->origin.addr_type = pj_str("IP4"); + sdp->origin.addr = *pj_gethostname(); + sdp->name = pj_str("pjsip"); + + /* Since we only support one media stream at present, put the + * SDP connection line in the session level. + */ + sdp->conn = pj_pool_zalloc (pool, sizeof(pjmedia_sdp_conn)); + sdp->conn->net_type = pj_str("IN"); + sdp->conn->addr_type = pj_str("IP4"); + sdp->conn->addr = app.local_addr; + + + /* SDP time and attributes. */ + sdp->time.start = sdp->time.stop = 0; + sdp->attr_count = 0; + + /* Create media stream 0: */ + + sdp->media_count = 1; + m = pj_pool_zalloc (pool, sizeof(pjmedia_sdp_media)); + sdp->media[0] = m; + + /* Standard media info: */ + m->desc.media = pj_str("audio"); + m->desc.port = pj_ntohs(tpinfo.sock_info.rtp_addr_name.ipv4.sin_port); + m->desc.port_count = 1; + m->desc.transport = pj_str("RTP/AVP"); + + /* Add format and rtpmap for each codec. */ + m->desc.fmt_count = 1; + m->attr_count = 0; + + { + pjmedia_sdp_rtpmap rtpmap; + pjmedia_sdp_attr *attr; + char ptstr[10]; + + sprintf(ptstr, "%d", app.audio_codec.pt); + pj_strdup2(pool, &m->desc.fmt[0], ptstr); + rtpmap.pt = m->desc.fmt[0]; + rtpmap.clock_rate = app.audio_codec.clock_rate; + rtpmap.enc_name = pj_str(app.audio_codec.name); + rtpmap.param.slen = 0; + + pjmedia_sdp_rtpmap_to_attr(pool, &rtpmap, &attr); + m->attr[m->attr_count++] = attr; + } + + /* Add sendrecv attribute. */ + attr = pj_pool_zalloc(pool, sizeof(pjmedia_sdp_attr)); + attr->name = pj_str("sendrecv"); + m->attr[m->attr_count++] = attr; + +#if 1 + /* + * Add support telephony event + */ + m->desc.fmt[m->desc.fmt_count++] = pj_str("121"); + /* Add rtpmap. */ + attr = pj_pool_zalloc(pool, sizeof(pjmedia_sdp_attr)); + attr->name = pj_str("rtpmap"); + attr->value = pj_str("121 telephone-event/8000"); + m->attr[m->attr_count++] = attr; + /* Add fmtp */ + attr = pj_pool_zalloc(pool, sizeof(pjmedia_sdp_attr)); + attr->name = pj_str("fmtp"); + attr->value = pj_str("121 0-15"); + m->attr[m->attr_count++] = attr; +#endif + + /* Done */ + *p_sdp = sdp; + + return PJ_SUCCESS; +} + + +#if defined(PJ_WIN32) && PJ_WIN32 != 0 +#include <windows.h> +static void boost_priority(void) +{ + SetPriorityClass( GetCurrentProcess(), REALTIME_PRIORITY_CLASS); + SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST); +} + +#elif defined(PJ_LINUX) && PJ_LINUX != 0 +#include <pthread.h> +static void boost_priority(void) +{ +#define POLICY SCHED_FIFO + struct sched_param tp; + int max_prio; + int policy; + int rc; + + if (sched_get_priority_min(POLICY) < sched_get_priority_max(POLICY)) + max_prio = sched_get_priority_max(POLICY)-1; + else + max_prio = sched_get_priority_max(POLICY)+1; + + /* + * Adjust process scheduling algorithm and priority + */ + rc = sched_getparam(0, &tp); + if (rc != 0) { + app_perror( THIS_FILE, "sched_getparam error", + PJ_RETURN_OS_ERROR(rc)); + return; + } + tp.__sched_priority = max_prio; + + rc = sched_setscheduler(0, POLICY, &tp); + if (rc != 0) { + app_perror( THIS_FILE, "sched_setscheduler error", + PJ_RETURN_OS_ERROR(rc)); + } + + PJ_LOG(4, (THIS_FILE, "New process policy=%d, priority=%d", + policy, tp.__sched_priority)); + + /* + * Adjust thread scheduling algorithm and priority + */ + rc = pthread_getschedparam(pthread_self(), &policy, &tp); + if (rc != 0) { + app_perror( THIS_FILE, "pthread_getschedparam error", + PJ_RETURN_OS_ERROR(rc)); + return; + } + + PJ_LOG(4, (THIS_FILE, "Old thread policy=%d, priority=%d", + policy, tp.__sched_priority)); + + policy = POLICY; + tp.__sched_priority = max_prio; + + rc = pthread_setschedparam(pthread_self(), policy, &tp); + if (rc != 0) { + app_perror( THIS_FILE, "pthread_setschedparam error", + PJ_RETURN_OS_ERROR(rc)); + return; + } + + PJ_LOG(4, (THIS_FILE, "New thread policy=%d, priority=%d", + policy, tp.__sched_priority)); +} + +#else +# define boost_priority() +#endif + + +/* + * This callback is called by media transport on receipt of RTP packet. + */ +static void on_rx_rtp(void *user_data, void *pkt, pj_ssize_t size) +{ + struct media_stream *strm; + pj_status_t status; + const pjmedia_rtp_hdr *hdr; + const void *payload; + unsigned payload_len; + + strm = user_data; + + /* Discard packet if media is inactive */ + if (!strm->active) + return; + + /* Check for errors */ + if (size < 0) { + app_perror(THIS_FILE, "RTP recv() error", -size); + return; + } + + /* Decode RTP packet. */ + status = pjmedia_rtp_decode_rtp(&strm->in_sess, + pkt, size, + &hdr, &payload, &payload_len); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "RTP decode error", status); + return; + } + + //PJ_LOG(4,(THIS_FILE, "Rx seq=%d", pj_ntohs(hdr->seq))); + + /* Update the RTCP session. */ + pjmedia_rtcp_rx_rtp(&strm->rtcp, pj_ntohs(hdr->seq), + pj_ntohl(hdr->ts), payload_len); + + /* Update RTP session */ + pjmedia_rtp_session_update(&strm->in_sess, hdr, NULL); + +} + +/* + * This callback is called by media transport on receipt of RTCP packet. + */ +static void on_rx_rtcp(void *user_data, void *pkt, pj_ssize_t size) +{ + struct media_stream *strm; + + strm = user_data; + + /* Discard packet if media is inactive */ + if (!strm->active) + return; + + /* Check for errors */ + if (size < 0) { + app_perror(THIS_FILE, "Error receiving RTCP packet", -size); + return; + } + + /* Update RTCP session */ + pjmedia_rtcp_rx_rtcp(&strm->rtcp, pkt, size); +} + + +/* + * Media thread + * + * This is the thread to send and receive both RTP and RTCP packets. + */ +static int media_thread(void *arg) +{ + enum { RTCP_INTERVAL = 5000, RTCP_RAND = 2000 }; + struct media_stream *strm = arg; + char packet[1500]; + unsigned msec_interval; + pj_timestamp freq, next_rtp, next_rtcp; + + + /* Boost thread priority if necessary */ + boost_priority(); + + /* Let things settle */ + pj_thread_sleep(100); + + msec_interval = strm->samples_per_frame * 1000 / strm->clock_rate; + pj_get_timestamp_freq(&freq); + + pj_get_timestamp(&next_rtp); + next_rtp.u64 += (freq.u64 * msec_interval / 1000); + + next_rtcp = next_rtp; + next_rtcp.u64 += (freq.u64 * (RTCP_INTERVAL+(pj_rand()%RTCP_RAND)) / 1000); + + + while (!strm->thread_quit_flag) { + pj_timestamp now, lesser; + pj_time_val timeout; + pj_bool_t send_rtp, send_rtcp; + + send_rtp = send_rtcp = PJ_FALSE; + + /* Determine how long to sleep */ + if (next_rtp.u64 < next_rtcp.u64) { + lesser = next_rtp; + send_rtp = PJ_TRUE; + } else { + lesser = next_rtcp; + send_rtcp = PJ_TRUE; + } + + pj_get_timestamp(&now); + if (lesser.u64 <= now.u64) { + timeout.sec = timeout.msec = 0; + //printf("immediate "); fflush(stdout); + } else { + pj_uint64_t tick_delay; + tick_delay = lesser.u64 - now.u64; + timeout.sec = 0; + timeout.msec = (pj_uint32_t)(tick_delay * 1000 / freq.u64); + pj_time_val_normalize(&timeout); + + //printf("%d:%03d ", timeout.sec, timeout.msec); fflush(stdout); + } + + /* Wait for next interval */ + //if (timeout.sec!=0 && timeout.msec!=0) { + pj_thread_sleep(PJ_TIME_VAL_MSEC(timeout)); + if (strm->thread_quit_flag) + break; + //} + + pj_get_timestamp(&now); + + if (send_rtp || next_rtp.u64 <= now.u64) { + /* + * Time to send RTP packet. + */ + pj_status_t status; + const void *p_hdr; + const pjmedia_rtp_hdr *hdr; + pj_ssize_t size; + int hdrlen; + + /* Format RTP header */ + status = pjmedia_rtp_encode_rtp( &strm->out_sess, strm->si.tx_pt, + 0, /* marker bit */ + strm->bytes_per_frame, + strm->samples_per_frame, + &p_hdr, &hdrlen); + if (status == PJ_SUCCESS) { + + //PJ_LOG(4,(THIS_FILE, "\t\tTx seq=%d", pj_ntohs(hdr->seq))); + + hdr = (const pjmedia_rtp_hdr*) p_hdr; + + /* Copy RTP header to packet */ + pj_memcpy(packet, hdr, hdrlen); + + /* Zero the payload */ + pj_bzero(packet+hdrlen, strm->bytes_per_frame); + + /* Send RTP packet */ + size = hdrlen + strm->bytes_per_frame; + status = pjmedia_transport_send_rtp(strm->transport, + packet, size); + if (status != PJ_SUCCESS) + app_perror(THIS_FILE, "Error sending RTP packet", status); + + } else { + pj_assert(!"RTP encode() error"); + } + + /* Update RTCP SR */ + pjmedia_rtcp_tx_rtp( &strm->rtcp, (pj_uint16_t)strm->bytes_per_frame); + + /* Schedule next send */ + next_rtp.u64 += (msec_interval * freq.u64 / 1000); + } + + + if (send_rtcp || next_rtcp.u64 <= now.u64) { + /* + * Time to send RTCP packet. + */ + void *rtcp_pkt; + int rtcp_len; + pj_ssize_t size; + pj_status_t status; + + /* Build RTCP packet */ + pjmedia_rtcp_build_rtcp(&strm->rtcp, &rtcp_pkt, &rtcp_len); + + + /* Send packet */ + size = rtcp_len; + status = pjmedia_transport_send_rtcp(strm->transport, + rtcp_pkt, size); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Error sending RTCP packet", status); + } + + /* Schedule next send */ + next_rtcp.u64 += (freq.u64 * (RTCP_INTERVAL+(pj_rand()%RTCP_RAND)) / + 1000); + } + } + + return 0; +} + + +/* Callback to be called when SDP negotiation is done in the call: */ +static void call_on_media_update( pjsip_inv_session *inv, + pj_status_t status) +{ + struct call *call; + pj_pool_t *pool; + struct media_stream *audio; + const pjmedia_sdp_session *local_sdp, *remote_sdp; + struct codec *codec_desc = NULL; + unsigned i; + + call = inv->mod_data[mod_siprtp.id]; + pool = inv->dlg->pool; + audio = &call->media[0]; + + /* If this is a mid-call media update, then destroy existing media */ + if (audio->thread != NULL) + destroy_call_media(call->index); + + + /* Do nothing if media negotiation has failed */ + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "SDP negotiation failed", status); + return; + } + + + /* Capture stream definition from the SDP */ + pjmedia_sdp_neg_get_active_local(inv->neg, &local_sdp); + pjmedia_sdp_neg_get_active_remote(inv->neg, &remote_sdp); + + status = pjmedia_stream_info_from_sdp(&audio->si, inv->pool, app.med_endpt, + local_sdp, remote_sdp, 0); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Error creating stream info from SDP", status); + return; + } + + /* Get the remainder of codec information from codec descriptor */ + if (audio->si.fmt.pt == app.audio_codec.pt) + codec_desc = &app.audio_codec; + else { + /* Find the codec description in codec array */ + for (i=0; i<PJ_ARRAY_SIZE(audio_codecs); ++i) { + if (audio_codecs[i].pt == audio->si.fmt.pt) { + codec_desc = &audio_codecs[i]; + break; + } + } + + if (codec_desc == NULL) { + PJ_LOG(3, (THIS_FILE, "Error: Invalid codec payload type")); + return; + } + } + + audio->clock_rate = audio->si.fmt.clock_rate; + audio->samples_per_frame = audio->clock_rate * codec_desc->ptime / 1000; + audio->bytes_per_frame = codec_desc->bit_rate * codec_desc->ptime / 1000 / 8; + + + pjmedia_rtp_session_init(&audio->out_sess, audio->si.tx_pt, + pj_rand()); + pjmedia_rtp_session_init(&audio->in_sess, audio->si.fmt.pt, 0); + pjmedia_rtcp_init(&audio->rtcp, "rtcp", audio->clock_rate, + audio->samples_per_frame, 0); + + + /* Attach media to transport */ + status = pjmedia_transport_attach(audio->transport, audio, + &audio->si.rem_addr, + &audio->si.rem_rtcp, + sizeof(pj_sockaddr_in), + &on_rx_rtp, + &on_rx_rtcp); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Error on pjmedia_transport_attach()", status); + return; + } + + /* Start media thread. */ + audio->thread_quit_flag = 0; +#if PJ_HAS_THREADS + status = pj_thread_create( inv->pool, "media", &media_thread, audio, + 0, 0, &audio->thread); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Error creating media thread", status); + return; + } +#endif + + /* Set the media as active */ + audio->active = PJ_TRUE; +} + + + +/* Destroy call's media */ +static void destroy_call_media(unsigned call_index) +{ + struct media_stream *audio = &app.call[call_index].media[0]; + + if (audio) { + audio->active = PJ_FALSE; + + if (audio->thread) { + audio->thread_quit_flag = 1; + pj_thread_join(audio->thread); + pj_thread_destroy(audio->thread); + audio->thread = NULL; + audio->thread_quit_flag = 0; + } + + pjmedia_transport_detach(audio->transport, audio); + } +} + + +/***************************************************************************** + * USER INTERFACE STUFFS + */ + +static void call_get_duration(int call_index, pj_time_val *dur) +{ + struct call *call = &app.call[call_index]; + pjsip_inv_session *inv; + + dur->sec = dur->msec = 0; + + if (!call) + return; + + inv = call->inv; + if (!inv) + return; + + if (inv->state >= PJSIP_INV_STATE_CONFIRMED && call->connect_time.sec) { + + pj_gettimeofday(dur); + PJ_TIME_VAL_SUB((*dur), call->connect_time); + } +} + + +static 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.%02dK", + val / 1000, + (val % 1000) / 100); + } else { + pj_ansi_sprintf(buf, "%d.%02dM", + val / 1000000, + (val % 1000000) / 10000); + } + + return buf; +} + + + +static void print_avg_stat(void) +{ +#define MIN_(var,val) if ((int)val < (int)var) var = val +#define MAX_(var,val) if ((int)val > (int)var) var = val +#define AVG_(var,val) var = ( ((var * count) + val) / (count+1) ) +#define BIGVAL 0x7FFFFFFFL + struct stat_entry + { + int min, avg, max; + }; + + struct stat_entry call_dur, call_pdd; + pjmedia_rtcp_stat min_stat, avg_stat, max_stat; + + char srx_min[16], srx_avg[16], srx_max[16]; + char brx_min[16], brx_avg[16], brx_max[16]; + char stx_min[16], stx_avg[16], stx_max[16]; + char btx_min[16], btx_avg[16], btx_max[16]; + + + unsigned i, count; + + pj_bzero(&call_dur, sizeof(call_dur)); + call_dur.min = BIGVAL; + + pj_bzero(&call_pdd, sizeof(call_pdd)); + call_pdd.min = BIGVAL; + + pj_bzero(&min_stat, sizeof(min_stat)); + min_stat.rx.pkt = min_stat.tx.pkt = BIGVAL; + min_stat.rx.bytes = min_stat.tx.bytes = BIGVAL; + min_stat.rx.loss = min_stat.tx.loss = BIGVAL; + min_stat.rx.dup = min_stat.tx.dup = BIGVAL; + min_stat.rx.reorder = min_stat.tx.reorder = BIGVAL; + min_stat.rx.jitter.min = min_stat.tx.jitter.min = BIGVAL; + min_stat.rtt.min = BIGVAL; + + pj_bzero(&avg_stat, sizeof(avg_stat)); + pj_bzero(&max_stat, sizeof(max_stat)); + + + for (i=0, count=0; i<app.max_calls; ++i) { + + struct call *call = &app.call[i]; + struct media_stream *audio = &call->media[0]; + pj_time_val dur; + unsigned msec_dur; + + if (call->inv == NULL || + call->inv->state < PJSIP_INV_STATE_CONFIRMED || + call->connect_time.sec == 0) + { + continue; + } + + /* Duration */ + call_get_duration(i, &dur); + msec_dur = PJ_TIME_VAL_MSEC(dur); + + MIN_(call_dur.min, msec_dur); + MAX_(call_dur.max, msec_dur); + AVG_(call_dur.avg, msec_dur); + + /* Connect delay */ + if (call->connect_time.sec) { + pj_time_val t = call->connect_time; + PJ_TIME_VAL_SUB(t, call->start_time); + msec_dur = PJ_TIME_VAL_MSEC(t); + } else { + msec_dur = 10; + } + + MIN_(call_pdd.min, msec_dur); + MAX_(call_pdd.max, msec_dur); + AVG_(call_pdd.avg, msec_dur); + + /* RX Statistisc: */ + + /* Packets */ + MIN_(min_stat.rx.pkt, audio->rtcp.stat.rx.pkt); + MAX_(max_stat.rx.pkt, audio->rtcp.stat.rx.pkt); + AVG_(avg_stat.rx.pkt, audio->rtcp.stat.rx.pkt); + + /* Bytes */ + MIN_(min_stat.rx.bytes, audio->rtcp.stat.rx.bytes); + MAX_(max_stat.rx.bytes, audio->rtcp.stat.rx.bytes); + AVG_(avg_stat.rx.bytes, audio->rtcp.stat.rx.bytes); + + + /* Packet loss */ + MIN_(min_stat.rx.loss, audio->rtcp.stat.rx.loss); + MAX_(max_stat.rx.loss, audio->rtcp.stat.rx.loss); + AVG_(avg_stat.rx.loss, audio->rtcp.stat.rx.loss); + + /* Packet dup */ + MIN_(min_stat.rx.dup, audio->rtcp.stat.rx.dup); + MAX_(max_stat.rx.dup, audio->rtcp.stat.rx.dup); + AVG_(avg_stat.rx.dup, audio->rtcp.stat.rx.dup); + + /* Packet reorder */ + MIN_(min_stat.rx.reorder, audio->rtcp.stat.rx.reorder); + MAX_(max_stat.rx.reorder, audio->rtcp.stat.rx.reorder); + AVG_(avg_stat.rx.reorder, audio->rtcp.stat.rx.reorder); + + /* Jitter */ + MIN_(min_stat.rx.jitter.min, audio->rtcp.stat.rx.jitter.min); + MAX_(max_stat.rx.jitter.max, audio->rtcp.stat.rx.jitter.max); + AVG_(avg_stat.rx.jitter.mean, audio->rtcp.stat.rx.jitter.mean); + + + /* TX Statistisc: */ + + /* Packets */ + MIN_(min_stat.tx.pkt, audio->rtcp.stat.tx.pkt); + MAX_(max_stat.tx.pkt, audio->rtcp.stat.tx.pkt); + AVG_(avg_stat.tx.pkt, audio->rtcp.stat.tx.pkt); + + /* Bytes */ + MIN_(min_stat.tx.bytes, audio->rtcp.stat.tx.bytes); + MAX_(max_stat.tx.bytes, audio->rtcp.stat.tx.bytes); + AVG_(avg_stat.tx.bytes, audio->rtcp.stat.tx.bytes); + + /* Packet loss */ + MIN_(min_stat.tx.loss, audio->rtcp.stat.tx.loss); + MAX_(max_stat.tx.loss, audio->rtcp.stat.tx.loss); + AVG_(avg_stat.tx.loss, audio->rtcp.stat.tx.loss); + + /* Packet dup */ + MIN_(min_stat.tx.dup, audio->rtcp.stat.tx.dup); + MAX_(max_stat.tx.dup, audio->rtcp.stat.tx.dup); + AVG_(avg_stat.tx.dup, audio->rtcp.stat.tx.dup); + + /* Packet reorder */ + MIN_(min_stat.tx.reorder, audio->rtcp.stat.tx.reorder); + MAX_(max_stat.tx.reorder, audio->rtcp.stat.tx.reorder); + AVG_(avg_stat.tx.reorder, audio->rtcp.stat.tx.reorder); + + /* Jitter */ + MIN_(min_stat.tx.jitter.min, audio->rtcp.stat.tx.jitter.min); + MAX_(max_stat.tx.jitter.max, audio->rtcp.stat.tx.jitter.max); + AVG_(avg_stat.tx.jitter.mean, audio->rtcp.stat.tx.jitter.mean); + + + /* RTT */ + MIN_(min_stat.rtt.min, audio->rtcp.stat.rtt.min); + MAX_(max_stat.rtt.max, audio->rtcp.stat.rtt.max); + AVG_(avg_stat.rtt.mean, audio->rtcp.stat.rtt.mean); + + ++count; + } + + if (count == 0) { + puts("No active calls"); + return; + } + + printf("Total %d call(s) active.\n" + " Average Statistics\n" + " min avg max \n" + " -----------------------\n" + " call duration: %7d %7d %7d %s\n" + " connect delay: %7d %7d %7d %s\n" + " RX stat:\n" + " packets: %7s %7s %7s %s\n" + " payload: %7s %7s %7s %s\n" + " loss: %7d %7d %7d %s\n" + " percent loss: %7.3f %7.3f %7.3f %s\n" + " dup: %7d %7d %7d %s\n" + " reorder: %7d %7d %7d %s\n" + " jitter: %7.3f %7.3f %7.3f %s\n" + " TX stat:\n" + " packets: %7s %7s %7s %s\n" + " payload: %7s %7s %7s %s\n" + " loss: %7d %7d %7d %s\n" + " percent loss: %7.3f %7.3f %7.3f %s\n" + " dup: %7d %7d %7d %s\n" + " reorder: %7d %7d %7d %s\n" + " jitter: %7.3f %7.3f %7.3f %s\n" + " RTT : %7.3f %7.3f %7.3f %s\n" + , + count, + call_dur.min/1000, call_dur.avg/1000, call_dur.max/1000, + "seconds", + + call_pdd.min, call_pdd.avg, call_pdd.max, + "ms", + + /* rx */ + + good_number(srx_min, min_stat.rx.pkt), + good_number(srx_avg, avg_stat.rx.pkt), + good_number(srx_max, max_stat.rx.pkt), + "packets", + + good_number(brx_min, min_stat.rx.bytes), + good_number(brx_avg, avg_stat.rx.bytes), + good_number(brx_max, max_stat.rx.bytes), + "bytes", + + min_stat.rx.loss, avg_stat.rx.loss, max_stat.rx.loss, + "packets", + + min_stat.rx.loss*100.0/(min_stat.rx.pkt+min_stat.rx.loss), + avg_stat.rx.loss*100.0/(avg_stat.rx.pkt+avg_stat.rx.loss), + max_stat.rx.loss*100.0/(max_stat.rx.pkt+max_stat.rx.loss), + "%", + + + min_stat.rx.dup, avg_stat.rx.dup, max_stat.rx.dup, + "packets", + + min_stat.rx.reorder, avg_stat.rx.reorder, max_stat.rx.reorder, + "packets", + + min_stat.rx.jitter.min/1000.0, + avg_stat.rx.jitter.mean/1000.0, + max_stat.rx.jitter.max/1000.0, + "ms", + + /* tx */ + + good_number(stx_min, min_stat.tx.pkt), + good_number(stx_avg, avg_stat.tx.pkt), + good_number(stx_max, max_stat.tx.pkt), + "packets", + + good_number(btx_min, min_stat.tx.bytes), + good_number(btx_avg, avg_stat.tx.bytes), + good_number(btx_max, max_stat.tx.bytes), + "bytes", + + min_stat.tx.loss, avg_stat.tx.loss, max_stat.tx.loss, + "packets", + + min_stat.tx.loss*100.0/(min_stat.tx.pkt+min_stat.tx.loss), + avg_stat.tx.loss*100.0/(avg_stat.tx.pkt+avg_stat.tx.loss), + max_stat.tx.loss*100.0/(max_stat.tx.pkt+max_stat.tx.loss), + "%", + + min_stat.tx.dup, avg_stat.tx.dup, max_stat.tx.dup, + "packets", + + min_stat.tx.reorder, avg_stat.tx.reorder, max_stat.tx.reorder, + "packets", + + min_stat.tx.jitter.min/1000.0, + avg_stat.tx.jitter.mean/1000.0, + max_stat.tx.jitter.max/1000.0, + "ms", + + /* rtt */ + min_stat.rtt.min/1000.0, + avg_stat.rtt.mean/1000.0, + max_stat.rtt.max/1000.0, + "ms" + ); + +} + + +#include "siprtp_report.c" + + +static void list_calls() +{ + unsigned i; + puts("List all calls:"); + for (i=0; i<app.max_calls; ++i) { + if (!app.call[i].inv) + continue; + print_call(i); + } +} + +static void hangup_call(unsigned index) +{ + pjsip_tx_data *tdata; + pj_status_t status; + + if (app.call[index].inv == NULL) + return; + + status = pjsip_inv_end_session(app.call[index].inv, 603, NULL, &tdata); + if (status==PJ_SUCCESS && tdata!=NULL) + pjsip_inv_send_msg(app.call[index].inv, tdata); +} + +static void hangup_all_calls() +{ + unsigned i; + for (i=0; i<app.max_calls; ++i) { + if (!app.call[i].inv) + continue; + hangup_call(i); + pj_thread_sleep(app.call_gap); + } + + /* Wait until all calls are terminated */ + for (i=0; i<app.max_calls; ++i) { + while (app.call[i].inv) + pj_thread_sleep(10); + } +} + +static pj_bool_t simple_input(const char *title, char *buf, pj_size_t len) +{ + char *p; + + printf("%s (empty to cancel): ", title); fflush(stdout); + if (fgets(buf, len, stdin) == NULL) + return PJ_FALSE; + + /* Remove trailing newlines. */ + for (p=buf; ; ++p) { + if (*p=='\r' || *p=='\n') *p='\0'; + else if (!*p) break; + } + + if (!*buf) + return PJ_FALSE; + + return PJ_TRUE; +} + + +static const char *MENU = +"\n" +"Enter menu character:\n" +" s Summary\n" +" l List all calls\n" +" h Hangup a call\n" +" H Hangup all calls\n" +" q Quit\n" +"\n"; + + +/* Main screen menu */ +static void console_main() +{ + char input1[10]; + unsigned i; + + printf("%s", MENU); + + for (;;) { + printf(">>> "); fflush(stdout); + if (fgets(input1, sizeof(input1), stdin) == NULL) { + puts("EOF while reading stdin, will quit now.."); + break; + } + + switch (input1[0]) { + + case 's': + print_avg_stat(); + break; + + case 'l': + list_calls(); + break; + + case 'h': + if (!simple_input("Call number to hangup", input1, sizeof(input1))) + break; + + i = atoi(input1); + hangup_call(i); + break; + + case 'H': + hangup_all_calls(); + break; + + case 'q': + goto on_exit; + + default: + puts("Invalid command"); + printf("%s", MENU); + break; + } + + fflush(stdout); + } + +on_exit: + hangup_all_calls(); +} + + +/***************************************************************************** + * Below is a simple module to log all incoming and outgoing SIP messages + */ + + +/* Notification on incoming messages */ +static pj_bool_t logger_on_rx_msg(pjsip_rx_data *rdata) +{ + PJ_LOG(4,(THIS_FILE, "RX %d bytes %s from %s:%d:\n" + "%s\n" + "--end msg--", + rdata->msg_info.len, + pjsip_rx_data_get_info(rdata), + rdata->pkt_info.src_name, + rdata->pkt_info.src_port, + rdata->msg_info.msg_buf)); + + /* Always return false, otherwise messages will not get processed! */ + return PJ_FALSE; +} + +/* Notification on outgoing messages */ +static pj_status_t logger_on_tx_msg(pjsip_tx_data *tdata) +{ + + /* Important note: + * tp_info field is only valid after outgoing messages has passed + * transport layer. So don't try to access tp_info when the module + * has lower priority than transport layer. + */ + + PJ_LOG(4,(THIS_FILE, "TX %d bytes %s to %s:%d:\n" + "%s\n" + "--end msg--", + (tdata->buf.cur - tdata->buf.start), + pjsip_tx_data_get_info(tdata), + tdata->tp_info.dst_name, + tdata->tp_info.dst_port, + tdata->buf.start)); + + /* Always return success, otherwise message will not get sent! */ + return PJ_SUCCESS; +} + +/* The module instance. */ +static pjsip_module msg_logger = +{ + NULL, NULL, /* prev, next. */ + { "mod-siprtp-log", 14 }, /* Name. */ + -1, /* Id */ + PJSIP_MOD_PRIORITY_TRANSPORT_LAYER-1,/* Priority */ + NULL, /* load() */ + NULL, /* start() */ + NULL, /* stop() */ + NULL, /* unload() */ + &logger_on_rx_msg, /* on_rx_request() */ + &logger_on_rx_msg, /* on_rx_response() */ + &logger_on_tx_msg, /* on_tx_request. */ + &logger_on_tx_msg, /* on_tx_response() */ + NULL, /* on_tsx_state() */ + +}; + + + +/***************************************************************************** + * Console application custom logging: + */ + + +static FILE *log_file; + + +static void app_log_writer(int level, const char *buffer, int len) +{ + /* Write to both stdout and file. */ + + if (level <= app.app_log_level) + pj_log_write(level, buffer, len); + + if (log_file) { + int count = fwrite(buffer, len, 1, log_file); + PJ_UNUSED_ARG(count); + fflush(log_file); + } +} + + +pj_status_t app_logging_init(void) +{ + /* Redirect log function to ours */ + + pj_log_set_log_func( &app_log_writer ); + + /* If output log file is desired, create the file: */ + + if (app.log_filename) { + log_file = fopen(app.log_filename, "wt"); + if (log_file == NULL) { + PJ_LOG(1,(THIS_FILE, "Unable to open log file %s", + app.log_filename)); + return -1; + } + } + + return PJ_SUCCESS; +} + + +void app_logging_shutdown(void) +{ + /* Close logging file, if any: */ + + if (log_file) { + fclose(log_file); + log_file = NULL; + } +} + + +/* + * main() + */ +int main(int argc, char *argv[]) +{ + unsigned i; + pj_status_t status; + + /* Must init PJLIB first */ + status = pj_init(); + if (status != PJ_SUCCESS) + return 1; + + /* Get command line options */ + status = init_options(argc, argv); + if (status != PJ_SUCCESS) + return 1; + + /* Verify options: */ + + /* Auto-quit can not be specified for UAS */ + if (app.auto_quit && app.uri_to_call.slen == 0) { + printf("Error: --auto-quit option only valid for outgoing " + "mode (UAC) only\n"); + return 1; + } + + /* Init logging */ + status = app_logging_init(); + if (status != PJ_SUCCESS) + return 1; + + /* Init SIP etc */ + status = init_sip(); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Initialization has failed", status); + destroy_sip(); + return 1; + } + + /* Register module to log incoming/outgoing messages */ + pjsip_endpt_register_module(app.sip_endpt, &msg_logger); + + /* Init media */ + status = init_media(); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Media initialization failed", status); + destroy_sip(); + return 1; + } + + /* Start worker threads */ +#if PJ_HAS_THREADS + for (i=0; i<app.thread_count; ++i) { + pj_thread_create( app.pool, "app", &sip_worker_thread, NULL, + 0, 0, &app.sip_thread[i]); + } +#endif + + /* If URL is specified, then make call immediately */ + if (app.uri_to_call.slen) { + unsigned i; + + PJ_LOG(3,(THIS_FILE, "Making %d calls to %s..", app.max_calls, + app.uri_to_call.ptr)); + + for (i=0; i<app.max_calls; ++i) { + status = make_call(&app.uri_to_call); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Error making call", status); + break; + } + pj_thread_sleep(app.call_gap); + } + + if (app.auto_quit) { + /* Wait for calls to complete */ + while (app.uac_calls < app.max_calls) + pj_thread_sleep(100); + pj_thread_sleep(200); + } else { +#if PJ_HAS_THREADS + /* Start user interface loop */ + console_main(); +#endif + } + + } else { + + PJ_LOG(3,(THIS_FILE, "Ready for incoming calls (max=%d)", + app.max_calls)); + +#if PJ_HAS_THREADS + /* Start user interface loop */ + console_main(); +#endif + } + +#if !PJ_HAS_THREADS + PJ_LOG(3,(THIS_FILE, "Press Ctrl-C to quit")); + for (;;) { + pj_time_val t = {0, 10}; + pjsip_endpt_handle_events(app.sip_endpt, &t); + } +#endif + + /* Shutting down... */ + destroy_sip(); + destroy_media(); + + if (app.pool) { + pj_pool_release(app.pool); + app.pool = NULL; + pj_caching_pool_destroy(&app.cp); + } + + app_logging_shutdown(); + + /* Shutdown PJLIB */ + pj_shutdown(); + + return 0; +} + diff --git a/pjsip-apps/src/samples/siprtp_report.c b/pjsip-apps/src/samples/siprtp_report.c new file mode 100644 index 0000000..6643f9d --- /dev/null +++ b/pjsip-apps/src/samples/siprtp_report.c @@ -0,0 +1,231 @@ +/* $Id: siprtp_report.c 3553 2011-05-05 06:14:19Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 + */ + + +/* + * DO NOT COMPILE THIS FILE ON ITS OWN! + * + * This file is included by siprtp.c to implement the reporting capability + * to a separate file, so that user can implement different reporting + * functionality (such as writing to XML file). + */ + + +static void print_call(int call_index) +{ + struct call *call = &app.call[call_index]; + int len; + pjsip_inv_session *inv = call->inv; + pjsip_dialog *dlg = inv->dlg; + struct media_stream *audio = &call->media[0]; + char userinfo[128]; + char duration[80], last_update[80]; + char bps[16], ipbps[16], packets[16], bytes[16], ipbytes[16]; + unsigned decor; + pj_time_val now; + + + decor = pj_log_get_decor(); + pj_log_set_decor(PJ_LOG_HAS_NEWLINE); + + pj_gettimeofday(&now); + + if (app.report_filename) + puts(app.report_filename); + + /* Print duration */ + if (inv->state >= PJSIP_INV_STATE_CONFIRMED && call->connect_time.sec) { + + PJ_TIME_VAL_SUB(now, call->connect_time); + + sprintf(duration, " [duration: %02ld:%02ld:%02ld.%03ld]", + now.sec / 3600, + (now.sec % 3600) / 60, + (now.sec % 60), + now.msec); + + } else { + duration[0] = '\0'; + } + + + + /* Call number and state */ + PJ_LOG(3, (THIS_FILE, + "Call #%d: %s%s", + call_index, pjsip_inv_state_name(inv->state), + duration)); + + + + /* Call identification */ + 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'; + + PJ_LOG(3, (THIS_FILE, " %s", userinfo)); + + + if (call->inv == NULL || call->inv->state < PJSIP_INV_STATE_CONFIRMED || + call->connect_time.sec == 0) + { + pj_log_set_decor(decor); + return; + } + + + /* Signaling quality */ + { + char pdd[64], connectdelay[64]; + pj_time_val t; + + if (call->response_time.sec) { + t = call->response_time; + PJ_TIME_VAL_SUB(t, call->start_time); + sprintf(pdd, "got 1st response in %ld ms", PJ_TIME_VAL_MSEC(t)); + } else { + pdd[0] = '\0'; + } + + if (call->connect_time.sec) { + t = call->connect_time; + PJ_TIME_VAL_SUB(t, call->start_time); + sprintf(connectdelay, ", connected after: %ld ms", + PJ_TIME_VAL_MSEC(t)); + } else { + connectdelay[0] = '\0'; + } + + PJ_LOG(3, (THIS_FILE, + " Signaling quality: %s%s", pdd, connectdelay)); + } + + + PJ_LOG(3, (THIS_FILE, + " Stream #0: audio %.*s@%dHz, %dms/frame, %sB/s (%sB/s +IP hdr)", + (int)audio->si.fmt.encoding_name.slen, + audio->si.fmt.encoding_name.ptr, + audio->clock_rate, + audio->samples_per_frame * 1000 / audio->clock_rate, + good_number(bps, audio->bytes_per_frame * audio->clock_rate / audio->samples_per_frame), + good_number(ipbps, (audio->bytes_per_frame+32) * audio->clock_rate / audio->samples_per_frame))); + + if (audio->rtcp.stat.rx.update_cnt == 0) + strcpy(last_update, "never"); + else { + pj_gettimeofday(&now); + PJ_TIME_VAL_SUB(now, audio->rtcp.stat.rx.update); + sprintf(last_update, "%02ldh:%02ldm:%02ld.%03lds ago", + now.sec / 3600, + (now.sec % 3600) / 60, + now.sec % 60, + now.msec); + } + + PJ_LOG(3, (THIS_FILE, + " RX stat last update: %s\n" + " total %s packets %sB received (%sB +IP hdr)%s\n" + " pkt loss=%d (%3.1f%%), dup=%d (%3.1f%%), reorder=%d (%3.1f%%)%s\n" + " (msec) min avg max last\n" + " loss period: %7.3f %7.3f %7.3f %7.3f%s\n" + " jitter : %7.3f %7.3f %7.3f %7.3f%s", + last_update, + good_number(packets, audio->rtcp.stat.rx.pkt), + good_number(bytes, audio->rtcp.stat.rx.bytes), + good_number(ipbytes, audio->rtcp.stat.rx.bytes + audio->rtcp.stat.rx.pkt * 32), + "", + audio->rtcp.stat.rx.loss, + audio->rtcp.stat.rx.loss * 100.0 / (audio->rtcp.stat.rx.pkt + audio->rtcp.stat.rx.loss), + audio->rtcp.stat.rx.dup, + audio->rtcp.stat.rx.dup * 100.0 / (audio->rtcp.stat.rx.pkt + audio->rtcp.stat.rx.loss), + audio->rtcp.stat.rx.reorder, + audio->rtcp.stat.rx.reorder * 100.0 / (audio->rtcp.stat.rx.pkt + audio->rtcp.stat.rx.loss), + "", + audio->rtcp.stat.rx.loss_period.min / 1000.0, + audio->rtcp.stat.rx.loss_period.mean / 1000.0, + audio->rtcp.stat.rx.loss_period.max / 1000.0, + audio->rtcp.stat.rx.loss_period.last / 1000.0, + "", + audio->rtcp.stat.rx.jitter.min / 1000.0, + audio->rtcp.stat.rx.jitter.mean / 1000.0, + audio->rtcp.stat.rx.jitter.max / 1000.0, + audio->rtcp.stat.rx.jitter.last / 1000.0, + "" + )); + + + if (audio->rtcp.stat.tx.update_cnt == 0) + strcpy(last_update, "never"); + else { + pj_gettimeofday(&now); + PJ_TIME_VAL_SUB(now, audio->rtcp.stat.tx.update); + sprintf(last_update, "%02ldh:%02ldm:%02ld.%03lds ago", + now.sec / 3600, + (now.sec % 3600) / 60, + now.sec % 60, + now.msec); + } + + PJ_LOG(3, (THIS_FILE, + " TX stat last update: %s\n" + " total %s packets %sB sent (%sB +IP hdr)%s\n" + " pkt loss=%d (%3.1f%%), dup=%d (%3.1f%%), reorder=%d (%3.1f%%)%s\n" + " (msec) min avg max last\n" + " loss period: %7.3f %7.3f %7.3f %7.3f%s\n" + " jitter : %7.3f %7.3f %7.3f %7.3f%s", + last_update, + good_number(packets, audio->rtcp.stat.tx.pkt), + good_number(bytes, audio->rtcp.stat.tx.bytes), + good_number(ipbytes, audio->rtcp.stat.tx.bytes + audio->rtcp.stat.tx.pkt * 32), + "", + audio->rtcp.stat.tx.loss, + audio->rtcp.stat.tx.loss * 100.0 / (audio->rtcp.stat.tx.pkt + audio->rtcp.stat.tx.loss), + audio->rtcp.stat.tx.dup, + audio->rtcp.stat.tx.dup * 100.0 / (audio->rtcp.stat.tx.pkt + audio->rtcp.stat.tx.loss), + audio->rtcp.stat.tx.reorder, + audio->rtcp.stat.tx.reorder * 100.0 / (audio->rtcp.stat.tx.pkt + audio->rtcp.stat.tx.loss), + "", + audio->rtcp.stat.tx.loss_period.min / 1000.0, + audio->rtcp.stat.tx.loss_period.mean / 1000.0, + audio->rtcp.stat.tx.loss_period.max / 1000.0, + audio->rtcp.stat.tx.loss_period.last / 1000.0, + "", + audio->rtcp.stat.tx.jitter.min / 1000.0, + audio->rtcp.stat.tx.jitter.mean / 1000.0, + audio->rtcp.stat.tx.jitter.max / 1000.0, + audio->rtcp.stat.tx.jitter.last / 1000.0, + "" + )); + + + PJ_LOG(3, (THIS_FILE, + " RTT delay : %7.3f %7.3f %7.3f %7.3f%s\n", + audio->rtcp.stat.rtt.min / 1000.0, + audio->rtcp.stat.rtt.mean / 1000.0, + audio->rtcp.stat.rtt.max / 1000.0, + audio->rtcp.stat.rtt.last / 1000.0, + "" + )); + + pj_log_set_decor(decor); +} + diff --git a/pjsip-apps/src/samples/sipstateless.c b/pjsip-apps/src/samples/sipstateless.c new file mode 100644 index 0000000..0de89a6 --- /dev/null +++ b/pjsip-apps/src/samples/sipstateless.c @@ -0,0 +1,243 @@ +/* $Id: sipstateless.c 3553 2011-05-05 06:14:19Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 + */ + +/** + * sipcore.c + * + * A simple program to respond any incoming requests (except ACK, of course!) + * with any status code (taken from command line argument, with the default + * is 501/Not Implemented). + */ + + +/* Include all headers. */ +#include <pjsip.h> +#include <pjlib-util.h> +#include <pjlib.h> + + +/* If this macro is set, UDP transport will be initialized at port 5060 */ +#define HAS_UDP_TRANSPORT + +/* If this macro is set, TCP transport will be initialized at port 5060 */ +#define HAS_TCP_TRANSPORT (1 && PJ_HAS_TCP) + +/* Log identification */ +#define THIS_FILE "sipstateless.c" + + +/* Global SIP endpoint */ +static pjsip_endpoint *sip_endpt; + +/* What response code to be sent (default is 501/Not Implemented) */ +static int code = PJSIP_SC_NOT_IMPLEMENTED; + +/* Additional header list */ +struct pjsip_hdr hdr_list; + +/* usage() */ +static void usage(void) +{ + puts("Usage:"); + puts(" sipstateless [code] [-H HDR] .."); + puts(""); + puts("Options:"); + puts(" code SIP status code to send (default: 501/Not Implemented"); + puts(" -H HDR Specify additional headers to send with response"); + puts(" This option may be specified more than once."); + puts(" Example:"); + puts(" -H 'Expires: 300' -H 'Contact: <sip:localhost>'"); +} + + +/* Callback to handle incoming requests. */ +static pj_bool_t on_rx_request( pjsip_rx_data *rdata ) +{ + /* Respond (statelessly) all incoming requests (except ACK!) + * with 501 (Not Implemented) + */ + if (rdata->msg_info.msg->line.req.method.id != PJSIP_ACK_METHOD) { + pjsip_endpt_respond_stateless( sip_endpt, rdata, + code, NULL, + &hdr_list, NULL); + } + return PJ_TRUE; +} + + + +/* + * main() + * + */ +int main(int argc, char *argv[]) +{ + pj_caching_pool cp; + pj_pool_t *pool = NULL; + pjsip_module mod_app = + { + NULL, NULL, /* prev, next. */ + { "mod-app", 7 }, /* Name. */ + -1, /* Id */ + PJSIP_MOD_PRIORITY_APPLICATION, /* Priority */ + NULL, /* load() */ + NULL, /* start() */ + NULL, /* stop() */ + NULL, /* unload() */ + &on_rx_request, /* on_rx_request() */ + NULL, /* on_rx_response() */ + NULL, /* on_tx_request. */ + NULL, /* on_tx_response() */ + NULL, /* on_tsx_state() */ + }; + int c; + pj_status_t status; + + /* Must init PJLIB first: */ + status = pj_init(); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + /* Then init PJLIB-UTIL: */ + status = pjlib_util_init(); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* Must create a pool factory before we can allocate any memory. */ + pj_caching_pool_init(&cp, &pj_pool_factory_default_policy, 0); + + /* Create global endpoint: */ + { + /* Endpoint MUST be assigned a globally unique name. + * Ideally we should put hostname or public IP address, but + * we'll just use an arbitrary name here. + */ + + /* Create the endpoint: */ + status = pjsip_endpt_create(&cp.factory, "sipstateless", + &sip_endpt); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + } + + /* Parse arguments */ + pj_optind = 0; + pj_list_init(&hdr_list); + while ((c=pj_getopt(argc, argv , "H:")) != -1) { + switch (c) { + case 'H': + if (pool == NULL) { + pool = pj_pool_create(&cp.factory, "sipstateless", 1000, + 1000, NULL); + } + + if (pool) { + char *name; + name = strtok(pj_optarg, ":"); + if (name == NULL) { + puts("Error: invalid header format"); + return 1; + } else { + char *val = strtok(NULL, "\r\n"); + pjsip_generic_string_hdr *h; + pj_str_t hname, hvalue; + + hname = pj_str(name); + hvalue = pj_str(val); + + h = pjsip_generic_string_hdr_create(pool, &hname, &hvalue); + + pj_list_push_back(&hdr_list, h); + + PJ_LOG(4,(THIS_FILE, "Header %s: %s added", name, val)); + } + } + break; + default: + puts("Error: invalid argument"); + usage(); + return 1; + } + } + + if (pj_optind != argc) { + code = atoi(argv[pj_optind]); + if (code < 200 || code > 699) { + puts("Error: invalid status code"); + usage(); + return 1; + } + } + + PJ_LOG(4,(THIS_FILE, "Returning %d to incoming requests", code)); + + + /* + * Add UDP transport, with hard-coded port + */ +#ifdef HAS_UDP_TRANSPORT + { + pj_sockaddr_in addr; + + addr.sin_family = pj_AF_INET(); + addr.sin_addr.s_addr = 0; + addr.sin_port = pj_htons(5060); + + status = pjsip_udp_transport_start( sip_endpt, &addr, NULL, 1, NULL); + if (status != PJ_SUCCESS) { + PJ_LOG(3,(THIS_FILE, + "Error starting UDP transport (port in use?)")); + return 1; + } + } +#endif + +#if HAS_TCP_TRANSPORT + /* + * Add UDP transport, with hard-coded port + */ + { + pj_sockaddr_in addr; + + addr.sin_family = pj_AF_INET(); + addr.sin_addr.s_addr = 0; + addr.sin_port = pj_htons(5060); + + status = pjsip_tcp_transport_start(sip_endpt, &addr, 1, NULL); + if (status != PJ_SUCCESS) { + PJ_LOG(3,(THIS_FILE, + "Error starting TCP transport (port in use?)")); + return 1; + } + } +#endif + + /* + * Register our module to receive incoming requests. + */ + status = pjsip_endpt_register_module( sip_endpt, &mod_app); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + /* Done. Loop forever to handle incoming events. */ + PJ_LOG(3,(THIS_FILE, "Press Ctrl-C to quit..")); + + for (;;) { + pjsip_endpt_handle_events(sip_endpt, NULL); + } +} diff --git a/pjsip-apps/src/samples/stateful_proxy.c b/pjsip-apps/src/samples/stateful_proxy.c new file mode 100644 index 0000000..e8e2c25 --- /dev/null +++ b/pjsip-apps/src/samples/stateful_proxy.c @@ -0,0 +1,587 @@ +/* $Id: stateful_proxy.c 3553 2011-05-05 06:14:19Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 + */ +#define THIS_FILE "stateful_proxy.c" + +/* Common proxy functions */ +#define STATEFUL 1 +#include "proxy.h" + + +/* + * mod_stateful_proxy is the module to receive SIP request and + * response message that is outside any transaction context. + */ +static pj_bool_t proxy_on_rx_request(pjsip_rx_data *rdata ); +static pj_bool_t proxy_on_rx_response(pjsip_rx_data *rdata ); + +static pjsip_module mod_stateful_proxy = +{ + NULL, NULL, /* prev, next. */ + { "mod-stateful-proxy", 18 }, /* Name. */ + -1, /* Id */ + PJSIP_MOD_PRIORITY_UA_PROXY_LAYER, /* Priority */ + NULL, /* load() */ + NULL, /* start() */ + NULL, /* stop() */ + NULL, /* unload() */ + &proxy_on_rx_request, /* on_rx_request() */ + &proxy_on_rx_response, /* on_rx_response() */ + NULL, /* on_tx_request. */ + NULL, /* on_tx_response() */ + NULL, /* on_tsx_state() */ +}; + + +/* + * mod_tu (tu=Transaction User) is the module to receive notification + * from transaction when the transaction state has changed. + */ +static void tu_on_tsx_state(pjsip_transaction *tsx, pjsip_event *event); + +static pjsip_module mod_tu = +{ + NULL, NULL, /* prev, next. */ + { "mod-transaction-user", 20 }, /* Name. */ + -1, /* Id */ + PJSIP_MOD_PRIORITY_APPLICATION, /* Priority */ + NULL, /* load() */ + NULL, /* start() */ + NULL, /* stop() */ + NULL, /* unload() */ + NULL, /* on_rx_request() */ + NULL, /* on_rx_response() */ + NULL, /* on_tx_request. */ + NULL, /* on_tx_response() */ + &tu_on_tsx_state, /* on_tsx_state() */ +}; + + +/* This is the data that is attached to the UAC transaction */ +struct uac_data +{ + pjsip_transaction *uas_tsx; + pj_timer_entry timer; +}; + + +/* This is the data that is attached to the UAS transaction */ +struct uas_data +{ + pjsip_transaction *uac_tsx; +}; + + + +static pj_status_t init_stateful_proxy(void) +{ + pj_status_t status; + + status = pjsip_endpt_register_module( global.endpt, &mod_stateful_proxy); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + status = pjsip_endpt_register_module( global.endpt, &mod_tu); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + return PJ_SUCCESS; +} + + +/* Callback to be called to handle new incoming requests. */ +static pj_bool_t proxy_on_rx_request( pjsip_rx_data *rdata ) +{ + pjsip_transaction *uas_tsx, *uac_tsx; + struct uac_data *uac_data; + struct uas_data *uas_data; + pjsip_tx_data *tdata; + pj_status_t status; + + if (rdata->msg_info.msg->line.req.method.id != PJSIP_CANCEL_METHOD) { + + /* Verify incoming request */ + status = proxy_verify_request(rdata); + if (status != PJ_SUCCESS) { + app_perror("RX invalid request", status); + return PJ_TRUE; + } + + /* + * Request looks sane, next clone the request to create transmit data. + */ + status = pjsip_endpt_create_request_fwd(global.endpt, rdata, NULL, + NULL, 0, &tdata); + if (status != PJ_SUCCESS) { + pjsip_endpt_respond_stateless(global.endpt, rdata, + PJSIP_SC_INTERNAL_SERVER_ERROR, + NULL, NULL, NULL); + return PJ_TRUE; + } + + + /* Process routing */ + status = proxy_process_routing(tdata); + if (status != PJ_SUCCESS) { + app_perror("Error processing route", status); + return PJ_TRUE; + } + + /* Calculate target */ + status = proxy_calculate_target(rdata, tdata); + if (status != PJ_SUCCESS) { + app_perror("Error calculating target", status); + return PJ_TRUE; + } + + /* Everything is set to forward the request. */ + + /* If this is an ACK request, forward statelessly. + * This happens if the proxy records route and this ACK + * is sent for 2xx response. An ACK that is sent for non-2xx + * final response will be absorbed by transaction layer, and + * it will not be received by on_rx_request() callback. + */ + if (tdata->msg->line.req.method.id == PJSIP_ACK_METHOD) { + status = pjsip_endpt_send_request_stateless(global.endpt, tdata, + NULL, NULL); + if (status != PJ_SUCCESS) { + app_perror("Error forwarding request", status); + return PJ_TRUE; + } + + return PJ_TRUE; + } + + /* Create UAC transaction for forwarding the request. + * Set our module as the transaction user to receive further + * events from this transaction. + */ + status = pjsip_tsx_create_uac(&mod_tu, tdata, &uac_tsx); + if (status != PJ_SUCCESS) { + pjsip_tx_data_dec_ref(tdata); + pjsip_endpt_respond_stateless(global.endpt, rdata, + PJSIP_SC_INTERNAL_SERVER_ERROR, + NULL, NULL, NULL); + return PJ_TRUE; + } + + /* Create UAS transaction to handle incoming request */ + status = pjsip_tsx_create_uas(&mod_tu, rdata, &uas_tsx); + if (status != PJ_SUCCESS) { + pjsip_tx_data_dec_ref(tdata); + pjsip_endpt_respond_stateless(global.endpt, rdata, + PJSIP_SC_INTERNAL_SERVER_ERROR, + NULL, NULL, NULL); + pjsip_tsx_terminate(uac_tsx, PJSIP_SC_INTERNAL_SERVER_ERROR); + return PJ_TRUE; + } + + /* Feed the request to the UAS transaction to drive it's state + * out of NULL state. + */ + pjsip_tsx_recv_msg(uas_tsx, rdata); + + /* Attach a data to the UAC transaction, to be used to find the + * UAS transaction when we receive response in the UAC side. + */ + uac_data = (struct uac_data*) + pj_pool_alloc(uac_tsx->pool, sizeof(struct uac_data)); + uac_data->uas_tsx = uas_tsx; + uac_tsx->mod_data[mod_tu.id] = (void*)uac_data; + + /* Attach data to the UAS transaction, to find the UAC transaction + * when cancelling INVITE request. + */ + uas_data = (struct uas_data*) + pj_pool_alloc(uas_tsx->pool, sizeof(struct uas_data)); + uas_data->uac_tsx = uac_tsx; + uas_tsx->mod_data[mod_tu.id] = (void*)uas_data; + + /* Everything is setup, forward the request */ + status = pjsip_tsx_send_msg(uac_tsx, tdata); + if (status != PJ_SUCCESS) { + pjsip_tx_data *err_res; + + /* Fail to send request, for some reason */ + + /* Destroy transmit data */ + pjsip_tx_data_dec_ref(tdata); + + /* I think UAC transaction should have been destroyed when + * it fails to send request, so no need to destroy it. + pjsip_tsx_terminate(uac_tsx, PJSIP_SC_INTERNAL_SERVER_ERROR); + */ + + /* Send 500/Internal Server Error to UAS transaction */ + pjsip_endpt_create_response(global.endpt, rdata, + 500, NULL, &err_res); + pjsip_tsx_send_msg(uas_tsx, err_res); + + return PJ_TRUE; + } + + /* Send 100/Trying if this is an INVITE */ + if (rdata->msg_info.msg->line.req.method.id == PJSIP_INVITE_METHOD) { + pjsip_tx_data *res100; + + pjsip_endpt_create_response(global.endpt, rdata, 100, NULL, + &res100); + pjsip_tsx_send_msg(uas_tsx, res100); + } + + } else { + /* This is CANCEL request */ + pjsip_transaction *invite_uas; + struct uas_data *uas_data; + pj_str_t key; + + /* Find the UAS INVITE transaction */ + pjsip_tsx_create_key(rdata->tp_info.pool, &key, PJSIP_UAS_ROLE, + pjsip_get_invite_method(), rdata); + invite_uas = pjsip_tsx_layer_find_tsx(&key, PJ_TRUE); + if (!invite_uas) { + /* Invite transaction not found, respond CANCEL with 481 */ + pjsip_endpt_respond_stateless(global.endpt, rdata, 481, NULL, + NULL, NULL); + return PJ_TRUE; + } + + /* Respond 200 OK to CANCEL */ + pjsip_endpt_respond(global.endpt, NULL, rdata, 200, NULL, NULL, + NULL, NULL); + + /* Send CANCEL to cancel the UAC transaction. + * The UAS INVITE transaction will get final response when + * we receive final response from the UAC INVITE transaction. + */ + uas_data = (struct uas_data*) invite_uas->mod_data[mod_tu.id]; + if (uas_data->uac_tsx && uas_data->uac_tsx->status_code < 200) { + pjsip_tx_data *cancel; + + pj_mutex_lock(uas_data->uac_tsx->mutex); + + pjsip_endpt_create_cancel(global.endpt, uas_data->uac_tsx->last_tx, + &cancel); + pjsip_endpt_send_request(global.endpt, cancel, -1, NULL, NULL); + + pj_mutex_unlock(uas_data->uac_tsx->mutex); + } + + /* Unlock UAS tsx because it is locked in find_tsx() */ + pj_mutex_unlock(invite_uas->mutex); + } + + return PJ_TRUE; +} + + +/* Callback to be called to handle incoming response outside + * any transactions. This happens for example when 2xx/OK + * for INVITE is received and transaction will be destroyed + * immediately, so we need to forward the subsequent 2xx/OK + * retransmission statelessly. + */ +static pj_bool_t proxy_on_rx_response( pjsip_rx_data *rdata ) +{ + pjsip_tx_data *tdata; + pjsip_response_addr res_addr; + pjsip_via_hdr *hvia; + pj_status_t status; + + /* Create response to be forwarded upstream (Via will be stripped here) */ + status = pjsip_endpt_create_response_fwd(global.endpt, rdata, 0, &tdata); + if (status != PJ_SUCCESS) { + app_perror("Error creating response", status); + return PJ_TRUE; + } + + /* Get topmost Via header */ + hvia = (pjsip_via_hdr*) pjsip_msg_find_hdr(tdata->msg, PJSIP_H_VIA, NULL); + if (hvia == NULL) { + /* Invalid response! Just drop it */ + pjsip_tx_data_dec_ref(tdata); + return PJ_TRUE; + } + + /* Calculate the address to forward the response */ + pj_bzero(&res_addr, sizeof(res_addr)); + res_addr.dst_host.type = PJSIP_TRANSPORT_UDP; + res_addr.dst_host.flag = + pjsip_transport_get_flag_from_type(PJSIP_TRANSPORT_UDP); + + /* Destination address is Via's received param */ + res_addr.dst_host.addr.host = hvia->recvd_param; + if (res_addr.dst_host.addr.host.slen == 0) { + /* Someone has messed up our Via header! */ + res_addr.dst_host.addr.host = hvia->sent_by.host; + } + + /* Destination port is the rport */ + if (hvia->rport_param != 0 && hvia->rport_param != -1) + res_addr.dst_host.addr.port = hvia->rport_param; + + if (res_addr.dst_host.addr.port == 0) { + /* Ugh, original sender didn't put rport! + * At best, can only send the response to the port in Via. + */ + res_addr.dst_host.addr.port = hvia->sent_by.port; + } + + /* Forward response */ + status = pjsip_endpt_send_response(global.endpt, &res_addr, tdata, + NULL, NULL); + if (status != PJ_SUCCESS) { + app_perror("Error forwarding response", status); + return PJ_TRUE; + } + + return PJ_TRUE; +} + + +/* Callback to be called to handle transaction state changed. */ +static void tu_on_tsx_state(pjsip_transaction *tsx, pjsip_event *event) +{ + struct uac_data *uac_data; + pj_status_t status; + + if (tsx->role == PJSIP_ROLE_UAS) { + if (tsx->state == PJSIP_TSX_STATE_TERMINATED) { + struct uas_data *uas_data; + + uas_data = (struct uas_data*) tsx->mod_data[mod_tu.id]; + if (uas_data->uac_tsx) { + uac_data = (struct uac_data*) + uas_data->uac_tsx->mod_data[mod_tu.id]; + uac_data->uas_tsx = NULL; + } + + } + return; + } + + /* Get the data that we attached to the UAC transaction previously */ + uac_data = (struct uac_data*) tsx->mod_data[mod_tu.id]; + + + /* Handle incoming response */ + if (event->body.tsx_state.type == PJSIP_EVENT_RX_MSG) { + + pjsip_rx_data *rdata; + pjsip_response_addr res_addr; + pjsip_via_hdr *hvia; + pjsip_tx_data *tdata; + + rdata = event->body.tsx_state.src.rdata; + + /* Do not forward 100 response for INVITE (we already responded + * INVITE with 100) + */ + if (tsx->method.id == PJSIP_INVITE_METHOD && + rdata->msg_info.msg->line.status.code == 100) + { + return; + } + + /* Create response to be forwarded upstream + * (Via will be stripped here) + */ + status = pjsip_endpt_create_response_fwd(global.endpt, rdata, 0, + &tdata); + if (status != PJ_SUCCESS) { + app_perror("Error creating response", status); + return; + } + + /* Get topmost Via header of the new response */ + hvia = (pjsip_via_hdr*) pjsip_msg_find_hdr(tdata->msg, PJSIP_H_VIA, + NULL); + if (hvia == NULL) { + /* Invalid response! Just drop it */ + pjsip_tx_data_dec_ref(tdata); + return; + } + + /* Calculate the address to forward the response */ + pj_bzero(&res_addr, sizeof(res_addr)); + res_addr.dst_host.type = PJSIP_TRANSPORT_UDP; + res_addr.dst_host.flag = + pjsip_transport_get_flag_from_type(PJSIP_TRANSPORT_UDP); + + /* Destination address is Via's received param */ + res_addr.dst_host.addr.host = hvia->recvd_param; + if (res_addr.dst_host.addr.host.slen == 0) { + /* Someone has messed up our Via header! */ + res_addr.dst_host.addr.host = hvia->sent_by.host; + } + + /* Destination port is the rport */ + if (hvia->rport_param != 0 && hvia->rport_param != -1) + res_addr.dst_host.addr.port = hvia->rport_param; + + if (res_addr.dst_host.addr.port == 0) { + /* Ugh, original sender didn't put rport! + * At best, can only send the response to the port in Via. + */ + res_addr.dst_host.addr.port = hvia->sent_by.port; + } + + /* Forward response with the UAS transaction */ + pjsip_tsx_send_msg(uac_data->uas_tsx, tdata); + + } + + /* If UAC transaction is terminated, terminate the UAS as well. + * This could happen because of: + * - timeout on the UAC side + * - receipt of 2xx response to INVITE + */ + if (tsx->state == PJSIP_TSX_STATE_TERMINATED && uac_data && + uac_data->uas_tsx) + { + + pjsip_transaction *uas_tsx; + struct uas_data *uas_data; + + uas_tsx = uac_data->uas_tsx; + uas_data = (struct uas_data*) uas_tsx->mod_data[mod_tu.id]; + uas_data->uac_tsx = NULL; + + if (event->body.tsx_state.type == PJSIP_EVENT_TIMER) { + + /* Send 408/Timeout if this is an INVITE transaction, since + * we must have sent provisional response before. For non + * INVITE transaction, just destroy it. + */ + if (tsx->method.id == PJSIP_INVITE_METHOD) { + + pjsip_tx_data *tdata = uas_tsx->last_tx; + + tdata->msg->line.status.code = PJSIP_SC_REQUEST_TIMEOUT; + tdata->msg->line.status.reason = pj_str("Request timed out"); + tdata->msg->body = NULL; + + pjsip_tx_data_add_ref(tdata); + pjsip_tx_data_invalidate_msg(tdata); + + pjsip_tsx_send_msg(uas_tsx, tdata); + + } else { + /* For non-INVITE, just destroy the UAS transaction */ + pjsip_tsx_terminate(uas_tsx, PJSIP_SC_REQUEST_TIMEOUT); + } + + } else if (event->body.tsx_state.type == PJSIP_EVENT_RX_MSG) { + + if (uas_tsx->state < PJSIP_TSX_STATE_TERMINATED) { + pjsip_msg *msg; + int code; + + msg = event->body.tsx_state.src.rdata->msg_info.msg; + code = msg->line.status.code; + + uac_data->uas_tsx = NULL; + pjsip_tsx_terminate(uas_tsx, code); + } + } + } +} + + +/* + * main() + */ +int main(int argc, char *argv[]) +{ + pj_status_t status; + + global.port = 5060; + global.record_route = 0; + + pj_log_set_level(4); + + status = init_options(argc, argv); + if (status != PJ_SUCCESS) + return 1; + + status = init_stack(); + if (status != PJ_SUCCESS) { + app_perror("Error initializing stack", status); + return 1; + } + + status = init_proxy(); + if (status != PJ_SUCCESS) { + app_perror("Error initializing proxy", status); + return 1; + } + + status = init_stateful_proxy(); + if (status != PJ_SUCCESS) { + app_perror("Error initializing stateful proxy", status); + return 1; + } + +#if PJ_HAS_THREADS + status = pj_thread_create(global.pool, "sproxy", &worker_thread, + NULL, 0, 0, &global.thread); + if (status != PJ_SUCCESS) { + app_perror("Error creating thread", status); + return 1; + } + + while (!global.quit_flag) { + char line[10]; + + puts("\n" + "Menu:\n" + " q quit\n" + " d dump status\n" + " dd dump detailed status\n" + ""); + + if (fgets(line, sizeof(line), stdin) == NULL) { + puts("EOF while reading stdin, will quit now.."); + global.quit_flag = PJ_TRUE; + break; + } + + if (line[0] == 'q') { + global.quit_flag = PJ_TRUE; + } else if (line[0] == 'd') { + pj_bool_t detail = (line[1] == 'd'); + pjsip_endpt_dump(global.endpt, detail); + pjsip_tsx_layer_dump(detail); + } + } + + pj_thread_join(global.thread); + +#else + puts("\nPress Ctrl-C to quit\n"); + for (;;) { + pj_time_val delay = {0, 0}; + pjsip_endpt_handle_events(global.endpt, &delay); + } +#endif + + destroy_stack(); + + return 0; +} + diff --git a/pjsip-apps/src/samples/stateless_proxy.c b/pjsip-apps/src/samples/stateless_proxy.c new file mode 100644 index 0000000..826385c --- /dev/null +++ b/pjsip-apps/src/samples/stateless_proxy.c @@ -0,0 +1,255 @@ +/* $Id: stateless_proxy.c 3553 2011-05-05 06:14:19Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 + */ +#define THIS_FILE "stateless_proxy.c" + +/* Common proxy functions */ +#define STATEFUL 0 +#include "proxy.h" + + +/* Callback to be called to handle incoming requests. */ +static pj_bool_t on_rx_request( pjsip_rx_data *rdata ); + +/* Callback to be called to handle incoming response. */ +static pj_bool_t on_rx_response( pjsip_rx_data *rdata ); + + +static pj_status_t init_stateless_proxy(void) +{ + static pjsip_module mod_stateless_proxy = + { + NULL, NULL, /* prev, next. */ + { "mod-stateless-proxy", 19 }, /* Name. */ + -1, /* Id */ + PJSIP_MOD_PRIORITY_UA_PROXY_LAYER, /* Priority */ + NULL, /* load() */ + NULL, /* start() */ + NULL, /* stop() */ + NULL, /* unload() */ + &on_rx_request, /* on_rx_request() */ + &on_rx_response, /* on_rx_response() */ + NULL, /* on_tx_request. */ + NULL, /* on_tx_response() */ + NULL, /* on_tsx_state() */ + }; + + pj_status_t status; + + /* Register our module to receive incoming requests. */ + status = pjsip_endpt_register_module( global.endpt, &mod_stateless_proxy); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + return PJ_SUCCESS; +} + + +/* Callback to be called to handle incoming requests. */ +static pj_bool_t on_rx_request( pjsip_rx_data *rdata ) +{ + pjsip_tx_data *tdata; + pj_status_t status; + + + /* Verify incoming request */ + status = proxy_verify_request(rdata); + if (status != PJ_SUCCESS) { + app_perror("RX invalid request", status); + return PJ_TRUE; + } + + /* + * Request looks sane, next clone the request to create transmit data. + */ + status = pjsip_endpt_create_request_fwd(global.endpt, rdata, NULL, + NULL, 0, &tdata); + if (status != PJ_SUCCESS) { + pjsip_endpt_respond_stateless(global.endpt, rdata, + PJSIP_SC_INTERNAL_SERVER_ERROR, NULL, + NULL, NULL); + return PJ_TRUE; + } + + + /* Process routing */ + status = proxy_process_routing(tdata); + if (status != PJ_SUCCESS) { + app_perror("Error processing route", status); + return PJ_TRUE; + } + + /* Calculate target */ + status = proxy_calculate_target(rdata, tdata); + if (status != PJ_SUCCESS) { + app_perror("Error calculating target", status); + return PJ_TRUE; + } + + /* Target is set, forward the request */ + status = pjsip_endpt_send_request_stateless(global.endpt, tdata, + NULL, NULL); + if (status != PJ_SUCCESS) { + app_perror("Error forwarding request", status); + return PJ_TRUE; + } + + return PJ_TRUE; +} + + +/* Callback to be called to handle incoming response. */ +static pj_bool_t on_rx_response( pjsip_rx_data *rdata ) +{ + pjsip_tx_data *tdata; + pjsip_response_addr res_addr; + pjsip_via_hdr *hvia; + pj_status_t status; + + /* Create response to be forwarded upstream (Via will be stripped here) */ + status = pjsip_endpt_create_response_fwd(global.endpt, rdata, 0, &tdata); + if (status != PJ_SUCCESS) { + app_perror("Error creating response", status); + return PJ_TRUE; + } + + /* Get topmost Via header */ + hvia = (pjsip_via_hdr*) pjsip_msg_find_hdr(tdata->msg, PJSIP_H_VIA, NULL); + if (hvia == NULL) { + /* Invalid response! Just drop it */ + pjsip_tx_data_dec_ref(tdata); + return PJ_TRUE; + } + + /* Calculate the address to forward the response */ + pj_bzero(&res_addr, sizeof(res_addr)); + res_addr.dst_host.type = PJSIP_TRANSPORT_UDP; + res_addr.dst_host.flag = pjsip_transport_get_flag_from_type(PJSIP_TRANSPORT_UDP); + + /* Destination address is Via's received param */ + res_addr.dst_host.addr.host = hvia->recvd_param; + if (res_addr.dst_host.addr.host.slen == 0) { + /* Someone has messed up our Via header! */ + res_addr.dst_host.addr.host = hvia->sent_by.host; + } + + /* Destination port is the rpot */ + if (hvia->rport_param != 0 && hvia->rport_param != -1) + res_addr.dst_host.addr.port = hvia->rport_param; + + if (res_addr.dst_host.addr.port == 0) { + /* Ugh, original sender didn't put rport! + * At best, can only send the response to the port in Via. + */ + res_addr.dst_host.addr.port = hvia->sent_by.port; + } + + /* Forward response */ + status = pjsip_endpt_send_response(global.endpt, &res_addr, tdata, + NULL, NULL); + if (status != PJ_SUCCESS) { + app_perror("Error forwarding response", status); + return PJ_TRUE; + } + + return PJ_TRUE; +} + + +/* + * main() + */ +int main(int argc, char *argv[]) +{ + pj_status_t status; + + global.port = 5060; + pj_log_set_level(4); + + status = init_options(argc, argv); + if (status != PJ_SUCCESS) + return 1; + + status = init_stack(); + if (status != PJ_SUCCESS) { + app_perror("Error initializing stack", status); + return 1; + } + + status = init_proxy(); + if (status != PJ_SUCCESS) { + app_perror("Error initializing proxy", status); + return 1; + } + + status = init_stateless_proxy(); + if (status != PJ_SUCCESS) { + app_perror("Error initializing stateless proxy", status); + return 1; + } + +#if PJ_HAS_THREADS + status = pj_thread_create(global.pool, "sproxy", &worker_thread, + NULL, 0, 0, &global.thread); + if (status != PJ_SUCCESS) { + app_perror("Error creating thread", status); + return 1; + } + + while (!global.quit_flag) { + char line[10]; + + puts("\n" + "Menu:\n" + " q quit\n" + " d dump status\n" + " dd dump detailed status\n" + ""); + + if (fgets(line, sizeof(line), stdin) == NULL) { + puts("EOF while reading stdin, will quit now.."); + global.quit_flag = PJ_TRUE; + break; + } + + if (line[0] == 'q') { + global.quit_flag = PJ_TRUE; + } else if (line[0] == 'd') { + pj_bool_t detail = (line[1] == 'd'); + pjsip_endpt_dump(global.endpt, detail); +#if STATEFUL + pjsip_tsx_layer_dump(detail); +#endif + } + } + + pj_thread_join(global.thread); + +#else + puts("\nPress Ctrl-C to quit\n"); + for (;;) { + pj_time_val delay = {0, 0}; + pjsip_endpt_handle_events(global.endpt, &delay); + } +#endif + + destroy_stack(); + + return 0; +} + diff --git a/pjsip-apps/src/samples/stereotest.c b/pjsip-apps/src/samples/stereotest.c new file mode 100644 index 0000000..8931270 --- /dev/null +++ b/pjsip-apps/src/samples/stereotest.c @@ -0,0 +1,336 @@ +/* $Id: stereotest.c 3664 2011-07-19 03:42:28Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 + */ + +/** + * \page page_pjmedia_samples_stereo_c Samples: Using Stereo Port + * + * This example demonstrates how to use @ref PJMEDIA_STEREO_PORT to + * change the channel count of the media streams. + * + * This file is pjsip-apps/src/samples/stereotest.c + * + * \includelineno stereotest.c + */ + +#include <pjmedia.h> +#include <pjlib-util.h> +#include <pjlib.h> + +#include <stdio.h> +#include <stdlib.h> + +#include "util.h" + +#define REC_CLOCK_RATE 16000 +#define PTIME 20 + +#define MODE_PLAY 1 +#define MODE_RECORD 2 + + +/* For logging purpose. */ +#define THIS_FILE "stereotest.c" + + +static const char *desc = +" FILE \n" +" \n" +" stereotest.c \n" +" \n" +" PURPOSE \n" +" \n" +" Demonstrate how use stereo port to play a WAV file to sound \n" +" device or record to a WAV file from sound device with different \n" +" channel count. \n" +" \n" +" USAGE \n" +" \n" +" stereotest [options] WAV \n" +" \n" +" Options: \n" +" -m, --mode=N Operation mode: 1 = playing, 2 = recording.\n" +" -C, --rec-ch-cnt=N Number of channel for recording file. \n" +" -c, --snd-ch-cnt=N Number of channel for opening sound device.\n" +" \n"; + +int main(int argc, char *argv[]) +{ + pj_caching_pool cp; + pjmedia_endpt *med_endpt; + pj_pool_t *pool; + + pjmedia_port *file_port = NULL; + pjmedia_port *stereo_port = NULL; + pjmedia_snd_port *snd_port = NULL; + + int dev_id = -1; + char tmp[10]; + pj_status_t status; + + char *wav_file = NULL; + unsigned mode = 0; + unsigned rec_ch_cnt = 1; + unsigned snd_ch_cnt = 2; + + enum { + OPT_MODE = 'm', + OPT_REC_CHANNEL = 'C', + OPT_SND_CHANNEL = 'c', + }; + + struct pj_getopt_option long_options[] = { + { "mode", 1, 0, OPT_MODE }, + { "rec-ch-cnt", 1, 0, OPT_REC_CHANNEL }, + { "snd-ch-cnt", 1, 0, OPT_SND_CHANNEL }, + { NULL, 0, 0, 0 }, + }; + + int c; + int option_index; + + /* Must init PJLIB first: */ + status = pj_init(); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* Parse arguments */ + pj_optind = 0; + while((c=pj_getopt_long(argc,argv, "m:C:c:", long_options, &option_index))!=-1) { + + switch (c) { + case OPT_MODE: + if (mode) { + app_perror(THIS_FILE, "Cannot record and play at once!", + PJ_EINVAL); + return 1; + } + mode = atoi(pj_optarg); + break; + + case OPT_REC_CHANNEL: + rec_ch_cnt = atoi(pj_optarg); + break; + + case OPT_SND_CHANNEL: + snd_ch_cnt = atoi(pj_optarg); + break; + + default: + printf("Invalid options %s\n", argv[pj_optind]); + puts(desc); + return 1; + } + + } + + wav_file = argv[pj_optind]; + + /* Verify arguments. */ + if (!wav_file) { + app_perror(THIS_FILE, "WAV file not specified!", PJ_EINVAL); + puts(desc); + return 1; + } + if (!snd_ch_cnt || !rec_ch_cnt || rec_ch_cnt > 6) { + app_perror(THIS_FILE, "Invalid or too many channel count!", PJ_EINVAL); + puts(desc); + return 1; + } + if (mode != MODE_RECORD && mode != MODE_PLAY) { + app_perror(THIS_FILE, "Invalid operation mode!", PJ_EINVAL); + puts(desc); + return 1; + } + + /* Must create a pool factory before we can allocate any memory. */ + pj_caching_pool_init(&cp, &pj_pool_factory_default_policy, 0); + + /* + * Initialize media endpoint. + * This will implicitly initialize PJMEDIA too. + */ + status = pjmedia_endpt_create(&cp.factory, NULL, 1, &med_endpt); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* Create memory pool for our file player */ + pool = pj_pool_create( &cp.factory, /* pool factory */ + "app", /* pool name. */ + 4000, /* init size */ + 4000, /* increment size */ + NULL /* callback on error */ + ); + + if (mode == MODE_PLAY) { + /* Create WAVE file player port. */ + status = pjmedia_wav_player_port_create( pool, wav_file, PTIME, 0, + 0, &file_port); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to open file", status); + return 1; + } + + /* Create sound player port. */ + status = pjmedia_snd_port_create_player( + pool, /* pool */ + dev_id, /* device id. */ + PJMEDIA_PIA_SRATE(&file_port->info),/* clock rate. */ + snd_ch_cnt, /* # of channels. */ + snd_ch_cnt * PTIME * /* samples per frame. */ + PJMEDIA_PIA_SRATE(&file_port->info) / 1000, + PJMEDIA_PIA_BITS(&file_port->info),/* bits per sample. */ + 0, /* options */ + &snd_port /* returned port */ + ); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to open sound device", status); + return 1; + } + + if (snd_ch_cnt != PJMEDIA_PIA_CCNT(&file_port->info)) { + status = pjmedia_stereo_port_create( pool, + file_port, + snd_ch_cnt, + 0, + &stereo_port); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to create stereo port", status); + return 1; + } + + status = pjmedia_snd_port_connect(snd_port, stereo_port); + } else { + status = pjmedia_snd_port_connect(snd_port, file_port); + } + + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to connect sound port", status); + return 1; + } + + } else { + /* Create WAVE file writer port. */ + status = pjmedia_wav_writer_port_create(pool, wav_file, + REC_CLOCK_RATE, + rec_ch_cnt, + rec_ch_cnt * PTIME * + REC_CLOCK_RATE / 1000, + NBITS, + 0, 0, + &file_port); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to open file", status); + return 1; + } + + /* Create sound player port. */ + status = pjmedia_snd_port_create_rec( + pool, /* pool */ + dev_id, /* device id. */ + REC_CLOCK_RATE, /* clock rate. */ + snd_ch_cnt, /* # of channels. */ + snd_ch_cnt * PTIME * + REC_CLOCK_RATE / 1000, /* samples per frame. */ + NBITS, /* bits per sample. */ + 0, /* options */ + &snd_port /* returned port */ + ); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to open sound device", status); + return 1; + } + + if (rec_ch_cnt != snd_ch_cnt) { + status = pjmedia_stereo_port_create( pool, + file_port, + snd_ch_cnt, + 0, + &stereo_port); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to create stereo port", status); + return 1; + } + + status = pjmedia_snd_port_connect(snd_port, stereo_port); + } else { + status = pjmedia_snd_port_connect(snd_port, file_port); + } + + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to connect sound port", status); + return 1; + } + } + + /* Dump memory usage */ + dump_pool_usage(THIS_FILE, &cp); + + /* + * File should be playing and looping now, using sound device's thread. + */ + + + /* Sleep to allow log messages to flush */ + pj_thread_sleep(100); + + printf("Mode = %s\n", (mode == MODE_PLAY? "playing" : "recording") ); + printf("File port channel count = %d\n", PJMEDIA_PIA_CCNT(&file_port->info)); + printf("Sound port channel count = %d\n", + PJMEDIA_PIA_CCNT(&pjmedia_snd_port_get_port(snd_port)->info)); + puts(""); + puts("Press <ENTER> to stop and quit"); + + if (fgets(tmp, sizeof(tmp), stdin) == NULL) { + puts("EOF while reading stdin, will quit now.."); + } + + /* Start deinitialization: */ + + + /* Destroy sound device */ + status = pjmedia_snd_port_destroy( snd_port ); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + /* Destroy stereo port and file_port. + * Stereo port will destroy all downstream ports (e.g. the file port) + */ + status = pjmedia_port_destroy( stereo_port? stereo_port : file_port); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + /* Release application pool */ + pj_pool_release( pool ); + + /* Destroy media endpoint. */ + pjmedia_endpt_destroy( med_endpt ); + + /* Destroy pool factory */ + pj_caching_pool_destroy( &cp ); + + /* Shutdown PJLIB */ + pj_shutdown(); + + + /* Done. */ + return 0; + +} + + + diff --git a/pjsip-apps/src/samples/streamutil.c b/pjsip-apps/src/samples/streamutil.c new file mode 100644 index 0000000..a59621b --- /dev/null +++ b/pjsip-apps/src/samples/streamutil.c @@ -0,0 +1,1174 @@ +/* $Id: streamutil.c 3664 2011-07-19 03:42:28Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 + */ + + +/** + * \page page_pjmedia_samples_streamutil_c Samples: Remote Streaming + * + * This example mainly demonstrates how to stream media file to remote + * peer using RTP. + * + * This file is pjsip-apps/src/samples/streamutil.c + * + * \includelineno streamutil.c + */ + +#include <pjlib.h> +#include <pjlib-util.h> +#include <pjmedia.h> +#include <pjmedia-codec.h> +#include <pjmedia/transport_srtp.h> + +#include <stdlib.h> /* atoi() */ +#include <stdio.h> + +#include "util.h" + + +static const char *desc = + " streamutil \n" + " \n" + " PURPOSE: \n" + " Demonstrate how to use pjmedia stream component to transmit/receive \n" + " RTP packets to/from sound device. \n" + "\n" + "\n" + " USAGE: \n" + " streamutil [options] \n" + "\n" + "\n" + " Options:\n" + " --codec=CODEC Set the codec name. \n" + " --local-port=PORT Set local RTP port (default=4000) \n" + " --remote=IP:PORT Set the remote peer. If this option is set, \n" + " the program will transmit RTP audio to the \n" + " specified address. (default: recv only) \n" + " --play-file=WAV Send audio from the WAV file instead of from \n" + " the sound device. \n" + " --record-file=WAV Record incoming audio to WAV file instead of \n" + " playing it to sound device. \n" + " --send-recv Set stream direction to bidirectional. \n" + " --send-only Set stream direction to send only \n" + " --recv-only Set stream direction to recv only (default) \n" + +#if defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0) + " --use-srtp[=NAME] Enable SRTP with crypto suite NAME \n" + " e.g: AES_CM_128_HMAC_SHA1_80 (default), \n" + " AES_CM_128_HMAC_SHA1_32 \n" + " Use this option along with the TX & RX keys, \n" + " formated of 60 hex digits (e.g: E148DA..) \n" + " --srtp-tx-key SRTP key for transmiting \n" + " --srtp-rx-key SRTP key for receiving \n" +#endif + + "\n" +; + + + + +#define THIS_FILE "stream.c" + + + +/* Prototype */ +static void print_stream_stat(pjmedia_stream *stream, + const pjmedia_codec_param *codec_param); + +/* Prototype for LIBSRTP utility in file datatypes.c */ +int hex_string_to_octet_string(char *raw, char *hex, int len); + +/* + * Register all codecs. + */ +static pj_status_t init_codecs(pjmedia_endpt *med_endpt) +{ + return pjmedia_codec_register_audio_codecs(med_endpt, NULL); +} + + +/* + * Create stream based on the codec, dir, remote address, etc. + */ +static pj_status_t create_stream( pj_pool_t *pool, + pjmedia_endpt *med_endpt, + const pjmedia_codec_info *codec_info, + pjmedia_dir dir, + pj_uint16_t local_port, + const pj_sockaddr_in *rem_addr, +#if defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0) + pj_bool_t use_srtp, + const pj_str_t *crypto_suite, + const pj_str_t *srtp_tx_key, + const pj_str_t *srtp_rx_key, +#endif + pjmedia_stream **p_stream ) +{ + pjmedia_stream_info info; + pjmedia_transport *transport = NULL; + pj_status_t status; +#if defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0) + pjmedia_transport *srtp_tp = NULL; +#endif + + + /* Reset stream info. */ + pj_bzero(&info, sizeof(info)); + + + /* Initialize stream info formats */ + info.type = PJMEDIA_TYPE_AUDIO; + info.dir = dir; + pj_memcpy(&info.fmt, codec_info, sizeof(pjmedia_codec_info)); + info.tx_pt = codec_info->pt; + info.ssrc = pj_rand(); + +#if PJMEDIA_HAS_RTCP_XR && PJMEDIA_STREAM_ENABLE_XR + /* Set default RTCP XR enabled/disabled */ + info.rtcp_xr_enabled = PJ_TRUE; +#endif + + /* Copy remote address */ + pj_memcpy(&info.rem_addr, rem_addr, sizeof(pj_sockaddr_in)); + + /* If remote address is not set, set to an arbitrary address + * (otherwise stream will assert). + */ + if (info.rem_addr.addr.sa_family == 0) { + const pj_str_t addr = pj_str("127.0.0.1"); + pj_sockaddr_in_init(&info.rem_addr.ipv4, &addr, 0); + } + + /* Create media transport */ + status = pjmedia_transport_udp_create(med_endpt, NULL, local_port, + 0, &transport); + if (status != PJ_SUCCESS) + return status; + +#if defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0) + /* Check if SRTP enabled */ + if (use_srtp) { + pjmedia_srtp_crypto tx_plc, rx_plc; + + status = pjmedia_transport_srtp_create(med_endpt, transport, + NULL, &srtp_tp); + if (status != PJ_SUCCESS) + return status; + + pj_bzero(&tx_plc, sizeof(pjmedia_srtp_crypto)); + pj_bzero(&rx_plc, sizeof(pjmedia_srtp_crypto)); + + tx_plc.key = *srtp_tx_key; + tx_plc.name = *crypto_suite; + rx_plc.key = *srtp_rx_key; + rx_plc.name = *crypto_suite; + + status = pjmedia_transport_srtp_start(srtp_tp, &tx_plc, &rx_plc); + if (status != PJ_SUCCESS) + return status; + + transport = srtp_tp; + } +#endif + + /* Now that the stream info is initialized, we can create the + * stream. + */ + + status = pjmedia_stream_create( med_endpt, pool, &info, + transport, + NULL, p_stream); + + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Error creating stream", status); + pjmedia_transport_close(transport); + return status; + } + + + return PJ_SUCCESS; +} + + +/* + * usage() + */ +static void usage() +{ + puts(desc); +} + +/* + * main() + */ +int main(int argc, char *argv[]) +{ + pj_caching_pool cp; + pjmedia_endpt *med_endpt; + pj_pool_t *pool; + pjmedia_port *rec_file_port = NULL, *play_file_port = NULL; + pjmedia_master_port *master_port = NULL; + pjmedia_snd_port *snd_port = NULL; + pjmedia_stream *stream = NULL; + pjmedia_port *stream_port; + char tmp[10]; + pj_status_t status; + +#if defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0) + /* SRTP variables */ + pj_bool_t use_srtp = PJ_FALSE; + char tmp_tx_key[64]; + char tmp_rx_key[64]; + pj_str_t srtp_tx_key = {NULL, 0}; + pj_str_t srtp_rx_key = {NULL, 0}; + pj_str_t srtp_crypto_suite = {NULL, 0}; + int tmp_key_len; +#endif + + /* Default values */ + const pjmedia_codec_info *codec_info; + pjmedia_codec_param codec_param; + pjmedia_dir dir = PJMEDIA_DIR_DECODING; + pj_sockaddr_in remote_addr; + pj_uint16_t local_port = 4000; + char *codec_id = NULL; + char *rec_file = NULL; + char *play_file = NULL; + + enum { + OPT_CODEC = 'c', + OPT_LOCAL_PORT = 'p', + OPT_REMOTE = 'r', + OPT_PLAY_FILE = 'w', + OPT_RECORD_FILE = 'R', + OPT_SEND_RECV = 'b', + OPT_SEND_ONLY = 's', + OPT_RECV_ONLY = 'i', +#if defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0) + OPT_USE_SRTP = 'S', +#endif + OPT_SRTP_TX_KEY = 'x', + OPT_SRTP_RX_KEY = 'y', + OPT_HELP = 'h', + }; + + struct pj_getopt_option long_options[] = { + { "codec", 1, 0, OPT_CODEC }, + { "local-port", 1, 0, OPT_LOCAL_PORT }, + { "remote", 1, 0, OPT_REMOTE }, + { "play-file", 1, 0, OPT_PLAY_FILE }, + { "record-file", 1, 0, OPT_RECORD_FILE }, + { "send-recv", 0, 0, OPT_SEND_RECV }, + { "send-only", 0, 0, OPT_SEND_ONLY }, + { "recv-only", 0, 0, OPT_RECV_ONLY }, +#if defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0) + { "use-srtp", 2, 0, OPT_USE_SRTP }, + { "srtp-tx-key", 1, 0, OPT_SRTP_TX_KEY }, + { "srtp-rx-key", 1, 0, OPT_SRTP_RX_KEY }, +#endif + { "help", 0, 0, OPT_HELP }, + { NULL, 0, 0, 0 }, + }; + + int c; + int option_index; + + + pj_bzero(&remote_addr, sizeof(remote_addr)); + + + /* init PJLIB : */ + status = pj_init(); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + /* Parse arguments */ + pj_optind = 0; + while((c=pj_getopt_long(argc,argv, "h", long_options, &option_index))!=-1) { + + switch (c) { + case OPT_CODEC: + codec_id = pj_optarg; + break; + + case OPT_LOCAL_PORT: + local_port = (pj_uint16_t) atoi(pj_optarg); + if (local_port < 1) { + printf("Error: invalid local port %s\n", pj_optarg); + return 1; + } + break; + + case OPT_REMOTE: + { + pj_str_t ip = pj_str(strtok(pj_optarg, ":")); + pj_uint16_t port = (pj_uint16_t) atoi(strtok(NULL, ":")); + + status = pj_sockaddr_in_init(&remote_addr, &ip, port); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Invalid remote address", status); + return 1; + } + } + break; + + case OPT_PLAY_FILE: + play_file = pj_optarg; + break; + + case OPT_RECORD_FILE: + rec_file = pj_optarg; + break; + + case OPT_SEND_RECV: + dir = PJMEDIA_DIR_ENCODING_DECODING; + break; + + case OPT_SEND_ONLY: + dir = PJMEDIA_DIR_ENCODING; + break; + + case OPT_RECV_ONLY: + dir = PJMEDIA_DIR_DECODING; + break; + +#if defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0) + case OPT_USE_SRTP: + use_srtp = PJ_TRUE; + if (pj_optarg) { + pj_strset(&srtp_crypto_suite, pj_optarg, strlen(pj_optarg)); + } else { + srtp_crypto_suite = pj_str("AES_CM_128_HMAC_SHA1_80"); + } + break; + + case OPT_SRTP_TX_KEY: + tmp_key_len = hex_string_to_octet_string(tmp_tx_key, pj_optarg, strlen(pj_optarg)); + pj_strset(&srtp_tx_key, tmp_tx_key, tmp_key_len/2); + break; + + case OPT_SRTP_RX_KEY: + tmp_key_len = hex_string_to_octet_string(tmp_rx_key, pj_optarg, strlen(pj_optarg)); + pj_strset(&srtp_rx_key, tmp_rx_key, tmp_key_len/2); + break; +#endif + + case OPT_HELP: + usage(); + return 1; + + default: + printf("Invalid options %s\n", argv[pj_optind]); + return 1; + } + + } + + + /* Verify arguments. */ + if (dir & PJMEDIA_DIR_ENCODING) { + if (remote_addr.sin_addr.s_addr == 0) { + printf("Error: remote address must be set\n"); + return 1; + } + } + + if (play_file != NULL && dir != PJMEDIA_DIR_ENCODING) { + printf("Direction is set to --send-only because of --play-file\n"); + dir = PJMEDIA_DIR_ENCODING; + } + +#if defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0) + /* SRTP validation */ + if (use_srtp) { + if (!srtp_tx_key.slen || !srtp_rx_key.slen) + { + printf("Error: Key for each SRTP stream direction must be set\n"); + return 1; + } + } +#endif + + /* Must create a pool factory before we can allocate any memory. */ + pj_caching_pool_init(&cp, &pj_pool_factory_default_policy, 0); + + /* + * Initialize media endpoint. + * This will implicitly initialize PJMEDIA too. + */ + status = pjmedia_endpt_create(&cp.factory, NULL, 1, &med_endpt); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* Create memory pool for application purpose */ + pool = pj_pool_create( &cp.factory, /* pool factory */ + "app", /* pool name. */ + 4000, /* init size */ + 4000, /* increment size */ + NULL /* callback on error */ + ); + + + /* Register all supported codecs */ + status = init_codecs(med_endpt); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + /* Find which codec to use. */ + if (codec_id) { + unsigned count = 1; + pj_str_t str_codec_id = pj_str(codec_id); + pjmedia_codec_mgr *codec_mgr = pjmedia_endpt_get_codec_mgr(med_endpt); + status = pjmedia_codec_mgr_find_codecs_by_id( codec_mgr, + &str_codec_id, &count, + &codec_info, NULL); + if (status != PJ_SUCCESS) { + printf("Error: unable to find codec %s\n", codec_id); + return 1; + } + } else { + /* Default to pcmu */ + pjmedia_codec_mgr_get_codec_info( pjmedia_endpt_get_codec_mgr(med_endpt), + 0, &codec_info); + } + + /* Create stream based on program arguments */ + status = create_stream(pool, med_endpt, codec_info, dir, local_port, + &remote_addr, +#if defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0) + use_srtp, &srtp_crypto_suite, + &srtp_tx_key, &srtp_rx_key, +#endif + &stream); + if (status != PJ_SUCCESS) + goto on_exit; + + /* Get codec default param for info */ + status = pjmedia_codec_mgr_get_default_param( + pjmedia_endpt_get_codec_mgr(med_endpt), + codec_info, + &codec_param); + /* Should be ok, as create_stream() above succeeded */ + pj_assert(status == PJ_SUCCESS); + + /* Get the port interface of the stream */ + status = pjmedia_stream_get_port( stream, &stream_port); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + if (play_file) { + unsigned wav_ptime; + + wav_ptime = PJMEDIA_PIA_PTIME(&stream_port->info); + status = pjmedia_wav_player_port_create(pool, play_file, wav_ptime, + 0, -1, &play_file_port); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to use file", status); + goto on_exit; + } + + status = pjmedia_master_port_create(pool, play_file_port, stream_port, + 0, &master_port); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to create master port", status); + goto on_exit; + } + + status = pjmedia_master_port_start(master_port); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Error starting master port", status); + goto on_exit; + } + + printf("Playing from WAV file %s..\n", play_file); + + } else if (rec_file) { + + status = pjmedia_wav_writer_port_create(pool, rec_file, + PJMEDIA_PIA_SRATE(&stream_port->info), + PJMEDIA_PIA_CCNT(&stream_port->info), + PJMEDIA_PIA_SPF(&stream_port->info), + PJMEDIA_PIA_BITS(&stream_port->info), + 0, 0, &rec_file_port); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to use file", status); + goto on_exit; + } + + status = pjmedia_master_port_create(pool, stream_port, rec_file_port, + 0, &master_port); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to create master port", status); + goto on_exit; + } + + status = pjmedia_master_port_start(master_port); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Error starting master port", status); + goto on_exit; + } + + printf("Recording to WAV file %s..\n", rec_file); + + } else { + + /* Create sound device port. */ + if (dir == PJMEDIA_DIR_ENCODING_DECODING) + status = pjmedia_snd_port_create(pool, -1, -1, + PJMEDIA_PIA_SRATE(&stream_port->info), + PJMEDIA_PIA_CCNT(&stream_port->info), + PJMEDIA_PIA_SPF(&stream_port->info), + PJMEDIA_PIA_BITS(&stream_port->info), + 0, &snd_port); + else if (dir == PJMEDIA_DIR_ENCODING) + status = pjmedia_snd_port_create_rec(pool, -1, + PJMEDIA_PIA_SRATE(&stream_port->info), + PJMEDIA_PIA_CCNT(&stream_port->info), + PJMEDIA_PIA_SPF(&stream_port->info), + PJMEDIA_PIA_BITS(&stream_port->info), + 0, &snd_port); + else + status = pjmedia_snd_port_create_player(pool, -1, + PJMEDIA_PIA_SRATE(&stream_port->info), + PJMEDIA_PIA_CCNT(&stream_port->info), + PJMEDIA_PIA_SPF(&stream_port->info), + PJMEDIA_PIA_BITS(&stream_port->info), + 0, &snd_port); + + + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Unable to create sound port", status); + goto on_exit; + } + + /* Connect sound port to stream */ + status = pjmedia_snd_port_connect( snd_port, stream_port ); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + } + + /* Start streaming */ + pjmedia_stream_start(stream); + + + /* Done */ + + if (dir == PJMEDIA_DIR_DECODING) + printf("Stream is active, dir is recv-only, local port is %d\n", + local_port); + else if (dir == PJMEDIA_DIR_ENCODING) + printf("Stream is active, dir is send-only, sending to %s:%d\n", + pj_inet_ntoa(remote_addr.sin_addr), + pj_ntohs(remote_addr.sin_port)); + else + printf("Stream is active, send/recv, local port is %d, " + "sending to %s:%d\n", + local_port, + pj_inet_ntoa(remote_addr.sin_addr), + pj_ntohs(remote_addr.sin_port)); + + + for (;;) { + + puts(""); + puts("Commands:"); + puts(" s Display media statistics"); + puts(" q Quit"); + puts(""); + + printf("Command: "); fflush(stdout); + + if (fgets(tmp, sizeof(tmp), stdin) == NULL) { + puts("EOF while reading stdin, will quit now.."); + break; + } + + if (tmp[0] == 's') + print_stream_stat(stream, &codec_param); + else if (tmp[0] == 'q') + break; + + } + + + + /* Start deinitialization: */ +on_exit: + + /* Destroy sound device */ + if (snd_port) { + pjmedia_snd_port_destroy( snd_port ); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + } + + /* If there is master port, then we just need to destroy master port + * (it will recursively destroy upstream and downstream ports, which + * in this case are file_port and stream_port). + */ + if (master_port) { + pjmedia_master_port_destroy(master_port, PJ_TRUE); + play_file_port = NULL; + stream = NULL; + } + + /* Destroy stream */ + if (stream) { + pjmedia_transport *tp; + + tp = pjmedia_stream_get_transport(stream); + pjmedia_stream_destroy(stream); + + pjmedia_transport_close(tp); + } + + /* Destroy file ports */ + if (play_file_port) + pjmedia_port_destroy( play_file_port ); + if (rec_file_port) + pjmedia_port_destroy( rec_file_port ); + + + /* Release application pool */ + pj_pool_release( pool ); + + /* Destroy media endpoint. */ + pjmedia_endpt_destroy( med_endpt ); + + /* Destroy pool factory */ + pj_caching_pool_destroy( &cp ); + + /* Shutdown PJLIB */ + pj_shutdown(); + + + return (status == PJ_SUCCESS) ? 0 : 1; +} + + + + +static 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; +} + + +#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) + + +/* + * Print stream statistics + */ +static void print_stream_stat(pjmedia_stream *stream, + const pjmedia_codec_param *codec_param) +{ + char duration[80], last_update[80]; + char bps[16], ipbps[16], packets[16], bytes[16], ipbytes[16]; + pjmedia_port *port; + pjmedia_rtcp_stat stat; + pj_time_val now; + + + pj_gettimeofday(&now); + pjmedia_stream_get_stat(stream, &stat); + pjmedia_stream_get_port(stream, &port); + + puts("Stream statistics:"); + + /* Print duration */ + PJ_TIME_VAL_SUB(now, stat.start); + sprintf(duration, " Duration: %02ld:%02ld:%02ld.%03ld", + now.sec / 3600, + (now.sec % 3600) / 60, + (now.sec % 60), + now.msec); + + + printf(" Info: audio %dHz, %dms/frame, %sB/s (%sB/s +IP hdr)\n", + PJMEDIA_PIA_SRATE(&port->info), + PJMEDIA_PIA_PTIME(&port->info), + good_number(bps, (codec_param->info.avg_bps+7)/8), + good_number(ipbps, ((codec_param->info.avg_bps+7)/8) + + (40 * 1000 / + codec_param->setting.frm_per_pkt / + codec_param->info.frm_ptime))); + + 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); + } + + printf(" RX stat last update: %s\n" + " total %s packets %sB received (%sB +IP hdr)%s\n" + " pkt loss=%d (%3.1f%%), dup=%d (%3.1f%%), reorder=%d (%3.1f%%)%s\n" + " (msec) min avg max last dev\n" + " loss period: %7.3f %7.3f %7.3f %7.3f %7.3f%s\n" + " jitter : %7.3f %7.3f %7.3f %7.3f %7.3f%s\n", + last_update, + good_number(packets, stat.rx.pkt), + good_number(bytes, stat.rx.bytes), + good_number(ipbytes, stat.rx.bytes + stat.rx.pkt * 32), + "", + stat.rx.loss, + stat.rx.loss * 100.0 / (stat.rx.pkt + stat.rx.loss), + stat.rx.dup, + stat.rx.dup * 100.0 / (stat.rx.pkt + stat.rx.loss), + stat.rx.reorder, + stat.rx.reorder * 100.0 / (stat.rx.pkt + stat.rx.loss), + "", + 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, + "", + 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 (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); + } + + printf(" TX stat last update: %s\n" + " total %s packets %sB sent (%sB +IP hdr)%s\n" + " pkt loss=%d (%3.1f%%), dup=%d (%3.1f%%), reorder=%d (%3.1f%%)%s\n" + " (msec) min avg max last dev\n" + " loss period: %7.3f %7.3f %7.3f %7.3f %7.3f%s\n" + " jitter : %7.3f %7.3f %7.3f %7.3f %7.3f%s\n", + last_update, + good_number(packets, stat.tx.pkt), + good_number(bytes, stat.tx.bytes), + good_number(ipbytes, stat.tx.bytes + stat.tx.pkt * 32), + "", + stat.tx.loss, + stat.tx.loss * 100.0 / (stat.tx.pkt + stat.tx.loss), + stat.tx.dup, + stat.tx.dup * 100.0 / (stat.tx.pkt + stat.tx.loss), + stat.tx.reorder, + stat.tx.reorder * 100.0 / (stat.tx.pkt + stat.tx.loss), + "", + 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, + "", + 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, + "" + ); + + + printf(" RTT delay : %7.3f %7.3f %7.3f %7.3f %7.3f%s\n", + 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 defined(PJMEDIA_HAS_RTCP_XR) && (PJMEDIA_HAS_RTCP_XR != 0) + /* RTCP XR Reports */ + do { + 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; + + if (pjmedia_stream_get_stat_xr(stream, &xr_stat) != PJ_SUCCESS) + break; + + puts("\nExtended reports:"); + + /* Statistics Summary */ + puts(" Statistics Summary"); + + 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, + port->info.fmt.det.aud.clock_rate); + SAMPLES_TO_USEC(jmax, xr_stat.rx.stat_sum.jitter.max, + port->info.fmt.det.aud.clock_rate); + SAMPLES_TO_USEC(jmean, xr_stat.rx.stat_sum.jitter.mean, + port->info.fmt.det.aud.clock_rate); + SAMPLES_TO_USEC(jdev, + pj_math_stat_get_stddev(&xr_stat.rx.stat_sum.jitter), + port->info.fmt.det.aud.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); + } + + printf(" RX last update: %s\n" + " begin seq=%d, end seq=%d%s\n" + " pkt loss=%s, dup=%s%s\n" + " (msec) min avg max dev\n" + " jitter : %s\n" + " toh : %s\n", + last_update, + xr_stat.rx.stat_sum.begin_seq, xr_stat.rx.stat_sum.end_seq, + "", + loss, dup, + "", + jitter, + toh + ); + + 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, + port->info.fmt.det.aud.clock_rate); + SAMPLES_TO_USEC(jmax, xr_stat.tx.stat_sum.jitter.max, + port->info.fmt.det.aud.clock_rate); + SAMPLES_TO_USEC(jmean, xr_stat.tx.stat_sum.jitter.mean, + port->info.fmt.det.aud.clock_rate); + SAMPLES_TO_USEC(jdev, + pj_math_stat_get_stddev(&xr_stat.tx.stat_sum.jitter), + port->info.fmt.det.aud.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); + } + + printf(" TX last update: %s\n" + " begin seq=%d, end seq=%d%s\n" + " pkt loss=%s, dup=%s%s\n" + " (msec) min avg max dev\n" + " jitter : %s\n" + " toh : %s\n", + last_update, + xr_stat.tx.stat_sum.begin_seq, xr_stat.tx.stat_sum.end_seq, + "", + loss, dup, + "", + jitter, + toh + ); + + /* VoIP Metrics */ + puts(" VoIP Metrics"); + + 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); + } + + printf(" RX last update: %s\n" + " packets : loss rate=%d (%.2f%%), discard rate=%d (%.2f%%)\n" + " burst : density=%d (%.2f%%), duration=%d%s\n" + " gap : density=%d (%.2f%%), duration=%d%s\n" + " delay : round trip=%d%s, end system=%d%s\n" + " level : signal=%s%s, noise=%s%s, RERL=%s%s\n" + " quality : R factor=%s, ext R factor=%s\n" + " MOS LQ=%s, MOS CQ=%s\n" + " config : PLC=%s, JB=%s, JB rate=%s, Gmin=%d\n" + " JB delay : cur=%d%s, max=%d%s, abs max=%d%s\n", + last_update, + /* pakcets */ + 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 */ + 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 */ + 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 */ + xr_stat.rx.voip_mtc.rnd_trip_delay, "ms", + xr_stat.rx.voip_mtc.end_sys_delay, "ms", + /* level */ + signal_lvl, "dB", + noise_lvl, "dB", + rerl, "", + /* quality */ + r_factor, ext_r_factor, mos_lq, mos_cq, + /* config */ + plc, jba, jbr, xr_stat.rx.voip_mtc.gmin, + /* JB delay */ + 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" + ); + + 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); + } + + printf(" TX last update: %s\n" + " packets : loss rate=%d (%.2f%%), discard rate=%d (%.2f%%)\n" + " burst : density=%d (%.2f%%), duration=%d%s\n" + " gap : density=%d (%.2f%%), duration=%d%s\n" + " delay : round trip=%d%s, end system=%d%s\n" + " level : signal=%s%s, noise=%s%s, RERL=%s%s\n" + " quality : R factor=%s, ext R factor=%s\n" + " MOS LQ=%s, MOS CQ=%s\n" + " config : PLC=%s, JB=%s, JB rate=%s, Gmin=%d\n" + " JB delay : cur=%d%s, max=%d%s, abs max=%d%s\n", + last_update, + /* pakcets */ + 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 */ + 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 */ + 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 */ + xr_stat.tx.voip_mtc.rnd_trip_delay, "ms", + xr_stat.tx.voip_mtc.end_sys_delay, "ms", + /* level */ + signal_lvl, "dB", + noise_lvl, "dB", + rerl, "", + /* quality */ + r_factor, ext_r_factor, mos_lq, mos_cq, + /* config */ + plc, jba, jbr, xr_stat.tx.voip_mtc.gmin, + /* JB delay */ + 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" + ); + + + /* RTT delay (by receiver side) */ + printf(" (msec) min avg max last dev\n"); + printf(" RTT delay : %7.3f %7.3f %7.3f %7.3f %7.3f%s\n", + 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, + "" + ); + } while (0); +#endif /* PJMEDIA_HAS_RTCP_XR */ + +} + diff --git a/pjsip-apps/src/samples/strerror.c b/pjsip-apps/src/samples/strerror.c new file mode 100644 index 0000000..b03e8c1 --- /dev/null +++ b/pjsip-apps/src/samples/strerror.c @@ -0,0 +1,71 @@ +/* $Id: strerror.c 3553 2011-05-05 06:14:19Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 + */ + +/** + * \page page_strerror_c Samples: Print out error message + * + * This file is pjsip-apps/src/samples/strerror.c + * + * \includelineno strerror.c + */ + + +#include <pjlib.h> +#include <pjlib-util.h> +#include <pjsip.h> +#include <pjmedia.h> +#include <pjnath.h> +#include <pjsip_simple.h> + +/* + * main() + */ +int main(int argc, char *argv[]) +{ + pj_caching_pool cp; + pjmedia_endpt *med_ept; + pjsip_endpoint *sip_ept; + char errmsg[PJ_ERR_MSG_SIZE]; + pj_status_t code; + + if (argc != 2) { + puts("Usage: strerror ERRNUM"); + return 1; + } + + pj_log_set_level(3); + + pj_init(); + pj_caching_pool_init(&cp, NULL, 0); + pjlib_util_init(); + pjnath_init(); + pjmedia_endpt_create(&cp.factory, NULL, 0, &med_ept); + pjsip_endpt_create(&cp.factory, "localhost", &sip_ept); + pjsip_evsub_init_module(sip_ept); + + code = atoi(argv[1]); + pj_strerror(code, errmsg, sizeof(errmsg)); + + printf("Status %d: %s\n", code, errmsg); + + pj_shutdown(); + return 0; +} + diff --git a/pjsip-apps/src/samples/tonegen.c b/pjsip-apps/src/samples/tonegen.c new file mode 100644 index 0000000..5190186 --- /dev/null +++ b/pjsip-apps/src/samples/tonegen.c @@ -0,0 +1,159 @@ +/* $Id: tonegen.c 3553 2011-05-05 06:14:19Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 + */ + +/** + * \page page_pjmedia_samples_tonegen_c Samples: Sine Wave/Dual-Tone Generation + * + * This is a simple program to generate a tone and write the samples to + * a raw PCM file. The main purpose of this file is to analyze the + * quality of the tones/sine wave generated by PJMEDIA tone/sine wave + * generator. + * + * This file is pjsip-apps/src/samples/tonegen.c + * + * \includelineno tonegen.c + */ + + +#include <pjmedia.h> +#include <pjlib.h> + +#define SAMPLES_PER_FRAME 64 +#define ON_DURATION 100 +#define OFF_DURATION 100 + + +/* + * main() + */ +int main() +{ + pj_caching_pool cp; + pjmedia_endpt *med_endpt; + pj_pool_t *pool; + pjmedia_port *port; + unsigned i; + pj_status_t status; + + + /* Must init PJLIB first: */ + status = pj_init(); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* Must create a pool factory before we can allocate any memory. */ + pj_caching_pool_init(&cp, &pj_pool_factory_default_policy, 0); + + /* + * Initialize media endpoint. + * This will implicitly initialize PJMEDIA too. + */ + status = pjmedia_endpt_create(&cp.factory, NULL, 1, &med_endpt); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* Create memory pool for our file player */ + pool = pj_pool_create( &cp.factory, /* pool factory */ + "app", /* pool name. */ + 4000, /* init size */ + 4000, /* increment size */ + NULL /* callback on error */ + ); + + status = pjmedia_tonegen_create(pool, 8000, 1, SAMPLES_PER_FRAME, 16, 0, &port); + if (status != PJ_SUCCESS) + return 1; + + { + pjmedia_tone_desc tones[3]; + + tones[0].freq1 = 200; + tones[0].freq2 = 0; + tones[0].on_msec = ON_DURATION; + tones[0].off_msec = OFF_DURATION; + + tones[1].freq1 = 400; + tones[1].freq2 = 0; + tones[1].on_msec = ON_DURATION; + tones[1].off_msec = OFF_DURATION; + + tones[2].freq1 = 800; + tones[2].freq2 = 0; + tones[2].on_msec = ON_DURATION; + tones[2].off_msec = OFF_DURATION; + + status = pjmedia_tonegen_play(port, 3, tones, 0); + PJ_ASSERT_RETURN(status==PJ_SUCCESS, 1); + } + + { + pjmedia_tone_digit digits[2]; + + digits[0].digit = '0'; + digits[0].on_msec = ON_DURATION; + digits[0].off_msec = OFF_DURATION; + + digits[1].digit = '0'; + digits[1].on_msec = ON_DURATION; + digits[1].off_msec = OFF_DURATION; + + status = pjmedia_tonegen_play_digits(port, 2, digits, 0); + PJ_ASSERT_RETURN(status==PJ_SUCCESS, 1); + } + + { + pjmedia_frame frm; + FILE *f; + void *buf; + + buf = pj_pool_alloc(pool, 2*8000); + frm.buf = buf; + + f = fopen("tonegen.pcm", "wb"); + + for (i=0; i<8000/SAMPLES_PER_FRAME; ++i) { + int count; + pjmedia_port_get_frame(port, &frm); + count = fwrite(buf, SAMPLES_PER_FRAME, 2, f); + if (count != 2) + break; + } + + pj_assert(pjmedia_tonegen_is_busy(port) == 0); + fclose(f); + } + + /* Delete port */ + pjmedia_port_destroy(port); + + /* Release application pool */ + pj_pool_release( pool ); + + /* Destroy media endpoint. */ + pjmedia_endpt_destroy( med_endpt ); + + /* Destroy pool factory */ + pj_caching_pool_destroy( &cp ); + + /* Shutdown PJLIB */ + pj_shutdown(); + + + /* Done. */ + return 0; +} diff --git a/pjsip-apps/src/samples/util.h b/pjsip-apps/src/samples/util.h new file mode 100644 index 0000000..40a8f6e --- /dev/null +++ b/pjsip-apps/src/samples/util.h @@ -0,0 +1,173 @@ +/* $Id: util.h 3550 2011-05-05 05:33:27Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 <stdlib.h> /* strtol() */ + +/* Util to display the error message for the specified error code */ +static int app_perror( const char *sender, const char *title, + pj_status_t status) +{ + char errmsg[PJ_ERR_MSG_SIZE]; + + pj_strerror(status, errmsg, sizeof(errmsg)); + + PJ_LOG(3,(sender, "%s: %s [code=%d]", title, errmsg, status)); + return 1; +} + + + +/* Constants */ +#define CLOCK_RATE 44100 +#define NSAMPLES (CLOCK_RATE * 20 / 1000) +#define NCHANNELS 1 +#define NBITS 16 + +/* + * Common sound options. + */ +#define SND_USAGE \ +" -d, --dev=NUM Sound device use device id NUM (default=-1) \n"\ +" -r, --rate=HZ Set clock rate in samples per sec (default=44100)\n"\ +" -c, --channel=NUM Set # of channels (default=1 for mono). \n"\ +" -f, --frame=NUM Set # of samples per frame (default equival 20ms)\n"\ +" -b, --bit=NUM Set # of bits per sample (default=16) \n" + + +/* + * This utility function parses the command line and look for + * common sound options. + */ +pj_status_t get_snd_options(const char *app_name, + int argc, + char *argv[], + int *dev_id, + int *clock_rate, + int *channel_count, + int *samples_per_frame, + int *bits_per_sample) +{ + struct pj_getopt_option long_options[] = { + { "dev", 1, 0, 'd' }, + { "rate", 1, 0, 'r' }, + { "channel", 1, 0, 'c' }, + { "frame", 1, 0, 'f' }, + { "bit", 1, 0, 'b' }, + { NULL, 0, 0, 0 }, + }; + int c; + int option_index; + long val; + char *err; + + *samples_per_frame = 0; + + pj_optind = 0; + while((c=pj_getopt_long(argc,argv, "d:r:c:f:b:", + long_options, &option_index))!=-1) + { + + switch (c) { + case 'd': + /* device */ + val = strtol(pj_optarg, &err, 10); + if (*err) { + PJ_LOG(3,(app_name, "Error: invalid value for device id")); + return PJ_EINVAL; + } + *dev_id = val; + break; + + case 'r': + /* rate */ + val = strtol(pj_optarg, &err, 10); + if (*err) { + PJ_LOG(3,(app_name, "Error: invalid value for clock rate")); + return PJ_EINVAL; + } + *clock_rate = val; + break; + + case 'c': + /* channel count */ + val = strtol(pj_optarg, &err, 10); + if (*err) { + PJ_LOG(3,(app_name, "Error: invalid channel count")); + return PJ_EINVAL; + } + *channel_count = val; + break; + + case 'f': + /* frame count/samples per frame */ + val = strtol(pj_optarg, &err, 10); + if (*err) { + PJ_LOG(3,(app_name, "Error: invalid samples per frame")); + return PJ_EINVAL; + } + *samples_per_frame = val; + break; + + case 'b': + /* bit per sample */ + val = strtol(pj_optarg, &err, 10); + if (*err) { + PJ_LOG(3,(app_name, "Error: invalid samples bits per sample")); + return PJ_EINVAL; + } + *bits_per_sample = val; + break; + + default: + /* Unknown options */ + PJ_LOG(3,(app_name, "Error: unknown options '%c'", pj_optopt)); + return PJ_EINVAL; + } + + } + + if (*samples_per_frame == 0) { + *samples_per_frame = *clock_rate * *channel_count * 20 / 1000; + } + + return 0; +} + + +/* Dump memory pool usage. */ +void dump_pool_usage( const char *app_name, pj_caching_pool *cp ) +{ +#if !defined(PJ_HAS_POOL_ALT_API) || PJ_HAS_POOL_ALT_API==0 + pj_pool_t *p; + unsigned total_alloc = 0; + unsigned total_used = 0; + + /* Accumulate memory usage in active list. */ + p = cp->used_list.next; + while (p != (pj_pool_t*) &cp->used_list) { + total_alloc += pj_pool_get_capacity(p); + total_used += pj_pool_get_used_size(p); + p = p->next; + } + + PJ_LOG(3, (app_name, "Total pool memory allocated=%d KB, used=%d KB", + total_alloc / 1000, + total_used / 1000)); +#endif +} diff --git a/pjsip-apps/src/samples/vid_streamutil.c b/pjsip-apps/src/samples/vid_streamutil.c new file mode 100644 index 0000000..b56c398 --- /dev/null +++ b/pjsip-apps/src/samples/vid_streamutil.c @@ -0,0 +1,967 @@ +/* $Id: vid_streamutil.c 4084 2012-04-25 07:13:05Z ming $ */ +/* + * Copyright (C) 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 + */ + + +/** + * \page page_pjmedia_samples_vid_streamutil_c Samples: Video Streaming + * + * This example mainly demonstrates how to stream video to remote + * peer using RTP. + * + * This file is pjsip-apps/src/samples/vid_streamutil.c + * + * \includelineno vid_streamutil.c + */ + +#include <pjlib.h> +#include <pjlib-util.h> +#include <pjmedia.h> +#include <pjmedia-codec.h> +#include <pjmedia/transport_srtp.h> + + +#if defined(PJMEDIA_HAS_VIDEO) && (PJMEDIA_HAS_VIDEO != 0) + + +#include <stdlib.h> /* atoi() */ +#include <stdio.h> + +#include "util.h" + + +static const char *desc = + " vid_streamutil \n" + "\n" + " PURPOSE: \n" + " Demonstrate how to use pjmedia video stream component to \n" + " transmit/receive RTP packets to/from video device/file. \n" + "\n" + "\n" + " USAGE: \n" + " vid_streamutil [options] \n" + "\n" + "\n" + " Options: \n" + " --codec=CODEC Set the codec name. \n" + " --local-port=PORT Set local RTP port (default=4000) \n" + " --remote=IP:PORT Set the remote peer. If this option is set, \n" + " the program will transmit RTP audio to the \n" + " specified address. (default: recv only) \n" + " --play-file=AVI Send video from the AVI file instead of from \n" + " the video device. \n" + " --send-recv Set stream direction to bidirectional. \n" + " --send-only Set stream direction to send only \n" + " --recv-only Set stream direction to recv only (default) \n" + + " --send-width Video width to be sent \n" + " --send-height Video height to be sent \n" + " --send-width and --send-height not applicable \n" + " for file streaming (see --play-file) \n" + + " --send-pt Payload type for sending \n" + " --recv-pt Payload type for receiving \n" + +#if defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0) + " --use-srtp[=NAME] Enable SRTP with crypto suite NAME \n" + " e.g: AES_CM_128_HMAC_SHA1_80 (default), \n" + " AES_CM_128_HMAC_SHA1_32 \n" + " Use this option along with the TX & RX keys, \n" + " formated of 60 hex digits (e.g: E148DA..) \n" + " --srtp-tx-key SRTP key for transmiting \n" + " --srtp-rx-key SRTP key for receiving \n" +#endif + + "\n" +; + +#define THIS_FILE "vid_streamutil.c" + + +/* If set, local renderer will be created to play original file */ +#define HAS_LOCAL_RENDERER_FOR_PLAY_FILE 1 + + +/* Default width and height for the renderer, better be set to maximum + * acceptable size. + */ +#define DEF_RENDERER_WIDTH 640 +#define DEF_RENDERER_HEIGHT 480 + + +/* Prototype */ +static void print_stream_stat(pjmedia_vid_stream *stream, + const pjmedia_vid_codec_param *codec_param); + +/* Prototype for LIBSRTP utility in file datatypes.c */ +int hex_string_to_octet_string(char *raw, char *hex, int len); + +/* + * Register all codecs. + */ +static pj_status_t init_codecs(pj_pool_factory *pf) +{ + pj_status_t status; + + /* To suppress warning about unused var when all codecs are disabled */ + PJ_UNUSED_ARG(status); + +#if defined(PJMEDIA_HAS_FFMPEG_VID_CODEC) && PJMEDIA_HAS_FFMPEG_VID_CODEC != 0 + status = pjmedia_codec_ffmpeg_vid_init(NULL, pf); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); +#endif + + return PJ_SUCCESS; +} + +/* + * Register all codecs. + */ +static void deinit_codecs() +{ +#if defined(PJMEDIA_HAS_FFMPEG_VID_CODEC) && PJMEDIA_HAS_FFMPEG_VID_CODEC != 0 + pjmedia_codec_ffmpeg_vid_deinit(); +#endif +} + +static pj_status_t create_file_player( pj_pool_t *pool, + const char *file_name, + pjmedia_port **p_play_port) +{ + pjmedia_avi_streams *avi_streams; + pjmedia_avi_stream *vid_stream; + pjmedia_port *play_port; + pj_status_t status; + + status = pjmedia_avi_player_create_streams(pool, file_name, 0, &avi_streams); + if (status != PJ_SUCCESS) + return status; + + vid_stream = pjmedia_avi_streams_get_stream_by_media(avi_streams, + 0, + PJMEDIA_TYPE_VIDEO); + if (!vid_stream) + return PJ_ENOTFOUND; + + play_port = pjmedia_avi_stream_get_port(vid_stream); + pj_assert(play_port); + + *p_play_port = play_port; + + return PJ_SUCCESS; +} + +/* + * Create stream based on the codec, dir, remote address, etc. + */ +static pj_status_t create_stream( pj_pool_t *pool, + pjmedia_endpt *med_endpt, + const pjmedia_vid_codec_info *codec_info, + pjmedia_vid_codec_param *codec_param, + pjmedia_dir dir, + pj_int8_t rx_pt, + pj_int8_t tx_pt, + pj_uint16_t local_port, + const pj_sockaddr_in *rem_addr, +#if defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0) + pj_bool_t use_srtp, + const pj_str_t *crypto_suite, + const pj_str_t *srtp_tx_key, + const pj_str_t *srtp_rx_key, +#endif + pjmedia_vid_stream **p_stream ) +{ + pjmedia_vid_stream_info info; + pjmedia_transport *transport = NULL; + pj_status_t status; +#if defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0) + pjmedia_transport *srtp_tp = NULL; +#endif + + /* Reset stream info. */ + pj_bzero(&info, sizeof(info)); + + /* Initialize stream info formats */ + info.type = PJMEDIA_TYPE_VIDEO; + info.dir = dir; + info.codec_info = *codec_info; + info.tx_pt = (tx_pt == -1)? codec_info->pt : tx_pt; + info.rx_pt = (rx_pt == -1)? codec_info->pt : rx_pt; + info.ssrc = pj_rand(); + if (codec_param) + info.codec_param = codec_param; + + /* Copy remote address */ + pj_memcpy(&info.rem_addr, rem_addr, sizeof(pj_sockaddr_in)); + + /* If remote address is not set, set to an arbitrary address + * (otherwise stream will assert). + */ + if (info.rem_addr.addr.sa_family == 0) { + const pj_str_t addr = pj_str("127.0.0.1"); + pj_sockaddr_in_init(&info.rem_addr.ipv4, &addr, 0); + } + + /* Create media transport */ + status = pjmedia_transport_udp_create(med_endpt, NULL, local_port, + 0, &transport); + if (status != PJ_SUCCESS) + return status; + +#if defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0) + /* Check if SRTP enabled */ + if (use_srtp) { + pjmedia_srtp_crypto tx_plc, rx_plc; + + status = pjmedia_transport_srtp_create(med_endpt, transport, + NULL, &srtp_tp); + if (status != PJ_SUCCESS) + return status; + + pj_bzero(&tx_plc, sizeof(pjmedia_srtp_crypto)); + pj_bzero(&rx_plc, sizeof(pjmedia_srtp_crypto)); + + tx_plc.key = *srtp_tx_key; + tx_plc.name = *crypto_suite; + rx_plc.key = *srtp_rx_key; + rx_plc.name = *crypto_suite; + + status = pjmedia_transport_srtp_start(srtp_tp, &tx_plc, &rx_plc); + if (status != PJ_SUCCESS) + return status; + + transport = srtp_tp; + } +#endif + + /* Now that the stream info is initialized, we can create the + * stream. + */ + + status = pjmedia_vid_stream_create( med_endpt, pool, &info, + transport, + NULL, p_stream); + + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Error creating stream", status); + pjmedia_transport_close(transport); + return status; + } + + + return PJ_SUCCESS; +} + + +typedef struct play_file_data +{ + const char *file_name; + pjmedia_port *play_port; + pjmedia_port *stream_port; + pjmedia_vid_codec *decoder; + pjmedia_port *renderer; + void *read_buf; + pj_size_t read_buf_size; + void *dec_buf; + pj_size_t dec_buf_size; +} play_file_data; + + +static void clock_cb(const pj_timestamp *ts, void *user_data) +{ + play_file_data *play_file = (play_file_data*)user_data; + pjmedia_frame read_frame, write_frame; + pj_status_t status; + + PJ_UNUSED_ARG(ts); + + /* Read frame from file */ + read_frame.buf = play_file->read_buf; + read_frame.size = play_file->read_buf_size; + pjmedia_port_get_frame(play_file->play_port, &read_frame); + + /* Decode frame, if needed */ + if (play_file->decoder) { + pjmedia_vid_codec *decoder = play_file->decoder; + + write_frame.buf = play_file->dec_buf; + write_frame.size = play_file->dec_buf_size; + status = pjmedia_vid_codec_decode(decoder, 1, &read_frame, + write_frame.size, &write_frame); + if (status != PJ_SUCCESS) + return; + } else { + write_frame = read_frame; + } + + /* Display frame locally */ + if (play_file->renderer) + pjmedia_port_put_frame(play_file->renderer, &write_frame); + + /* Send frame */ + pjmedia_port_put_frame(play_file->stream_port, &write_frame); +} + + +/* + * usage() + */ +static void usage() +{ + puts(desc); +} + +/* + * main() + */ +int main(int argc, char *argv[]) +{ + pj_caching_pool cp; + pjmedia_endpt *med_endpt; + pj_pool_t *pool; + pjmedia_vid_stream *stream = NULL; + pjmedia_port *enc_port, *dec_port; + pj_status_t status; + + pjmedia_vid_port *capture=NULL, *renderer=NULL; + pjmedia_vid_port_param vpp; + +#if defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0) + /* SRTP variables */ + pj_bool_t use_srtp = PJ_FALSE; + char tmp_tx_key[64]; + char tmp_rx_key[64]; + pj_str_t srtp_tx_key = {NULL, 0}; + pj_str_t srtp_rx_key = {NULL, 0}; + pj_str_t srtp_crypto_suite = {NULL, 0}; + int tmp_key_len; +#endif + + /* Default values */ + const pjmedia_vid_codec_info *codec_info; + pjmedia_vid_codec_param codec_param; + pjmedia_dir dir = PJMEDIA_DIR_DECODING; + pj_sockaddr_in remote_addr; + pj_uint16_t local_port = 4000; + char *codec_id = NULL; + pjmedia_rect_size tx_size = {0}; + pj_int8_t rx_pt = -1, tx_pt = -1; + + play_file_data play_file = { NULL }; + pjmedia_port *play_port = NULL; + pjmedia_vid_codec *play_decoder = NULL; + pjmedia_clock *play_clock = NULL; + + enum { + OPT_CODEC = 'c', + OPT_LOCAL_PORT = 'p', + OPT_REMOTE = 'r', + OPT_PLAY_FILE = 'f', + OPT_SEND_RECV = 'b', + OPT_SEND_ONLY = 's', + OPT_RECV_ONLY = 'i', + OPT_SEND_WIDTH = 'W', + OPT_SEND_HEIGHT = 'H', + OPT_RECV_PT = 't', + OPT_SEND_PT = 'T', +#if defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0) + OPT_USE_SRTP = 'S', +#endif + OPT_SRTP_TX_KEY = 'x', + OPT_SRTP_RX_KEY = 'y', + OPT_HELP = 'h', + }; + + struct pj_getopt_option long_options[] = { + { "codec", 1, 0, OPT_CODEC }, + { "local-port", 1, 0, OPT_LOCAL_PORT }, + { "remote", 1, 0, OPT_REMOTE }, + { "play-file", 1, 0, OPT_PLAY_FILE }, + { "send-recv", 0, 0, OPT_SEND_RECV }, + { "send-only", 0, 0, OPT_SEND_ONLY }, + { "recv-only", 0, 0, OPT_RECV_ONLY }, + { "send-width", 1, 0, OPT_SEND_WIDTH }, + { "send-height", 1, 0, OPT_SEND_HEIGHT }, + { "recv-pt", 1, 0, OPT_RECV_PT }, + { "send-pt", 1, 0, OPT_SEND_PT }, +#if defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0) + { "use-srtp", 2, 0, OPT_USE_SRTP }, + { "srtp-tx-key", 1, 0, OPT_SRTP_TX_KEY }, + { "srtp-rx-key", 1, 0, OPT_SRTP_RX_KEY }, +#endif + { "help", 0, 0, OPT_HELP }, + { NULL, 0, 0, 0 }, + }; + + int c; + int option_index; + + + pj_bzero(&remote_addr, sizeof(remote_addr)); + + + /* init PJLIB : */ + status = pj_init(); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + /* Parse arguments */ + pj_optind = 0; + while((c=pj_getopt_long(argc,argv, "h", long_options, &option_index))!=-1) + { + switch (c) { + case OPT_CODEC: + codec_id = pj_optarg; + break; + + case OPT_LOCAL_PORT: + local_port = (pj_uint16_t) atoi(pj_optarg); + if (local_port < 1) { + printf("Error: invalid local port %s\n", pj_optarg); + return 1; + } + break; + + case OPT_REMOTE: + { + pj_str_t ip = pj_str(strtok(pj_optarg, ":")); + pj_uint16_t port = (pj_uint16_t) atoi(strtok(NULL, ":")); + + status = pj_sockaddr_in_init(&remote_addr, &ip, port); + if (status != PJ_SUCCESS) { + app_perror(THIS_FILE, "Invalid remote address", status); + return 1; + } + } + break; + + case OPT_PLAY_FILE: + play_file.file_name = pj_optarg; + break; + + case OPT_SEND_RECV: + dir = PJMEDIA_DIR_ENCODING_DECODING; + break; + + case OPT_SEND_ONLY: + dir = PJMEDIA_DIR_ENCODING; + break; + + case OPT_RECV_ONLY: + dir = PJMEDIA_DIR_DECODING; + break; + + case OPT_SEND_WIDTH: + tx_size.w = (unsigned)atoi(pj_optarg); + break; + + case OPT_SEND_HEIGHT: + tx_size.h = (unsigned)atoi(pj_optarg); + break; + + case OPT_RECV_PT: + rx_pt = (pj_int8_t)atoi(pj_optarg); + break; + + case OPT_SEND_PT: + tx_pt = (pj_int8_t)atoi(pj_optarg); + break; + +#if defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0) + case OPT_USE_SRTP: + use_srtp = PJ_TRUE; + if (pj_optarg) { + pj_strset(&srtp_crypto_suite, pj_optarg, strlen(pj_optarg)); + } else { + srtp_crypto_suite = pj_str("AES_CM_128_HMAC_SHA1_80"); + } + break; + + case OPT_SRTP_TX_KEY: + tmp_key_len = hex_string_to_octet_string(tmp_tx_key, pj_optarg, + strlen(pj_optarg)); + pj_strset(&srtp_tx_key, tmp_tx_key, tmp_key_len/2); + break; + + case OPT_SRTP_RX_KEY: + tmp_key_len = hex_string_to_octet_string(tmp_rx_key, pj_optarg, + strlen(pj_optarg)); + pj_strset(&srtp_rx_key, tmp_rx_key, tmp_key_len/2); + break; +#endif + + case OPT_HELP: + usage(); + return 1; + + default: + printf("Invalid options %s\n", argv[pj_optind]); + return 1; + } + + } + + + /* Verify arguments. */ + if (dir & PJMEDIA_DIR_ENCODING) { + if (remote_addr.sin_addr.s_addr == 0) { + printf("Error: remote address must be set\n"); + return 1; + } + } + + if (play_file.file_name != NULL && dir != PJMEDIA_DIR_ENCODING) { + printf("Direction is set to --send-only because of --play-file\n"); + dir = PJMEDIA_DIR_ENCODING; + } + +#if defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0) + /* SRTP validation */ + if (use_srtp) { + if (!srtp_tx_key.slen || !srtp_rx_key.slen) + { + printf("Error: Key for each SRTP stream direction must be set\n"); + return 1; + } + } +#endif + + /* Must create a pool factory before we can allocate any memory. */ + pj_caching_pool_init(&cp, &pj_pool_factory_default_policy, 0); + + /* + * Initialize media endpoint. + * This will implicitly initialize PJMEDIA too. + */ + status = pjmedia_endpt_create(&cp.factory, NULL, 1, &med_endpt); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* Create memory pool for application purpose */ + pool = pj_pool_create( &cp.factory, /* pool factory */ + "app", /* pool name. */ + 4000, /* init size */ + 4000, /* increment size */ + NULL /* callback on error */ + ); + + /* Init video format manager */ + pjmedia_video_format_mgr_create(pool, 64, 0, NULL); + + /* Init video converter manager */ + pjmedia_converter_mgr_create(pool, NULL); + + /* Init event manager */ + pjmedia_event_mgr_create(pool, 0, NULL); + + /* Init video codec manager */ + pjmedia_vid_codec_mgr_create(pool, NULL); + + /* Init video subsystem */ + pjmedia_vid_dev_subsys_init(&cp.factory); + + /* Register all supported codecs */ + status = init_codecs(&cp.factory); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + + /* Find which codec to use. */ + if (codec_id) { + unsigned count = 1; + pj_str_t str_codec_id = pj_str(codec_id); + + status = pjmedia_vid_codec_mgr_find_codecs_by_id(NULL, + &str_codec_id, &count, + &codec_info, NULL); + if (status != PJ_SUCCESS) { + printf("Error: unable to find codec %s\n", codec_id); + return 1; + } + } else { + static pjmedia_vid_codec_info info[1]; + unsigned count = PJ_ARRAY_SIZE(info); + + /* Default to first codec */ + pjmedia_vid_codec_mgr_enum_codecs(NULL, &count, info, NULL); + codec_info = &info[0]; + } + + /* Get codec default param for info */ + status = pjmedia_vid_codec_mgr_get_default_param(NULL, codec_info, + &codec_param); + pj_assert(status == PJ_SUCCESS); + + /* Set outgoing video size */ + if (tx_size.w && tx_size.h) + codec_param.enc_fmt.det.vid.size = tx_size; + +#if DEF_RENDERER_WIDTH && DEF_RENDERER_HEIGHT + /* Set incoming video size */ + if (DEF_RENDERER_WIDTH > codec_param.dec_fmt.det.vid.size.w) + codec_param.dec_fmt.det.vid.size.w = DEF_RENDERER_WIDTH; + if (DEF_RENDERER_HEIGHT > codec_param.dec_fmt.det.vid.size.h) + codec_param.dec_fmt.det.vid.size.h = DEF_RENDERER_HEIGHT; +#endif + + if (play_file.file_name) { + pjmedia_video_format_detail *file_vfd; + pjmedia_clock_param clock_param; + char fmt_name[5]; + + /* Create file player */ + status = create_file_player(pool, play_file.file_name, &play_port); + if (status != PJ_SUCCESS) + goto on_exit; + + /* Collect format info */ + file_vfd = pjmedia_format_get_video_format_detail(&play_port->info.fmt, + PJ_TRUE); + PJ_LOG(2, (THIS_FILE, "Reading video stream %dx%d %s @%.2ffps", + file_vfd->size.w, file_vfd->size.h, + pjmedia_fourcc_name(play_port->info.fmt.id, fmt_name), + (1.0*file_vfd->fps.num/file_vfd->fps.denum))); + + /* Allocate file read buffer */ + play_file.read_buf_size = PJMEDIA_MAX_VIDEO_ENC_FRAME_SIZE; + play_file.read_buf = pj_pool_zalloc(pool, play_file.read_buf_size); + + /* Create decoder, if the file and the stream uses different codec */ + if (codec_info->fmt_id != (pjmedia_format_id)play_port->info.fmt.id) { + const pjmedia_video_format_info *dec_vfi; + pjmedia_video_apply_fmt_param dec_vafp = {0}; + const pjmedia_vid_codec_info *codec_info2; + pjmedia_vid_codec_param codec_param2; + + /* Find decoder */ + status = pjmedia_vid_codec_mgr_get_codec_info2(NULL, + play_port->info.fmt.id, + &codec_info2); + if (status != PJ_SUCCESS) + goto on_exit; + + /* Init decoder */ + status = pjmedia_vid_codec_mgr_alloc_codec(NULL, codec_info2, + &play_decoder); + if (status != PJ_SUCCESS) + goto on_exit; + + status = play_decoder->op->init(play_decoder, pool); + if (status != PJ_SUCCESS) + goto on_exit; + + /* Open decoder */ + status = pjmedia_vid_codec_mgr_get_default_param(NULL, codec_info2, + &codec_param2); + if (status != PJ_SUCCESS) + goto on_exit; + + codec_param2.dir = PJMEDIA_DIR_DECODING; + status = play_decoder->op->open(play_decoder, &codec_param2); + if (status != PJ_SUCCESS) + goto on_exit; + + /* Get decoder format info and apply param */ + dec_vfi = pjmedia_get_video_format_info(NULL, + codec_info2->dec_fmt_id[0]); + if (!dec_vfi || !dec_vfi->apply_fmt) { + status = PJ_ENOTSUP; + goto on_exit; + } + dec_vafp.size = file_vfd->size; + (*dec_vfi->apply_fmt)(dec_vfi, &dec_vafp); + + /* Allocate buffer to receive decoder output */ + play_file.dec_buf_size = dec_vafp.framebytes; + play_file.dec_buf = pj_pool_zalloc(pool, play_file.dec_buf_size); + } + + /* Create player clock */ + clock_param.usec_interval = PJMEDIA_PTIME(&file_vfd->fps); + clock_param.clock_rate = codec_info->clock_rate; + status = pjmedia_clock_create2(pool, &clock_param, + PJMEDIA_CLOCK_NO_HIGHEST_PRIO, + &clock_cb, &play_file, &play_clock); + if (status != PJ_SUCCESS) + goto on_exit; + + /* Override stream codec param for encoding direction */ + codec_param.enc_fmt.det.vid.size = file_vfd->size; + codec_param.enc_fmt.det.vid.fps = file_vfd->fps; + + } else { + pjmedia_vid_port_param_default(&vpp); + + /* Set as active for all video devices */ + vpp.active = PJ_TRUE; + + /* Create video device port. */ + if (dir & PJMEDIA_DIR_ENCODING) { + /* Create capture */ + status = pjmedia_vid_dev_default_param( + pool, + PJMEDIA_VID_DEFAULT_CAPTURE_DEV, + &vpp.vidparam); + if (status != PJ_SUCCESS) + goto on_exit; + + pjmedia_format_copy(&vpp.vidparam.fmt, &codec_param.enc_fmt); + vpp.vidparam.fmt.id = codec_param.dec_fmt.id; + vpp.vidparam.dir = PJMEDIA_DIR_CAPTURE; + + status = pjmedia_vid_port_create(pool, &vpp, &capture); + if (status != PJ_SUCCESS) + goto on_exit; + } + + if (dir & PJMEDIA_DIR_DECODING) { + /* Create renderer */ + status = pjmedia_vid_dev_default_param( + pool, + PJMEDIA_VID_DEFAULT_RENDER_DEV, + &vpp.vidparam); + if (status != PJ_SUCCESS) + goto on_exit; + + pjmedia_format_copy(&vpp.vidparam.fmt, &codec_param.dec_fmt); + vpp.vidparam.dir = PJMEDIA_DIR_RENDER; + vpp.vidparam.disp_size = vpp.vidparam.fmt.det.vid.size; + vpp.vidparam.flags |= PJMEDIA_VID_DEV_CAP_OUTPUT_WINDOW_FLAGS; + vpp.vidparam.window_flags = PJMEDIA_VID_DEV_WND_BORDER | + PJMEDIA_VID_DEV_WND_RESIZABLE; + + status = pjmedia_vid_port_create(pool, &vpp, &renderer); + if (status != PJ_SUCCESS) + goto on_exit; + } + } + + /* Set to ignore fmtp */ + codec_param.ignore_fmtp = PJ_TRUE; + + /* Create stream based on program arguments */ + status = create_stream(pool, med_endpt, codec_info, &codec_param, + dir, rx_pt, tx_pt, local_port, &remote_addr, +#if defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0) + use_srtp, &srtp_crypto_suite, + &srtp_tx_key, &srtp_rx_key, +#endif + &stream); + if (status != PJ_SUCCESS) + goto on_exit; + + /* Get the port interface of the stream */ + status = pjmedia_vid_stream_get_port(stream, PJMEDIA_DIR_ENCODING, + &enc_port); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + status = pjmedia_vid_stream_get_port(stream, PJMEDIA_DIR_DECODING, + &dec_port); + PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1); + + /* Start streaming */ + status = pjmedia_vid_stream_start(stream); + if (status != PJ_SUCCESS) + goto on_exit; + + /* Start renderer */ + if (renderer) { + status = pjmedia_vid_port_connect(renderer, dec_port, PJ_FALSE); + if (status != PJ_SUCCESS) + goto on_exit; + status = pjmedia_vid_port_start(renderer); + if (status != PJ_SUCCESS) + goto on_exit; + } + + /* Start capture */ + if (capture) { + status = pjmedia_vid_port_connect(capture, enc_port, PJ_FALSE); + if (status != PJ_SUCCESS) + goto on_exit; + status = pjmedia_vid_port_start(capture); + if (status != PJ_SUCCESS) + goto on_exit; + } + + /* Start playing file */ + if (play_file.file_name) { + +#if HAS_LOCAL_RENDERER_FOR_PLAY_FILE + /* Create local renderer */ + pjmedia_vid_port_param_default(&vpp); + vpp.active = PJ_FALSE; + status = pjmedia_vid_dev_default_param( + pool, + PJMEDIA_VID_DEFAULT_RENDER_DEV, + &vpp.vidparam); + if (status != PJ_SUCCESS) + goto on_exit; + + vpp.vidparam.dir = PJMEDIA_DIR_RENDER; + pjmedia_format_copy(&vpp.vidparam.fmt, &codec_param.dec_fmt); + vpp.vidparam.fmt.det.vid.size = play_port->info.fmt.det.vid.size; + vpp.vidparam.fmt.det.vid.fps = play_port->info.fmt.det.vid.fps; + vpp.vidparam.disp_size = vpp.vidparam.fmt.det.vid.size; + vpp.vidparam.flags |= PJMEDIA_VID_DEV_CAP_OUTPUT_WINDOW_FLAGS; + vpp.vidparam.window_flags = PJMEDIA_VID_DEV_WND_BORDER | + PJMEDIA_VID_DEV_WND_RESIZABLE; + + status = pjmedia_vid_port_create(pool, &vpp, &renderer); + if (status != PJ_SUCCESS) + goto on_exit; + status = pjmedia_vid_port_start(renderer); + if (status != PJ_SUCCESS) + goto on_exit; +#endif + + /* Init play file data */ + play_file.play_port = play_port; + play_file.stream_port = enc_port; + play_file.decoder = play_decoder; + if (renderer) { + play_file.renderer = pjmedia_vid_port_get_passive_port(renderer); + } + + status = pjmedia_clock_start(play_clock); + if (status != PJ_SUCCESS) + goto on_exit; + } + + /* Done */ + + if (dir == PJMEDIA_DIR_DECODING) + printf("Stream is active, dir is recv-only, local port is %d\n", + local_port); + else if (dir == PJMEDIA_DIR_ENCODING) + printf("Stream is active, dir is send-only, sending to %s:%d\n", + pj_inet_ntoa(remote_addr.sin_addr), + pj_ntohs(remote_addr.sin_port)); + else + printf("Stream is active, send/recv, local port is %d, " + "sending to %s:%d\n", + local_port, + pj_inet_ntoa(remote_addr.sin_addr), + pj_ntohs(remote_addr.sin_port)); + + if (dir & PJMEDIA_DIR_ENCODING) + PJ_LOG(2, (THIS_FILE, "Sending %dx%d %.*s @%.2ffps", + codec_param.enc_fmt.det.vid.size.w, + codec_param.enc_fmt.det.vid.size.h, + codec_info->encoding_name.slen, + codec_info->encoding_name.ptr, + (1.0*codec_param.enc_fmt.det.vid.fps.num/ + codec_param.enc_fmt.det.vid.fps.denum))); + + for (;;) { + char tmp[10]; + + puts(""); + puts("Commands:"); + puts(" q Quit"); + puts(""); + + printf("Command: "); fflush(stdout); + + if (fgets(tmp, sizeof(tmp), stdin) == NULL) { + puts("EOF while reading stdin, will quit now.."); + break; + } + + if (tmp[0] == 'q') + break; + + } + + + + /* Start deinitialization: */ +on_exit: + + /* Stop video devices */ + if (capture) + pjmedia_vid_port_stop(capture); + if (renderer) + pjmedia_vid_port_stop(renderer); + + /* Stop and destroy file clock */ + if (play_clock) { + pjmedia_clock_stop(play_clock); + pjmedia_clock_destroy(play_clock); + } + + /* Destroy file reader/player */ + if (play_port) + pjmedia_port_destroy(play_port); + + /* Destroy file decoder */ + if (play_decoder) { + play_decoder->op->close(play_decoder); + pjmedia_vid_codec_mgr_dealloc_codec(NULL, play_decoder); + } + + /* Destroy video devices */ + if (capture) + pjmedia_vid_port_destroy(capture); + if (renderer) + pjmedia_vid_port_destroy(renderer); + + /* Destroy stream */ + if (stream) { + pjmedia_transport *tp; + + tp = pjmedia_vid_stream_get_transport(stream); + pjmedia_vid_stream_destroy(stream); + + pjmedia_transport_close(tp); + } + + /* Deinit codecs */ + deinit_codecs(); + + /* Shutdown video subsystem */ + pjmedia_vid_dev_subsys_shutdown(); + + /* Destroy event manager */ + pjmedia_event_mgr_destroy(NULL); + + /* Release application pool */ + pj_pool_release( pool ); + + /* Destroy media endpoint. */ + pjmedia_endpt_destroy( med_endpt ); + + /* Destroy pool factory */ + pj_caching_pool_destroy( &cp ); + + /* Shutdown PJLIB */ + pj_shutdown(); + + return (status == PJ_SUCCESS) ? 0 : 1; +} + + +#else + +int main(int argc, char *argv[]) +{ + PJ_UNUSED_ARG(argc); + PJ_UNUSED_ARG(argv); + puts("Error: this sample requires video capability (PJMEDIA_HAS_VIDEO == 1)"); + return -1; +} + +#endif /* PJMEDIA_HAS_VIDEO */ |