多人音视频会议


多人音视频采用的是媒体流发布和订阅的技术架构。发布是指参会者发布媒体流(即发言,包括视频流和音频流)到服务器,其他人收到发布事件然后去订阅拉取媒体流。

多人音视频里有管理员,主播和观众三种角色。

  • 管理员拥有最高权限,可以发布媒体流,订阅媒体流,设定其他人是主播还是观众
  • 主播可以发布媒体流,订阅媒体流
  • 观众只有订阅媒体流权限

环信多人音视频有2种比较常见的使用模式:多人音视频会议和多人音视频互动直播。2种模式使用的是相同的技术架构,开发者可以根据业务场景设置不同的参数,主要区别是:

  • 多人音视频会议场景中,在创建会议时指定默认角色为主播,即每个参会者加入会议后都可以发言。开发者也可以根据场景需要,设定一些参会者是以观众角色加入会议。
  • 多人音视频互动直播场景中,在创建会议时指定默认角色为观众,除了管理员(同时也可以是主播)以外,其他人默认是观众。管理员可以设置指定观众成主播实现上麦操作;或设置指定主播为观众,实现下麦操作。

多人音视频会议和多人音视频互动直播模式都支持白板、共享桌面。

注意:会议类型将只支持Communication类型,原Large Communication和Live会议类型将弃用。

  • 多人音视频会议的音频会议支持千人参会者。视频会议支持最多30个主播,支持最多9个主播发布视频;
  • SDK采用模块化设计,极简的 API 设计,方便用户使用单一模块实现特定功能;
  • 适配主流 iOS 设备和 主流 Android 终端设备,保证用户体验一致;
  • 支持手机端和 Web 端互通,极大方便开发者的全平台业务;
  • 视频码率(带宽占用)自适应的,非固定值,会自动根据网络拥塞情况调整清晰度,码率,帧率等。各个分辨率的码率范围如下:
    240p:150k~400kbps,推荐 300kbps
    480p:300k~1Mbps,推荐 500kbps
    720p:900k ~2.5Mbps,推荐 1Mbps
    1080p: 2M ~ 5Mbps,推荐 3Mbps

SDK端创建和操作音视频会议的过程简单来说,可以分为以下几步:

  1. 设置监听
  2. create: 创建会议
  3. join: 加入会议
  4. pub: 发布音视频数据流
  5. sub: 订阅并播放音视频数据流
  6. leave: 离开会议
  • EMConference: 会议实例,保存着与当前会议的相关信息,会议加入成功后会在callback中返回该实例
  • EMConferenceMember: 会议成员实例,保存着该会议成员的相关信息,有成员加入或退出会议时会在callback中返回该实例
  • EMConferenceStream: 会议中的数据流,包含音频数据和视频数据的相关信息,有成员推流或停止推流时会在相应callback中返回该实例
  • EMStreamParam:自己发布的数据流的各种配置,需要在调用发布接口时作为参数传入
  • EMConferenceListener:会议监听类,会回调会议成员变化,会议数据流变化等,需要在代码中设置监听
  • EMConferenceRole:多人会议成员角色
     1. 观众Audience:只能观看收听音视频,即只能订阅流
     2. 主播Speaker:能上传自己的音视频,能观看收听其他主播的音视频,即能发布流和订阅流
     3. 管理员Admin:能创建会议,销毁会议,移除会议成员,切换其他成员的角色,订阅流,发布流
  
     注意:
     >> 每个人必须调用join接口成功后,才算是加入会议(即成为会议成员)。会议成员才允许进行其他操作比如订阅流、发布流等
     >> 成员如果想改变自己角色,必须想办法通知管理员,只有管理员才能修改
  • EMConferenceType:多人会议类型, 目前已经优化,请使用Communication类型。
      1. Communication:普通通信会议,最多支持参会者9人,成员都可以自由说话和发布视频,成员角色Speaker
      2. Large Communication:大型通信会议,最多参会者30人,成员都可以自由说话和发布视频,成员角色Speaker<
      3. Live:互动视频会议,会议里支持最多9个主播和600个观众

以下是从创建会议到离开会议完整的流程讲解:

注册监听

进入会议之前,调用EMConferenceManager#addConferenceListener(EMConferenceListener listener)方法指定回调监听,成员加入或离开会议,数据流更新等 注意:该回调监听中的所有方法运行在子线程中,请勿在其中操作UI

EMConferenceListener listener = new EMConferenceListener() {
    @Override public void onMemberJoined(String username) {
        // 有成员加入
    }
                                                           
    @Override public void onMemberExited(String username) {
        // 有成员离开
    }
                                                               
    @Override public void onStreamAdded(EMConferenceStream stream) {
        // 有流加入
    }
                                                               
    @Override public void onStreamRemoved(EMConferenceStream stream) {
        // 有流移除
    }
                                                               
    @Override public void onStreamUpdate(EMConferenceStream stream) {
        // 有流更新
    }
                                                               
    @Override public void onPassiveLeave(int error, String message) {
        // 被动离开
    }
                                                               
    @Override public void onConferenceState(ConferenceState state) {
        // 聊天室状态回调
    }
                                                               
    @Override public void onStreamSetup(String streamId) {
        // 流操作成功回调
    }
                                                                   
    @Override public void onSpeakers(final List<String> speakers) {
        // 当前说话者回调
    }
                                                                   
    @Override public void onReceiveInvite(String confId, String password, String extension) {
        // 收到会议邀请
        if(easeUI.getTopActivity().getClass().getSimpleName().equals("ConferenceActivity")) {
            return;
        }
        Intent conferenceIntent = new Intent(appContext, ConferenceActivity.class);
        conferenceIntent.putExtra(Constant.EXTRA_CONFERENCE_ID, confId);
        conferenceIntent.putExtra(Constant.EXTRA_CONFERENCE_PASS, password);
        conferenceIntent.putExtra(Constant.EXTRA_CONFERENCE_IS_CREATOR, false);
        conferenceIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        appContext.startActivity(conferenceIntent);
    }
});
    
// 在Activity#onCreate()中添加监听
EMClient.getInstance().conferenceManager().addConferenceListener(conferenceListener);
    
// 在Activity#onDestroy()中移除监听
EMClient.getInstance().conferenceManager().removeConferenceListener(conferenceListener);

用户A创建会议

SDK没有提供单独的创建接口,只提供了一个createAndJoin接口,A调用该接口后,将拥有一个会议实例Conference,同时A将成为该Conference的成员且角色是Admin.

用户创建会议时可以设置参数指定是否支持小程序音视频,是否需要在服务器端录制,录制时是否合并流。

EMClient.getInstance().conferenceManager().createAndJoinConference(emConferenceType,
                password, true, false, false, new EMValueCallBack<EMConference>() {
                    @Override
                    public void onSuccess(EMConference value) {
                        // 返回当前会议对象实例 value
                        // 可进行推流等相关操作
                        // 运行在子线程中,勿直接操作UI
                    }
    
                    @Override
                    public void onError(int error, String errorMsg) {
                        // 运行在子线程中,勿直接操作UI
                    }
                });

用户也可以使用下列接口加入房间。可以指定房间的名称,并且在房间不存在时会自动创建。 另外可以在加入会议时指定角色。

/**
     * \~chinese
     * 加入多人音视频加议房间
     * @param room        会议房间名
     * @param password    会议房间密码
     * @param role    当前用户加入时指定角色 (EMConferenceRole类型)
     * @param callback    回调
     */
    public void joinRoom(final String room ,final String password,final EMConferenceRole role, final EMValueCallBack<EMConference> callback)


    /**
     * \~chinese
     * 加入多人音视频加议房间
     * @param room        会议房间名
     * @param password    会议房间密码
     * @param roletype    当前用户加入时指定角色 (EMConferenceRole类型)
     * @param param       设置会议参数 (EMRoomConfig类型)
     * @param callback    回调函数
     */
    public void joinRoom(final String room ,final String password,final EMConferenceRole roletype ,final EMRoomConfig param, final EMValueCallBack<EMConference> callback)

管理员A邀请其他人加入会议

SDK没有提供邀请接口,你可以自己实现,比如使用环信IM通过发消息邀请,比如通过发邮件邀请等等。 至于需要发送哪些邀请信息,可以参照SDK中的join接口,目前是需要Conference的confrId和password

比如用环信IM发消息邀请

final EMMessage message = EMMessage.createTxtSendMessage("邀请你观看直播", to);
message.setAttribute(Constant.EM_CONFERENCE_ID, conference.getConferenceId());
message.setAttribute(Constant.EM_CONFERENCE_PASSWORD, conference.getPassword());
EMClient.getInstance().chatManager().sendMessage(message);
  注意: 使用环信IM邀请多个人时,建议使用群组消息,如果使用单聊发消息请注意每条消息中间的时间间隔,以防触发环信的垃圾消息防御机制

用户B接收到邀请加入会议

用户B解析出邀请消息中带的confrId和password,调用SDK的join接口加入会议,成为会议成员且角色是Speaker.

EMClient.getInstance().conferenceManager().joinConference(conferenceId, password, new 
    EMValueCallBack<EMConference>() {
        @Override
        public void onSuccess(EMConference value) {
            // 返回当前会议对象实例 value
            // 可进行推流等相关操作
            // 运行在子线程中,勿直接操作UI
        }
    
        @Override
        public void onError(int error, String errorMsg) {
            // 运行在子线程中,勿直接操作UI
        }
    });

用户B成功加入会议后,会议中其他成员会收到回调EMConferenceListener#onMemberJoined(EMConferenceMember member)

@Override
public void onMemberJoined(final EMConferenceMember member) {
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            Toast.makeText(activity, member.memberName + " joined conference!", Toast.LENGTH_SHORT).show();
        }
    });
}

成员A发布音视频流

成员A和成员B都有发布流的权限,可以调用SDK的publish接口发布流,该接口用到了EMStreamParam参数,你可以自由配置,比如是否上传视频,是否上传音频,使用前置或后置摄像头,视频码率,显示视频页面等等

pirvate void pubLocalStream() {
    EMStreamParam param = new EMStreamParam();
    param.setStreamType(EMConferenceStream.StreamType.NORMAL);
    param.setVideoOff(false);
    param.setAudioOff(false);
        
    EMClient.getInstance().conferenceManager().publish(param, new EMValueCallBack<String>() {
        @Override
        public void onSuccess(String streamId) {
        }
        
        @Override
        public void onError(int error, String errorMsg) {
            EMLog.e(TAG, "publish failed: error=" + error + ", msg=" + errorMsg);
        }
    });
}
  
  注意:如果是纯音频会议,只需要在发布数据流时将EMStreamParam中的setVideoOff置为true即可

其他成员收到通知并订阅流

成员A成功发布数据流后,会议中其他成员会收到监听类回调EMConferenceListener#onStreamAdded(EMConferenceStream stream),如果成员B想看成员A的音视频,可以调用subscribe接口进行订阅

@Override
public void onStreamAdded(final EMConferenceStream stream) {
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            ConferenceMemberView memberView = new ConferenceMemberView(activity);
            // 添加当前view到界面
            callConferenceViewGroup.addView(memberView);
            // 设置当前view的一些状态
            memberView.setUsername(stream.getUsername());
            memberView.setStreamId(stream.getStreamId());
            memberView.setAudioOff(stream.isAudioOff());
            memberView.setVideoOff(stream.isVideoOff());
            memberView.setDesktop(stream.getStreamType() == EMConferenceStream.StreamType.DESKTOP);
                
            EMClient.getInstance().conferenceManager().subscribe(stream, memberView.getSurfaceView(), new EMValueCallBack<String>() {
                @Override
                public void onSuccess(String value) {
                }
                
                @Override
                public void onError(int error, String errorMsg) {
                
                }
            });
        }
    });
}

成员A设置自己发布的流

当成员A对自己的数据流做以上操作成功后,会议中的其他成员会收到回调EMConferenceListener#onStreamUpdate(EMConferenceStream stream)

@Override
public void onStreamUpdate(final EMConferenceStream stream) {
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            Toast.makeText(activity, stream.getUsername() + " stream update!", Toast.LENGTH_SHORT).show();
            // 更新当前stream所对应View
            updateConferenceMemberView(stream);
    }
    });
}

成员B取消订阅流

成员B如果不想再看成员A的音视频,可以调用SDK接口unsubscribe

EMClient.getInstance().conferenceManager().unsubscribe(emConferenceStream, new EMValueCallBack<String>() {
    @Override
    public void onSuccess(String value) {
    }
    
    @Override
    public void onError(int error, String errorMsg) {
    
    }
});

成员A取消发布流

成员A可以调用unpublish接口取消自己已经发布的数据流,操作成功后,会议中的其他成员会收到回调EMConferenceListener#onStreamRemoved(EMConferenceStream stream),将对应的数据流信息移除

@Override
public void onStreamRemoved(final EMConferenceStream stream) {
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            Toast.makeText(activity, stream.getUsername() + " stream removed!", Toast.LENGTH_SHORT).show();
            if (streamList.contains(stream)) {
                removeConferenceView(stream);
            }
        }
    });
}

成员B离开会议

成员B调用SDK接口离开会议,会议中的其他成员会收到回调EMConferenceListener#onMemberExited(EMConferenceMember member)

EMClient.getInstance().conferenceManager().exitConference()
  注意:当最后一个成员调用leave接口后,会议会自动销毁
@Override
public void onMemberExited(final EMConferenceMember member) {
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            Toast.makeText(activity, member.memberName + " removed conference!", Toast.LENGTH_SHORT).show();
        }
    });
}

获取会议信息

在会议进行中,可以通过getConferenceInfo 方法来查询会议信息,从而可以拿到主播列表,观众人数等信息。

/**
     * \~chinese
     * 查询会议信息
     *
     * @param confId   会议id
     * @param password 会议密码
     * @param callback 获取结果
     */
    public void getConferenceInfo(final String confId, final String password,
                                          final EMValueCallBack<EMConference> callback)
                                          
    
    /**
     * 获取主播列表
     */
    public String[] getTalkers() 
    /**
     * 获取观众总数
     */                                     
    public int getAudienceTotal()

会议角色管理

会议管理员可以通过下面的接口来更改会议中成员的角色。 其他成员可以通过会议属性或者IM 消息等方式来跟管理员申请。

/**
     * \~chinese
     * 用户角色: Admin > Talker > Audience
     * 当角色升级时,用户需要给管理员发送申请,管理通过该接口改变用户接口.
     * 当角色降级时,用户直接调用该接口即可.
     * 注意: 暂时不支持Admin降级自己
     *
     * @param confId   会议id
     * @param member   {@link EMConferenceMember},目前使用memberName进行的操作
     * @param toRole   目标角色,{@link EMConferenceRole}
     * @param callback 结果回调
     *
     */
    public void grantRole(final String confId, final EMConferenceMember member, final EMConferenceRole toRole, final EMValueCallBack<String> callback)

如果用户需要自己采集特定的数据或者对于数据需要先进行一些处理,可以使用SDK的外部输入数据的方法进行。

例如如果想要使用美颜或者变音等功能,需要用户使用系统的摄像头或者麦克风,然后启动监听系统设备,获取到数据后进行处理,处理后再调用我们输入数据的api发布出去。

外部视频数据的使用: 在EMStreamParam中将使用外部数据打开

//需要初始化camera, 监听camera数据
//处理数据后使用下面的接口将数据输入
streamParam.setUsingExternalSource(true);
EMClient.getInstance().conferenceManager().inputExternalVideoData(bitmap);

外部音频数据的使用:

//初始化AudioRecord
        try {
            audioRecord = new AudioRecord(audioSource, sampleRate, AudioFormat.CHANNEL_IN_MONO,
                    AudioFormat.ENCODING_PCM_16BIT, bufferSizeInBytes);
        } catch (IllegalArgumentException e) {
            Log.d(TAG, "AudioRecord ctor error: " + e.getMessage());
            releaseAudioResources();
            return -1;
        }
        
        //在单独的线程中读取数据
        int bytesRead = audioRecord.read(byteBuffer, byteBuffer.capacity());
        
        //输入处理后的数据
        int ret = 
        EMClient.getInstance().conferenceManager().inputExternalAudioData(byteBuffer.array(), byteBuffer.capacity());

在android 5.0以上系统中,可以使用外部输入视频数据的方法,将采集的桌面图像数据发布出去。 主要方法如下:

desktopParam = new EMStreamParam();
desktopParam.setAudioOff(true);
desktopParam.setVideoOff(true);
desktopParam.setStreamType(EMConferenceStream.StreamType.DESKTOP);

public void publishDesktop() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            desktopParam.setShareView(null);
        } else {
            desktopParam.setShareView(activity.getWindow().getDecorView());
        }
        EMClient.getInstance().conferenceManager().publish(desktopParam, new EMValueCallBack<String>() {
            @Override
            public void onSuccess(String value) {
                conference.setPubStreamId(value, EMConferenceStream.StreamType.DESKTOP);
                startScreenCapture();
            }

            @Override
            public void onError(int error, String errorMsg) {

            }
        });
    }
    
    
    private void startScreenCapture() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            if (ScreenCaptureManager.getInstance().state == ScreenCaptureManager.State.IDLE) {
                ScreenCaptureManager.getInstance().init(activity);
                ScreenCaptureManager.getInstance().setScreenCaptureCallback(new ScreenCaptureManager.ScreenCaptureCallback() {
                    @Override
                    public void onBitmap(Bitmap bitmap) {
                        EMClient.getInstance().conferenceManager().inputExternalVideoData(bitmap);
                    }
                });
            }
        }
    }
   

具体实现可以参考IM demo中的ConferenceActivity.java文件

在Android系统可以将图片资源设置为视频流的水印。首先将图片资源转换为Bitmap对象,然后设置WaterMarkOption中的属性,比如位置,分辨率及距离边缘的margin等。

try {
    InputStream in = this.getResources().getAssets().open("watermark.png");
    watermarkbitmap = BitmapFactory.decodeStream(in);
} catch (Exception e) {
    e.printStackTrace();
}
watermark = new WaterMarkOption(watermarkbitmap, 75, 25, WaterMarkPosition.TOP_RIGHT, 8, 8);
// 开启音频传输
 EMClient.getInstance().conferenceManager().openVoiceTransfer();
 // 关闭音频传输
 EMClient.getInstance().conferenceManager().closeVoiceTransfer();
 // 开启视频传输
 EMClient.getInstance().conferenceManager().openVideoTransfer();
 // 关闭视频传输
 EMClient.getInstance().conferenceManager().closeVideoTransfer();
 
 PS:以上这四个方法都是修改 stream,群里其他成员都会收到 EMConferenceListener.onStreamUpdate()回调
 
 // 切换摄像头
 EMClient.getInstance().conferenceManager().switchCamera();
 // 设置展示本地画面的 view
 EMClient.getInstance().conferenceManager().setLocalSurfaceView(localView);
 // 更新展示本地画面 view
 EMClient.getInstance().conferenceManager().updateLocalSurfaceView(localView);
 // 更新展示远端画面的 view
 EMClient.getInstance().conferenceManager().updateRemoteSurfaceView(streamId, remoteView);
 
 // 开始监听说话者,参数为间隔时间
 EMClient.getInstance().conferenceManager().startMonitorSpeaker(300);
 
 // 停止监听说话者
 EMClient.getInstance().conferenceManager().stopMonitorSpeaker();
 

上一页:1V1实时通话

下一页:多设备