pjsip源码解析之pjsip中makeCall外呼流程简析一

本文只是对外呼流程的简单分析,只是对呼叫的流程进行梳理。流程主要跟踪到通过网络传输出去invite为止。

欢迎进入q群761341723,大家一起讨论问题。hpng该网站为我自己网站,一些想法也会发到这里

pjsua_call_make_call

pjsua_call_make_call 函数定义位于 pjsip/src/pjsua-lib/pjsua_call.c 中,其中内容如下所示:

/*
 * Make outgoing call to the specified URI using the specified account.
 */
PJ_DEF(pj_status_t) pjsua_call_make_call(pjsua_acc_id acc_id,
                                         const pj_str_t *dest_uri,
                                         const pjsua_call_setting *opt,
                                         void *user_data,
                                         const pjsua_msg_data *msg_data,
                                         pjsua_call_id *p_call_id)
{
    pj_pool_t *tmp_pool = NULL;
    pjsip_dialog *dlg = NULL;
    pjsua_acc *acc;
    pjsua_call *call = NULL;
    int call_id = -1;
    pj_str_t contact;
    pj_status_t status;

    /* Check that account is valid */
    PJ_ASSERT_RETURN(acc_id>=0 && acc_id<(int)PJ_ARRAY_SIZE(pjsua_var.acc),
                     PJ_EINVAL);

    /* Check arguments */
    PJ_ASSERT_RETURN(dest_uri, PJ_EINVAL);

    PJ_LOG(4,(THIS_FILE, "Making call with acc #%d to %.*s", acc_id,
              (int)dest_uri->slen, dest_uri->ptr));

    pj_log_push_indent();

    PJSUA_LOCK();

    acc = &pjsua_var.acc[acc_id];
    if (!acc->valid) {
        pjsua_perror(THIS_FILE, "Unable to make call because account "
                     "is not valid", PJ_EINVALIDOP);
        status = PJ_EINVALIDOP;
        goto on_error;
    }

    /* Find free call slot. */
    call_id = alloc_call_id();

    if (call_id == PJSUA_INVALID_ID) {
        pjsua_perror(THIS_FILE, "Error making call", PJ_ETOOMANY);
        status = PJ_ETOOMANY;
        goto on_error;
    }

    /* Clear call descriptor */
    reset_call(call_id);

    call = &pjsua_var.calls[call_id];

    /* Associate session with account */
    call->acc_id = acc_id;
    call->call_hold_type = acc->cfg.call_hold_type;

    /* Generate per-session RTCP CNAME, according to RFC 7022. */
    pj_create_random_string(call->cname_buf, call->cname.slen);

    /* Apply call setting */
    status = apply_call_setting(call, opt, NULL);
    if (status != PJ_SUCCESS) {
        pjsua_perror(THIS_FILE, "Failed to apply call setting", status);
        goto on_error;
    }
    
    /* Create sound port if none is instantiated, to check if sound device
     * can be used. But only do this with the conference bridge, as with
     * audio switchboard (i.e. APS-Direct), we can only open the sound
     * device once the correct format has been known
     */
    if (!pjsua_var.is_mswitch && pjsua_var.snd_port==NULL &&
        pjsua_var.null_snd==NULL && !pjsua_var.no_snd && call->opt.aud_cnt > 0)
    {
        status = pjsua_set_snd_dev(pjsua_var.cap_dev, pjsua_var.play_dev);
        if (status != PJ_SUCCESS)
            goto on_error;
    }

    /* Create temporary pool */
    tmp_pool = pjsua_pool_create("tmpcall10", 512, 256);

    /* Verify that destination URI is valid before calling
     * pjsua_acc_create_uac_contact, or otherwise there
     * a misleading "Invalid Contact URI" error will be printed
     * when pjsua_acc_create_uac_contact() fails.
     */
    if (1) {
        pjsip_uri *uri;
        pj_str_t dup;

        pj_strdup_with_null(tmp_pool, &dup, dest_uri);
        uri = pjsip_parse_uri(tmp_pool, dup.ptr, dup.slen, 0);

        if (uri == NULL) {
            pjsua_perror(THIS_FILE, "Unable to make call",
                         PJSIP_EINVALIDREQURI);
            status = PJSIP_EINVALIDREQURI;
            goto on_error;
        }
    }

    /* Mark call start time. */
    pj_gettimeofday(&call->start_time);

    /* Reset first response time */
    call->res_time.sec = 0;

    /* Create suitable Contact header unless a Contact header has been
     * set in the account.
     */
    if (acc->contact.slen) {
        contact = acc->contact;
    } else {
        status = pjsua_acc_create_uac_contact(tmp_pool, &contact,
                                              acc_id, dest_uri);
        if (status != PJ_SUCCESS) {
            pjsua_perror(THIS_FILE, "Unable to generate Contact header",
                         status);
            goto on_error;
        }
    }

    /* Create outgoing dialog: */
    status = pjsip_dlg_create_uac( pjsip_ua_instance(),
                                   (msg_data && msg_data->local_uri.slen?
                                    &msg_data->local_uri: &acc->cfg.id),
                                   &contact,
                                   dest_uri,
                                   (msg_data && msg_data->target_uri.slen?
                                    &msg_data->target_uri: dest_uri),
                                   &dlg);
    if (status != PJ_SUCCESS) {
        pjsua_perror(THIS_FILE, "Dialog creation failed", status);
        goto on_error;
    }

    /* Increment the dialog's lock otherwise when invite session creation
     * fails the dialog will be destroyed prematurely.
     */
    pjsip_dlg_inc_lock(dlg);

    dlg_set_via(dlg, acc);

    /* Calculate call's secure level */
    call->secure_level = get_secure_level(acc_id, dest_uri);

    /* Attach user data */
    call->user_data = user_data;

    /* Store variables required for the callback after the async
     * media transport creation is completed.
     */
    if (msg_data) {
        call->async_call.call_var.out_call.msg_data = pjsua_msg_data_clone(
                                                          dlg->pool, msg_data);
    }
    call->async_call.dlg = dlg;

    /* Temporarily increment dialog session. Without this, dialog will be
     * prematurely destroyed if dec_lock() is called on the dialog before
     * the invite session is created.
     */
    pjsip_dlg_inc_session(dlg, &pjsua_var.mod);

    if ((call->opt.flag & PJSUA_CALL_NO_SDP_OFFER) == 0) {
        /* Init media channel */
        status = pjsua_media_channel_init(call->index, PJSIP_ROLE_UAC,
                                          call->secure_level, dlg->pool,
                                          NULL, NULL, PJ_TRUE,
                                          &on_make_call_med_tp_complete);
    }
    if (status == PJ_SUCCESS) {
        status = on_make_call_med_tp_complete(call->index, NULL);
        if (status != PJ_SUCCESS)
            goto on_error;
    } else if (status != PJ_EPENDING) {
        pjsua_perror(THIS_FILE, "Error initializing media channel", status);
        pjsip_dlg_dec_session(dlg, &pjsua_var.mod);
        goto on_error;
    }

    /* Done. */

    if (p_call_id)
        *p_call_id = call_id;

    pjsip_dlg_dec_lock(dlg);
    pj_pool_release(tmp_pool);
    PJSUA_UNLOCK();

    pj_log_pop_indent();

    return PJ_SUCCESS;


on_error:
    if (dlg && call) {
        /* This may destroy the dialog */
        pjsip_dlg_dec_lock(dlg);
        call->async_call.dlg = NULL;
    }

    if (call_id != -1) {
        pjsua_media_channel_deinit(call_id);
        reset_call(call_id);
    }

    pjsua_check_snd_dev_idle();

    if (tmp_pool)
        pj_pool_release(tmp_pool);
    PJSUA_UNLOCK();

    pj_log_pop_indent();
    return status;
}
  • apply_call_setting: 处理通话的媒体流信息,防止有已经存在的媒体流
  • pjsua_set_snd_dev: 音频端口没有实例化的情况下则实例化
  • pjsua_acc_create_uac_contact: 创建 contact,如果没有则创建
  • pjsip_dlg_create_uac: 创建 uac 的 dialog
  • pjsua_media_channel_init: 初始化媒体流通道
  • on_make_call_med_tp_complete: 媒体流初始化完后回调函数。

媒体流初始化完后,我们才能知道能够提供的编解码能力,因而在发出 invite 之前需要等待媒体流初始化完成后。

on_make_call_med_tp_complete

on_make_call_med_tp_complete 函数的定义位于 pjsip/src/pjsua-lib/pjsua_call.c 中,其中内容如下所示:

/* Outgoing call callback when media transport creation is completed. */
static pj_status_t
on_make_call_med_tp_complete(pjsua_call_id call_id,
                             const pjsua_med_tp_state_info *info)
{
    pjmedia_sdp_session *offer = NULL;
    pjsip_inv_session *inv = NULL;
    pjsua_call *call = &pjsua_var.calls[call_id];
    pjsua_acc *acc = &pjsua_var.acc[call->acc_id];
    pjsip_dialog *dlg = call->async_call.dlg;
    unsigned options = 0;
    pjsip_tx_data *tdata;
    pj_bool_t cb_called = PJ_FALSE;
    pjsip_tpselector tp_sel;
    pj_status_t status = (info? info->status: PJ_SUCCESS);

    PJSUA_LOCK();

    /* Increment the dialog's lock otherwise when invite session creation
     * fails the dialog will be destroyed prematurely.
     */
    pjsip_dlg_inc_lock(dlg);

    /* Decrement dialog session. */
    pjsip_dlg_dec_session(dlg, &pjsua_var.mod);

    if (status != PJ_SUCCESS) {
        pj_str_t err_str;
        pj_ssize_t title_len;

        call->last_code = PJSIP_SC_TEMPORARILY_UNAVAILABLE;
        pj_strcpy2(&call->last_text, "Media init error: ");

        title_len = call->last_text.slen;
        err_str = pj_strerror(status, call->last_text_buf_ + title_len,
                              sizeof(call->last_text_buf_) - title_len);
        call->last_text.slen += err_str.slen;

        pjsua_perror(THIS_FILE, "Error initializing media channel", status);
        goto on_error;
    }

    /* pjsua_media_channel_deinit() has been called or
     * call has been hung up.
     */
    if (call->async_call.med_ch_deinit ||
        call->async_call.call_var.out_call.hangup)
    {
        PJ_LOG(4,(THIS_FILE, "Call has been hung up or media channel has "
                             "been deinitialized"));
        goto on_error;
    }

    /* Create offer */
    if ((call->opt.flag & PJSUA_CALL_NO_SDP_OFFER) == 0) {
        status = pjsua_media_channel_create_sdp(call->index, dlg->pool, NULL,
                                                &offer, NULL);
        if (status != PJ_SUCCESS) {
            pjsua_perror(THIS_FILE, "Error initializing media channel", status);
            goto on_error;
        }
    }

    /* Create the INVITE session: */
    options |= PJSIP_INV_SUPPORT_100REL;
    if (acc->cfg.require_100rel == PJSUA_100REL_MANDATORY)
        options |= PJSIP_INV_REQUIRE_100REL;
    if (acc->cfg.use_timer != PJSUA_SIP_TIMER_INACTIVE) {
        options |= PJSIP_INV_SUPPORT_TIMER;
        if (acc->cfg.use_timer == PJSUA_SIP_TIMER_REQUIRED)
            options |= PJSIP_INV_REQUIRE_TIMER;
        else if (acc->cfg.use_timer == PJSUA_SIP_TIMER_ALWAYS)
            options |= PJSIP_INV_ALWAYS_USE_TIMER;
    }
    if (acc->cfg.ice_cfg.enable_ice &&
        acc->cfg.ice_cfg.ice_opt.trickle != PJ_ICE_SESS_TRICKLE_DISABLED)
    {
        options |= PJSIP_INV_SUPPORT_TRICKLE_ICE;
    }

    status = pjsip_inv_create_uac( dlg, offer, options, &inv);
    if (status != PJ_SUCCESS) {
        pjsua_perror(THIS_FILE, "Invite session creation failed", status);
        goto on_error;
    }

    /* Init Session Timers */
    status = pjsip_timer_init_session(inv, &acc->cfg.timer_setting);
    if (status != PJ_SUCCESS) {
        pjsua_perror(THIS_FILE, "Session Timer init failed", status);
        goto on_error;
    }

    /* Create and associate our data in the session. */
    call->inv = inv;

    dlg->mod_data[pjsua_var.mod.id] = call;
    inv->mod_data[pjsua_var.mod.id] = call;

    /* Set dialog's transport based on acc's config. */
    pjsua_init_tpselector(call->acc_id, &tp_sel);
    pjsip_dlg_set_transport(dlg, &tp_sel);

    /* Set dialog Route-Set: */
    if (!pj_list_empty(&acc->route_set))
        pjsip_dlg_set_route_set(dlg, &acc->route_set);


    /* Set credentials: */
    if (acc->cred_cnt) {
        pjsip_auth_clt_set_credentials( &dlg->auth_sess,
                                        acc->cred_cnt, acc->cred);
    }

    /* Set authentication preference */
    pjsip_auth_clt_set_prefs(&dlg->auth_sess, &acc->cfg.auth_pref);

    /* Create initial INVITE: */

    status = pjsip_inv_invite(inv, &tdata);
    if (status != PJ_SUCCESS) {
        pjsua_perror(THIS_FILE, "Unable to create initial INVITE request",
                     status);
        goto on_error;
    }


    /* Add additional headers etc */

    pjsua_process_msg_data( tdata,
                            call->async_call.call_var.out_call.msg_data);

    /* Must increment call counter now */
    ++pjsua_var.call_cnt;

    /* Send initial INVITE: */

    status = pjsip_inv_send_msg(inv, tdata);
    if (status != PJ_SUCCESS) {
        cb_called = PJ_TRUE;

        /* Upon failure to send first request, the invite
         * session would have been cleared.
         */
        call->inv = inv = NULL;
        goto on_error;
    }

    /* Done. */
    call->med_ch_cb = NULL;

    pjsip_dlg_dec_lock(dlg);
    PJSUA_UNLOCK();

    return PJ_SUCCESS;

on_error:
    if (inv == NULL && call_id != -1 && !cb_called &&
        !call->hanging_up &&
        pjsua_var.ua_cfg.cb.on_call_state)
    {
        /* Use user event rather than NULL to avoid crash in
         * unsuspecting app.
         */
        pjsip_event user_event;
        PJSIP_EVENT_INIT_USER(user_event, 0, 0, 0, 0);

        (*pjsua_var.ua_cfg.cb.on_call_state)(call_id, &user_event);
    }

    /* This may destroy the dialog */
    pjsip_dlg_dec_lock(dlg);
    call->async_call.dlg = NULL;

    if (inv != NULL) {
        pjsip_inv_terminate(inv, PJSIP_SC_OK, PJ_FALSE);
        call->inv = NULL;
    }

    if (call_id != -1) {
        pjsua_media_channel_deinit(call_id);
        reset_call(call_id);
    }

    call->med_ch_cb = NULL;

    pjsua_check_snd_dev_idle();

    PJSUA_UNLOCK();
    return status;
}
  • pjsua_media_channel_create_sdp: 创建媒体流的 sdp 信息
  • pjsip_inv_create_uac: 创建 uac
  • pjsip_timer_init_session: 初始化 session 的 timer
  • pjsua_init_tpselector: 初始化 transport 的 selector
  • pjsip_inv_invite: 初始化 invite
  • pjsua_process_msg_data: 处理额外的 header 头信息
  • pjsip_inv_send_msg:发送一个消息

pjsip_inv_send_msg

pjsip_inv_send_msg 的函数定义位于 pjsip/src/pjsip-ua/sip_inv.c 中,其中内容如下所示:

/*
 * Send a request or response message.
 */
PJ_DEF(pj_status_t) pjsip_inv_send_msg( pjsip_inv_session *inv,
                                        pjsip_tx_data *tdata)
{
    pj_status_t status;

    /* Verify arguments. */
    PJ_ASSERT_RETURN(inv && tdata, PJ_EINVAL);

    pj_log_push_indent();

    PJ_LOG(5,(inv->obj_name, "Sending %s", 
              pjsip_tx_data_get_info(tdata)));

    if (tdata->msg->type == PJSIP_REQUEST_MSG) {
        struct tsx_inv_data *tsx_inv_data;

        pjsip_dlg_inc_lock(inv->dlg);

        /* Check again that we didn't receive incoming re-INVITE */
        if (tdata->msg->line.req.method.id==PJSIP_INVITE_METHOD && 
            inv->invite_tsx) 
        {
            pjsip_tx_data_dec_ref(tdata);
            pjsip_dlg_dec_lock(inv->dlg);
            status = PJ_EINVALIDOP;
            goto on_error;
        }

        /* Don't send BYE before ACK is received
         * https://github.com/pjsip/pjproject/issues/1712
         */
        if (tdata->msg->line.req.method.id == PJSIP_BYE_METHOD &&
            inv->role == PJSIP_ROLE_UAS &&
            inv->state == PJSIP_INV_STATE_CONNECTING &&
            inv->cause != PJSIP_SC_REQUEST_TIMEOUT &&
            inv->cause != PJSIP_SC_TSX_TRANSPORT_ERROR)
        {
            if (inv->pending_bye)
                pjsip_tx_data_dec_ref(inv->pending_bye);

            inv->pending_bye = tdata;
            PJ_LOG(4, (inv->obj_name, "Delaying BYE request until "
                       "ACK is received"));
            pjsip_dlg_dec_lock(inv->dlg);
            goto on_return;
        }

        /* Associate our data in outgoing invite transaction */
        tsx_inv_data = PJ_POOL_ZALLOC_T(inv->pool, struct tsx_inv_data);
        tsx_inv_data->inv = inv;
        tsx_inv_data->has_sdp = tx_data_has_sdp(tdata);

        pjsip_dlg_dec_lock(inv->dlg);

        status = pjsip_dlg_send_request(inv->dlg, tdata, mod_inv.mod.id, 
                                        tsx_inv_data);
        if (status != PJ_SUCCESS) {
            goto on_error;
        }

        /* Check if this is delayed manual ACK (see #416) */
        if (mod_inv.cb.on_send_ack &&
            tdata->msg->line.req.method.id == PJSIP_ACK_METHOD &&
            tdata == inv->last_ack)
        {
            pjsip_dlg_inc_lock(inv->dlg);

            /* Set state to CONFIRMED (if we're not in CONFIRMED yet).
             * But don't set it to CONFIRMED if we're already DISCONNECTED
             * (this may have been a late 200/OK response.
             */
            if (inv->state < PJSIP_INV_STATE_CONFIRMED) {
                pjsip_event ack_e;
                PJSIP_EVENT_INIT_TX_MSG(ack_e, inv->last_ack);
                inv_set_state(inv, PJSIP_INV_STATE_CONFIRMED, &ack_e);
            } else if (inv->state == PJSIP_INV_STATE_DISCONNECTED) {
                /* Avoid possible leaked tdata when invite session is
                 * already destroyed.
                 * https://github.com/pjsip/pjproject/pull/2432
                 */
                pjsip_tx_data_dec_ref(inv->last_ack);
                inv->last_ack = NULL;
            }

            pjsip_dlg_dec_lock(inv->dlg);
        }

    } else {
        pjsip_cseq_hdr *cseq;

        /* Can only do this to send response to original INVITE
         * request.
         */
        cseq = (pjsip_cseq_hdr*)pjsip_msg_find_hdr(tdata->msg, PJSIP_H_CSEQ, NULL);
        PJ_ASSERT_ON_FAIL(cseq != NULL
                          && (inv->invite_tsx && cseq->cseq == inv->invite_tsx->cseq),
                          { pjsip_tx_data_dec_ref(tdata); return PJ_EINVALIDOP; });

        if (inv->options & PJSIP_INV_REQUIRE_100REL) {
            status = pjsip_100rel_tx_response(inv, tdata);
        } else 
        {
            status = pjsip_dlg_send_response(inv->dlg, inv->invite_tsx, tdata);
        }

        if (status != PJ_SUCCESS) {
            goto on_error;
        }
    }

    /* Done */
on_return:
    pj_log_pop_indent();
    return PJ_SUCCESS;

on_error:
    pj_log_pop_indent();
    return status;
}
  • pjsip_dlg_send_request: dialog发送数据到远端

pjsip_dlg_send_request

pjsip_dlg_send_request 函数的定义位于 pjsip/src/pjsip/sip_dialog.c 中,其中内容如下所示:

PJ_DEF(pj_status_t) pjsip_dlg_send_request( pjsip_dialog *dlg,
                                            pjsip_tx_data *tdata,
                                            int mod_data_id,
                                            void *mod_data)
{
    pjsip_transaction *tsx;
    pjsip_msg *msg = tdata->msg;
    pj_status_t status;

    /* Check arguments. */
    PJ_ASSERT_RETURN(dlg && tdata && tdata->msg, PJ_EINVAL);
    PJ_ASSERT_RETURN(tdata->msg->type == PJSIP_REQUEST_MSG,
                     PJSIP_ENOTREQUESTMSG);

    pj_log_push_indent();
    PJ_LOG(5,(dlg->obj_name, "Sending %s",
              pjsip_tx_data_get_info(tdata)));

    /* Lock and increment session */
    pjsip_dlg_inc_lock(dlg);

    /* Put this dialog in tdata's mod_data */
    tdata->mod_data[dlg->ua->id] = dlg;

    /* If via_addr is set, use this address for the Via header. */
    if (dlg->via_addr.host.slen > 0) {
        tdata->via_addr = dlg->via_addr;
        tdata->via_tp = dlg->via_tp;
    }

    /* Update dialog's CSeq and message's CSeq if request is not
     * ACK nor CANCEL.
     */
    if (msg->line.req.method.id != PJSIP_CANCEL_METHOD &&
        msg->line.req.method.id != PJSIP_ACK_METHOD)
    {
        pjsip_cseq_hdr *ch;

        ch = PJSIP_MSG_CSEQ_HDR(msg);
        PJ_ASSERT_RETURN(ch!=NULL, PJ_EBUG);

        ch->cseq = dlg->local.cseq++;

        /* Force the whole message to be re-printed. */
        pjsip_tx_data_invalidate_msg( tdata );
    }

    /* Create a new transaction if method is not ACK.
     * The transaction user is the user agent module.
     */
    if (msg->line.req.method.id != PJSIP_ACK_METHOD) {
        int tsx_count;

        status = pjsip_tsx_create_uac(dlg->ua, tdata, &tsx);
        if (status != PJ_SUCCESS)
            goto on_error;

        /* Set transport selector */
        status = pjsip_tsx_set_transport(tsx, &dlg->tp_sel);
        pj_assert(status == PJ_SUCCESS);

        /* Attach this dialog to the transaction, so that user agent
         * will dispatch events to this dialog.
         */
        tsx->mod_data[dlg->ua->id] = dlg;

        /* Copy optional caller's mod_data, if present */
        if (mod_data_id >= 0 && mod_data_id < PJSIP_MAX_MODULE)
            tsx->mod_data[mod_data_id] = mod_data;

        /* Increment transaction counter. */
        tsx_count = ++dlg->tsx_count;

        /* Send the message. */
        status = pjsip_tsx_send_msg(tsx, tdata);
        if (status != PJ_SUCCESS) {
            if (dlg->tsx_count == tsx_count)
                pjsip_tsx_terminate(tsx, tsx->status_code);
            goto on_error;
        }

    } else {
        dlg->ack_sent = PJ_TRUE;

        /* Set transport selector */
        pjsip_tx_data_set_transport(tdata, &dlg->tp_sel);

        /* Send request */
        status = pjsip_endpt_send_request_stateless(dlg->endpt, tdata,
                                                    NULL, &send_ack_callback);
        if (status != PJ_SUCCESS)
            goto on_error;

    }

    /* Unlock dialog, may destroy dialog. */
    pjsip_dlg_dec_lock(dlg);
    pj_log_pop_indent();
    return PJ_SUCCESS;

on_error:
    /* Unlock dialog, may destroy dialog. */
    pjsip_dlg_dec_lock(dlg);

    /* Whatever happen delete the message. */
    pjsip_tx_data_dec_ref( tdata );
    pj_log_pop_indent();
    return status;
}
  • pjsip_tsx_create_uac: 为 uac 创建事务
  • pjsip_tsx_set_transport: 向事务里面添加 transport 的 selector
  • pjsip_tsx_send_msg: 通过事务向远端发送数据
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值