V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Agora
V2EX  ›  程序员

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

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

    叔想做个直播 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 时传入。

    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();    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
    sobigfish
        1
    sobigfish  
       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   ·   实用小工具   ·   2563 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 15:26 · PVG 23:26 · LAX 07:26 · JFK 10:26
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.