通过参会者事件回调开发者可获取到参会者信息,根据参会者信息可通过平台拉取音视频流。
平台默认返回20路以内的音视频流,如需获取更多,可咨询技术支持人员。
将拉流的数据结合布局策略开发者可自由定义多种布局。
布局的解决方案包含自动布局(autoLayout)及自定义布局(customLayout)。
部分平台只提供自定义布局模式,开发者可具体参考各平台文档详情。
自动布局指封装了参会者位置、请流策略等基础的布局业务逻辑的解决方案,开发者可通过接口快速实现画面布局。
自定义布局适用于开发者需要根据应用场景自定义布局策略的情况,比如设置参会者的位置、设置画面视频分辨率等。
自动布局封装了画廊模式及演讲者模式,同时支持两种布局模式的切换,样式如下图: 同时,通过自定义布局也可以实现以上两种或更多灵活的布局方案。
通过自定义布局实现两种布局模式的实现方案已在图中标注,并可通过下一节内容了解实现详情。其中:
黑色字体的L标识本地画面,P标识参会者画面。
红色字体的Q标识视频质量,P标识优先级。
在XYLink Android SDK v2.24.0之前的版本,参会者视频质量(帧率与分辨率)是由native sdk(libxxx.so)来决策的。 如:一路大屏视频请流质量可为720p 30fps,多路小屏可为180p 7.5fps,这种将布局策略写在底层的方式不够灵活, 上层UI没有决策权,满足不了灵活多变的UI布局,如不同的设备性能,不同尺寸的屏幕,不同级别的带宽,不同的客户需求, 因此从SDKv2.24.0以后将不再支持autolayout(native sdk 自动布局),增加一种所谓customlayout(用户自定义布局)的接口对外,native层暴露给SDK,SDK加工处理后也向APP集成者提供灵活的布局决策API, 方便开发者实现灵活多变的布局方案。
实现customlayout我们只需要实现LayoutBuilder接口的compute方法,在该方法中根据rosterInfo(参会者信息)以及多种条件构造出最终的请流集合,即compute方法的返回结果。(参考demo中SpeakerLayoutBuilder&GalleryLayoutBuilder的实现)
请流后会在onVideoDataSourceChange回调中返回请流数据。接下来就要实现将每一个VideoInfo渲染到屏幕上,即布局。
具体的代码实现可以参考demo中SpeakerVideoGroup、GalleryVideoGroup的实现,核心点就是实现视频View的布局,所以此部分完全可由开发者自行实现。
小鱼SDK对开发者提供的OpenGLTextureView是将VideoInfo转换渲染到屏幕上的关键, 我们主要关注OpenGLTextureView的这几个方法:
这样就可以将远端参会者或者 在最终退出会议时也需要调用releaseLayout方法来释放layout资源。 在前面我们已经提到了每一个参会者对应一个VideoInfo对象,因此每一个参会者的视频窗口的其他元素状态也会来自于VideoInfo。接下来统一了解一下VideoInfo中常用到的字段参数以及可扩展功能。
请流方法:
XyCallActivity.java
public class MyVideoPagerListener extends ViewPager.SimpleOnPageChangeListener {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels);
...
}
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
//切换页面时,请求对应页面的视频流
...
if (position == 0) {
//设置第一视频页请流
NemoSDK.getInstance().setLayoutBuilder(new SpeakerLayoutBuilder());
} else if (position == 1) {
//设置第二视频页请流
NemoSDK.getInstance().setLayoutBuilder(new GalleryLayoutBuilder(1));
} else {
// other pager
NemoSDK.getInstance().setLayoutBuilder(new GalleryLayoutBuilder(position));
}
...
}
}
请流信息生成逻辑:
SpeakerLayoutBuilder.java
//开发者自定义LayoutBuilder
public class SpeakerLayoutBuilder implements LayoutBuilder {
@Override
public List<LayoutElement> compute(LayoutPolicy policy) {
List<LayoutElement> layoutElements = new ArrayList<>();
PostRosterInfo rosterInfo = policy.getRosterInfo(); //得到Roster信息
.... // 结合rosterInfo构建layoutElements
return layoutElements;
}
监听请流回调:
XyCallPresenter.java
NemoSDK.getInstance().setNemoSDKListener(new SimpleNemoSDkListener() {
@Override
public void onVideoDataSourceChange(List<VideoInfo> videoInfos, boolean hasVideoContent) {
//收到VideoInfo列表,接下来布局以及渲染视频
}
});
布局视频View:
SpeakerVideoGroup.java
public class SpeakerVideoGroup extends VideoCellGroup implements WhiteBoardTextureView.WhiteBoardViewListener,
WhiteBoardCell.OnWhiteBoardCellEventListener {
....
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (getWidth() > getHeight()) {
layoutLandscape(l, t, r, b); //横屏layoutView
} else if (getHeight() > getWidth()) {
layoutPortrait(l, t, r, b); //竖屏layoutView
}
.....
}
渲染视频View:
VideoCell.java
public class VideoCell extends ViewGroup implements CellStateView.OnCellStateEventListener {
protected OpenGLTextureView videoView;
private VideoInfo layoutInfo = null;
....
protected void initVideoView() {
videoView = new OpenGLTextureView(getContext(), isUvcCamera);
addView(videoView);
}
...
public void setLayoutInfo(VideoInfo layoutInfo) {
this.layoutInfo = layoutInfo;
if (layoutInfo != null) {
...
videoView.setSourceID(layoutInfo.getDataSourceID()); //设置视频渲染ID
videoView.setContent(layoutInfo.isContent());
....
}
}
public void requestRender() {
if (videoView != null) videoView.requestRender();
}
protected Runnable mRenderRunnabler = new Runnable() {
@Override
public void run() {
requestRender();
}
};
/**
*开始渲染
*/
private void requestRender(boolean isRendering) {
removeCallbacks(mRenderRunnabler);
if (isRendering) {
postDelayed(mRenderRunnabler, 1000 / frameRate);
}
}
释放资源:
XyCallActivity.java
NemoSDK.getInstance().releaseLayout()
全量roster上报:
XyCallActivity.java
@Override
public void onBulkRosterChange(BulkRosterWrapper rosterWrapper) {
if (rosterWrapper.sessionId != 0) {
return;
}
List<BulkRoster> meetingRosterList = meetingViewModel.getMeetingRosterList();
if (meetingRosterList == null) {
meetingRosterList = new ArrayList<>();
}
int rosterType = rosterWrapper.rosterType;
if (BulkRosterType.BULK_ROSTER_TYPE_FULL == rosterType) {
meetingRosterList.clear();
//add
meetingRosterList.addAll(rosterWrapper.addRosters);
} else if (BulkRosterType.BULK_ROSTER_TYPE_INCREMENT == rosterType) {
//add
meetingRosterList.addAll(rosterWrapper.addRosters);
for (String deviceId : rosterWrapper.deleteRosters) {
Iterator<BulkRoster> iterator = meetingRosterList.iterator();
while (iterator.hasNext()) {
BulkRoster cachedRoster = iterator.next();
if (cachedRoster.deviceId.equals(deviceId)) {
//delete
iterator.remove();
}
}
}
for (BulkRoster roster : rosterWrapper.changeRosters) {
for (BulkRoster cachedRoster : meetingRosterList) {
if (cachedRoster.deviceId.equals(roster.deviceId)) {
//update
}
}
}
}
...
}
从3.3.4版本开始支持上报300路roster,如需使用全量roster需要联系小鱼技术支持。