PJSUA2 (PJSIP 2.8) Android アプリを構築していますが、いくつかの問題があります。つまり、着信通話でのみ、通話状態が「PJSIP_INV_STATE_CONNECTING」のままになり、32 秒後に通話が切断されます。
数日後、問題の原因を探しています。Google でいろいろ調べたところ、ほとんどの場合、この問題は 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();
たが、何も変わりませんでした。
1 つのメモ: IdUri を sip:username@127.0.0.1 として作成しました。これは、ホストがないと連絡先が表示されず、ユーザー部分の欠落が問題またはその一部の原因である可能性があると考えたためです。
以下は、通話中のアプリ <-> 私のアスタリスク サーバー通信のトレースです (コンテンツの長さが超過したためリンクされています)。
https://gist.github.com/ivano85/a212ddc9a808f3cd991234725c2bdb45
ServerIpはインターネットのパブリック IP で、MyIp[5.XXX.XXX.XXX] は私の電話のパブリック IP です。
ログからわかるように、私のアプリは 100 Trying を送信し、電話が鳴ると 180 Ringing を送信し、ユーザーが応答するとアプリは 200 OK を送信します。サーバーは ACK メッセージで応答します (PJSIP が ACK を受信するため、NAT の問題ではないと思います)。アスタリスクからも同じことがわかります。
この後、コールが PJSIP_INV_STATE_CONNECTING から PJSIP_INV_STATE_CONFIRMED に移行すると予想しますが、発生しないため、PJSIP は 200 OK を送信し続け、約 2 秒ごとに ACK を受信し、コールが 32 秒後にタイムアウトになり、PJSIP がコールを切断します ( BYEを送る)。
PJSIP は ACK メッセージを無視するだけで、間違った動作をしているだけだと思い始めています。ここで何が起こっているのかを理解するのを手伝ってください。よろしくお願いします!
詳細が必要だと思われる場合は、明らかにお知らせください。