官方公告
小程序直播连麦方案现已发布 立即体验
推荐
  • 质量评测指南
    自测点一:视频质量(画质、卡顿、延时)   1. 设备    2台同型号iPhone(主播用,A,B)    2台同型号iPhone或两台同型号安卓(观众用,C,D)   2. 网络   1)A、B连接联通3G   2)D连接Wifi(需保证Wifi连接没有别的干扰)   3)设置A,B的开发者选项的Network Link Conditioner中的上行带宽和上行丢包率(如500kbps+10%) 3. 流程   1)A,B分别开启AgoraLive和竞品主播端,进行直播(尽量保证A,B的前置摄像头面对同一个位置物体);   2)C,D分别开启AgoraLive和竞品观众端,观看A,B端的直播(C,D可以并排放置);   3)A,B端前放置磁悬浮地球仪,C,D端观察运动的流畅性;   4)A,B端前放置色度图或其他颜色丰富的物体,C,D端观察色度的失真;   5)A,B端前放置秒表,C,D端观察延时;   6)A,B端前拍摄人物,C,D端观察总体感受。   自测点二:视频质量(画质、卡顿、延时)   在真实通讯中,我们常常会遇到网络卡顿、通讯不流畅的情形,它所反映的问题很有可能是真实网络中所存在的丢包率高和带宽不足等问题。那么在没有专业的网损环境的情况下,如何快速地模拟测试不同丢包率和不同带宽限制下的音视频通话质量呢?这里我们推荐大家使用iOS自带网损模拟器做简单的丢包和限带宽测试。具体步骤如下: 1.点击“设置”---- 进入后,滑至“开发者”,点击进入 [attach]1174[/attach]    (注:关于iOS上的开发者选项 想要打开这个功能,你需要将iPhone或iPad和一台Mac电脑相连接,然后在Mac上打开Xcode开发工具,此时iPhone的设置里就会出现“开发者”这个选项) 2.点击进入“Status”(默认初始是off)---- 进入后,将菜单顶部的“Enable”打开(默认是关闭的) [attach]1176[/attach] [attach]1178[/attach]      3.点击“Add a profile…”根据自己的测试需求,新建一个测试设置,可在“Name”中新建名称便于标记   [attach]1175[/attach] [attach]1177[/attach] 可以分别设置上行和下行的丢包率和带宽上限 [attach]1180[/attach] [attach]1179[/attach]    5.设置完成之后点击“存储”,之后就可以根据需要进行丢包或带宽限制测试 [attach]1181[/attach] 自测点三:在移动数据网络下,音频质量测试 方法:  不同运营商、地域、 移动数据类型的网络条件相差甚远,而在现实应用中跨运营商,跨地域,跨通讯网络的场景十分普遍。而在上线前,这部分测试有必要进行。 建议在不同的网络运营商(移动、电信、联通)、不同地区之间、连接不同的移动数据网络(2G、3G、4G)通讯等。如果还要考虑到海外用户,全球应用的话。  自测四:在特定机型上(中低端的安卓机,iphone6s外放等)上,音频工作是否正常              
  • Agora SDK demo集锦,总有你需要的
    1. 纯语音通话1对1入门版 功能:加入、离开、静音、切换外放等简单功能。 iOS-Swift  Android   2. 视频通话1对1入门版 功能:加入、离开、切摄像头、静音、开关摄像头等简单功能。 iOS-Swift   iOS-Objective-C  Android  macOS-Swift  macOS-Objective-C  Windows  Web   3. 语音通话进阶版 功能:多人纯语音通话、加入、离开、切换外放、静音、显示SDK log等功能 iOS-Swift  Android   4. 视频通话进阶版 功能:多人视频、加入、离开、切摄像头、切换外放、静音、开关摄像头、屏幕共享(PC)、消息通道、滤镜、视频参数设置、 设备测试(PC)等功能 iOS-Swift  iOS-Objective-C  Android  macOS-Swift  Windows   5. 语音直播进阶版 功能:多人纯语音直播,包括加入、离开、切换外放、静音、观众/主播切换等功能 iOS-Swift  Android   6. 视频直播进阶版 功能:多人视频直播,包括加入、离开、切摄像头、切换外放、静音、开关摄像头、美颜、视频参数设置、观众/主播切换等功能 iOS-Swift  iOS-Objective-C  Android macOS-Swift  Windows 7. 信令入门 功能:使用信令 SDK 做一对一/多对多消息发送 iOS-Swift  Android  Web  macOS-Swift  Linux   8. 信令进阶 功能:登录信令、拨号和呼叫、接听和挂断、静音和解除静音、切换前置摄像头和后置摄像头 https://github.com/AgoraIO/OpenDuo-iOS-Objective-C https://github.com/AgoraIO/OpenDuo-Android https://github.com/AgoraIO/OpenDuo-Web   9. 录制 功能:在 Linux 服务器上启动录制服务 https://github.com/AgoraIO/Agora-LinuxServer-Recording   10. 整理基本的动态加载 so 方式 https://github.com/AgoraIO/Agora-Dynamic-Loading-Sample-App-Android   11. Callkit 功能:使用 CallKit 创建呼叫 / 接听呼叫 / 拒接呼叫 / 结束通话。并在呼叫建立后使用 Agora SDK 进行视频通话。 https://github.com/AgoraIO/Agora-RTC-With-CallKit   12. 视频通话中集成AR 功能:使用 ARKit / ARCore 创建一个虚拟显示屏,显示频道中其他主播的画面。 https://github.com/AgoraIO/Agora-Video-With-ARKit​  https://github.com/AgoraIO/Agora-Video-With-ARCore​  13. 自定义媒体设备 Demo 功能:使用 2.1 SDK 开始的模块化自定义 采集 / 渲染 接口 https://github.com/AgoraIO/Agora-Custom-Media-Device-iOS https://github.com/AgoraIO/Agora-Custom-Media-Device-Android   14. 截图功能 https://github.com/AgoraIO/Agora-Plugin-Raw-Data-API-iOS-Objective-C  https://github.com/AgoraIO/Agora-Plugin-Raw-Data-API-Android-Java   
  • 多人视频开发教程-Android [附源码]
    这篇文章是转自掘金开发者社区,是一个热心开发者基于Agora SDK开发的多人视频应用,以下开始正文:   GitHub地址 https://github.com/uncleleonfan/LaoTieParty   叔想做个直播demo很久了,最近终于得空,做了一个视频群聊Demo,以飨观众。 直播云有很多大厂在做,经老铁介绍,Agora不错,遂入坑。Agora提供多种模式,一个频道可以设置一种模式, 切换方便:   Agora SDK集成 叔专注SDK集成几十年,Agora SDK集成也并没有搞什么事情,大家按照下面步骤上车就行。   1. 导入库文件 将libs文件夹的下的文件导入Android Studio, 最终效果如下: [attach]446[/attach] 2. 添加必要权限   在AndroidManifest.xml中添加如下权限 <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.CAMERA" /> 3. 配置APP ID 在values文件夹下创建strings-config.xml, 配置在官网创建应用的App ID。 <resources> <string name="private_app_id">YOU APP ID</string> </resources> 至此,Agoria SDK集成已经完毕,似不似如丝般顺滑?如果官方能够提供Gradle依赖,还可以省掉下载SDK和导入库文件的步骤,那就更滑了。接下来我们就可以愉快地和SDK玩耍了。   主界面(MainActivity) [attach]453[/attach]   在主界面,我们需要检查先Camera和Audio权限,以适配Andriod6.0及以上版本。 private static final int PERMISSION_REQ_ID_RECORD_AUDIO = 0; private static final int PERMISSION_REQ_ID_CAMERA = 1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //检查Audio权限 if (checkSelfPermission(Manifest.permission.RECORD_AUDIO, PERMISSION_REQ_ID_RECORD_AUDIO)) { //检查Camera权限 checkSelfPermission(Manifest.permission.CAMERA, PERMISSION_REQ_ID_CAMERA); } } public boolean checkSelfPermission(String permission, int requestCode) { if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{permission}, requestCode); return false; } return true; } 频道界面 (ChannelActivity) 点击开PA!,进入频道选择界面 [attach]455[/attach] 创建频道列表 这里使用RecyclerView创建频道列表。 /** * 初始化频道列表 */ private void initRecyclerView() { mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view); mRecyclerView.setHasFixedSize(true); mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); mRecyclerView.setAdapter(new ChannelAdapter(this, mockChannelList())); } 前置摄像头预览 频道界面背景为前置摄像头预览,这个可以使用Android SDK自己实现。但Agora SDK提供了相关API可以直接实现前置摄像头预览的功能。具体实现如下: 1. 初始化RtcEngine RtcEngine是Agora SDK的核心类,叔用一个管理类AgoraManager进行了简单的封装,提供操作RtcEngine的核心功能。 初始化如下: /** * 初始化RtcEngine */ public void init(Context context) { //创建RtcEngine对象, mRtcEventHandler为RtcEngine的回调 mRtcEngine = RtcEngine.create(context, context.getString(R.string.private_app_id), mRtcEventHandler); //开启视频功能 mRtcEngine.enableVideo(); //视频配置,设置为360P mRtcEngine.setVideoProfile(Constants.VIDEO_PROFILE_360P, false); mRtcEngine.setChannelProfile(Constants.CHANNEL_PROFILE_COMMUNICATION);//设置为通信模式(默认) //mRtcEngine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING);设置为直播模式 //mRtcEngine.setChannelProfile(Constants.CHANNEL_PROFILE_GAME);设置为游戏模式 } /** * 在Application类中初始化RtcEngine,注意在AndroidManifest.xml中配置下Application */ public class LaoTieApplication extends Application { @Override public void onCreate() { super.onCreate(); AgoraManager.getInstance().init(getApplicationContext()); } } 2. 设置本地视频​ /** * 设置本地视频,即前置摄像头预览 */ public AgoraManager setupLocalVideo(Context context) { //创建一个SurfaceView用作视频预览 SurfaceView surfaceView = RtcEngine.CreateRendererView(context); //将SurfaceView保存起来在SparseArray中,后续会将其加入界面。key为视频的用户id,这里是本地视频, 默认id是0 mSurfaceViews.put(mLocalUid, surfaceView); //设置本地视频,渲染模式选择VideoCanvas.RENDER_MODE_HIDDEN,如果选其他模式会出现视频不会填充满整个SurfaceView的情况, //具体渲染模式的区别是什么,官方也没有详细的说明 mRtcEngine.setupLocalVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_HIDDEN, mLocalUid)); return this;//返回AgoraManager以作链式调用 } 3. 添加SurfaceView到布局 @Override protected void onResume() { super.onResume(); //先清空容器 mFrameLayout.removeAllViews(); //设置本地前置摄像头预览并启动 AgoraManager.getInstance().setupLocalVideo(getApplicationContext()).startPreview(); //将本地摄像头预览的SurfaceView添加到容器中 mFrameLayout.addView(AgoraManager.getInstance().getLocalSurfaceView()); } 4. 停止预览   /** * 停止预览 */ @Override protected void onPause() { super.onPause(); AgoraManager.getInstance().stopPreview(); } 聊天室 (PartyRoomActivity) 点击频道列表中的选项,跳转到聊天室界面。聊天室界面显示规则是:1个人是全屏,2个人是2分屏,3-4个人是4分屏,5-6个人是6分屏, 4分屏和6分屏模式下,双击一个小窗,窗会变大,其余小窗在底部排列。最多支持六人同时聊天。基于这种需求,叔决定写一个自定义控件PartyRoomLayout来完成。PartyRoomLayout直接继承ViewGroup,根据不同的显示模式来完成孩子的测量和布局。 1人全屏 [attach]454[/attach]   1人全屏其实就是前置摄像头预览效果。 前置摄像头预览   //设置前置摄像头预览并开启 AgoraManager.getInstance() .setupLocalVideo(getApplicationContext()) .startPreview(); //将摄像头预览的SurfaceView加入PartyRoomLayout mPartyRoomLayout.addView(AgoraManager.getInstance().getLocalSurfaceView()); PartyRoomLayout处理1人全屏 /** * 测量一个孩子的情况,孩子的宽高和父容器即PartyRoomLayout一样 */ private void measureOneChild(int widthMeasureSpec, int heightMeasureSpec) { View child = getChildAt(0); child.measure(widthMeasureSpec, heightMeasureSpec); } /** * 布局一个孩子的情况 */ private void layoutOneChild() { View child = getChildAt(0); child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight()); } 加入频道 从频道列表跳转过来后,需要加入到用户所选的频道。 //更新频道的TextView mChannel = (TextView) findViewById(R.id.channel); String channel = getIntent().getStringExtra(“Channel”); mChannel.setText(channel); //在AgoraManager中封装了加入频道的API AgoraManager.getInstance() .setupLocalVideo(getApplicationContext()) .joinChannel(channel)//加入频道 .startPreview(); 挂断 [attach]447[/attach] 当用户点击挂断按钮可以退出频道 mEndCall = (ImageButton) findViewById(R.id.end_call); mEndCall.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //AgoraManager里面封装了挂断的API, 退出频道 AgoraManager.getInstance().leaveChannel(); finish(); } }); 二分屏   [attach]451[/attach] 事件监听器   IRtcEngineEventHandler类里面封装了Agora SDK里面的很多事件回调,在AgoraManager中我们创建了IRtcEngineEventHandler的一个对象mRtcEventHandler,并在创建RtcEngine时传入。   private IRtcEngineEventHandler mRtcEventHandler = new IRtcEngineEventHandler() { /** * 当获取用户uid的远程视频的回调 */ @Override public void onFirstRemoteVideoDecoded(int uid, int width, int height, int elapsed) { if (mOnPartyListener != null) { mOnPartyListener.onGetRemoteVideo(uid); } } /** * 加入频道成功的回调 */ @Override public void onJoinChannelSuccess(String channel, int uid, int elapsed) { if (mOnPartyListener != null) { mOnPartyListener.onJoinChannelSuccess(channel, uid); } } /** * 退出频道 */ @Override public void onLeaveChannel(RtcStats stats) { if (mOnPartyListener != null) { mOnPartyListener.onLeaveChannelSuccess(); } } /** * 用户uid离线时的回调 */ @Override public void onUserOffline(int uid, int reason) { if (mOnPartyListener != null) { mOnPartyListener.onUserOffline(uid); } } }; 同时,我们也提供了一个接口,暴露给AgoraManager外部。 public interface OnPartyListener { void onJoinChannelSuccess(String channel, int uid); void onGetRemoteVideo(int uid); void onLeaveChannelSuccess(); void onUserOffline(int uid); } 在PartyRoomActivity中监听事件​   AgoraManager.getInstance() .setupLocalVideo(getApplicationContext()) .setOnPartyListener(mOnPartyListener)//设置监听 .joinChannel(channel) .startPreview(); 设置远程用户视频 private AgoraManager.OnPartyListener mOnPartyListener = new AgoraManager.OnPartyListener() { /** * 获取远程用户视频的回调 */ @Override public void onGetRemoteVideo(final int uid) { //操作UI,需要切换到主线程 runOnUiThread(new Runnable() { @Override public void run() { //设置远程用户的视频 AgoraManager.getInstance().setupRemoteVideo(PartyRoomActivity.this, uid); //将远程用户视频的SurfaceView添加到PartyRoomLayout中,这会触发PartyRoomLayout重新走一遍绘制流程 mPartyRoomLayout.addView(AgoraManager.getInstance().getSurfaceView(uid)); } }); } }; 测量布局二分屏 当第一次回调onGetRemoteVideo时,说明现在有两个用户了,所以在PartyRoomLayout中需要对二分屏模式进行处理 /** * 二分屏时的测量 */ private void measureTwoChild(int widthMeasureSpec, int heightMeasureSpec) { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); int size = MeasureSpec.getSize(heightMeasureSpec); //孩子高度为父容器高度的一半 int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(size / 2, MeasureSpec.EXACTLY); child.measure(widthMeasureSpec, childHeightMeasureSpec); } } /** * 二分屏模式的布局 */ private void layoutTwoChild() { int left = 0; int top = 0; int right = getMeasuredWidth(); int bottom = getChildAt(0).getMeasuredHeight(); for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); child.layout(left, top, right, bottom); top += child.getMeasuredHeight(); bottom += child.getMeasuredHeight(); } } 用户离线时的处理 当有用户离线时,我们需要移除该用户视频对应的SurfaceView private AgoraManager.OnPartyListener mOnPartyListener = new AgoraManager.OnPartyListener() { @Override public void onUserOffline(final int uid) { runOnUiThread(new Runnable() { @Override public void run() { //从PartyRoomLayout移除远程视频的SurfaceView mPartyRoomLayout.removeView(AgoraManager.getInstance().getSurfaceView(uid)); //清除缓存的SurfaceView AgoraManager.getInstance().removeSurfaceView(uid); } }); } }; 四分屏和六分屏 当有3个或者4个老铁开趴,界面显示成四分屏, 当有5个或者6个老铁开趴,界面切分成六分屏 [attach]452[/attach] [attach]449[/attach] 由于之前已经处理了新进用户就会创建SurfaceView加入PartyRoomLayout的逻辑,所以这里只需要处理四六分屏时的测量和布局 四六分屏测量 private void measureMoreChildSplit(int widthMeasureSpec, int heightMeasureSpec) { //列数为两列,计算行数 int row = getChildCount() / 2; if (getChildCount() % 2 != 0) { row = row + 1; } //根据行数平分高度 int childHeight = MeasureSpec.getSize(heightMeasureSpec) / row; //宽度为父容器PartyRoomLayout的宽度一般,即屏宽的一半 int childWidth = MeasureSpec.getSize(widthMeasureSpec) / 2; for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY); int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } 四六分屏布局 private void layoutMoreChildSplit() { int left = 0; int top = 0; for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); int right = left + child.getMeasuredWidth(); int bottom = top + child.getMeasuredHeight(); child.layout(left, top, right, bottom); if ( (i + 1 )% 2 == 0) {//满足换行条件,更新left和top,布局下一行 left = 0; top += child.getMeasuredHeight(); } else { //不满足换行条件,更新left值,继续布局一行中的下一个孩子 left += child.getMeasuredWidth(); } } } 双击上下分屏布局 [attach]448[/attach] [attach]450[/attach] 在四六分屏模式下,双击一个小窗,窗会变大,其余小窗在底部排列, 成上下分屏模式。实现思路就是监听PartyRoomLayout的触摸时间,当是双击时,则重新布局。 触摸事件处理​ /** * 拦截所有的事件 */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return true; } /** * 让GestureDetector处理触摸事件 */ @Override public boolean onTouchEvent(MotionEvent event) { mGestureDetector.onTouchEvent(event); return true; } //四六分屏模式 private static int DISPLAY_MODE_SPLIT = 0; //上下分屏模式 private static int DISPLAY_MODE_TOP_BOTTOM = 1; //显示模式的变量,默认是四六分屏 private int mDisplayMode = DISPLAY_MODE_SPLIT; //上下分屏时上面View的下标 private int mTopViewIndex = -1; private GestureDetector.SimpleOnGestureListener mOnGestureListener = new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDoubleTap(MotionEvent e) { handleDoubleTap(e);//处理双击事件 return true; } private void handleDoubleTap(MotionEvent e) { //遍历所有的孩子 for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); //获取孩子view的矩形 Rect rect = new Rect(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()); if (rect.contains((int)e.getX(), (int)e.getY())) {//找到双击位置的孩子是谁 if (mTopViewIndex == i) {//如果点击的位置就是上面的view, 则切换成四六分屏模式 mDisplayMode = DISPLAY_MODE_SPLIT; mTopViewIndex = -1;//重置上面view的下标 } else { //切换成上下分屏模式, mTopViewIndex = i;//保存双击位置的下标,即上面View的下标 mDisplayMode = DISPLAY_MODE_TOP_BOTTOM; } requestLayout();//请求重新布局 break; } } } }; 上下分屏测量 处理完双击事件后,切换显示模式,请求重新布局,这时候又会触发测量和布局。 /** * 上下分屏模式的测量 */ private void measureMoreChildTopBottom(int widthMeasureSpec, int heightMeasureSpec) { for (int i = 0; i < getChildCount(); i++) { if (i == mTopViewIndex) { //测量上面View measureTopChild(widthMeasureSpec, heightMeasureSpec); } else { //测量下面View measureBottomChild(i, widthMeasureSpec, heightMeasureSpec); } } } /** * 上下分屏模式时上面View的测量 */ private void measureTopChild(int widthMeasureSpec, int heightMeasureSpec) { int size = MeasureSpec.getSize(heightMeasureSpec); //高度为PartyRoomLayout的一半 int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(size / 2, MeasureSpec.EXACTLY); getChildAt(mTopViewIndex).measure(widthMeasureSpec, childHeightMeasureSpec); } /** * 上下分屏模式时底部View的测量 */ private void measureBottomChild(int i, int widthMeasureSpec, int heightMeasureSpec) { //除去顶部孩子后还剩的孩子个数 int childCountExcludeTop = getChildCount() - 1; //当底部孩子个数小于等于3时 if (childCountExcludeTop <= 3) { //平分孩子宽度 int childWidth = MeasureSpec.getSize(widthMeasureSpec) / childCountExcludeTop; int size = MeasureSpec.getSize(heightMeasureSpec); //高度为PartyRoomLayout的一半 int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(size / 2, MeasureSpec.EXACTLY); int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY); getChildAt(i).measure(childWidthMeasureSpec, childHeightMeasureSpec); } else if (childCountExcludeTop == 4) {//当底部孩子个数为4个时 int childWidth = MeasureSpec.getSize(widthMeasureSpec) / 2;//宽度为PartyRoomLayout的一半 int childHeight = MeasureSpec.getSize(heightMeasureSpec) / 4;//高度为PartyRoomLayout的1/4 int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY); int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY); getChildAt(i).measure(childWidthMeasureSpec, childHeightMeasureSpec); } else {//当底部孩子大于4个时 //计算行的个数 int row = childCountExcludeTop / 3; if (row % 3 != 0) { row ++; } //孩子的宽度为PartyRoomLayout宽度的1/3 int childWidth = MeasureSpec.getSize(widthMeasureSpec) / 3; //底部孩子平分PartyRoomLayout一半的高度 int childHeight = (MeasureSpec.getSize(heightMeasureSpec) / 2) / row; int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY); int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY); getChildAt(i).measure(childWidthMeasureSpec, childHeightMeasureSpec); } } 上下分屏布局​ private void layoutMoreChildTopBottom() { //布局上面View View topView = getChildAt(mTopViewIndex); topView.layout(0, 0, topView.getMeasuredWidth(), topView.getMeasuredHeight()); int left = 0; int top = topView.getMeasuredHeight(); for (int i = 0; i < getChildCount(); i++) { //上面已经布局过上面的View, 这里就跳过 if (i == mTopViewIndex) { continue; } View view = getChildAt(i); int right = left + view.getMeasuredWidth(); int bottom = top + view.getMeasuredHeight(); //布局下面的一个View view.layout(left, top, right, bottom); left = left + view.getMeasuredWidth(); if (left >= getWidth()) {//满足换行条件则换行 left = 0; top += view.getMeasuredHeight(); } } }

同时采集桌面视频和摄像头视频

2 小时前

Windows 关注1 回答0 浏览8  2018-04-22 22:49 • 来自相关话题

unity 打包ios,个别IOS打开视频时出现闪退

10 小时前

Unity 关注1 回答0 浏览14  2018-04-22 15:00 • 来自相关话题

信令调试的时候没问题,杀死重新运行就报so库错误

2 天前

Android 关注1 回答0 浏览20  2018-04-20 13:19 • 来自相关话题

按照文档接入音频通话,进入房间,但是听到的声音太小了 3 天前

iOS/macOS 关注2 回答1 浏览24  2018-04-19 22:48 • 来自相关话题

网络抖动问题 3 天前

Web 网络抖动问题 关注2 回答1 浏览27  2018-04-19 22:46 • 来自相关话题

跑sample异常 android 3 天前

Android 关注2 回答1 浏览29  2018-04-19 22:45 • 来自相关话题

录制 视频正常音频异常,感觉是倍速播放 3 天前

录制 关注2 回答1 浏览19  2018-04-19 22:41 • 来自相关话题

Android 信令demo下载后在Android studio上打开没法编译 3 天前

信令 Android 信令 关注4 回答2 浏览397  2018-04-19 17:38 • 来自相关话题

webview不支持webrtc有什么结解决方案没有 3 天前

Web 混合APPwebview 关注2 回答1 浏览44  2018-04-19 14:20 • 来自相关话题

视频多人连接成功但是F12查看之后发现一直在报错 3 天前

Web 关注2 回答1 浏览37  2018-04-19 10:55 • 来自相关话题