在陪玩系统源码中,用户之间主要的交流方式就是语音通话,实时互动性的语音通话能让人产生面对面交谈的感觉,所以在陪玩系统源码中,语音通话功能的开发非常重要,今天我们就一起来看看如何用腾讯即时通讯IM和实时音视频实现陪玩系统源码的语音通话功能吧。
大致分为以下几步:
- 初步实现语音通话
- 完善通话逻辑
- 铃声震动实现、悬浮窗实现
初步实现陪玩系统源码的语音通话
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 在通话进行中或者发起人主动挂断的情况下只会发送取消通话命令
根据正常的打电话逻辑,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移动到后台。
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;}
}
以上就是“用腾讯即时通讯IM和实时音视频实现陪玩系统源码的语音通话功能”的全部内容,希望对大家有帮助。