Android集成多人通话


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

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

  注意: 
  创建加入会议有两组api: createAndJoinConference 和 joinRoom. 
  joinConference 是通过会议ID,密码方式加入会议,而joinRoom是通过房间名称加入。
  创建会议成功以后,默认超时时间为三分钟,超过三分钟没有人加入,会议会自动销毁;另外当会议中所有人离开2分钟后,会议也会被销毁。
  joinRoom api在会议不存在会自动去创建。
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
                    }
                });
EMClient.getInstance().conferenceManager().joinConference(confId,
                password,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即可

设置视频流的参数

如果用户想对发布的音频或者视频流的参数进行调整,可以使用EMStreamParam中的参数进行设置。 例如只发布音频流,可以设置关闭视频。

normalParam.setVideoOff(true);

例如,设置视频流的分辨率

normalParam.setVideoHeight(720);
normalParam.setVideoWidth(1280);

另外为保证视频清晰度的话,需要注意最小码率的控制,比如720p,需要保证码率在900k ~2.5Mbps范围内 因此需要设置最小码率为900

normalParam.setMinVideoKbps(900);

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

成员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();
        }
    });
}

成员B销毁会议

成员B调用SDK接口销毁会议,会议中的其他成员会收到回调 EMConferenceListener#onPassiveLeave(final int error, final String message)

EMClient.getInstance().conferenceManager().destoryConference()
  注意:只有管理员角色可以调用这个接口,可以在会议中显式调用这个接口,强制结束进行中的会议,会议中其他人在EMConferenceListener#onPassiveLeave回调里收到 error为 -411,message为 reason-conference-dismissed,收到这个以后调EMClient.getInstance().conferenceManager().exitConference() 主动退出会议。
EMClient.getInstance().conferenceManager().destroyConference(new EMValueCallBack() {
            @Override
            public void onSuccess(Object value) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(activity, "destroy conference succeed!", Toast.LENGTH_SHORT).show();
                    }
                });
            }

            @Override
            public void onError(int error, String errorMsg) {             
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(activity, "destroy conference failed:"+errorMsg, 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
     * 当角色升级时,用户需要给管理员发送申请,管理通过该接口改变用户接口.
     * 当角色降级时,用户直接调用该接口即可.
     *
     * @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)

管理员踢人操作

管理员可以调用kickMember api强制将成员从会议中移除。

  注意:只有管理员角色可以调用踢人接口,可以在会议中踢走主播,被踢的主播在EMConferenceListener#onPassiveLeave回调里收到 error为 -412,message为 reason-beenkicked,收到这个以后调用 EMClient.getInstance().conferenceManager().exitConference() 主动退出会议。
  
/**
     * \~chinese
     * 踢走会议中存在的主播
     * 用户角色: Admin > Talker > Audience
     * 注意: 踢人只允许Admin 去操作,Admin 不能踢自己
     *
     * @param confId   会议id
     * @param members  移除的主播列表
     * @param callback 结果回调
     *
     * \~english
     * Remove talkers from the Conference
     *
     * @param confId   Conference id
     * @param members  Removed list of talkers
     * @param callback Result callback
     */
    public void kickMember(final String confId, final List<String> members, final EMValueCallBack<String> callback)

调用踢人接口,会议中被踢的主播收到回调 EMConferenceListener#onPassiveLeave(final int error, final String message)

全体静音/解除全体静音

管理员可以对会议进行全体静音/解除全体静音设置,设置后,会议中的主播都将处于静音状态,新加入的主播也将自动处于静音状态。只有管理员可以调用此接口。 接口API如下:

/**
     * \~chinese
     * 全体静音 取消全体静音
     * 用户角色: Admin > Talker > Audience
     * 注意:  全体静音只允许Admin 去操作
     *
     * @param confId   会议id
     * @param mute  是否设置静音(ture 为设置全体静音 false 为取消全体静音)
     * @param callback 结果回调
     *
     * \~english
     * All mute cancel all mute
     *
     * @param confId   Conference id
     * @param mute  Whether to set mute (true to set all mute , false to cancel all mute)
     * @param callback Result callback
     */
    public void muteAll(final String confId ,final boolean mute, final EMValueCallBack<String> callback);

管理员调用此接口后,会议中的主播将收到全体静音状态的回调,回调函数如下

/**
     * \~chinese
     * 被全体静音 取消全体静音通知
     *
     * \~english
     * Be or cancel all muted  notification
     */
    default void onMuteAll(boolean mute){};

指定成员静音/解除静音

管理员可以对会议中的指定成员进行静音/解除静音设置,被指定成员可以是主播也可以是管理员。设置后,被指定成员将静音/解除静音。只有管理员可以调用此接口。 接口API如下:

/**
     * \~chinese
     * 发送静音命令
     * 注意: 只允许Admin 去操作
     *
     * @param memberId  memberId  被静音的成员的memberId;
     *
     * \~english
     * Send the mute command
     *
     * @param memberId  MemberId of the member being mute (only admin can sends mute command);
     *
     * */
    public void muteMember(String memberId);
/**
     * \~chinese
     * 发送解除静音命令
     * 注意: 只允许Admin 去操作
     *
     * @param memberId  被取消静音的成员的memberId;
     *
     * \~english
     * Send the unmute command
     *
     * @param memberId  Send the memberId of the member whose unmute command is cancelled;
     *
     *
     * */
    public void unmuteMember(String memberId);

管理员调用此接口后,被指定的成员将收到静音状态的回调,回调函数如下

/**
     * \~chinese
     * 被静音通知
     *
     * \~english
     * Be muted notification
     */
    default void onMute(String adminId, String memId){};
/**
     * \~chinese
     * 被取消静音通知
     *
     * \~english
     * Be unmuted notification
     */
    default void onUnMute(String adminId, String memId){};

观众申请主播

会议中的观众角色可以向管理员发申请成为主播,管理员可以选择同意或者拒绝。观众申请主持人的接口需要管理员的memId,先通过获取会议属性接口获取到管理员的memName,然后根据memName以及成员加入的回调中获取到的EMConferenceMember,获取到memId。接口如下

/**
     * \~chinese
     * 发送上麦请求
     *
     * @param memberId 管理员的memberId(只有管理员可处理上麦请求);
     *
     * \~english
     * Request to be speaker
     *
     * @param  memberId of the admin (only the admin can process the request on the mic);
     */
    public void applyTobeSpeaker(String memberId);

观众发出申请后,管理员将会收到以下回调:

/**
     * \~chinese
     * 请求上麦通知 (只有管理员能收到)
     *
     * \~english
     * Request On wheat notification
     */
    default void onReqSpeaker(String memId,String memName,String nickName);

在回调中,管理员可以选择同意或者拒绝,如果同意,需要调用改变用户权限的接口,然后调用回复接口,如果拒绝,则直接调用回复接口。回复接口如下:

/**
     * \~chinese
     * 管理员处理 上麦请求
     * 注意:只允许Admin 去操作
     *
     * @param memberId  请求者 memberId;
     * @param accept   true 代表同意  false 代表不同意
     *
     * \~english
     * Admin handle to be speak requests
     *
     * @param memberId  requestor memberId;
     * @param accept   true means agree , false means disagree
     * */
    public void handleSpeakerApplication(String memberId, boolean accept);

主播申请管理员

会议中的主播角色可以向管理员发申请成为管理员,管理员可以选择同意或者拒绝,成为管理员后,各管理员之间的权限是相同的。主播申请主持人的接口需要管理员的memId,先通过获取会议属性接口获取到管理员的memName,然后根据memName以及成员加入的回调中获取到的EMConferenceMember,获取到管理员memId。接口如下:

/**
     * \~chinese
     * 发送申请管理员请求
     *
     * @param memberId 管理员的memberId;
     *
     * \~english
     * Request to be admin
     *
     * @param memberId of the admin;
     */
    public void applyTobeAdmin(String memberId);

主播发出申请后,管理员将会收到以下回调:

/**
     * \~chinese
     * 请求成为管理员通知(只有管理员能收到)
     *
     * \~english
     * Request administrator notification
     */
    default void onReqAdmin(String memId,String memName,String nickName){}

在回调中,管理员可以选择同意或者拒绝,如果同意,需要调用改变用户权限的接口,然后调用回复接口,如果拒绝,则直接调用回复接口。回复接口如下:

/**
     * \~chinese
     * 管理员处理 申请管理员请求
     * 注意: 只允许Admin 去操作
     *
     * @param memberId  请求者 memberId;
     * @param accept   true 代表同意  false 代表不同意
     *
     * \~english
     * Admin handle to be admin requests
     *
     * @param memberId  requestor memberId;
     * @param accept   true means agree false means disagree
     * */
    public void handleAdminApplication(String memberId, boolean accept);

如果用户需要自己采集特定的数据或者对于数据需要先进行一些处理,可以使用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);

多人音视频支持不同集群区域的人员使用代理,减小延迟、丢包率。使用多集群代理需要音视频后台配置IP及端口的映射文件rtcconfig.json,sdk开启相应开关。 启用多集群代理功能开关如下,该方法在EMOptions类中,具体如下:

public void setUseRtcConfig(boolean useRtcConfig);//true为开启,false为不开启

多人音视频支持将会议中的音视频流合并成一个流,推送到第三方的cdn直播服务器。整个合流推流过程包括开启cdn推流,更新推流布局,停止推流。

开启cdn推流

会议的创建者在创建会议时使用EMRoomConfig的接口,可以决定是否开启cdn推流,推流配置EMLiveConfig是EMRoomConfig的一个参数,可设置cdn推流的相关信息。开启过程如下:

EMCDNCanvas canvas = new EMCDNCanvas(ConferenceInfo.CanvasWidth, ConferenceInfo.CanvasHeight, 0,30,900,"H264");
 String url = PreferenceManager.getInstance().getCDNUrl();
 EMLiveConfig liveConfig = new EMLiveConfig(url, canvas);
 roomConfig.setLiveConfig(liveConfig);

EMLiveConfig可设置的参数如下:

/**
 *  \~chinese
 *  CDN 画布设置,创建会议时使用
 *  width (画布 宽)
 *  height(画布 高)
 * 	bgclr (画布 背景色 ,格式为 RGB 定义下的 Hex 值,不要带 # 号,
 * 	       如 0xFFB6C1 表示浅粉色。默认0x000000,黑色)
 *  fps (推流帧率,可设置范围10-30 )
 *  kpbs (推流码率,单位kbps,width和height较大时,码率需要提高,可设置范围1-5000)
 *  codec (推流编码格式,目前只支持"H264")
 *
 *  \~english
 *  The cdn canvas config
 *  width (Canvas width)
 *  height(Canvas height)
 *  bgclr (Canvas background color, Hex value defined by RGB, no #,
 *         such as 0xFFB6C1 means light pink.Default 0x000000, black))
 *  fps (The fps of cdn live,valid valuei is 10-30)
 *  kpbs (The birateBps of cdn live,the unit is kbps,valid value is 1-5000 )
 *  codec (The codec of cdn live,now only support H264)
 */
public class EMCDNCanvas {
    private  int  width = -1;
    private  int  height = -1;
    private  int  bgclr = 0;
    private  int  fps = -1;
    private  int  kpbs = -1;
    private  String  codec = null;

    public EMCDNCanvas(){

    }
    public EMCDNCanvas(int width, int height, int bgclr,int fps,int kpbs,String codec){
        this.width = width;
        this.height = height;
        this.bgclr = bgclr;
        this.fps = fps;
        this.kpbs = kpbs;
        this.codec  = codec;
    } 
}

/**
 *  \~Chinese
 *  CDN推流设置
 *  cdnurl     cdn推流地址
 *  cdnCanvas  画布设置 (cdnCanvas可以缺省)
 *  liveLayoutStyle
 *
 *  \~English
 *  The CDN push stream config
 *  cdnurl     cdn push stream address
 *  cdnCanvas  canvas settings (cdnCanvas can be default)
 */
public class EMLiveConfig {
    private String cdnurl;
    private EMCDNCanvas cdnCanvas = null;
    public EMLiveConfig(){
    }
    public EMLiveConfig(String cdnurl, EMCDNCanvas cdnCanvas){
        this.cdnurl = cdnurl;
        this.cdnCanvas = cdnCanvas;
    }
}

更新布局

当用户调用更新布局接口后,cdn推流方式将强制变成CUSTOM模式,所有流的位置信息都由用户自己定义。 更新布局的接口如下:

/**
     * \~chinese
     * CDN 推流更新布局(只有管理员可操作)
     * 用户角色: Admin > Talker > Audience
     * 注意:  更新布局只允许Admin 去操作
     *
     * @param regions EMCanvasRegion布局对象列表
     * @param callback 结果回调
     *
     * \~english
     * CDN pushes to update the layout
     *
     * @param regions Layout EMCanvasRegion list
     * @param callback Result callback
     */
    public void updateLiveLayout(List<EMLiveRegion> regions , final EMValueCallBack<String> callback)

EMLiveRegion的结构如下:

/**
 *  \~Chinese
 *  视频流在画布宽高及显示位置等参数
 *  x         在画布横坐标位置
 *  y         在画布纵坐标位置
 *  width     视频流宽度(64~1280)
 *  height    视频流高度(64~1280)
 *  zorder    视频流zorder层位置(最小值为 0(默认值),表示该区域图像位于最下层
 *                              最大值为 100,表示该区域图像位于最上层)
 *  style     视频流显示方式(fit模式或者fill模式)
 *  streamId  视频流ID
 *
 *
 *  \~English
 *  Video streaming in the canvas width and height and display location and other parameters
 *  x         On the horizontal position of the canvas
 *  y         On the vertical position of the canvas
 *  width     Video stream width
 *  height    Video stream height
 *  zorder    Video stream zorder layer location
 *  style     Video stream display mode (fit mode or fill mode)
 *  streamId  The video stream ID
 */
public class EMLiveRegion {
    private int x;
    private int y;
    private int width;
    private int height;
    private int zorder;
    private EMRegionStyle style;
    private String streamId;

    public EMLiveRegion(){}
    public EMLiveRegion(int x,int y, int width, int height, int zorder, EMRegionStyle style, String streamId){
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
        this.zorder = zorder;
        this.style = style;
        this.streamId = streamId;
    }  
    public enum EMRegionStyle {
        FIT, // fit模式
        FILL // fill模式
    }
}

使用方法如下:

List<EMLiveRegion> regionsList = new LinkedList<>();
EMLiveRegion region = new EMLiveRegion();
region.setStreamId(streamInfo.getStreamId());           
region.setStyle(EMLiveRegion.EMRegionStyle.FILL);
region.setX(80);
region.setY(60);
region.setWidth(320);
region.setHeight(640);
region.setZorder(1);
regionsList.add(region);
EMClient.getInstance().conferenceManager().updateLiveLayout(regionsList,new EMValueCallBack<String>();

停止推流

多人音视频支持停止向某一个地址的推流,停止推流接口如下:

/**
     * \~chinese
     * 停止CDN推流
     * @param callback 结果回调
     *
     * \~english
     * Stop the CDN push
     *
     * @param callback Result callback
     */
    public void stopLiveStream(final EMValueCallBack<String> callback)
// 开启音频传输
 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();

 // 开启数据统计,用于debug视频过程中的具体参数,开启后会在onStreamStatistics回调中读取到
 // 各项数据参数,具体参见EMStreamStatistics。
 EMClient.getInstance().conferenceManager().enableStatistics(true);
 
 public void onStreamStatistics(EMStreamStatistics statistics) {
        EMLog.i(TAG, "onStreamStatistics" + statistics.toString());
        debugPanelView.onStreamStatisticsChange(statistics);
    }