资讯专栏INFORMATION COLUMN

使用 Agora SDK 实现视频对话应用 HouseParty-附 Android 源码

CocoaChina / 2686人阅读

摘要:叔想做个直播很久了,最近终于得空,做了一个视频群聊,以飨观众。主界面在主界面,我们需要检查先和权限,以适配及以上版本。但提供了相关可以直接实现前置摄像头预览的功能。最多支持六人同时聊天。直接继承,根据不同的显示模式来完成孩子的测量和布局。

叔想做个直播demo很久了,最近终于得空,做了一个视频群聊Demo,以飨观众。 直播云有很多大厂在做,经老铁介绍,Agora不错,遂入坑。Agora提供多种模式,一个频道可以设置一种模式。

Agora SDK集成

叔专注SDK集成几十年,Agora SDK集成也并没有搞什么事情,大家按照下面步骤上车就行。

1.注册

登录官网,注册个人账号,这个叔就不介绍了。

2.创建应用

注册账号登录后,进入后台,找到“添加新项目”按钮,点击创建新项目,创建好后就会获取到一个App ID, 做过SDK集成的老铁们都知道这是干啥用的。

3.下载SDK

进入官方下载界面, 这里我们选择视频通话 + 直播 SDK中的Android版本下载。下载后解压之后又两个文件夹,分别是libs和samples, libs文件夹存放的是库文件,samples是官方Demo源码,大叔曾说过欲练此SDK,必先跑Sample, 有兴趣的同学可以跑跑。

4.集成SDK

1. 导入库文件

将libs文件夹的下的文件导入Android Studio, 最终效果如下:

2.添加必要权限

在AndroidManifest.xml中添加如下权限

</>复制代码

3.配置APP ID

在values文件夹下创建strings-config.xml, 配置在官网创建应用的App ID。

</>复制代码

  1. 6ffa586315ed49e6a8cdff064ad8a0b0
主界面(MainActivity)

在主界面,我们需要检查先Camera和Audio权限,以适配Andriod6.0及以上版本。

</>复制代码

  1. private static final int PERMISSION_REQ_ID_RECORD_AUDIO = 0;
  2. private static final int PERMISSION_REQ_ID_CAMERA = 1;
  3. @Override
  4. protected void onCreate(Bundle savedInstanceState) {
  5. super.onCreate(savedInstanceState);
  6. setContentView(R.layout.activity_main);
  7. //检查Audio权限
  8. if (checkSelfPermission(Manifest.permission.RECORD_AUDIO, PERMISSION_REQ_ID_RECORD_AUDIO)) {
  9. //检查Camera权限
  10. checkSelfPermission(Manifest.permission.CAMERA, PERMISSION_REQ_ID_CAMERA);
  11. }
  12. }
  13. public boolean checkSelfPermission(String permission, int requestCode) {
  14. if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
  15. ActivityCompat.requestPermissions(this, new String[]{permission}, requestCode);
  16. return false;
  17. }
  18. return true;
  19. }
频道界面 (ChannelActivity)

点击开PA!,进入频道选择界面

创建频道列表

这里使用RecyclerView创建频道列表。

</>复制代码

  1. /**
  2. * 初始化频道列表
  3. */private void initRecyclerView() {
  4. mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
  5. mRecyclerView.setHasFixedSize(true);
  6. mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
  7. mRecyclerView.setAdapter(new ChannelAdapter(this, mockChannelList()));
  8. }
前置摄像头预览

频道界面背景为前置摄像头预览,这个可以使用Android SDK自己实现。但Agora SDK提供了相关API可以直接实现前置摄像头预览的功能。具体实现如下:

1. 初始化RtcEngineZ

RtcEngine是Agora SDK的核心类,叔用一个管理类AgoraManager进行了简单的封装,提供操作RtcEngine的核心功能。

初始化如下:

</>复制代码

  1. /**
  2. * 初始化RtcEngine
  3. */
  4. public void init(Context context) {
  5. //创建RtcEngine对象, mRtcEventHandler为RtcEngine的回调
  6. mRtcEngine = RtcEngine.create(context, context.getString(R.string.private_app_id), mRtcEventHandler);
  7. //开启视频功能
  8. mRtcEngine.enableVideo();
  9. //视频配置,设置为360P
  10. mRtcEngine.setVideoProfile(Constants.VIDEO_PROFILE_360P, false);
  11. mRtcEngine.setChannelProfile(Constants.CHANNEL_PROFILE_COMMUNICATION);//设置为通信模式(默认)
  12. //mRtcEngine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING);设置为直播模式
  13. //mRtcEngine.setChannelProfile(Constants.CHANNEL_PROFILE_GAME);设置为游戏模式
  14. }
  15. /**
  16. * 在Application类中初始化RtcEngine,注意在AndroidManifest.xml中配置下Application
  17. */
  18. public class LaoTieApplication extends Application {
  19. @Override
  20. public void onCreate() {
  21. super.onCreate();
  22. AgoraManager.getInstance().init(getApplicationContext());
  23. }
  24. }

2. 设置本地视频

</>复制代码

  1. /**
  2. * 设置本地视频,即前置摄像头预览
  3. */
  4. public AgoraManager setupLocalVideo(Context context) {
  5. //创建一个SurfaceView用作视频预览
  6. SurfaceView surfaceView = RtcEngine.CreateRendererView(context);
  7. //将SurfaceView保存起来在SparseArray中,后续会将其加入界面。key为视频的用户id,这里是本地视频, 默认id是0
  8. mSurfaceViews.put(mLocalUid, surfaceView);
  9. //设置本地视频,渲染模式选择VideoCanvas.RENDER_MODE_HIDDEN,如果选其他模式会出现视频不会填充满整个SurfaceView的情况,
  10. //具体渲染模式的区别是什么,官方也没有详细的说明
  11. mRtcEngine.setupLocalVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_HIDDEN, mLocalUid));
  12. return this;//返回AgoraManager以作链式调用
  13. }

3. 添加SurfaceView到布局

</>复制代码

  1. @Override
  2. protected void onResume() {
  3. super.onResume();
  4. //先清空容器
  5. mFrameLayout.removeAllViews();
  6. //设置本地前置摄像头预览并启动
  7. AgoraManager.getInstance().setupLocalVideo(getApplicationContext()).startPreview();
  8. //将本地摄像头预览的SurfaceView添加到容器中
  9. mFrameLayout.addView(AgoraManager.getInstance().getLocalSurfaceView());
  10. }

4. 停止预览

</>复制代码

  1. /**
  2. * 停止预览
  3. */
  4. @Override
  5. protected void onPause() {
  6. super.onPause();
  7. AgoraManager.getInstance().stopPreview();
  8. }
聊天室 (PartyRoomActivity)

点击频道列表中的选项,跳转到聊天室界面。聊天室界面显示规则是:1个人是全屏,2个人是2分屏,3-4个人是4分屏,5-6个人是6分屏, 4分屏和6分屏模式下,双击一个小窗,窗会变大,其余小窗在底部排列。最多支持六人同时聊天。基于这种需求,叔决定写一个自定义控件PartyRoomLayout来完成。PartyRoomLayout直接继承ViewGroup,根据不同的显示模式来完成孩子的测量和布局。

1人全屏


1人全屏其实就是前置摄像头预览效果。

前置摄像头预览

</>复制代码

  1. //设置前置摄像头预览并开启
  2. AgoraManager.getInstance()
  3. .setupLocalVideo(getApplicationContext())
  4. .startPreview();
  5. //将摄像头预览的SurfaceView加入PartyRoomLayout
  6. mPartyRoomLayout.addView(AgoraManager.getInstance().getLocalSurfaceView());
  7. PartyRoomLayout处理1人全屏
  8. /**
  9. * 测量一个孩子的情况,孩子的宽高和父容器即PartyRoomLayout一样
  10. */
  11. private void measureOneChild(int widthMeasureSpec, int heightMeasureSpec) {
  12. View child = getChildAt(0);
  13. child.measure(widthMeasureSpec, heightMeasureSpec);
  14. }
  15. /**
  16. * 布局一个孩子的情况
  17. */
  18. private void layoutOneChild() {
  19. View child = getChildAt(0);
  20. child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
  21. }
加入频道

从频道列表跳转过来后,需要加入到用户所选的频道。

</>复制代码

  1. //更新频道的TextView
  2. mChannel = (TextView) findViewById(R.id.channel);
  3. String channel = getIntent().getStringExtra(“Channel”);
  4. mChannel.setText(channel);
  5. //在AgoraManager中封装了加入频道的API
  6. AgoraManager.getInstance()
  7. .setupLocalVideo(getApplicationContext())
  8. .joinChannel(channel)//加入频道
  9. .startPreview();
挂断

</>复制代码

  1. mEndCall = (ImageButton) findViewById(R.id.end_call);
  2. mEndCall.setOnClickListener(new View.OnClickListener() {
  3. @Override
  4. public void onClick(View v) {
  5. //AgoraManager里面封装了挂断的API, 退出频道
  6. AgoraManager.getInstance().leaveChannel();
  7. finish();
  8. }
  9. });
二分屏

事件监听器

IRtcEngineEventHandler类里面封装了Agora SDK里面的很多事件回调,在AgoraManager中我们创建了IRtcEngineEventHandler的一个对象mRtcEventHandler,并在创建RtcEngine时传入。
private IRtcEngineEventHandler mRtcEventHandler = new IRtcEngineEventHandler() { /**

</>复制代码

  1. private IRtcEngineEventHandler mRtcEventHandler = new IRtcEngineEventHandler() {
  2. /**
  3. * 当获取用户uid的远程视频的回调
  4. */
  5. @Override
  6. public void onFirstRemoteVideoDecoded(int uid, int width, int height, int elapsed) {
  7. if (mOnPartyListener != null) {
  8. mOnPartyListener.onGetRemoteVideo(uid);
  9. }
  10. }
  11. /**
  12. * 加入频道成功的回调
  13. */
  14. @Override
  15. public void onJoinChannelSuccess(String channel, int uid, int elapsed) {
  16. if (mOnPartyListener != null) {
  17. mOnPartyListener.onJoinChannelSuccess(channel, uid);
  18. }
  19. }
  20. /**
  21. * 退出频道
  22. */
  23. @Override
  24. public void onLeaveChannel(RtcStats stats) {
  25. if (mOnPartyListener != null) {
  26. mOnPartyListener.onLeaveChannelSuccess();
  27. }
  28. }
  29. /**
  30. * 用户uid离线时的回调
  31. */
  32. @Override
  33. public void onUserOffline(int uid, int reason) {
  34. if (mOnPartyListener != null) {
  35. mOnPartyListener.onUserOffline(uid);
  36. }
  37. }
  38. };

同时,我们也提供了一个接口,暴露给AgoraManager外部。

</>复制代码

  1. public interface OnPartyListener {
  2. void onJoinChannelSuccess(String channel, int uid);
  3. void onGetRemoteVideo(int uid);
  4. void onLeaveChannelSuccess();
  5. void onUserOffline(int uid);
  6. }

在PartyRoomActivity中监听事件

</>复制代码

  1. AgoraManager.getInstance()
  2. .setupLocalVideo(getApplicationContext())
  3. .setOnPartyListener(mOnPartyListener)//设置监听
  4. .joinChannel(channel)
  5. .startPreview();

设置远程用户视频

</>复制代码

  1. private AgoraManager.OnPartyListener mOnPartyListener = new AgoraManager.OnPartyListener() {
  2. /**
  3. * 获取远程用户视频的回调
  4. */
  5. @Override
  6. public void onGetRemoteVideo(final int uid) {
  7. //操作UI,需要切换到主线程
  8. runOnUiThread(new Runnable() {
  9. @Override
  10. public void run() {
  11. //设置远程用户的视频
  12. AgoraManager.getInstance().setupRemoteVideo(PartyRoomActivity.this, uid);
  13. //将远程用户视频的SurfaceView添加到PartyRoomLayout中,这会触发PartyRoomLayout重新走一遍绘制流程
  14. mPartyRoomLayout.addView(AgoraManager.getInstance().getSurfaceView(uid));
  15. }
  16. });
  17. }
  18. };

测量布局二分屏

当第一次回调onGetRemoteVideo时,说明现在有两个用户了,所以在PartyRoomLayout中需要对二分屏模式进行处理

</>复制代码

  1. /**
  2. * 二分屏时的测量
  3. */
  4. private void measureTwoChild(int widthMeasureSpec, int heightMeasureSpec) {
  5. for (int i = 0; i < getChildCount(); i++) {
  6. View child = getChildAt(i);
  7. int size = MeasureSpec.getSize(heightMeasureSpec);
  8. //孩子高度为父容器高度的一半
  9. int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(size / 2, MeasureSpec.EXACTLY);
  10. child.measure(widthMeasureSpec, childHeightMeasureSpec);
  11. }
  12. }
  13. /**
  14. * 二分屏模式的布局
  15. */
  16. private void layoutTwoChild() {
  17. int left = 0;
  18. int top = 0;
  19. int right = getMeasuredWidth();
  20. int bottom = getChildAt(0).getMeasuredHeight();
  21. for (int i = 0; i < getChildCount(); i++) {
  22. View child = getChildAt(i);
  23. child.layout(left, top, right, bottom);
  24. top += child.getMeasuredHeight();
  25. bottom += child.getMeasuredHeight();
  26. }
  27. }

用户离线时的处理

当有用户离线时,我们需要移除该用户视频对应的SurfaceView

</>复制代码

  1. private AgoraManager.OnPartyListener mOnPartyListener = new AgoraManager.OnPartyListener() {
  2. @Override
  3. public void onUserOffline(final int uid) {
  4. runOnUiThread(new Runnable() {
  5. @Override
  6. public void run() {
  7. //从PartyRoomLayout移除远程视频的SurfaceView
  8. mPartyRoomLayout.removeView(AgoraManager.getInstance().getSurfaceView(uid));
  9. //清除缓存的SurfaceView
  10. AgoraManager.getInstance().removeSurfaceView(uid);
  11. }
  12. });
  13. }
  14. };
四分屏和六分屏

当有3个或者4个老铁开趴,界面显示成四分屏, 当有5个或者6个老铁开趴,界面切分成六分屏

由于之前已经处理了新进用户就会创建SurfaceView加入PartyRoomLayout的逻辑,所以这里只需要处理四六分屏时的测量和布局

四六分屏测量

</>复制代码

  1. private void measureMoreChildSplit(int widthMeasureSpec, int heightMeasureSpec) {
  2. //列数为两列,计算行数
  3. int row = getChildCount() / 2;
  4. if (getChildCount() % 2 != 0) {
  5. row = row + 1;
  6. }
  7. //根据行数平分高度
  8. int childHeight = MeasureSpec.getSize(heightMeasureSpec) / row;
  9. //宽度为父容器PartyRoomLayout的宽度一般,即屏宽的一半
  10. int childWidth = MeasureSpec.getSize(widthMeasureSpec) / 2;
  11. for (int i = 0; i < getChildCount(); i++) {
  12. View child = getChildAt(i);
  13. int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY);
  14. int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
  15. child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
  16. }
  17. }
四六分屏布局

</>复制代码

  1. private void layoutMoreChildSplit() {
  2. int left = 0;
  3. int top = 0;
  4. for (int i = 0; i < getChildCount(); i++) {
  5. View child = getChildAt(i);
  6. int right = left + child.getMeasuredWidth();
  7. int bottom = top + child.getMeasuredHeight();
  8. child.layout(left, top, right, bottom);
  9. if ( (i + 1 )% 2 == 0) {//满足换行条件,更新left和top,布局下一行
  10. left = 0;
  11. top += child.getMeasuredHeight();
  12. } else {
  13. //不满足换行条件,更新left值,继续布局一行中的下一个孩子
  14. left += child.getMeasuredWidth();
  15. }
  16. }
  17. }
双击上下分屏布局

在四六分屏模式下,双击一个小窗,窗会变大,其余小窗在底部排列, 成上下分屏模式。实现思路就是监听PartyRoomLayout的触摸时间,当是双击时,则重新布局。

触摸事件处理

</>复制代码

  1. /**
  2. * 拦截所有的事件
  3. */
  4. @Override
  5. public boolean onInterceptTouchEvent(MotionEvent ev) {
  6. return true;
  7. }
  8. /**
  9. * 让GestureDetector处理触摸事件
  10. */
  11. @Override
  12. public boolean onTouchEvent(MotionEvent event) {
  13. mGestureDetector.onTouchEvent(event);
  14. return true;
  15. }
  16. //四六分屏模式
  17. private static int DISPLAY_MODE_SPLIT = 0;
  18. //上下分屏模式
  19. private static int DISPLAY_MODE_TOP_BOTTOM = 1;
  20. //显示模式的变量,默认是四六分屏
  21. private int mDisplayMode = DISPLAY_MODE_SPLIT;
  22. //上下分屏时上面View的下标
  23. private int mTopViewIndex = -1;
  24. private GestureDetector.SimpleOnGestureListener mOnGestureListener = new GestureDetector.SimpleOnGestureListener() {
  25. @Override
  26. public boolean onDoubleTap(MotionEvent e) {
  27. handleDoubleTap(e);//处理双击事件
  28. return true;
  29. }
  30. private void handleDoubleTap(MotionEvent e) {
  31. //遍历所有的孩子
  32. for (int i = 0; i < getChildCount(); i++) {
  33. View view = getChildAt(i);
  34. //获取孩子view的矩形
  35. Rect rect = new Rect(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
  36. if (rect.contains((int)e.getX(), (int)e.getY())) {//找到双击位置的孩子是谁
  37. if (mTopViewIndex == i) {//如果点击的位置就是上面的view, 则切换成四六分屏模式
  38. mDisplayMode = DISPLAY_MODE_SPLIT;
  39. mTopViewIndex = -1;//重置上面view的下标
  40. } else {
  41. //切换成上下分屏模式,
  42. mTopViewIndex = i;//保存双击位置的下标,即上面View的下标
  43. mDisplayMode = DISPLAY_MODE_TOP_BOTTOM;
  44. }
  45. requestLayout();//请求重新布局
  46. break;
  47. }
  48. }
  49. }
  50. };
上下分屏测量

处理完双击事件后,切换显示模式,请求重新布局,这时候又会触发测量和布局。

</>复制代码

  1. /**
  2. * 上下分屏模式的测量
  3. */
  4. private void measureMoreChildTopBottom(int widthMeasureSpec, int heightMeasureSpec) {
  5. for (int i = 0; i < getChildCount(); i++) {
  6. if (i == mTopViewIndex) {
  7. //测量上面View
  8. measureTopChild(widthMeasureSpec, heightMeasureSpec);
  9. } else {
  10. //测量下面View
  11. measureBottomChild(i, widthMeasureSpec, heightMeasureSpec);
  12. }
  13. }
  14. }
  15. /**
  16. * 上下分屏模式时上面View的测量
  17. */
  18. private void measureTopChild(int widthMeasureSpec, int heightMeasureSpec) {
  19. int size = MeasureSpec.getSize(heightMeasureSpec);
  20. //高度为PartyRoomLayout的一半
  21. int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(size / 2, MeasureSpec.EXACTLY);
  22. getChildAt(mTopViewIndex).measure(widthMeasureSpec, childHeightMeasureSpec);
  23. }
  24. /**
  25. * 上下分屏模式时底部View的测量
  26. */
  27. private void measureBottomChild(int i, int widthMeasureSpec, int heightMeasureSpec) {
  28. //除去顶部孩子后还剩的孩子个数
  29. int childCountExcludeTop = getChildCount() - 1;
  30. //当底部孩子个数小于等于3时
  31. if (childCountExcludeTop <= 3) {
  32. //平分孩子宽度
  33. int childWidth = MeasureSpec.getSize(widthMeasureSpec) / childCountExcludeTop;
  34. int size = MeasureSpec.getSize(heightMeasureSpec);
  35. //高度为PartyRoomLayout的一半
  36. int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(size / 2, MeasureSpec.EXACTLY);
  37. int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
  38. getChildAt(i).measure(childWidthMeasureSpec, childHeightMeasureSpec);
  39. } else if (childCountExcludeTop == 4) {//当底部孩子个数为4个时
  40. int childWidth = MeasureSpec.getSize(widthMeasureSpec) / 2;//宽度为PartyRoomLayout的一半
  41. int childHeight = MeasureSpec.getSize(heightMeasureSpec) / 4;//高度为PartyRoomLayout的1/4
  42. int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY);
  43. int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
  44. getChildAt(i).measure(childWidthMeasureSpec, childHeightMeasureSpec);
  45. } else {//当底部孩子大于4个时
  46. //计算行的个数
  47. int row = childCountExcludeTop / 3;
  48. if (row % 3 != 0) {
  49. row ++;
  50. }
  51. //孩子的宽度为PartyRoomLayout宽度的1/3
  52. int childWidth = MeasureSpec.getSize(widthMeasureSpec) / 3;
  53. //底部孩子平分PartyRoomLayout一半的高度
  54. int childHeight = (MeasureSpec.getSize(heightMeasureSpec) / 2) / row;
  55. int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY);
  56. int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
  57. getChildAt(i).measure(childWidthMeasureSpec, childHeightMeasureSpec);
  58. }
  59. }
上下分屏布局

</>复制代码

  1. private void layoutMoreChildTopBottom() {
  2. //布局上面View
  3. View topView = getChildAt(mTopViewIndex);
  4. topView.layout(0, 0, topView.getMeasuredWidth(), topView.getMeasuredHeight());
  5. int left = 0;
  6. int top = topView.getMeasuredHeight();
  7. for (int i = 0; i < getChildCount(); i++) {
  8. //上面已经布局过上面的View, 这里就跳过
  9. if (i == mTopViewIndex) {
  10. continue;
  11. }
  12. View view = getChildAt(i);
  13. int right = left + view.getMeasuredWidth();
  14. int bottom = top + view.getMeasuredHeight();
  15. //布局下面的一个View
  16. view.layout(left, top, right, bottom);
  17. left = left + view.getMeasuredWidth();
  18. if (left >= getWidth()) {//满足换行条件则换行
  19. left = 0;
  20. top += view.getMeasuredHeight();
  21. }
  22. }
  23. }

至此,一个功能类似Houseparty的demo就完成了,github地址:

https://github.com/uncleleonf...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/70046.html

相关文章

  • Android实现多人音视频聊天应用(一)

    作者:声网Agora用户,资深Android开发者吴东洋。本系列文章分享了基于Agora SDK 2.1实现多人视频通话的实践经验。 自从2016年,鼓吹互联网寒冬的论调甚嚣尘上,2017年亦有愈演愈烈之势。但连麦直播、在线抓娃娃、直播问答、远程狼人杀等类型的项目却异军突起,成了投资人的风口,创业者的蓝海和用户的必装App,而这些方向的项目都有一个共同的特点——都依赖视频通话和全互动直播技术。 声...

    raoyi 评论0 收藏0

发表评论

0条评论

CocoaChina

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<