今年是不平凡的一年,因为疫情原因 ,大家只能呆着家里,作为一个开发者,严重影响了我正常的学习、生活和工作,在这种情况下,只能宅在家办公,这时候大家就会经常用到线上视频会议,目前很多互联网公司提供这种服务,比较出名的就比如:腾讯会议、钉钉、zoom等,用这些是很方便,但是如果能开发自己的视频会议APP,那会不会更好或者更有成就感呢?下面就简单介绍我这个项目和大概的开发过程。

1.jpeg

图片来源于pexels


本项目基于环信音视频云来完成,实现的主要功能有:


  创建会议、删除会议、获取指定会议室详情、加入会议室、退出会议室等关于会议的管理 ;

  获取会议室参会人名列表、踢人,设置观众为主播,设置主播为观众等关于会议室的人员管理;

  共享桌面(web端);

  三个端的实现:Android,iOS,Web


 上面这些功能在项目中都已经实现。还有水印 ,变声等高级功能在环信音视频SDK的接口内部都已经封装好,本项目没有实现 ,大家可以自行去实现。有关多人音视频功能更详细的介绍大家可以参考:这儿。多人音视频实现的实现主要有以下一些场景:社交交友,远程心理咨询、远程医疗、一对一在线教育、远程视频辅助等。咳咳 ,接下来就是纯干货了,给大家介绍我是如何一步步开发出一个完整的多人音视频app。


项目截图


   首先给大家展示下项目运行的效果图,会议界面 主窗口是一个大的 RelativeLayout ,最下面的那一排排小窗口是的实现方法是HorizontalScrollView加上一个开源的组件 com.jaouan.compoundlayout.RadioLayoutGroup 实现的,点击下面的小窗口后,可以 把小窗口的视频流显示在大屏上,具体是调用 updateRemoteSurfaceView(String streamId, EMCallSurfaceView remoteView)来更新SurfaceView,具体的细节大家可以看看代码里面的实现 最后会公布代码开源地址。




准备工作


    大家得下载安装Android Studio,配置好Android 开发环境,怎么详细配置我就在这不再细说了 网上有很多的教程,大家自己可以找找看,然后大家可以看看环信多人音视频会议的主要功能和一些基本概念介绍。


集成 


  1. 首先大家会想问怎么调用环信的SDK ,大家可以使用 远程依赖SDK包,建议大家用最新版本的远程依赖:


     com.hyphenate:hyphenate-sdk:3.6.6 ,依赖包可以放在 build.gradle里面的 dependencies 选项下面,如下图所示:


2.其次怎么使用环信的appkey ,可以在环信 console 后台注册一个 账号申请appkey ,可以参考这里 ,获取到  appkey 以后添加到AndroidManifest.xml中 ,如下图所示:


3.经过以上两个重要的前期配置准备 ,接下来我们就可以开始进行代码开发了,首先我们先创建一个项目的DemoApplication类和      DemoHelper类,DemoApplication 类和DemoHelper类都是一个单例类 ,DemoApplication 主要功能就是进行DemoHelper      的初始化,而DemoHelper里面主要是主要有一些option 配置和EMClient 进行初始化,代码如下所示:

public void init(Context context) {
                EMOptions options = initChatOptions(context);
		EMClient.getInstance().init(context, options);
		PreferenceManager.init(context);
	}

  DemoHelper还有一个重要的功能就是设置  EMConferenceListener 进行会议监听,有关 EMConferenceListener的类的详细介绍 ,通过这个监听可以再加入会议的时候获取到已经在会议中的流和主播信息,分别是通过其中以下两个回调获取:

@Override 
public void onMemberJoined(EMConferenceMember member){
 
}
 
@Override 
public void onStreamAdded(EMConferenceStream stream){
 
}

4.DemoApplication类完成以后,接下来就是怎么去登陆 环信IM 账号和 创建加入会议房间了,首次安装的时候都没有账号,我们使用的办法是自动注册一个账号 在本地进行保存,然后进行登录 ,注册 登录详细接口请看 这儿,  注册 登录的调用大概如下所示: 

 try {
        //注册一个环信ID
        EMClient.getInstance().createAccount(username, password);
            
        //注册成功进行登录
        PreferenceManager.getInstance().setCurrentUserName(username);
        PreferenceManager.getInstance().setCurrentuserPassword(password);
        login();
    } catch (final HyphenateException e) {
       runOnUiThread(new Runnable() {
               public void run() {
                  int errorCode=e.getErrorCode();
                   if(errorCode==EMError.NETWORK_ERROR){
                    Toast.makeText(getApplicationContext(), getResources().getString(R.string.network_anomalies), Toast.LENGTH_SHORT).show();
      }
   }   
}
 EMClient.getInstance().conferenceManager().joinRoom(currentRoomname, currentPassword, conferenceRole,roomConfig, new EMValueCallBack<EMConference>(){
                    @Override
                    public void onSuccess(EMConference value) {
                        EMLog.i(TAG, "join  conference success");
                        Intent intent = new Intent(MainActivity.this, ConferenceActivity.class);
                        startActivity(intent);
                        finish();
                    }
                    @Override
                    public void onError(final int error, final String errorMsg) {
                        EMLog.e(TAG, "join conference failed error " + error + ", msg " + errorMsg);
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                setBtnEnable(true);
                                if(error == CALL_TALKER_ISFULL) {
                                    takerFullDialogDisplay();
                                }else{
                                    Toast.makeText(getApplicationContext(), "Join conference failed " + error + " " + errorMsg, Toast.LENGTH_SHORT).show();
                                }
                            }
                        });
                    }
                });

登录完成以后,我们可以根据房间名创建并加入房间,主要代码大概如下:

 EMClient.getInstance().conferenceManager().joinRoom(currentRoomname, currentPassword, conferenceRole,roomConfig, new EMValueCallBack<EMConference>(){
                    @Override
                    public void onSuccess(EMConference value) {
                        EMLog.i(TAG, "join  conference success");
                        Intent intent = new Intent(MainActivity.this, ConferenceActivity.class);
                        startActivity(intent);
                        finish();
                    }
                    @Override
                    public void onError(final int error, final String errorMsg) {
                        EMLog.e(TAG, "join conference failed error " + error + ", msg " + errorMsg);
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                setBtnEnable(true);
                                if(error == CALL_TALKER_ISFULL) {
                                    takerFullDialogDisplay();
                                }else{
                                    Toast.makeText(getApplicationContext(), "Join conference failed " + error + " " + errorMsg, Toast.LENGTH_SHORT).show();
                                }
                            }
                        });
                    }
                });

EMClient.getInstance().conferenceManager().joinRoom() API可以根据房间名创建指定会议,当以该房间名命名的会议不存在时候,会直接创建,当会议已经创建好 可以根据正确的房间名和密码加入房间 ,到这一步为止,我们已经成功的创建 并加入会议。


5.加入会议以后我们进入到会议界面,展示从DemoHelper类 EMConferenceListener 中的 onStreamAdded 回调 和 onMemberJoined 获取到的流和主播列表 ,在ConferenceActivity 中实现 EMConferenceListener ,然后直接把 ConferenceActivity 注册监听,用以下方法  EMClient.getInstance().conferenceManager().addConferenceListener(this); 这样就可实现 EMConferenceListener 事件的处理,比如 主播 进出房间 :


public void onMemberJoined(final EMConferenceMember member);


public void onMemberExited(final EMConferenceMember member);


增加流 移除流:


public void onStreamAdded(final EMConferenceStream stream)


 public void onStreamRemoved(final EMConferenceStream stream)


管理员变更: 


public void onAdminAdded(String streamId) ;  


public void onAdminRemoved(String streamId)


角色变更  用户被踢  谁在说话等各种回调,可以处理各种业务逻辑 ,详细的请参考 项目中的实现 ,最后会附上项目的开源地址。


6 进入会议房间以后如果用户角色为主播可以进行发布视频流 ,观众只能订阅视频流 不能发布视频流 ,可以调用SDK的publish接口发布流,该接口用到了EMStreamParam参数,你可以自由配置,比如是否上传视频,是否上传音频,使用前置或后置摄像头,视频码率,显示视频页面等等,具体实现可以参考 中发布 订阅视频流的内容, 关于以上的代码逻辑如以 如以下:

 //发布视频流
 normalParam = new EMStreamParam();
 normalParam.setStreamType(EMConferenceStream.StreamType.NORMAL);
 normalParam.setVideoOff(true);
 normalParam.setAudioOff(true);
 
EMClient.getInstance().conferenceManager().publish(normalParam, new EMValueCallBack<String>() {
            @Override
            public void onSuccess(String value) {
                conference.setPubStreamId(value, EMConferenceStream.StreamType.NORMAL);
                addOrUpdateStreamList("local-stream", value);
          
       PhoneStateManager.get(ConferenceActivity.this).addStateCallback(phoneStateCallback);
            }
 
            @Override
            public void onError(int error, String errorMsg) {
                EMLog.i(TAG, "publish failed: error=" + error + ", msg=" + errorMsg);
            }
        });
 //订阅其他主播的视频流
private void subscribe(EMConferenceStream stream, EMCallSurfaceView surfaceView) {
        EMClient.getInstance().conferenceManager().subscribe(stream, surfaceView, new EMValueCallBack<String>() {
            @Override
            public void onSuccess(String value) {
            }
 
            @Override
            public void onError(int error, String errorMsg) {
 
            }
        });
    }

7.有关上麦 下麦 的逻辑处理,观众可以请求上麦成为主播,主播可以下麦成为观众,上麦 下麦 是利用 EMConferenceAttribute进行处理 ,EMConferenceAttribute  是一个事件广播,广播事件是一个key-value格式,key-value 可以由开发者进行自行定义,增添事件以后 ,服务器会把事件进行广播。会议中成员会收到 onAttributesUpdated回调。例如本项目中的会议上麦 下麦 代码如下所示:

//上麦申请  
 
EMClient.getInstance().conferenceManager().setConferenceAttribute(
    EMClient.getInstance().getCurrentUser(),
    "request_tobe_speaker", 
    new EMValueCallBack<Void>() {
                 @Override
                 public void onSuccess(Void value) {
                        EMLog.i(TAG, "request_tobe_speaker scuessed");
   
                 }
 
                 @Override
                 public void onError(int error, String errorMsg) {
                        EMLog.i(TAG, "request_tobe_speaker failed: error=" + error 
                                   
                 }
             });
 //下麦申请
EMClient.getInstance().conferenceManager().setConferenceAttribute(EMClient.getInstance().getCurrentUser()
                        , "request_tobe_audience", new EMValueCallBack<Void>() {
                            @Override
                            public void onSuccess(Void value) {
                                EMLog.i(TAG, "request_tobe_audience scuessed");
                            }
 
                            @Override
                            public void onError(int error, String errorMsg) {
                                EMLog.i(TAG, "request_tobe_audience failed: error=" + error + ", msg=" + errorMsg);
                            }
                        });

 上麦 下麦 请求发出以后 只能由主持人去处理,处理在 EMConferenceListener  的回调 onAttributesUpdated(EMConferenceAttribute[] attributes) 去处理 ,收到回调以后 解析attributes 然后进行处理请求,处理的过程代码大概如下:

   EMClient.getInstance().conferenceManager().grantRole(conference.getConferenceId()
                        , new EMConferenceMember(memName, null, null,null)
                        , EMConferenceManager.EMConferenceRole.Talker, new EMValueCallBack<String>() {
                            @Override
                            public void onSuccess(String value) {
                                EMLog.i(TAG, " requestTalkerDisplay  request_tobe_speaker changeRole success, result: " + value);
                                dialog.dismiss();
                            }
                            @Override
                            public void onError(int error, String errorMsg) {
                                EMLog.i(TAG, " requestTalkerDisplay  request_tobe_speaker changeRole failed, error: " + error + " - " + errorMsg);
                          
                            }
                        });

下麦也是和上麦一样是利用 EMConferenceAttribute进行处理。


9.有关退出会议 销毁会议 普通主播  观众只能退出会议 ,主持人还可以 销毁会议 正在进行中的会议可以进行销毁,退出会议 销毁会议 具体代码如下:

 EMClient.getInstance().conferenceManager().exitConference(new EMValueCallBack() {
            @Override
            public void onSuccess(Object value) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(getApplicationContext(), "您已成功退出当前会议!", Toast.LENGTH_SHORT).show();
                    }
                });
            }
 
            @Override
            public void onError(int error, String errorMsg) {
                EMLog.i(TAG, "exit conference failed " + error + ", " + errorMsg);
            }
        });
 EMClient.getInstance().conferenceManager().destroyConference(new EMValueCallBack() {
            @Override
            public void onSuccess(Object value) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(getApplicationContext(), "您已成功销毁当前会议!", Toast.LENGTH_SHORT).show();
                    }
                });
                EMLog.i(TAG, "finish ConferenceActivity");
                finish();
            }


尾语


至此整个多人音视频会议开发的详细步骤已经完成 ,虽然比较麻烦 但是每个步骤都很清晰 ,有不太清楚的欢迎大家积极讨论, 附上本项目的github地址:https://github.com/easemob/videocall-android  欢迎大家积极参与 ,谢谢支持。


本人联系方式:727402046@qq.com


Demo下载 二维码如下 欢迎大家体验(iOS版和web版下载地址请见:https://www.easemob.com/download/rtc)