/* 
 * 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 <pjsua-lib/pjsua.h>
#include <pjsua-lib/pjsua_internal.h>
#include <pjmedia/vid_codec_util.h>


#define THIS_FILE               "pjsua_media.c"

#ifndef PJSUA_REQUIRE_CONSECUTIVE_RTCP_PORT
#   define PJSUA_REQUIRE_CONSECUTIVE_RTCP_PORT  0
#endif

#ifndef PJSUA_RESET_SRTP_ROC_ON_REM_ADDRESS_CHANGE
#   define PJSUA_RESET_SRTP_ROC_ON_REM_ADDRESS_CHANGE   0
#endif

static void stop_media_stream(pjsua_call *call, unsigned med_idx);

char *pjsua_get_basename(const char* path, unsigned len)
{
    char* p = ((char*)path) + len;

    if (len == 0)
        return p;

    for (--p; p != path && *p != '/' && *p != '\\'; ) --p;

    return (p == path) ? p : p + 1;
}

static void pjsua_media_config_dup(pj_pool_t *pool,
                                   pjsua_media_config *dst,
                                   const pjsua_media_config *src)
{
    pj_memcpy(dst, src, sizeof(*src));
    pj_strdup(pool, &dst->turn_server, &src->turn_server);
    pj_stun_auth_cred_dup(pool, &dst->turn_auth_cred, &src->turn_auth_cred);
#if PJ_HAS_SSL_SOCK
    pj_turn_sock_tls_cfg_dup(pool, &dst->turn_tls_setting,
                             &src->turn_tls_setting);
#endif
}


/**
 * Init media subsystems.
 */
pj_status_t pjsua_media_subsys_init(const pjsua_media_config *cfg)
{
    pj_status_t status;

    pj_log_push_indent();

    /* Specify which audio device settings are save-able */
    pjsua_var.aud_svmask = 0xFFFFFFFF;
    /* These are not-settable */
    pjsua_var.aud_svmask &= ~(PJMEDIA_AUD_DEV_CAP_EXT_FORMAT |
                              PJMEDIA_AUD_DEV_CAP_INPUT_SIGNAL_METER |
                              PJMEDIA_AUD_DEV_CAP_OUTPUT_SIGNAL_METER);
    /* EC settings use different API */
    pjsua_var.aud_svmask &= ~(PJMEDIA_AUD_DEV_CAP_EC |
                              PJMEDIA_AUD_DEV_CAP_EC_TAIL);

    /* Copy configuration */
    pjsua_media_config_dup(pjsua_var.pool, &pjsua_var.media_cfg, cfg);

    /* Normalize configuration */
    if (pjsua_var.media_cfg.snd_clock_rate == 0) {
        pjsua_var.media_cfg.snd_clock_rate = pjsua_var.media_cfg.clock_rate;
    }

    if (pjsua_var.media_cfg.has_ioqueue &&
        pjsua_var.media_cfg.thread_cnt == 0)
    {
        pjsua_var.media_cfg.thread_cnt = 1;
    }

    if (pjsua_var.media_cfg.max_media_ports < pjsua_var.ua_cfg.max_calls) {
        pjsua_var.media_cfg.max_media_ports = pjsua_var.ua_cfg.max_calls + 2;
    }

    if (pjsua_var.media_cfg.conf_threads < 1) {
        pjsua_var.media_cfg.conf_threads = 1;
    }

    /* Create media endpoint. */
    status = pjmedia_endpt_create(&pjsua_var.cp.factory, 
                                  pjsua_var.media_cfg.has_ioqueue? NULL :
                                     pjsip_endpt_get_ioqueue(pjsua_var.endpt),
                                  pjsua_var.media_cfg.thread_cnt,
                                  &pjsua_var.med_endpt);
    if (status != PJ_SUCCESS) {
        pjsua_perror(THIS_FILE, 
                     "Media stack initialization has returned error", 
                     status);
        goto on_error;
    }

    status = pjsua_aud_subsys_init();
    if (status != PJ_SUCCESS)
        goto on_error;

#if defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0)
    /* Initialize SRTP library (ticket #788). */
    status = pjmedia_srtp_init_lib(pjsua_var.med_endpt);
    if (status != PJ_SUCCESS) {
        pjsua_perror(THIS_FILE, "Error initializing SRTP library", 
                     status);
        goto on_error;
    }
#endif

    /* Video */
#if PJMEDIA_HAS_VIDEO
    status = pjsua_vid_subsys_init();
    if (status != PJ_SUCCESS)
        goto on_error;
#endif

    /* Create event manager */
    if (!pjmedia_event_mgr_instance()) {
        status = pjmedia_event_mgr_create(pjsua_var.pool, 0, NULL);
        if (status != PJ_SUCCESS) {
            pjsua_perror(THIS_FILE, "Error creating PJMEDIA event manager",
                         status);
            goto on_error;
        }
    }

    pj_log_pop_indent();
    return PJ_SUCCESS;

on_error:
    pj_log_pop_indent();
    return status;
}

/*
 * Start pjsua media subsystem.
 */
pj_status_t pjsua_media_subsys_start(void)
{
    pj_status_t status;

    pj_log_push_indent();

#if DISABLED_FOR_TICKET_1185
    /* Create media for calls, if none is specified */
    if (pjsua_var.calls[0].media[0].tp == NULL) {
        pjsua_transport_config transport_cfg;

        /* Create default transport config */
        pjsua_transport_config_default(&transport_cfg);
        transport_cfg.port = DEFAULT_RTP_PORT;

        status = pjsua_media_transports_create(&transport_cfg);
        if (status != PJ_SUCCESS) {
            pj_log_pop_indent();
            return status;
        }
    }
#endif

    /* Audio */
    status = pjsua_aud_subsys_start();
    if (status != PJ_SUCCESS) {
        pj_log_pop_indent();
        return status;
    }

    /* Video */
#if PJMEDIA_HAS_VIDEO
    status = pjsua_vid_subsys_start();
    if (status != PJ_SUCCESS) {
        pjsua_aud_subsys_destroy();
        pj_log_pop_indent();
        return status;
    }
#endif

    /* Perform NAT detection */
    // Performed only when STUN server resolution by pjsua_init() completed
    // successfully (see ticket #1865).
    //if (pjsua_var.ua_cfg.stun_srv_cnt) {
        //status = pjsua_detect_nat_type();
        //if (status != PJ_SUCCESS) {
        //    PJ_PERROR(1,(THIS_FILE, status, "NAT type detection failed"));
        //}
    //}

    pj_log_pop_indent();
    return PJ_SUCCESS;
}


/*
 * Destroy pjsua media subsystem.
 */
pj_status_t pjsua_media_subsys_destroy(unsigned flags)
{
    PJ_UNUSED_ARG(flags);

    PJ_LOG(4,(THIS_FILE, "Shutting down media.."));
    pj_log_push_indent();

    if (pjsua_var.med_endpt) {
        /* Wait for media endpoint's worker threads to quit. */
        pjmedia_endpt_stop_threads(pjsua_var.med_endpt);

        pjsua_aud_subsys_destroy();
    }

#if 0
    // This part has been moved out to pjsua_destroy() (see also #1717).
    /* Close media transports */
    for (i=0; i<pjsua_var.ua_cfg.max_calls; ++i) {
        /* TODO: check if we're not allowed to send to network in the
         *       "flags", and if so do not do TURN allocation...
         */
        PJ_UNUSED_ARG(flags);
        pjsua_media_channel_deinit(i);
    }
#endif

    /* Destroy media endpoint. */
    if (pjsua_var.med_endpt) {

#       if PJMEDIA_HAS_VIDEO
            pjsua_vid_subsys_destroy();
#       endif

        pjmedia_endpt_destroy(pjsua_var.med_endpt);
        pjsua_var.med_endpt = NULL;

        /* Deinitialize sound subsystem */
        // Not necessary, as pjmedia_snd_deinit() should have been called
        // in pjmedia_endpt_destroy().
        //pjmedia_snd_deinit();
    }

    if (pjmedia_event_mgr_instance())
        pjmedia_event_mgr_destroy(NULL);

    pj_log_pop_indent();

    return PJ_SUCCESS;
}

static int get_media_ip_version(pjsua_call_media *call_med,
                                const pjmedia_sdp_session *rem_sdp,
                                pj_bool_t loop_tp,
                                pj_bool_t ice_tp)
{
#if PJ_HAS_IPV6

    pjsua_ipv6_use ipv6_use;

    ipv6_use = pjsua_var.acc[call_med->call->acc_id].cfg.ipv6_media_use;

    if (rem_sdp) {
        /* Match the default address family according to the offer */
        const pj_str_t ID_IP6 = { "IP6", 3};
        const pjmedia_sdp_media *m;
        const pjmedia_sdp_conn *c;

        m = rem_sdp->media[call_med->idx];
        c = m->conn? m->conn : rem_sdp->conn;

        if (pj_stricmp(&c->addr_type, &ID_IP6) == 0 &&
            ipv6_use != PJSUA_IPV6_DISABLED)
        {
            /* Use IPv6. */
            return 6;
        }

    } else {
        pj_sockaddr addr = {{0}};
        pj_status_t status;

        /* Status:
         * - not avail: no IP address, gethostip() returns error.
         * - available: IP address is loopback, local-link, or disabled.
         * - usable   : IP address is not loopback, local-link, nor disabled.
         */
        unsigned ipv6_status = 0; /* 0: not avail, 1: available, 2: usable*/
        unsigned ipv4_status = 0; /* 0: not avail, 1: available, 2: usable*/

        /* Use IPv4 regardless. */
        if (ipv6_use == PJSUA_IPV6_DISABLED)
            return 4;

        /* Use IPv6 regardless. */
        if (ipv6_use == PJSUA_IPV6_ENABLED_USE_IPV6_ONLY)
            return 6;

        /* For loop transport, use the preferred */
        if (loop_tp) {
            if (ipv6_use == PJSUA_IPV6_ENABLED_PREFER_IPV6)
                return 6;
            if (ipv6_use == PJSUA_IPV6_ENABLED_PREFER_IPV4)
                return 4;

            /* No preference, just use IPv4 */
            return 4;
        }


        /* Check IPv6 & IPv4 availability & usability */

        status = pj_gethostip(PJ_AF_INET6, &addr);
        if (status == PJ_SUCCESS) {
            /* IPv6 is available */
            ipv6_status = 1;

            /* Check if it is usable */
            if (!pj_check_addr_type(&addr,
                                    PJ_ADDR_TYPE_DISABLED |
                                    PJ_ADDR_TYPE_LOOPBACK |
                                    PJ_ADDR_TYPE_LINK_LOCAL))
            {
                /* If IPv6 is usable & preferred, use it */
                if (ipv6_use == PJSUA_IPV6_ENABLED_PREFER_IPV6)
                    return 6;

                /* IPv6 seems usable */
                ipv6_status = 2;
            }
        }

        /* For ICE transport, use IPv6 if available */
        if (ice_tp) {
            return (ipv6_status >= 1)? 6 : 4;
        }

        status = pj_gethostip(PJ_AF_INET, &addr);
        if (status == PJ_SUCCESS) {
            /* IPv4 is available */
            ipv4_status = 1;

            /* Check if it is usable */
            if (!pj_check_addr_type(&addr,
                                    PJ_ADDR_TYPE_DISABLED |
                                    PJ_ADDR_TYPE_LOOPBACK |
                                    PJ_ADDR_TYPE_LINK_LOCAL))
            {
                /* If IPv4 is usable & preferred, use it */
                if (ipv6_use == PJSUA_IPV6_ENABLED_PREFER_IPV4)
                    return 4;

                /* IPv4 seems usable */
                ipv4_status = 2;
            }
        }


        /* Preferred IP version is not usable, fallback to any usable.
         * In this point, either only one is usable (but not preferred)
         * or there is no preference (IPv4 will be returned).
         */
        if (ipv4_status == 2)
            return 4;
        if (ipv6_status == 2)
            return 6;

        PJ_LOG(3,(THIS_FILE,"Call %d: Media %d: Warning, no usable "
                            "IP address for SDP offer",
                            call_med->call->index, call_med->idx));

        /* No usable IP version, pick based on availability & preference. */
        if (ipv6_use == PJSUA_IPV6_ENABLED_PREFER_IPV6 && ipv6_status > 0)
            return 6;
        if (ipv6_use == PJSUA_IPV6_ENABLED_PREFER_IPV4 && ipv4_status > 0)
            return 4;

        /* Preferred IP version is not available, fallback to any available.
         * In this point, either only one is available (but not preferred)
         * or there is no preference (IPv4 will be returned).
         */
        if (ipv4_status == 1)
            return 4;
        if (ipv6_status == 1)
            return 6;

        PJ_LOG(3,(THIS_FILE,"Call %d: Media %d: Warning, no available "
                            "IP address for SDP offer",
                            call_med->call->index, call_med->idx));
    }
#else
    PJ_UNUSED_ARG(call_med);
    PJ_UNUSED_ARG(rem_sdp);
    PJ_UNUSED_ARG(loop_tp);
    PJ_UNUSED_ARG(ice_tp);
#endif

    /* Use IPv4. */
    return 4;
}

/*
 * Create RTP and RTCP socket pair, and possibly resolve their public
 * address via STUN/UPnP.
 */
static pj_status_t create_rtp_rtcp_sock(pjsua_call_media *call_med,
                                        const pjsua_transport_config *cfg,
                                        pjmedia_sock_info *skinfo,
                                        const pjmedia_sdp_session *rem_sdp)
{
    enum {
        RTP_RETRY = 100
    };
    int i;
    pj_bool_t use_ipv6, use_nat64;
    int af;
    pj_sockaddr bound_addr;
    pj_sockaddr mapped_addr[2];
    pj_status_t status = PJ_SUCCESS;
    char addr_buf[PJ_INET6_ADDRSTRLEN+10];
    pjsua_acc *acc = &pjsua_var.acc[call_med->call->acc_id];
    pj_sock_t sock[2];

    use_ipv6 = (get_media_ip_version(call_med, rem_sdp, PJ_FALSE, PJ_FALSE)==6);
    use_nat64 = PJ_HAS_IPV6 && (acc->cfg.nat64_opt != PJSUA_NAT64_DISABLED);
    af = (use_ipv6 || use_nat64) ? pj_AF_INET6() : pj_AF_INET();

    /* Make sure STUN server resolution has completed */
    if ((!use_ipv6 || use_nat64) &&
        pjsua_media_acc_is_using_stun(call_med->call->acc_id))
    {
        pj_bool_t retry_stun = (acc->cfg.media_stun_use &
                                PJSUA_STUN_RETRY_ON_FAILURE) ==
                                PJSUA_STUN_RETRY_ON_FAILURE;
        status = resolve_stun_server(PJ_TRUE, retry_stun,
                                     (unsigned)acc->cfg.nat64_opt);
        if (status != PJ_SUCCESS) {
            pjsua_perror(THIS_FILE, "Error resolving STUN server", status);
            return status;
        }
    }

    if (acc->next_rtp_port == 0 || cfg->port == 0) {
        if (cfg->port != 0 && cfg->port_range != 0 && cfg->randomize_port) {
            unsigned offset = ((pj_rand() % (cfg->port_range)) / 2) * 2;
            acc->next_rtp_port = (pj_uint16_t)(cfg->port + offset);
        } else {
            acc->next_rtp_port = (pj_uint16_t)cfg->port;
        }
    }

    for (i=0; i<2; ++i)
        sock[i] = PJ_INVALID_SOCKET;

    pj_sockaddr_init(af, &bound_addr, NULL, 0);
    if (cfg->bound_addr.slen) {
        status = pj_sockaddr_set_str_addr(af, &bound_addr, &cfg->bound_addr);
        if (status != PJ_SUCCESS) {
            pjsua_perror(THIS_FILE, "Unable to resolve transport bind address",
                         status);
            return status;
        }
    }

    /* Loop retry to bind RTP and RTCP sockets. */
    for (i=0; i<RTP_RETRY; ++i, acc->next_rtp_port += 2) {

        if (cfg->port > 0 && cfg->port_range > 0 &&
            (acc->next_rtp_port > cfg->port + cfg->port_range ||
             acc->next_rtp_port < cfg->port))
        {
            acc->next_rtp_port = (pj_uint16_t)cfg->port;
        }

        /* Create RTP socket. */
        status = pj_sock_socket(af, pj_SOCK_DGRAM() | pj_SOCK_CLOEXEC(), 0, &sock[0]);
        if (status != PJ_SUCCESS) {
            pjsua_perror(THIS_FILE, "socket() error", status);
            return status;
        }

        /* Apply QoS to RTP socket, if specified */
        status = pj_sock_apply_qos2(sock[0], cfg->qos_type,
                                    &cfg->qos_params,
                                    2, THIS_FILE, "RTP socket");

        /* Apply sockopt, if specified */
        if (cfg->sockopt_params.cnt)
            status = pj_sock_setsockopt_params(sock[0], &cfg->sockopt_params);

        /* Bind RTP socket */
        pj_sockaddr_set_port(&bound_addr, acc->next_rtp_port);
        status=pj_sock_bind(sock[0], &bound_addr,
                            pj_sockaddr_get_len(&bound_addr));
        if (status != PJ_SUCCESS) {
            pj_sock_close(sock[0]);
            sock[0] = PJ_INVALID_SOCKET;
            PJ_PERROR(1,(THIS_FILE, status, "RTP socket bind() at %s error",
                      pj_sockaddr_print(&bound_addr, addr_buf,
                                        sizeof(addr_buf), 3)));
            continue;
        }
        
        /* If bound to random port, find out the port number. */
        if (acc->next_rtp_port == 0) {
            pj_sockaddr sock_addr;
            int addr_len = sizeof(pj_sockaddr);

            status = pj_sock_getsockname(sock[0], &sock_addr, &addr_len);
            if (status != PJ_SUCCESS) {
                pjsua_perror(THIS_FILE, "getsockname() error", status);
                pj_sock_close(sock[0]);
                return status;
            }
            acc->next_rtp_port = pj_sockaddr_get_port(&sock_addr);
        }

        /* Create RTCP socket. */
        status = pj_sock_socket(af, pj_SOCK_DGRAM() | pj_SOCK_CLOEXEC(), 0, &sock[1]);
        if (status != PJ_SUCCESS) {
            pjsua_perror(THIS_FILE, "socket() error", status);
            pj_sock_close(sock[0]);
            return status;
        }

        /* Apply QoS to RTCP socket, if specified */
        status = pj_sock_apply_qos2(sock[1], cfg->qos_type,
                                    &cfg->qos_params,
                                    2, THIS_FILE, "RTCP socket");

        /* Apply sockopt, if specified */
        if (cfg->sockopt_params.cnt)
            status = pj_sock_setsockopt_params(sock[1], &cfg->sockopt_params);

        /* Bind RTCP socket */
        pj_sockaddr_set_port(&bound_addr, (pj_uint16_t)(acc->next_rtp_port+1));
        status=pj_sock_bind(sock[1], &bound_addr,
                            pj_sockaddr_get_len(&bound_addr));
        if (status != PJ_SUCCESS) {
            pj_sock_close(sock[0]);
            sock[0] = PJ_INVALID_SOCKET;

            pj_sock_close(sock[1]);
            sock[1] = PJ_INVALID_SOCKET;

            PJ_PERROR(1,(THIS_FILE, status, "RTCP socket bind() at %s error",
                      pj_sockaddr_print(&bound_addr, addr_buf,
                                        sizeof(addr_buf), 3)));
            continue;
        }

        /*
         * If we're configured to use STUN, then find out the mapped address,
         * and make sure that the mapped RTCP port is adjacent with the RTP.
         */
        if ((!use_ipv6 || use_nat64) &&
            pjsua_media_acc_is_using_stun(call_med->call->acc_id) &&
            pjsua_var.stun_srv.addr.sa_family != 0)
        {
            char ip_addr[PJ_INET6_ADDRSTRLEN];
            pj_str_t stun_srv;
            pj_sockaddr_in resolved_addr[2];
            pjstun_setting stun_opt;

            pj_sockaddr_print(&pjsua_var.stun_srv, ip_addr,sizeof(ip_addr),0);
            stun_srv = pj_str(ip_addr);

            pj_bzero(&stun_opt, sizeof(stun_opt));
            stun_opt.use_stun2 = pjsua_var.ua_cfg.stun_map_use_stun2;
            stun_opt.af = pjsua_var.stun_srv.addr.sa_family;
            stun_opt.srv1  = stun_opt.srv2  = stun_srv;
            stun_opt.port1 = stun_opt.port2 = 
                             pj_sockaddr_get_port(&pjsua_var.stun_srv);
            status=pjstun_get_mapped_addr2(&pjsua_var.cp.factory, &stun_opt,
                                           2, sock, resolved_addr);
#if defined(PJ_IPHONE_OS_HAS_MULTITASKING_SUPPORT) && \
            PJ_IPHONE_OS_HAS_MULTITASKING_SUPPORT!=0
            /* Handle EPIPE (Broken Pipe) error, which happens on UDP socket
             * after app wakes up from suspended state. In this case, simply
             * just retry.
             * P.S.: The magic status is PJ_STATUS_FROM_OS(EPIPE)
             */
            if (status == 120032) {
                PJ_LOG(4,(THIS_FILE, "Got EPIPE error, retrying.."));
                pj_sock_close(sock[0]);
                sock[0] = PJ_INVALID_SOCKET;

                pj_sock_close(sock[1]);
                sock[1] = PJ_INVALID_SOCKET;

                continue;
            }
            else
#endif

            if (status != PJ_SUCCESS && pjsua_var.ua_cfg.stun_srv_cnt > 1 &&
                ((acc->cfg.media_stun_use & PJSUA_STUN_RETRY_ON_FAILURE)==
                  PJSUA_STUN_RETRY_ON_FAILURE))
            {
                pj_str_t srv = 
                             pjsua_var.ua_cfg.stun_srv[pjsua_var.stun_srv_idx];

                PJ_LOG(4,(THIS_FILE, "Failed to get STUN mapped address, "
                       "retrying other STUN servers"));

                if (pjsua_var.stun_srv_idx < pjsua_var.ua_cfg.stun_srv_cnt-1) {
                    PJSUA_LOCK();
                    /* Move the unusable STUN server to the last position
                     * as the least prioritize.
                     */
                    pj_array_erase(pjsua_var.ua_cfg.stun_srv, 
                                   sizeof(pj_str_t),
                                   pjsua_var.ua_cfg.stun_srv_cnt,
                                   pjsua_var.stun_srv_idx);

                    pj_array_insert(pjsua_var.ua_cfg.stun_srv, 
                                    sizeof(pj_str_t),
                                    pjsua_var.ua_cfg.stun_srv_cnt-1,
                                    pjsua_var.ua_cfg.stun_srv_cnt-1,
                                    &srv);

                    PJSUA_UNLOCK();
                }
                status=pjsua_update_stun_servers(pjsua_var.ua_cfg.stun_srv_cnt,
                                                 pjsua_var.ua_cfg.stun_srv,
                                                 PJ_TRUE);
                if (status == PJ_SUCCESS) {
                    if (pjsua_var.stun_srv.addr.sa_family != 0) {
                        pj_sockaddr_print(&pjsua_var.stun_srv,
                                          ip_addr, sizeof(ip_addr), 0);
                        stun_srv = pj_str(ip_addr);
                    } else {
                        stun_srv.slen = 0;
                    }
            
                    stun_opt.af = pjsua_var.stun_srv.addr.sa_family;
                    stun_opt.srv1  = stun_opt.srv2  = stun_srv;
                    stun_opt.port1 = stun_opt.port2 = 
                                pj_sockaddr_get_port(&pjsua_var.stun_srv);
                    status = pjstun_get_mapped_addr2(&pjsua_var.cp.factory,
                                                     &stun_opt, 2, sock,
                                                     resolved_addr);
                }
            }

            if (status != PJ_SUCCESS) {
                if (!pjsua_var.ua_cfg.stun_ignore_failure) {
                    pjsua_perror(THIS_FILE, "STUN resolve error", status);
                    goto on_error;
                }

                PJ_LOG(4,(THIS_FILE, "Ignoring STUN resolve error %d", 
                          status));

                if (!pj_sockaddr_has_addr(&bound_addr)) {
                    pj_sockaddr addr;

                    /* Get local IP address. */
                    status = pj_gethostip(af, &addr);
                    if (status != PJ_SUCCESS)
                        goto on_error;

                    pj_sockaddr_copy_addr(&bound_addr, &addr);
                }

                for (i=0; i<2; ++i) {
                    pj_sockaddr_init(af, &mapped_addr[i], NULL, 0);
                    pj_sockaddr_copy_addr(&mapped_addr[i], &bound_addr);
                    pj_sockaddr_set_port(&mapped_addr[i],
                                         (pj_uint16_t)(acc->next_rtp_port+i));
                }
                break;
            }

            pj_sockaddr_cp(&mapped_addr[0], &resolved_addr[0]);
            pj_sockaddr_cp(&mapped_addr[1], &resolved_addr[1]);

#if PJSUA_REQUIRE_CONSECUTIVE_RTCP_PORT
            if (pj_sockaddr_get_port(&mapped_addr[1]) ==
                pj_sockaddr_get_port(&mapped_addr[0])+1)
            {
                /* Success! */
                break;
            }

            pj_sock_close(sock[0]);
            sock[0] = PJ_INVALID_SOCKET;

            pj_sock_close(sock[1]);
            sock[1] = PJ_INVALID_SOCKET;
#else
            if (pj_sockaddr_get_port(&mapped_addr[1]) !=
                pj_sockaddr_get_port(&mapped_addr[0])+1)
            {
                PJ_LOG(4,(THIS_FILE,
                          "Note: STUN mapped RTCP port %d is not adjacent"
                          " to RTP port %d",
                          pj_sockaddr_get_port(&mapped_addr[1]),
                          pj_sockaddr_get_port(&mapped_addr[0])));
            }
            /* Success! */
            break;
#endif

#if defined(PJNATH_HAS_UPNP) && (PJNATH_HAS_UPNP != 0)
        } else if ((!use_ipv6 || use_nat64) &&
                   pjsua_media_acc_is_using_upnp(call_med->call->acc_id) &&
                   pjsua_var.upnp_status == PJ_SUCCESS)
        {
            status = pj_upnp_add_port_mapping(2, sock, NULL, mapped_addr);
            if (status == PJ_SUCCESS) {
                call_med->use_upnp = PJ_TRUE;
                pj_sockaddr_cp(&call_med->mapped_addr[0], &mapped_addr[0]);
                pj_sockaddr_cp(&call_med->mapped_addr[1], &mapped_addr[1]);
            } else {
                pjsua_perror(THIS_FILE, "Error adding UPnP "
                                        "port mapping", status);

                if (!pj_sockaddr_has_addr(&bound_addr)) {
                    pj_sockaddr addr;

                    /* Get local IP address. */
                    status = pj_gethostip(af, &addr);
                    if (status != PJ_SUCCESS)
                        goto on_error;

                    pj_sockaddr_copy_addr(&bound_addr, &addr);
                }

                for (i = 0; i < 2; ++i) {
                    pj_sockaddr_init(af, &mapped_addr[i], NULL, 0);
                    pj_sockaddr_copy_addr(&mapped_addr[i], &bound_addr);
                    pj_sockaddr_set_port(&mapped_addr[i],
                                         (pj_uint16_t)(acc->next_rtp_port+i));
                }
                break;
            }

            break;
#endif

        } else if (cfg->public_addr.slen) {

            status = pj_sockaddr_init(af, &mapped_addr[0], &cfg->public_addr,
                                      (pj_uint16_t)acc->next_rtp_port);
            if (status != PJ_SUCCESS)
                goto on_error;

            status = pj_sockaddr_init(af, &mapped_addr[1], &cfg->public_addr,
                                      (pj_uint16_t)(acc->next_rtp_port+1));
            if (status != PJ_SUCCESS)
                goto on_error;

            break;

        } else {
            if (acc->cfg.allow_sdp_nat_rewrite && acc->reg_mapped_addr.slen) {
                pj_status_t status2;

                /* Take the address from mapped addr as seen by registrar */
                status2 = pj_sockaddr_set_str_addr(af, &bound_addr,
                                                   &acc->reg_mapped_addr);
                if (status2 != PJ_SUCCESS) {
                    /* just leave bound_addr with whatever it was
                    pj_bzero(pj_sockaddr_get_addr(&bound_addr),
                             pj_sockaddr_get_addr_len(&bound_addr));
                     */
                }
            }

            if (!pj_sockaddr_has_addr(&bound_addr)) {
                pj_sockaddr addr;

                /* Get local IP address. */
                status = pj_gethostip(af, &addr);
                if (status != PJ_SUCCESS)
                    goto on_error;

                pj_sockaddr_copy_addr(&bound_addr, &addr);
            }

            for (i=0; i<2; ++i) {
                pj_sockaddr_init(af, &mapped_addr[i], NULL, 0);
                pj_sockaddr_copy_addr(&mapped_addr[i], &bound_addr);
                pj_sockaddr_set_port(&mapped_addr[i],
                                     (pj_uint16_t)(acc->next_rtp_port+i));
            }

            break;
        }
    }

    if (sock[0] == PJ_INVALID_SOCKET) {
        PJ_LOG(1,(THIS_FILE,
                  "Unable to find appropriate RTP/RTCP ports combination"));
        goto on_error;
    }


    skinfo->rtp_sock = sock[0];
    pj_sockaddr_cp(&skinfo->rtp_addr_name, &mapped_addr[0]);

    skinfo->rtcp_sock = sock[1];
    pj_sockaddr_cp(&skinfo->rtcp_addr_name, &mapped_addr[1]);

    PJ_LOG(4,(THIS_FILE, "RTP%s socket reachable at %s",
              (call_med->enable_rtcp_mux? " & RTCP": ""),
              pj_sockaddr_print(&skinfo->rtp_addr_name, addr_buf,
                                sizeof(addr_buf), 3)));
    PJ_LOG(4,(THIS_FILE, "RTCP socket reachable at %s",
              pj_sockaddr_print(&skinfo->rtcp_addr_name, addr_buf,
                                sizeof(addr_buf), 3)));

    acc->next_rtp_port += 2;
    return PJ_SUCCESS;

on_error:
    for (i=0; i<2; ++i) {
        if (sock[i] != PJ_INVALID_SOCKET)
            pj_sock_close(sock[i]);
    }
    return status;
}

/* Create normal UDP media transports */
static pj_status_t create_udp_media_transport(const pjsua_transport_config *cfg,
                                              pjsua_call_media *call_med,
                                              const pjmedia_sdp_session *rem_sdp)
{
    pjmedia_sock_info skinfo;
    pj_status_t status;

    status = create_rtp_rtcp_sock(call_med, cfg, &skinfo, rem_sdp);
    if (status != PJ_SUCCESS) {
        pjsua_perror(THIS_FILE, "Unable to create RTP/RTCP socket",
                     status);
        goto on_error;
    }

    status = pjmedia_transport_udp_attach(pjsua_var.med_endpt, NULL,
                                          &skinfo, 0, &call_med->tp);
    if (status != PJ_SUCCESS) {
        pjsua_perror(THIS_FILE, "Unable to create media transport",
                     status);
        goto on_error;
    }

    pjmedia_transport_simulate_lost(call_med->tp, PJMEDIA_DIR_ENCODING,
                                    pjsua_var.media_cfg.tx_drop_pct);

    pjmedia_transport_simulate_lost(call_med->tp, PJMEDIA_DIR_DECODING,
                                    pjsua_var.media_cfg.rx_drop_pct);

    call_med->tp_ready = PJ_SUCCESS;

    return PJ_SUCCESS;

on_error:
    if (call_med->tp)
        pjmedia_transport_close(call_med->tp);

    return status;
}

/* Create loop media transport */
static pj_status_t create_loop_media_transport(
                       const pjsua_transport_config *cfg,
                       pjsua_call_media *call_med,
                       const pjmedia_sdp_session *rem_sdp)
{
    pj_status_t status;
    pjmedia_loop_tp_setting opt;
    pj_bool_t use_ipv6, use_nat64;
    int af;
    pjsua_acc *acc = &pjsua_var.acc[call_med->call->acc_id];

    use_ipv6 = (get_media_ip_version(call_med, rem_sdp, PJ_TRUE, PJ_FALSE)==6);
    use_nat64 = PJ_HAS_IPV6 && (acc->cfg.nat64_opt != PJSUA_NAT64_DISABLED);
    af = (use_ipv6 || use_nat64) ? pj_AF_INET6() : pj_AF_INET();

    pjmedia_loop_tp_setting_default(&opt);
    opt.af = af;
    if (cfg->bound_addr.slen)
        opt.addr = cfg->bound_addr;

    if (acc->next_rtp_port == 0 || cfg->port == 0) {
        if (cfg->port != 0 && cfg->port_range != 0 && cfg->randomize_port) {
            unsigned offset = ((pj_rand() % (cfg->port_range)) / 2) * 2;
            acc->next_rtp_port = (pj_uint16_t)(cfg->port + offset);
        } else {
            acc->next_rtp_port = (pj_uint16_t)cfg->port;
        }
    }

    if (cfg->port > 0 && cfg->port_range > 0 &&
        (acc->next_rtp_port > cfg->port + cfg->port_range ||
         acc->next_rtp_port < cfg->port))
    {
        acc->next_rtp_port = (pj_uint16_t)cfg->port;
    }
    opt.port = acc->next_rtp_port;
    acc->next_rtp_port += 2;

    opt.disable_rx=!pjsua_var.acc[call_med->call->acc_id].cfg.enable_loopback;
    status = pjmedia_transport_loop_create2(pjsua_var.med_endpt, &opt,
                                            &call_med->tp);
    if (status != PJ_SUCCESS) {
        pjsua_perror(THIS_FILE, "Unable to create loop media transport",
                     status);
        goto on_error;
    }

    pjmedia_transport_simulate_lost(call_med->tp, PJMEDIA_DIR_ENCODING,
                                    pjsua_var.media_cfg.tx_drop_pct);

    pjmedia_transport_simulate_lost(call_med->tp, PJMEDIA_DIR_DECODING,
                                    pjsua_var.media_cfg.rx_drop_pct);

    call_med->tp_ready = PJ_SUCCESS;

    return PJ_SUCCESS;

on_error:
    if (call_med->tp)
        pjmedia_transport_close(call_med->tp);

    return status;
}

#if DISABLED_FOR_TICKET_1185
/* Create normal UDP media transports */
static pj_status_t create_udp_media_transports(pjsua_transport_config *cfg)
{
    unsigned i;
    pj_status_t status;

    for (i=0; i < pjsua_var.ua_cfg.max_calls; ++i) {
        pjsua_call *call = &pjsua_var.calls[i];
        unsigned strm_idx;

        for (strm_idx=0; strm_idx < call->med_cnt; ++strm_idx) {
            pjsua_call_media *call_med = &call->media[strm_idx];

            status = create_udp_media_transport(cfg, &call_med->tp);
            if (status != PJ_SUCCESS)
                goto on_error;
        }
    }

    return PJ_SUCCESS;

on_error:
    for (i=0; i < pjsua_var.ua_cfg.max_calls; ++i) {
        pjsua_call *call = &pjsua_var.calls[i];
        unsigned strm_idx;

        for (strm_idx=0; strm_idx < call->med_cnt; ++strm_idx) {
            pjsua_call_media *call_med = &call->media[strm_idx];

            if (call_med->tp) {
                pjmedia_transport_close(call_med->tp);
                call_med->tp = NULL;
            }
        }
    }
    return status;
}
#endif

/* Deferred callback to notify ICE init complete */
static void ice_init_complete_cb(void *user_data)
{
    pjsua_call_media *call_med = (pjsua_call_media*)user_data;

    if (call_med->call == NULL || call_med->tp_ready == PJ_SUCCESS)
        return;

    /* No need to acquire_call() if we only change the tp_ready flag
     * (i.e. transport is being created synchronously). Otherwise
     * calling acquire_call() here may cause deadlock. See
     * https://github.com/pjsip/pjproject/issues/1578
     */
    call_med->tp_ready = call_med->tp_result;

    if (call_med->med_create_cb) {
        pjsua_call *call = NULL;
        pjsip_dialog *dlg = NULL;
        pj_status_t status;

        status = acquire_call("ice_init_complete_cb", call_med->call->index,
                              &call, &dlg);
        if (status != PJ_SUCCESS) {
            if (status != PJSIP_ESESSIONTERMINATED) {
                /* Retry, if call is still active */
                pjsua_schedule_timer2(&ice_init_complete_cb, call_med, 10);
            }
            return;
        }

        (*call_med->med_create_cb)(call_med, call_med->tp_ready,
                                   call_med->call->secure_level, NULL);

        if (dlg)
            pjsip_dlg_dec_lock(dlg);
    }
}

/* Deferred callback to notify ICE negotiation failure */
static void ice_failed_nego_cb(void *user_data)
{
    int call_id = (int)(pj_ssize_t)user_data;
    pjsua_call *call = NULL;
    pjsip_dialog *dlg = NULL;
    pj_status_t status;

    status = acquire_call("ice_failed_nego_cb", call_id, &call, &dlg);
    if (status != PJ_SUCCESS) {
        if (status != PJSIP_ESESSIONTERMINATED) {
            /* Retry, if call is still active */
            pjsua_schedule_timer2(&ice_failed_nego_cb,
                                  (void*)(pj_ssize_t)call_id, 10);
        }
        return;
    }

    if (!call->hanging_up)
        pjsua_var.ua_cfg.cb.on_call_media_state(call_id);

    if (dlg)
        pjsip_dlg_dec_lock(dlg);

}

/* This callback is called when ICE negotiation completes */
static void on_ice_complete(pjmedia_transport *tp, 
                            pj_ice_strans_op op,
                            pj_status_t result)
{
    pjsua_call_media *call_med = (pjsua_call_media*)tp->user_data;
    pjsua_call *call;

    if (!call_med)
        return;

    call = call_med->call;
    
    switch (op) {
    case PJ_ICE_STRANS_OP_INIT:
        call_med->tp_result = result;
        pjsua_schedule_timer2(&ice_init_complete_cb, call_med, 1);
        break;
    case PJ_ICE_STRANS_OP_NEGOTIATION:
        if (result == PJ_SUCCESS) {
            /* Update RTP address */
            pjmedia_transport_info tpinfo;
            pjmedia_transport_info_init(&tpinfo);
            pjmedia_transport_get_info(call_med->tp, &tpinfo);
            pj_sockaddr_cp(&call_med->rtp_addr, &tpinfo.sock_info.rtp_addr_name);
        } else {
            call_med->state = PJSUA_CALL_MEDIA_ERROR;
            call_med->dir = PJMEDIA_DIR_NONE;
            if (!call->hanging_up &&
                pjsua_var.ua_cfg.cb.on_call_media_state)
            {
                /* Defer the callback to a timer */
                pjsua_schedule_timer2(&ice_failed_nego_cb,
                                      (void*)(pj_ssize_t)call->index, 1);
            }
        }

        /* Stop trickling */
        if (call->trickle_ice.trickling < PJSUA_OP_STATE_DONE) {
            call->trickle_ice.trickling = PJSUA_OP_STATE_DONE;
            pjsua_cancel_timer(&call->trickle_ice.timer);
            PJ_LOG(4,(THIS_FILE, "Call %d: ICE trickle stopped trickling as "
                      "ICE nego completed",
                      call->index));
        }

        /* Check if default ICE transport address is changed */
        call->reinv_ice_sent = PJ_FALSE;
        pjsua_call_schedule_reinvite_check(call, 0);
        break;
    case PJ_ICE_STRANS_OP_KEEP_ALIVE:
    case PJ_ICE_STRANS_OP_ADDR_CHANGE:
        if (result != PJ_SUCCESS) {
            PJ_PERROR(4,(THIS_FILE, result,
                         "ICE keep alive failure for transport %d:%d",
                         call->index, call_med->idx));
        }
        if (!call->hanging_up &&
            pjsua_var.ua_cfg.cb.on_call_media_transport_state)
        {
            pjsua_med_tp_state_info info;

            pj_bzero(&info, sizeof(info));
            info.med_idx = call_med->idx;
            info.state = call_med->tp_st;
            info.status = result;
            info.ext_info = &op;
            (*pjsua_var.ua_cfg.cb.on_call_media_transport_state)(
                call->index, &info);
        }
        if (pjsua_var.ua_cfg.cb.on_ice_transport_error &&
            op == PJ_ICE_STRANS_OP_KEEP_ALIVE)
        {
            pjsua_call_id id = call->index;
            (*pjsua_var.ua_cfg.cb.on_ice_transport_error)(id, op, result,
                                                          NULL);
        }
        break;
    }
}


/* Create ICE media transports (when ice is enabled) */
static pj_status_t create_ice_media_transport(
                                const pjsua_transport_config *cfg,
                                pjsua_call_media *call_med,
                                const pjmedia_sdp_session *remote_sdp,
                                pj_bool_t async)
{
    char stunip[PJ_INET6_ADDRSTRLEN];
    pjsua_acc_config *acc_cfg;
    pj_ice_strans_cfg ice_cfg;
    pjmedia_ice_cb ice_cb;
    char name[32];
    unsigned comp_cnt;
    pj_status_t status;
    pj_bool_t use_ipv6, use_nat64;
    pj_bool_t trickle = PJ_FALSE;
    pjmedia_sdp_session *rem_sdp;

    acc_cfg = &pjsua_var.acc[call_med->call->acc_id].cfg;
    use_ipv6 = (get_media_ip_version(call_med, remote_sdp, PJ_FALSE, PJ_TRUE)
                == 6);
    use_nat64 = PJ_HAS_IPV6 && (acc_cfg->nat64_opt != PJSUA_NAT64_DISABLED);

    /* Make sure STUN server resolution has completed */
    if (pjsua_media_acc_is_using_stun(call_med->call->acc_id)) {
        pj_bool_t retry_stun = (acc_cfg->media_stun_use &
                                PJSUA_STUN_RETRY_ON_FAILURE) ==
                                PJSUA_STUN_RETRY_ON_FAILURE;
        status = resolve_stun_server(PJ_TRUE, retry_stun,
                                     (unsigned)acc_cfg->nat64_opt);
        if (status != PJ_SUCCESS) {
            pjsua_perror(THIS_FILE, "Error resolving STUN server", status);
            return status;
        }
    }

    /* Create ICE stream transport configuration */
    pj_ice_strans_cfg_default(&ice_cfg);
    pj_bzero(&ice_cfg.stun, sizeof(ice_cfg.stun));
    pj_bzero(&ice_cfg.turn, sizeof(ice_cfg.turn));
    pj_stun_config_init(&ice_cfg.stun_cfg, &pjsua_var.cp.factory, 0,
                        pjsip_endpt_get_ioqueue(pjsua_var.endpt),
                        pjsip_endpt_get_timer_heap(pjsua_var.endpt));
    
    ice_cfg.resolver = pjsua_var.resolver;
    
    ice_cfg.opt = acc_cfg->ice_cfg.ice_opt;
    if (use_ipv6 || use_nat64) {
        ice_cfg.af = pj_AF_INET6();
    }

    /* Should not wait for ICE STUN/TURN ready when trickle ICE is enabled */
    rem_sdp = call_med->call->async_call.rem_sdp;
    if (ice_cfg.opt.trickle != PJ_ICE_SESS_TRICKLE_DISABLED &&
        (call_med->call->inv == NULL || 
         call_med->call->inv->state < PJSIP_INV_STATE_CONFIRMED))
    {
        if (rem_sdp) {
            /* As answerer: and when remote signals trickle ICE in SDP */
            trickle = pjmedia_ice_sdp_has_trickle(rem_sdp, call_med->idx);
            if (trickle) {
                call_med->call->trickle_ice.remote_sup = PJ_TRUE;
                call_med->call->trickle_ice.enabled = PJ_TRUE;
            }
        } else {
            /* As offerer: and when trickle ICE mode is full */
            trickle = (ice_cfg.opt.trickle==PJ_ICE_SESS_TRICKLE_FULL);
            call_med->call->trickle_ice.enabled = PJ_TRUE;
        }

        /* Check if trickle ICE can start trickling/sending SIP INFO */
        pjsua_ice_check_start_trickling(call_med->call, PJ_FALSE, NULL);
    } else {
        /* For non-initial INVITE, always use regular ICE */
        ice_cfg.opt.trickle = PJ_ICE_SESS_TRICKLE_DISABLED;
    }

    /* If STUN transport is configured, initialize STUN transport settings */
    if ((pj_sockaddr_has_addr(&pjsua_var.stun_srv) &&
         pjsua_media_acc_is_using_stun(call_med->call->acc_id)) ||
        acc_cfg->ice_cfg.ice_max_host_cands != 0)
    {
        ice_cfg.stun_tp_cnt = 1;
        pj_ice_strans_stun_cfg_default(&ice_cfg.stun_tp[0]);
        if (use_nat64) {
            ice_cfg.stun_tp[0].af = pj_AF_INET6();
        } else if (use_ipv6 && PJ_ICE_MAX_STUN >= 2) {
            ice_cfg.stun_tp_cnt = 2;
            pj_ice_strans_stun_cfg_default(&ice_cfg.stun_tp[1]);
            ice_cfg.stun_tp[1].af = pj_AF_INET6();
        }
    }

    /* Configure STUN transport settings */
    if (ice_cfg.stun_tp_cnt) {
        unsigned i;

        if (pj_sockaddr_has_addr(&pjsua_var.stun_srv)) {
            pj_sockaddr_print(&pjsua_var.stun_srv, stunip,
                              sizeof(stunip), 0);
        }

        for (i = 0; i < ice_cfg.stun_tp_cnt; ++i) {
            pj_str_t IN6_ADDR_ANY = {"0", 1};

            /* Configure STUN server */
            if (pjsua_media_acc_is_using_stun(call_med->call->acc_id) &&
                pj_sockaddr_has_addr(&pjsua_var.stun_srv) &&
                pjsua_var.stun_srv.addr.sa_family == ice_cfg.stun_tp[i].af)
            {
                ice_cfg.stun_tp[i].server = pj_str(stunip);
                ice_cfg.stun_tp[i].port = pj_sockaddr_get_port(
                                              &pjsua_var.stun_srv);
            }

            /* Configure max host candidates */
            if (acc_cfg->ice_cfg.ice_max_host_cands >= 0) {
                ice_cfg.stun_tp[i].max_host_cands =
                                acc_cfg->ice_cfg.ice_max_host_cands;
            }

            /* Configure binding address */
            pj_sockaddr_init(ice_cfg.stun_tp[i].af,
                             &ice_cfg.stun_tp[i].cfg.bound_addr,
                             (ice_cfg.stun_tp[i].af == pj_AF_INET()?
                             &cfg->bound_addr: &IN6_ADDR_ANY),
                             (pj_uint16_t)cfg->port);
            ice_cfg.stun_tp[i].cfg.port_range = (pj_uint16_t)cfg->port_range;
            if (cfg->port != 0 && ice_cfg.stun_tp[i].cfg.port_range == 0) {
                ice_cfg.stun_tp[i].cfg.port_range = 
                            (pj_uint16_t)(pjsua_var.ua_cfg.max_calls * 10);
            }

            /* Configure QoS setting */
            ice_cfg.stun_tp[i].cfg.qos_type = cfg->qos_type;
            pj_memcpy(&ice_cfg.stun_tp[i].cfg.qos_params, &cfg->qos_params,
                      sizeof(cfg->qos_params));

            /* Configure max packet size */
            ice_cfg.stun_tp[i].cfg.max_pkt_size = PJMEDIA_MAX_MRU;

            /* Configure manual host candidates */
            ice_cfg.stun_tp[i].manual_host_cnt =
                                        acc_cfg->ice_cfg.ice_manual_host_cnt;
            pj_memcpy(&ice_cfg.stun_tp[i].manual_host,
                      &acc_cfg->ice_cfg.ice_manual_host,
                      sizeof(ice_cfg.stun_tp[i].manual_host));
        }
    }

    /* Configure TURN settings */
    if (acc_cfg->turn_cfg.enable_turn) {
        unsigned i, idx = 0;
        
        if (use_ipv6 && !use_nat64 && PJ_ICE_MAX_TURN >= 3) {
            ice_cfg.turn_tp_cnt = 3;
            idx = 1;
        } else {
            ice_cfg.turn_tp_cnt = 1;
        }
        
        for (i = 0; i < ice_cfg.turn_tp_cnt; i++)
            pj_ice_strans_turn_cfg_default(&ice_cfg.turn_tp[i]);

        if (use_ipv6 || use_nat64) {
            if (!use_nat64)
                ice_cfg.turn_tp[idx++].af = pj_AF_INET6();

            /* Additional candidate: IPv4 relay via IPv6 TURN server */
            ice_cfg.turn_tp[idx].af = pj_AF_INET6();
            ice_cfg.turn_tp[idx].alloc_param.af = pj_AF_INET();
        }

        /* Configure TURN server */

        /* Parse the server entry into host:port */
        status = pj_sockaddr_parse2(pj_AF_UNSPEC(), 0,
                                    &acc_cfg->turn_cfg.turn_server,
                                    &ice_cfg.turn_tp[0].server,
                                    &ice_cfg.turn_tp[0].port,
                                    NULL);
        if (status != PJ_SUCCESS || ice_cfg.turn_tp[0].server.slen == 0) {
            PJ_LOG(1,(THIS_FILE, "Invalid TURN server setting"));
            return PJ_EINVAL;
        }

        if (ice_cfg.turn_tp[0].port == 0)
            ice_cfg.turn_tp[0].port = 3479;

        for (i = 0; i < ice_cfg.turn_tp_cnt; i++) {
            pj_str_t IN6_ADDR_ANY = {"0", 1};

            /* Configure TURN connection settings and credential */
            ice_cfg.turn_tp[i].server    = ice_cfg.turn_tp[0].server;
            ice_cfg.turn_tp[i].port      = ice_cfg.turn_tp[0].port;
            ice_cfg.turn_tp[i].conn_type = acc_cfg->turn_cfg.turn_conn_type;
            pj_memcpy(&ice_cfg.turn_tp[i].auth_cred, 
                      &acc_cfg->turn_cfg.turn_auth_cred,
                      sizeof(ice_cfg.turn_tp[i].auth_cred));

            /* Configure QoS setting */
            ice_cfg.turn_tp[i].cfg.qos_type = cfg->qos_type;
            pj_memcpy(&ice_cfg.turn_tp[i].cfg.qos_params, &cfg->qos_params,
                      sizeof(cfg->qos_params));

            /* Configure binding address */
            pj_sockaddr_init(ice_cfg.turn_tp[i].af,
                             &ice_cfg.turn_tp[i].cfg.bound_addr,
                             (ice_cfg.turn_tp[i].af == pj_AF_INET()?
                             &cfg->bound_addr: &IN6_ADDR_ANY),
                             (pj_uint16_t)cfg->port);
            ice_cfg.turn_tp[i].cfg.port_range = (pj_uint16_t)cfg->port_range;
            if (cfg->port != 0 && ice_cfg.turn_tp[i].cfg.port_range == 0)
                ice_cfg.turn_tp[i].cfg.port_range = 
                                 (pj_uint16_t)(pjsua_var.ua_cfg.max_calls * 10);

            /* Configure max packet size */
            ice_cfg.turn_tp[i].cfg.max_pkt_size = PJMEDIA_MAX_MRU;

#if PJ_HAS_SSL_SOCK
            if (ice_cfg.turn_tp[i].conn_type == PJ_TURN_TP_TLS) {
                pj_memcpy(&ice_cfg.turn_tp[i].cfg.tls_cfg, 
                          &acc_cfg->turn_cfg.turn_tls_setting,
                          sizeof(ice_cfg.turn_tp[i].cfg.tls_cfg));
            }
#endif
        }
    }

    pj_bzero(&ice_cb, sizeof(pjmedia_ice_cb));
    ice_cb.on_ice_complete = &on_ice_complete;
    pj_ansi_snprintf(name, sizeof(name), "icetp%02d", call_med->idx);
    call_med->tp_ready = trickle? PJ_SUCCESS : PJ_EPENDING;

    comp_cnt = 1;
    if (PJMEDIA_ADVERTISE_RTCP && !acc_cfg->ice_cfg.ice_no_rtcp)
        ++comp_cnt;

    status = pjmedia_ice_create3(pjsua_var.med_endpt, name, comp_cnt,
                                 &ice_cfg, &ice_cb, PJSUA_ICE_TRANSPORT_OPTION,
                                 call_med, &call_med->tp);
    if (status != PJ_SUCCESS) {
        pjsua_perror(THIS_FILE, "Unable to create ICE media transport",
                     status);
        goto on_error;
    }

    /* Wait until transport is initialized, or time out */
    if (!async && !trickle) {
        pj_bool_t has_pjsua_lock = PJSUA_LOCK_IS_LOCKED();
        pjsip_dialog *dlg = call_med->call->inv ?
                                call_med->call->inv->dlg : NULL;
        if (has_pjsua_lock)
            PJSUA_UNLOCK();
        if (dlg) {
            /* Don't lock otherwise deadlock:
             * https://github.com/pjsip/pjproject/issues/1737
             */
            pjsip_dlg_inc_session(dlg, &pjsua_var.mod);
            pjsip_dlg_dec_lock(dlg);
        }
        while (call_med->tp_ready == PJ_EPENDING) {
            pjsua_handle_events(100);
        }
        if (dlg) {
            pjsip_dlg_inc_lock(dlg);
            pjsip_dlg_dec_session(dlg, &pjsua_var.mod);
        }
        if (has_pjsua_lock)
            PJSUA_LOCK();
    }

    if (!call_med->tp) {
        /* Call has been disconnected, and media transports have been cleared
         * (see ticket #1759).
         */
        PJ_LOG(4,(THIS_FILE, "Media transport initialization cancelled "
                             "because call has been disconnected"));
        status = PJ_ECANCELLED;
        goto on_error;
    } else if (async && call_med->tp_ready == PJ_EPENDING) {
        return PJ_EPENDING;
    } else if (call_med->tp_ready != PJ_SUCCESS) {
        pjsua_perror(THIS_FILE, "Error initializing ICE media transport",
                     call_med->tp_ready);
        status = call_med->tp_ready;
        goto on_error;
    }

    pjmedia_transport_simulate_lost(call_med->tp, PJMEDIA_DIR_ENCODING,
                                    pjsua_var.media_cfg.tx_drop_pct);

    pjmedia_transport_simulate_lost(call_med->tp, PJMEDIA_DIR_DECODING,
                                    pjsua_var.media_cfg.rx_drop_pct);
    
    return PJ_SUCCESS;

on_error:
    if (call_med->tp != NULL) {
        pjmedia_transport_close(call_med->tp);
        call_med->tp = NULL;
    }

    return status;
}

#if DISABLED_FOR_TICKET_1185
/* Create ICE media transports (when ice is enabled) */
static pj_status_t create_ice_media_transports(pjsua_transport_config *cfg)
{
    unsigned i;
    pj_status_t status;

    for (i=0; i < pjsua_var.ua_cfg.max_calls; ++i) {
        pjsua_call *call = &pjsua_var.calls[i];
        unsigned strm_idx;

        for (strm_idx=0; strm_idx < call->med_cnt; ++strm_idx) {
            pjsua_call_media *call_med = &call->media[strm_idx];

            status = create_ice_media_transport(cfg, call_med);
            if (status != PJ_SUCCESS)
                goto on_error;
        }
    }

    return PJ_SUCCESS;

on_error:
    for (i=0; i < pjsua_var.ua_cfg.max_calls; ++i) {
        pjsua_call *call = &pjsua_var.calls[i];
        unsigned strm_idx;

        for (strm_idx=0; strm_idx < call->med_cnt; ++strm_idx) {
            pjsua_call_media *call_med = &call->media[strm_idx];

            if (call_med->tp) {
                pjmedia_transport_close(call_med->tp);
                call_med->tp = NULL;
            }
        }
    }
    return status;
}
#endif

#if DISABLED_FOR_TICKET_1185
/*
 * Create media transports for all the calls. This function creates
 * one UDP media transport for each call.
 */
PJ_DEF(pj_status_t) pjsua_media_transports_create(
                        const pjsua_transport_config *app_cfg)
{
    pjsua_transport_config cfg;
    unsigned i;
    pj_status_t status;


    /* Make sure pjsua_init() has been called */
    PJ_ASSERT_RETURN(pjsua_var.ua_cfg.max_calls>0, PJ_EINVALIDOP);

    PJSUA_LOCK();

    /* Delete existing media transports */
    for (i=0; i<pjsua_var.ua_cfg.max_calls; ++i) {
        pjsua_call *call = &pjsua_var.calls[i];
        unsigned strm_idx;

        for (strm_idx=0; strm_idx < call->med_cnt; ++strm_idx) {
            pjsua_call_media *call_med = &call->media[strm_idx];

            if (call_med->tp && call_med->tp_auto_del) {
                pjmedia_transport_close(call_med->tp);
                call_med->tp = NULL;
                call_med->tp_orig = NULL;
            }
        }
    }

    /* Copy config */
    pjsua_transport_config_dup(pjsua_var.pool, &cfg, app_cfg);

    /* Create the transports */
    if (pjsua_var.ice_cfg.enable_ice) {
        status = create_ice_media_transports(&cfg);
    } else {
        status = create_udp_media_transports(&cfg);
    }

    /* Set media transport auto_delete to True */
    for (i=0; i<pjsua_var.ua_cfg.max_calls; ++i) {
        pjsua_call *call = &pjsua_var.calls[i];
        unsigned strm_idx;

        for (strm_idx=0; strm_idx < call->med_cnt; ++strm_idx) {
            pjsua_call_media *call_med = &call->media[strm_idx];

            call_med->tp_auto_del = PJ_TRUE;
        }
    }

    PJSUA_UNLOCK();

    return status;
}

/*
 * Attach application's created media transports.
 */
PJ_DEF(pj_status_t) pjsua_media_transports_attach(pjsua_media_transport tp[],
                                                  unsigned count,
                                                  pj_bool_t auto_delete)
{
    unsigned i;

    PJ_ASSERT_RETURN(tp && count==pjsua_var.ua_cfg.max_calls, PJ_EINVAL);

    /* Assign the media transports */
    for (i=0; i<pjsua_var.ua_cfg.max_calls; ++i) {
        pjsua_call *call = &pjsua_var.calls[i];
        unsigned strm_idx;

        for (strm_idx=0; strm_idx < call->med_cnt; ++strm_idx) {
            pjsua_call_media *call_med = &call->media[strm_idx];

            if (call_med->tp && call_med->tp_auto_del) {
                pjmedia_transport_close(call_med->tp);
                call_med->tp = NULL;
                call_med->tp_orig = NULL;
            }
        }

        PJ_TODO(remove_pjsua_media_transports_attach);

        call->media[0].tp = tp[i].transport;
        call->media[0].tp_auto_del = auto_delete;
    }

    return PJ_SUCCESS;
}
#endif

/* Go through the list of media in the SDP, find acceptable media, and
 * sort them based on the below criteria, and store the indexes
 * in the specified array. The criteria is as follows:
 * 1. enabled, i.e: SDP media port not zero
 * 2. transport protocol in the SDP matching account config's
 *    secure media transport usage (\a use_srtp field).
 * 3. active, i.e: SDP media direction is not "inactive"
 * 4. media order (according to the SDP).
 */
static void sort_media(const pjmedia_sdp_session *sdp,
                       const pj_str_t *type,
                       pjmedia_srtp_use use_srtp,
                       pj_uint8_t midx[],
                       unsigned *p_count,
                       unsigned *p_total_count)
{
    unsigned i;
    unsigned count = 0;
    int score[PJSUA_MAX_CALL_MEDIA];

    pj_assert(*p_count >= PJSUA_MAX_CALL_MEDIA);
    pj_assert(*p_total_count >= PJSUA_MAX_CALL_MEDIA);

    *p_count = 0;
    *p_total_count = 0;
    for (i=0; i<PJSUA_MAX_CALL_MEDIA; ++i)
        score[i] = 1;

    /* Score each media */
    for (i=0; i<sdp->media_count && count<PJSUA_MAX_CALL_MEDIA; ++i) {
        const pjmedia_sdp_media *m = sdp->media[i];
        const pjmedia_sdp_conn *c;
        pj_uint32_t proto;

        /* Skip different media */
        if (pj_stricmp(&m->desc.media, type) != 0) {
            score[count++] = -22000;
            continue;
        }

        c = m->conn? m->conn : sdp->conn;

        /* Supported transports */
        proto = pjmedia_sdp_transport_get_proto(&m->desc.transport);
        if (PJMEDIA_TP_PROTO_HAS_FLAG(proto, PJMEDIA_TP_PROTO_RTP_SAVP))
        {
            switch (use_srtp) {
            case PJMEDIA_SRTP_MANDATORY:
            case PJMEDIA_SRTP_OPTIONAL:
                ++score[i];
                break;
            case PJMEDIA_SRTP_DISABLED:
                //--score[i];
                score[i] -= 5;
                break;
            }
        } else if (PJMEDIA_TP_PROTO_HAS_FLAG(proto, PJMEDIA_TP_PROTO_RTP_AVP))
        {
            switch (use_srtp) {
            case PJMEDIA_SRTP_MANDATORY:
                //--score[i];
                score[i] -= 5;
                break;
            case PJMEDIA_SRTP_OPTIONAL:
                /* No change in score */
                break;
            case PJMEDIA_SRTP_DISABLED:
                ++score[i];
                break;
            }
        } else {
            score[i] -= 10;
        }

        /* Is media disabled? */
        if (m->desc.port == 0)
            score[i] -= 10;

        /* Is media inactive? */
        if (pjmedia_sdp_media_find_attr2(m, "inactive", NULL) ||
            pj_strcmp2(&c->addr, "0.0.0.0") == 0)
        {
            //score[i] -= 10;
            score[i] -= 1;
        }

        ++count;
    }

    /* Created sorted list based on quality */
    for (i=0; i<count; ++i) {
        unsigned j;
        int best = 0;

        for (j=1; j<count; ++j) {
            if (score[j] > score[best])
                best = j;
        }
        /* Don't put media with negative score, that media is unacceptable
         * for us.
         */
        midx[i] = (pj_uint8_t)best;
        if (score[best] >= 0)
            (*p_count)++;
        if (score[best] > -22000)
            (*p_total_count)++;

        score[best] = -22000;

    }
}


/* Go through the list of media in the call, find acceptable media, and
 * sort them based on the "quality" of the media, and store the indexes
 * in the specified array. Media with the best quality will be listed
 * first in the array.
 */
static void sort_media2(const pjsua_call_media *call_med,
                        pj_bool_t check_tp,
                        unsigned call_med_cnt,
                        pjmedia_type type,
                        pj_uint8_t midx[],
                        unsigned *p_count,
                        unsigned *p_total_count)
{
    unsigned i;
    unsigned count = 0;
    int score[PJSUA_MAX_CALL_MEDIA];

    pj_assert(*p_count >= PJSUA_MAX_CALL_MEDIA);
    pj_assert(*p_total_count >= PJSUA_MAX_CALL_MEDIA);

    *p_count = 0;
    *p_total_count = 0;
    for (i=0; i<PJSUA_MAX_CALL_MEDIA; ++i)
        score[i] = 1;

    /* Score each media */
    for (i=0; i<call_med_cnt && count<PJSUA_MAX_CALL_MEDIA; ++i) {

        /* Skip different media */
        if (call_med[i].type != type) {
            score[count++] = -22000;
            continue;
        }

        /* Is it active? */
        if (check_tp && !call_med[i].tp) {
            score[i] -= 10;
        }

        ++count;
    }

    /* Created sorted list based on quality */
    for (i=0; i<count; ++i) {
        unsigned j;
        int best = 0;

        for (j=1; j<count; ++j) {
            if (score[j] > score[best])
                best = j;
        }
        /* Don't put media with negative score, that media is unacceptable
         * for us.
         */
        midx[i] = (pj_uint8_t)best;
        if (score[best] >= 0)
            (*p_count)++;
        if (score[best] > -22000)
            (*p_total_count)++;

        score[best] = -22000;

    }
}

/* Callback to receive global media events */
pj_status_t on_media_event(pjmedia_event *event, void *user_data)
{
    char ev_name[5];
    pj_status_t status = PJ_SUCCESS;

    PJ_UNUSED_ARG(user_data);

    pjmedia_fourcc_name(event->type, ev_name);
    PJ_LOG(4,(THIS_FILE, "Received media event type=%s, src=%p, epub=%p",
                         ev_name, event->src, event->epub));

    /* Forward the event */
    if (pjsua_var.ua_cfg.cb.on_media_event) {
        (*pjsua_var.ua_cfg.cb.on_media_event)(event);
    }

    return status;
}

/* Call on_call_media_event() callback using timer */
void call_med_event_cb(void *user_data)
{
    pjsua_event_list *eve = (pjsua_event_list *)user_data;
    
    (*pjsua_var.ua_cfg.cb.on_call_media_event)(eve->call_id,
                                               eve->med_idx,
                                               &eve->event);

    pj_mutex_lock(pjsua_var.timer_mutex);
    pj_list_push_back(&pjsua_var.event_list, eve);
    pj_mutex_unlock(pjsua_var.timer_mutex);
}

/* Callback to receive media events of a call */
pj_status_t call_media_on_event(pjmedia_event *event,
                                void *user_data)
{
    pjsua_call_media *call_med = (pjsua_call_media*)user_data;
    pjsua_call *call = call_med->call;
    char ev_name[5];
    pj_status_t status = PJ_SUCCESS;

    pjmedia_fourcc_name(event->type, ev_name);
    PJ_LOG(5,(THIS_FILE, "Call %d: Media %d: Received media event, type=%s, "
                         "src=%p, epub=%p",
                         call->index, call_med->idx, ev_name,
                         event->src, event->epub));

    switch(event->type) {
        case PJMEDIA_EVENT_KEYFRAME_MISSING:
            if (call->opt.req_keyframe_method & PJSUA_VID_REQ_KEYFRAME_SIP_INFO)
            {
                pj_timestamp now;

                pj_get_timestamp(&now);
                if (pj_elapsed_msec(&call_med->last_req_keyframe, &now) >=
                    PJSUA_VID_REQ_KEYFRAME_INTERVAL)
                {
                    pjsua_msg_data msg_data;
                    const pj_str_t SIP_INFO = {"INFO", 4};
                    const char *BODY_TYPE = "application/media_control+xml";
                    const char *BODY =
                        "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
                        "<media_control><vc_primitive><to_encoder>"
                        "<picture_fast_update/>"
                        "</to_encoder></vc_primitive></media_control>";

                    PJ_LOG(4,(THIS_FILE, 
                              "Sending video keyframe request via SIP INFO"));

                    pjsua_msg_data_init(&msg_data);
                    pj_cstr(&msg_data.content_type, BODY_TYPE);
                    pj_cstr(&msg_data.msg_body, BODY);
                    status = pjsua_call_send_request(call->index, &SIP_INFO, 
                                                     &msg_data);
                    if (status != PJ_SUCCESS) {
                        PJ_PERROR(3,(THIS_FILE, status,
                                  "Failed requesting keyframe via SIP INFO"));
                    } else {
                        call_med->last_req_keyframe = now;
                    }
                }
            }
            break;

#if PJSUA_HAS_VIDEO
        case PJMEDIA_EVENT_FMT_CHANGED:
            if (call_med->strm.v.rdr_win_id != PJSUA_INVALID_ID) {
                pjsua_vid_win *w = &pjsua_var.win[call_med->strm.v.rdr_win_id];
                if (event->epub == w->vp_rend) {
                    /* Renderer just changed format, update its
                     * conference bridge port.
                     */
                    pjsua_vid_conf_update_port(w->rend_slot);
                }
            }

            if (call_med->strm.v.strm_dec_slot != PJSUA_INVALID_ID) {
                /* Stream decoder changed format, update all conf listeners
                 * by reconnecting.
                 */
                pjmedia_port *strm_dec;

                status = pjmedia_vid_stream_get_port(call_med->strm.v.stream,
                                                     PJMEDIA_DIR_DECODING,
                                                     &strm_dec);
                if (status != PJ_SUCCESS)
                    break;

                /* Verify that the publisher is the stream decoder */
                if (event->epub != strm_dec)
                    break;

                /* Stream decoder just changed format, update its
                 * conference bridge port.
                 */
                pjsua_vid_conf_update_port(call_med->strm.v.strm_dec_slot);
            }
            break;

        case PJMEDIA_EVENT_VID_DEV_ERROR:
            {
                PJ_PERROR(3,(THIS_FILE, event->data.vid_dev_err.status,
                             "Video device id=%d error for call %d",
                             event->data.vid_dev_err.id,
                             call->index));
            }
            break;
#endif

        default:
            break;
    }

    if (pjsua_var.ua_cfg.cb.on_call_media_event) {
        pjsua_event_list *eve = NULL;
 
        pj_mutex_lock(pjsua_var.timer_mutex);

        if (pj_list_empty(&pjsua_var.event_list)) {
            eve = PJ_POOL_ALLOC_T(pjsua_var.timer_pool, pjsua_event_list);
        } else {
            eve = pjsua_var.event_list.next;
            pj_list_erase(eve);
        }

        pj_mutex_unlock(pjsua_var.timer_mutex);

        if (call->hanging_up)
            return status;

        eve->call_id = call->index;
        eve->med_idx = call_med->idx;
        pj_memcpy(&eve->event, event, sizeof(pjmedia_event));
        pjsua_schedule_timer2(&call_med_event_cb, eve, 1);
    }

    return status;
}

/* Set media transport state and notify the application via the callback. */
void pjsua_set_media_tp_state(pjsua_call_media *call_med,
                              pjsua_med_tp_st tp_st)
{
    if (!call_med->call->hanging_up &&
        pjsua_var.ua_cfg.cb.on_call_media_transport_state &&
        call_med->tp_st != tp_st)
    {
        pjsua_med_tp_state_info info;

        pj_bzero(&info, sizeof(info));
        info.med_idx = call_med->idx;
        info.state = tp_st;
        info.status = call_med->tp_ready;
        (*pjsua_var.ua_cfg.cb.on_call_media_transport_state)(
            call_med->call->index, &info);
    }

    call_med->tp_st = tp_st;
}

#if defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0)
/* This callback is called when SRTP negotiation completes */
static void on_srtp_nego_complete(pjmedia_transport *tp, 
                                  pj_status_t result)
{
    pjsua_call_media *call_med = (pjsua_call_media*)tp->user_data;
    pjsua_call *call;

    if (!call_med)
        return;

    call = call_med->call;
    PJ_PERROR(4,(THIS_FILE, result,
                 "Call %d: Media %d: SRTP negotiation completes",
                 call->index, call_med->idx));

    if (result != PJ_SUCCESS) {
        call_med->state = PJSUA_CALL_MEDIA_ERROR;
        call_med->dir = PJMEDIA_DIR_NONE;
        if (!call->hanging_up &&
            pjsua_var.ua_cfg.cb.on_call_media_state)
        {
            /* Defer the callback to a timer */
            pjsua_schedule_timer2(&ice_failed_nego_cb,
                                  (void*)(pj_ssize_t)call->index, 1);
        }
    }
}
#endif

/* Callback to resume pjsua_call_media_init() after media transport
 * creation is completed.
 */
static pj_status_t call_media_init_cb(pjsua_call_media *call_med,
                                      pj_status_t status,
                                      int security_level,
                                      int *sip_err_code)
{
    pjsua_acc *acc = &pjsua_var.acc[call_med->call->acc_id];
    pjmedia_transport_info tpinfo;
    int err_code = 0;

    if (status != PJ_SUCCESS) {
        err_code = PJSIP_SC_TEMPORARILY_UNAVAILABLE;
        goto on_return;
    }

    /* Check if media is deinitializing */
    if (call_med->call->async_call.med_ch_deinit || !call_med->tp) {
        status = PJ_ECANCELLED;
        goto on_return;
    }

    pjmedia_transport_simulate_lost(call_med->tp, PJMEDIA_DIR_ENCODING,
                                    pjsua_var.media_cfg.tx_drop_pct);

    pjmedia_transport_simulate_lost(call_med->tp, PJMEDIA_DIR_DECODING,
                                    pjsua_var.media_cfg.rx_drop_pct);

    if (call_med->tp_st == PJSUA_MED_TP_CREATING)
        pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_IDLE);

    if (!call_med->tp_orig &&
        pjsua_var.ua_cfg.cb.on_create_media_transport)
    {
        call_med->use_custom_med_tp = PJ_TRUE;
    } else
        call_med->use_custom_med_tp = PJ_FALSE;

#if defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0)
    /* This function may be called when SRTP transport already exists
     * (e.g: in re-invite, update), don't need to destroy/re-create.
     */
    if (!call_med->tp_orig) {
        pjmedia_srtp_setting srtp_opt;
        pjsua_srtp_opt *acc_srtp_opt = &acc->cfg.srtp_opt;
        pjmedia_transport *srtp = NULL;
        unsigned i;

        /* Check if SRTP requires secure signaling */
        if (acc->cfg.use_srtp != PJMEDIA_SRTP_DISABLED) {
            if (security_level < acc->cfg.srtp_secure_signaling) {
                err_code = PJSIP_SC_NOT_ACCEPTABLE;
                status = PJSIP_ESESSIONINSECURE;
                goto on_return;
            }
        }

        /* Always create SRTP adapter */
        pjmedia_srtp_setting_default(&srtp_opt);
        srtp_opt.close_member_tp = PJ_TRUE;
        srtp_opt.cb.on_srtp_nego_complete = &on_srtp_nego_complete;
        srtp_opt.user_data = call_med;

        /* Get crypto and keying settings from account settings */
        srtp_opt.crypto_count = acc_srtp_opt->crypto_count;
        for (i = 0; i < srtp_opt.crypto_count; ++i)
            srtp_opt.crypto[i] = acc_srtp_opt->crypto[i];
        srtp_opt.keying_count = acc_srtp_opt->keying_count;
        for (i = 0; i < srtp_opt.keying_count; ++i)
            srtp_opt.keying[i] = acc_srtp_opt->keying[i];

        /* If media session has been ever established, let's use remote's 
         * preference in SRTP usage policy, especially when it is stricter.
         */
        if (call_med->rem_srtp_use > acc->cfg.use_srtp)
            srtp_opt.use = call_med->rem_srtp_use;
        else
            srtp_opt.use = acc->cfg.use_srtp;

        if (pjsua_var.ua_cfg.cb.on_create_media_transport_srtp) {
            pjmedia_srtp_setting srtp_opt2 = srtp_opt;
            pjsua_call *call = call_med->call;

            /* Warn that this callback is deprecated (see also #2100) */
            PJ_LOG(1,(THIS_FILE, "Warning: on_create_media_transport_srtp "
                                 "is deprecated and will be removed in the "
                                 "future release"));

            (*pjsua_var.ua_cfg.cb.on_create_media_transport_srtp)
                (call->index, call_med->idx, &srtp_opt2);

            /* Only apply SRTP usage policy if this is initial INVITE */
            if (call->inv && call->inv->state < PJSIP_INV_STATE_CONFIRMED) {
                srtp_opt.use = srtp_opt2.use;
            }

            /* Apply crypto and keying settings from callback */
            srtp_opt.crypto_count = srtp_opt2.crypto_count;
            for (i = 0; i < srtp_opt.crypto_count; ++i)
                srtp_opt.crypto[i] = srtp_opt2.crypto[i];
            srtp_opt.keying_count = srtp_opt2.keying_count;
            for (i = 0; i < srtp_opt.keying_count; ++i)
                srtp_opt.keying[i] = srtp_opt2.keying[i];
        }

        status = pjmedia_transport_srtp_create(pjsua_var.med_endpt,
                                               call_med->tp,
                                               &srtp_opt, &srtp);
        if (status != PJ_SUCCESS) {
            err_code = PJSIP_SC_INTERNAL_SERVER_ERROR;
            goto on_return;
        }

        /* Set SRTP as current media transport */
        call_med->tp_orig = call_med->tp;
        call_med->tp = srtp;
    }
#else
    call_med->tp_orig = call_med->tp;
    PJ_UNUSED_ARG(security_level);
    PJ_UNUSED_ARG(acc);
#endif


    pjmedia_transport_info_init(&tpinfo);
    pjmedia_transport_get_info(call_med->tp, &tpinfo);

    pj_sockaddr_cp(&call_med->rtp_addr, &tpinfo.sock_info.rtp_addr_name);


on_return:
    if (status != PJ_SUCCESS) {
        if (call_med->tp) {
            pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_NULL);
            pjmedia_transport_close(call_med->tp);
            call_med->tp = NULL;
        }

        if (sip_err_code)
            *sip_err_code = err_code;
    }

    if (call_med->med_init_cb) {
        pjsua_med_tp_state_info info;

        pj_bzero(&info, sizeof(info));
        info.status = status;
        info.state = call_med->tp_st;
        info.med_idx = call_med->idx;
        info.sip_err_code = err_code;
        (*call_med->med_init_cb)(call_med->call->index, &info);
    }

    return status;
}

/* Determine if call's media is being changed, for example when video is being
 * added. Then we can reject incoming re-INVITE, for example. This is the
 * solution for https://github.com/pjsip/pjproject/issues/1738
 */
pj_bool_t  pjsua_call_media_is_changing(pjsua_call *call)
{
    /* The problem in #1738 occurs because we do handle_events() loop while
     * adding media, which could cause incoming re-INVITE to be processed and
     * cause havoc. Since the handle_events() loop only happens while adding
     * media, it is sufficient to only check if "prov > cnt" for now.
     */
    return call->med_prov_cnt > call->med_cnt;
}

/* Initialize the media line */
pj_status_t pjsua_call_media_init(pjsua_call_media *call_med,
                                  pjmedia_type type,
                                  const pjmedia_sdp_session *rem_sdp,
                                  const pjsua_transport_config *tcfg,
                                  int security_level,
                                  int *sip_err_code,
                                  pj_bool_t async,
                                  pjsua_med_tp_state_cb cb)
{
    pj_status_t status = PJ_SUCCESS;

    /*
     * Note: this function may be called when the media already exists
     * (e.g. in reinvites, updates, etc.)
     */
    call_med->type = type;

    /* Create the media transport for initial call. Here are the possible
     * media transport state and the action needed:
     * - PJSUA_MED_TP_NULL or call_med->tp==NULL, create one.
     * - PJSUA_MED_TP_RUNNING, do nothing.
     * - PJSUA_MED_TP_DISABLED, re-init (media_create(), etc). Currently,
     *   this won't happen as media_channel_update() will always clean up
     *   the unused transport of a disabled media.
     */
    if (call_med->tp == NULL) {
        pjsua_acc *acc = &pjsua_var.acc[call_med->call->acc_id];

        /* Initializations. If media transport creation completes immediately, 
         * we don't need to call the callbacks.
         */
        call_med->med_init_cb = NULL;
        call_med->med_create_cb = NULL;

#if defined(PJMEDIA_HAS_VIDEO) && (PJMEDIA_HAS_VIDEO != 0)
        /* While in initial call, set default video devices */
        if (type == PJMEDIA_TYPE_VIDEO) {
            status = pjsua_vid_channel_init(call_med);
            if (status != PJ_SUCCESS)
                return status;
        }
#endif

        pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_CREATING);

        if (acc->cfg.use_loop_med_tp) {
            status = create_loop_media_transport(tcfg, call_med, rem_sdp);
        } else if (acc->cfg.ice_cfg.enable_ice) {
            status = create_ice_media_transport(tcfg, call_med, rem_sdp,
                                                async);
            if (async && status == PJ_EPENDING) {
                /* We will resume call media initialization in the
                 * on_ice_complete() callback.
                 */
                call_med->med_create_cb = &call_media_init_cb;
                call_med->med_init_cb = cb;
                
                return PJ_EPENDING;
            }
        } else {
            status = create_udp_media_transport(tcfg, call_med, rem_sdp);
        }

        if (status != PJ_SUCCESS) {
            if (sip_err_code)
                *sip_err_code = PJSIP_SC_INTERNAL_SERVER_ERROR;
            call_med->tp_ready = status;
            pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_NULL);
            pjsua_perror(THIS_FILE, "Error creating media transport", status);
            return status;
        }

    } else if (call_med->tp_st == PJSUA_MED_TP_DISABLED) {
        /* Media is being reenabled. */
        //pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_IDLE);

        pj_assert(!"Currently no media transport reuse");
    }

    return call_media_init_cb(call_med, status, security_level,
                              sip_err_code);
}

/* Callback to resume pjsua_media_channel_init() after media transport
 * initialization is completed.
 */
static pj_status_t media_channel_init_cb(pjsua_call_id call_id,
                                         const pjsua_med_tp_state_info *info)
{
    pjsua_call *call = &pjsua_var.calls[call_id];
    pj_status_t status = (info? info->status : PJ_SUCCESS);
    unsigned mi;

    if (info) {
        pj_mutex_lock(call->med_ch_mutex);

        /* Set the callback to NULL to indicate that the async operation
         * has completed.
         */
        call->media_prov[info->med_idx].med_init_cb = NULL;

        /* In case of failure, save the information to be returned
         * by the last media transport to finish.
         */
        if (info->status != PJ_SUCCESS)
            pj_memcpy(&call->med_ch_info, info, sizeof(*info));

        /* Check whether all the call's medias have finished calling their
         * callbacks.
         */
        for (mi=0; mi < call->med_prov_cnt; ++mi) {
            pjsua_call_media *call_med = &call->media_prov[mi];

            if (call_med->med_init_cb) {
                pj_mutex_unlock(call->med_ch_mutex);
                return PJ_SUCCESS;
            }

            if (call_med->tp_ready != PJ_SUCCESS)
                status = call_med->tp_ready;
        }

        /* OK, we are called by the last media transport finished. */
        pj_mutex_unlock(call->med_ch_mutex);
    }

    if (call->med_ch_mutex) {
        pj_mutex_destroy(call->med_ch_mutex);
        call->med_ch_mutex = NULL;
    }

    PJ_PERROR(5,(THIS_FILE, status,
                 "Call %d: media transport initialization complete", call_id));

    if (status != PJ_SUCCESS) {
        if (call->med_ch_info.status == PJ_SUCCESS) {
            call->med_ch_info.status = status;
            call->med_ch_info.sip_err_code = PJSIP_SC_TEMPORARILY_UNAVAILABLE;
        }

        /* Revert back provisional media. */
        pjsua_media_prov_revert(call_id);

        goto on_return;
    }

    /* Tell the media transport of a new offer/answer session */
    for (mi=0; mi < call->med_prov_cnt; ++mi) {
        pjsua_call_media *call_med = &call->media_prov[mi];

        /* Note: tp may be NULL if this media line is disabled */
        if (call_med->tp && call_med->tp_st == PJSUA_MED_TP_IDLE) {
            pj_pool_t *tmp_pool = call->async_call.pool_prov;
            
            if (!tmp_pool) {
                tmp_pool = (call->inv? call->inv->pool_prov:
                            call->async_call.dlg->pool);
            }

            if (call_med->use_custom_med_tp) {
                unsigned custom_med_tp_flags = PJSUA_MED_TP_CLOSE_MEMBER;
                pj_grp_lock_t *tp_grp_lock = call_med->tp->grp_lock;

                /* Use custom media transport returned by the application */
                call_med->tp =
                    (*pjsua_var.ua_cfg.cb.on_create_media_transport)
                        (call_id, mi, call_med->tp,
                         custom_med_tp_flags);
                if (!call_med->tp) {
                    status =
                        PJSIP_ERRNO_FROM_SIP_STATUS(PJSIP_SC_TEMPORARILY_UNAVAILABLE);
                }

                /* Check if the media transport adapter has no group lock. */
                if (call_med->tp && call_med->tp->grp_lock==NULL) {
                    PJ_LOG(3, (THIS_FILE, "Call %d media %d: Warning, "
                               "media transport adapter should manage its "
                               "lifetime using group lock of the underlying "
                               "transport", call_id, mi));

                    /* Assign group lock, this will maintain the lifetime of
                     * the original transport only, not the transport adapter.
                     */
                    call_med->tp->grp_lock = tp_grp_lock;
                }
            }

            if (call_med->tp) {
                unsigned options = (call_med->enable_rtcp_mux?
                                    PJMEDIA_TPMED_RTCP_MUX: 0);
                status = pjmedia_transport_media_create(
                             call_med->tp, tmp_pool,
                             options, call->async_call.rem_sdp, mi);
            }
            if (status != PJ_SUCCESS) {
                call->med_ch_info.status = status;
                call->med_ch_info.med_idx = mi;
                call->med_ch_info.state = call_med->tp_st;
                call->med_ch_info.sip_err_code = PJSIP_SC_TEMPORARILY_UNAVAILABLE;

                /* Revert back provisional media. */
                pjsua_media_prov_revert(call_id);

                goto on_return;
            }

            pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_INIT);
        }
    }

    call->med_ch_info.status = PJ_SUCCESS;

on_return:
    if (call->med_ch_cb)
        (*call->med_ch_cb)(call->index, &call->med_ch_info);

    return status;
}


/* Clean up media transports in provisional media that is not used by
 * call media.
 */
void pjsua_media_prov_clean_up(pjsua_call_id call_id)
{
    pjsua_call *call = &pjsua_var.calls[call_id];
    unsigned i;

    if (call->med_prov_cnt > call->med_cnt) {
        PJ_LOG(4,(THIS_FILE, "Call %d: cleaning up provisional media, "
                             "prov_med_cnt=%d, med_cnt=%d",
                             call_id, call->med_prov_cnt, call->med_cnt));
    }

    for (i = 0; i < call->med_prov_cnt; ++i) {
        pjsua_call_media *call_med = &call->media_prov[i];
        unsigned j;
        pj_bool_t used = PJ_FALSE;

        if (call_med->tp == NULL)
            continue;

        for (j = 0; j < call->med_cnt; ++j) {
            if (call->media[j].tp == call_med->tp) {
                used = PJ_TRUE;
                break;
            }
        }

        if (!used) {
            if (call_med->tp_st > PJSUA_MED_TP_IDLE) {
                pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_IDLE);
                pjmedia_transport_media_stop(call_med->tp);
            }
            pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_NULL);
            pjmedia_transport_close(call_med->tp);
            call_med->tp = call_med->tp_orig = NULL;
        }
    }
    
    // Cleaning up unused media transports should not change provisional
    // media count.
    //call->med_prov_cnt = 0;
}


/* Revert back provisional media. */
void pjsua_media_prov_revert(pjsua_call_id call_id)
{
    pjsua_call *call = &pjsua_var.calls[call_id];

    /* Clean up unused media transport */
    pjsua_media_prov_clean_up(call_id);

    /* Copy provisional media from active media */
    pj_memcpy(call->media_prov, call->media,
              sizeof(call->media[0]) * call->med_cnt);
    call->med_prov_cnt = call->med_cnt;
}


pj_status_t pjsua_media_channel_init(pjsua_call_id call_id,
                                     pjsip_role_e role,
                                     int security_level,
                                     pj_pool_t *tmp_pool,
                                     const pjmedia_sdp_session *rem_sdp,
                                     int *sip_err_code,
                                     pj_bool_t async,
                                     pjsua_med_tp_state_cb cb)
{
    const pj_str_t STR_AUDIO = { "audio", 5 };
    const pj_str_t STR_VIDEO = { "video", 5 };
    const pj_str_t STR_TEXT  = { "text", 4 };
    pjsua_call *call = &pjsua_var.calls[call_id];
    pjsua_acc *acc = &pjsua_var.acc[call->acc_id];
    pj_uint8_t maudidx[PJSUA_MAX_CALL_MEDIA] = {0};
    unsigned maudcnt = PJ_ARRAY_SIZE(maudidx);
    unsigned mtotaudcnt = PJ_ARRAY_SIZE(maudidx);
    pj_uint8_t mvididx[PJSUA_MAX_CALL_MEDIA] = {0};
    unsigned mvidcnt = PJ_ARRAY_SIZE(mvididx);
    unsigned mtotvidcnt = PJ_ARRAY_SIZE(mvididx);
    pj_uint8_t mtxtidx[PJSUA_MAX_CALL_MEDIA] = {0};
    unsigned mtxtcnt = PJ_ARRAY_SIZE(mtxtidx);
    unsigned mtottxtcnt = PJ_ARRAY_SIZE(mtxtidx);
    unsigned mi;
    pj_bool_t pending_med_tp = PJ_FALSE;
    pj_bool_t reinit = PJ_FALSE;
    pj_status_t status;

    PJ_UNUSED_ARG(role);

    /*
     * Note: this function may be called when the media already exists
     * (e.g. in reinvites, updates, etc).
     */

    if (pjsua_get_state() != PJSUA_STATE_RUNNING) {
        if (sip_err_code)
            *sip_err_code = PJSIP_SC_SERVICE_UNAVAILABLE;
        return PJ_EBUSY;
    }

    if (async) {
        pj_pool_t *tmppool = (call->inv? call->inv->pool_prov:
                              call->async_call.dlg->pool);

        status = pj_mutex_create_simple(tmppool, NULL, &call->med_ch_mutex);
        if (status != PJ_SUCCESS) {
            if (sip_err_code)
                *sip_err_code = PJSIP_SC_INTERNAL_SERVER_ERROR;
            return status;
        }
    }

    if (call->inv && call->inv->state == PJSIP_INV_STATE_CONFIRMED)
        reinit = PJ_TRUE;

    PJ_LOG(4,(THIS_FILE, "Call %d: %sinitializing media..",
                         call_id, (reinit?"re-":"") ));

    pj_log_push_indent();

    /* Init provisional media state */
    if (call->med_cnt == 0) {
        /* New media session, just copy whole from call media state. */
        pj_memcpy(call->media_prov, call->media, sizeof(call->media));
    } else {
        /* Clean up any unused transports. Note that when local SDP reoffer
         * is rejected by remote, there may be any initialized transports that
         * are not used by call media and currently there is no notification
         * from PJSIP level regarding the reoffer rejection.
         */
        pjsua_media_prov_clean_up(call_id);

        /* Updating media session, copy from call media state. */
        pj_memcpy(call->media_prov, call->media,
                  sizeof(call->media[0]) * call->med_cnt);
    }
    call->med_prov_cnt = call->med_cnt;

#if DISABLED_FOR_TICKET_1185
    /* Return error if media transport has not been created yet
     * (e.g. application is starting)
     */
    for (i=0; i<call->med_cnt; ++i) {
        if (call->media[i].tp == NULL) {
            status = PJ_EBUSY;
            goto on_error;
        }
    }
#endif

    /* Get media count for each media type */
    if (rem_sdp) {

        /* We are sending answer, check media count for each media type
         * from the remote SDP.
         */
        sort_media(rem_sdp, &STR_AUDIO, acc->cfg.use_srtp,
                   maudidx, &maudcnt, &mtotaudcnt);

#if PJMEDIA_HAS_VIDEO
        sort_media(rem_sdp, &STR_VIDEO, acc->cfg.use_srtp,
                   mvididx, &mvidcnt, &mtotvidcnt);
#else
        mvidcnt = mtotvidcnt = 0;
        PJ_UNUSED_ARG(STR_VIDEO);
#endif

        sort_media(rem_sdp, &STR_TEXT, acc->cfg.use_srtp,
                   mtxtidx, &mtxtcnt, &mtottxtcnt);

        if (maudcnt + mvidcnt + mtxtcnt == 0) {
            /* Expecting media in the offer */
            if (sip_err_code)
                *sip_err_code = PJSIP_SC_NOT_ACCEPTABLE_HERE;
            status = PJSIP_ERRNO_FROM_SIP_STATUS(PJSIP_SC_NOT_ACCEPTABLE_HERE);
            goto on_error;
        }

        /* Update media count only when remote add any media, this media count
         * must never decrease. Also note that we shouldn't apply the media
         * count setting (of the call setting) before the SDP negotiation.
         */
        if (call->med_prov_cnt < rem_sdp->media_count)
            call->med_prov_cnt = PJ_MIN(rem_sdp->media_count,
                                        PJSUA_MAX_CALL_MEDIA);

        call->rem_offerer = PJ_TRUE;
        call->rem_aud_cnt = maudcnt;
        call->rem_vid_cnt = mvidcnt;
        call->rem_txt_cnt = mtxtcnt;

    } else {

        /* If call is already established, adjust the existing call media list
         * to media count setting in call setting, e.g: re-enable/disable/add
         * media from existing media.
         * Otherwise, apply media count from the call setting directly.
         */
        if (reinit) {
            pj_bool_t sort_check_tp;

            /* Media sorting below will check transport, i.e: media without
             * transport will have lower priority. If PJSUA_CALL_REINIT_MEDIA
             * is set, we must not check transport.
             */
            sort_check_tp = !(call->opt.flag & PJSUA_CALL_REINIT_MEDIA);

            /* We are sending reoffer, check media count for each media type
             * from the existing call media list.
             */
            sort_media2(call->media_prov, sort_check_tp, call->med_prov_cnt,
                        PJMEDIA_TYPE_AUDIO, maudidx, &maudcnt, &mtotaudcnt);

            /* No need to assert if there's no media. */
            //pj_assert(maudcnt > 0);

            sort_media2(call->media_prov, sort_check_tp, call->med_prov_cnt,
                        PJMEDIA_TYPE_VIDEO, mvididx, &mvidcnt, &mtotvidcnt);

            sort_media2(call->media_prov, sort_check_tp, call->med_prov_cnt,
                        PJMEDIA_TYPE_TEXT, mtxtidx, &mtxtcnt, &mtottxtcnt);

            /* Call setting may add or remove media. Adding media is done by
             * enabling any disabled/port-zeroed media first, then adding new
             * media whenever needed. Removing media is done by disabling
             * media with the lowest 'quality'.
             */

            /* Check if we need to add new audio */
            if (maudcnt < call->opt.aud_cnt &&
                mtotaudcnt < call->opt.aud_cnt)
            {
                for (mi = 0; mi < call->opt.aud_cnt - mtotaudcnt; ++mi)
                    maudidx[maudcnt++] = (pj_uint8_t)call->med_prov_cnt++;
                
                mtotaudcnt = call->opt.aud_cnt;
            }
            maudcnt = call->opt.aud_cnt;

            /* Check if we need to add new video */
            if (mvidcnt < call->opt.vid_cnt &&
                mtotvidcnt < call->opt.vid_cnt)
            {
                for (mi = 0; mi < call->opt.vid_cnt - mtotvidcnt; ++mi)
                    mvididx[mvidcnt++] = (pj_uint8_t)call->med_prov_cnt++;

                mtotvidcnt = call->opt.vid_cnt;
            }
            mvidcnt = call->opt.vid_cnt;

            /* Check if we need to add new text */
            if (mtxtcnt < call->opt.txt_cnt &&
                mtottxtcnt < call->opt.txt_cnt)
            {
                for (mi = 0; mi < call->opt.txt_cnt - mtottxtcnt; ++mi)
                    mtxtidx[mtxtcnt++] = (pj_uint8_t)call->med_prov_cnt++;

                mtottxtcnt = call->opt.txt_cnt;
            }
            mtxtcnt = call->opt.txt_cnt;

            /* In case of media reinit, 'med_prov_cnt' may be decreased
             * because the new call->opt says so. As media count should
             * never decrease, we should verify 'med_prov_cnt' to be
             * at least equal to 'med_cnt' (see also #1987).
             */
            if ((call->opt.flag & PJSUA_CALL_REINIT_MEDIA) &&
                call->med_prov_cnt < call->med_cnt)
            {
                call->med_prov_cnt = call->med_cnt;
            }

        } else {

            maudcnt = mtotaudcnt = call->opt.aud_cnt;
            for (mi=0; mi<maudcnt; ++mi) {
                maudidx[mi] = (pj_uint8_t)mi;
            }
            mvidcnt = mtotvidcnt = call->opt.vid_cnt;
            for (mi=0; mi<mvidcnt; ++mi) {
                mvididx[mi] = (pj_uint8_t)(maudcnt + mi);
            }
            mtxtcnt = mtottxtcnt = call->opt.txt_cnt;
            for (mi=0; mi<mtxtcnt; ++mi) {
                mtxtidx[mi] = (pj_uint8_t)(maudcnt + mvidcnt + mi);
            }
            call->med_prov_cnt = maudcnt + mvidcnt + mtxtcnt;

            /* Need to publish supported media? */
            if (call->opt.flag & PJSUA_CALL_INCLUDE_DISABLED_MEDIA) {
                if (mtotaudcnt == 0) {
                    mtotaudcnt = 1;
                    maudidx[0] = (pj_uint8_t)call->med_prov_cnt++;
                }
#if PJMEDIA_HAS_VIDEO
                if (mtotvidcnt == 0) {
                    mtotvidcnt = 1;
                    mvididx[0] = (pj_uint8_t)call->med_prov_cnt++;
                }
#endif
                if (mtottxtcnt == 0) {
                    mtottxtcnt = 1;
                    mtxtidx[0] = (pj_uint8_t)call->med_prov_cnt++;
                }
            }
        }

        call->rem_offerer = PJ_FALSE;
    }

    if (call->med_prov_cnt == 0) {
        /* Expecting at least one media */
        if (sip_err_code)
            *sip_err_code = PJSIP_SC_NOT_ACCEPTABLE_HERE;
        status = PJSIP_ERRNO_FROM_SIP_STATUS(PJSIP_SC_NOT_ACCEPTABLE_HERE);
        goto on_error;
    }

    if (async) {
        call->med_ch_cb = cb;
    }

    if (rem_sdp && call->inv) {
        call->async_call.rem_sdp =
            pjmedia_sdp_session_clone(call->inv->pool_prov, rem_sdp);
    } else {
        call->async_call.rem_sdp = NULL;
    }

    call->async_call.pool_prov = tmp_pool;

    /* Initialize each media line */
    for (mi=0; mi < call->med_prov_cnt; ++mi) {
        pjsua_call_media *call_med = &call->media_prov[mi];
        pj_bool_t enabled = PJ_FALSE;
        pjmedia_type media_type = PJMEDIA_TYPE_UNKNOWN;

        if (pj_memchr(maudidx, mi, mtotaudcnt * sizeof(maudidx[0]))) {
            media_type = PJMEDIA_TYPE_AUDIO;
            if (call->opt.aud_cnt &&
                pj_memchr(maudidx, mi, maudcnt * sizeof(maudidx[0])))
            {
                enabled = PJ_TRUE;
            }
        } else if (pj_memchr(mvididx, mi, mtotvidcnt * sizeof(mvididx[0]))) {
            media_type = PJMEDIA_TYPE_VIDEO;
            if (call->opt.vid_cnt &&
                pj_memchr(mvididx, mi, mvidcnt * sizeof(mvididx[0])))
            {
                enabled = PJ_TRUE;
            }
        } else if (pj_memchr(mtxtidx, mi, mtottxtcnt * sizeof(mtxtidx[0]))) {
            media_type = PJMEDIA_TYPE_TEXT;
            if (call->opt.txt_cnt &&
                pj_memchr(mtxtidx, mi, mtxtcnt * sizeof(mtxtidx[0])))
            {
                enabled = PJ_TRUE;
            }
        }

        if (call->opt.flag & PJSUA_CALL_SET_MEDIA_DIR) {
            call_med->def_dir = call->opt.media_dir[mi];
            PJ_LOG(4,(THIS_FILE, "Call %d: setting media direction "
                                 "#%d to %d.", call_id, mi,
                                 call_med->def_dir));
        } else if (!reinit || mi >= call->med_cnt) {
            /* Initialize default media direction as bidirectional,
             * for initial media or newly added media.
             */
            call_med->def_dir = PJMEDIA_DIR_ENCODING_DECODING;
        }

        if (enabled) {
            call_med->enable_rtcp_mux = acc->cfg.enable_rtcp_mux;

            status = pjsua_call_media_init(call_med, media_type, rem_sdp,
                                           &acc->cfg.rtp_cfg,
                                           security_level, sip_err_code,
                                           async,
                                           (async? &media_channel_init_cb:
                                            NULL));
            if (status == PJ_EPENDING) {
                pending_med_tp = PJ_TRUE;
            } else if (status != PJ_SUCCESS) {
                if (pending_med_tp) {
                    /* Save failure information. */
                    call_med->tp_ready = status;
                    pj_bzero(&call->med_ch_info, sizeof(call->med_ch_info));
                    call->med_ch_info.status = status;
                    call->med_ch_info.state = call_med->tp_st;
                    call->med_ch_info.med_idx = call_med->idx;
                    if (sip_err_code)
                        call->med_ch_info.sip_err_code = *sip_err_code;

                    /* We will return failure in the callback later. */
                    return PJ_EPENDING;
                }

                /* Revert back provisional media. */
                pjsua_media_prov_revert(call_id);

                goto on_error;
            }

            /* Find and save "a=mid". Currently this is for trickle ICE.
             * Trickle ICE match media in SDP of SIP INFO by comparing this
             * attribute, so remote SDP must be received first before remote
             * SDP in SIP INFO can be processed.
             */
            if (rem_sdp && call_med->rem_mid.slen == 0) {
                const pjmedia_sdp_media *m = rem_sdp->media[mi];
                pjmedia_sdp_attr *a;

                a = pjmedia_sdp_media_find_attr2(m, "mid", NULL);
                if (a)
                    call_med->rem_mid = a->value;
            }

        } else {
            /* By convention, the media is disabled if transport is NULL 
             * or transport state is PJSUA_MED_TP_DISABLED.
             */
            if (call_med->tp) {
                // Don't close transport here, as SDP negotiation has not been
                // done and stream may be still active. Once SDP negotiation
                // is done (channel_update() invoked), this transport will be
                // closed there.
                //pjmedia_transport_close(call_med->tp);
                //call_med->tp = NULL;
                pj_assert(call_med->tp_st == PJSUA_MED_TP_INIT || 
                          call_med->tp_st == PJSUA_MED_TP_RUNNING);
                pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_DISABLED);
            }

            /* Put media type just for info if not yet defined */
            if (call_med->type == PJMEDIA_TYPE_NONE)
                call_med->type = media_type;
        }
    }

    if (maudcnt > 0) {
        call->audio_idx = maudidx[0];

        PJ_LOG(4,(THIS_FILE, "Media index %d selected for audio call %d",
                  call->audio_idx, call->index));
    } else {
        call->audio_idx = -1;
    }

    if (pending_med_tp) {
        /* We shouldn't use temporary pool anymore. */
        call->async_call.pool_prov = NULL;
        /* We have a pending media transport initialization. */
        pj_log_pop_indent();
        return PJ_EPENDING;
    }

    /* Media transport initialization completed immediately, so 
     * we don't need to call the callback.
     */
    call->med_ch_cb = NULL;

    status = media_channel_init_cb(call_id, NULL);
    if (status != PJ_SUCCESS && sip_err_code) {
        if (call->med_ch_info.sip_err_code)
            *sip_err_code = call->med_ch_info.sip_err_code;
        else
            *sip_err_code = PJSIP_ERRNO_TO_SIP_STATUS(status);
    }

    pj_log_pop_indent();
    return status;

on_error:
    if (call->med_ch_mutex) {
        pj_mutex_destroy(call->med_ch_mutex);
        call->med_ch_mutex = NULL;
    }

    if (sip_err_code && *sip_err_code == 0)
        *sip_err_code = PJSIP_ERRNO_TO_SIP_STATUS(status);

    pj_log_pop_indent();
    return status;
}


/* Create SDP based on the current media channel. Note that, this function
 * will not modify the media channel, so when receiving new offer or
 * updating media count (via call setting), media channel must be reinit'd
 * (using pjsua_media_channel_init()) first before calling this function.
 */
pj_status_t pjsua_media_channel_create_sdp(pjsua_call_id call_id, 
                                           pj_pool_t *pool,
                                           const pjmedia_sdp_session *rem_sdp,
                                           pjmedia_sdp_session **p_sdp,
                                           int *sip_err_code)
{
    enum { MAX_MEDIA = PJSUA_MAX_CALL_MEDIA };
    pjmedia_sdp_session *sdp;
    pj_sockaddr origin;
    pjsua_call *call = &pjsua_var.calls[call_id];
    pjsua_acc *acc = &pjsua_var.acc[call->acc_id];
    pjmedia_sdp_neg_state sdp_neg_state = PJMEDIA_SDP_NEG_STATE_NULL;
    unsigned mi;
    unsigned tot_bandw_tias = 0;
    pj_status_t status;

    if (pjsua_get_state() != PJSUA_STATE_RUNNING) {
        status = PJ_EBUSY;
        goto on_error;
    }

#if 0
    // This function should not really change the media channel.
    if (rem_sdp) {
        /* If this is a re-offer, let's re-initialize media as remote may
         * add or remove media
         */
        if (call->inv && call->inv->state == PJSIP_INV_STATE_CONFIRMED) {
            status = pjsua_media_channel_init(call_id, PJSIP_ROLE_UAS,
                                              call->secure_level, pool,
                                              rem_sdp, sip_err_code,
                                              PJ_FALSE, NULL);
            if (status != PJ_SUCCESS)
                return status;
        }
    } else {
        /* Audio is first in our offer, by convention */
        // The audio_idx should not be changed here, as this function may be
        // called in generating re-offer and the current active audio index
        // can be anywhere.
        //call->audio_idx = 0;
    }
#endif

#if 0
    // Since r3512, old-style hold should have got transport, created by 
    // pjsua_media_channel_init() in initial offer/answer or remote reoffer.
    /* Create media if it's not created. This could happen when call is
     * currently on-hold (with the old style hold)
     */
    if (call->media[call->audio_idx].tp == NULL) {
        pjsip_role_e role;
        role = (rem_sdp ? PJSIP_ROLE_UAS : PJSIP_ROLE_UAC);
        status = pjsua_media_channel_init(call_id, role, call->secure_level, 
                                          pool, rem_sdp, sip_err_code);
        if (status != PJ_SUCCESS)
            return status;
    }
#endif

    /* Get SDP negotiator state */
    if (call->inv && call->inv->neg)
        sdp_neg_state = pjmedia_sdp_neg_get_state(call->inv->neg);

    PJ_UNUSED_ARG(sdp_neg_state);

    /* Get one address to use in the origin field */
    pj_bzero(&origin, sizeof(origin));
    for (mi=0; mi<call->med_prov_cnt; ++mi) {
        pjmedia_transport_info tpinfo;

        if (call->media_prov[mi].tp == NULL)
            continue;

        pjmedia_transport_info_init(&tpinfo);
        pjmedia_transport_get_info(call->media_prov[mi].tp, &tpinfo);
        pj_sockaddr_cp(&origin, &tpinfo.sock_info.rtp_addr_name);
        break;
    }

    /* Create the base (blank) SDP */
    status = pjmedia_endpt_create_base_sdp(pjsua_var.med_endpt, pool, NULL,
                                           &origin, &sdp);
    if (status != PJ_SUCCESS)
        goto on_error;

    /* Process each media line */
    for (mi=0; mi<call->med_prov_cnt; ++mi) {
        pjsua_call_media *call_med = &call->media_prov[mi];
        pjmedia_sdp_media *m = NULL;
        pjmedia_transport_info tpinfo;
        pjmedia_endpt_create_sdp_param param;
        unsigned i;

        if (rem_sdp && mi >= rem_sdp->media_count) {
            /* Remote might have removed some media lines. */
            /* Note that we must not modify the current active media
             * (e.g: stop stream, close/cleanup media transport), as if
             * SDP nego fails, the current active media should be maintained.
             * Also note that our media count should never decrease, even when
             * remote removed some media lines.
             */
            break;
        }

        if (call_med->tp == NULL || call_med->tp_st == PJSUA_MED_TP_DISABLED)
        {
            /*
             * This media is disabled. Just create a valid SDP with zero
             * port.
             */
            if (rem_sdp) {
                /* Just clone the remote media and deactivate it */
                m = pjmedia_sdp_media_clone_deactivate(pool,
                                                       rem_sdp->media[mi]);
            } else {
                m = PJ_POOL_ZALLOC_T(pool, pjmedia_sdp_media);
                m->desc.transport = pj_str("RTP/AVP");
                m->desc.fmt_count = 1;

                switch (call_med->type) {
                case PJMEDIA_TYPE_AUDIO:
                    m->desc.media = pj_str("audio");
                    m->desc.fmt[0] = pj_str("0");
                    break;
                case PJMEDIA_TYPE_VIDEO:
                    m->desc.media = pj_str("video");
                    m->desc.fmt[0] = pj_str("31");
                    break;
                case PJMEDIA_TYPE_TEXT:
                    m->desc.media = pj_str("text");
                    m->desc.fmt[0] = pj_str("0");
                    break;
                default:
                    /* This must be us generating re-offer, and some unknown
                     * media may exist, so just clone from active local SDP
                     * (and it should have been deactivated already).
                     */
                    pj_assert(call->inv && call->inv->neg &&
                              sdp_neg_state == PJMEDIA_SDP_NEG_STATE_DONE);
                    {
                        const pjmedia_sdp_session *s_;
                        pjmedia_sdp_neg_get_active_local(call->inv->neg, &s_);

                        if (mi < s_->media_count) {
                            m = pjmedia_sdp_media_clone(pool, s_->media[mi]);
                            m->desc.port = 0;
                        } else {
                            /* Remote may have removed some media lines in
                             * previous negotiations. However, since our
                             * media count may never decrease (as per
                             * the RFC), we'll just offer unknown media here.
                             */
                            m->desc.media = pj_str("unknown");
                            m->desc.fmt[0] = pj_str("0");
                        }
                    }
                    break;
                }
            }

            /* Add connection line, if none */
            if (m->conn == NULL && sdp->conn == NULL) {
                pj_bool_t use_ipv6;
                pj_bool_t use_nat64;

                if (rem_sdp) {
                    use_ipv6 = (get_media_ip_version(call_med, rem_sdp,
                                                     PJ_FALSE, PJ_FALSE) == 6);
                } else {
                    //use_ipv6 = PJ_HAS_IPV6 &&
                    //    (pjsua_var.acc[call->acc_id].cfg.ipv6_media_use !=
                    //     PJSUA_IPV6_DISABLED);
                    use_ipv6 = (get_media_ip_version(call_med, rem_sdp,
                                                     PJ_FALSE, PJ_FALSE) == 6);
                }
                use_nat64 = PJ_HAS_IPV6 &&
                            (pjsua_var.acc[call->acc_id].cfg.nat64_opt !=
                             PJSUA_NAT64_DISABLED);

                m->conn = PJ_POOL_ZALLOC_T(pool, pjmedia_sdp_conn);
                m->conn->net_type = pj_str("IN");
                if (use_ipv6 && !use_nat64) {
                    m->conn->addr_type = pj_str("IP6");
                    m->conn->addr = pj_str("::1");
                } else {
                    m->conn->addr_type = pj_str("IP4");
                    m->conn->addr = pj_str("127.0.0.1");
                }
            }

            sdp->media[sdp->media_count++] = m;
            continue;
        }

        /* Get transport address info */
        pjmedia_transport_info_init(&tpinfo);
        pjmedia_transport_get_info(call_med->tp, &tpinfo);

        /* Ask pjmedia endpoint to create SDP media line */
        pjmedia_endpt_create_sdp_param_default(&param);
        param.dir = call_med->def_dir;
        switch (call_med->type) {
        case PJMEDIA_TYPE_AUDIO:
            status = pjmedia_endpt_create_audio_sdp(pjsua_var.med_endpt, pool,
                                                    &tpinfo.sock_info,
                                                    &param, &m);
            break;
#if defined(PJMEDIA_HAS_VIDEO) && (PJMEDIA_HAS_VIDEO != 0)
        case PJMEDIA_TYPE_VIDEO:
            status = pjmedia_endpt_create_video_sdp(pjsua_var.med_endpt, pool,
                                                    &tpinfo.sock_info,
                                                    &param, &m);
            break;
#endif
        case PJMEDIA_TYPE_TEXT:
            param.red_level = acc->cfg.txt_red_level;
            status = pjmedia_endpt_create_text_sdp(pjsua_var.med_endpt, pool,
                                                    &tpinfo.sock_info,
                                                    &param, &m);
            break;
        default:
            pj_assert(!"Invalid call_med media type");
            status = PJ_EBUG;
        }

        if (status != PJ_SUCCESS)
            goto on_error;

        /* Add generated media to SDP session */
        sdp->media[sdp->media_count++] = m;

        /* Disable media if it has zero format/codec */
        if (m->desc.fmt_count == 0) {
            m->desc.fmt[m->desc.fmt_count++] = pj_str("0");
            pjmedia_sdp_media_deactivate(pool, m);
            PJ_LOG(3,(THIS_FILE,
                      "Call %d media %d: Disabled due to no active codec",
                      call_id, mi));
            continue;
        }

        /* Check if request supports PJSIP_INV_REQUIRE_SIPREC. If so Get label
         * attribute in SDP offer and add label attribute to SDP answer
         */
        if (call->inv && (call->inv->options & PJSIP_INV_REQUIRE_SIPREC) &&
            rem_sdp)
        {
            pjmedia_sdp_attr *label_attr;
            const pj_str_t STR_LABEL_ATTR = {"label", 5};

            label_attr = pjmedia_sdp_media_find_attr(
                            rem_sdp->media[mi], &STR_LABEL_ATTR, NULL);
            m->attr[m->attr_count++] = pjmedia_sdp_attr_create_label(pool,
                                                        &label_attr->value);
        }

        /* Add ssrc and cname attribute */
        m->attr[m->attr_count++] = pjmedia_sdp_attr_create_ssrc(pool,
                                                                call_med->ssrc,
                                                                &call->cname);

        /* Give to transport */
        status = pjmedia_transport_encode_sdp(call_med->tp, pool,
                                              sdp, rem_sdp, mi);
        if (status != PJ_SUCCESS) {
            if (sip_err_code) *sip_err_code = PJSIP_SC_NOT_ACCEPTABLE;
            goto on_error;
        }

#if PJSUA_SDP_SESS_HAS_CONN
        /* Copy c= line of the first media to session level,
         * if there's none.
         */
        if (sdp->conn == NULL) {
            sdp->conn = pjmedia_sdp_conn_clone(pool, m->conn);
        }
#endif

        
        /* Find media bandwidth info */
        for (i = 0; i < m->bandw_count; ++i) {
            const pj_str_t STR_BANDW_MODIFIER_TIAS = { "TIAS", 4 };
            if (!pj_stricmp(&m->bandw[i]->modifier, &STR_BANDW_MODIFIER_TIAS))
            {
                tot_bandw_tias += m->bandw[i]->value;
                break;
            }
        }

        /* Setup RTCP-FB */
        {
            pjmedia_rtcp_fb_setting rtcp_cfg;
            pjmedia_rtcp_fb_setting_default(&rtcp_cfg);

            /* Add RTCP-FB PLI if PJSUA_VID_REQ_KEYFRAME_RTCP_PLI is set */
            if (call_med->type == PJMEDIA_TYPE_VIDEO &&
                (call->opt.req_keyframe_method &
                 PJSUA_VID_REQ_KEYFRAME_RTCP_PLI))
            {
                rtcp_cfg.cap_count = 1;
                pj_strset2(&rtcp_cfg.caps[0].codec_id, (char*)"*");
                rtcp_cfg.caps[0].type = PJMEDIA_RTCP_FB_NACK;
                pj_strset2(&rtcp_cfg.caps[0].param, (char*)"pli");
            }

            /* Should we put "RTP/AVPF" in SDP?*/
            if (rem_sdp) {
                /* For answer, match remote offer */
                unsigned rem_proto = 0;
                rem_proto = pjmedia_sdp_transport_get_proto(
                                        &rem_sdp->media[mi]->desc.transport);
                rtcp_cfg.dont_use_avpf =
                        !PJMEDIA_TP_PROTO_HAS_FLAG(rem_proto, 
                                                PJMEDIA_TP_PROFILE_RTCP_FB);
            } else {
                /* For offer, check account setting */
                rtcp_cfg.dont_use_avpf = acc->cfg.rtcp_fb_cfg.dont_use_avpf ||
                                         (acc->cfg.rtcp_fb_cfg.cap_count == 0
                                          && rtcp_cfg.cap_count == 0);
            }

            status = pjmedia_rtcp_fb_encode_sdp(pool, pjsua_var.med_endpt,
                                                &rtcp_cfg, sdp,
                                                mi, rem_sdp);
            if (status != PJ_SUCCESS) {
                PJ_PERROR(3,(THIS_FILE, status,
                             "Call %d media %d: Failed to encode RTCP-FB PLI "
                             "setting to SDP",
                             call_id, mi));
            }

            /* For offer, add any other RTCP-FB setting configured in account
             * setting.
             */
            if (!rem_sdp && acc->cfg.rtcp_fb_cfg.cap_count) {
                pj_bool_t tmp = rtcp_cfg.dont_use_avpf;
                rtcp_cfg = acc->cfg.rtcp_fb_cfg;
                rtcp_cfg.dont_use_avpf = tmp;
                status = pjmedia_rtcp_fb_encode_sdp(pool, pjsua_var.med_endpt,
                                                    &rtcp_cfg, sdp,
                                                    mi, rem_sdp);
                if (status != PJ_SUCCESS) {
                    PJ_PERROR(3,(THIS_FILE, status,
                                 "Call %d media %d: Failed to encode account "
                                 "RTCP-FB setting to SDP",
                                 call_id, mi));
                }
            }
        }

        /* Find and save "a=mid". Currently this is for trickle ICE. Trickle
         * ICE match media in SDP of SIP INFO by comparing this attribute,
         * so remote SDP must be received first before remote SDP in SIP INFO
         * can be processed.
         */
        if (call_med->rem_mid.slen == 0) {
            pjmedia_sdp_attr *a;

            a = pjmedia_sdp_media_find_attr2(m, "mid", NULL);
            if (a)
                call_med->rem_mid = a->value;
        }
    }

    /* Add NAT info in the SDP */
    if (pjsua_var.ua_cfg.nat_type_in_sdp) {
        pjmedia_sdp_attr *a;
        pj_str_t value;
        char nat_info[80];

        value.ptr = nat_info;
        if (pjsua_var.ua_cfg.nat_type_in_sdp == 1) {
            value.slen = pj_ansi_snprintf(nat_info, sizeof(nat_info),
                                          "%d", pjsua_var.nat_type);
        } else {
            const char *type_name = pj_stun_get_nat_name(pjsua_var.nat_type);
            value.slen = pj_ansi_snprintf(nat_info, sizeof(nat_info),
                                          "%d %s",
                                          pjsua_var.nat_type,
                                          type_name);
        }

        a = pjmedia_sdp_attr_create(pool, "X-nat", &value);

        pjmedia_sdp_attr_add(&sdp->attr_count, sdp->attr, a);

    }


    /* Add bandwidth info in session level using bandwidth modifier "AS". */
    if (tot_bandw_tias) {
        unsigned bandw;
        const pj_str_t STR_BANDW_MODIFIER_AS = { "AS", 2 };
        pjmedia_sdp_bandw *b;

        /* AS bandwidth = RTP bitrate + RTCP bitrate.
         * RTP bitrate  = payload bitrate (total TIAS) + overheads (~16kbps).
         * RTCP bitrate = est. 5% of RTP bitrate.
         * Note that AS bandwidth is in kbps.
         */
        bandw = tot_bandw_tias + 16000;
        bandw += bandw * 5 / 100;
        b = PJ_POOL_ALLOC_T(pool, pjmedia_sdp_bandw);
        b->modifier = STR_BANDW_MODIFIER_AS;
        b->value = bandw / 1000;
        sdp->bandw[sdp->bandw_count++] = b;
    }

#if defined(PJMEDIA_HAS_RTCP_XR) && (PJMEDIA_HAS_RTCP_XR != 0)
    if (acc->cfg.enable_rtcp_xr) {
        pjmedia_sdp_attr *a;
        const char *STR_RTCP_XR = "rtcp-xr";
        /* Currently we don't support pkt-loss-rle pkt-dup-rle pkt-rcpt-times
         */
        const pj_str_t value = pj_str("rcvr-rtt stat-summary voip-metrics");

        a = pjmedia_sdp_attr_create(pool, STR_RTCP_XR, &value);
        pjmedia_sdp_attr_add(&sdp->attr_count, sdp->attr, a);
    }
#endif

#if DISABLED_FOR_TICKET_1185 && defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0)
    /* Check if SRTP is in optional mode and configured to use duplicated
     * media, i.e: secured and unsecured version, in the SDP offer.
     */
    if (!rem_sdp &&
        pjsua_var.acc[call->acc_id].cfg.use_srtp == PJMEDIA_SRTP_OPTIONAL &&
        pjsua_var.acc[call->acc_id].cfg.srtp_optional_dup_offer)
    {
        unsigned i;

        for (i = 0; i < sdp->media_count; ++i) {
            pjmedia_sdp_media *m = sdp->media[i];

            /* Check if this media is unsecured but has SDP "crypto"
             * attribute.
             */
            if (pj_stricmp2(&m->desc.transport, "RTP/AVP") == 0 &&
                pjmedia_sdp_media_find_attr2(m, "crypto", NULL) != NULL)
            {
                if (i == (unsigned)call->audio_idx &&
                    sdp_neg_state == PJMEDIA_SDP_NEG_STATE_DONE)
                {
                    /* This is a session update, and peer has chosen the
                     * unsecured version, so let's make this unsecured too.
                     */
                    pjmedia_sdp_media_remove_all_attr(m, "crypto");
                } else {
                    /* This is new offer, duplicate media so we'll have
                     * secured (with "RTP/SAVP" transport) and and unsecured
                     * versions.
                     */
                    pjmedia_sdp_media *new_m;

                    /* Duplicate this media and apply secured transport */
                    new_m = pjmedia_sdp_media_clone(pool, m);
                    pj_strdup2(pool, &new_m->desc.transport, "RTP/SAVP");

                    /* Remove the "crypto" attribute in the unsecured media */
                    pjmedia_sdp_media_remove_all_attr(m, "crypto");

                    /* Insert the new media before the unsecured media */
                    if (sdp->media_count < PJMEDIA_MAX_SDP_MEDIA) {
                        pj_array_insert(sdp->media, sizeof(new_m),
                                        sdp->media_count, i, &new_m);
                        ++sdp->media_count;
                        ++i;
                    }
                }
            }
        }
    }
#endif

    call->rem_offerer = (rem_sdp != NULL);

    /* Notify application */
    if (!call->hanging_up && pjsua_var.ua_cfg.cb.on_call_sdp_created) {
        (*pjsua_var.ua_cfg.cb.on_call_sdp_created)(call_id, sdp,
                                                   pool, rem_sdp);
    }

    if (sdp->media_count > PJSUA_MAX_CALL_MEDIA) {
        PJ_LOG(1, (THIS_FILE,
                   "Error in provided SDP: media count %u exceeds "
                   "PJSUA_MAX_CALL_MEDIA=%u",
                   sdp->media_count, PJSUA_MAX_CALL_MEDIA));
        status = PJ_ETOOMANY;
        goto on_error;
    }

    /* Sync med_prov_cnt with the final SDP media count in case the callback
     * added or replaced media sections (e.g. custom SDP with more m= lines).
     * Mirrors the same invariant maintained for the rem_sdp (UAS) path above:
     * med_prov_cnt must never decrease. */
    if (call->med_prov_cnt < sdp->media_count)
        call->med_prov_cnt = sdp->media_count;

    *p_sdp = sdp;
    return PJ_SUCCESS;

on_error:
    if (sip_err_code && *sip_err_code == 0)
        *sip_err_code = PJSIP_SC_INTERNAL_SERVER_ERROR;

    return status;
}


static void stop_media_stream(pjsua_call *call, unsigned med_idx)
{
    pjsua_call_media *call_med;
    
    if (pjsua_call_media_is_changing(call)) {
        call_med = &call->media_prov[med_idx];
        if (med_idx >= call->med_prov_cnt)
            return;
    } else {
        call_med = &call->media[med_idx];
        if (med_idx >= call->med_cnt)
            return;
    }

    pj_log_push_indent();

    call_med->prev_type = call_med->type;
    if (call_med->type == PJMEDIA_TYPE_AUDIO) {
        pjsua_aud_stop_stream(call_med);
    }

#if PJMEDIA_HAS_VIDEO
    else if (call_med->type == PJMEDIA_TYPE_VIDEO) {
        pjsua_vid_stop_stream(call_med);
    }
#endif
    else if (call_med->type == PJMEDIA_TYPE_TEXT) {
        pjsua_txt_stop_stream(call_med);
    }

    PJ_LOG(4,(THIS_FILE, "Media stream call%02d:%d is destroyed",
                         call->index, med_idx));
    call_med->prev_state = call_med->state;
    call_med->state = PJSUA_CALL_MEDIA_NONE;

    /* Try to sync recent changes to provisional media */
    if (med_idx < call->med_prov_cnt && 
        call->media_prov[med_idx].tp == call_med->tp)
    {
        pjsua_call_media *prov_med = &call->media_prov[med_idx];

        /* Media state */
        prov_med->prev_state = call_med->prev_state;
        prov_med->state      = call_med->state;

        /* RTP seq/ts */
        prov_med->rtp_tx_seq_ts_set = call_med->rtp_tx_seq_ts_set;
        prov_med->rtp_tx_seq        = call_med->rtp_tx_seq;
        prov_med->rtp_tx_ts         = call_med->rtp_tx_ts;

        /* Saved media type and addresses */
        prov_med->prev_type = call_med->prev_type;
        prov_med->prev_local_addr = call_med->prev_local_addr;
        prov_med->prev_rem_addr   = call_med->prev_rem_addr;

        /* Stream */
        if (call_med->type == PJMEDIA_TYPE_AUDIO) {
            prov_med->strm.a.conf_slot = call_med->strm.a.conf_slot;
            prov_med->strm.a.stream    = call_med->strm.a.stream;
        }
#if PJMEDIA_HAS_VIDEO
        else if (call_med->type == PJMEDIA_TYPE_VIDEO) {
            prov_med->strm.v.cap_win_id = call_med->strm.v.cap_win_id;
            prov_med->strm.v.rdr_win_id = call_med->strm.v.rdr_win_id;
            prov_med->strm.v.stream     = call_med->strm.v.stream;
        }
#endif
        else if (call_med->type == PJMEDIA_TYPE_TEXT) {
            prov_med->strm.t.stream     = call_med->strm.t.stream;
        }
    }

    pj_log_pop_indent();
}

static void stop_media_session(pjsua_call_id call_id)
{
    pjsua_call *call = &pjsua_var.calls[call_id];
    unsigned mi;

    for (mi=0; mi<call->med_cnt; ++mi) {
        stop_media_stream(call, mi);
    }
}


/*
 * Print log of call states. Since call states may be too long for logger,
 * printing it is a bit tricky, it should be printed part by part as long 
 * as the logger can accept.
 */
static void log_call_dump(int call_id) 
{
    pj_pool_t *pool;
    unsigned call_dump_len;
    unsigned part_len;
    unsigned part_idx;
    unsigned log_decor;
    char *buf;
    enum { BUF_LEN = 10*1024 };
    pj_status_t status;

    pool = pjsua_pool_create("tmp", 1024, 1024);
    if (!pool) return;
    buf = pj_pool_alloc(pool, sizeof(char) * BUF_LEN);

    status = pjsua_call_dump(call_id, PJ_TRUE, buf, BUF_LEN, "  ");
    if (status != PJ_SUCCESS)
        goto on_return;

    call_dump_len = (unsigned)pj_ansi_strlen(buf);

    log_decor = pj_log_get_decor();
    pj_log_set_decor(log_decor & ~(PJ_LOG_HAS_NEWLINE | PJ_LOG_HAS_CR));
    PJ_LOG(3,(THIS_FILE, "\n"));
    pj_log_set_decor(0);

    part_idx = 0;
    part_len = PJ_LOG_MAX_SIZE-80;
    while (part_idx < call_dump_len) {
        char p_orig, *p;

        p = buf + part_idx;
        if (part_idx + part_len > call_dump_len)
            part_len = call_dump_len - part_idx;
        p_orig = p[part_len];
        p[part_len] = '\0';
        PJ_LOG(3,(THIS_FILE, "%s", p));
        p[part_len] = p_orig;
        part_idx += part_len;
    }
    pj_log_set_decor(log_decor);

on_return:
    pj_pool_release(pool);
}


pj_status_t pjsua_media_channel_deinit(pjsua_call_id call_id)
{
    pjsua_call *call = &pjsua_var.calls[call_id];
    pjsip_dialog *dlg;
    unsigned mi;

    for (mi=0; mi<call->med_cnt; ++mi) {
        pjsua_call_media *call_med = &call->media[mi];

        if (call_med->tp_st == PJSUA_MED_TP_CREATING) {
            /* We will do the deinitialization after media transport
             * creation is completed.
             */
            call->async_call.med_ch_deinit = PJ_TRUE;
            return PJ_SUCCESS;
        }
    }

    PJ_LOG(4,(THIS_FILE, "Call %d: deinitializing media..", call_id));
    pj_log_push_indent();

    /* Print call dump first */
    dlg = (call->inv? call->inv->dlg : call->async_call.dlg);
    if (dlg && pj_log_get_level() >= 3)
        log_call_dump(call_id);

    /* Stop all media */
    stop_media_session(call_id);

    /* Destroy media synchronizer */
    if (call->av_sync)
        pjmedia_av_sync_destroy(call->av_sync);
    call->av_sync = NULL;

    /* Stop trickle ICE timer */
    if (call->trickle_ice.trickling > PJSUA_OP_STATE_NULL) {
        call->trickle_ice.trickling = PJSUA_OP_STATE_NULL;
        pjsua_cancel_timer(&call->trickle_ice.timer);
    }
    call->trickle_ice.enabled = PJ_FALSE;
    call->trickle_ice.pending_info = PJ_FALSE;
    call->trickle_ice.remote_sup = PJ_FALSE;
    call->trickle_ice.retrans18x_count = 0;

    /* Clean up media transports */
    pjsua_media_prov_clean_up(call_id);
    call->med_prov_cnt = 0;
    for (mi=0; mi<call->med_cnt; ++mi) {
        pjsua_call_media *call_med = &call->media[mi];

        if (call_med->use_upnp) {
#if defined(PJNATH_HAS_UPNP) && (PJNATH_HAS_UPNP != 0)
            pj_upnp_del_port_mapping(&call_med->mapped_addr[0]);
            pj_upnp_del_port_mapping(&call_med->mapped_addr[1]);
#endif
        }

        if (call_med->tp_st > PJSUA_MED_TP_IDLE) {
            pjmedia_transport_info tpinfo;
            pjmedia_srtp_info *srtp_info;
            pjmedia_ice_transport_info *ice_info;

            pjmedia_transport_info_init(&tpinfo);
            pjmedia_transport_get_info(call_med->tp, &tpinfo);
            srtp_info = (pjmedia_srtp_info *)
                        pjmedia_transport_info_get_spc_info(
                            &tpinfo, PJMEDIA_TRANSPORT_TYPE_SRTP);
            ice_info = (pjmedia_ice_transport_info *)
                       pjmedia_transport_info_get_spc_info(
                           &tpinfo, PJMEDIA_TRANSPORT_TYPE_ICE);

            call_med->prev_srtp_use = (srtp_info && srtp_info->active)?
                                      PJ_TRUE: PJ_FALSE;
            if (call_med->prev_srtp_use)
                call_med->prev_srtp_info = *srtp_info;
            call_med->prev_ice_use = (ice_info && ice_info->active)?
                                     PJ_TRUE: PJ_FALSE;
            if (call_med->prev_ice_use)
                call_med->prev_ice_info = *ice_info;

            /* Try to sync recent changes to provisional media */
            if (mi < call->med_prov_cnt && 
                call->media_prov[mi].tp == call_med->tp)
            {
                pjsua_call_media *prov_med = &call->media_prov[mi];

                prov_med->prev_ice_use = call_med->prev_ice_use;
                prov_med->prev_ice_info = call_med->prev_ice_info;
                prov_med->prev_srtp_use = call_med->prev_srtp_use;
                prov_med->prev_srtp_info = call_med->prev_srtp_info;
            }

            pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_IDLE);
            pjmedia_transport_media_stop(call_med->tp);
        }

        if (call_med->tp) {
            pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_NULL);
            pjmedia_transport_close(call_med->tp);
            call_med->tp = call_med->tp_orig = NULL;
        }
        call_med->tp_orig = NULL;
        call_med->rem_srtp_use = PJMEDIA_SRTP_UNKNOWN;
    }

    pj_log_pop_indent();

    return PJ_SUCCESS;
}


/* Match codec fmtp. This will compare the values and the order. */
static pj_bool_t match_codec_fmtp(const pjmedia_codec_fmtp *fmtp1,
                                  const pjmedia_codec_fmtp *fmtp2)
{
    unsigned i;

    if (fmtp1->cnt != fmtp2->cnt)
        return PJ_FALSE;

    for (i = 0; i < fmtp1->cnt; ++i) {
        if (pj_stricmp(&fmtp1->param[i].name, &fmtp2->param[i].name))
            return PJ_FALSE;
        if (pj_stricmp(&fmtp1->param[i].val, &fmtp2->param[i].val))
            return PJ_FALSE;
    }

    return PJ_TRUE;
}

#if PJSUA_MEDIA_HAS_PJMEDIA || PJSUA_THIRD_PARTY_STREAM_HAS_GET_INFO

static pj_bool_t is_ice_running(pjmedia_transport *tp)
{
    pjmedia_transport_info tpinfo;
    pjmedia_ice_transport_info *ice_info;

    pjmedia_transport_info_init(&tpinfo);
    pjmedia_transport_get_info(tp, &tpinfo);
    ice_info = (pjmedia_ice_transport_info*)
               pjmedia_transport_info_get_spc_info(&tpinfo,
                                                   PJMEDIA_TRANSPORT_TYPE_ICE);
    return (ice_info && ice_info->sess_state == PJ_ICE_STRANS_STATE_RUNNING);
}


#if defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0)

static void check_srtp_roc(pjsua_call *call,
                           unsigned med_idx,
                           const pjsua_stream_info *new_si_,
                           const pjmedia_sdp_session *local_sdp,
                           const pjmedia_sdp_session *rem_sdp)
{
    pjsua_call_media *call_med = &call->media[med_idx];
    pjmedia_transport_info tpinfo;
    pjmedia_srtp_info *srtp_info;
    pjmedia_transport *srtp;
    pjmedia_ice_transport_info *ice_info;
    const pj_sockaddr *prev_local_addr = NULL;
    const pj_sockaddr *prev_rem_addr = NULL;
    pjmedia_stream_info aud_si;
#if defined(PJMEDIA_HAS_VIDEO) && (PJMEDIA_HAS_VIDEO != 0)
    pjmedia_vid_stream_info vid_si;
#endif
    pjmedia_txt_stream_info txt_si;
    pj_bool_t local_change = PJ_FALSE, rem_change = PJ_FALSE;
    pjmedia_srtp_setting setting;

    pjmedia_transport_info_init(&tpinfo);
    pjmedia_transport_get_info(call_med->tp, &tpinfo);
    srtp_info = (pjmedia_srtp_info *) pjmedia_transport_info_get_spc_info(
                    &tpinfo, PJMEDIA_TRANSPORT_TYPE_SRTP);
    srtp = pjmedia_transport_info_get_transport(&tpinfo,
                                                PJMEDIA_TRANSPORT_TYPE_SRTP);

    /* Just return if there is no SRTP transport in the transport stack. */
    if (!srtp_info || !srtp)
        return;

    ice_info = (pjmedia_ice_transport_info*)
               pjmedia_transport_info_get_spc_info(&tpinfo,
                   PJMEDIA_TRANSPORT_TYPE_ICE);

    /* RFC 3711 section 3.3.1: 
     * After a re-keying occurs (changing to a new master key), the rollover
     * counter always maintains its sequence of values, i.e., it MUST NOT be
     * reset to zero. 
     *
     * RFC 4568 section 7.1.4:
     * If the offerer includes an IP address and/or port that differs from
     * that used previously for a media stream (or FEC stream), the offerer
     * MUST include a new master key with the offer (and in so doing, it
     * will be creating a new crypto context where the ROC is set to zero).
     * Similarly, if the answerer includes an IP address and/or port that
     * differs from that used previously for a media stream (or FEC stream),
     * the answerer MUST include a new master key with the answer (and hence
     * create a new crypto context with the ROC set to zero).
     */
    if (call->opt.flag & PJSUA_CALL_REINIT_MEDIA) {
        if (!call_med->prev_srtp_use) return;
        
        /* The stream has been deinitialized by now, so we need to retrieve
         * the previous stream info from the stored data.
         */
        prev_local_addr = &call_med->prev_local_addr;
        prev_rem_addr = &call_med->prev_rem_addr;
    } else {
        call_med->prev_srtp_use = PJ_TRUE;
        call_med->prev_srtp_info = *srtp_info;
        call_med->prev_ice_use = (ice_info && ice_info->active)?
                                 PJ_TRUE: PJ_FALSE;
        if (call_med->prev_ice_use)
            call_med->prev_ice_info = *ice_info;

        if (call_med->type == PJMEDIA_TYPE_AUDIO) {
            /* Get current active audio stream info */
            if (call_med->strm.a.stream) {
                pjmedia_stream_get_info(call_med->strm.a.stream, &aud_si);
                prev_local_addr = &aud_si.local_addr;
                prev_rem_addr = &aud_si.rem_addr;
            }
        } 
#if defined(PJMEDIA_HAS_VIDEO) && (PJMEDIA_HAS_VIDEO != 0)
        else if (call_med->type == PJMEDIA_TYPE_VIDEO) {
            /* Get current active video stream info */
            if (call_med->strm.v.stream) {
                pjmedia_vid_stream_get_info(call_med->strm.v.stream, &vid_si);
                prev_local_addr = &vid_si.local_addr;
                prev_rem_addr = &vid_si.rem_addr;
            }
        }
#endif
        else if (call_med->type == PJMEDIA_TYPE_TEXT) {
            /* Get current active text stream info */
            if (call_med->strm.t.stream) {
                pjmedia_txt_stream_get_info(call_med->strm.t.stream, &txt_si);
                prev_local_addr = &txt_si.local_addr;
                prev_rem_addr = &txt_si.rem_addr;
            }
        }
    }
    
#if 0
    PJ_LOG(4, (THIS_FILE, "SRTP TX ROC %u %d",
                          call_med->prev_srtp_info.tx_roc.ssrc,
                          call_med->prev_srtp_info.tx_roc.roc));
    PJ_LOG(4, (THIS_FILE, "SRTP RX ROC %u %d",
                          call_med->prev_srtp_info.rx_roc.ssrc,
                          call_med->prev_srtp_info.rx_roc.roc));
#endif
    
    if (prev_local_addr) {
        const pj_sockaddr *new_local_addr, *new_rem_addr;

        if (call_med->type == PJMEDIA_TYPE_AUDIO) {
            new_local_addr = &new_si_->info.aud.local_addr;
            new_rem_addr = &new_si_->info.aud.rem_addr;
        } 
#if defined(PJMEDIA_HAS_VIDEO) && (PJMEDIA_HAS_VIDEO != 0)
        else if (call_med->type == PJMEDIA_TYPE_VIDEO) {
            new_local_addr = &new_si_->info.vid.local_addr;
            new_rem_addr = &new_si_->info.vid.rem_addr;
        }
#endif
        else if (call_med->type == PJMEDIA_TYPE_TEXT) {
            new_local_addr = &new_si_->info.txt.local_addr;
            new_rem_addr = &new_si_->info.txt.rem_addr;
        } else {
            /* Just return for other media type */
            return;
        }

        /* Local IP address changes */
        if (pj_sockaddr_cmp(prev_local_addr, new_local_addr))
            local_change = PJ_TRUE;
        /* Remote IP address changes */
        if (pj_sockaddr_cmp(prev_rem_addr, new_rem_addr))
            rem_change = PJ_TRUE;
    }

    /* There are some complications if we are using ICE, because default
     * IP address can change after negotiation. In this case, we'll consider
     * this as a change of IP address only if ICE is restarted as well
     * (i.e. if ufrag changes).
     */
    if (ice_info && call_med->prev_ice_use) {
        const pj_str_t STR_ICE_UFRAG = { "ice-ufrag", 9 };
        pjmedia_sdp_attr *attr;

        if (local_change) {
            pjmedia_sdp_media *local_m = local_sdp->media[med_idx];

            attr = pjmedia_sdp_attr_find(local_m->attr_count,
                                         local_m->attr, &STR_ICE_UFRAG,
                                         NULL);
            if (attr == NULL) {
                /* Find ice-ufrag attribute in session level */
                attr = pjmedia_sdp_attr_find(local_sdp->attr_count,
                                             local_sdp->attr, &STR_ICE_UFRAG,
                                             NULL);
            }
            if (attr && !pj_strcmp(&call_med->prev_ice_info.loc_ufrag,
                                   &attr->value))
            {
                PJ_LOG(4, (THIS_FILE, "ICE unchanged, SRTP TX ROC "
                                      "maintained"));
                local_change = PJ_FALSE;
            }
        }

        if (rem_change) {
            pjmedia_sdp_media *rem_m = rem_sdp->media[med_idx];

            attr = pjmedia_sdp_attr_find(rem_m->attr_count,
                                         rem_m->attr, &STR_ICE_UFRAG,
                                         NULL);
            if (attr == NULL) {
                /* Find ice-ufrag attribute in session level */
                attr = pjmedia_sdp_attr_find(rem_sdp->attr_count,
                                             rem_sdp->attr, &STR_ICE_UFRAG,
                                             NULL);
            }
            if (attr && !pj_strcmp(&call_med->prev_ice_info.rem_ufrag,
                                   &attr->value))
            {
                PJ_LOG(4, (THIS_FILE, "ICE unchanged, SRTP RX ROC "
                                      "maintained"));
                rem_change = PJ_FALSE;
            }
         }          
    }

    pjmedia_transport_srtp_get_setting(srtp, &setting);
    setting.tx_roc = call_med->prev_srtp_info.tx_roc;
    setting.rx_roc = call_med->prev_srtp_info.rx_roc;
    if (local_change) {
        PJ_LOG(4, (THIS_FILE, "Local address change detected, "
                              "resetting SRTP TX ROC"));
        setting.tx_roc.roc = 0;
        /* Depending on the interpretation of the RFC, remote
         * may or may not reset its ROC as well. So we anticipate both.
         */
        setting.prev_rx_roc = call_med->prev_srtp_info.rx_roc;
        setting.prev_rx_roc.roc = 0;
    }
    if (rem_change) {
        PJ_LOG(4, (THIS_FILE, "Remote address change detected, "
                              "resetting SRTP RX ROC"));
        setting.rx_roc.roc = 0;
        /* There is a possibility that remote's IP address in the SDP
         * changes, but its actual IP address actually doesn't change,
         * such as when using ICE. So the only thing we can do here
         * is to anticipate if remote doesn't reset the ROC.
         */
        setting.prev_rx_roc = call_med->prev_srtp_info.rx_roc;
#if PJSUA_RESET_SRTP_ROC_ON_REM_ADDRESS_CHANGE
        if (!local_change) {
            PJ_LOG(4, (THIS_FILE, "Remote address change detected, "
                                  "resetting SRTP TX ROC"));
            setting.tx_roc.roc = 0;
        }
#endif 
    }
    pjmedia_transport_srtp_modify_setting(srtp, &setting);
}
#endif

static pj_bool_t is_media_changed(const pjsua_call *call,
                                  unsigned med_idx,
                                  const pjsua_stream_info *new_si_)
{
    const pjsua_call_media *call_med = &call->media[med_idx];

    /* Check for newly added media */
    if (med_idx >= call->med_cnt)
        return PJ_TRUE;

    /* Compare media type */
    if (call_med->type != new_si_->type)
        return PJ_TRUE;

    /* Audio update checks */
    if (call_med->type == PJMEDIA_TYPE_AUDIO) {
        pjmedia_stream_info the_old_si;
        const pjmedia_stream_info *old_si = NULL;
        const pjmedia_stream_info *new_si = &new_si_->info.aud;
        const pjmedia_codec_info *old_ci = NULL;
        const pjmedia_codec_info *new_ci = &new_si->fmt;
        const pjmedia_codec_param *old_cp = NULL;
        const pjmedia_codec_param *new_cp = new_si->param;

        /* Compare media direction */
        if (call_med->dir != new_si->dir)
            return PJ_TRUE;

        /* Get current active stream info */
        if (call_med->strm.a.stream) {
            pjmedia_stream_get_info(call_med->strm.a.stream, &the_old_si);
            old_si = &the_old_si;
            old_ci = &old_si->fmt;
            old_cp = old_si->param;
        } else {
            /* The stream is inactive. */
            return (new_si->dir != PJMEDIA_DIR_NONE);
        }

        if (old_si->rtcp_mux != new_si->rtcp_mux)
            return PJ_TRUE;

        /* Compare remote RTP address. If ICE is running, change in default
         * address can happen after negotiation, this can be handled
         * internally by ICE and does not need to cause media restart.
         */
        if (!is_ice_running(call_med->tp) &&
            pj_sockaddr_cmp(&old_si->rem_addr, &new_si->rem_addr))
        {
            return PJ_TRUE;
        }

        /* Compare codec info */
        if (pj_stricmp(&old_ci->encoding_name, &new_ci->encoding_name) ||
            old_ci->clock_rate != new_ci->clock_rate ||
            old_ci->channel_cnt != new_ci->channel_cnt ||
            old_si->rx_pt != new_si->rx_pt ||
            old_si->tx_pt != new_si->tx_pt ||
            old_si->rx_event_pt != new_si->rx_event_pt ||
            old_si->tx_event_pt != new_si->tx_event_pt)
        {
            return PJ_TRUE;
        }

        /* Compare codec param */
        PJ_ASSERT_RETURN(new_cp, PJ_FALSE);
        if (old_cp->setting.frm_per_pkt != new_cp->setting.frm_per_pkt ||
            old_cp->setting.vad != new_cp->setting.vad ||
            old_cp->setting.cng != new_cp->setting.cng ||
            old_cp->setting.plc != new_cp->setting.plc ||
            old_cp->setting.penh != new_cp->setting.penh ||
            !match_codec_fmtp(&old_cp->setting.dec_fmtp,
                              &new_cp->setting.dec_fmtp) ||
            !match_codec_fmtp(&old_cp->setting.enc_fmtp,
                              &new_cp->setting.enc_fmtp))
        {
            return PJ_TRUE;
        }
    }

#if PJMEDIA_HAS_VIDEO
    else if (call_med->type == PJMEDIA_TYPE_VIDEO) {
        pjmedia_vid_stream_info the_old_si;
        const pjmedia_vid_stream_info *old_si = NULL;
        const pjmedia_vid_stream_info *new_si = &new_si_->info.vid;
        const pjmedia_vid_codec_info *old_ci = NULL;
        const pjmedia_vid_codec_info *new_ci = &new_si->codec_info;
        const pjmedia_vid_codec_param *old_cp = NULL;
        const pjmedia_vid_codec_param *new_cp = new_si->codec_param;

        /* Compare media direction */
        if (call_med->dir != new_si->dir)
            return PJ_TRUE;

        /* Get current active stream info */
        if (call_med->strm.v.stream) {
            pjmedia_vid_stream_get_info(call_med->strm.v.stream, &the_old_si);
            old_si = &the_old_si;
            old_ci = &old_si->codec_info;
            old_cp = old_si->codec_param;
        } else {
            /* The stream is inactive. */
            return (new_si->dir != PJMEDIA_DIR_NONE);
        }

        /* Compare remote RTP address. If ICE is running, change in default
         * address can happen after negotiation, this can be handled
         * internally by ICE and does not need to cause media restart.
         */
        if (old_si->rtcp_mux != new_si->rtcp_mux)
            return PJ_TRUE;
        if (!is_ice_running(call_med->tp) &&
            pj_sockaddr_cmp(&old_si->rem_addr, &new_si->rem_addr))
        {
            return PJ_TRUE;
        }

        /* Compare codec info */
        if (pj_stricmp(&old_ci->encoding_name, &new_ci->encoding_name) ||
            old_si->rx_pt != new_si->rx_pt ||
            old_si->tx_pt != new_si->tx_pt)
        {
            return PJ_TRUE;
        }

        /* Compare SDP fmtp for both directions */
        if (!match_codec_fmtp(&old_cp->dec_fmtp, &new_cp->dec_fmtp) ||
            !match_codec_fmtp(&old_cp->enc_fmtp, &new_cp->enc_fmtp))
        {
            return PJ_TRUE;
        }

        /* Compare format for encoding only */
        if (pj_memcmp(&old_cp->enc_fmt.det, &new_cp->enc_fmt.det,
                      sizeof(pjmedia_video_format_detail)))
        {
            /* For H264, resolution may be adjusted to remote's profile-level,
             * so check if it is different because of the adjusted format.
             */
            if (pj_strcmp2(&new_ci->encoding_name, "H264") == 0) {
                pjmedia_vid_codec_param param = {0};
                pj_status_t status;

                param.dir = PJMEDIA_DIR_ENCODING;
                pj_memcpy(&param.enc_fmt, &new_cp->enc_fmt,
                          sizeof(param.enc_fmt));
                pj_memcpy(&param.enc_fmtp,&new_cp->enc_fmtp,
                          sizeof(param.enc_fmtp));
                status = pjmedia_vid_codec_h264_apply_fmtp(&param);
                if (status != PJ_SUCCESS)
                    return PJ_TRUE;

                if (pj_memcmp(&old_cp->enc_fmt.det, &param.enc_fmt.det,
                              sizeof(pjmedia_video_format_detail)))
                {
                    return PJ_TRUE;
                }
            } else {
                return PJ_TRUE;
            }
        }
    }

#endif

    else {
        /* Just return PJ_TRUE for other media type */
        return PJ_TRUE;
    }

    return PJ_FALSE;
}

#else /* PJSUA_MEDIA_HAS_PJMEDIA || PJSUA_THIRD_PARTY_STREAM_HAS_GET_INFO */

static pj_bool_t is_media_changed(const pjsua_call *call,
                                  unsigned med_idx,
                                  const pjsua_stream_info *new_si_)
{
    PJ_UNUSED_ARG(call);
    PJ_UNUSED_ARG(med_idx);
    PJ_UNUSED_ARG(new_si_);
    /* Always assume that media has been changed */
    return PJ_TRUE;
}

#endif /* PJSUA_MEDIA_HAS_PJMEDIA || PJSUA_THIRD_PARTY_STREAM_HAS_GET_INFO */


/* Apply media update. */
static pj_status_t apply_med_update(pjsua_call_media *call_med,
                                    const pjmedia_sdp_session *local_sdp,
                                    const pjmedia_sdp_session *remote_sdp,
                                    pj_bool_t *need_renego_sdp)
{
    const char *STR_SENDRECV = "sendrecv";
    const char *STR_SENDONLY = "sendonly";
    const char *STR_RECVONLY = "recvonly";
    const char *STR_INACTIVE = "inactive";

    pjsua_call *call = call_med->call;
    pjsua_acc *acc = &pjsua_var.acc[call->acc_id];
    pjsua_call_id call_id = call->index;
    unsigned mi = call_med->idx;
    pj_bool_t media_changed = PJ_FALSE;
    pj_pool_t *tmp_pool = call->inv->pool_prov;
    pj_status_t status = PJ_SUCCESS;

    /* Initialize the following variables to avoid warnings from compiler
     * and code analysis. Actually the function will always init the vars
     * when the media type is valid/recognized.
     */
    pjmedia_stream_info asi = {0};
#if defined(PJMEDIA_HAS_VIDEO) && (PJMEDIA_HAS_VIDEO != 0)
    pjmedia_vid_stream_info vsi = {0};
#endif
    pjmedia_txt_stream_info tsi;
    pjmedia_stream_info_common *si = NULL;
    pjsua_stream_info stream_info = {0};
    pj_str_t *enc_name = NULL;

    if (call_med->type == PJMEDIA_TYPE_AUDIO) {
        si = (pjmedia_stream_info_common *)&asi;
        status = pjmedia_stream_info_from_sdp(
                                    &asi, tmp_pool, pjsua_var.med_endpt,
                                    local_sdp, remote_sdp, mi);
        stream_info.info.aud = asi;
        enc_name = &asi.fmt.encoding_name;

#if defined(PJMEDIA_HAS_VIDEO) && (PJMEDIA_HAS_VIDEO != 0)
    } else if (call_med->type == PJMEDIA_TYPE_VIDEO) {
        si = (pjmedia_stream_info_common *)&vsi;
        status = pjmedia_vid_stream_info_from_sdp(
                                    &vsi, tmp_pool, pjsua_var.med_endpt,
                                    local_sdp, remote_sdp, mi);
        stream_info.info.vid = vsi;
        enc_name = &vsi.codec_info.encoding_name;
#endif
    } else if (call_med->type == PJMEDIA_TYPE_TEXT) {
        si = (pjmedia_stream_info_common *)&tsi;
        status = pjmedia_txt_stream_info_from_sdp(
                                    &tsi, tmp_pool, pjsua_var.med_endpt,
                                    local_sdp, remote_sdp, mi);
        stream_info.info.txt = tsi;
        enc_name = &tsi.fmt.encoding_name;
    }
    else {
        status = PJMEDIA_EUNSUPMEDIATYPE;
    }

    if (status != PJ_SUCCESS) {
        PJ_PERROR(1,(THIS_FILE, status,
                     "pjmedia_stream_info_from_sdp() failed "
                         "for call_id %d media %d",
                     call_id, mi));
        return status;
    }

#if defined(PJMEDIA_HAS_RTCP_XR) && (PJMEDIA_HAS_RTCP_XR != 0)
    /* Enable/disable RTCP XR based on account setting. */
    si->rtcp_xr_enabled = acc->cfg.enable_rtcp_xr;
#endif

    /* Check if remote wants RTP and RTCP multiplexing,
     * but we don't enable it.
     */
    if (si->rtcp_mux && !call_med->enable_rtcp_mux) {
        si->rtcp_mux = PJ_FALSE;
    }

    if (call_med->type == PJMEDIA_TYPE_AUDIO) {
        /* Codec parameter of stream info (si->param) can be NULL if
         * the stream is rejected or disabled.
         */
        /* Override ptime, if this option is specified. */
        if (pjsua_var.media_cfg.ptime != 0 && asi.param) {
            asi.param->setting.frm_per_pkt = (pj_uint8_t)
                (pjsua_var.media_cfg.ptime / asi.param->info.frm_ptime);
            if (asi.param->setting.frm_per_pkt == 0)
                asi.param->setting.frm_per_pkt = 1;
        }

        /* Disable VAD, if this option is specified. */
        if (pjsua_var.media_cfg.no_vad && asi.param) {
            asi.param->setting.vad = 0;
        }

        if (call->audio_idx==-1 && status==PJ_SUCCESS &&
            call_med->tp && local_sdp->media[mi]->desc.port != 0)
        {
            call->audio_idx = mi;
        }
    }

    if (!pjmedia_sdp_neg_was_answer_remote(call->inv->neg) &&
        si->dir != PJMEDIA_DIR_NONE)
    {
        pjmedia_dir dir = si->dir;

        if (call->opt.flag & PJSUA_CALL_SET_MEDIA_DIR) {
            call_med->def_dir = call->opt.media_dir[mi];
            PJ_LOG(4,(THIS_FILE, "Call %d: setting %s media "
                                 "direction #%d to %d.",
                                 call_id, pjmedia_type_name(call_med->type), mi,
                                 call_med->def_dir));
        }

        /* If the default direction specifies we do not wish
         * encoding/decoding, clear that direction.
         */
        if ((call_med->def_dir & PJMEDIA_DIR_ENCODING) == 0) {
            dir &= ~PJMEDIA_DIR_ENCODING;
        }
        if ((call_med->def_dir & PJMEDIA_DIR_DECODING) == 0) {
            dir &= ~PJMEDIA_DIR_DECODING;
        }

        if (dir != si->dir) {
            const char *str_attr = NULL;
            pjmedia_sdp_attr *attr;
            pjmedia_sdp_media *m;

            if (!*need_renego_sdp) {
                pjmedia_sdp_session *local_sdp_renego;
                local_sdp_renego =
                    pjmedia_sdp_session_clone(tmp_pool, local_sdp);
                local_sdp = local_sdp_renego;
                *need_renego_sdp = PJ_TRUE;
            }

            si->dir = dir;
            m = local_sdp->media[mi];

            /* Remove existing directions attributes */
            pjmedia_sdp_media_remove_all_attr(m, STR_SENDRECV);
            pjmedia_sdp_media_remove_all_attr(m, STR_SENDONLY);
            pjmedia_sdp_media_remove_all_attr(m, STR_RECVONLY);

            if (si->dir == PJMEDIA_DIR_ENCODING_DECODING) {
                str_attr = STR_SENDRECV;
            } else if (si->dir == PJMEDIA_DIR_ENCODING) {
                str_attr = STR_SENDONLY;
            } else if (si->dir == PJMEDIA_DIR_DECODING) {
                str_attr = STR_RECVONLY;
            } else {
                str_attr = STR_INACTIVE;
            }
            attr = pjmedia_sdp_attr_create(tmp_pool, str_attr, NULL);
            pjmedia_sdp_media_add_attr(m, attr);
        }
    }

    stream_info.type = call_med->type;

#if PJSUA_MEDIA_HAS_PJMEDIA || PJSUA_THIRD_PARTY_STREAM_HAS_GET_INFO
#if defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0)
    /* Check if we need to reset or maintain SRTP ROC */
    check_srtp_roc(call, mi, &stream_info, local_sdp, remote_sdp);
#endif
#endif

    /* Check if this media is changed */
    if (pjsua_var.media_cfg.no_smart_media_update ||
        is_media_changed(call, mi, &stream_info))
    {
        media_changed = PJ_TRUE;
        /* Stop the media */
        stop_media_stream(call, mi);
    } else {
        PJ_LOG(4,(THIS_FILE, "Call %d: stream #%d (%s) unchanged.",
                  call_id, mi, pjmedia_type_name(call_med->type)));
    }

    /* Check if no media is active */
    if (local_sdp->media[mi]->desc.port == 0) {

        /* Update call media state and direction */
        call_med->state = PJSUA_CALL_MEDIA_NONE;
        call_med->dir = PJMEDIA_DIR_NONE;

    } else if (call_med->tp) {
        pjmedia_transport_info tp_info;
        pjmedia_srtp_info *srtp_info;

        /* Call media direction */
        call_med->dir = si->dir;

        /* Call media state */
        if (call->local_hold ||
            ((call_med->dir & PJMEDIA_DIR_DECODING) == 0 &&
             (call_med->def_dir & PJMEDIA_DIR_DECODING) == 0))
        {
            /* Local hold: Either the user holds the call, or sets
             * the media direction that requests the remote party to
             * stop sending media (i.e. sendonly or inactive).
             */
            call_med->state = PJSUA_CALL_MEDIA_LOCAL_HOLD;
        } else if ((call_med->dir & PJMEDIA_DIR_ENCODING) == 0 &&
                   (call_med->def_dir & PJMEDIA_DIR_DECODING) != 0)
        {
            /* Remote hold: Remote doesn't want us to send media
             * (recvonly or inactive) and we don't set media dir that
             * locally holds the media.
             */
            call_med->state = PJSUA_CALL_MEDIA_REMOTE_HOLD;
        } else {
            call_med->state = PJSUA_CALL_MEDIA_ACTIVE;
        }

        if (call->inv->following_fork) {
            unsigned options = (call_med->enable_rtcp_mux?
                                PJMEDIA_TPMED_RTCP_MUX: 0);
            /* Normally media transport will automatically restart
             * itself (if needed, based on info from the SDP) in
             * pjmedia_transport_media_start(), however in "following
             * forked media" case (see #1644), we need to explicitly
             * restart it as it cannot detect fork scenario from
             * the SDP only.
             */
            status = pjmedia_transport_media_stop(call_med->tp);
            if (status != PJ_SUCCESS) {
                PJ_PERROR(1,(THIS_FILE, status,
                             "pjmedia_transport_media_stop() failed "
                             "for call_id %d media %d",
                             call_id, mi));
                return status;
            }
            status = pjmedia_transport_media_create(call_med->tp,
                                                    tmp_pool,
                                                    options, NULL, mi);
            if (status != PJ_SUCCESS) {
                PJ_PERROR(1,(THIS_FILE, status,
                             "pjmedia_transport_media_create() failed "
                             "for call_id %d media %d",
                             call_id, mi));
                return status;
            }
        }

        /* Start/restart media transport based on info in SDP */
        status = pjmedia_transport_media_start(call_med->tp,
                                               tmp_pool, local_sdp,
                                               remote_sdp, mi);
        if (status != PJ_SUCCESS) {
            PJ_PERROR(1,(THIS_FILE, status,
                         "pjmedia_transport_media_start() failed "
                         "for call_id %d media %d",
                         call_id, mi));
            return status;
        }

        pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_RUNNING);

        /* Get remote SRTP usage policy */
        pjmedia_transport_info_init(&tp_info);
        pjmedia_transport_get_info(call_med->tp, &tp_info);
        srtp_info = (pjmedia_srtp_info*)
                    pjmedia_transport_info_get_spc_info(
                            &tp_info, PJMEDIA_TRANSPORT_TYPE_SRTP);
        if (srtp_info) {
            call_med->rem_srtp_use = srtp_info->peer_use;
        }

        /* Update audio channel */
        if (media_changed) {
            if (call_med->type == PJMEDIA_TYPE_AUDIO) {
                status = pjsua_aud_channel_update(call_med,
                                                  call->inv->pool, &asi,
                                                  local_sdp, remote_sdp);
            } else if (call_med->type == PJMEDIA_TYPE_VIDEO) {
#if defined(PJMEDIA_HAS_VIDEO) && (PJMEDIA_HAS_VIDEO != 0)
                status = pjsua_vid_channel_update(call_med,
                                                  call->inv->pool, &vsi,
                                                  local_sdp, remote_sdp);
#endif
            } else if (call_med->type == PJMEDIA_TYPE_TEXT) {
                status = pjsua_txt_channel_update(call_med,
                                                  call->inv->pool, &tsi,
                                                  local_sdp, remote_sdp);
            }
            if (status != PJ_SUCCESS) {
                PJ_PERROR(1,(THIS_FILE, status,
                             "%s channel_update failed "
                             "for call_id %d media %d",
                             pjmedia_type_name(call_med->type),
                             call_id, mi));
                return status;
            }

            if (pjmedia_transport_info_get_spc_info(
                            &tp_info, PJMEDIA_TRANSPORT_TYPE_LOOP))
            {
                pjmedia_transport_loop_disable_rx(
                        call_med->tp, call_med->strm.a.stream,
                        !acc->cfg.enable_loopback);
            }
        }
    }

    /* Print info. */
    if (status == PJ_SUCCESS) {
        char info[80];
        int info_len = 0;
        int len;
        const char *dir;

        switch (si->dir) {
        case PJMEDIA_DIR_NONE:
            dir = "inactive";
            break;
        case PJMEDIA_DIR_ENCODING:
            dir = "sendonly";
            break;
        case PJMEDIA_DIR_DECODING:
            dir = "recvonly";
            break;
        case PJMEDIA_DIR_ENCODING_DECODING:
            dir = "sendrecv";
            break;
        default:
            dir = "unknown";
            break;
        }
        len = pj_ansi_snprintf( info+info_len, sizeof(info)-info_len,
                               ", stream #%d: %.*s (%s)", mi,
                               (int)enc_name->slen, enc_name->ptr,
                               dir);
        if (len > 0)
            info_len += len;
        PJ_LOG(4,(THIS_FILE,"%s updated%s",
                  pjmedia_type_name(call_med->type), info));
    }
    return status;
}


pj_status_t pjsua_media_channel_update(pjsua_call_id call_id,
                                       const pjmedia_sdp_session *local_sdp,
                                       const pjmedia_sdp_session *remote_sdp)
{
    pjsua_call *call = &pjsua_var.calls[call_id];
    pjsua_acc *acc = &pjsua_var.acc[call->acc_id];
    pj_pool_t *tmp_pool = call->inv->pool_prov;
    unsigned mi;
    pj_bool_t got_media = PJ_FALSE;
    pj_status_t status = PJ_SUCCESS;

    const pj_str_t STR_AUDIO = { "audio", 5 };
    const pj_str_t STR_VIDEO = { "video", 5 };
    const pj_str_t STR_TEXT  = { "text", 4 };
    pj_uint8_t maudidx[PJSUA_MAX_CALL_MEDIA] = {0};
    unsigned maudcnt = PJ_ARRAY_SIZE(maudidx);
    unsigned mtotaudcnt = PJ_ARRAY_SIZE(maudidx);
    pj_uint8_t mvididx[PJSUA_MAX_CALL_MEDIA] = {0};
    unsigned mvidcnt = PJ_ARRAY_SIZE(mvididx);
    unsigned mtotvidcnt = PJ_ARRAY_SIZE(mvididx);
    pj_uint8_t mtxtidx[PJSUA_MAX_CALL_MEDIA] = {0};
    unsigned mtxtcnt = PJ_ARRAY_SIZE(mtxtidx);
    unsigned mtottxtcnt = PJ_ARRAY_SIZE(mtxtidx);
    pj_bool_t need_renego_sdp = PJ_FALSE;

    if (pjsua_get_state() != PJSUA_STATE_RUNNING)
        return PJ_EBUSY;

    PJ_LOG(4,(THIS_FILE, "Call %d: updating media..", call_id));
    pj_log_push_indent();

    /* Destroy existing media session, if any. */
    //stop_media_session(call->index);

    /* Call media count must be at least equal to SDP media. Note that
     * it may not be equal when remote removed any SDP media line.
     */
    pj_assert(call->med_prov_cnt >= local_sdp->media_count);

    /* Reset audio_idx first */
    call->audio_idx = -1;

    /* Sort audio/video based on "quality" */
    sort_media(local_sdp, &STR_AUDIO, acc->cfg.use_srtp,
               maudidx, &maudcnt, &mtotaudcnt);
#if PJMEDIA_HAS_VIDEO
    sort_media(local_sdp, &STR_VIDEO, acc->cfg.use_srtp,
               mvididx, &mvidcnt, &mtotvidcnt);
#else
    PJ_UNUSED_ARG(STR_VIDEO);
    mvidcnt = mtotvidcnt = 0;
#endif
    sort_media(local_sdp, &STR_TEXT, acc->cfg.use_srtp,
               mtxtidx, &mtxtcnt, &mtottxtcnt);

    /* We need to re-nego SDP or modify our answer when:
     * - media count exceeds the configured limit,
     * - RTCP-FB is enabled (so a=rtcp-fb will only be printed for negotiated
     *   codecs)
     */
    if (!pjmedia_sdp_neg_was_answer_remote(call->inv->neg) &&
        ((maudcnt > call->opt.aud_cnt || mvidcnt > call->opt.vid_cnt ||
          mtxtcnt > call->opt.txt_cnt) ||
        (acc->cfg.rtcp_fb_cfg.cap_count)))
    {
        pjmedia_sdp_session *local_sdp_renego = NULL;

        local_sdp_renego = pjmedia_sdp_session_clone(tmp_pool, local_sdp);
        local_sdp = local_sdp_renego;
        need_renego_sdp = PJ_TRUE;

        /* Add RTCP-FB info into local SDP answer */
        if (acc->cfg.rtcp_fb_cfg.cap_count) {
            for (mi=0; mi < local_sdp_renego->media_count; ++mi) {
                status = pjmedia_rtcp_fb_encode_sdp(
                                        tmp_pool, pjsua_var.med_endpt,
                                        &acc->cfg.rtcp_fb_cfg,
                                        local_sdp_renego, mi, remote_sdp);
                if (status != PJ_SUCCESS) {
                    PJ_PERROR(3,(THIS_FILE, status,
                                 "Call %d media %d: Failed to encode RTCP-FB "
                                 "setting to SDP",
                                 call_id, mi));
                }
            }
        }

        /* Applying media count limitation. Note that in generating SDP
         * answer, no media count limitation applied as we didn't know yet
         * which media would pass the SDP negotiation.
         */
        if (maudcnt > call->opt.aud_cnt || mvidcnt > call->opt.vid_cnt ||
            mtxtcnt > call->opt.txt_cnt)
        {
            maudcnt = PJ_MIN(maudcnt, call->opt.aud_cnt);
            mvidcnt = PJ_MIN(mvidcnt, call->opt.vid_cnt);
            mtxtcnt = PJ_MIN(mtxtcnt, call->opt.txt_cnt);

            for (mi=0; mi < local_sdp_renego->media_count; ++mi) {
                pjmedia_sdp_media *m = local_sdp_renego->media[mi];

                if (m->desc.port == 0 ||
                    pj_memchr(maudidx, mi, maudcnt*sizeof(maudidx[0])) ||
                    pj_memchr(mvididx, mi, mvidcnt*sizeof(mvididx[0])) ||
                    pj_memchr(mtxtidx, mi, mtxtcnt*sizeof(mtxtidx[0])))
                {
                    continue;
                }
            
                /* Deactivate this excess media */
                pjmedia_sdp_media_deactivate(tmp_pool, m);
            }
        }
    }

    /* Update call media from provisional media */
    call->med_cnt = call->med_prov_cnt;
    pj_memcpy(call->media, call->media_prov,
              sizeof(call->media_prov[0]) * call->med_prov_cnt);

    /* Create/reset synchronizer */
    if ((call->opt.flag & PJSUA_CALL_NO_MEDIA_SYNC)==0 && 
        (maudcnt+mvidcnt) > 1)
    {
        if (call->av_sync) {
            pjmedia_av_sync_reset(call->av_sync);
        } else {
            pjmedia_av_sync_setting setting;
            char name[PJ_MAX_OBJ_NAME];

            pj_ansi_snprintf(name, sizeof(name), "avsync-call_%02d", call_id);
            pjmedia_av_sync_setting_default(&setting);
            setting.is_streaming = PJ_TRUE;
            setting.name = name;
            status = pjmedia_av_sync_create(tmp_pool, &setting, &call->av_sync);
            if (status != PJ_SUCCESS) {
                PJ_PERROR(3, (THIS_FILE, status,
                              "Call %d: Failed to create synchronizer", call_id));
            }
        }
    }

    /* Destroy existing synchronizer if synchronization is cancelled */
    else if ((call->opt.flag & PJSUA_CALL_NO_MEDIA_SYNC) && call->av_sync) {
        pjmedia_av_sync_destroy(call->av_sync);
        call->av_sync = NULL;
    }

    /* Process each media stream */
    for (mi=0; mi < call->med_cnt; ++mi) {
        pjsua_call_media *call_med = &call->media[mi];

        if (mi >= local_sdp->media_count ||
            mi >= remote_sdp->media_count)
        {
            /* This may happen when remote removed any SDP media lines in
             * its re-offer.
             */

            /* Stop stream */
            stop_media_stream(call, mi);

            /* Close the media transport */
            if (call_med->tp) {
                pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_NULL);
                pjmedia_transport_close(call_med->tp);
                call_med->tp = call_med->tp_orig = NULL;
            }
            continue;
#if 0
            /* Something is wrong */
            PJ_LOG(1,(THIS_FILE, "Error updating media for call %d: "
                      "invalid media index %d in SDP", call_id, mi));
            status = PJMEDIA_SDP_EINSDP;
            goto on_error;
#endif
        }

        /* Apply media update action */
        if (call_med->type==PJMEDIA_TYPE_AUDIO) {
            status = apply_med_update(call_med, local_sdp, remote_sdp, &need_renego_sdp);
            if (status != PJ_SUCCESS)
                goto on_check_med_status;

#if defined(PJMEDIA_HAS_VIDEO) && (PJMEDIA_HAS_VIDEO != 0)
        } else if (call_med->type==PJMEDIA_TYPE_VIDEO) {
            status = apply_med_update(call_med, local_sdp, remote_sdp, &need_renego_sdp);
            if (status != PJ_SUCCESS)
                goto on_check_med_status;
#endif
        } else if (call_med->type==PJMEDIA_TYPE_TEXT) {
            status = apply_med_update(call_med, local_sdp, remote_sdp,
                                      &need_renego_sdp);
            if (status != PJ_SUCCESS)
                goto on_check_med_status;
        } else {
            status = PJMEDIA_EUNSUPMEDIATYPE;
        }

        /* Close the transport of deactivated media, need this here as media
         * can be deactivated by the SDP negotiation and the max media count
         * (account) setting.
         */
        if (local_sdp->media[mi]->desc.port==0 && call_med->tp) {
            pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_NULL);
            pjmedia_transport_close(call_med->tp);
            call_med->tp = call_med->tp_orig = NULL;
        }

on_check_med_status:
        if (status != PJ_SUCCESS) {
            /* Stop stream */
            stop_media_stream(call, mi);

            /* Close the media transport */
            if (call_med->tp) {
                pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_NULL);
                pjmedia_transport_close(call_med->tp);
                call_med->tp = call_med->tp_orig = NULL;
            }

            /* Update media states */
            call_med->state = PJSUA_CALL_MEDIA_ERROR;
            call_med->dir = PJMEDIA_DIR_NONE;

            if (status != PJMEDIA_EUNSUPMEDIATYPE) {
                PJ_PERROR(1, (THIS_FILE, status, "Error updating media "
                              "call%02d:%d", call_id, mi));
            } else {
                PJ_PERROR(3, (THIS_FILE, status, "Skipped updating media "
                              "call%02d:%d (media type=%s)", call_id, mi, 
                              pjmedia_type_name(call_med->type)));
            }

        } else {
            /* Only set 'got_media' flag if this media is not disabled */
            if (local_sdp->media[mi]->desc.port != 0)
                got_media = PJ_TRUE;
        }
    }

    /* Sync provisional media to call media */
    call->med_prov_cnt = call->med_cnt;
    pj_memcpy(call->media_prov, call->media,
              sizeof(call->media[0]) * call->med_cnt);

    /* Perform SDP re-negotiation. */
    if (got_media && need_renego_sdp) {
        pjmedia_sdp_neg *neg = call->inv->neg;

        status = pjmedia_sdp_neg_set_remote_offer(tmp_pool, neg, remote_sdp);
        if (status != PJ_SUCCESS)
            goto on_error;

        status = pjmedia_sdp_neg_set_local_answer(tmp_pool, neg, local_sdp);
        if (status != PJ_SUCCESS)
            goto on_error;

        status = pjmedia_sdp_neg_negotiate(tmp_pool, neg, 0);
        if (status != PJ_SUCCESS)
            goto on_error;
    }

    pj_log_pop_indent();
    return (got_media? PJ_SUCCESS : PJMEDIA_SDPNEG_ENOMEDIA);

on_error:
    pj_log_pop_indent();
    return status;
}

/*****************************************************************************
 * Codecs.
 */

/*
 * Enum all supported codecs in the system.
 */
PJ_DEF(pj_status_t) pjsua_enum_codecs( pjsua_codec_info id[],
                                       unsigned *p_count )
{
    pjmedia_codec_mgr *codec_mgr;
    pjmedia_codec_info info[32];
    unsigned i, count, prio[32];
    pj_status_t status;

    codec_mgr = pjmedia_endpt_get_codec_mgr(pjsua_var.med_endpt);
    count = PJ_ARRAY_SIZE(info);
    status = pjmedia_codec_mgr_enum_codecs( codec_mgr, &count, info, prio);
    if (status != PJ_SUCCESS) {
        *p_count = 0;
        return status;
    }

    if (count > *p_count) count = *p_count;

    for (i=0; i<count; ++i) {
        pj_bzero(&id[i], sizeof(pjsua_codec_info));

        pjmedia_codec_info_to_id(&info[i], id[i].buf_, sizeof(id[i].buf_));
        id[i].codec_id = pj_str(id[i].buf_);
        id[i].priority = (pj_uint8_t) prio[i];
    }

    *p_count = count;

    return PJ_SUCCESS;
}


/*
 * Change codec priority.
 */
PJ_DEF(pj_status_t) pjsua_codec_set_priority( const pj_str_t *codec_id,
                                              pj_uint8_t priority )
{
    const pj_str_t all = { NULL, 0 };
    pjmedia_codec_mgr *codec_mgr;

    codec_mgr = pjmedia_endpt_get_codec_mgr(pjsua_var.med_endpt);

    if (codec_id->slen==1 && *codec_id->ptr=='*')
        codec_id = &all;

    return pjmedia_codec_mgr_set_codec_priority(codec_mgr, codec_id, 
                                                priority);
}


/*
 * Get codec parameters.
 */
PJ_DEF(pj_status_t) pjsua_codec_get_param( const pj_str_t *codec_id,
                                           pjmedia_codec_param *param )
{
    const pj_str_t all = { NULL, 0 };
    const pjmedia_codec_info *info;
    pjmedia_codec_mgr *codec_mgr;
    unsigned count = 1;
    pj_status_t status;

    codec_mgr = pjmedia_endpt_get_codec_mgr(pjsua_var.med_endpt);

    if (codec_id->slen==1 && *codec_id->ptr=='*')
        codec_id = &all;

    status = pjmedia_codec_mgr_find_codecs_by_id(codec_mgr, codec_id,
                                                 &count, &info, NULL);
    if (status != PJ_SUCCESS)
        return status;

    if (count != 1)
        return (count > 1? PJ_ETOOMANY : PJ_ENOTFOUND);

    status = pjmedia_codec_mgr_get_default_param( codec_mgr, info, param);
    return status;
}


/*
 * Set codec parameters.
 */
PJ_DEF(pj_status_t) pjsua_codec_set_param( const pj_str_t *codec_id,
                                           const pjmedia_codec_param *param)
{
    const pjmedia_codec_info *info[2];
    pjmedia_codec_mgr *codec_mgr;
    unsigned count = 2;
    pj_status_t status;

    codec_mgr = pjmedia_endpt_get_codec_mgr(pjsua_var.med_endpt);

    status = pjmedia_codec_mgr_find_codecs_by_id(codec_mgr, codec_id,
                                                 &count, info, NULL);
    if (status != PJ_SUCCESS)
        return status;

    /* Codec ID should be specific, except for G.722.1 */
    if (count > 1 && 
        pj_strnicmp2(codec_id, "G7221/16", 8) != 0 &&
        pj_strnicmp2(codec_id, "G7221/32", 8) != 0)
    {
        pj_assert(!"Codec ID is not specific");
        return PJ_ETOOMANY;
    }

    status = pjmedia_codec_mgr_set_default_param(codec_mgr, info[0], param);
    return status;
}


pj_status_t pjsua_media_apply_xml_control(pjsua_call_id call_id,
                                          const pj_str_t *xml_st)
{
#if PJMEDIA_HAS_VIDEO
    pjsua_call *call = &pjsua_var.calls[call_id];
    const pj_str_t PICT_FAST_UPDATE = {"picture_fast_update", 19};

    if (pj_strstr(xml_st, &PICT_FAST_UPDATE)) {
        unsigned i;

        PJ_LOG(4,(THIS_FILE, "Received keyframe request via SIP INFO"));

        for (i = 0; i < call->med_cnt; ++i) {
            pjsua_call_media *cm = &call->media[i];
            if (cm->type != PJMEDIA_TYPE_VIDEO || !cm->strm.v.stream)
                continue;

            pjmedia_vid_stream_send_keyframe(cm->strm.v.stream);
        }

        return PJ_SUCCESS;
    }
#endif

    /* Just to avoid compiler warning of unused var */
    PJ_UNUSED_ARG(call_id);
    PJ_UNUSED_ARG(xml_st);

    return PJ_ENOTSUP;
}

