产品文档 直播技术文档 Android 直播 Core SDK

Android 直播 Core SDK

功能介绍

百家云直播Android SDK提供了Core SDK (liveplayer-sdk-core)开源UI (BJLiveUI-Android)

  • UI库基于Core实现,提供了一个针对教育场景下师生互动模板,主要包括师生一对一音视频互动,多人音视频互动,课件展示、文字聊天等功能,可以快速接入,集成工作量小,适合需要快速上线的同学,该库开源。
  • Core为核心库,涵盖了直播间几乎所有的功能,包括音视频推拉流、信令服务器通信、聊天服务器通信、课件展示与画笔绘制等功能,2.0版本新增了支持webRTC音视频方案。

1. 概念

老师 主讲人,拥有直播间最高权限,可以 设置上下课、发公告、处理他人举手、远程开关他人音视频、开关录课、开关聊天禁言
助教 管理员,拥有部分老师的权限,不包含上、下课等改变教室状态的功能
学生 听讲人,权限受限,无法对他人的直播间内容进行管理
教室 直播间,提供创建、管理等一系列功能。提供上课、下课等接口,大多数功能模块只有在上课状态下有效
举手 学生申请发言,老师和管理员可以允许或拒绝
发言 发布音频、视频,SDK 层面发言不要求举手状态
播放 播放他人发布的视频,支持同时播放多个人的视频
录课 云端录制课程
聊天 直播间内的群聊功能,支持发送图片、表情
课件 课件第一页是白板,主要用于添加画笔;老师可上传图片格式的课件,上传成功之后可在直播间内显示;支持 PPT 动画(需要在 PC 端上传)
画笔 老师、助教或发言状态的学生可以在 白板和 PPT 上添加、清除画笔;添加画笔的用户当前的 PPT 页必须与老师保持一致
公告 由老师编辑、发布,可包含跳转链接,即时更新

2.主要功能

模块 功能 接口
教室管理 进入 / 退出教室及相应的事件监听 LiveRoom
异常监听
进入教室的加载状态监听
老师:上课 / 下课
禁言
在线用户信息管理 加载在线用户信息 OnlineUserVM
监听用户进入、退出教室
音视频采集 开启/关闭音视频采集 LPRecorder
音视频采集状态监听
采集设置:视频方向,清晰度,美颜
设置用于音视频的上行链路的类型:UDP/TCP
视频播放 播放、关闭指定用户的视频 LPPlayer
老师:远程开关用户麦克风、摄像头
监听对象音视频开关状态、音视频用户列表变化
设置用于音视频的下行链路的类型:UDP/TCP
举手、发言邀请 学生举手、取消举手,老师处理举手申请 SpeakQueueVM
学生接收、处理发言邀请
课件管理 上传、添加课件,删除课件,课件翻页 DocListVM
加载所有课件 PPTView
监听课件添加、删除
画笔 开启/关闭画笔,清空画板 PPTView
聊天 发送消息(文字、图片、表情) ChatVM
监听收到消息
录课 老师:监听云端录课不可用的通知,获取云端录课状态,开启/停止云端录课 LiveRoom
公告 发布公告 LiveRoom
获取教室公告,监听公告变化
测验 获取历史题目、新题目、答题统计 SurveyVM
学生:答题

引入SDK

1.添加maven仓库

maven { url 'https://raw.github.com/baijia/maven/master/' }

对于部分国内用户,如果github被墙或者访问比较慢,可以使用我们国内的镜像仓库

maven { url 'http://git.baijiashilian.com/open-android/maven/raw/master/' }

2.添加依赖

dependencies {
    implementation 'com.baijiayun.live:liveplayer-sdk-core:2.0.6'
}

3.设置客户专属域名前缀

专属域名从百家云账号中心获取,需要在进入直播和回放之前设置。例如专属域名为 demo123.at.baijiayun.com,则前缀为 demo123,参考 专属域名说明

LiveSDK.customEnvironmentPrefix = "demo123";

API说明

1.要点说明

  • SDK所有的直播功能都是基于教室这个场景的,进入教室成功之后才能正常使用各个功能模块。要进入教室,需要调用LiveSDK.enterRoom,如果成功会回调到LPLaunchListener.onLaunchSuccess(LiveRoom liveRoom),返回的LiveRoom实例可以获取到各个功能对应的ViewModel,后文会对进入教室和各个ViewModel具体说明。

  • RxJava订阅之后在不使用时需要反订阅,例如

// 监听上课
Disposiable disposiable = liveRoom.getObservableOfClassStart().subscribe(consumer);
// 在onDestroy时,需要反订阅
disposiable.dispose();

本文为了简单起见,忽略了反订阅操作。

2.创建、进入教室

进入直播间

LiveSDK.enterRoom目前提供房间号和参加码两种方式进入房间,通过参加码(joinCode)和通过房间号(roomId),接口如下

/**
 * @param context
 * @param roomId     房间号
 * @param userNumber 用户 ID
 * @param userName   用户名
 * @param userType   用户类型 {@link com.baijiahulian.livecore.context.LPConstants.LPUserType}
 * @param userAvatar 用户头像
 * @param sign       请求接口参数签名, 签名由 (roomId, userNumber, userName, userType, userAvatar) 5 个参数生成
 * @param listener   进房间回调
 */
public static void enterRoom(Context context, final long roomId, String userNumber, String userName, LPConstants.LPUserType userType, String userAvatar, String sign, final LPLaunchListener listener)

/**
 * @param context
 * @param roomId     房间号
 * @param groupId    分组号 (分组直播)
 * @param userNumber 用户 ID
 * @param userName   用户名
 * @param userType   用户类型 {@link com.baijiahulian.livecore.context.LPConstants.LPUserType}
 * @param userAvatar 用户头像
 * @param sign       请求接口参数签名, 签名由 (roomId, userNumber, userName, userType, userAvatar) 5 个参数生成
 * @param listener   进房间回调
 */
public static LiveRoom enterRoom(Context context, final long roomId, int groupId, String userNumber, String userName,
                                     LPConstants.LPUserType userType, String userAvatar, String sign, final LPLaunchListener listener)

/**
 * @param context
 * @param joinCode 参加吗
 * @param userName 昵称
 * @param listener 进房间回调
 */
public static void enterRoom(Context context, String joinCode, String userName, final LPLaunchListener listener)

/**
* @param context
* @param joinCode   参加吗
* @param userName   昵称
* @param userType   角色
* @param userAvatar 头像
* @param listener   进房间回调
*/
public static LiveRoom enterRoom(@NonNull final Context context, @NonNull String joinCode, @NonNull String userName,
                                     LPConstants.LPUserType userType, String userAvatar, final LPLaunchListener listener)

Sign原则上由后端计算返给前端,计算规则

进入房间回调说明

LPLaunchListener {
    @Override
    public void onLaunchSteps(int step, int totalStep) {
        //进直播间初始化任务队列回调。因为涉及信令与聊天服务器的连接,进直播间时间可能会比较长,可以根据step/totoalStep实现友好的loading效果
    }

    @Override
    public void onLaunchError(LPError error) {
        //进直播间错误回调
    }

    @Override
    public void onLaunchSuccess(LiveRoom liveRoom) {
        //进入直播间成功,返回LiveRoom对象
    }
};

离开直播间

一般在ActivityonDestroy()中调用

liveRoom.quitRoom();

3.音视频管理

音视频模块从2.0版本开始增加了WebRTC推拉流方案,并且保留了1.x版本中的百家云自研AVSDK方案。两套推拉流方案对集成方是透明的,提供一套统一的API,可以在百家云后台配置无缝切换。为了简化,后文称WebRTC推拉流方案为RTC引擎,百家云自研AVSDK方案为AV引擎(系好安全带吧!)。

发布音视频(推流)

发布音视频主要使用到了LPRecorder。基本方法如下

LPRecorder recorder = liveRoom.getRecorder();
recorder.publish();           // 发布流
recorder.attachAudio();         // 打开音频
recorder.attachVideo();         // 打开视频
recorder.detachAudio();         // 关闭音频
recorder.detachVideo();         // 关闭视频
recorder.stopPublishing();    // 关闭流

例如:发布本地音频时,先调用recorder.publish();然后再调用recorder.attachAudio();即可。 注意:发布视频时需要先设置本地视频采集的preview,然后再调用recorder.attachVideo();

LPCameraView cameraView = new LPCameraView(context);// 也可在布局文件里创建
recorder.setPreview(cameraView);

LPCameraView为本地视频预览,提供了SurfaceView和TextureView两种View进行视频渲染(如果为RTC引擎,目前仅支持SurfaceView)

cameraView.setViewType(LPPConstants.LPVideoViewType.SURFACE_VIEW);
// 或者
cameraView.setViewType(LPConstants.LPVideoViewType.TEXTURE_VIEW);
// 获得当前view类型
LPConstants.LPVideoViewType cameraView.getViewType();

注意:请确保在采集前APP已经获得相应麦克风或者摄像头的权限,recorder.setPreview(cameraView)建议在recorder.publish();之前调用。

除此之外,LPRecorder还提供如下一些方法满足某些使用场景

boolean isVideoAttached();    // 是否正在推视频
boolean isAudioAttached();    // 是否正在推音频
Observable<Boolean> getObservableOfCameraOn();  //摄像头是否开启回调
void switchCamera();           //切换摄像头(如果有)
int getCameraCount();          //获得系统摄像头数量
LPConstants.LPLinkType getLinkType();  //获得上行链路类型(TCP/UDP) (RTC引擎不支持)
boolean setLinkType(LPConstants.LPLinkType linkType);  //设置上行链路类型(RTC引擎不支持)
Observable<LPConstants.LPLinkType> getObservableOfLinkType(); //链路类型改变回调
boolean isPublishing();        //流是否正在上传
void setCaptureVideoDefinition(LPConstants.LPResolutionType definition);//设置分辨率
void openBeautyFilter();       //开启美颜模式(RTC引擎暂不支持)
void closeBeautyFilter();      //关闭美颜模式(RTC引擎暂不支持)
int getPublishIndex();         //上行服务器index(RTC引擎不支持)
LPIpAddress getUpLinkServer(); //获得上行服务器地址(RTC引擎不支持)

注:

1.LPLinkType包含TCP和UDP,TCP即通过CDN推/拉RTMP流,偏远地区可达性较好,但是延时大,适用于老师讲学生听的场景;UDP是走百家云自建的音视频服务器,延时小,适合教室内有老师和学生互动的场景。

2.上行设置分辨率支持流畅(320*240)、高清(640*480)、720P(1280*720)和1080P(1920*1080)四种分辨率。

3.美颜模式仅AV引擎支持,系统版本需要18及其以上。

播放音视频(拉流)

用户(老师/学生)进入教室之后,如果教室内已经有人上麦发言,我们称之为ActiveUser,可以通过如下方法批量获取ActiveUser。

Observable<List<IMediaModel>> obs = liveRoom.getSpeakQueueVM().getObservableOfActiveUsers();
Consumer<List<IMediaModel>> consumer = iMediaModels -> initView();

// 先订阅
obs.subscribe(consumer);
// 再发送ActiveUser请求
liveRoom.getSpeakQueueVM().requestActiveUsers();

至此,已经获取到了教室里所有上麦的用户及其音视频的状态,如果之后有新的用户上麦发言或者ActiveUser中某个用户的音视频状态发生了变化,比如老师关闭了麦克风或者摄像头等,会回调getObservableOfMediaPublish(),例如

liveRoom.getSpeakQueueVM().getObservableOfMediaPublish().observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<IMediaModel>() {
    @Override
    public void accept(IMediaModel iMediaModel) {

    }
});

其中,IMediaModel包含用户信息、音视频状态等。

public interface IMediaModel {
    String getMediaId(); //获取流唯一标识
    boolean isVideoOn(); //是否有视频
    boolean isAudioOn(); //是否有音频
    IUserModel getUser(); //获取对应的User
    boolean isMixedStream(); // 是否是合流
    List<LPConstants.VideoDefinition> getVideoDefinitions(); //获取支持的清晰度列表
    //等等...
}

注:所有接口类型均打进了源码包sources.jar,可以在Android Studio中直接搜索类名看到该类及注释。

当获得这个用户对象的mediaId就能开始拉流,播放其音视频了。

LPPlayer player = liveRoom.getPlayer();
String mediaId = iMediaModel.getMediaId();
player.playAudio(mediaId);   //播放音频
player.playVideo(mediaId, videoView);   //播放音视频
player.playAVClose(mediaId); //关闭音视频流

注意:player.playVideo(mediaId, videoView)中的videoView为显示视频的view

LPVideoView videoView = new LPVideoView(context);// 也可在布局文件里创建

LPVideoView提供了SurfaceView和TextureView两种View进行视频渲染(如果为RTC引擎,目前仅支持SurfaceView)

videoView.setViewType(LPPConstants.LPVideoViewType.SURFACE_VIEW);
// 或者
videoView.setViewType(LPConstants.LPVideoViewType.TEXTURE_VIEW);

此外,LPPlayer还提供如下一些方法满足某些使用场景

void addPlayerListener(LPPlayerListener listener);     //增加播放音视频回调
void removePlayerListener(LPPlayerListener listener);  //移除播放音视频回调
LPConstants.LPLinkType getLinkType();                  //获得下行链路类型  (RTC引擎不支持)
void setLinkType(LPConstants.LPLinkType linkType);     //设置下行链路类型  (RTC引擎不支持)
Observable<LPConstants.LPLinkType> getObservableOfLinkType(); //链路类型改变回调  (RTC引擎不支持)
int getCurrentUdpDownLinkIndex();                      //UDP下行服务器Index  (RTC引擎不支持)
void setCurrentUdpDownLinkIndex(int index);  // (RTC引擎不支持)
LPConstants.MediaSourceType getMediaSourceType();  // 获取媒体内容类型 
List<? extends IMediaModel> getExtraStreams();  // 获取其他流 仅在ActiveUser回调时可能不为空

同一用户多路流

老师在推摄像头视频数据的同时,也可以进行屏幕分享、播放媒体文件、推辅助摄像头,RTC引擎是支持一并观看的,AV引擎会自动替换视频内容。这个时候API稍有不同。

对于RTC引擎,默认屏幕分享、媒体文件会自动替换老师视频,即playVideo传进来的view在播放老师摄像头数据时,以屏幕分享为例,老师进行了屏幕分享,那么这个view会立即拉老师的屏幕分享的流,播屏幕分享,老师结束了屏幕分享即切回摄像头播放,一般能满足大部分场景了,表现同AV引擎一致,无需做任何处理。老师播放媒体文件也是如此。

如果需要同时播放老师的摄像头视频和屏幕分享,需要指定不自动替换老师的流,即

// WebRTC中,是否自动播放老师的屏幕分享和媒体文件并替换调老师的视频
LiveSDK.AUTO_PLAY_SHARING_SCREEN_AND_MEDIA = false;

需要在enterRoom之前设置。

此时,正在播放老师的视频,还是以屏幕分享为例,老师进行了屏幕分享,不会替换掉老师的视频了,此时会回调getObservableOfMediaPublish()方法,通知到UI层有新的流到达,并且user是老师,这样就可以通过playVideo传入新的view播放老师的屏幕分享了。并且可以通过LPConstants.MediaSourceType getMediaSourceType()获取媒体的视频类型。

如果是进教室之前,老师已经在屏幕分享了,那么这个时候进教室请求的ActiveUser中,老师的boolean hasExtraStreams();就为true了,可以通过List<? extends IMediaModel> getExtraStreams();获取到屏幕分享流信息。

最后,老师辅助摄像头始终走不自动替换的逻辑;PC端作为学生时,某些课也可以进行屏幕分享,学生屏幕分享始终走自动替换的逻辑。

4.举手发言

学生请求举手发言相关接口

liveRoom.getSpeakQueueVM().requestSpeakApply(); // 请求举手
liveRoom.getSpeakQueueVM().requestSpeakApply(OnSpeakApplyCountDownListener listener); // 支持举手倒计时回调
liveRoom.getSpeakQueueVM().cancelSpeakApply(); // 取消举手
Observable<IMediaControlModel> getObservableOfSpeakResponse(); // 学生监听老师同意或者拒绝举手

学生请求发言后,会在百家云标准老师端接受到这个事件,老师可以选择同意或者拒绝,同意后学生即可调用LPRecorder的推流方法进行推流,当然如果您的APP没有举手流程,学生也可以直接上麦推流。

学生收到老师远程控制信令

liveRoom.getSpeakQueueVM().getObservableOfMediaControl().observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Consumer<IMediaControlModel>() {
            @Override
            public void accept(IMediaControlModel iMediaControlModel) {
                if (!iMediaControlModel.isApplyAgreed()) {
                    // 老师关闭发言
                }
            }
        });

如果是APP作为老师端,还可以使用以下一些方法

收到学生举手申请回调

Observable<IMediaModel> getObservableOfSpeakApply();

同意/拒绝学生举手

void agreeSpeakApply(String userId);
void disagreeSpeakApply(String userId);

关闭他人发言,仅老师角色可用

liveRoom.getSpeakQueueVM().closeOtherSpeak(userId);

控制学生推音视频状态,仅老师角色可用

void controlRemoteSpeak(String userId, boolean isVideoOn, boolean isAudioOn);

5.在线用户

在线用户模块可以通过liveRoom.getOnlineUserVM获得,其提供了

int getUserCount();
IUserModel getUser(int position);

两个方法,可以方便高效的绑定UI的Adapter。由于服务器压力,房间人数大于100人时,不再广播用户进入和退出,所以提供了一个加载更多用户的接口。(每次加载30个)

liveRoom.getOnlineUserVM().loadMoreUser();

此外,如果不直接绑定adapter,还可以直接监听整个列表变化,用户进入、退出和loadMoreUser()都会触发此回调

liveRoom.getOnlineUserVM().getObservableOfOnlineUser().subscribe(new Consumer<List<IUserModel>>() {
    @Override
    public void accept(List<IUserModel> iUserModels) {
    }
});

用户进入房间(房间人数小于100人时)

liveRoom.getObservableOfUserIn().observeOn(AndroidSchedulers.mainThread()).subscribe(new Consumer<IUserInModel>() {
    @Override
    public void accept(IUserInModel iUserInModel) {        
    }
});

用户退出房间(房间人数小于100人时)

liveRoom.getObservableOfUserOut().observeOn(AndroidSchedulers.mainThread()).subscribe(new Consumer<String>() {
    @Override
    public void accept(String userId) {        
    }
});

房间人数变化(实时)

liveRoom.getObservableOfUserNumberChange().observeOn(AndroidSchedulers.mainThread()).subscribe(new Consumer<Integer>() {
    @Override
    public void accept(Integer integer) {
    }
});

6.课件、画笔

Core SDK提供了一个PPTView来展示PPT课件及画笔,支持多种PPT交互及画笔交互,自适应静态PPT和动态PPT(动态PPT指包含PowerPoint动画效果的课件,使用WebView实现)切换。将复杂的手势交互逻辑及多达12可定制图形绘制封装起来方便集成开发使用。初始化方法如下,

PPTView pptView = new PPTView(context); // 或在布局文件中创建
void attachLiveRoom(liveRoom); 

// 销毁时需要手动调用:
void destroy();

切换PPT在容器中显示全屏\铺满

void setPPTShowWay(LPConstants.LPPPTShowWay.SHOW_FULL_SCREEN); //全屏
void setPPTShowWay(LPConstants.LPPPTShowWay.SHOW_COVERED); //铺满
LPConstants.LPPPTShowWay getPPTShowWay() // 获取显示模式

静态\动态PPT模式切换 (只有老师上传了动态PPT时才可以切换!)

 /**
 * @param animPPTEnable true 切为动态PPT,false切为静态PPT
 * @return true 切换成功, false 切换失败
 */
boolean setAnimPPTEnable(true); 

 /**
 * @return 当前是否为动态PPT
 */
boolean isAnimPPTEnable()

PPTView常用事件监听

void setOnViewTapListener(new OnViewTapListener() {
            @Override
            public void onViewTap(View view, float x, float y) {
            //处理单击事件
            }
        });

void setOnDoubleTapListener(new OnDoubleTapListener() {
            @Override
            public void onDoubleTapConfirmed() {
            //处理双击事件   
            }
        });


void setOnPageSelectedListener(new WhiteboardView.OnPageSelectedListener() {
            @Override
            public void onPageSelected(int position) {
             // 翻页回调;
            }
        });

void setPPTErrorListener(new OnPPTErrorListener() {
            @Override
            public void onAnimPPTLoadError(int errorCode, String description) {
              //动态PPT加载失败监听
            }
        });

PPTView提供3种编辑模式(EditMode,即PPTView处理触摸事件的模式):

  • NormalMode PPT滑动翻页,PPT缩放;

  • ShapeMode 画笔支持绘制多钟图形。

  • SelectMode 点选、框选、移动或缩放画笔;

可以通过如下方法设置,

 /**
 * 设置PPT编辑模式
 * @param pptEditMode
 */
 void setPPTEditMode(LPConstants.PPTEditMode pptEditMode);

 /**
 * 获取PPT编辑模式
 * @return
 */    
 LPConstants.PPTEditMode getPPTEditMode();

NormalMode 翻页及缩放

在此模式下,PPT将触摸事件处理为翻页和所缩放。静态PPT使用viewpager实现滑动翻页,动态PPT可以翻页但暂无滑动效果。如果想要静止滑动翻页可以调用

void setFlingEnable(false);
boolean isFlingEnable(); //是否支持滑动

:学生端主动滑动PPT翻页的逻辑是不大于老师PPT的当前页面;一旦老师翻页了,学生会立即同步到老师的页面。

此外PPT翻页也可以通过API调用实现(即使禁止了滑动翻页也生效)

/**
* 翻到PPT任意页数
*@param index 目的页数
*/
void updatePage(int index);

/**
* 翻到下一页
*/
void gotoNextPage();

/**
* 翻回上一页
*/
void gotoPrevPage();

显示/隐藏默认页码框

void showPPTPageView();
void hidePPTPageView();

其他页码相关API

/**
*@return 当前页数
*/
int getCurrentPageIndex();

/**
* 获取PPT总页数
* @return
*/
int getTotalPageNumber();

/**
*@return 当前页是否是老师/助教所在页(学生端可以调用)
*/
boolean isCurrentMaxPage();

缩放功能静态PPT默认开启,可以通过双击或者手势放大及缩小PPT,动态PPT在暂不支持。

ShapeMode 画笔支持绘制多钟图形

在此模式下,PPT将触摸事件处理成画笔的绘制。画笔绘制提供多达12种画笔类型,其中包括任意曲线(Doodle)、文字(Text)、激光笔(Point)、直线(StraightLine)、单箭头(Arrow)、双箭头(DoubleArrow)、空心矩形(Rect)、实心矩形(RectSolid)、空心椭圆(Oval)、实心椭圆(OvalSolid)、空心等边三角(Triangle)和实心等边三角(TriangleSolid)。一些API接口如下,

/**
* 绘制定制图形
* @param shapeType 
*/
void setCustomShapeType(LPConstants.ShapeType shapeType);

/**
* 设置Doodle画笔线宽   仅绘制Doodle(任意曲线)时生效
* @param strokeWidth
*/
void setShapeStrokeWidth(float strokeWidth);

/**
* 设置定制图形 线宽     绘制除Doodle外其他图形时生效
* @param strokeWidth
*/
void setCustomShapeStrokeWidth(float strokeWidth);

/**
* 设置画笔颜色
* @param paintColor
*/  
void setPaintColor(int paintColor);

/**
* 设置编辑文字大小 (12 px -- 80 px) 仅绘制文字时生效
* @param textSize
*/
void setPaintTextSize(int textSize);

/**
 *  清除当前页面所有画笔
 */ 
void eraseAllShapes();

SelectMode 点选、框选、移动或缩放画笔

在此模式下,PPT将触摸事件处理成画笔的点选和框选。对于选中的画笔可以进行移动、缩放和删除。

 /**
 * 删除选中的shape
 */
 void eraseShapes(); 

7.聊天

发送消息

文字消息

liveRoom.getChatVM().sendMessage(msg);
liveRoom.getChatVM().sendMessage(msg, channel);

发送表情

liveRoom.getChatVM().sendEmojiMessage("[" + emoji.key + "]");

获取配置的表情库

List<IExpressionModel> expressions = liveRoom.getChatVM().getExpressions();

发送图片

String imageContent = LPChatMessageParser.toImageMessage(imageUrl);
liveRoom.getChatVM().sendImageMessage(imageContent, imageWidth, imageHeight);

接收消息

收到新消息

liveRoom.getChatVM().getObservableOfReceiveMessage().subscribe(new Consumer<IMessageModel>() {
    @Override
    public void accept(IMessageModel iMessageModel) {
        String channel = iMessageModel.getChannel();
        String msg = iMessageModel.getFrom().getName() + ":" + iMessageModel.getContent();
    }
});

或者也可以使用

int getMessageCount();
IMessageModel getMessage(int position);

来绑定您列表的adapter,并在liveRoom.getChatVM().getObservableOfNotifyDataChange().subscribe(consumer);更新列表即可

8.录课

云端录制功能只有老师角色可以调用

liveRoom.requestCloudRecord(ture);                          // 开始录制
liveRoom.requestCloudRecord(false);                         // 停止录制
Observable<Boolean> getObservableOfCloudRecordStatus();     // 云端录制状态KVO

9.公告

房间公告支持跳转,如果不需要可以直接传null。 主动获取直播间公告

liveRoom.requestAnnouncement();

设置直播间公告,仅老师角色可用

liveRoom.changeRoomAnnouncement(content, link);

直播间公告变更通知

liveRoom.getObservableOfAnnouncementChange().observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Consumer<IAnnouncementModel>() {
        @Override
        public void accept(IAnnouncementModel iAnnouncementModel) {
            String content = iAnnouncementModel.getContent();
            String url = iAnnouncementModel.getLink();
    }
});

10.测验V1

下为测验V1的API,如果需要使用旧版测验可以联系技术支持在服务器进行配置。

获取历史测验

liveRoom.getSurveyVM().requestPreviousSurvey(liveRoom.getCurrentUser().getNumber())
        .subscribe(new Consumer<IPreviousSurveyModel>() {
    @Override
    public void accept(IPreviousSurveyModel iPreviousSurveyModel) {
        iPreviousSurveyModel.getPreviousSurvey() //历史测验List
        iPreviousSurveyModel.getRightCount()     //当前用户答对几题
        iPreviousSurveyModel.getWrongCount()     //当前用户打错几题
    }
});

收到老师发送新的测验

liveRoom.getSurveyVM().getObservableOfSurveyReceive().observeOn(AndroidSchedulers.mainThread()).subscribe(new Consumer<ISurveyReceiveModel>() {
    @Override
    public void accept(final ISurveyReceiveModel iSurveyReceiveModel) {
        iSurveyReceiveModel.getSurvey()  //新的测验
    }
});

学生发送答案

/**
 * 学生发送答案
 *
 * @param order      题目序号
 * @param userName   学生姓名
 * @param userNumber
 * @param answer     [A, B ,C] 数组元素是 option 下 key
 * @param result     0 正确 1 错误 -1 没有答案(老师没有设置正确答案,所有的option的isAnswer都是false)
 */
liveRoom.getSurveyVM().sendAnswer(int order, String userName, String userNumber, List<String> answer, int result);

服务器答题统计

服务器会10秒汇总一次,如果有答题状态更新的话就广播下发

/**
 * 收到测验统计结果回调
 *
 * @return
 */
Observable<ISurveyStatisticModel> getObservableOfAnswerStatistic();

模型接口说明

ISurveyModel {
    int getOrder();                             //题目序号
    String getQuestion();                       //获取题干
    List<ISurveyOptionModel> getOptionList();   //各个选项
}
ISurveyOptionModel{
    String getKey();                            //获得选项标识 A,B,C \ 1,2,3 ...
    String getValue();                          //获得选项值
    boolean isAnswer();                         //是否是正确答案
    int getUserCount();                         //该选项选择人数
}
ISurveyStatisticModel{
    int getOrder();                              //题目序号
    Map<String, Integer> getResult();            //获得统计结果  key 是 option key, value 是选择的人数
}

可以参考UI实现

11.测验V2

新版测验接口在QuizVM类中,调用liveRoom.getQuizVM

测验v2信令

发布答题
/**
 * 发布答题
 *
 * @param quizId
 * @param forceJoin true:强制答题 false:不强制答题
 */
void requestQuizStart(String quizId, boolean forceJoin);
转发
/**
 * 服务端转发开始答题
 * @return
 * LPJsonModel:
 * {
 *     message_type: "quiz_start",
 *     quiz_id: {string},
 *     force_join: {number} // 0 不强制答题 1 强制答题
 * }
 */
Observable<LPJsonModel> getObservableOfQuizStart();
终止答题
/*
 * 终止答题
 * @param quizId
 */
void requestQuizEnd(String quizId);
转发终止答题
/**
 * 服务端转发终止答题
 * @return
 * LPJsonModel:
 * {
 *     message_type: "quiz_end",
 *     quiz_id: {string}
 * }
 */
Observable<LPJsonModel> getObservableOfQuizEnd();
老师公布答案
/**
 * 老师公布答案
 *
 * @param quizId
 * @param solution 后面参数的solution的map都是如下形式:
 * {
 *     "123": 1,// question_id => solution
 *     "124": [12, 13],
 *     "125": "长江",
 * }
 */
void requestQuizSolution(String quizId, Map<String, Object> solution);
服务器转发答案
/**
 * 服务端转发答案
 * @return
 * LPJsonModel:
 * {
 *     message_type: "quiz_solution",
 *     quiz_id: {string},
 *     solution: {
 *         "123": 1,// question_id => solution
 *         "124": [12, 13],
 *         "125": "长江",
 *     }
 * }
 */
Observable<LPJsonModel> getObservableOfQuizSolution();
请求当前正在答的题
/**
 * 当前正在答的题
 *
 * @return
 */
Observable<LPJsonModel> getObservableOfQuizRes();
当前正在答的题
/**
 * 当前正在答的题
 * @return
 * LPJsonModel:
 * {
 *     message_type: "quiz_res",
 *     quiz_id: {string},   // 如果当前无答题,则quiz_id为空字符串
 *     solution: {
 *         "123": 1,// question_id => solution
 *         "124": [12, 13],
 *         "125": "长江",
 *     }, // 如果曾经提交过,返回之前提交的结果,否则为空
 *     force_join: {number}, // 0 不强制答题 1 强制答题
 *     end_flag: {number}  // 0 未触发结束答题,1 已触发结束答题
 * }
 */
Observable<LPJsonModel> getObservableOfQuizRes();
学生答题
/**
 * 学生答题
 *
 * @param quizId
 * @param solution
 */
void submitQuiz(String quizId, Map<String, Object> solution);
学生答题转发给老师和助教
/**
 * 学生答题发给老师或助教
 *
 * @param quizId
 * @param solution
 */
void submitQuizToSuper(String quizId, Map<String, Object> solution);

测验v2接口

获取试卷列表(老师)
/**
 * 获取试卷列表
 *
 * @return 
 *LPQuizModel中仅有quizId和title
 */
Observable<List<LPQuizModel>> getListQuiz();
新建/更新试卷(老师)
/**
 * 新建/更新试卷
 *
 * @param lpQuizModel 新建quiz_id和question_id和option_id为0
 * @return
 */
LPError saveQuiz(LPQuizModel lpQuizModel);
删除试卷(老师)
/**
 * 删除试卷
 *
 * @param quizId
 * @return
 */
Observable<Boolean> deleteQuiz(long quizId);
获取试卷详情/答题详情(老师)
/**
 * 获取试卷详情/答题详情
 *
 * @param quizId
 * @param type   0试卷详情1答题详情
 * @return
 */
Observable<LPQuizModel> getQuizDetail(long quizId, LPConstants.LPExamQuizType type);
导入试卷(老师)
/**
 * 导入试卷  excel文件
 *
 * @param excelPath
 * @return
 */
Observable<Boolean> importExcel(String excelPath);
获取试卷导出地址(老师)
/**
 * 获取试卷导出地址
 * @param quizId
 * @param type 0 导出试卷 1导出测验结果
 * @return
 */
Observable<String> getExportUrl(long quizId, LPConstants.LPExamQuizType type);
学生接口获取试卷(学生)
/**
 * 学生接口获取试卷
 * @param quizId
 * @return
 */
Observable<LPQuizModel> getQuizInfo(long quizId);
测验广播列表
/**
 * 测验广播列表
 *
 * @return
 */
LPQuizCacheModel getQuizCacheList();
/**
 * 获取测验广播列表
 *
 * @return
 */
Observable<LPQuizCacheModel> getObservableOfQuizCacheList();
大小班小测同步信息请求
/**
 * 大班课小测同步信息(大班切到小班)
 */
void requestRoomQuiz();
大小班小测同步信息响应
/**
 * 大班课小测同步信息
 * @return
 * {
 *    "quiz_id":{string},
 *    "quiz_title":{string}
 * }
 */
Observable<List<LPQuizModel>> getObservableOfRoomQuiz();

12.问答

目前移动端只支持学生端问答,请求历史问答:

/**
* 请求历史问答
* @return LPError {@link LPError#CODE_ERROR_INVALID_ARGUMENT} 没有更多问题信息
*/
LPError loadMoreQuestions();

/**
* 是否有更多问题页数
*
* @return boolean
*/
boolean isHasMoreQuestions();

学生发送问题:

 /**
 * 发送问题
 *
 * @param content 提问问题
 * @return LPError
 * {@link LPError#CODE_ERROR_INVALID_ARGUMENT} 输入内容错误;
 * {@link LPError#CODE_ERROR_QUESTION_SEND_FORBID} 权限错误
 */
 LPError sendQuestion(String content);

问答队列监听

/**
* 问答 返回问答队列
*
* @return List<LPQuestionPullResItem>
*/
Observable<List<LPQuestionPullResItem>> getObservableOfQuestionQueue();

模型接口说明

LPQuestionPullResItem {
    int status;                            // 已发布 1,未发布 2, 已回复 4,未回复 8,全部 15

    boolean forbid;                        // 是否禁止该学生提问

    String id;                             // 问题id

    List<LPQuestionPullListItem> itemList; // 问题和回复列表 (问题和老师追加的回复)
}

LPQuestionPullListItem {
    long time;                            //发布时间戳

    String content;                       //发布内容

    LPUserModel from;                     //发布人信息
}

可以参考UI实现

13.答题器

答题器接口在ToolBoxVM类中,调用liveRoom.getToolBoxVM

发布答题(触发)

/**
 * 发布答题(触发) 一般由老师调用
 *
 * @param lpAnswerModel
 */
void requestAnswerStart(LPAnswerModel lpAnswerModel);

发布答题(响应)

/**
 * 发布答题(响应)
 *
 * @return
 */
Observable<LPAnswerModel> getObservableOfAnswerStart();

停止/撤销答题(触发)

/**
 * 停止/撤销答题(触发)
 * @param isRevoke 是否是撤销
 * @param delay    延时结束
 */
void requestAnswerEnd(boolean isRevoke, long delay);

停止/撤销答题(响应)

/**
 * 停止/撤销答题(响应)
 */
Observable<LPAnswerEndModel> getObservableOfAnswerEnd();

答题数据更新

/**
 * 答题数据更新(如有学生答题即可收到更新)
 */
Observable<LPAnswerModel> getObservableOfAnswerUpdate();

请求历史答题数据(请求)

/**
 * 请求历史答题数据(请求)
 * @param id 传id则返回某次的答题数据,传""则返回本节课所有答题历史数据
 */
void requestAnswerPullReq(String id);

请求历史答题数据(返回)

/**
 * 请求历史答题数据(返回)
 * @return map的key为某次答题的id
 */
Observable<Map<Object, LPAnswerRecordModel>> getObservableOfAnswerPullRes();

14.点赞

助教和老师可以给学生点赞,学生无法点赞,助教和老师不能被点赞,下课清空点赞记录。相关API在LiveRoom中。

/**
 * 获取视频点赞监听
 *
 * @return
 */
Observable<LPInteractionAwardModel> getObservableOfAward();

/**
 * 发送点赞请求
 *
 * @param to     被点赞学生的userNumber
 * @param record 全部点赞集合
 */
void requestAward(String to, HashMap<String, Integer> record);

LPInteractionAwardModel保存了所有历史点赞数据model.value.record为userId做key,点赞数为value的map,以及本次被点赞的学生的userNumber model.value.to

15.点名

点名由老师发起,学生监听并答到。

routerListener.getLiveRoom().setOnRollCallListener(new OnPhoneRollCallListener() {
    @Override
    public void onRollCall(int time, RollCall rollCallListener) {
        // 收到点名

        //学生答到API
        rollCallListener.call();
    }

    @Override
    public void onRollCallTimeOut() {
        // 点名超时
    }
});

可以参考UI实现

16.LiveRoom其他API

上课/下课

liveRoom.requestClassStart();
liveRoom.requestClassEnd();

liveRoom.getObservableOfClassStart().subscribe(consumer);
liveRoom.getObservableOfClassEnd().subscribe(consumer);

一般地,requestClassStart和requestClassEnd均由老师角色调用

获取当前用户

IUserModel currentUser = liveRoom.getCurrentUser();

获取老师用户

IUserModel currentUser = liveRoom.getTeacherUser();

被踢下线(单点登录)

可以监听此回调,ILoginConflictModel会返回冲突的用户在什么终端登录,被踢时也会报LPError

liveRoom.getObservableOfLoginConflict().observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<ILoginConflictModel>() {
    @Override
    public void accept(ILoginConflictModel iLoginConflictModel) {
    }
});

全体禁言

liveRoom.requestForbidAllChat(true);                        // 开启全体禁言
liveRoom.requestForbidAllChat(false);                       // 关闭全体禁言
Observable<Boolean> getObservableOfForbidAllChatStatus();   // 全体禁言状态KVO

单个禁言

单个用户禁言,仅限老师角色

/**
* 禁言(teacher only)
*
* @param forbidUser 禁言用户
* @param duration   禁言时长
*/
liveRoom.forbidChat(IUserModel forbidUser, long duration);

禁言回调(包含其他人被禁言)

liveRoom.getObservableOfForbidChat().subscribe(new Consumer<IForbidChatModel>() {
    @Override
    public void accept(IForbidChatModel iForbidChatModel) {
    }
});

当前用户是否被禁言

liveRoom.getObservableOfIsSelfChatForbid().subscribe(new Consumer<Boolean>() {
    @Override
    public void accept(Boolean isChatForbid) {
    }
})

自定义事件广播接收

liveRoom.getObservableOfBroadcast().observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<LPKVModel>() {
    @Override
    public void accept(LPKVModel lpkvModel) {
        String key = lpkvModel.key;
        String value = lpkvModel.value;
    }
});

获取本地日志

获取AV引擎的日志

liveRoom.getAVLogFilePath();

设置音频输出

LiveSDK.setAudioOutput(LPConstants.VoiceType.VOICE_CALL);    //通话通道输出
LiveSDK.setAudioOutput(LPConstants.VoiceType.VOICE_MEDIA);   //媒体通道输出

注意:需要在进教室之前调用

17.出错回调

liveRoom.setOnLiveRoomListener(new OnLiveRoomListener() {
    @Override
    public void onError(LPError lpError) {
    }
});

注:与服务器断开连接会报lpError.code = CODE_ERROR_ROOMSERVER_LOSE_CONNECTION的错误,这个时候可以检测网络状态,如果有网可以进行断网重连。一般的重连步骤为quitRoom(退出教室),然后重新enterRoom(进入教室)就行了,LiveRoom为新实例,UI资源也需要适时销毁和重新创建。

错误码

public static final int CODE_ERROR_NETWORK_FAILURE = -0x01;//无网
public static final int CODE_ERROR_UNKNOWN = -0x04;// 未知错误
public static final int CODE_ERROR_JSON_PARSE_FAIL = -0x05;// 数据解析失败
public static final int CODE_ERROR_INVALID_PARAMS = -0x06; // 无效参数
public static final int CODE_ERROR_ROOMSERVER_FAILED = -0x07; //roomserver登录失败
public static final int CODE_ERROR_OPEN_AUDIO_RECORD_FAILED = -0x08;//打开麦克风失败,采集声音失败
public static final int CODE_ERROR_OPEN_AUDIO_CAMERA_FAILED = -0x09;//打开摄像头失败,采集图像失败
public static final int CODE_ERROR_MAX_STUDENT = -0x0A;//人数上限
public static final int CODE_ERROR_ROOMSERVER_LOSE_CONNECTION = -0x0B; // roomserver 连接断开
public static final int CODE_ERROR_LOGIN_CONFLICT = -0x0C; // 被踢下线
public static final int CODE_ERROR_PERMISSION_DENY = -0x0D; // 权限错误
public static final int CODE_RECONNECT_SUCCESS = -0x0E; // 重连成功
public static final int CODE_ERROR_STATUS_ERROR = -0x0F; // 状态错误
public static final int CODE_ERROR_MEDIA_SERVER_CONNECT_FAILED = -0x10; //音视频服务器连接错误
public static final int CODE_ERROR_MEDIA_PLAY_FAILED = -0x11; //音视频播放失败
public static final int CODE_ERROR_CHATSERVER_LOSE_CONNECTION = -0x12; // chatserver 连接断开
public static final int CODE_ERROR_MESSAGE_SEND_FORBID = -0x13; //发言被禁止
public static final int CODE_ERROR_VIDEO_PLAY_EXCEED = -0x14; // 超出最大播放视频数量
public static final int CODE_ERROR_LOGIN_KICK_OUT = -0x15; //被踢
public static final int CODE_ERROR_FORBID_RAISE_HAND = -0x16; //举手被禁止
public static final int CODE_ERROR_NEW_SMALL_COURSE = -0x17; // 移动端禁止新小班课
public static final int CODE_ERROR_ENTER_ROOM_FORBIDDEN = -0x18; // 禁止进入教室
public static final int CODE_ERROR_INVALID_SIGNAL_KEY = -0x19; // 错误的自定义信令类型
public static final int CODE_ERROR_INVALID_SIGNAL_VALUE = -0x1A; // 自定义信令内容过长
public static final int CODE_ERROR_SIGNAL_FREQUENCY_TOO_HIGH = -0x1B; // 自定义信令发送频率过高
public static final int CODE_ERROR_INVALID_USER_ROLE = -0x1C; //角色权限错误
public static final int CODE_ERROR_INVALID_ARGUMENT = -0x1D;// 传入参数错误;
public static final int CODE_ERROR_FORBID_AUDIO_DISABLE = -0x1F;//静音功能被禁止;
public static final int CODE_ERROR_MIC_OPEN_FORBID = -0x20;//老师禁止打开麦克风
public static final int CODE_WARNING_PLAYER_LAG = -0x21; //直播卡顿
public static final int CODE_WARNING_PLAYER_MEDIA_SUBSCRIBE_TIME_OUT = -0x22; //media subscribe time out
public static final int CODE_ERROR_WEBRTC_SERVER_DISCONNECTED = -0x23; // WebRTC音视频服务器断开连接

18.混淆规则

//百家云混淆规则
-dontwarn com.baijiahulian.**
-dontwarn com.bjhl.**
-keep public class com.baijiahulian.**{*;}
-keep public class com.bjhl.**{*;}
-keep public class com.baijia.**{*;}
-keep public class com.baijiayun.**{*;}
-keep class org.webrtc.**{*;}

// 点播SDK
-keep class tv.danmaku.ijk.**{*;}

// RxJava混淆规则
-dontwarn sun.misc.**
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
    long producerIndex;
    long consumerIndex;
}
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
    rx.internal.util.atomic.LinkedQueueNode producerNode;
}
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueConsumerNodeRef {
    rx.internal.util.atomic.LinkedQueueNode consumerNode;
}

集成常见问题

  • 是否支持模拟器?
    答:直播暂时不支持x86架构,模拟器的话不能使用音视频模块。
  • 视频窗口之间叠加时,显示不出来?
    答:当视频窗口采用SurfaceView时,会存在SurfaceView叠加的问题,可以采用SurfaceView的setZOrderMediaOverLayer或者setZOrderOnTop来解决,原理请参见官方文档