1

我正在构建一个 PJSUA2 (PJSIP 2.8) Android 应用程序,但我遇到了一些问题:即仅在来电时,呼叫状态保持在“PJSIP_INV_STATE_CONNECTING”,32 秒后呼叫中断。

几天以来我一直在寻找问题的原因,我搜索了很多,我发现的只是:在大多数情况下,这个问题与 NAT 管理或与 NAT 相关的网络问题有关。简而言之:大多数情况下,被叫方接听电话后并没有收到ACK。最后,我能够记录我的应用程序和 SIP 服务器之间的所有 SIP 消息,并发现我的应用程序从服务器接收到 ACK,所以我认为这不是网络相关问题。

我用 OpenSSL 和 SRTP 支持编译了 PJSIP 2.8,但没有视频支持(至少目前我不需要它)。如果有任何区别,则该应用程序的目标版本为 28,最低 SDK 版本为 19。

我尝试了市场上的几个应用程序,它们在使用和不使用 SRTP 以及所有信令传输(UDP、TCP、TLS)的情况下都运行良好,WebRTC 也运行良好(使用 SipML5 测试),所以我会排除服务器配置错误。我的应用程序也是如此(目前我遇到一些问题的 SRTP 除外)。

我也尝试过使用 UDP 的 SIP 提供商(MessageNet),并且行为始终相同。我尝试使用紧凑的 SIP 消息,它的行为相同,有和没有 uri 参数,有和没有 STUN 和或 ICE,没有任何变化。移动网络和 WiFi 网络给出相同的结果。

我也尝试在 PJSIP 库中进行调试,但没有任何成功,然后我尝试按照代码进行操作,以了解我做错了什么,但在我看来并没有明显的问题。

以下是初始化 PJSIP 的代码(最新版本):

public class SipService extends Service {

    private Looper serviceLooper;
    private ServiceHandler serviceHandler;
    private final Messenger mMessenger = new Messenger(new IncomingHandler());
    private LocalBroadcastManager localBroadcast;
    private LifecycleBroadcastReceiver lifecycleBroadcastReceiver;
    private boolean lastCheckConnected;

    private Endpoint endpoint;
    private LogWriter logWriter;
    private EpConfig epConfig;
    private final List<ManagedSipAccount> accounts = new ArrayList<>();
    private final Map<String, Messenger> eventRegistrations = new HashMap<>();

    @TargetApi(Build.VERSION_CODES.N)
    @Override
    public void onCreate() {
        super.onCreate();

        String userAgent = "MyApp";

        try {
            PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
            String appLabel = (pInfo.applicationInfo.labelRes == 0 ? pInfo.applicationInfo.nonLocalizedLabel.toString() : getString(pInfo.applicationInfo.labelRes));
            userAgent = appLabel + "/" + pInfo.versionName;
        } catch (PackageManager.NameNotFoundException e) {
            Log.e("SipService", "Unable to get app version", e);
        }

        try {

            endpoint = new MyAppEndpoint();
            endpoint.libCreate();
            epConfig = new EpConfig();
            // Logging
            logWriter = new PJSIPToAndroidLogWriter();
            epConfig.getLogConfig().setWriter(logWriter);
            epConfig.getLogConfig().setLevel(5);
            // UA
            epConfig.getUaConfig().setMaxCalls(4);
            epConfig.getUaConfig().setUserAgent(userAgent);
            // STUN
            StringVector stunServer = new StringVector();
            stunServer.add("stun.pjsip.org");
            epConfig.getUaConfig().setStunServer(stunServer);
            // General Media
            epConfig.getMedConfig().setSndClockRate(16000);

            endpoint.libInit(epConfig);

            // UDP transport
            TransportConfig udpCfg = new TransportConfig();
            udpCfg.setQosType(pj_qos_type.PJ_QOS_TYPE_VOICE);
            endpoint.transportCreate(pjsip_transport_type_e.PJSIP_TRANSPORT_UDP, udpCfg);
            // TCP transport
            TransportConfig tcpCfg = new TransportConfig();
            //tcpCfg.setPort(5060);
            endpoint.transportCreate(pjsip_transport_type_e.PJSIP_TRANSPORT_TCP, tcpCfg);
            // TLS transport
            TransportConfig tlsCfg = new TransportConfig();
            endpoint.transportCreate(pjsip_transport_type_e.PJSIP_TRANSPORT_TLS, tlsCfg);

            endpoint.libStart();

        } catch (Exception e) {
            throw new RuntimeException("Unable to initialize and start PJSIP", e);
        }

        ConnectivityManager cm = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
        lastCheckConnected = activeNetwork != null && activeNetwork.isConnected();

        updateForegroundNotification();

        startForeground(MyAppConstants.N_FOREGROUND_NOTIFICATION_ID, buildForegroundNotification());

        localBroadcast = LocalBroadcastManager.getInstance(this);

        HandlerThread thread = new HandlerThread("ServiceStartArguments",
                Process.THREAD_PRIORITY_BACKGROUND);
        thread.start();

        // Get the HandlerThread's Looper and use it for our Handler
        serviceLooper = thread.getLooper();
        serviceHandler = new ServiceHandler(serviceLooper);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            // Register LifeCycleBroadcastReceiver to receive network change notification
            // It seems it's mandatory to do it programmatically since Android N (24)
            lifecycleBroadcastReceiver = new LifecycleBroadcastReceiver();
            IntentFilter intentFilter = new IntentFilter("android.net.conn.CONNECTIVITY_CHANGE");
            registerReceiver(lifecycleBroadcastReceiver, intentFilter);
        }

        // Initialization
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
        if (prefs != null) {
            try {
                CodecInfoVector codecs = endpoint.codecEnum();
                SharedPreferences.Editor editor = prefs.edit();
                for (int i = 0; i < codecs.size(); i++) {
                    CodecInfo codec = codecs.get(i);
                    int priority = prefs.getInt("codecs.audio{" + codec.getCodecId() + "}", 0);
                    try {
                        endpoint.codecSetPriority(codec.getCodecId(), (short) priority);
                        codec.setPriority((short) priority);
                    } catch (Exception e) {
                        Log.e("SipService", "Unexpected error setting codec priority for codec " + codec.getCodecId(), e);
                    }
                }
            } catch (Exception e) {
                Log.e("SipService", "Unexpected error loading codecs priorities", e);
            }
        }
    }

    @Override
    public void onDestroy() {
        for (Account acc : accounts) {
            acc.delete();
        }
        accounts.clear();
        try {
            endpoint.libDestroy();
        } catch (Exception e) {
            e.printStackTrace();
        }
        endpoint.delete();
        endpoint = null;
        epConfig = null;

        if (lifecycleBroadcastReceiver != null) {
            unregisterReceiver(lifecycleBroadcastReceiver);
        }
        super.onDestroy();
    }

    .......
}

以下是我的带有创建和注册代码的 Account 类:

public class ManagedSipAccount extends Account {

    public final String TAG;

    private final VoipAccount account;
    private final PhoneAccountHandle handle;
    private final SipService service;
    private final AccountStatus status;
    private final Map<Integer, VoipCall> calls = new HashMap<>();
    private final Map<String, VoipBuddy> buddies = new HashMap<>();
    private AccountConfig acfg;
    private List<SrtpCrypto> srtpCryptos = new ArrayList<>();
    private AuthCredInfo authCredInfo;

    public ManagedSipAccount(SipService service, VoipAccount account, PhoneAccountHandle handle) {
        super();

        TAG = "ManagedSipAccount/" + account.getId();

        this.service = service;
        this.account = account;
        this.handle = handle;
        this.status = new AccountStatus(account.getUserName() + "@" + account.getHost());

        acfg = new AccountConfig();
    }

    public void register(Map<String, String> contactParameters) throws Exception {

        StringBuilder contactBuilder = new StringBuilder();
        for (Map.Entry<String, String> entry : contactParameters.entrySet()) {
            contactBuilder.append(';');
            contactBuilder.append(URLEncoder.encode(entry.getKey(), "UTF-8"));
            contactBuilder.append("=\"");
            contactBuilder.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
            contactBuilder.append("\"");
        }

        StringBuilder logBuilder = new StringBuilder();
        logBuilder.append("Registering: ");
        logBuilder.append(account.getProtocol().name());
        /*logBuilder.append('(');
        logBuilder.append(service.getTransport(account.getProtocol()));
        logBuilder.append(')');*/
        if (account.isEncryptionSRTP()) {
            logBuilder.append(" SRTP");
        }
        if (account.isIce()) {
            logBuilder.append(" ICE");
        }
        Log.d(TAG, logBuilder.toString());

        String idUri = "sip:" + account.getUserName();
        if (!"*".equals(account.getRealm())) {
            idUri += "@" + account.getRealm();
        }
        else {
            idUri += "@127.0.0.1" /*+ account.getHost()*/;
        }

        acfg.setIdUri(idUri);
        acfg.getRegConfig().setRegistrarUri("sip:" + account.getHost() + ":" + account.getPort() + ";transport=" + account.getProtocol().name().toLowerCase());
        acfg.getRegConfig().setRetryIntervalSec(account.getRetryInterval());
        acfg.getRegConfig().setRegisterOnAdd(false);
        acfg.getSipConfig().setContactUriParams(contactBuilder.toString());
        // NAT management
        acfg.getNatConfig().setSipStunUse(pjsua_stun_use.PJSUA_STUN_USE_DEFAULT);
        if (account.isIce()) {
            acfg.getNatConfig().setIceEnabled(true);
            acfg.getNatConfig().setIceAlwaysUpdate(true);
            acfg.getNatConfig().setIceAggressiveNomination(true);
        }
        else {
            acfg.getNatConfig().setSdpNatRewriteUse(1);
        }
        acfg.getMediaConfig().getTransportConfig().setQosType(pj_qos_type.PJ_QOS_TYPE_VOICE);
        if (account.isEncryptionSRTP()) {
            acfg.getMediaConfig().setSrtpUse(pjmedia_srtp_use.PJMEDIA_SRTP_MANDATORY);
            acfg.getMediaConfig().setSrtpSecureSignaling(0);
            //acfg.getMediaConfig().getSrtpOpt().setKeyings(new IntVector(2));
            acfg.getMediaConfig().getSrtpOpt().getKeyings().clear();
            acfg.getMediaConfig().getSrtpOpt().getKeyings().add(pjmedia_srtp_keying_method.PJMEDIA_SRTP_KEYING_SDES.swigValue());
            acfg.getMediaConfig().getSrtpOpt().getKeyings().add(pjmedia_srtp_keying_method.PJMEDIA_SRTP_KEYING_DTLS_SRTP.swigValue());
            acfg.getMediaConfig().getSrtpOpt().getCryptos().clear();
            StringVector cryptos = Endpoint.instance().srtpCryptoEnum();
            for (int i = 0; i < cryptos.size(); i++) {
                SrtpCrypto crypto = new SrtpCrypto();
                crypto.setName(cryptos.get(i));
                crypto.setFlags(0);
                srtpCryptos.add(crypto);
                acfg.getMediaConfig().getSrtpOpt().getCryptos().add(crypto);
            }
        }
        else {
            acfg.getMediaConfig().setSrtpUse(pjmedia_srtp_use.PJMEDIA_SRTP_DISABLED);
            acfg.getMediaConfig().setSrtpSecureSignaling(0);
        }
        authCredInfo = new AuthCredInfo("digest",
                account.getRealm(),
                account.getAuthenticationId() != null && account.getAuthenticationId().trim().length() > 0 ? account.getAuthenticationId() : account.getUserName(),
                0,
                account.getPassword());
        acfg.getSipConfig().getAuthCreds().add( authCredInfo );

        acfg.getIpChangeConfig().setHangupCalls(false);
        acfg.getIpChangeConfig().setShutdownTp(true);

        create(acfg);

        ConnectivityManager cm = (ConnectivityManager)service.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
        boolean isConnected = activeNetwork != null && activeNetwork.isConnected();
        if (isConnected) {
            setRegistration(true);
        }
    }

    @Override
    public void onRegStarted(OnRegStartedParam prm) {
        super.onRegStarted(prm);

        Log.d(TAG, "Status: Registering...");

        status.setStatus(AccountStatus.Status.REGISTERING);
        service.updateStatus(this);
    }

    @Override
    public void onRegState(OnRegStateParam prm) {
        super.onRegState(prm);

        try {

            Log.d(TAG, "Registration state: " + prm.getCode().swigValue() + " " + prm.getReason());

            AccountInfo ai = getInfo();
            status.setStatus(ai.getRegIsActive() ? AccountStatus.Status.REGISTERED : AccountStatus.Status.UNREGISTERED);

            Log.d(TAG, "Status: " + status.getStatus().name() + " " + super.getInfo().getUri());

            service.updateStatus(this);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    .....
}

最后,我现在如何在扩展 PJSIP 的 Call 类的类中回答代码:

@Override
    public void answerCall() {
        Log.d(TAG, "Answering call...");
        CallOpParam prm = new CallOpParam(true);
        prm.setStatusCode(pjsip_status_code.PJSIP_SC_OK);
        prm.getOpt().setAudioCount(1);
        prm.getOpt().setVideoCount(0);
        try {
            this.answer(prm);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

我也尝试了new CallOpParam();仅使用状态代码而没有其他任何内容,但没有任何变化。

注意事项:我将 IdUri 创建为 sip:username@127.0.0.1 因为没有主机,结果联系人是,我认为丢失的用户部分可能是问题的原因或部分原因。

以下是调用期间应用程序 <-> 我的 Asterisk 服务器通信的跟踪(由于内容长度超过而链接)。

https://gist.github.com/ivano85/a212ddc9a808f3cd991234725c2bdb45

ServerIp是互联网公网 IP,而 MyIp[5.XXX.XXX.XXX] 是我手机的公网 IP

正如您从日志中看到的那样,我的应用程序发送 100 Trying,然后在电话响铃时发送 180 Ringing,然后用户接听并且应用程序发送 200 OK。服务器回复 ACK 消息(我会说这不是 NAT 问题,因为 PJSIP 接收到 ACK)。我从 Asterisk 看到了同样的情况。

在此之后,我希望呼叫从 PJSIP_INV_STATE_CONNECTING 转到 PJSIP_INV_STATE_CONFIRMED,但它没有发生,因此 PJSIP 继续发送 200 OK 并每隔大约 2 秒接收一次 ACK,直到呼叫在 32 秒后超时并且 PJSIP 断开呼叫(发送再见)。

我开始认为 PJSIP 只是忽略了 ACK 消息并且只是有错误的行为。请帮助我了解这里发生了什么。我会非常感激!

显然,如果您认为需要更多详细信息,请告诉我。

4

0 回答 0