这一篇,我们先讲一些跟ice有关的接口函数的设计
我们仿照icedemo写两个结构体
struct zktrans_nat_cfg
{
/** STUN srv */
const char *stun_srv;
int stun_port;
/** TURN srv */
const char *turn_srv;
int turn_port;
const char *turn_username;
const char *turn_passwd;
/** callback */
PFN_zktrans_nat_cb_rtp cb_rtp;
PFN_zktrans_nat_cb_rtcp cb_rtcp;
PFN_zktrans_nat_cb_state cb_state;
void *userptr;
};
struct zktrans_nat_t
{
zktrans_nat_cfg cfg; // 用户配置信息
pj_caching_pool cp;
pj_pool_t *pool;
pj_ice_strans_cfg ice_cfg;
pj_ice_strans *ice_strans;
/** 保存本地, 对方 sdp 字符串 */
char *local_sdp, *remote_sdp;
pj_thread_t *thread;
pj_bool_t thread_quit_flag;
char *stun_srv, *turn_srv, *turn_user, *turn_passwd;
PFN_zktrans_nat_cb_rtp rtp_cb;
PFN_zktrans_nat_cb_rtp rtcp_cb;
void *userptr;
pj_thread_t *th_rtp, *th_rtcp;
pj_thread_desc th_rtp_desc, th_rtcp_desc;
pj_sockaddr def_rtp, def_rtcp;
};
1 结构体配置及库初始化 这一步主要围绕zktrans_nat_t 结构体中的ice_cfg这个变量展开。
int zktrans_nat_send_rtp(zktrans_nat_t *trans,const char *data,int len)
{
static bool _inited = false;
if(!_inited)// lib initialize
{
pj_init();
pjlib_util_init();
pjnath_init();
_inited = true;
}
zktrans_nat_t *ins =new zktrans_nat_t;
ins->cfg =*cfg;
ins->ice_strans=0;
pj_cacing_pool_init(&init->cp,0,0);
//default ice stream trans cfg
pj_ice_strans_cfg_default(&ins->ice_cfg);
ins->ice_cfg.stun.cfg.pf=&ins->cp.factory;
//ins->pool=pj_pool_create(&ins->cp.factory,"zktrans_nathlp",512,512,0);
//time heap
pj_timer_heap_create(ins->pool,100,&ins->ice_cfg.stun.cfg.timer_heap);//创建定时器堆,最多可放入100个定时器
//ioqueue
pj_ioqueue_create(ins->pool,16,&ins->ice_cfg.stun_cfg.ioqueue);
ins->quit=false;
// start worker thread for timer & ioqueue
pj_thread_create(ins->pool,"zktrans_nathlp poll thread",_poll_worker_thread,ins,0,0,&ins->worker_thread);
ins->local_stun_server=0,ins->local_turn_server=0,ins->local_turn_user=0,ins->local_turn_passwd=0;
if(ins->cfg.stun_srv)
{
ins->local_stun_server=strdup(ins->cfg.stun_srv);
ins->ice_cfg.stun.server.ptr=ins->local_stun_server;
ins->ice_cfg.stun.server.slen=strlen(ins->local_stun_server);
ins->ice_cfg.stun.port=ins->cfg.stun_port;
}
if(ins->cfg.turn_srv)
{
ins->local_turn_server=strdup(ins->cfg.turn_srv);
ins->local_turn_user=strdup(ins->cfg.turn_username);
ins->local_turn_passwd=strdup(ins->cfg.turn_passwd);
ins->ice_cfg.turn.server.ptr=ins->local_turn_server;
ins->ice_cfg.turn.server.slen=strlen(ins->local_turn.server);
ins->ice_cfg.turn.port=ins->cfg.turn_port;
ins->ice_cfg.turn.auth_cred.type= PJ_STUN_AUTH_CRED_STATIC;
ins->ice_cfg.turn.auth_cred.data.static_cred.username.ptr = ins->local_turn_user;
ins->ice_cfg.turn.auth_cred.data.static_cred.username.slen = strlen(ins->local_turn_user);
ins->ice_cfg.turn.auth_cred.data.static_cred.data_type = PJ_STUN_PASSWD_PLAIN;
ins->ice_cfg.turn.auth_cred.data.static_cred.data.ptr = ins->local_turn_passwd;
ins->ice_cfg.turn.auth_cred.data.static_cred.data.slen = strlen(ins->local_turn_passwd);
ins->local_sdp=0;
ins->remote_sdp=0;
}
return ins;
}
2 通过pj_ice_strans_create收集候选者信息
void zktrans_nat_start(zktrans_nat_t *ins,int caller)
{
assert(ins->ice_strans==0);
pj_ice_strans_cb *cb=new pj_ice_strans_cb;
cb->on_rx_data=cb_on_rx_data;
cb->on_ice_complete=cb_on_ice_complete;
ins->caller=caller;
//通过pj_ice_strans_create收集候选者信息
pj_ice_strans_create("zktrans_nat",&ins->ice_cfg,2,ins,cb,&ins->ice_strans);
//这个函数用ins->ice_strans记录创建的成果,然后将ins作为数据信息传入,我们要在后面的cb_on_ice_complete中获取这个数据信息
}
这个函数执行完,我们本方的候选ip端口对已经收集好了,我们看看cb_on_rx_data和cb_on_ice_complete这两个函数。
其中cb_on_rx_data用于接收跟ice的消息数据无关的应用程序数据。例如RTP/RTCP。而cb_on_ice_complete用来报告ICE各种操作的结果。也就是说我们收集完候选者信息之后,也可以在这里得到报告。下面我们看看实现吧。
(1)先看cb_on_ice_complete
static void cb_on_ice_complete (pj_ice_strans *strans,pj_ice_strans_op op,pj_status_t status)
{
// 我们通过op参数获取ICE状态完成信息
const char *opname = op == PJ_ICE_STRANS_OP_INIT ? "initialization":op == PJ_ICE_STRANS_OP_NEGOTIATION? "negotiation":"unknown_op";
//下面我们获取数据信息
zktrans_nat_t *ctx=(zktrans_nat_t *)pj_ice_strans_get_user_data(strans);
if(op==PJ_ICE_STRANS_OP_INIT &&status==PJSUCCESS)
{
//说明收集候选者任务已经完成,启动ice
pj_ice_strans_init_ice(ctx->ice_strans,ctx->caller?PJ_ICE_SESS_ROLE_CONTROLLING:PJ_ICE_SESS_CONTROLLED,0,0);
//开始建立本地的sdp信息
_bulid_local_sdp(ctx);
}
if(status == PJ_SUCCESS)
{
const char *info="OK";
fprintf(stdout,"[%s] ICE %s successful \n",_FUNCTION_,opname);
if(op== PJ_ICE_STRANS_OP_INIT)
ctx->cfg.cb_state(ctx,1,0,info,ctx->cfg.userptr);
else if(op == PJ_ICE_STRANS_OP_NEGOTIATION)
ctx->cfg.cb_state(ctx,2,0,info,ctx->cfg.usertpr);
else if(op== PJ_ICE_STRANS_OP_KEEP_ALIVE)
ctx->cfg.cb_state(ctx,3,0,info,ctx->cfg.userptr);
else
ctx->cfg.cb_state(ctx,100+op,0,info,ctx->cfg.userptr);
}
else{
char errmsg[PJ_ERR_MSG_SIZE];
pj_strerror(status, errmsg, sizeof(errmsg));
fprintf(stderr, "[%s] ICE %s failed: %s\n", __FUNCTION__, opname, errmsg);
if (op == PJ_ICE_STRANS_OP_INIT)
ctx->cfg.cb_state(ctx, 1, status, errmsg, ctx->cfg.userptr);
else if (op == PJ_ICE_STRANS_OP_NEGOTIATION)
ctx->cfg.cb_state(ctx, 2, status, errmsg, ctx->cfg.userptr);
else if (op == PJ_ICE_STRANS_OP_KEEP_ALIVE)
ctx->cfg.cb_state(ctx, 3, status, errmsg, ctx->cfg.userptr);
else {
ctx->cfg.cb_state(ctx, 100+op, status, errmsg, ctx->cfg.userptr);
}
}
}
(2)再看看cb_on_rx_data
static void cb_on_rx_data (pj_ice_strans *strans,
unsigned comp_id,
void *pkt, pj_size_t size,
const pj_sockaddr_t *from,
unsigned from_len)
{
// 收到的 rtp, rtcp 数据
zktrans_nat_t *trans = (zktrans_nat_t *)pj_ice_strans_get_user_data(strans);
if (comp_id == 1 && trans->cfg.cb_rtp)
trans->cfg.cb_rtp(trans, pkt, size, trans->cfg.userptr);
else if (comp_id == 2 && trans->cfg.cb_rtcp)
trans->cfg.cb_rtcp(trans, pkt, size, trans->cfg.userptr);
}
我们再看看如何建立本地的sdp,其实就把自己的ice的用户名,密码等组成sdp的格式放到结构体的一个数组中存好。
static void _build_local_sdp (zktrans_nat_t *trans)
{
size_t pos = 0;
pj_ice_sess_cand cands[PJ_ICE_ST_MAX_CAND];
char ipaddr[128];
trans->local_sdp = (char*)malloc(4096); // 足够了 :)
// 实际这里的 v o s t 无用, 仅仅为了符合 sdp 要求
strcpy(trans->local_sdp, "v=0\r\no=- 3414953978 3414953978 IN IP4 localhost\r\ns=ice\r\nt=0 0\r\n");
pos = strlen(trans->local_sdp);
// ice-ufrag & ice-pwd
pj_str_t ufrag, pwd;
pj_ice_strans_get_ufrag_pwd(trans->ice_strans, &ufrag, &pwd, 0, 0);
sprintf(trans->local_sdp+pos, "a=ice-ufrag:%.*s\r\na=ice-pwd:%.*s\r\n",
(int)ufrag.slen, ufrag.ptr,
(int)pwd.slen, pwd.ptr);
pos = strlen(trans->local_sdp);
// default candidate for rtp
pj_status_t rc = pj_ice_strans_get_def_cand(trans->ice_strans, 1, &cands[0]);
if (rc != PJ_SUCCESS) {
fprintf(stderr, "[%s] pj_ice_strans_get_def_cand for RTP err\n", __FUNCTION__);
exit(-1);
}
sprintf(trans->local_sdp+pos, "m=%s %d RTP/AVP %d\r\n"
"c=IN IP4 %s\r\n",
"audio", (int)pj_sockaddr_get_port(&cands[0].addr),
91,
pj_sockaddr_print(&cands[0].addr, ipaddr, sizeof(ipaddr), 0));
pos = strlen(trans->local_sdp);
// default candidate for rtcp
rc = pj_ice_strans_get_def_cand(trans->ice_strans, 2, &cands[0]);
if (rc != PJ_SUCCESS) {
fprintf(stderr, "[%s] pj_ice_strans_get_def_cand for RTCP err\n", __FUNCTION__);
exit(-1);
}
sprintf(trans->local_sdp+pos, "a=rtcp:%d IN IP4 %s\r\n",
(int)pj_sockaddr_get_port(&cands[0].addr),
pj_sockaddr_print(&cands[0].addr, ipaddr, sizeof(ipaddr), 0));
pos = strlen(trans->local_sdp);
// all candidate attrs of RTP
unsigned cnt = PJ_ICE_ST_MAX_CAND;
rc = pj_ice_strans_enum_cands(trans->ice_strans, 1, &cnt, cands);
pj_assert(rc == PJ_SUCCESS);
for (int i = 0; i < cnt; i++) {
sprintf(trans->local_sdp+pos, "a=candidate:%.*s %u UDP %u %s %u typ %s\r\n",
(int)cands[i].foundation.slen, cands[i].foundation.ptr,
1, cands[i].prio,
pj_sockaddr_print(&cands[i].addr, ipaddr, sizeof(ipaddr), 0),
(unsigned)pj_sockaddr_get_port(&cands[i].addr),
pj_ice_get_cand_type_name(cands[i].type));
pos = strlen(trans->local_sdp);
}
// all candidate attrs of RTCP
cnt = PJ_ICE_ST_MAX_CAND;
rc = pj_ice_strans_enum_cands(trans->ice_strans, 2, &cnt, cands);
pj_assert(rc == PJ_SUCCESS);
for (int i = 0; i < cnt; i++) {
sprintf(trans->local_sdp+pos, "a=candidate:%.*s %u UDP %u %s %u typ %s\r\n",
(int)cands[i].foundation.slen, cands[i].foundation.ptr,
2, cands[i].prio,
pj_sockaddr_print(&cands[i].addr, ipaddr, sizeof(ipaddr), 0),
(unsigned)pj_sockaddr_get_port(&cands[i].addr),
pj_ice_get_cand_type_name(cands[i].type));
pos = strlen(trans->local_sdp);
}
trans->local_sdp[pos] = 0;
}
3 协商出最佳路径,这里要好好说说了。
假设我们是clientA,跟clientB要通信。clientA收集好自己候选者IP端口队后,要和clientB的候选者进行配对。找出最好的一条路径。这就是所谓的协商。那么我们需要知道clientB的候选者们,还有clientB的用户名密码,这些信息都得通过sdp消息格式获取
void zktrans_nat_negotiate (zktrans_nat_t *ins, const char *sdp)//这里的sdp就是clientB的sdp信息
{
xfree(ins->remote_sdp);
ins->remote_sdp=strdup(sdp);//存好
char ufrag[128], pwd[128];
char default_rtp[80], default_rtcp[80];
int default_rtp_port, default_rtcp_port;
pj_ice_sess_cand cands[PJ_ICE_ST_MAX_CAND];
int cnt = _parse_sdp(ins, sdp, ufrag, pwd, default_rtp, &default_rtp_port,
default_rtcp, &default_rtcp_port, cands);
pj_str_t user = pj_str(ufrag);
pj_str_t passwd = pj_str(pwd);
到这里我们就获取了clientB的候选者,用户,密码了,另外我们还获取了rtp和rtcp的端口,地址,我们存好,以后好用来向clientB发送rtp,rtcp数据
// save def_rtp, def_rtcp
pj_sockaddr_init(pj_AF_INET(), &ins->def_rtp, 0, 0);
pj_sockaddr_init(pj_AF_INET(), &ins->def_rtcp, 0, 0);
pj_str_t ip = pj_str(default_rtp);
pj_sockaddr_set_str_addr(pj_AF_INET(), &ins->def_rtp, &ip);
ip = pj_str(default_rtcp);
pj_sockaddr_set_str_addr(pj_AF_INET(), &ins->def_rtcp, &ip);
pj_sockaddr_set_port(&ins->def_rtp, default_rtp_port);
pj_sockaddr_set_port(&ins->def_rtcp, default_rtcp_port);
pj_ice_sess_options opts;
pj_ice_strans_get_options(ins->ice_strans, &opts);
opts.controlled_agent_want_nom_timeout = -1;
pj_ice_strans_set_options(ins->ice_strans, &opts);
}
4 协商完成后,我们可以发送rtp,rtcp数据了
int zktrans_nat_send_rtp (zktrans_nat_t *trans, const char *data, int len)
{
if (!pj_thread_is_registered()) {
pj_thread_register("zknath_rtp_sender_thread", trans->th_rtp_desc, &trans->th_rtp);
}
// sendto()
pj_status_t rc = pj_ice_strans_sendto(trans->ice_strans, 1,
data, len, &trans->def_rtp,
pj_sockaddr_get_len(&trans->def_rtp));
if (rc == PJ_SUCCESS)
return len;
return -1;
}