最近项目中有需要语音、视频通话需求,看到这个像环信、融云等SDK都有具体Demo实现,但咋的领导对腾讯情有独钟啊,IM要用腾讯云IM,不妙的是腾讯云IM并不包含有音视频通话都要自己实现,没办法深入了解腾讯云产品后,决定自己基于腾讯云实时音视频做去语音、视频通话功能。在这里把实现过程记录下为以后用到便于查阅,另一方面也给有需要的人提供一个思路,让大家少走弯路,有可能我的实现的方法不是最好,但是这或许是一个可行的方案,大家不喜勿喷。基于腾讯云实时音视频SDK 6.5.7272版本,腾讯DEMO下载地址

一、实现效果

     

二、实现思路

我把实现思路拆分为了两步:1、视频通话Activity的最小化。 2、视频通话悬浮框的开启

具体思路是这样的:当用户点击左上角最小化按钮的时候,最小化视频通话Activity(这时Activity处于后台状态),于此同时开启悬浮框,新建一个新的ViewGroup将全局Constents.mVideoViewLayout中用户选中的最大View动态添加到悬浮框里面去,监听悬浮框的触摸事件,让悬浮框可以拖拽移动;自定义点击事件,如果用户点击了悬浮框,则移除悬浮框然后重新调起我们在后台的视频通话Activity。

1.Activity是如何实现最小化的?

Activity本身自带了一个moveTaskToBack(boolean nonRoot),我们要实现最小化只需要调用moveTaskToBack(true)传入一个true值就可以了,但是这里有一个前提,就是需要设置Activity的启动模式为singleInstance模式,两步搞定。(注:activity最小化后重新从后台回到前台会回调onRestart()方法)

  @Overridepublic boolean moveTaskToBack(boolean nonRoot) {return super.moveTaskToBack(nonRoot);}

2.悬浮框是如何开启的?

悬浮框的实现方法最好写在Service里面,将悬浮框的开启关闭与服务Service的绑定解绑所关联起来,开启服务即相当于开启我们的悬浮框,解绑服务则相当于关闭关闭的悬浮框,以此来达到更好的控制效果。

a. 首先我们声明一个服务类,取名为FloatVideoWindowService:

public class FloatVideoWindowService extends Service {@Nullable@Overridepublic IBinder onBind(Intent intent) {return new MyBinder();}public class MyBinder extends Binder {public FloatVideoWindowService getService() {return FloatVideoWindowService.this;}}@Overridepublic void onCreate() {super.onCreate();}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {return super.onStartCommand(intent, flags, startId);}@Overridepublic void onDestroy() {super.onDestroy();}
}

b. 为悬浮框建立一个布局文件float_video_window_layout,悬浮框大小我这里固定为长80dp,高120dp,id为small_size_preview的RelativeLayout主要是一个容器,可以动态的添加view到里面去

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/small_size_frame_layout"android:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@color/colorComBg"android:orientation="vertical"><com.tencent.rtmp.ui.TXCloudVideoViewandroid:id="@+id/float_videoview"android:layout_width="80dp"android:layout_height="120dp"android:descendantFocusability="blocksDescendants"android:orientation="vertical" /></LinearLayout>

c. 布局定义好后,接下来就要对悬浮框做一些初始化操作了,初始化操作这里我们放在服务的onCreate()生命周期里面执行,因为只需要执行一次就行了。这里的初始化主要包括对:悬浮框的基本参数(位置,宽高等),悬浮框的点击事件以及悬浮框的触摸事件(即可拖动范围)等的设置,在onBind()中从Intent中取出了Activity中用户选中最大View的id,以便在后面从 Constents.mVideoViewLayout中取出对应View,然后加入悬浮窗布局中

/*** 视频悬浮窗服务*/
public class FloatVideoWindowService extends Service {private WindowManager mWindowManager;private WindowManager.LayoutParams wmParams;private LayoutInflater inflater;private String currentBigUserId;//浮动布局viewprivate View mFloatingLayout;//容器父布局private RelativeLayout smallSizePreviewLayout;private TXCloudVideoView mLocalVideoView;@Overridepublic void onCreate() {super.onCreate();initWindow();//设置悬浮窗基本参数(位置、宽高等)}@Nullable@Overridepublic IBinder onBind(Intent intent) {currentBigUserId = intent.getStringExtra("userId");initFloating();//悬浮框点击事件的处理return new MyBinder();}public class MyBinder extends Binder {public FloatVideoWindowService getService() {return FloatVideoWindowService.this;}}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {return super.onStartCommand(intent, flags, startId);}/*** 设置悬浮框基本参数(位置、宽高等)*/private void initWindow() {mWindowManager = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);//设置好悬浮窗的参数wmParams = getParams();// 悬浮窗默认显示以左上角为起始坐标wmParams.gravity = Gravity.LEFT | Gravity.TOP;//悬浮窗的开始位置,因为设置的是从左上角开始,所以屏幕左上角是x=0;y=0wmParams.x = 70;wmParams.y = 210;//得到容器,通过这个inflater来获得悬浮窗控件inflater = LayoutInflater.from(getApplicationContext());// 获取浮动窗口视图所在布局mFloatingLayout = inflater.inflate(R.layout.alert_float_video_layout, null);// 添加悬浮窗的视图mWindowManager.addView(mFloatingLayout, wmParams);}private WindowManager.LayoutParams getParams() {wmParams = new WindowManager.LayoutParams();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;//设置悬浮窗口长宽数据wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;return wmParams;}private void initFloating() {}
}

d. 在悬浮框成功被初始化以及相关参数被设置后,接下来就需要将Activity中用户选中最大的View添加到悬浮框里面去了,这样我们才能看到视频画面嘛,同样我们是在Service的onCreate这个生命周期中initFloating()完成这个操作的,代码如下所示:

TRTCVideoViewLayout mTRTCVideoViewLayout = Constents.mVideoViewLayout;
TXCloudVideoView mLocalVideoView = mTRTCVideoViewLayout.getCloudVideoViewByUseId(currentBigUserId);
if (mLocalVideoView == null) {mLocalVideoView = mTRTCVideoViewLayout.getCloudVideoViewByIndex(0);
}
if (ConstData.userid.equals(currentBigUserId)) {TXCGLSurfaceView mTXCGLSurfaceView = mLocalVideoView.getGLSurfaceView();if (mTXCGLSurfaceView != null && mTXCGLSurfaceView.getParent() != null) {((ViewGroup) mTXCGLSurfaceView.getParent()).removeView(mTXCGLSurfaceView);mTXCloudVideoView.addVideoView(mTXCGLSurfaceView);}
} else {TextureView mTextureView = mLocalVideoView.getVideoView();if (mTextureView != null && mTextureView.getParent() != null) {((ViewGroup) mTextureView.getParent()).removeView(mTextureView);mTXCloudVideoView.addVideoView(mTextureView);}
}

e. 我们上面说到要将服务Service的绑定与解绑与悬浮框的开启和关闭相结合,所以既然我们在服务的onCreate()方法中开启了悬浮框,那么就应该在其onDestroy()方法中对悬浮框进行关闭,关闭悬浮框的本质是将相关View给移除掉,在服务的onDestroy()方法中执行如下代码:

 @Overridepublic void onDestroy() {super.onDestroy();if (mFloatingLayout != null) {// 移除悬浮窗口mWindowManager.removeView(mFloatingLayout);mFloatingLayout = null;Constents.isShowFloatWindow = false;}}

f. 服务的绑定方式有bindService和startService两种,使用不同的绑定方式其生命周期也会不一样,已知我们需要让悬浮框在视频通话activity finish掉的时候也顺便关掉,那么理所当然我们就应该采用bind方式来启动服务,让他的生命周期跟随他的开启者,也即是跟随开启它的activity生命周期。

intent = new Intent(this, FloatVideoWindowService.class);//开启服务显示悬浮框
bindService(intent, mVideoServiceConnection, Context.BIND_AUTO_CREATE);ServiceConnection mVideoServiceConnection = new ServiceConnection() {@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {// 获取服务的操作对象FloatVideoWindowService.MyBinder binder = (FloatVideoWindowService.MyBinder) service;binder.getService();}@Overridepublic void onServiceDisconnected(ComponentName name) {}};

Service完整代码如下:

/*** 视频悬浮窗服务*/
public class FloatVideoWindowService extends Service {private WindowManager mWindowManager;private WindowManager.LayoutParams wmParams;private LayoutInflater inflater;private String currentBigUserId;//浮动布局viewprivate View mFloatingLayout;//容器父布局private TXCloudVideoView mTXCloudVideoView;@Overridepublic void onCreate() {super.onCreate();initWindow();//设置悬浮窗基本参数(位置、宽高等)}@Nullable@Overridepublic IBinder onBind(Intent intent) {currentBigUserId = intent.getStringExtra("userId");initFloating();//悬浮框点击事件的处理return new MyBinder();}public class MyBinder extends Binder {public FloatVideoWindowService getService() {return FloatVideoWindowService.this;}}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {return super.onStartCommand(intent, flags, startId);}@Overridepublic void onDestroy() {super.onDestroy();if (mFloatingLayout != null) {// 移除悬浮窗口mWindowManager.removeView(mFloatingLayout);mFloatingLayout = null;Constents.isShowFloatWindow = false;}}/*** 设置悬浮框基本参数(位置、宽高等)*/private void initWindow() {mWindowManager = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);//设置好悬浮窗的参数wmParams = getParams();// 悬浮窗默认显示以左上角为起始坐标wmParams.gravity = Gravity.LEFT | Gravity.TOP;//悬浮窗的开始位置,因为设置的是从左上角开始,所以屏幕左上角是x=0;y=0wmParams.x = 70;wmParams.y = 210;//得到容器,通过这个inflater来获得悬浮窗控件inflater = LayoutInflater.from(getApplicationContext());// 获取浮动窗口视图所在布局mFloatingLayout = inflater.inflate(R.layout.alert_float_video_layout, null);// 添加悬浮窗的视图mWindowManager.addView(mFloatingLayout, wmParams);}private WindowManager.LayoutParams getParams() {wmParams = new WindowManager.LayoutParams();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;//设置悬浮窗口长宽数据wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;return wmParams;}private void initFloating() {mTXCloudVideoView = mFloatingLayout.findViewById(R.id.float_videoview);TRTCVideoViewLayout mTRTCVideoViewLayout = Constents.mVideoViewLayout;TXCloudVideoView mLocalVideoView = mTRTCVideoViewLayout.getCloudVideoViewByUseId(currentBigUserId);if (mLocalVideoView == null) {mLocalVideoView = mTRTCVideoViewLayout.getCloudVideoViewByIndex(0);}if (ConstData.userid.equals(currentBigUserId)) {TXCGLSurfaceView mTXCGLSurfaceView = mLocalVideoView.getGLSurfaceView();if (mTXCGLSurfaceView != null && mTXCGLSurfaceView.getParent() != null) {((ViewGroup) mTXCGLSurfaceView.getParent()).removeView(mTXCGLSurfaceView);mTXCloudVideoView.addVideoView(mTXCGLSurfaceView);}} else {TextureView mTextureView = mLocalVideoView.getVideoView();if (mTextureView != null && mTextureView.getParent() != null) {((ViewGroup) mTextureView.getParent()).removeView(mTextureView);mTXCloudVideoView.addVideoView(mTextureView);}}Constents.isShowFloatWindow = true;//悬浮框触摸事件,设置悬浮框可拖动mTXCloudVideoView.setOnTouchListener(new FloatingListener());//悬浮框点击事件mTXCloudVideoView.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {//在这里实现点击重新回到ActivityIntent intent = new Intent(FloatVideoWindowService.this, TRTCVideoCallActivity.class);startActivity(intent);}});}//开始触控的坐标,移动时的坐标(相对于屏幕左上角的坐标)private int mTouchStartX, mTouchStartY, mTouchCurrentX, mTouchCurrentY;//开始时的坐标和结束时的坐标(相对于自身控件的坐标)private int mStartX, mStartY, mStopX, mStopY;//判断悬浮窗口是否移动,这里做个标记,防止移动后松手触发了点击事件private boolean isMove;private class FloatingListener implements View.OnTouchListener {@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;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;}}}

Activity中的操作

现在我们将思路了捋一下,假设现在我正在进行视频通话,点击视频最小化按钮,我们应该按顺序执行如下步骤:应该是会出现个悬浮框。我们用mServiceBound保存Service注册状态,后面解绑时候用这个去判断,不能有些从其他页面过来调用OnRestart()方法的会报错 说 Service not register之类的错误。

/** 开启悬浮Video服务*/
private void startVideoService() {//最小化ActivitymoveTaskToBack(true);Constents.mVideoViewLayout = mVideoViewLayout;//开启服务显示悬浮框Intent floatVideoIntent = new Intent(this, FloatVideoWindowService.class);floatVideoIntent.putExtra("userId", currentBigUserId);mServiceBound=bindService(floatVideoIntent, mVideoCallServiceConnection, Context.BIND_AUTO_CREATE);
}

注意:这里用了一个全部变量 Constents.mVideoViewLayout 保存Activity中的mVideoViewLayout,以便在上面的Service中使用。

当我们点击悬浮框的时候,可以使用startActivity(intent)来再次打开我们的activity,这时候视频通话activity会回调onRestart()方法,我们在onRestart()生命周期里面unbind解绑掉悬浮框服务,并且重新设置mVideoViewLayout展示

@Overrideprotected void onRestart() {super.onRestart();//不显示悬浮框if (mServiceBound) {unbindService(mVideoCallServiceConnection);mServiceBound = false;}TXCloudVideoView txCloudVideoView = mVideoViewLayout.getCloudVideoViewByUseId(currentBigUserId);if (txCloudVideoView == null) {txCloudVideoView = mVideoViewLayout.getCloudVideoViewByIndex(0);}if(ConstData.userid.equals(currentBigUserId)){TXCGLSurfaceView mTXCGLSurfaceView=txCloudVideoView.getGLSurfaceView();if (mTXCGLSurfaceView!=null && mTXCGLSurfaceView.getParent() != null) {((ViewGroup) mTXCGLSurfaceView.getParent()).removeView(mTXCGLSurfaceView);txCloudVideoView.addVideoView(mTXCGLSurfaceView);}}else{TextureView mTextureView=txCloudVideoView.getVideoView();if (mTextureView!=null && mTextureView.getParent() != null) {((ViewGroup) mTextureView.getParent()).removeView(mTextureView);txCloudVideoView.addVideoView(mTextureView);}}}

视频Activity是在Demo中TRTCMainActivity的基础上修改完善的

视频Activity全部代码如下:

public class TRTCVideoCallActivity extends Activity implements View.OnClickListener,TRTCSettingDialog.ISettingListener, TRTCMoreDialog.IMoreListener,TRTCVideoViewLayout.ITRTCVideoViewLayoutListener, TRTCVideoViewLayout.OnVideoToChatClickListener,TRTCCallMessageManager.TRTCVideoCallMessageCancelListener {private final static String TAG = TRTCVideoCallActivity.class.getSimpleName();private boolean bEnableVideo = true, bEnableAudio = true;private boolean mCameraFront = true;private TextView tvRoomId;private ImageView ivCamera, ivVoice;private TRTCVideoViewLayout mVideoViewLayout;//通话计时private Chronometer callTimeChronometer;private TRTCCloudDef.TRTCParams trtcParams;     /// TRTC SDK 视频通话房间进入所必须的参数private TRTCCloud trtcCloud;              /// TRTC SDK 实例对象private TRTCCloudListenerImpl trtcListener;    /// TRTC SDK 回调监听private HashSet<String> mRoomMembers = new HashSet<>();private int mSdkAppId = -1;private String trtcCallFrom;private String trtcCallType;private int roomId;private String userSig;private CountDownTimer countDownTimer;private ImageView trtcSmallIv;private String currentBigUserId = ConstData.userid;private HomeWatcher mHomeWatcher;private boolean mServiceBound = false;/*** 不包含自己的接收人列表(单聊情况)*/private List<SampleUser> receiveUsers = new ArrayList<>();private static class VideoStream {String userId;int streamType;public boolean equals(Object obj) {if (obj == null || userId == null) return false;VideoStream stream = (VideoStream) obj;return (this.streamType == stream.streamType && this.userId.equals(stream.userId));}}/*** 定义服务绑定的回调 开启视频通话服务连接*/private ServiceConnection mVideoCallServiceConnection = new ServiceConnection() {@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {// 获取服务的操作对象FloatVideoWindowService.MyBinder binder = (FloatVideoWindowService.MyBinder) service;binder.getService();}@Overridepublic void onServiceDisconnected(ComponentName name) {}};private ArrayList<VideoStream> mVideosInRoom = new ArrayList<>();@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);//应用运行时,保持屏幕高亮,不锁屏getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);requestWindowFeature(Window.FEATURE_NO_TITLE);getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);ActivityUtil.addDestoryActivityToMap(TRTCVideoCallActivity.this, TAG);TRTCCallMessageManager.getInstance().setTRTCVideoCallMessageListener(this);//获取前一个页面得到的进房参数Intent intent = getIntent();long mSdkAppIdTemp = intent.getLongExtra("sdkAppId", 0);mSdkAppId = Integer.parseInt(String.valueOf(mSdkAppIdTemp));roomId = intent.getIntExtra("roomId", 0);trtcCallFrom = intent.getStringExtra("trtcCallFrom");trtcCallType = intent.getStringExtra("trtcCallType");ConstData.currentTrtcCallType = trtcCallType;ConstData.currentRoomId = roomId + "";receiveUsers = (List<SampleUser>) getIntent().getSerializableExtra("receiveUserList");userSig = intent.getStringExtra("userSig");trtcParams = new TRTCCloudDef.TRTCParams(mSdkAppId, ConstData.userid, userSig, roomId, "", "");trtcParams.role = TRTCCloudDef.TRTCRoleAnchor;//初始化 UI 控件initView();//创建 TRTC SDK 实例trtcListener = new TRTCCloudListenerImpl(this);trtcCloud = TRTCCloud.sharedInstance(this);trtcCloud.setListener(trtcListener);//开始进入视频通话房间enterRoom();/** 倒计时30秒,一次1秒 */countDownTimer = new CountDownTimer(30 * 1000, 1000) {@Overridepublic void onTick(long millisUntilFinished) {// TODO Auto-generated method stubif (!TRTCVideoCallActivity.this.isFinishing() && ConstData.enterRoomUserIdSet.size() > 0) {countDownTimer.cancel();}}@Overridepublic void onFinish() {//倒计时全部结束执行操作if (!TRTCVideoCallActivity.this.isFinishing() && ConstData.enterRoomUserIdSet.size() == 0) {exitRoom();}}};countDownTimer.start();/*** home键监听相关*/mHomeWatcher = new HomeWatcher(this);mHomeWatcher.setOnHomePressedListener(new HomeWatcher.OnHomePressedListener() {@Overridepublic void onHomePressed() {//按了HOME键//如果悬浮窗没有显示 就开启服务展示悬浮窗if (!Constents.isShowFloatWindow) {startVideoService();}}@Overridepublic void onRecentAppsPressed() {//最近app任务列表按键if (!Constents.isShowFloatWindow) {startVideoService();}}});mHomeWatcher.startWatch();}@Overrideprotected void onResume() {super.onResume();}@Overrideprotected void onDestroy() {super.onDestroy();if (countDownTimer != null) {countDownTimer.cancel();}trtcCloud.setListener(null);TRTCCloud.destroySharedInstance();ConstData.isEnterTRTCCALL = false;//解绑 不显示悬浮框if (mServiceBound) {unbindService(mVideoCallServiceConnection);mServiceBound = false;}if (mHomeWatcher != null) {mHomeWatcher.stopWatch();// 在销毁时停止监听,不然会报错的。}}/*** 重写onBackPressed* 屏蔽返回键*/@Overridepublic void onBackPressed() {
//        super.onBackPressed();//要去掉这句}/*** 初始化界面控件,包括主要的视频显示View,以及底部的一排功能按钮*/private void initView() {setContentView(R.layout.activity_trtc_video);trtcSmallIv = (ImageView) findViewById(R.id.trtc_small_iv);trtcSmallIv.setOnClickListener(this);initClickableLayout(R.id.ll_camera);initClickableLayout(R.id.ll_voice);initClickableLayout(R.id.ll_change_camera);mVideoViewLayout = (TRTCVideoViewLayout) findViewById(R.id.video_ll_mainview);mVideoViewLayout.setUserId(trtcParams.userId);mVideoViewLayout.setListener(this);mVideoViewLayout.setOnVideoToChatListener(this);callTimeChronometer = (Chronometer) findViewById(R.id.call_time_chronometer);ivVoice = (ImageView) findViewById(R.id.iv_mic);ivCamera = (ImageView) findViewById(R.id.iv_camera);tvRoomId = (TextView) findViewById(R.id.tv_room_id);tvRoomId.setText(ConstData.username + "(自己)");findViewById(R.id.video_ring_off_btn).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {exitRoom();/*** 单人通话时* 新增主叫方在接收方未接听前挂断时* 发送消息给接收方 让接收方取消响铃页面或者 来电弹框*/if ( trtcCallType.equals(Constents.ONE_TO_ONE_VIDEO_CALL)) {//ConstData.enterRoomUserIdSet.size() == 0表示还没有接收方加入房间if (ConstData.enterRoomUserIdSet.size() == 0) {sendDeclineMsg();}}}});}private LinearLayout initClickableLayout(int resId) {LinearLayout layout = (LinearLayout) findViewById(resId);layout.setOnClickListener(this);return layout;}/*** 设置视频通话的视频参数:需要 TRTCSettingDialog 提供的分辨率、帧率和流畅模式等参数*/private void setTRTCCloudParam() {// 大画面的编码器参数设置// 设置视频编码参数,包括分辨率、帧率、码率等等,这些编码参数来自于 TRTCSettingDialog 的设置// 注意(1):不要在码率很低的情况下设置很高的分辨率,会出现较大的马赛克// 注意(2):不要设置超过25FPS以上的帧率,因为电影才使用24FPS,我们一般推荐15FPS,这样能将更多的码率分配给画质TRTCCloudDef.TRTCVideoEncParam encParam = new TRTCCloudDef.TRTCVideoEncParam();encParam.videoResolution = TRTCCloudDef.TRTC_VIDEO_RESOLUTION_640_360;encParam.videoFps = 15;encParam.videoBitrate = 600;encParam.videoResolutionMode = TRTCCloudDef.TRTC_VIDEO_RESOLUTION_MODE_PORTRAIT;trtcCloud.setVideoEncoderParam(encParam);TRTCCloudDef.TRTCNetworkQosParam qosParam = new TRTCCloudDef.TRTCNetworkQosParam();qosParam.controlMode = TRTCCloudDef.VIDEO_QOS_CONTROL_SERVER;qosParam.preference = TRTCCloudDef.TRTC_VIDEO_QOS_PREFERENCE_CLEAR;trtcCloud.setNetworkQosParam(qosParam);trtcCloud.setPriorRemoteVideoStreamType(TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG);}/*** 加入视频房间:需要 TRTCNewViewActivity 提供的  TRTCParams 函数*/private void enterRoom() {// 预览前配置默认参数setTRTCCloudParam();// 开启视频采集预览if (trtcParams.role == TRTCCloudDef.TRTCRoleAnchor) {startLocalVideo(true);}trtcCloud.setBeautyStyle(TRTCCloudDef.TRTC_BEAUTY_STYLE_SMOOTH, 5, 5, 5);if (trtcParams.role == TRTCCloudDef.TRTCRoleAnchor) {trtcCloud.startLocalAudio();}setVideoFillMode(true);setVideoRotation(true);enableAudioHandFree(true);enableGSensor(true);enableAudioVolumeEvaluation(false);/*** 2019/08/08* 默认打开是前置摄像头* 前置摄像头就设置镜像 true*/enableVideoEncMirror(true);setLocalViewMirrorMode(TRTCCloudDef.TRTC_VIDEO_MIRROR_TYPE_AUTO);mVideosInRoom.clear();mRoomMembers.clear();trtcCloud.enterRoom(trtcParams, TRTCCloudDef.TRTC_APP_SCENE_VIDEOCALL);}/*** 退出视频房间*/private void exitRoom() {if (trtcCloud != null) {trtcCloud.exitRoom();}ToastUtil.toastShortMessage("通话已结束");}@Overridepublic void onClick(View v) {if (v.getId() == R.id.trtc_small_iv) {startVideoService();} else if (v.getId() == R.id.ll_camera) {onEnableVideo();} else if (v.getId() == R.id.ll_voice) {onEnableAudio();} else if (v.getId() == R.id.ll_change_camera) {onChangeCamera();}}/*** 发送挂断/拒接电话消息*/private void sendDeclineMsg() {TIMMessage timMessage = new TIMMessage();TIMCustomElem ele = new TIMCustomElem();/*** 挂断/拒接语音、视频通话消息* msgContent不放内容*/String msgStr = null;if (trtcCallType.equals(Constents.ONE_TO_ONE_AUDIO_CALL)|| trtcCallType.equals(Constents.ONE_TO_MULTIPE_AUDIO_CALL)) {msgStr = JsonUtil.toJson(Constents.AUDIO_CALL_MESSAGE_DECLINE_DESC, null);} else if (trtcCallType.equals(Constents.ONE_TO_ONE_VIDEO_CALL)|| trtcCallType.equals(Constents.ONE_TO_MULTIPE_VIDEO_CALL)) {msgStr = JsonUtil.toJson(Constents.VIDEO_CALL_MESSAGE_DECLINE_DESC, null);}ele.setData(msgStr.getBytes());timMessage.addElement(ele);String receiveUserId = null;if (!receiveUsers.isEmpty()) {SampleUser sampleUser = receiveUsers.get(0);receiveUserId = sampleUser.getUserid();}TIMConversation conversation = TIMManager.getInstance().getConversation(TIMConversationType.C2C, receiveUserId);//发送消息conversation.sendOnlineMessage(timMessage, new TIMValueCallBack<TIMMessage>() {@Overridepublic void onError(int code, String desc) {//发送消息失败//错误码 code 和错误描述 desc,可用于定位请求失败原因//错误码 code 含义请参见错误码表Log.d("NNN", "send message failed. code: " + code + " errmsg: " + desc);}@Overridepublic void onSuccess(TIMMessage msg) {//发送消息成功Log.e("NNN", "SendMsg ok");}});}/*** 开启悬浮Video服务*/private void startVideoService() {//最小化ActivitymoveTaskToBack(true);Constents.mVideoViewLayout = mVideoViewLayout;//开启服务显示悬浮框Intent floatVideoIntent = new Intent(this, FloatVideoWindowService.class);floatVideoIntent.putExtra("userId", currentBigUserId);mServiceBound = bindService(floatVideoIntent, mVideoCallServiceConnection, Context.BIND_AUTO_CREATE);}@Overrideprotected void onRestart() {super.onRestart();//不显示悬浮框if (mServiceBound) {unbindService(mVideoCallServiceConnection);mServiceBound = false;}TXCloudVideoView txCloudVideoView = mVideoViewLayout.getCloudVideoViewByUseId(currentBigUserId);if (txCloudVideoView == null) {txCloudVideoView = mVideoViewLayout.getCloudVideoViewByIndex(0);}if(ConstData.userid.equals(currentBigUserId)){TXCGLSurfaceView mTXCGLSurfaceView=txCloudVideoView.getGLSurfaceView();if (mTXCGLSurfaceView!=null && mTXCGLSurfaceView.getParent() != null) {((ViewGroup) mTXCGLSurfaceView.getParent()).removeView(mTXCGLSurfaceView);txCloudVideoView.addVideoView(mTXCGLSurfaceView);}}else{TextureView mTextureView=txCloudVideoView.getVideoView();if (mTextureView!=null && mTextureView.getParent() != null) {((ViewGroup) mTextureView.getParent()).removeView(mTextureView);txCloudVideoView.addVideoView(mTextureView);}}}/*** 开启/关闭视频上行*/private void onEnableVideo() {bEnableVideo = !bEnableVideo;startLocalVideo(bEnableVideo);mVideoViewLayout.updateVideoStatus(trtcParams.userId, bEnableVideo);ivCamera.setImageResource(bEnableVideo ? R.mipmap.remote_video_enable : R.mipmap.remote_video_disable);}/*** 开启/关闭音频上行*/private void onEnableAudio() {bEnableAudio = !bEnableAudio;trtcCloud.muteLocalAudio(!bEnableAudio);ivVoice.setImageResource(bEnableAudio ? R.mipmap.mic_enable : R.mipmap.mic_disable);}/*** 点击切换摄像头*/private void onChangeCamera() {mCameraFront = !mCameraFront;onSwitchCamera(mCameraFront);}@Overridepublic void onComplete() {setTRTCCloudParam();setVideoFillMode(true);
//        moreDlg.updateVideoFillMode(true);}/*** SDK内部状态回调*/static class TRTCCloudListenerImpl extends TRTCCloudListener implements TRTCCloudListener.TRTCVideoRenderListener {private WeakReference<TRTCVideoCallActivity> mContext;private HashMap<String, TestRenderVideoFrame> mCustomRender;public TRTCCloudListenerImpl(TRTCVideoCallActivity activity) {super();mContext = new WeakReference<>(activity);mCustomRender = new HashMap<>(10);}/*** 加入房间*/@Overridepublic void onEnterRoom(long elapsed) {final TRTCVideoCallActivity activity = mContext.get();if (activity != null) {activity.mVideoViewLayout.onRoomEnter();activity.updateCloudMixtureParams();activity.callTimeChronometer.setBase(SystemClock.elapsedRealtime());activity.callTimeChronometer.start();}}/*** 离开房间*/@Overridepublic void onExitRoom(int reason) {TRTCVideoCallActivity activity = mContext.get();ConstData.enterRoomUserIdSet.clear();ConstData.receiveUserSet.clear();ConstData.isEnterTRTCCALL = false;Log.e(TAG, "onExitRoom:11111111111111111111 ");if (activity != null) {activity.callTimeChronometer.stop();activity.finish();}}/*** ERROR 大多是不可恢复的错误,需要通过 UI 提示用户*/@Overridepublic void onError(int errCode, String errMsg, Bundle extraInfo) {Log.d(TAG, "sdk callback onError");TRTCVideoCallActivity activity = mContext.get();if (activity == null) {return;}if (errCode == TXLiteAVCode.ERR_ROOM_REQUEST_TOKEN_HTTPS_TIMEOUT ||errCode == TXLiteAVCode.ERR_ROOM_REQUEST_IP_TIMEOUT ||errCode == TXLiteAVCode.ERR_ROOM_REQUEST_ENTER_ROOM_TIMEOUT) {Toast.makeText(activity, "进房超时,请检查网络或稍后重试:" + errCode + "[" + errMsg + "]", Toast.LENGTH_SHORT).show();activity.exitRoom();return;}if (errCode == TXLiteAVCode.ERR_ROOM_REQUEST_TOKEN_INVALID_PARAMETER ||errCode == TXLiteAVCode.ERR_ENTER_ROOM_PARAM_NULL ||errCode == TXLiteAVCode.ERR_SDK_APPID_INVALID ||errCode == TXLiteAVCode.ERR_ROOM_ID_INVALID ||errCode == TXLiteAVCode.ERR_USER_ID_INVALID ||errCode == TXLiteAVCode.ERR_USER_SIG_INVALID) {Toast.makeText(activity, "进房参数错误:" + errCode + "[" + errMsg + "]", Toast.LENGTH_SHORT).show();activity.exitRoom();return;}if (errCode == TXLiteAVCode.ERR_ACCIP_LIST_EMPTY ||errCode == TXLiteAVCode.ERR_SERVER_INFO_UNPACKING_ERROR ||errCode == TXLiteAVCode.ERR_SERVER_INFO_TOKEN_ERROR ||errCode == TXLiteAVCode.ERR_SERVER_INFO_ALLOCATE_ACCESS_FAILED ||errCode == TXLiteAVCode.ERR_SERVER_INFO_GENERATE_SIGN_FAILED ||errCode == TXLiteAVCode.ERR_SERVER_INFO_TOKEN_TIMEOUT ||errCode == TXLiteAVCode.ERR_SERVER_INFO_INVALID_COMMAND ||errCode == TXLiteAVCode.ERR_SERVER_INFO_GENERATE_KEN_ERROR ||errCode == TXLiteAVCode.ERR_SERVER_INFO_GENERATE_TOKEN_ERROR ||errCode == TXLiteAVCode.ERR_SERVER_INFO_DATABASE ||errCode == TXLiteAVCode.ERR_SERVER_INFO_BAD_ROOMID ||errCode == TXLiteAVCode.ERR_SERVER_INFO_BAD_SCENE_OR_ROLE ||errCode == TXLiteAVCode.ERR_SERVER_INFO_ROOMID_EXCHANGE_FAILED ||errCode == TXLiteAVCode.ERR_SERVER_INFO_STRGROUP_HAS_INVALID_CHARS ||errCode == TXLiteAVCode.ERR_SERVER_ACC_TOKEN_TIMEOUT ||errCode == TXLiteAVCode.ERR_SERVER_ACC_SIGN_ERROR ||errCode == TXLiteAVCode.ERR_SERVER_ACC_SIGN_TIMEOUT ||errCode == TXLiteAVCode.ERR_SERVER_CENTER_INVALID_ROOMID ||errCode == TXLiteAVCode.ERR_SERVER_CENTER_CREATE_ROOM_FAILED ||errCode == TXLiteAVCode.ERR_SERVER_CENTER_SIGN_ERROR ||errCode == TXLiteAVCode.ERR_SERVER_CENTER_SIGN_TIMEOUT ||errCode == TXLiteAVCode.ERR_SERVER_CENTER_ADD_USER_FAILED ||errCode == TXLiteAVCode.ERR_SERVER_CENTER_FIND_USER_FAILED ||errCode == TXLiteAVCode.ERR_SERVER_CENTER_SWITCH_TERMINATION_FREQUENTLY ||errCode == TXLiteAVCode.ERR_SERVER_CENTER_LOCATION_NOT_EXIST ||errCode == TXLiteAVCode.ERR_SERVER_CENTER_ROUTE_TABLE_ERROR ||errCode == TXLiteAVCode.ERR_SERVER_CENTER_INVALID_PARAMETER) {Toast.makeText(activity, "进房失败,请稍后重试:" + errCode + "[" + errMsg + "]", Toast.LENGTH_SHORT).show();activity.exitRoom();return;}if (errCode == TXLiteAVCode.ERR_SERVER_CENTER_ROOM_FULL ||errCode == TXLiteAVCode.ERR_SERVER_CENTER_REACH_PROXY_MAX) {Toast.makeText(activity, "进房失败,房间满了,请稍后重试:" + errCode + "[" + errMsg + "]", Toast.LENGTH_SHORT).show();activity.exitRoom();return;}if (errCode == TXLiteAVCode.ERR_SERVER_CENTER_ROOM_ID_TOO_LONG) {Toast.makeText(activity, "进房失败,roomID超出有效范围:" + errCode + "[" + errMsg + "]", Toast.LENGTH_SHORT).show();activity.exitRoom();return;}if (errCode == TXLiteAVCode.ERR_SERVER_ACC_ROOM_NOT_EXIST ||errCode == TXLiteAVCode.ERR_SERVER_CENTER_ROOM_NOT_EXIST) {Toast.makeText(activity, "进房失败,请确认房间号正确:" + errCode + "[" + errMsg + "]", Toast.LENGTH_SHORT).show();activity.exitRoom();return;}if (errCode == TXLiteAVCode.ERR_SERVER_INFO_SERVICE_SUSPENDED) {Toast.makeText(activity, "进房失败,请确认腾讯云实时音视频账号状态是否欠费:" + errCode + "[" + errMsg + "]", Toast.LENGTH_SHORT).show();activity.exitRoom();return;}if (errCode == TXLiteAVCode.ERR_SERVER_INFO_PRIVILEGE_FLAG_ERROR ||errCode == TXLiteAVCode.ERR_SERVER_CENTER_NO_PRIVILEDGE_CREATE_ROOM ||errCode == TXLiteAVCode.ERR_SERVER_CENTER_NO_PRIVILEDGE_ENTER_ROOM) {Toast.makeText(activity, "进房失败,无权限进入房间:" + errCode + "[" + errMsg + "]", Toast.LENGTH_SHORT).show();activity.exitRoom();return;}if (errCode <= TXLiteAVCode.ERR_SERVER_SSO_SIG_EXPIRED &&errCode >= TXLiteAVCode.ERR_SERVER_SSO_INTERNAL_ERROR) {// 错误参考 https://cloud.tencent.com/document/product/269/1671#.E5.B8.90.E5.8F.B7.E7.B3.BB.E7.BB.9FToast.makeText(activity, "进房失败,userSig错误:" + errCode + "[" + errMsg + "]", Toast.LENGTH_SHORT).show();activity.exitRoom();return;}Toast.makeText(activity, "onError: " + errMsg + "[" + errCode + "]", Toast.LENGTH_SHORT).show();}/*** WARNING 大多是一些可以忽略的事件通知,SDK内部会启动一定的补救机制*/@Overridepublic void onWarning(int warningCode, String warningMsg, Bundle extraInfo) {Log.d(TAG, "sdk callback onWarning");}/*** 有新的用户加入了当前视频房间*/@Overridepublic void onUserEnter(String userId) {TRTCVideoCallActivity activity = mContext.get();ConstData.enterRoomUserIdSet.add(userId);if (activity != null) {// 创建一个View用来显示新的一路画面
//                TXCloudVideoView renderView = activity.mVideoViewLayout.onMemberEnter(userId + TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG);TXCloudVideoView renderView = activity.mVideoViewLayout.onMemberEnter(userId);if (renderView != null) {// 设置仪表盘数据显示renderView.setVisibility(View.VISIBLE);}}}/*** 有用户离开了当前视频房间*/@Overridepublic void onUserExit(String userId, int reason) {TRTCVideoCallActivity activity = mContext.get();ConstData.enterRoomUserIdSet.remove(userId);if (activity != null) {if (activity.trtcCallFrom.equals(userId)) {activity.exitRoom();} else {if (ConstData.enterRoomUserIdSet.size() == 0) {activity.exitRoom();}}//停止观看画面activity.trtcCloud.stopRemoteView(userId);activity.trtcCloud.stopRemoteSubStreamView(userId);//更新视频UI
//                activity.mVideoViewLayout.onMemberLeave(userId + TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG);
//                activity.mVideoViewLayout.onMemberLeave(userId + TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_SUB);activity.mVideoViewLayout.onMemberLeave(userId );activity.mVideoViewLayout.onMemberLeave(userId );activity.mRoomMembers.remove(userId);activity.updateCloudMixtureParams();TestRenderVideoFrame customRender = mCustomRender.get(userId);if (customRender != null) {customRender.stop();mCustomRender.remove(userId);}}}/*** 有用户屏蔽了画面*/@Overridepublic void onUserVideoAvailable(final String userId, boolean available) {TRTCVideoCallActivity activity = mContext.get();if (activity != null) {if (available) {
//                    final TXCloudVideoView renderView = activity.mVideoViewLayout.onMemberEnter(userId + TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG);final TXCloudVideoView renderView = activity.mVideoViewLayout.onMemberEnter(userId);if (renderView != null) {// 启动远程画面的解码和显示逻辑,FillMode 可以设置是否显示黑边activity.trtcCloud.setRemoteViewFillMode(userId, TRTCCloudDef.TRTC_VIDEO_RENDER_MODE_FIT);activity.trtcCloud.startRemoteView(userId, renderView);activity.runOnUiThread(new Runnable() {@Overridepublic void run() {
//                                renderView.setUserId(userId + TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG);renderView.setUserId(userId );}});}activity.mRoomMembers.add(userId);activity.updateCloudMixtureParams();} else {activity.trtcCloud.stopRemoteView(userId);
//                    activity.mVideoViewLayout.onMemberLeave(userId + TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG);activity.mVideoViewLayout.onMemberLeave(userId );activity.mRoomMembers.remove(userId);activity.updateCloudMixtureParams();}activity.mVideoViewLayout.updateVideoStatus(userId, available);}}@Overridepublic void onUserSubStreamAvailable(final String userId, boolean available) {TRTCVideoCallActivity activity = mContext.get();if (activity != null) {if (available) {
//                    final TXCloudVideoView renderView = activity.mVideoViewLayout.onMemberEnter(userId + TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_SUB);final TXCloudVideoView renderView = activity.mVideoViewLayout.onMemberEnter(userId );if (renderView != null) {// 启动远程画面的解码和显示逻辑,FillMode 可以设置是否显示黑边activity.trtcCloud.setRemoteSubStreamViewFillMode(userId, TRTCCloudDef.TRTC_VIDEO_RENDER_MODE_FIT);activity.trtcCloud.startRemoteSubStreamView(userId, renderView);activity.runOnUiThread(new Runnable() {@Overridepublic void run() {
//                                renderView.setUserId(userId + TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_SUB);renderView.setUserId(userId );}});}} else {activity.trtcCloud.stopRemoteSubStreamView(userId);
//                    activity.mVideoViewLayout.onMemberLeave(userId + TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_SUB);activity.mVideoViewLayout.onMemberLeave(userId );}}}/*** 有用户屏蔽了声音*/@Overridepublic void onUserAudioAvailable(String userId, boolean available) {TRTCVideoCallActivity activity = mContext.get();if (activity != null) {if (available) {
//                    final TXCloudVideoView renderView = activity.mVideoViewLayout.onMemberEnter(userId + TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG);final TXCloudVideoView renderView = activity.mVideoViewLayout.onMemberEnter(userId );if (renderView != null) {renderView.setVisibility(View.VISIBLE);}}}}/*** 首帧渲染回调*/@Overridepublic void onFirstVideoFrame(String userId, int streamType, int width, int height) {TRTCVideoCallActivity activity = mContext.get();Log.e(TAG, "onFirstVideoFrame: 77777777777777777777777");if (activity != null) {
//                activity.mVideoViewLayout.freshToolbarLayoutOnMemberEnter(userId + TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG);activity.mVideoViewLayout.freshToolbarLayoutOnMemberEnter(userId );}}@Overridepublic void onStartPublishCDNStream(int err, String errMsg) {}@Overridepublic void onStopPublishCDNStream(int err, String errMsg) {}@Overridepublic void onRenderVideoFrame(String userId, int streamType, TRTCCloudDef.TRTCVideoFrame frame) {
//            Log.w(TAG, String.format("onRenderVideoFrame userId: %s, type: %d",userId, streamType));}@Overridepublic void onUserVoiceVolume(ArrayList<TRTCCloudDef.TRTCVolumeInfo> userVolumes, int totalVolume) {
//            mContext.get().mVideoViewLayout.resetAudioVolume();for (int i = 0; i < userVolumes.size(); ++i) {mContext.get().mVideoViewLayout.updateAudioVolume(userVolumes.get(i).userId, userVolumes.get(i).volume);}}@Overridepublic void onStatistics(TRTCStatistics statics) {}@Overridepublic void onConnectOtherRoom(final String userID, final int err, final String errMsg) {TRTCVideoCallActivity activity = mContext.get();if (activity != null) {}}@Overridepublic void onDisConnectOtherRoom(final int err, final String errMsg) {TRTCVideoCallActivity activity = mContext.get();if (activity != null) {}}@Overridepublic void onNetworkQuality(TRTCCloudDef.TRTCQuality localQuality, ArrayList<TRTCCloudDef.TRTCQuality> remoteQuality) {TRTCVideoCallActivity activity = mContext.get();if (activity != null) {activity.mVideoViewLayout.updateNetworkQuality(localQuality.userId, localQuality.quality);for (TRTCCloudDef.TRTCQuality qualityInfo : remoteQuality) {activity.mVideoViewLayout.updateNetworkQuality(qualityInfo.userId, qualityInfo.quality);}}}}@Overridepublic void onEnableRemoteVideo(final String userId, boolean enable) {if (enable) {
//            final TXCloudVideoView renderView = mVideoViewLayout.getCloudVideoViewByUseId(userId + TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG);final TXCloudVideoView renderView = mVideoViewLayout.getCloudVideoViewByUseId(userId );if (renderView != null) {trtcCloud.setRemoteViewFillMode(userId, TRTCCloudDef.TRTC_VIDEO_RENDER_MODE_FIT);trtcCloud.startRemoteView(userId, renderView);runOnUiThread(new Runnable() {@Overridepublic void run() {
//                        renderView.setUserId(userId + TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG);renderView.setUserId(userId);mVideoViewLayout.freshToolbarLayoutOnMemberEnter(userId);}});}} else {trtcCloud.stopRemoteView(userId);}}@Overridepublic void onEnableRemoteAudio(String userId, boolean enable) {trtcCloud.muteRemoteAudio(userId, !enable);}@Overridepublic void onChangeVideoFillMode(String userId, boolean adjustMode) {trtcCloud.setRemoteViewFillMode(userId, adjustMode ? TRTCCloudDef.TRTC_VIDEO_RENDER_MODE_FIT : TRTCCloudDef.TRTC_VIDEO_RENDER_MODE_FILL);}@Overridepublic void onChangeVideoShowFrame(String userId, String userName) {currentBigUserId = userId;tvRoomId.setText(userName);}@Overridepublic void onSwitchCamera(boolean bCameraFront) {trtcCloud.switchCamera();/*** 2019/08/08* 此处增加判断* 前置摄像头就设置镜像 true* 后置摄像头就不设置镜像 false*/if (bCameraFront) {enableVideoEncMirror(true);} else {enableVideoEncMirror(false);}}/*** 视频里点击进入和某人聊天** @param userId*/@Overridepublic void onVideoToChatClick(String userId) {Intent chatIntent = new Intent(TRTCVideoCallActivity.this, IMSingleActivity.class);chatIntent.putExtra(IMKeys.INTENT_ID, userId);startActivity(chatIntent);if (!Constents.isShowFloatWindow) {startVideoService();}}/*** 拒接视频通话回调*/@Overridepublic void onTRTCVideoCallMessageCancel() {exitRoom();}@Overridepublic void onFillModeChange(boolean bFillMode) {setVideoFillMode(bFillMode);}@Overridepublic void onVideoRotationChange(boolean bVertical) {setVideoRotation(bVertical);}@Overridepublic void onEnableAudioCapture(boolean bEnable) {enableAudioCapture(bEnable);}@Overridepublic void onEnableAudioHandFree(boolean bEnable) {enableAudioHandFree(bEnable);}@Overridepublic void onMirrorLocalVideo(int localViewMirror) {setLocalViewMirrorMode(localViewMirror);}@Overridepublic void onMirrorRemoteVideo(boolean bMirror) {enableVideoEncMirror(bMirror);}@Overridepublic void onEnableGSensor(boolean bEnable) {enableGSensor(bEnable);}@Overridepublic void onEnableAudioVolumeEvaluation(boolean bEnable) {enableAudioVolumeEvaluation(bEnable);}@Overridepublic void onEnableCloudMixture(boolean bEnable) {updateCloudMixtureParams();}private void setVideoFillMode(boolean bFillMode) {if (bFillMode) {trtcCloud.setLocalViewFillMode(TRTCCloudDef.TRTC_VIDEO_RENDER_MODE_FILL);} else {trtcCloud.setLocalViewFillMode(TRTCCloudDef.TRTC_VIDEO_RENDER_MODE_FIT);}}private void setVideoRotation(boolean bVertical) {if (bVertical) {trtcCloud.setLocalViewRotation(TRTCCloudDef.TRTC_VIDEO_ROTATION_0);} else {trtcCloud.setLocalViewRotation(TRTCCloudDef.TRTC_VIDEO_ROTATION_90);}}private void enableAudioCapture(boolean bEnable) {if (bEnable) {trtcCloud.startLocalAudio();} else {trtcCloud.stopLocalAudio();}}private void enableAudioHandFree(boolean bEnable) {if (bEnable) {trtcCloud.setAudioRoute(TRTCCloudDef.TRTC_AUDIO_ROUTE_SPEAKER);} else {trtcCloud.setAudioRoute(TRTCCloudDef.TRTC_AUDIO_ROUTE_EARPIECE);}}private void enableVideoEncMirror(boolean bMirror) {trtcCloud.setVideoEncoderMirror(bMirror);}private void setLocalViewMirrorMode(int mirrorMode) {trtcCloud.setLocalViewMirror(mirrorMode);}private void enableGSensor(boolean bEnable) {if (bEnable) {trtcCloud.setGSensorMode(TRTCCloudDef.TRTC_GSENSOR_MODE_UIFIXLAYOUT);} else {trtcCloud.setGSensorMode(TRTCCloudDef.TRTC_GSENSOR_MODE_DISABLE);}}private void enableAudioVolumeEvaluation(boolean bEnable) {if (bEnable) {trtcCloud.enableAudioVolumeEvaluation(300);mVideoViewLayout.showAllAudioVolumeProgressBar();} else {trtcCloud.enableAudioVolumeEvaluation(0);mVideoViewLayout.hideAllAudioVolumeProgressBar();}}private void updateCloudMixtureParams() {// 背景大画面宽高int videoWidth = 720;int videoHeight = 1280;// 小画面宽高int subWidth = 180;int subHeight = 320;int offsetX = 5;int offsetY = 50;int bitrate = 200;int resolution = TRTCCloudDef.TRTC_VIDEO_RESOLUTION_640_360;switch (resolution) {case TRTCCloudDef.TRTC_VIDEO_RESOLUTION_160_160: {videoWidth = 160;videoHeight = 160;subWidth = 27;subHeight = 48;offsetY = 20;bitrate = 200;break;}case TRTCCloudDef.TRTC_VIDEO_RESOLUTION_320_180: {videoWidth = 192;videoHeight = 336;subWidth = 54;subHeight = 96;offsetY = 30;bitrate = 400;break;}case TRTCCloudDef.TRTC_VIDEO_RESOLUTION_320_240: {videoWidth = 240;videoHeight = 320;subWidth = 54;subHeight = 96;bitrate = 400;break;}case TRTCCloudDef.TRTC_VIDEO_RESOLUTION_480_480: {videoWidth = 480;videoHeight = 480;subWidth = 72;subHeight = 128;bitrate = 600;break;}case TRTCCloudDef.TRTC_VIDEO_RESOLUTION_640_360: {videoWidth = 368;videoHeight = 640;subWidth = 90;subHeight = 160;bitrate = 800;break;}case TRTCCloudDef.TRTC_VIDEO_RESOLUTION_640_480: {videoWidth = 480;videoHeight = 640;subWidth = 90;subHeight = 160;bitrate = 800;break;}case TRTCCloudDef.TRTC_VIDEO_RESOLUTION_960_540: {videoWidth = 544;videoHeight = 960;subWidth = 171;subHeight = 304;bitrate = 1000;break;}case TRTCCloudDef.TRTC_VIDEO_RESOLUTION_1280_720: {videoWidth = 720;videoHeight = 1280;subWidth = 180;subHeight = 320;bitrate = 1500;break;}default:break;}TRTCCloudDef.TRTCTranscodingConfig config = new TRTCCloudDef.TRTCTranscodingConfig();config.appId = -1;  // 请从"实时音视频"控制台的帐号信息中获取config.bizId = -1;  // 请进入 "实时音视频"控制台 https://console.cloud.tencent.com/rav,点击对应的应用,然后进入“帐号信息”菜单中,复制“直播信息”模块中的"bizid"config.videoWidth = videoWidth;config.videoHeight = videoHeight;config.videoGOP = 1;config.videoFramerate = 15;config.videoBitrate = bitrate;config.audioSampleRate = 48000;config.audioBitrate = 64;config.audioChannels = 1;// 设置混流后主播的画面位置TRTCCloudDef.TRTCMixUser broadCaster = new TRTCCloudDef.TRTCMixUser();broadCaster.userId = trtcParams.userId; // 以主播uid为broadcaster为例broadCaster.zOrder = 0;broadCaster.x = 0;broadCaster.y = 0;broadCaster.width = videoWidth;broadCaster.height = videoHeight;config.mixUsers = new ArrayList<>();config.mixUsers.add(broadCaster);// 设置混流后各个小画面的位置int index = 0;for (String userId : mRoomMembers) {TRTCCloudDef.TRTCMixUser audience = new TRTCCloudDef.TRTCMixUser();audience.userId = userId;audience.zOrder = 1 + index;if (index < 3) {// 前三个小画面靠右从下往上铺audience.x = videoWidth - offsetX - subWidth;audience.y = videoHeight - offsetY - index * subHeight - subHeight;audience.width = subWidth;audience.height = subHeight;} else if (index < 6) {// 后三个小画面靠左从下往上铺audience.x = offsetX;audience.y = videoHeight - offsetY - (index - 3) * subHeight - subHeight;audience.width = subWidth;audience.height = subHeight;} else {// 最多只叠加六个小画面}config.mixUsers.add(audience);++index;}trtcCloud.setMixTranscodingConfig(config);}protected String stringToMd5(String string) {if (TextUtils.isEmpty(string)) {return "";}MessageDigest md5 = null;try {md5 = MessageDigest.getInstance("MD5");byte[] bytes = md5.digest(string.getBytes());String result = "";for (byte b : bytes) {String temp = Integer.toHexString(b & 0xff);if (temp.length() == 1) {temp = "0" + temp;}result += temp;}return result;} catch (NoSuchAlgorithmException e) {e.printStackTrace();}return "";}private void startLocalVideo(boolean enable) {TXCloudVideoView localVideoView = mVideoViewLayout.getCloudVideoViewByUseId(trtcParams.userId);if (localVideoView == null) {localVideoView = mVideoViewLayout.getFreeCloudVideoView();}localVideoView.setUserId(trtcParams.userId);localVideoView.setVisibility(View.VISIBLE);if (enable) {// 设置 TRTC SDK 的状态trtcCloud.enableCustomVideoCapture(false);//启动SDK摄像头采集和渲染trtcCloud.startLocalPreview(mCameraFront, localVideoView);} else {trtcCloud.stopLocalPreview();}}
}

有评论区小伙伴要求晒出Constents.java,这里我也把这个类分享出来,Constents类主要是定义一些全局变量

Constents完整源码如下:

public class Constents {/*** 1对1语音通话*/public final static String ONE_TO_ONE_AUDIO_CALL = "1";/*** 1对多语音通话*/public final static String ONE_TO_MULTIPE_AUDIO_CALL = "2";/*** 1对1视频通话*/public final static String ONE_TO_ONE_VIDEO_CALL = "3";/*** 1对多视频通话*/public final static String ONE_TO_MULTIPE_VIDEO_CALL = "4";/*** 实时语音通话消息描述内容*/public final static String AUDIO_CALL_MESSAGE_DESC = "AUDIO_CALL_MESSAGE_DESC";/*** 实时视频通话消息描述内容*/public final static String VIDEO_CALL_MESSAGE_DESC = "VIDEO_CALL_MESSAGE_DESC";/*** 实时语音通话消息拒接*/public final static String AUDIO_CALL_MESSAGE_DECLINE_DESC = "AUDIO_CALL_MESSAGE_DECLINE_DESC";/*** 实时视频通话消息拒接*/public final static String VIDEO_CALL_MESSAGE_DECLINE_DESC = "VIDEO_CALL_MESSAGE_DECLINE_DESC";/*** 悬浮窗与TRTCVideoActivity共享的视频View*/public static TRTCVideoViewLayout mVideoViewLayout;/*** 悬浮窗是否开启*/public static boolean isShowFloatWindow = false;/*** 语音通话开始计时时间(悬浮窗要显示时间在这里记录开始值)*/public static long audioCallStartTime;}

Android基于腾讯云实时音视频实现类似微信视频通话最小化悬浮相关推荐

  1. android修改视频聊天帧率,Android基于腾讯云实时音视频仿微信视频通话最小化悬浮(4)...

    Activity中的操作 现在我们将思路了捋一下,假设现在我正在进行视频通话,点击视频最小化按钮,我们应该按顺序执行如下步骤:应该是会出现个悬浮框.我们用mServiceBound保存Service注 ...

  2. 基于腾讯云实时音视频(TRTC)的web端 多人人脸识别小游戏

    场景介绍 一个双人视频互动的小游戏, 连接后,可以实时看到对方的视频情况, 根据对方的视频情况实时进行游戏操作 摇头进行控制挡板 不让球掉落. 眨眼发球. 准备工作 获取应用 SDKAppID 和 应 ...

  3. 一个基于腾讯云实时音视频的SDKdemo

    哈咯大家好,这里是码农的搬运工!! 这几天也是一时兴起,想到腾讯云的实时音视频的SDK来进行一个真正的视频通话,也是编写了一个小小的demo 话不多说,整活,开干!!! 首先哈,我们评估下腾讯云的这个 ...

  4. 基于腾讯云实时音视频Web API之Demo运行

    曾针对一个视频通话项目需要借助到腾讯云对应的Web API 实现,众所周知,了解其用法最好的途径就是先运行Demo程序,但对于不具备Vue基础的人来说,运行官网Demo是一件困难的事.下面记录一下适合 ...

  5. 腾讯云实时音视频( TRTC)通话质量监控仪表盘

    腾讯云实时音视频(Tencent RTC,简称 TRTC)是一项低延时.高并发.稳定可靠的音视频 PaaS 云服务,主要提供多人实时通话以及低延时互动直播能力.TRTC 将腾讯 21 年来在网络与音视 ...

  6. 腾讯云实时音视频带你玩转语音聊天室

    声音交友,在线K歌,自由上麦,声波传达-- 从最初单一的一对一语音通话,到后来火爆的多人语音群聊,再到现在聚集了一批"音控"的纯语音聊天室,当代年轻人的社交APP里最不可少的功能就 ...

  7. 新知实验室 腾讯云实时音视频产品体验

    新知实验室 腾讯云实时音视频TRTC产品体验 接入腾讯云实时音视频 TRTC基本实现逻辑 创建实时音视频应用 获取APPID 下载含UI的集成方案代码 方案中重要代码结构简析 index.vue co ...

  8. 新知实验室 腾讯云实时音视频 RTC WEB端初识

    这里写目录标题 前言 初识产品 产品介绍 基础功能 高级功能 扩展功能 快速上手 位置 创建 源码下载 源码文档 写入密钥 使用 调试区域 前言 当前时代是信息行业飞速发展的时代,万物都在朝物联网方向 ...

  9. 微信小程序接入腾讯云实时音视频TRTC----基本使用

    前言:虽然说的有点乱,我觉得有必要说一下什么是音视频与整体开发需要些什么东西,因为当时我也是不知道这是啥? .我的理解音视频就是类似微信视频通话的东西,以自己的角度来看,我与好友建立视频的步骤简单来讲 ...

最新文章

  1. 通过form表单请求servlet资源代码
  2. 【Flask项目2】项目基本架构配置(1)
  3. 系统实施基础:系统实施的相关知识介绍
  4. 2-sat模板- 输出可行解
  5. 2022年百度新能源汽车行业洞察
  6. 三层BP神经网络的python实现
  7. 基于JAVA+SpringMVC+Mybatis+MYSQL的校园失物招领系统
  8. eclipse javafx找不到或无法加载主类_JVM类加载
  9. L1正则化与嵌入式特征选择(稀疏性)
  10. 黑马程序员全套Java教程_Java基础入门视频教程零基础自学Java必备教程视频讲义(2)
  11. sketch 导出html,用 Sketch 设计和输出响应式网页
  12. 【深度学习实验报告】实验 1:PyTorch 使用简介
  13. 阅读软件怎么添加书源_【看书听书】两款神器软件,我已深深的为之折服了
  14. AtCoder 神题汇总
  15. Microsoft Remote Procedure Call Runtime 远程代码执行漏洞(CVE-2022-26809)
  16. AAAI 2020 | 清华大学牛人为BabelNet同义词集预测Sememe建立多语言知识库
  17. 【上海赛区】2022数学建模国赛获奖名单公布
  18. react 购物车组件
  19. JS中的call与call.call
  20. [z] 人工智能和图形学、图像处理方面的各种会议的评级

热门文章

  1. python 视频滤镜_Python-OpenCV 处理图像(二):滤镜和图像运算
  2. Ubuntu(Linux) + mono + xsp4 + nginx +asp.net MVC3 部署
  3. 第011篇:易康(eCognition)中用点矢量文件(point.shp)制作样本(samples)的方法
  4. 给服务器弄了一个https协议,HTTP和HTTPS协议
  5. 游戏任务系统的设计要素、理念
  6. 什么是“双机系统”?
  7. hdu5575 Discover Water Tank
  8. vue-cli3部署到阿里云
  9. Android 多张图片转PDF文件
  10. 细说区块链共识机制之pos