使用 Agora SDK 实现视频对话应用 HouseParty-附 Android 源码 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
Agora
V2EX    程序员

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

  •  
  •   Agora 2017-05-12 15:47:57 +08:00 3584 次点击
    这是一个创建于 3125 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

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

    https://github.com/uncleleonfan/LaoTieParty

    1. 注册

    登录 Agora 官网,注册个人账号,这个叔就不介绍了。 https://www.agora.io/cn/

    2. 创建应用

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

    3. 下载 SDK

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

    集成 SDK

    1. 导入库文件

    将 libs 文件夹的下的文件导入 Android Studio

    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">6ffa586315ed49e6a8cdff064ad8a0b0</string> </resources> 

    主界面( MainActivity )

    在主界面,我们需要检查先 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!***,进入频道选择界面

    创建频道列表

    这里使用 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 人全屏

    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(); 

    挂断

    当用户点击挂断按钮可以退出频道

    mEndCall = (ImageButton) findViewById(R.id.end_call); mEndCall.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //AgoraManager 里面封装了挂断的 API, 退出频道 AgoraManager.getInstance().leaveChannel(); finish(); } }); 

    二分屏

    事件监听器

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

    privte 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(); v oid 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 个老铁开趴,界面切分成六分屏

    由于之前已经处理了新进用户就会创建 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(); } } } 

    双击上下分屏布局

    在四六分屏模式下,双击一个小窗,窗会变大,其余小窗在底部排列, 成上下分屏模式。实现思路就是监听 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(); } } } 

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

    https://github.com/uncleleonfan/LaoTieParty

    4 条回复    2017-05-16 15:21:58 +08:00
    run2
        1
    run2  
       2017-05-12 20:35:10 +08:00
    Agora 不错...
    推广硬说成分享系列...
    begeekmyfriend
        2
    begeekmyfriend  
       2017-05-12 23:05:56 +08:00
    我也是做直播的,手动点赞!
    xjdata
        3
    xjdata  
       2017-05-13 00:59:09 +08:00 via Android
    打车问下各位,想做个视频通话的应用,ios,安卓,win,不需要直播,就需要端对端的视频通话。特殊的要求是结束后通话的视频能够下载到本地保存起来。 目前看了网易云信,还有什么值得推荐的吗?
    Agora
        4
    Agora  
    OP
       2017-05-16 15:21:58 +08:00
    @xjdata Agora 可以,使用录制就可以。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     5024 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 26ms UTC 09:37 PVG 17:37 LAX 01:37 JFK 04:37
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86