用腾讯即时通讯IM和实时音视频实现完整语音通话功能

说来奇怪,即时通讯领域的霸主QQ,微信,旗下产品出的腾讯即时通讯IM就像个残疾人一样,这里不对那里不对,要达到生产级别,就不得不去改它很多源码才行。今天先不吐槽其他的,我们看看如何在腾讯Im里面完成语音通话功能。
大致分为以下几步:

  • 原材料准备
  • 初步实现语音通话
  • 完善通话逻辑
  • 铃声震动实现、悬浮窗实现
  • 细节优化
原材料准备
  • 腾讯最新版实时音视频SDK(我这里下载的是精简版TRTC)
  • Android Studio 3.5+(需要升级Android Studio的可以参考一下我写的
    升级Android Studio踩坑)的文章,Android 4.1及以上系统(腾讯要求)
初步实现语音通话(根据腾讯的文档集成SDK)

1、集成SDK

  • 在模块的build.gradle中的 dependencies中添加
    dependencies {implementation 'com.tencent.liteav:LiteAVSDK_TRTC:latest.release'}
  • 在defaultCOnfig中,指定CPU架构
    defaultConfig {ndk {abiFilters "armeabi", "armeabi-v7a", "arm64-v8a"}}
  • 配置权限
最后,如果这篇对你有一丁点帮助,请点个赞再走吧,谢谢了喂。<uses-permission android:name="android.permission.INTERNET" />    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /><uses- permission android:name="android.permission.ACCESS_WIFI_STATE" /><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.RECORD_AUDIO" /><uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /><uses-permission android:name="android.permission.BLUETOOTH" /><uses-permission android:name="android.permission.CAMERA" /><uses-permission android:name="android.permission.READ_PHONE_STATE" /><uses-feature android:name="android.hardware.camera" /><uses-feature android:name="android.hardware.camera.autofocus" />
  • 设置混淆
    -keep class com.tencent.** { *; }
  • 设置打包参数
   packagingOptions {pickFirst '**/libc++_shared.so'doNotStrip "*/armeabi/libYTCommon.so"doNotStrip "*/armeabi-v7a/libYTCommon.so"doNotStrip "*/x86/libYTCommon.so"doNotStrip "*/arm64-v8a/libYTCommon.so"}

2、实现通话

  • 复制源码文件夹trtcaudiocalldemo 中的ui和model到项目中。这里看自己的需求进行选择,实现语音通话,我们只需要TRTCAudioCallActivity.java文件
  • 复制CallService 到项目中,这个Service主要负责处理接听电话的事务(接听电话需要进房需要查询用户信息,生成一个beingCallUserModel传入)
  • 调用 TRTCAudioCallActivity.startCallSomeone(getContext(), mContactList);发起语音通话,这里的mContactList 如果是单聊或者群聊只邀请一个人,只会有一个model,查询设置这个model的avatar、phone、userid、username、groupId即可。到此初步集成完毕,可以进行语音通话了。
完善通话逻辑

1、Android端的通话逻辑并不完善,让我们来看看它的问题

  • 不会发送结束消息,任何情况下的挂断都是发送 取消命令
  • 群通话远端用户离开房间不会触发通话挂断
    问题所在:TRTCAuduiCallImpl中的hangup 在通话进行中或者发起人主动挂断的情况下只会发送取消通话命令
    image.png
    腾讯自己也知道自己有问题,留了一个todo。那么我们如何修改呢?
    根据正常的打电话逻辑,A打给B,会有以下几种情况
    • 未通话:A取消,B拒绝,
    • 通话中:A挂断 ,B挂断
      首先B拒绝,会在hangup方法中进入reject()方法中,发送一个拒绝的消息,这个我们不用处理;然后是A取消的情况,可以通过判断邀请列表的人,如果邀请列表的人大于0,这个时候挂断,那么一定是A取消;再是A挂断和B挂断,这里得区分一下在群聊通话,还是单聊通话,如果是单聊通话,那么A挂断 就是A判断房间中用户数未0,发送一个通话结束消息出去,同理B一样。如果是群聊中,那么就是最后一个退出房间的人判断,发送一个通话结束的消息出去。
      所以在群聊和单聊中没我们可以这样判断:
                    Log.d(TAG, "Hangup: " + mCurRoomUserSet + " " + mCurInvitedList + "  " + mIsInRoom);if (mIsInRoom) {if (isCollectionEmpty(mCurRoomUserSet)) {if (mCurInvitedList.size() > 0) {//取消sendModel("", CallModel.VIDEO_CALL_ACTION_SPONSOR_CANCEL);} else {//通话结束sendModel("", CallModel.VIDEO_CALL_ACTION_HANGUP);}}}stopCall();exitRoom();}

并且如果是群聊 ,需要在远端用户退出群主,并且群主里面没有用户的时候发送通话结束的消息即 在preExitRoom方法里面调用groupHangup方法,并且退房相关操作需要注释掉,因为groupHangup方法里面会对房间参数进行判断,需要发消息,然后退房。
当然发送消息并退房并不是所有情况都适用,比如忙线,拒接、超时的时候,就只需要执行退房操作,所以在这些情况下不能调用groupHangup方法,只判断执行退房操作。
2、解析自定义消息
这个东西看需求,一般情况下,一次通话都会有两条消息,即一条发起通话消息,一条结束(拒绝、忙线、挂断、超时等情况),我这里贴一下我的解析方式和效果图:

  private void buildVoiceCallView(ICustomMessageViewGroup parent, MessageInfo info, TRTCAudioCallImpl.CallModel data) {if (data.action == TRTCAudioCallImpl.CallModel.VIDEO_CALL_ACTION_DIALING) {// 把自定义消息view添加到TUIKit内部的父容器里View view = LayoutInflater.from(AndroidApplication.getInstance()).inflate(R.layout.dial_senc_call_message, null, false);parent.addMessageItemView(view);TextView tv = view.findViewById(R.id.tv_content);if (info.isSelf()) {tv.setText("您发起了语音通话");} else {tv.setText("对方发起了语音通话");}return;}// 把自定义消息view添加到TUIKit内部的父容器里View view = LayoutInflater.from(AndroidApplication.getInstance()).inflate(R.layout.dial_custom_message, null, false);parent.addMessageContentView(view);// 自定义消息view的实现,这里仅仅展示文本信息,并且实现超链接跳转TextView textView = view.findViewById(R.id.tv_dial_status);ImageView ivLeft = view.findViewById(R.id.iv_left);ImageView ivRight = view.findViewById(R.id.iv_right);if (info.isSelf()) {ivRight.setVisibility(View.VISIBLE);ivLeft.setVisibility(View.GONE);textView.setTextColor(getResources().getColor(R.color.white));} else {ivRight.setVisibility(View.GONE);ivLeft.setVisibility(View.VISIBLE);textView.setTextColor(getResources().getColor(R.color.color_333333));}String text;switch (data.action) {case TRTCAudioCallImpl.CallModel.VIDEO_CALL_ACTION_SPONSOR_CANCEL:text = "已取消";break;case TRTCAudioCallImpl.CallModel.VIDEO_CALL_ACTION_REJECT:text = "已拒绝";break;case TRTCAudioCallImpl.CallModel.VIDEO_CALL_ACTION_SPONSOR_TIMEOUT:text = "无人接听";break;case TRTCAudioCallImpl.CallModel.VIDEO_CALL_ACTION_HANGUP:if (data.duration == 0) {text = "通话结束";} else {text = "通话结束 " + TimeUtils.millis2StringByCorrect(data.duration * 1000, data.duration >= 60 * 60 ? "HH:mm:ss" : "mm:ss");}break;case TRTCAudioCallImpl.CallModel.VIDEO_CALL_ACTION_LINE_BUSY:text = "忙线中";break;default:text = "未知通话错误";break;}textView.setText(text);}

效果图

铃声震动实现、悬浮窗实现

1、铃声震动(呼叫和待接听响铃,接听和挂断停止响铃)

  • 呼叫方 邀请页面响铃或震动,在showInvitingView()方法中添加
//开始呼叫响铃
if (mRingVibrateHelper != null) {    mRingVibrateHelper.initLocalCallRinging();}
  • 通话中停止响铃或震动,在showCallingView()方法中使用
//停止响铃if (mRingVibrateHelper != null) {    mRingVibrateHelper.stopRing();}
  • 接听方在,接听等待页面响铃或震动,在showWaitingResponseView()方法中使用
//响铃或者震动mRingVibrateHelper.initRemoteCallRinging();
  • 页面退出,停止响铃
 if (mRingVibrateHelper != null) {mRingVibrateHelper.stopRing();mRingVibrateHelper.releaseMediaPlayer();}

分享一下响铃震动帮助类TimRingVibrateHelper

/*** @author leary* 响铃震动帮助类*/
public class TimRingVibrateHelper {private static final String TAG = TimRingVibrateHelper.class.getSimpleName();/*** =============响铃 震动相关*/private MediaPlayer mMediaPlayer;private Vibrator mVibrator;private static TimRingVibrateHelper instance;public static TimRingVibrateHelper getInstance() {if (instance == null) {synchronized (TimRingVibrateHelper.class) {if (instance == null) {instance = new TimRingVibrateHelper();}}}return instance;}private TimRingVibrateHelper() {//铃声相关mMediaPlayer = new MediaPlayer();mMediaPlayer.setOnPreparedListener(mp -> {if (mp != null) {mp.setLooping(true);mp.start();}});}/*** ==============响铃、震动相关方法========================*/public void initLocalCallRinging() {try {AssetFileDescriptor assetFileDescriptor = AndroidApplication.getInstance().getResources().openRawResourceFd(R.raw.voip_outgoing_ring);mMediaPlayer.reset();mMediaPlayer.setDataSource(assetFileDescriptor.getFileDescriptor(),assetFileDescriptor.getStartOffset(), assetFileDescriptor.getLength());assetFileDescriptor.close();// 设置 MediaPlayer 播放的声音用途if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {AudioAttributes attributes = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION).build();mMediaPlayer.setAudioAttributes(attributes);} else {mMediaPlayer.setAudioStreamType(AudioManager.STREAM_VOICE_CALL);}mMediaPlayer.prepareAsync();final AudioManager am = (AudioManager) AndroidApplication.getInstance().getSystemService(Context.AUDIO_SERVICE);if (am != null) {am.setSpeakerphoneOn(false);// 设置此值可在拨打时控制响铃音量am.setMode(AudioManager.MODE_IN_COMMUNICATION);// 设置拨打时响铃音量默认值am.setStreamVolume(AudioManager.STREAM_VOICE_CALL, 8, AudioManager.STREAM_VOICE_CALL);}} catch (IOException e) {e.printStackTrace();}}/*** 判断系统响铃正东相关设置* 1、系统静音 不震动 就两个都不设置* 2、静音震动* 3、只响铃不震动* 4、响铃且震动*/public void initRemoteCallRinging() {int ringerMode = getRingerMode(AndroidApplication.getInstance());if (ringerMode != AudioManager.RINGER_MODE_SILENT) {if (ringerMode == AudioManager.RINGER_MODE_VIBRATE) {startVibrator();} else {if (isVibrateWhenRinging()) {startVibrator();}startRing();}}}private int getRingerMode(Context context) {AudioManager audio = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);return audio.getRingerMode();}/*** 开始响铃*/private void startRing() {Uri uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);try {mMediaPlayer.setDataSource(AndroidApplication.getInstance(), uri);mMediaPlayer.prepareAsync();} catch (Exception e) {e.printStackTrace();Log.e(TAG, "Ringtone not found : " + uri);try {uri = RingtoneManager.getValidRingtoneUri(AndroidApplication.getInstance());mMediaPlayer.setDataSource(AndroidApplication.getInstance(), uri);mMediaPlayer.prepareAsync();} catch (Exception e1) {e1.printStackTrace();Log.e(TAG, "Ringtone not found: " + uri);}}}/*** 开始震动*/private void startVibrator() {if (mVibrator == null) {mVibrator = (Vibrator) AndroidApplication.getInstance().getSystemService(Context.VIBRATOR_SERVICE);} else {mVibrator.cancel();}mVibrator.vibrate(new long[]{500, 1000}, 0);}/*** 判断系统是否设置了 响铃时振动*/private boolean isVibrateWhenRinging() {ContentResolver resolver = AndroidApplication.getInstance().getApplicationContext().getContentResolver();if (Build.MANUFACTURER.equals("Xiaomi")) {return Settings.System.getInt(resolver, "vibrate_in_normal", 0) == 1;} else if (Build.MANUFACTURER.equals("smartisan")) {return Settings.Global.getInt(resolver, "telephony_vibration_enabled", 0) == 1;} else {return Settings.System.getInt(resolver, "vibrate_when_ringing", 0) == 1;}}/*** 停止震动和响铃*/public void stopRing() {if (mMediaPlayer != null) {mMediaPlayer.reset();}if (mVibrator != null) {mVibrator.cancel();}if (AndroidApplication.getInstance() != null) {//通话时控制音量AudioManager audioManager = (AudioManager) AndroidApplication.getInstance().getApplicationContext().getSystemService(AUDIO_SERVICE);audioManager.setMode(AudioManager.MODE_NORMAL);}}/*** 释放资源*/public void releaseMediaPlayer() {if (mMediaPlayer != null) {mMediaPlayer.release();mMediaPlayer = null;}if (instance != null) {instance = null;}// 退出此页面后应设置成正常模式,否则按下音量键无法更改其他音频类型的音量if (AndroidApplication.getInstance() != null) {AudioManager am = (AudioManager) AndroidApplication.getInstance().getApplicationContext().getSystemService(Context.AUDIO_SERVICE);if (am != null) {am.setMode(AudioManager.MODE_NORMAL);}}}
}

2、悬浮窗 实现

  • 申请权限
  • 将当前通话Activity移动到后台执行
  • 开启悬浮窗服务

1)申请权限

   @TargetApi(19)public static boolean canDrawOverlays(final Context context, boolean needOpenPermissionSetting) {boolean result = true;if (Build.VERSION.SDK_INT >= 23) {try {boolean booleanValue = (Boolean) Settings.class.getDeclaredMethod("canDrawOverlays", Context.class).invoke((Object) null, context);if (!booleanValue && needOpenPermissionSetting) {ArrayList<String> permissionList = new ArrayList();permissionList.add("android.settings.action.MANAGE_OVERLAY_PERMISSION");showPermissionAlert(context, context.getString(R.string.tim_float_window_not_allowed), new DialogInterface.OnClickListener() {@Overridepublic void onClick(DialogInterface dialog, int which) {if (-1 == which) {Intent intent = new Intent("android.settings.action.MANAGE_OVERLAY_PERMISSION", Uri.parse("package:" + context.getPackageName()));context.startActivity(intent);}if (-2 == which) {Toasty.warning(context, "抱歉,您已拒绝DBC获得您的悬浮窗权限,将影响您接听对方发起的语音通话。").show();}}});}Log.i(TAG, "isFloatWindowOpAllowed allowed: " + booleanValue);return booleanValue;} catch (Exception var7) {Log.e(TAG, String.format("getDeclaredMethod:canDrawOverlays! Error:%s, etype:%s", var7.getMessage(), var7.getClass().getCanonicalName()));return true;}} else if (Build.VERSION.SDK_INT < 19) {return true;} else {Object systemService = context.getSystemService(Context.APP_OPS_SERVICE);Method method;try {method = Class.forName("android.app.AppOpsManager").getMethod("checkOp", Integer.TYPE, Integer.TYPE, String.class);} catch (NoSuchMethodException var9) {Log.e(TAG, String.format("NoSuchMethodException method:checkOp! Error:%s", var9.getMessage()));method = null;} catch (ClassNotFoundException var10) {var10.printStackTrace();method = null;}if (method != null) {try {Integer tmp = (Integer) method.invoke(systemService, 24, context.getApplicationInfo().uid, context.getPackageName());result = tmp == 0;} catch (Exception var8) {Log.e(TAG, String.format("call checkOp failed: %s etype:%s", var8.getMessage(), var8.getClass().getCanonicalName()));}}Log.i(TAG, "isFloatWindowOpAllowed allowed: " + result);return result;}}

当然申请悬浮窗全选会有跳转到设置界面这个过程,所以还需要添加判断是否具有悬浮窗权限的判断过程,这里就留点发挥空间了。

2)将当前通话Activity移动到后台执行
这个很简单,就是将Activity的lunchMode改为SingleInstance模式,然后直接调用moveTaskToBack(true);方法,这里传true,表示任何情况下 都会将Acitivty移动到后台。但是有得必有失,设置为SingleInstance模式会为我们带来一些问题,这些我会在后面说明。
3)绑定悬浮窗服务,开启悬浮窗
创建一个悬浮窗Service,获取WindowManager,在windowManager添加一个自定义的悬浮窗View即可,当然要想悬浮窗可以移动,得重写悬浮窗的,触摸事件。在悬浮窗里面注册一个本地广播,方便改变通话状态,记录通话时间等等。贴一下代码,需要自取。

public class TimFloatWindowService extends Service implements View.OnTouchListener {private WindowManager mWindowManager;private WindowManager.LayoutParams wmParams;private LayoutInflater inflater;/*** 浮动布局view*/private View mFloatingLayout;/*** 容器父布局*/private View mMainView;/*** 开始触控的坐标,移动时的坐标(相对于屏幕左上角的坐标)*/private int mTouchStartX, mTouchStartY, mTouchCurrentX, mTouchCurrentY;/*** 开始时的坐标和结束时的坐标(相对于自身控件的坐标)*/private int mStartX, mStartY, mStopX, mStopY;/*** 判断悬浮窗口是否移动,这里做个标记,防止移动后松手触发了点击事件*/private boolean isMove;/*** 判断是否绑定了服务*/private boolean isServiceBind;/*** 通话状态*/private TextView mAcceptStatus;public class TimBinder extends Binder {public TimFloatWindowService getService() {return TimFloatWindowService.this;}}private BroadcastReceiver mTimBroadCastReceiver = new BroadcastReceiver() {@Overridepublic void onReceive(Context context, Intent intent) {if (isServiceBind && CommonI.TIM.BROADCAST_FLAG_FLOAT_STATUS.equals(intent.getAction())&& mAcceptStatus != null) {String status = intent.getStringExtra(CommonI.TIM.KEY_ACCEPT_STATUS);mAcceptStatus.setText(status);}}};@Overridepublic IBinder onBind(Intent intent) {isServiceBind = true;initFloating();//悬浮框点击事件的处理return new TimBinder();}@Overridepublic void onCreate() {super.onCreate();//设置悬浮窗基本参数(位置、宽高等)initWindow();//注册 BroadcastReceiver 监听情景模式的切换IntentFilter filter = new IntentFilter();filter.addAction(CommonI.TIM.BROADCAST_FLAG_FLOAT_STATUS);LocalBroadcastManager.getInstance(this).registerReceiver(mTimBroadCastReceiver, filter);}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {return super.onStartCommand(intent, flags, startId);}@Overridepublic void onDestroy() {super.onDestroy();isServiceBind = false;if (mFloatingLayout != null) {// 移除悬浮窗口mWindowManager.removeView(mFloatingLayout);mFloatingLayout = null;}LocalBroadcastManager.getInstance(this).unregisterReceiver(mTimBroadCastReceiver);}/*** 设置悬浮框基本参数(位置、宽高等)*/private void initWindow() {mWindowManager = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);//设置好悬浮窗的参数wmParams = getParams();// 悬浮窗默认显示以右上角为起始坐标wmParams.gravity = Gravity.RIGHT | Gravity.TOP;// 不设置这个弹出框的透明遮罩显示为黑色wmParams.format = PixelFormat.TRANSLUCENT;//悬浮窗的开始位置,因为设置的是从右上角开始,所以屏幕左上角是x=0;y=0wmParams.x = 40;wmParams.y = 160;//得到容器,通过这个inflater来获得悬浮窗控件inflater = LayoutInflater.from(getApplicationContext());// 获取浮动窗口视图所在布局mFloatingLayout = inflater.inflate(R.layout.layout_tim_float_window, null);// 添加悬浮窗的视图mWindowManager.addView(mFloatingLayout, wmParams);}private WindowManager.LayoutParams getParams() {wmParams = new WindowManager.LayoutParams();if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {wmParams.type = WindowManager.LayoutParams.TYPE_TOAST;} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {wmParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;} else {wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;}//设置可以显示在状态栏上wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR |WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM |WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;//设置悬浮窗口长宽数据wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;return wmParams;}//加载远端视屏:在这对悬浮窗内内容做操作private void initFloating() {//将子View加载进悬浮窗View//悬浮窗父布局mMainView = mFloatingLayout.findViewById(R.id.layout_dial_float);//加载进悬浮窗的子View,这个VIew来自天转过来的那个Activity里面的那个需要加载的ViewmAcceptStatus = mFloatingLayout.findViewById(R.id.tv_accept_status);
//        View mChildView = renderView.getChildView();
//        mMainView.addView(mChildView);//将需要悬浮显示的Viewadd到mTXCloudVideoView中//悬浮框触摸事件,设置悬浮框可拖动mMainView.setOnTouchListener(this);//悬浮框点击事件mMainView.setOnClickListener(v -> {//绑定了服务才跳转,不绑定服务不跳转if (!isServiceBind) {return;}//在这里实现点击重新回到Activity//从该service跳转至该activity会将该activity从后台唤醒,所以activity会走onReStart()Intent intent = new Intent(TimFloatWindowService.this, TRTCAudioCallActivity.class);//需要Intent.FLAG_ACTIVITY_NEW_TASK,不然会崩溃intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);startActivity(intent);});}@Overridepublic boolean onTouch(View v, MotionEvent event) {int action = event.getAction();switch (action) {case MotionEvent.ACTION_DOWN:isMove = false;mTouchStartX = (int) event.getRawX();mTouchStartY = (int) event.getRawY();mStartX = (int) event.getX();mStartY = (int) event.getY();break;case MotionEvent.ACTION_MOVE:mTouchCurrentX = (int) event.getRawX();mTouchCurrentY = (int) event.getRawY();wmParams.x -= mTouchCurrentX - mTouchStartX;wmParams.y += mTouchCurrentY - mTouchStartY;Log.i("Tim_FloatingListener", " Cx: " + mTouchCurrentX + " Sx: " + mTouchStartX + " Cy: " + mTouchCurrentY + " Sy: " + mTouchStartY);if (mFloatingLayout != null) {mWindowManager.updateViewLayout(mFloatingLayout, wmParams);}mTouchStartX = mTouchCurrentX;mTouchStartY = mTouchCurrentY;break;case MotionEvent.ACTION_UP:mStopX = (int) event.getX();mStopY = (int) event.getY();if (Math.abs(mStartX - mStopX) >= 1 || Math.abs(mStartY - mStopY) >= 1) {isMove = true;}break;default:break;}//如果是移动事件不触发OnClick事件,防止移动的时候一放手形成点击事件return isMove;}
}
细节优化

1、SingleInstance的 Home键处理

当luncherModel为SingleInstance的时候,点击Home键会引发很多问题

  • 点击图标回到app的时候进入到的是第一个栈,而不是打电话页面
    我的解决办法是在聊天页面检测通话页面是否正在运行,如果在运行的话,生成一个正在进行语音通话的noticeLayout,然后给noticeLauout设置点击回到语音通话页面。
  • 点击recent键,会回到最初的状态,即 就算通话已经结束,从recent回去 会变成打电话的初始状态。
    设置一个通话是否结束的标记位,保存在SharePreference里面,在onCreate 中进行判断,如果是已经结束的通话,就加载另外一套通话结束的页面。

2、当应用退到后台的时候,部分手机无法唤起后台弹出(小米手机)功能,而有些手机又会直接弹出,显然这两种都不友好。

我们在接电话的地方设置一个30s的计时器,在这30s中不停检测应用是否在前台运行,并且判断通话是否结束,如果检测过程中两个条件都满足了,我们就打开通话页面,然后取消计时。这样做有两个好处,一个是,无法唤起后台弹出的手机,当我们打开app的收,在有效期之内还能接到电话。另外一个是,能后台自动弹出的手机,不会突兀的响铃和乱跳转页面。

3、离线打电话消息接收问题

腾讯的离线推送没有统一的处理,这使得我们监听离线消息变得十分困难,并且有些手机的离线推送甚至不能被检测到。这个时候我们换一种思路,我们直接在打开app的时候检测消息列表的历史消息,获取最后一条消息,进行语音通话的消息处理,这样们在接收离线通知的情况下,也能直接打开到通话页面

最后

使用腾讯IM和腾讯实时音视频 的坑很多,不过都被我们一一淌过来了,如果你遇到不好解决的问题,欢迎留言交流,最后,如果这篇对你有一丁点帮助,请点个赞再走吧,谢谢。

Published by

风君子

独自遨游何稽首 揭天掀地慰生平