在人际互动的手机APP中,增加语音视频聊天功能是一个常见的需求。而现在,更进一步,在某些场景下,我们需要能将自己的手机屏幕分享给他人,或者是观看他人的手机屏幕。那么,这些常见的功能是如何实现的了?
我分享一个安卓版的Demo供大家参考。
一.功能介绍
1. 视频聊天
(1)每个登录的用户都可向其他任意在线用户发送视频聊天请求。
(2)当收到来自其他在线用户的视频聊天邀请时,可接受或拒绝对方的请求。
(3)当接受其他在线用户的视频聊天邀请时,就启动视频聊天。
2.屏幕分享
(1)每个登录的用户都可向其他任意在线用户发送屏幕分享请求;当对方未响应时,可主动取消屏幕分享请求。
(2)当收到来自其他在线用户请求屏幕分享时,可接受或拒绝对方的请求。
(3)当发送方收到其他在线用户同意屏幕分享时,即可观看其屏幕
(4)被控端和主控端都可主动断开屏幕分享。
二.开发环境
1.开发工具:
Android Studio 4.0
2.开发语言:
JAVA
3.主要框架:
Netty 、OMCS
三.具体实现
类似视频聊天或屏幕分享这样的功能,一般是C/S架构的。在这种应用中,服务端相对简单,其主要是在客户端之间转发消息。本Demo提供了一个非常简易的C#服务端(开发环境:VS 2022),直接运行起来即可。下面我们将主要介绍安卓端的实现。
大家可以从文末下载安卓端的源码,在阅读本文时对照源码,就会更清楚些。
首先,我们先要确定客户端之间相互通信的消息类型。
1.自定义消息类型 InformationTypes
public class InformationTypes { /// <summary> /// 视频请求 0 /// </summary> public static final int VideoRequest = 0; /// <summary> /// 回复视频请求的结果 1 /// </summary> public static final int VideoResult = 1; /// <summary> /// 通知对方 挂断 视频连接 2 /// </summary> public static final int CloseVideo = 2; /// <summary> /// 通知好友 网络原因,导致 视频中断 3 /// </summary> public static final int NetReasonCloseVideo = 3; /// <summary> /// 通知对方(忙线中) 挂断 视频连接 4 /// </summary> public static final int BusyLine = 4; /// <summary> /// 屏幕分享请求 5 /// </summary> public static final int DesktopRequest = 5; /// <summary> /// 回复屏幕分享请求的结果 6 /// </summary> public static final int DesktopResult = 6; /// <summary> /// 主动取消屏幕分享请求 /// </summary> public static final int CancelDesktop = 7; /// <summary> /// 对方(主人端)主动断开屏幕分享 /// </summary> public static final int OwnerCloseDesktop = 8; /// <summary> /// 客人端断开屏幕分享 /// </summary> public static final int GuestCloseDesktop = 9; }
这里我们定义了为了实现第一部分“功能介绍”中的功能,所需要用到的消息类型。
2. 获取安卓系统权限
在安卓上进行视频聊天和屏幕分享,APP需要向安卓系统申请3个权限:麦克风、摄像头、屏幕录制。
(1)获取相机、麦克风、存储权限
private void getPermission() { List<PermissionItem> permissionItems = new ArrayList<PermissionItem>(); permissionItems.add(new PermissionItem(Manifest.permission.CAMERA, "相机", R.drawable.permission_ic_camera)); permissionItems.add(new PermissionItem(Manifest.permission.RECORD_AUDIO, "麦克风", R.drawable.permission_ic_micro_phone)); permissionItems.add(new PermissionItem(Manifest.permission.WRITE_EXTERNAL_STORAGE, "存储", R.drawable.permission_ic_storage)); permissionItems.add(new PermissionItem(Manifest.permission.READ_EXTERNAL_STORAGE, "", 0)); try { HiPermission.create(LoginActivity.this) .title("欢迎访问" + getString(R.string.app_name)) .permissions(permissionItems) .checkMutiPermission(new PermissionCallback() { String TAG = getString(R.string.app_name); @Override public void onClose() { Log.i(TAG, "onClose"); } @Override public void onFinish() { Log.i(TAG, "onFinish"); } @Override public void onDeny(String permission, int position) { Log.i(TAG, "onDeny- permission:" + permission + " position:" + position); } @Override public void onGuarantee(String permission, int position) { Log.i(TAG, "onGuarantee"); } }); } catch (Exception ex) { ex.printStackTrace(); } }
当安卓手机首次进入该Demo时, 将弹窗提示获取设备权限:
注:若禁止了这两个权限,后续就无法进行正常的视频聊天了!
(2)屏幕录制权限
CameraSurfaceView2 myView = null; MultimediaManagerFactory.GetSingleton().getAudioMessageController().dispose(); AndroidUtil.OpenSpeaker(this); try { MultimediaManagerFactory.GetSingleton().openCamera(); } catch (Exception e) { e.printStackTrace(); } this.tv_nick = (TextView) findViewById(R.id.tv_nick); myView = (CameraSurfaceView2) findViewById(R.id.local_surface); myView.setSurfaceEventLister(new CameraSurfaceView2.SurfaceEventLister() { @Override public void surfaceCreated(SurfaceHolder surfaceHolder) { setShowPreviewHolder(surfaceHolder); } }); myView.setZOrderOnTop(true); MultimediaManagerFactory.GetSingleton().setCameraDeviceIndex(1);//设置为前置摄像头 //设置摄像头打开成功回调函数 MultimediaManagerFactory.GetSingleton().setCameraOpenCallBack(this); if (StringHelper.isNullOrEmpty(userId)) { isSender = true; //我向对方发起视频 userId = getIntent().getStringExtra(TalkingID); if (StringHelper.isNullOrEmpty(userId)) { tv_nick.setText("未知requestID"); } else { ll_to_callLayout.setVisibility(View.VISIBLE); coming_callLayout.setVisibility(View.GONE); hangup.setVisibility(View.VISIBLE); MainActivity.getInstance().sendMediaCommunicate(userId, CommunicateType.Request); tv_tips.setText("正在等待对方接受邀请"); } }
4. 回复对方视频请求
当收到对方的视频聊天邀请时,将进入视频预览页面,显示视频邀请。
当点击“接听”或“挂断”按钮时,就会发送视频聊天回复消息:
//接听 answer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { try { MainActivity.getInstance().stopRingForCalling(); coming_callLayout.setVisibility(View.GONE); ll_to_callLayout.setVisibility(View.VISIBLE); openConnector(); MainActivity.getInstance().sendMediaCommunicate(userId, CommunicateType.Agree); } catch (Exception ex) { ex.printStackTrace(); } } }); //拒绝 refuse.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { try { MainActivity.getInstance().sendMediaCommunicate(userId, CommunicateType.Reject); MainActivity.getInstance().stopRingForCalling(); finish(); } catch (Exception ex) { ex.printStackTrace(); } } });
5. 相互连接对方的摄像头、麦克风
当对方回复同意时,自己和对方将相互连接到对方的麦克风和摄像头。
private void openConnector() { try { if (thread2 != null) { thread2.interrupt(); } hangup.setVisibility(View.VISIBLE); switch_camera_layout.setVisibility(View.VISIBLE); ll_top_container.setVisibility(View.INVISIBLE); thread2 = new Thread(new Runnable() { Override public void run() { //在这里关闭不能重新连接 cameraConnector = new CameraConnector(); cameraConnector.setOtherVideoPlayerSurfaceView(otherView); cameraConnector.setConnectorEventListener(new IConnectorEventListener() { @Override public void connectEnded(ConnectResult connectResult) { final String connectFailStr = MainActivity.getConnectFailStr(connectResult); if (!StringHelper.isNullOrEmpty(connectFailStr)) { mHandler.post(new Runnable() { @Override public void run() { tv_camera_failure_cause.setText("摄像头:" + connectFailStr); } }); } boolean isMobilePhone = cameraConnector.getOwnerMachineType() == MachineType.Android || cameraConnector.getOwnerMachineType() == MachineType.IOS; cameraConnector.setVideoUniformScale(true, isMobilePhone); //false 表示小的那边留黑边,true表示裁剪大的那一边 } @Override public void disconnected(ConnectorDisconnectedType connectorDisconnectedType) { } }); cameraConnector.beginConnect(loginID); microphoneConnector = new MicrophoneConnector(); microphoneConnector.setConnectorEventListener(new IConnectorEventListener() { @Override public void connectEnded(final ConnectResult connectResult) { mHandler.post(new Runnable() { @Override public void run() { if (connectResult == ConnectResult.Succeed) { startTimer(SystemClock.elapsedRealtime()); } else { String connectFailStr = MainActivity.getConnectFailStr(connectResult); tv_mic_failure_cause.setText("麦克风:" + connectFailStr); } } }); } @Override public void disconnected(ConnectorDisconnectedType connectorDisconnectedType) { } }); microphoneConnector.beginConnect(loginID); } }); thread2.start(); } catch (Exception ex) { ex.printStackTrace(); } }
当摄像头和麦克风都连接成功后,就可以正常视频聊天了。