#Android SipDemo 详解与实现
##首先SIP(Session Initiation Protocol,会话初始协议)是由IETF(Internet Engineering Task Force,因特网工程任务组)制定的多媒体通信协议。
它是一个基于文本的应用层控制协议,用于创建、修改和释放一个或多个参与者的会话。SIP 是一种源于互联网的IP 语音会话控制协议,具有灵活、易于实现、便于扩展等特点。
##android sip协议通话代码实现
简介
android里面的VOIP网络通话基于sip(Session initiation protocol)协议;android已经集成了sip协议栈,并提供了相应的API给应用开放使用,开发者不需要了解具体的协议内容
基于sip的网络通话基本过程
建立SIP服务器,关于如何建立SIP服务器,请参考这篇文章
需要所有参与通话的客户端注册用户到SIP服务器
一个客户端发起SIP通话到另一个客户端,这个消息首先发到SIP服务器,sip服务器收到消息后转发到目的客户端
目的客户端接收电话
##会话发起协议 Android提供了一个支持会话发起协议(SIP)的API,这可以让你添加基于SIP的网络电话功能到你的应用程序。Android包括一个完整的SIP协议栈和集成的呼叫管理服务,让应用轻松无需管理会话和传输层的沟通就可设置传出和传入的语音通话,或直接音频记录或播放。
以下类型的应用程序可能使用SIP API: 视频会议。 即时消息。
##Android客户端SipDemo代码实现
一AndroidManifest权限配置,代码如下
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".WalkieTalkieActivity"
android:configChanges="orientation|keyboardHidden">
</activity>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver android:name=".IncomingCallReceiver" android:label="Call Receiver"/>
</application>
<uses-permission android:name="android.permission.USE_SIP" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-sdk android:minSdkVersion="9" />
<uses-feature android:name="android.hardware.sip.voip" android:required="true" />
<uses-feature android:name="android.hardware.wifi" android:required="true" />
<uses-feature android:name="android.hardware.microphone" android:required="true" />
二、WalkieTalkieActivity类,实现SipManager的调用,Sip会话的发起,Sip会话的接收等,代码如下: /* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */
package com.example.mysipdemo;
import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.PendingIntent;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.sip.SipAudioCall;
import android.net.sip.SipException;
import android.net.sip.SipManager;
import android.net.sip.SipProfile;
import android.net.sip.SipRegistrationListener;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ToggleButton;
import java.text.ParseException;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
/**
-
Handles all calling, receiving calls, and UI interaction in the WalkieTalkie app.
*/
public class WalkieTalkieActivity extends Activity implements View.OnTouchListener {public String sipAddress = null;
public SipManager manager = null;
public SipProfile me = null;
public SipAudioCall call = null;
public IncomingCallReceiver callReceiver;private static final int CALL_ADDRESS = 1;
private static final int SET_AUTH_INFO = 2;
private static final int UPDATE_SETTINGS_DIALOG = 3;
private static final int HANG_UP = 4;//账户相关
private String userName = null;
private String userPassword = null;
private String server = null;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.walkietalkie);ToggleButton pushToTalkButton = (ToggleButton) findViewById(R.id.pushToTalk); Button button = (Button) findViewById(R.id.ben); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { showDialog(CALL_ADDRESS); } }); Button buttonStop = (Button)findViewById(R.id.stop); buttonStop.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { closeLocalProfile(); finish(); } }); pushToTalkButton.setOnTouchListener(this); // Set up the intent filter. This will be used to fire an // IncomingCallReceiver when someone calls the SIP address used by this // application. IntentFilter filter = new IntentFilter(); filter.addAction("android.SipDemo.INCOMING_CALL"); callReceiver = new IncomingCallReceiver(); this.registerReceiver(callReceiver, filter); // "Push to talk" can be a serious pain when the screen keeps turning off. // Let's prevent that. getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); Intent intent = getIntent(); userName = intent.getStringExtra("userName"); userPassword= intent.getStringExtra("userPassword"); server = intent.getStringExtra("server"); initializeManager();
}
@Override
public void onStart() {
super.onStart();
// When we get back from the preference setting Activity, assume
// settings have changed, and re-login with new auth info.
initializeManager();
}@Override
public void onDestroy() {
super.onDestroy();
if (call != null) {
call.close();
}closeLocalProfile(); if (callReceiver != null) { this.unregisterReceiver(callReceiver); }
}
public void initializeManager() {
if(manager == null) {
manager = SipManager.newInstance(this);
}//新加sip动态授权 if(ContextCompat.checkSelfPermission(this, Manifest.permission.USE_SIP) != PackageManager.PERMISSION_GRANTED){ ActivityCompat.requestPermissions(this,new String[]{ Manifest.permission.USE_SIP },2); }else { initializeLocalProfile(); }
}
/**
-
Logs you into your SIP provider, registering this device as the location to
-
send SIP calls to for your SIP address.
*/
public void initializeLocalProfile() {
if (manager == null) {
return;
}if (me != null) {
closeLocalProfile();
}
/* SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext());
String username = prefs.getString(“namePref”, “”);
String domain = prefs.getString(“domainPref”, “”);
String password = prefs.getString(“passPref”, “”);*/String username = userName; String domain = server; String password =userPassword; if (username.length() == 0 || domain.length() == 0 || password.length() == 0) { showDialog(UPDATE_SETTINGS_DIALOG); return; } try { SipProfile.Builder builder = new SipProfile.Builder(username, domain); builder.setPassword(password); me = builder.build(); Intent i = new Intent(); i.setAction("android.SipDemo.INCOMING_CALL"); PendingIntent pi = PendingIntent.getBroadcast(this, 0, i, Intent.FILL_IN_DATA); manager.open(me, pi, null); // This listener must be added AFTER manager.open is called, // Otherwise the methods aren't guaranteed to fire. manager.setRegistrationListener(me.getUriString(), new SipRegistrationListener() { public void onRegistering(String localProfileUri) { updateStatus("Registering with SIP Server..."); } public void onRegistrationDone(String localProfileUri, long expiryTime) { updateStatus("Ready"); } public void onRegistrationFailed(String localProfileUri, int errorCode, String errorMessage) { updateStatus("Registration failed. Please check settings."); } }); } catch (ParseException pe) { updateStatus("Connection Error."); } catch (SipException se) { updateStatus("Connection error."); }
}
/**
- Closes out your local profile, freeing associated objects into memory
- and unregistering your device from the server.
*/
public void closeLocalProfile() {
if (manager == null) {
return;
}
try {
if (me != null) {
manager.close(me.getUriString());
}
} catch (Exception ee) {
Log.d(“onDestroy”, “Failed to close local profile.”, ee);
}
}
/**
-
Make an outgoing call.
*/
public void initiateCall() {
if(ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE)
!= PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(this,new String[]{
Manifest.permission.CALL_PHONE
},1);
}else{
callPhone();
}
}
@Override
protected void onPause() {
closeLocalProfile();
super.onPause();
}private void callPhone(){
updateStatus(sipAddress);
try {
SipAudioCall.Listener listener = new SipAudioCall.Listener() {
// Much of the client’s interaction with the SIP Stack will
// happen via listeners. Even making an outgoing call, don’t
// forget to set up a listener to set things up once the call is established.
@Override
public void onCallEstablished(SipAudioCall call) {
Log.i(“panzhaoxuan”,"@1" + call);
call.startAudio();
Log.i(“panzhaoxuan”,"@2" + call);
call.setSpeakerMode(true);
call.toggleMute();
Log.i(“panzhaoxuan”,"@3" + call);
updateStatus(call);
}@Override public void onCallEnded(SipAudioCall call) { updateStatus("Ready."); } }; Log.i("panzhaoxuan","@me" + call); call = manager.makeAudioCall(me.getUriString(), sipAddress, listener, 30); Log.i("panzhaoxuan","@" + call); } catch (Exception e) { Log.i("InitiateCall", "Error when trying to close manager.", e); if (me != null) { try { manager.close(me.getUriString()); } catch (Exception ee) { Log.i("InitiateCall", "Error when trying to close manager.", ee); ee.printStackTrace(); } } if (call != null) { call.close(); } }
-
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch(requestCode){
case 1:
if(grantResults.length>0&&grantResults[0]==PackageManager.PERMISSION_GRANTED){
callPhone();
}else{
Toast.makeText(this,"You denied the permission",
Toast.LENGTH_SHORT).show();
}
break;
case 2:
if(grantResults.length>0&&grantResults[0]==PackageManager.PERMISSION_GRANTED){
initializeLocalProfile();
}else{
Toast.makeText(this,"You denied the permission",
Toast.LENGTH_SHORT).show();
}
break;
default:
break;
}
}
/**
* Updates the status box at the top of the UI with a messege of your choice.
* @param status The String to display in the status box.
*/
public void updateStatus(final String status) {
// Be a good citizen. Make sure UI changes fire on the UI thread.
this.runOnUiThread(new Runnable() {
public void run() {
Log.i("allenssip",status);
TextView labelView = (TextView) findViewById(R.id.sipLabel);
labelView.setText(status);
}
});
}
/**
* Updates the status box with the SIP address of the current call.
* @param call The current, active call.
*/
public void updateStatus(SipAudioCall call) {
String useName = call.getPeerProfile().getDisplayName();
if(useName == null) {
useName = call.getPeerProfile().getUserName();
}
updateStatus(useName + "@" + call.getPeerProfile().getSipDomain());
Log.i("panzhaoxuan",useName + "@" + call.getPeerProfile().getSipDomain());
}
/**
* Updates whether or not the user's voice is muted, depending on whether the button is pressed.
* @param v The View where the touch event is being fired.
* @param event The motion to act on.
* @return boolean Returns false to indicate that the parent view should handle the touch event
* as it normally would.
*/
public boolean onTouch(View v, MotionEvent event) {
if (call == null) {
return false;
} else if (event.getAction() == MotionEvent.ACTION_DOWN && call != null && call.isMuted()) {
call.toggleMute();
} else if (event.getAction() == MotionEvent.ACTION_UP && !call.isMuted()) {
call.toggleMute();
}
return false;
}
public boolean onCreateOptionsMenu(Menu menu) {
menu.add(0, CALL_ADDRESS, 0, "Call someone");
menu.add(0, SET_AUTH_INFO, 0, "Edit your SIP Info.");
menu.add(0, HANG_UP, 0, "End Current Call.");
return true;
}
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case CALL_ADDRESS:
showDialog(CALL_ADDRESS);
break;
case SET_AUTH_INFO:
updatePreferences();
break;
case HANG_UP:
if(call != null) {
try {
call.endCall();
} catch (SipException se) {
// Log.d(“WalkieTalkieActivity/onOptionsItemSelected”,
// “Error ending call.”, se);
}
call.close();
}
break;
}
return true;
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case CALL_ADDRESS:
LayoutInflater factory = LayoutInflater.from(this);
final View textBoxView = factory.inflate(R.layout.call_address_dialog, null);
return new AlertDialog.Builder(this)
.setTitle("Call Someone.")
.setView(textBoxView)
.setPositiveButton(
android.R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
EditText textField = (EditText)
(textBoxView.findViewById(R.id.calladdress_edit));
sipAddress = textField.getText().toString();
initiateCall();
}
})
.setNegativeButton(
android.R.string.cancel, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
// Noop.
}
})
.create();
case UPDATE_SETTINGS_DIALOG:
return new AlertDialog.Builder(this)
.setMessage("Please update your SIP Account Settings.")
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
updatePreferences();
}
})
.setNegativeButton(
android.R.string.cancel, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
// Noop.
}
})
.create();
}
return null;
}
public void updatePreferences() {
Intent settingsActivity = new Intent(getBaseContext(),
SipSettings.class);
startActivity(settingsActivity);
}
// @Override
// public void onClick(View v) {
// initiateCall();
// }
}
三Receiver的实现
/*
- Copyright © 2010 The Android Open Source Project
- Licensed under the Apache License, Version 2.0 (the “License”);
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an “AS IS” BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
*/
package com.example.mysipdemo;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.sip.SipAudioCall;
import android.net.sip.SipProfile;
/**
- Listens for incoming SIP calls, intercepts and hands them off to WalkieTalkieActivity.
/
public class IncomingCallReceiver extends BroadcastReceiver {
/
*-
Processes the incoming call, answers it, and hands it over to the
-
WalkieTalkieActivity.
-
@param context The context under which the receiver is running.
-
@param intent The intent being received.
*/
@Override
public void onReceive(Context context, Intent intent) {
SipAudioCall incomingCall = null;
try {SipAudioCall.Listener listener = new SipAudioCall.Listener() { @Override public void onRinging(SipAudioCall call, SipProfile caller) { try { call.answerCall(30); } catch (Exception e) { e.printStackTrace(); } } }; WalkieTalkieActivity wtActivity = (WalkieTalkieActivity) context; incomingCall = wtActivity.manager.takeAudioCall(intent, listener); incomingCall.answerCall(30); incomingCall.startAudio(); incomingCall.setSpeakerMode(true); if(incomingCall.isMuted()) { incomingCall.toggleMute(); } wtActivity.call = incomingCall; wtActivity.updateStatus(incomingCall);
} catch (Exception e) {
if (incomingCall != null) {
incomingCall.close();
}
}
}
-
}
四MainActivity类的实现,MainActivity主要实现Sip账号的注册。
package com.example.mysipdemo;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private EditText mEtUserName;
private EditText mEtUserPassword;
private EditText mEtServer;
private Button mBtnAddAccount;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
setListener();
}
private void initView(){
mEtUserName = findViewById(R.id.et_account);
mEtUserPassword = findViewById(R.id.et_password);
mEtServer = findViewById(R.id.et_server);
mBtnAddAccount = findViewById(R.id.btn_add);
}
private void setListener(){
mBtnAddAccount.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
addAccount();
}
});
}
private void addAccount(){
String userName = mEtUserName.getText().toString().trim();
String userPassword = mEtUserPassword.getText().toString().trim();
String server = mEtServer.getText().toString().trim();
if(!TextUtils.isEmpty(userName) && !TextUtils.isEmpty(userPassword)
&& !TextUtils.isEmpty(server)){
Intent intent = new Intent(this,WalkieTalkieActivity.class);
intent.putExtra("userName",userName);
intent.putExtra("userPassword",userPassword);
intent.putExtra("server",server);
startActivity(intent);
}else{
Toast.makeText(this,"输入不能为空",Toast.LENGTH_SHORT).show();
}
}
}
五,MainActivity 的布局文件
<LinearLayout
android:layout_width="300dp"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:orientation="vertical">
<LinearLayout
android:layout_marginTop="18dp"
android:layout_width="match_parent"
android:layout_height="40dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="用户名:"
android:textColor="@color/white"
android:gravity="center_vertical"
android:textSize="18dp"/>
<EditText
android:id="@+id/et_account"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_marginLeft="8dp"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:layout_weight="1"
android:singleLine="true"
android:textSize="18dp"
android:background="@drawable/shape"/>
</LinearLayout>
<LinearLayout
android:layout_marginTop="18dp"
android:layout_width="match_parent"
android:layout_height="40dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="密码:"
android:textColor="@color/white"
android:gravity="center_vertical"
android:textSize="18dp"/>
<EditText
android:id="@+id/et_password"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_marginLeft="8dp"
android:layout_weight="1"
android:singleLine="true"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:textSize="18dp"
android:background="@drawable/shape"
/>
</LinearLayout>
<LinearLayout
android:layout_marginTop="18dp"
android:layout_width="match_parent"
android:layout_height="40dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="域名:"
android:gravity="center_vertical"
android:textColor="@color/white"
android:textSize="18dp"/>
<EditText
android:id="@+id/et_server"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_marginLeft="8dp"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:layout_weight="1"
android:singleLine="true"
android:textSize="18dp"
android:background="@drawable/shape"/>
</LinearLayout>
<Button
android:gravity="center"
android:layout_width="260dp"
android:layout_height="40dp"
android:id="@+id/btn_add"
android:layout_marginTop="30dp"
android:textSize="18dp"
android:textColor="@color/white"
android:text="保存"
android:layout_gravity="center_horizontal"
android:background="@drawable/btn_blue"/>
</LinearLayout>
六WalkieTalkieActivity的布局文件
<?xml version="1.0" encoding="utf-8"?> <TextView
android:id="@+id/sipLabel"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_centerHorizontal="true"
android:textAppearance="?android:attr/textAppearanceLarge"
/>
<ToggleButton
android:layout_height="300dp"
android:layout_width="300dp"
android:text="@+string/talk"
android:id="@+id/pushToTalk"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:background="@drawable/btn_record"
android:textOff=""
android:textOn=""
android:layout_marginTop="-20dp" />
<Button
android:text="CALL"
android:id="@+id/ben"
android:layout_below="@+id/pushToTalk"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:text="STOP"
android:id="@+id/stop"
android:layout_below="@+id/ben"
android:layout_marginTop="20dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</RelativeLayout>
七,实现结果,如下:
电话实现
八遇见的坑
1.这个类名一定是WalkieTalkieActivity。
2. onPause中随时关闭closeLocalProfile();如下
@Override
protected void onPause() {
closeLocalProfile();
super.onPause();
}
3.动态检测Manifest.permission.USE_SIP权限和ManifestManifest.permission.CALL_PHONE权限,如下所示:
public void initiateCall() {
if(ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE)
!= PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(this,new String[]{
Manifest.permission.CALL_PHONE
},1);
}
else{
callPhone();
}
}
4.拨打电话的时候,一定是0+189手机电话+@10.254.0.2
,才能打出去。
5.麦克风的权限也要打开,否在接通了没有声音。
6.随时关闭 closeLocalProfile();,比如生命周期Onpause()中关闭,OnDestroy() 中关闭。
7、还要注册好Sip账号。
8.有的时候不支持Sip,要注意。
作者:old big
来源:oschina
链接:https://my.oschina.net/u/4380369/blog/4335711