前言
近期項目做即時聊天骤宣,以前已經(jīng)實現(xiàn)秦爆,使用環(huán)信實現(xiàn)的,當(dāng)時用時不到一個星期憔披,但是視頻通話質(zhì)量不是很好等限,老板不是很喜歡,要求換成騰訊芬膝,然后開始調(diào)研騰訊im 望门,騰訊im即時聊天的文檔中沒有音視頻通話,要自己實現(xiàn)锰霜,使用互動直播實現(xiàn)筹误,因此要實現(xiàn)這個功能就要花錢,公司有錢任性癣缅,但是自己也可以接觸下騰訊
這一番接觸下厨剪,騰訊給我的感覺并不友好,demo功能不是很全友存,很多自己實現(xiàn)祷膳,文檔寫的很不完善控乾,大數(shù)要靠自己猜樊拓,猜中了像中了500萬区匣,猜不中就跟謝謝惠顧的税产。帘撰。兄纺。吐槽無用還是要實現(xiàn)功能向族,現(xiàn)在我們來說一下實現(xiàn)音視頻通話侦讨。
騰訊專門封裝了一個callSdk 來實現(xiàn)音視頻通話吟策,Android Ios儒士,兩個客戶端都有,通過callSdk 實現(xiàn)音視頻會簡單很多檩坚,主要設(shè)置監(jiān)聽着撩,進(jìn)行業(yè)務(wù)處理
CallSdk 的api文檔:https://zhaoyang21cn.github.io/iLiveSDK_Help/callsdk/
音視頻通話的demo:https://github.com/zhaoyang21cn/CallSDK_Android_Demo
看到這個github上的demo圖片和運(yùn)行出來的圖片不符诅福,可能會傷心了,具體的邏輯要自己實現(xiàn)拖叙,demo運(yùn)行出來視頻建立后會出現(xiàn)黑屏氓润,是因為沒有開權(quán)限的問題,希望不要陷坑太久薯鳍。
互動直播的api文檔:https://zhaoyang21cn.github.io/iLiveSDK_Help/android_help/
因為用到AVRootView 控件咖气,所以參看這個文檔
先來看一下我們視頻通話實現(xiàn)的效果
視頻呼叫頁面
視頻應(yīng)答頁面
一.在gradle中加入
"com.tencent.ilivesdk:ilivesdk:1.8.6.3",
"com.tencent.callsdk:callsdk:1.0.31",
二.音視頻的主要邏輯代碼
package com.jyjt.ydyl.txim;
import android.Manifest;
import android.app.Activity;
import android.app.Application;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler;
import android.support.v4.app.ActivityCompat;
import android.text.TextUtils;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import com.google.gson.JsonObject;
import com.jyjt.ydyl.BaseActivity;
import com.jyjt.ydyl.BasePresenter;
import com.jyjt.ydyl.R;
import com.jyjt.ydyl.application.MyApplication;
import com.jyjt.ydyl.tools.AppUtils;
import com.jyjt.ydyl.tools.ConfigUtils;
import com.jyjt.ydyl.tools.LogUtils;
import com.jyjt.ydyl.tools.SwitchActivityManager;
import com.jyjt.ydyl.tools.ToastUtil;
import com.jyjt.ydyl.txim.presentation.event.VcallEvent;
import com.tencent.av.sdk.AVAudioCtrl;
import com.tencent.callsdk.ILVBCallMemberListener;
import com.tencent.callsdk.ILVCallConstants;
import com.tencent.callsdk.ILVCallListener;
import com.tencent.callsdk.ILVCallManager;
import com.tencent.callsdk.ILVCallNotification;
import com.tencent.callsdk.ILVCallOption;
import com.tencent.ilivesdk.ILiveCallBack;
import com.tencent.ilivesdk.ILiveConstants;
import com.tencent.ilivesdk.ILiveSDK;
import com.tencent.ilivesdk.core.ILiveLoginManager;
import com.tencent.ilivesdk.view.AVRootView;
import com.tencent.ilivesdk.view.AVVideoView;
import java.util.ArrayList;
import java.util.List;
import butterknife.BindView;
/**
* 蘇艷
* <p>
* 權(quán)限校驗
* 音視頻呼叫:呼叫的時候校驗麥克分或攝像頭權(quán)限
* 音視頻接聽:點擊接受時校驗麥克分或攝像頭權(quán)限
* 視頻通話:通話的時候時候校驗麥克分權(quán)限
*/
public class VoiceCallActivity extends BaseActivity implements ILVCallListener, ILVBCallMemberListener, ILiveLoginManager.TILVBStatusListener, AVRootView.onSubViewCreatedListener {
//大小頭像以及提示語========================================================
@BindView(R.id.rl_big_samll_head)
RelativeLayout rl_big_samll_head;
@BindView(R.id.tv_waring)
TextView tv_waring;
@BindView(R.id.iv_video_big_head)
ImageView iv_video_big_head;
@BindView(R.id.iv_video_small_head)
ImageView iv_video_small_head;
@BindView(R.id.tv_yuyin_timer)
public TextView tv_yuyin_timer;
//兩路視頻通話界面================================================================
@BindView(R.id.rl_video_conversation)
RelativeLayout rl_video_conversation;
@BindView(R.id.iv_switch_camera)
ImageView iv_switch_camera;
@BindView(R.id.av_root_view)
AVRootView av_root_view;
//視屏呼叫界面 1================================================================
@BindView(R.id.ll_bootom_call)
LinearLayout ll_bootom_call;
//視頻邀請界面 2================================================================
@BindView(R.id.ll_bottom_invitation)
LinearLayout ll_bottom_invitation;
@BindView(R.id.ll_video_yes)
LinearLayout ll_video_yes;
@BindView(R.id.ll_video_no)
LinearLayout ll_video_no;
//視頻通話界面 3==========================================================
@BindView(R.id.ll_bootom_conversation)
LinearLayout ll_bootom_conversation;
@BindView(R.id.ll_hangup)
LinearLayout ll_hangup;
@BindView(R.id.ll_no_voice)
LinearLayout ll_no_voice;
@BindView(R.id.ll_big_voice)
LinearLayout ll_big_voice;
@BindView(R.id.iv_no_voice)
ImageView iv_no_voice;
@BindView(R.id.iv_big_voice)
ImageView iv_big_voice;
//基本數(shù)據(jù)================================================================
//1.自己的id
private String mHostId = "";
//2.對方的id
private String mUserId = "";
//3.呼叫的類型 語音電話: CALL_TYPE_AUDIO 視頻電話 : CALL_TYPE_VIDEO
private int mCallType;
//4.頁面類型 1:視頻呼叫 2:視頻邀請 3:視頻通話
private int mType;
//5.通話的id
private int mCallId;
//6.對方頭像的url
private String mHeadUrl = "";
//7.對方的名字
private String mUserName = "";
//8.雙人視頻通話配置
private ILVCallOption mOption;
//9.麥克風(fēng)切換設(shè)置
private boolean mMicEnalbe = true;
//10.揚(yáng)聲器切換設(shè)置
private boolean mSpeaker = true;
//11.默認(rèn)設(shè)置前置攝像頭
private int mCurCameraId = ILiveConstants.FRONT_CAMERA;
//12.計時器
private Handler mHandler = new Handler();
//13.當(dāng)前毫秒數(shù)
private long currentSecond = 0;
//=======================================================================
@Override
protected BasePresenter loadPresenter() {
return null;
}
@Override
protected void initData() {
}
@Override
protected void initView() {
//1.獲取頁面類型
mType = getIntent().getIntExtra("Type", 0);
//2.獲取type類型
mCallType = getIntent().getIntExtra("CallType", ILVCallConstants.CALL_TYPE_VIDEO);
//3.獲取自己的id
mHostId = getIntent().getStringExtra("HostId");
//4.獲取對方的id
mUserId = getIntent().getStringExtra("UserId");
//5.獲取通話id
mCallId = getIntent().getIntExtra("CallId", 0);
//6.獲取對方頭像
mHeadUrl = getIntent().getStringExtra("HeadUrl");
//7.獲取對方的名字,來電的時候展示
mUserName = getIntent().getStringExtra("UserName");
//8.初始化視頻呼叫和接聽的CallOption
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("name", ConfigUtils.getMyName());
jsonObject.addProperty("head", ConfigUtils.getMyHead());
String s = jsonObject.toString();
mOption = new ILVCallOption(mHostId).callTips("").setMemberListener(this).setCallType(mCallType).customParam(s);
//9.根據(jù)type 類型展示布局
showLayout(mType);
}
@Override
protected void initListener() {
//1.設(shè)置視頻通話監(jiān)聽
ILVCallManager.getInstance().addCallListener(this);
//2.被迫下線監(jiān)聽
ILiveLoginManager.getInstance().setUserStatusListener(this);
//3.AVRootView設(shè)置渲染后監(jiān)聽
av_root_view.setSubCreatedListener(this);
//2.結(jié)束監(jiān)聽
ll_bootom_call.setOnClickListener(this);
//3.接受監(jiān)聽
ll_video_yes.setOnClickListener(this);
//4.拒絕監(jiān)聽
ll_video_no.setOnClickListener(this);
//5.掛斷監(jiān)聽
ll_hangup.setOnClickListener(this);
//6.靜音監(jiān)聽
ll_no_voice.setOnClickListener(this);
//7.免提監(jiān)聽
ll_big_voice.setOnClickListener(this);
//8.攝像頭切換
iv_switch_camera.setOnClickListener(this);
}
/**
* 根據(jù)mType 類型展示布局
*
* @param mType
*/
public void showLayout(int mType) {
switch (mType) {
case 1:
//1.判斷攝像頭權(quán)限
if (mCallType == ILVCallConstants.CALL_TYPE_VIDEO) {
openCamerePermisson();
} else {
openRecordPermisson();
}
//2.展示視頻呼叫布局
showDefault();
showBigSmallHead();
ll_bootom_call.setVisibility(View.VISIBLE);
//3.修改提示語,展示頭像
tv_waring.setText(((mCallType == ILVCallConstants.CALL_TYPE_VIDEO) ? "視頻電話撥通中..." : "聊天撥通中..."));
//4.呼叫
makecall(mUserId);
break;
case 2:
//1.展示視頻邀請布局
showDefault();
showBigSmallHead();
ll_bottom_invitation.setVisibility(View.VISIBLE);
//2.設(shè)置提示語
tv_waring.setText(mUserName + ((mCallType == ILVCallConstants.CALL_TYPE_VIDEO) ? "邀請你加入視頻通話" : "邀請你加入語音通話"));
break;
case 3:
//1.展示視頻通話布局
showDefault();
ll_bootom_conversation.setVisibility(View.VISIBLE);
//2.根據(jù)視頻通話和語音通話展示不同的布局
if (mCallType == ILVCallConstants.CALL_TYPE_VIDEO) {
//1.校驗麥克分權(quán)限(視頻權(quán)限在呼叫和接聽的時候校驗了)
openRecordPermisson();
//2.展示兩路視頻界面
rl_video_conversation.setVisibility(View.VISIBLE);
//3.初始化AV基礎(chǔ)
ILVCallManager.getInstance().initAvView(av_root_view);
} else {
//1.展示大小頭像界面
rl_big_samll_head.setVisibility(View.VISIBLE);
//2.設(shè)置提示語
tv_waring.setText("通話中");
}
//3.初始化計時器
timeRunable.run();
break;
default:
break;
}
}
//打開相機(jī)權(quán)限
public void openCamerePermisson() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN // api16之后需要權(quán)限
&& ActivityCompat.checkSelfPermission(VoiceCallActivity.this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {//權(quán)限不夠時,提示
ToastUtil.setToast("請打開攝像頭權(quán)限");
}
}
//打開麥克風(fēng)權(quán)限
public void openRecordPermisson() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN // api16之后需要權(quán)限
&& ActivityCompat.checkSelfPermission(VoiceCallActivity.this, Manifest.permission.RECORD_AUDIO)
!= PackageManager.PERMISSION_GRANTED) {//權(quán)限不夠時挖滤,提示
ToastUtil.setToast("請打開麥克風(fēng)權(quán)限權(quán)限");
}
}
//展示大小頭像
private void showBigSmallHead() {
rl_big_samll_head.setVisibility(View.VISIBLE);
AppUtils.loadCirclePic(this, R.mipmap.personal, mHeadUrl, iv_video_small_head);
AppUtils.loadBitmapCicleIcon(this, R.mipmap.personal_center, mHeadUrl, iv_video_big_head);
}
//計時器
private Runnable timeRunable = new Runnable() {
@Override
public void run() {
currentSecond = currentSecond + 1000;
tv_yuyin_timer.setText(getFormatHMS(currentSecond));
//遞歸調(diào)用本runable對象崩溪,實現(xiàn)每隔一秒一次執(zhí)行任務(wù)
mHandler.postDelayed(this, 1000);
}
};
// 根據(jù)毫秒返回時分秒
public static String getFormatHMS(long time) {
time = time / 1000;//總秒數(shù)
int s = (int) (time % 60);//秒
int m = (int) (time / 60);//分
int h = (int) (time / 3600);//秒
return String.format("%02d:%02d:%02d", h, m, s);
}
/**
* 默認(rèn)隱藏所有布局,根據(jù)需求展示
*/
public void showDefault() {
//1.大小頭像界面
rl_big_samll_head.setVisibility(View.GONE);
//2.兩路視頻界面
rl_video_conversation.setVisibility(View.GONE);
//3.呼叫底部
ll_bootom_call.setVisibility(View.GONE);
//4.邀請底部
ll_bottom_invitation.setVisibility(View.GONE);
//5.通話底部
ll_bootom_conversation.setVisibility(View.GONE);
}
/**
* type 為1時斩松,掉發(fā)起呼叫
*
* @param mUserId:對方id
*/
public void makecall(String mUserId) {
//1.發(fā)起呼叫生成id
mCallId = ILVCallManager.getInstance().makeCall(mUserId + "", mOption, new ILiveCallBack() {
@Override
public void onSuccess(Object data) {
}
@Override
public void onError(String module, int errCode, String errMsg) {
toast(errMsg);
finish();
}
});
//2.設(shè)置呼叫類型
VoiceService.isCallType = true;
//3.設(shè)置通話Id
VoiceService.isSendNOtifacation = false;
}
@Override
protected int getLayoutId() {
return R.layout.activity_voice_call;
}
@Override
protected void otherViewClick(View view) {
switch (view.getId()) {
case R.id.ll_bootom_call://取消
//1.結(jié)束掛斷電話
ILVCallManager.getInstance().endCall(mCallId);
break;
case R.id.ll_video_yes://接受
//1.判斷是否有網(wǎng)
if (!AppUtils.isAccessNetwork(mContext)) {
toast("請檢查您的網(wǎng)絡(luò)");
return;
}
//2.權(quán)限驗證
if (mCallType == ILVCallConstants.CALL_TYPE_VIDEO) {
openCamerePermisson();
} else {
openRecordPermisson();
}
//3.接受電話
ILVCallManager.getInstance().acceptCall(mCallId, mOption);
break;
case R.id.ll_video_no://拒絕
//1.若沒有啟動過app啟動app
showMainActivity();
//2.拒絕通知對方已拒絕
ILVCallManager.getInstance().rejectCall(mCallId);
break;
case R.id.ll_hangup://掛斷
//1.應(yīng)答界面需要校驗是否啟動過app
if (!TextUtils.isEmpty(mUserName)) {
showMainActivity();
}
//2.通話掛斷
ILVCallManager.getInstance().endCall(mCallId);
break;
case R.id.ll_no_voice: //靜音
changeMic();
break;
case R.id.ll_big_voice: //免提
changeSpeaker();
break;
case R.id.iv_switch_camera: //攝像頭切換
switchCamera();
break;
default:
break;
}
}
private void showMainActivity() {
List<Activity> activityList = MyApplication.getmApplication().getActivityList();
int size = activityList.size();
boolean isLanch = false;
for (int i = 0; i < size; i++) {
LogUtils.d("suyan", "=========棧里的acivity" + activityList.get(i).getLocalClassName());
if (activityList.get(i).getLocalClassName().equals("activity.MainActivity")) {
isLanch = true;
break;
}
}
if (!isLanch) {
LogUtils.d("suyan", "=========啟動");
SwitchActivityManager.startMainActivity(mContext);
}
}
//麥克風(fēng)切換
private void changeMic() {
if (mMicEnalbe) {
ILVCallManager.getInstance().enableMic(false);
} else {
ILVCallManager.getInstance().enableMic(true);
}
mMicEnalbe = !mMicEnalbe;
//關(guān)閉麥克風(fēng)
iv_no_voice.setImageResource(mMicEnalbe ? R.mipmap.jingyin_f : R.mipmap.jingyin_t);
}
// 揚(yáng)聲器切換
private void changeSpeaker() {
if (mSpeaker) {
ILiveSDK.getInstance().getAvAudioCtrl().setAudioOutputMode(AVAudioCtrl.OUTPUT_MODE_HEADSET);
} else {
ILiveSDK.getInstance().getAvAudioCtrl().setAudioOutputMode(AVAudioCtrl.OUTPUT_MODE_SPEAKER);
}
mSpeaker = !mSpeaker;
iv_big_voice.setImageResource(mSpeaker ? R.mipmap.hands_free_f : R.mipmap.hands_free_t);
}
//切換攝像頭
private void switchCamera() {
mCurCameraId = (ILiveConstants.FRONT_CAMERA == mCurCameraId) ? ILiveConstants.BACK_CAMERA : ILiveConstants.FRONT_CAMERA;
ILVCallManager.getInstance().switchCamera(mCurCameraId);
}
@Override
protected void onResume() {
ILVCallManager.getInstance().onResume();
super.onResume();
}
@Override
protected void onPause() {
ILVCallManager.getInstance().onPause();
super.onPause();
}
@Override
protected void onDestroy() {
ILVCallManager.getInstance().removeCallListener(this);
ILVCallManager.getInstance().onDestory();
tv_yuyin_timer = null;
timeRunable = null;
mHandler.removeMessages(0);
super.onDestroy();
}
@Override
public void showLoading() {
}
@Override
public void hideLoading() {
}
//攝像頭事件
@Override
public void onCameraEvent(String id, boolean bEnable) {
}
//麥克風(fēng)事件
@Override
public void onMicEvent(String id, boolean bEnable) {
}
//通話建立成功伶唯,視頻呼叫時回來的監(jiān)聽
@Override
public void onCallEstablish(int callId) {
//1.修改mType類型
mType = 3;
//2.建立通話界面
showLayout(mType);
//3. 交換兩路視頻
av_root_view.swapVideoView(0, 1);
}
//通話結(jié)束
@Override
public void onCallEnd(int callId, int endResult, String endInfo) {
finish();
}
//通話異常
@Override
public void onException(int iExceptionId, int errCode, String errMsg) {
toast(errMsg);
finish();
}
/**
* 監(jiān)聽Back鍵按下事件
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if ((keyCode == KeyEvent.KEYCODE_BACK)) {
if (mType == 1) { //取消通話
ILVCallManager.getInstance().endCall(mCallId);
} else if (mType == 2) {//拒絕通話
ILVCallManager.getInstance().rejectCall(mCallId);
} else if (mType == 3) { //結(jié)束通話
ILVCallManager.getInstance().endCall(mCallId);
}
}
return super.onKeyDown(keyCode, event);
}
@Override
public void onForceOffline(int error, String message) {
//1.被迫下線監(jiān)聽提示
toast("您的賬號在其他地方登陸");
//2.關(guān)閉界面
finish();
}
//渲染后監(jiān)聽設(shè)置拖動已經(jīng)切換兩路視頻
@Override
public void onSubViewCreated() {
LogUtils.d("suyan", "====視頻建立" + ILiveLoginManager.getInstance().getMyUserId() + "=" + av_root_view.getViewByIndex(0).getIdentifier() + "/" + av_root_view.getViewByIndex(1).getIdentifier());
//4.設(shè)置點擊小屏切換及可拖動
for (int i = 1; i < ILiveConstants.MAX_AV_VIDEO_NUM; i++) {
final int index = I;
AVVideoView minorView = av_root_view.getViewByIndex(i);
if (ILiveLoginManager.getInstance().getMyUserId().equals(minorView.getIdentifier())) {
minorView.setMirror(true); // 本地鏡像
}
minorView.setDragable(true); // 小屏可拖動
minorView.setGestureListener(new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
av_root_view.swapVideoView(0, index); // 與大屏交換
return false;
}
});
}
}
}
activity_voice_call 的布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#66000000"
tools:context="com.jyjt.ydyl.txim.VoiceCallActivity">
<!--大小頭像 ,呼叫提示-->
<RelativeLayout
android:id="@+id/rl_big_samll_head"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--大頭像-->
<ImageView
android:id="@+id/iv_video_big_head"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop" />
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/frame_blcak" />
<!--小頭像-->
<ImageView
android:id="@+id/iv_video_small_head"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_centerHorizontal="true"
android:layout_marginTop="125dp"
android:scaleType="centerCrop" />
<!--通話計時器-->
<TextView
android:id="@+id/tv_yuyin_timer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/iv_video_small_head"
android:layout_centerHorizontal="true"
android:layout_marginTop="10dp"
android:textColor="@color/white"
android:textSize="15sp" />
<TextView
android:id="@+id/tv_waring"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_yuyin_timer"
android:layout_centerHorizontal="true"
android:layout_marginTop="3dp"
android:text="通話中"
android:textColor="@color/white"
android:textSize="15sp" />
</RelativeLayout>
<!--兩路視頻通話界面-->
<RelativeLayout
android:id="@+id/rl_video_conversation"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"
android:visibility="visible">
<com.tencent.ilivesdk.view.AVRootView
android:id="@+id/av_root_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!--切換攝像頭-->
<ImageView
android:id="@+id/iv_switch_camera"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentRight="true"
android:layout_marginRight="20dp"
android:layout_marginTop="20dp"
android:src="@mipmap/ic_sw_camer" />
</RelativeLayout>
<!--底部惧盹,視屏呼叫界面,掛斷-->
<LinearLayout
android:id="@+id/ll_bootom_call"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="20dp"
android:orientation="vertical">
<ImageView
android:id="@+id/btn_hangup_call"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:src="@mipmap/huang_up" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="10dp"
android:gravity="center"
android:text="掛斷"
android:textColor="@color/white"
android:textSize="12sp" />
</LinearLayout>
<!--底部乳幸,視屏邀請界面-->
<LinearLayout
android:id="@+id/ll_bottom_invitation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="20dp"
android:orientation="horizontal">
<!--掛斷-->
<LinearLayout
android:id="@+id/ll_video_no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginLeft="22dp"
android:orientation="vertical"
android:visibility="visible">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:src="@mipmap/huang_up" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="10dp"
android:gravity="center"
android:text="掛斷"
android:textColor="@color/white"
android:textSize="12sp" />
</LinearLayout>
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<!--接聽-->
<LinearLayout
android:id="@+id/ll_video_yes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="22dp"
android:orientation="vertical">
<ImageView
android:id="@+id/btn_answer_call"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/jietong" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="10dp"
android:gravity="center"
android:text="接通"
android:textColor="@color/white"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
<!--底部,視頻通話界面,靜音钧椰,掛斷粹断,免提-->
<LinearLayout
android:id="@+id/ll_bootom_conversation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="20dp"
android:orientation="horizontal">
<!--靜音-->
<LinearLayout
android:id="@+id/ll_no_voice"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_no_voice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:src="@mipmap/jingyin_f" />
<TextView
android:id="@+id/tv_no_voice"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center"
android:text="@string/mute"
android:textColor="@color/white"
android:textSize="12sp" />
</LinearLayout>
<!--掛斷-->
<LinearLayout
android:id="@+id/ll_hangup"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_weight="1"
android:orientation="vertical"
android:visibility="visible">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:src="@mipmap/huang_up" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="10dp"
android:gravity="center"
android:text="掛斷"
android:textColor="@color/white"
android:textSize="12sp" />
</LinearLayout>
<!--免提-->
<LinearLayout
android:id="@+id/ll_big_voice"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_big_voice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:src="@mipmap/hands_free_f" />
<TextView
android:id="@+id/tv_big_voice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center"
android:text="@string/Hands_free"
android:textColor="@color/white"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
三.視頻呼叫
視頻呼叫啟動視頻呼叫界面
public void callVideo() {
if (TextUtils.isEmpty(user_id) || TextUtils.isEmpty(ConfigUtils.getUid())) {
toast("ID為空");
return;
}
startVoiceCallActivity(mContext, ConfigUtils.getUid(), user_id, 1, TXChatHead.getInstance().getUserHead(user_id) == null ? "" : TXChatHead.getInstance().getUserHead(user_id), ILVCallConstants.CALL_TYPE_VIDEO);
}
/**
* 音視頻界面
*
* @param HostId :自己的id,由服務(wù)器生成的
* @param UserId :對方的id嫡霞,由服務(wù)器生成的
* @param Type :頁面的類型 1:視頻呼叫 2:視頻邀請 3:視頻通話,本方法只能傳1
*/
public static void startVoiceCallActivity(Context context, String HostId, String UserId, int Type, String HeadUrl, int CallType) {
Intent intent = new Intent(context, VoiceCallActivity.class);
intent.putExtra("HostId", HostId);
intent.putExtra("CallType", CallType);
intent.putExtra("UserId", UserId);
intent.putExtra("Type", Type);
intent.putExtra("HeadUrl", HeadUrl);
context.startActivity(intent);
((Activity) context).overridePendingTransition(R.anim.left_out, R.anim.left_in);
}
四.應(yīng)答界面主要邏輯
應(yīng)答界面姿染,思路,callsdk 提供了來電監(jiān)聽ILVIncomingListener秒际,實現(xiàn)了這個監(jiān)聽就能收到所有來電,但是有個問題這個監(jiān)聽寫在哪里能讓整個app 都收到狡汉,所以我寫在了服務(wù)里面
注冊服務(wù)器
<service android:name="com.jyjt.ydyl.txim.VoiceService" />
在應(yīng)用啟動的時候打開服務(wù)
Intent serviceIntent = new Intent(MainActivity.this, VoiceService.class);
startService(serviceIntent);
服務(wù)中的應(yīng)答邏輯
package com.jyjt.ydyl.txim;
import android.app.Service;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.text.TextUtils;
import android.util.Log;
import com.jyjt.ydyl.application.MyApplication;
import com.jyjt.ydyl.tools.ConfigUtils;
import com.jyjt.ydyl.tools.LogUtils;
import com.jyjt.ydyl.tools.SwitchActivityManager;
import com.jyjt.ydyl.tools.ToastUtil;
import com.jyjt.ydyl.txim.model.TXChatHead;
import com.jyjt.ydyl.txim.presentation.event.VcallEvent;
import com.tencent.callsdk.ILVCallConfig;
import com.tencent.callsdk.ILVCallConstants;
import com.tencent.callsdk.ILVCallListener;
import com.tencent.callsdk.ILVCallManager;
import com.tencent.callsdk.ILVCallNotification;
import com.tencent.callsdk.ILVCallNotificationListener;
import com.tencent.callsdk.ILVIncomingListener;
import com.tencent.callsdk.ILVIncomingNotification;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.Observable;
import java.util.Observer;
/**
* 視頻邀請監(jiān)聽
* 1.服務(wù)在閃屏頁面啟動
* <p>
* 蘇艷
**/
public class VoiceService extends Service implements ILVIncomingListener, ILVCallListener, ILVCallNotificationListener {
//1.是否已經(jīng)啟動了應(yīng)答界面
boolean isStartActivity = false;
//2.是否是主動呼叫娄徊,true :呼叫 false:應(yīng)答
public static boolean isCallType = false;
//3.是否有心跳,true:有心跳 false :沒有心跳
public boolean havaHeardJump = false;
//4.本次會話是否發(fā)過通知,默認(rèn)沒有發(fā)過
public static boolean isSendNOtifacation = false;
//5.通話后對方掛斷盾戴,保留通話時長
public static String mCallTime = "";
//6.視頻通話建立的時間點
long mStartTime;
//7.狀態(tài)碼和對應(yīng)的提示文字
public static HashMap<Integer, String> mWraingHashMap = new HashMap();
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
//1.設(shè)置監(jiān)聽
ILVCallManager.getInstance().addCallListener(this);
ILVCallManager.getInstance().addIncomingListener(this);
ILVCallManager.getInstance().init(new ILVCallConfig().setAutoBusy(true).setNotificationListener(this));
//2.設(shè)置通知提示語
mWraingHashMap.put(ILVCallConstants.ERR_CALL_DISCONNECT, "通話被服務(wù)器回收");
mWraingHashMap.put(ILVCallConstants.ERR_CALL_FAILED, "請求失敗");
mWraingHashMap.put(ILVCallConstants.ERR_CALL_HANGUP, "通話結(jié)束");
mWraingHashMap.put(ILVCallConstants.ERR_CALL_LOCAL_CANCEL, "聊天已取消");
mWraingHashMap.put(ILVCallConstants.ERR_CALL_NOT_EXIST, "通話不存在");
mWraingHashMap.put(ILVCallConstants.ERR_CALL_RESPONDER_LINEBUSY, "接聽方占線");
mWraingHashMap.put(ILVCallConstants.ERR_CALL_RESPONDER_REFUSE, "已拒絕");
mWraingHashMap.put(ILVCallConstants.ERR_CALL_SPONSOR_CANCEL, "呼叫方取消");
mWraingHashMap.put(ILVCallConstants.ERR_CALL_SPONSOR_TIMEOUT, "呼叫方超時");
mWraingHashMap.put(ILVCallConstants.TCILiveCMD_Hangup, "對方結(jié)束通話");
mWraingHashMap.put(ILVCallConstants.TCILiveCMD_Reject, "對方已拒絕");
mWraingHashMap.put(ILVCallConstants.TCILiveCMD_SponsorCancel, "對方取消");
mWraingHashMap.put(ILVCallConstants.TCILiveCMD_SponsorTimeout, "無人接聽");
super.onCreate();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
@Override
public void onDestroy() {
ILVCallManager.getInstance().removeIncomingListener(this);
super.onDestroy();
}
@Override
public void onNewIncomingCall(final int callId, final int callType, final ILVIncomingNotification notification) {
Log.i("suyan", "========接聽電話+service" + callId + "=" + callType + "=" + notification.getSponsorId() + "=自己的id==");
if (callId != 0 && notification != null) {
//1.判斷呼叫是否超時,超時時間30m
if ((System.currentTimeMillis() / 1000) - notification.getTimeStamp() > 30) {
return;
}
//2.保存通話的id
isSendNOtifacation = false;
//3.延遲兩面后判斷是否有心跳
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if (havaHeardJump) {
//4.啟動應(yīng)答界面
try {
isStartActivity = true;
isCallType = false;
String json = ((ILVCallNotification) notification).getUserInfo();
JSONObject jsonObj = new JSONObject(json);
String name = jsonObj.getString("name");
String head = jsonObj.getString("head");
//5.保存頭像和昵稱寄锐,以防止首次聊天,沒有存過頭像和昵稱
TXChatHead.getInstance().addHead(head, notification.getSponsorId() + "", name);
//6.啟動應(yīng)答界面
SwitchActivityManager.startNewTaskVoiceCallActivity(VoiceService.this, 2, callId, ConfigUtils.getUid(), notification.getSponsorId(), TextUtils.isEmpty(head) ? "" : head, TextUtils.isEmpty(name) ? "" : name, callType);
} catch (JSONException e) {
e.printStackTrace();
}
}
}
}, 2000);
}
}
@Override
public void onCallEstablish(int callId) {
mStartTime = System.currentTimeMillis();
}
@Override
public void onCallEnd(int callId, int endResult, String endInfo) {
LogUtils.d("suyan", "========結(jié)束通話111");
havaHeardJump = false;
isSendNOtifacation = false;
//關(guān)閉視頻通話界面,處理弱網(wǎng)情況視頻邀請喚起尖啡,但是對方已掛斷問題
if (isStartActivity) {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
MyApplication.getmApplication().finishActivity(VoiceCallActivity.class);
isStartActivity = false;
}
}, 503);
}
}
@Override
public void onException(int iExceptionId, int errCode, String errMsg) {
}
@Override
public void onRecvNotification(int callid, ILVCallNotification notification) {
//心跳 10s 超時30s
ILVCallNotification notification = (ILVCallNotification) arg;
LogUtils.d("suyan", "===========通知的id" + notification.getNotifId() + "=====時間戳" + notification.getTimeStamp());
//1.是否有過心跳
if (notification.getNotifId() == ILVCallConstants.TCILiveCMD_HeartBeat) {
havaHeardJump = true;
}
//3.設(shè)置提示語
String waring = "";
waring = mWraingHashMap.get(notification.getNotifId());
String tostString = "";
switch (notification.getNotifId()) {
case ILVCallConstants.ERR_CALL_HANGUP: // 4 通話后橄仆,掛斷,顯示市場衅斩,自己發(fā)的通知
waring = "聊天時長" + (TextUtils.isEmpty(notification.getUserInfo()) ? "" : notification.getUserInfo());
tostString = "通話結(jié)束";
break;
case ILVCallConstants.TCILiveCMD_Hangup: //134 我呼叫盆顾,接通后對方掛斷
Long mCallTime = System.currentTimeMillis() - mStartTime;
String formatHMS = getFormatHMS(mCallTime);
waring = TextUtils.isEmpty(formatHMS) ? "對方結(jié)束通話" : "聊天時長" + formatHMS;
tostString = "對方結(jié)束通話";
break;
default:
break;
}
if (!TextUtils.isEmpty(waring) && !isSendNOtifacation) {
//1.發(fā)本地消息,isCallType=true:我主動呼叫消息由我發(fā)出 isCallType=false:我應(yīng)答消息消息有對方發(fā)出
waring = "[音視頻通話]" + waring;
//次方法是給在會話中存一條本地消息畏梆,在會話界面中顯示您宪,
//ChatActivity.saveLocalMessage(notification.getSender(), waring, isCallType, isCallType);
//2.本次通話已發(fā)送過通知
isSendNOtifacation = true;
//3.toast 提示
if (isStartActivity || isCallType) {
ToastUtil.setToast((notification.getNotifId() == ILVCallConstants.TCILiveCMD_Hangup || notification.getNotifId() == ILVCallConstants.ERR_CALL_HANGUP) ? tostString : waring);
}
}
}
// 根據(jù)毫秒返回時分秒
public String getFormatHMS(long time) {
time = time / 1000;//總秒數(shù)
int s = (int) (time % 60);//秒
int m = (int) (time / 60);//分
int h = (int) (time / 3600);//秒
return String.format("%02d:%02d:%02d", h, m, s);
}
}
五.跳轉(zhuǎn)應(yīng)答界面
/**
* 視頻邀請界面
*
* @param mType :頁面的類型 1:視頻呼叫 2:視頻邀請 3:視頻通話
* @param mCallId :會話id
*/
public static void startNewTaskVoiceCallActivity(Context context, int mType, int mCallId, String mHostId, String mUserId, String mHeadUrl, String mName, int CallType) {
Intent intent = new Intent(context, VoiceCallActivity.class);
intent.setAction(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra("CallType", CallType);
intent.putExtra("Type", mType);
intent.putExtra("CallId", mCallId);
intent.putExtra("HostId", mHostId);
intent.putExtra("HeadUrl", mHeadUrl);
intent.putExtra("UserName", mName);
intent.putExtra("UserId", mUserId);
context.startActivity(intent);
}
因為在service里面啟動app奈懒,有可能這個app還沒有啟動沒有棧,所以要單獨開個棧宪巨,設(shè)置flag Intent.FLAG_ACTIVITY_NEW_TASK
六.總結(jié)
以上就是音視頻通話的所有邏輯磷杏,主要都是參考callsdk中的demo 和callSdk的api文檔去實現(xiàn)。其中有幾個問題是demo中沒有的要自己實現(xiàn)捏卓,本代碼都已經(jīng)實現(xiàn)
1.通話狀態(tài)通知自己獲取添加
2.呼叫极祸,來電鈴聲
3.來電亮屏
4.通過心跳處理有效來電
總的來說,騰訊的音視頻通話怠晴,大部分功能要自己實現(xiàn)遥金,不像環(huán)信哪樣細(xì)節(jié)都已經(jīng)處理好了,所以自己實現(xiàn)時龄寞,要多考慮細(xì)節(jié)問題汰规,callSdk Demo 很簡單,只是實現(xiàn)了基本功能其余細(xì)小功能要多留意物邑,api文檔中有提供相應(yīng)方法的調(diào)用溜哮,要參考文檔去實現(xiàn)。什么SDK都不會保證百分之百不會出現(xiàn)問題色解,目前發(fā)現(xiàn)騰訊有時登錄會失敗茂嗓,需要自己進(jìn)行二次處理。
2018.11.16
最近得知callSdk 下架了科阎,鏈接找不到callsdk 的demo 述吸,咨詢了互動直播的技術(shù)人員,的確如此锣笨。
回復(fù)如下:
經(jīng)過咨詢?nèi)绻麑崿F(xiàn)音視頻通話蝌矛,可以參考下面Demo。
Android_TRTC
騰訊實時音視頻(TRTC)错英,集成了賬號登錄入撒、音視頻通話、文本消息聊天等基礎(chǔ)功能椭岩,可在無音視頻基礎(chǔ)技術(shù)的情況下茅逮,快速接入開發(fā)定制化的實時音視頻產(chǎn)品。