摘要:叔想做個(gè)直播很久了,最近終于得空,做了一個(gè)視頻群聊,以饗觀眾。主界面在主界面,我們需要檢查先和權(quán)限,以適配及以上版本。但提供了相關(guān)可以直接實(shí)現(xiàn)前置攝像頭預(yù)覽的功能。最多支持六人同時(shí)聊天。直接繼承,根據(jù)不同的顯示模式來(lái)完成孩子的測(cè)量和布局。
叔想做個(gè)直播demo很久了,最近終于得空,做了一個(gè)視頻群聊Demo,以饗觀眾。 直播云有很多大廠在做,經(jīng)老鐵介紹,Agora不錯(cuò),遂入坑。Agora提供多種模式,一個(gè)頻道可以設(shè)置一種模式。
Agora SDK集成叔專注SDK集成幾十年,Agora SDK集成也并沒(méi)有搞什么事情,大家按照下面步驟上車就行。
1.注冊(cè)
登錄官網(wǎng),注冊(cè)個(gè)人賬號(hào),這個(gè)叔就不介紹了。
2.創(chuàng)建應(yīng)用
注冊(cè)賬號(hào)登錄后,進(jìn)入后臺(tái),找到“添加新項(xiàng)目”按鈕,點(diǎn)擊創(chuàng)建新項(xiàng)目,創(chuàng)建好后就會(huì)獲取到一個(gè)App ID, 做過(guò)SDK集成的老鐵們都知道這是干啥用的。
3.下載SDK
進(jìn)入官方下載界面, 這里我們選擇視頻通話 + 直播 SDK中的Android版本下載。下載后解壓之后又兩個(gè)文件夾,分別是libs和samples, libs文件夾存放的是庫(kù)文件,samples是官方Demo源碼,大叔曾說(shuō)過(guò)欲練此SDK,必先跑Sample, 有興趣的同學(xué)可以跑跑。
4.集成SDK
1. 導(dǎo)入庫(kù)文件
將libs文件夾的下的文件導(dǎo)入Android Studio, 最終效果如下:
2.添加必要權(quán)限
在AndroidManifest.xml中添加如下權(quán)限
3.配置APP ID
在values文件夾下創(chuàng)建strings-config.xml, 配置在官網(wǎng)創(chuàng)建應(yīng)用的App ID。
主界面(MainActivity)6ffa586315ed49e6a8cdff064ad8a0b0
在主界面,我們需要檢查先Camera和Audio權(quán)限,以適配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權(quán)限 if (checkSelfPermission(Manifest.permission.RECORD_AUDIO, PERMISSION_REQ_ID_RECORD_AUDIO)) { //檢查Camera權(quán)限 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)
點(diǎn)擊開PA!,進(jìn)入頻道選擇界面
創(chuàng)建頻道列表這里使用RecyclerView創(chuàng)建頻道列表。
/** * 初始化頻道列表 */private void initRecyclerView() { mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view); mRecyclerView.setHasFixedSize(true); mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); mRecyclerView.setAdapter(new ChannelAdapter(this, mockChannelList())); }前置攝像頭預(yù)覽
頻道界面背景為前置攝像頭預(yù)覽,這個(gè)可以使用Android SDK自己實(shí)現(xiàn)。但Agora SDK提供了相關(guān)API可以直接實(shí)現(xiàn)前置攝像頭預(yù)覽的功能。具體實(shí)現(xiàn)如下:
1. 初始化RtcEngineZ
RtcEngine是Agora SDK的核心類,叔用一個(gè)管理類AgoraManager進(jìn)行了簡(jiǎn)單的封裝,提供操作RtcEngine的核心功能。
初始化如下:
/** * 初始化RtcEngine */ public void init(Context context) { //創(chuàng)建RtcEngine對(duì)象, mRtcEventHandler為RtcEngine的回調(diào) mRtcEngine = RtcEngine.create(context, context.getString(R.string.private_app_id), mRtcEventHandler); //開啟視頻功能 mRtcEngine.enableVideo(); //視頻配置,設(shè)置為360P mRtcEngine.setVideoProfile(Constants.VIDEO_PROFILE_360P, false); mRtcEngine.setChannelProfile(Constants.CHANNEL_PROFILE_COMMUNICATION);//設(shè)置為通信模式(默認(rèn)) //mRtcEngine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING);設(shè)置為直播模式 //mRtcEngine.setChannelProfile(Constants.CHANNEL_PROFILE_GAME);設(shè)置為游戲模式 } /** * 在Application類中初始化RtcEngine,注意在AndroidManifest.xml中配置下Application */ public class LaoTieApplication extends Application { @Override public void onCreate() { super.onCreate(); AgoraManager.getInstance().init(getApplicationContext()); } }
2. 設(shè)置本地視頻
/** * 設(shè)置本地視頻,即前置攝像頭預(yù)覽 */ public AgoraManager setupLocalVideo(Context context) { //創(chuàng)建一個(gè)SurfaceView用作視頻預(yù)覽 SurfaceView surfaceView = RtcEngine.CreateRendererView(context); //將SurfaceView保存起來(lái)在SparseArray中,后續(xù)會(huì)將其加入界面。key為視頻的用戶id,這里是本地視頻, 默認(rèn)id是0 mSurfaceViews.put(mLocalUid, surfaceView); //設(shè)置本地視頻,渲染模式選擇VideoCanvas.RENDER_MODE_HIDDEN,如果選其他模式會(huì)出現(xiàn)視頻不會(huì)填充滿整個(gè)SurfaceView的情況, //具體渲染模式的區(qū)別是什么,官方也沒(méi)有詳細(xì)的說(shuō)明 mRtcEngine.setupLocalVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_HIDDEN, mLocalUid)); return this;//返回AgoraManager以作鏈?zhǔn)秸{(diào)用 }
3. 添加SurfaceView到布局
@Override protected void onResume() { super.onResume(); //先清空容器 mFrameLayout.removeAllViews(); //設(shè)置本地前置攝像頭預(yù)覽并啟動(dòng) AgoraManager.getInstance().setupLocalVideo(getApplicationContext()).startPreview(); //將本地?cái)z像頭預(yù)覽的SurfaceView添加到容器中 mFrameLayout.addView(AgoraManager.getInstance().getLocalSurfaceView()); }
4. 停止預(yù)覽
/** * 停止預(yù)覽 */ @Override protected void onPause() { super.onPause(); AgoraManager.getInstance().stopPreview(); }聊天室 (PartyRoomActivity)
點(diǎn)擊頻道列表中的選項(xiàng),跳轉(zhuǎn)到聊天室界面。聊天室界面顯示規(guī)則是:1個(gè)人是全屏,2個(gè)人是2分屏,3-4個(gè)人是4分屏,5-6個(gè)人是6分屏, 4分屏和6分屏模式下,雙擊一個(gè)小窗,窗會(huì)變大,其余小窗在底部排列。最多支持六人同時(shí)聊天?;谶@種需求,叔決定寫一個(gè)自定義控件PartyRoomLayout來(lái)完成。PartyRoomLayout直接繼承ViewGroup,根據(jù)不同的顯示模式來(lái)完成孩子的測(cè)量和布局。
1人全屏
1人全屏其實(shí)就是前置攝像頭預(yù)覽效果。
//設(shè)置前置攝像頭預(yù)覽并開啟 AgoraManager.getInstance() .setupLocalVideo(getApplicationContext()) .startPreview(); //將攝像頭預(yù)覽的SurfaceView加入PartyRoomLayout mPartyRoomLayout.addView(AgoraManager.getInstance().getLocalSurfaceView()); PartyRoomLayout處理1人全屏 /** * 測(cè)量一個(gè)孩子的情況,孩子的寬高和父容器即PartyRoomLayout一樣 */ private void measureOneChild(int widthMeasureSpec, int heightMeasureSpec) { View child = getChildAt(0); child.measure(widthMeasureSpec, heightMeasureSpec); } /** * 布局一個(gè)孩子的情況 */ private void layoutOneChild() { View child = getChildAt(0); child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight()); }加入頻道
從頻道列表跳轉(zhuǎn)過(guò)來(lái)后,需要加入到用戶所選的頻道。
//更新頻道的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(); } });二分屏 事件監(jiān)聽(tīng)器
IRtcEngineEventHandler類里面封裝了Agora SDK里面的很多事件回調(diào),在AgoraManager中我們創(chuàng)建了IRtcEngineEventHandler的一個(gè)對(duì)象mRtcEventHandler,并在創(chuàng)建RtcEngine時(shí)傳入。
private IRtcEngineEventHandler mRtcEventHandler = new IRtcEngineEventHandler() { /**
private IRtcEngineEventHandler mRtcEventHandler = new IRtcEngineEventHandler() { /** * 當(dāng)獲取用戶uid的遠(yuǎn)程視頻的回調(diào) */ @Override public void onFirstRemoteVideoDecoded(int uid, int width, int height, int elapsed) { if (mOnPartyListener != null) { mOnPartyListener.onGetRemoteVideo(uid); } } /** * 加入頻道成功的回調(diào) */ @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離線時(shí)的回調(diào) */ @Override public void onUserOffline(int uid, int reason) { if (mOnPartyListener != null) { mOnPartyListener.onUserOffline(uid); } } };
同時(shí),我們也提供了一個(gè)接口,暴露給AgoraManager外部。
public interface OnPartyListener { void onJoinChannelSuccess(String channel, int uid); void onGetRemoteVideo(int uid); void onLeaveChannelSuccess(); void onUserOffline(int uid); }
在PartyRoomActivity中監(jiān)聽(tīng)事件
AgoraManager.getInstance() .setupLocalVideo(getApplicationContext()) .setOnPartyListener(mOnPartyListener)//設(shè)置監(jiān)聽(tīng) .joinChannel(channel) .startPreview();
設(shè)置遠(yuǎn)程用戶視頻
private AgoraManager.OnPartyListener mOnPartyListener = new AgoraManager.OnPartyListener() { /** * 獲取遠(yuǎn)程用戶視頻的回調(diào) */ @Override public void onGetRemoteVideo(final int uid) { //操作UI,需要切換到主線程 runOnUiThread(new Runnable() { @Override public void run() { //設(shè)置遠(yuǎn)程用戶的視頻 AgoraManager.getInstance().setupRemoteVideo(PartyRoomActivity.this, uid); //將遠(yuǎn)程用戶視頻的SurfaceView添加到PartyRoomLayout中,這會(huì)觸發(fā)PartyRoomLayout重新走一遍繪制流程 mPartyRoomLayout.addView(AgoraManager.getInstance().getSurfaceView(uid)); } }); } };
測(cè)量布局二分屏
當(dāng)?shù)谝淮位卣{(diào)onGetRemoteVideo時(shí),說(shuō)明現(xiàn)在有兩個(gè)用戶了,所以在PartyRoomLayout中需要對(duì)二分屏模式進(jìn)行處理
/** * 二分屏?xí)r的測(cè)量 */ 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(); } }
用戶離線時(shí)的處理
當(dāng)有用戶離線時(shí),我們需要移除該用戶視頻對(duì)應(yīng)的SurfaceView
private AgoraManager.OnPartyListener mOnPartyListener = new AgoraManager.OnPartyListener() { @Override public void onUserOffline(final int uid) { runOnUiThread(new Runnable() { @Override public void run() { //從PartyRoomLayout移除遠(yuǎn)程視頻的SurfaceView mPartyRoomLayout.removeView(AgoraManager.getInstance().getSurfaceView(uid)); //清除緩存的SurfaceView AgoraManager.getInstance().removeSurfaceView(uid); } }); } };四分屏和六分屏
當(dāng)有3個(gè)或者4個(gè)老鐵開趴,界面顯示成四分屏, 當(dāng)有5個(gè)或者6個(gè)老鐵開趴,界面切分成六分屏
由于之前已經(jīng)處理了新進(jìn)用戶就會(huì)創(chuàng)建SurfaceView加入PartyRoomLayout的邏輯,所以這里只需要處理四六分屏?xí)r的測(cè)量和布局
四六分屏測(cè)量private void measureMoreChildSplit(int widthMeasureSpec, int heightMeasureSpec) { //列數(shù)為兩列,計(jì)算行數(shù) int row = getChildCount() / 2; if (getChildCount() % 2 != 0) { row = row + 1; } //根據(jù)行數(shù)平分高度 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值,繼續(xù)布局一行中的下一個(gè)孩子 left += child.getMeasuredWidth(); } } }雙擊上下分屏布局
在四六分屏模式下,雙擊一個(gè)小窗,窗會(huì)變大,其余小窗在底部排列, 成上下分屏模式。實(shí)現(xiàn)思路就是監(jiān)聽(tīng)PartyRoomLayout的觸摸時(shí)間,當(dāng)是雙擊時(shí),則重新布局。
觸摸事件處理
/** * 攔截所有的事件 */ @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; //顯示模式的變量,默認(rèn)是四六分屏 private int mDisplayMode = DISPLAY_MODE_SPLIT; //上下分屏?xí)r上面View的下標(biāo) 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())) {//找到雙擊位置的孩子是誰(shuí) if (mTopViewIndex == i) {//如果點(diǎn)擊的位置就是上面的view, 則切換成四六分屏模式 mDisplayMode = DISPLAY_MODE_SPLIT; mTopViewIndex = -1;//重置上面view的下標(biāo) } else { //切換成上下分屏模式, mTopViewIndex = i;//保存雙擊位置的下標(biāo),即上面View的下標(biāo) mDisplayMode = DISPLAY_MODE_TOP_BOTTOM; } requestLayout();//請(qǐng)求重新布局 break; } } } };上下分屏測(cè)量
處理完雙擊事件后,切換顯示模式,請(qǐng)求重新布局,這時(shí)候又會(huì)觸發(fā)測(cè)量和布局。
/** * 上下分屏模式的測(cè)量 */ private void measureMoreChildTopBottom(int widthMeasureSpec, int heightMeasureSpec) { for (int i = 0; i < getChildCount(); i++) { if (i == mTopViewIndex) { //測(cè)量上面View measureTopChild(widthMeasureSpec, heightMeasureSpec); } else { //測(cè)量下面View measureBottomChild(i, widthMeasureSpec, heightMeasureSpec); } } } /** * 上下分屏模式時(shí)上面View的測(cè)量 */ 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); } /** * 上下分屏模式時(shí)底部View的測(cè)量 */ private void measureBottomChild(int i, int widthMeasureSpec, int heightMeasureSpec) { //除去頂部孩子后還剩的孩子個(gè)數(shù) int childCountExcludeTop = getChildCount() - 1; //當(dāng)?shù)撞亢⒆觽€(gè)數(shù)小于等于3時(shí) 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) {//當(dāng)?shù)撞亢⒆觽€(gè)數(shù)為4個(gè)時(shí) 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 {//當(dāng)?shù)撞亢⒆哟笥?個(gè)時(shí) //計(jì)算行的個(gè)數(shù) 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++) { //上面已經(jīng)布局過(guò)上面的View, 這里就跳過(guò) if (i == mTopViewIndex) { continue; } View view = getChildAt(i); int right = left + view.getMeasuredWidth(); int bottom = top + view.getMeasuredHeight(); //布局下面的一個(gè)View view.layout(left, top, right, bottom); left = left + view.getMeasuredWidth(); if (left >= getWidth()) {//滿足換行條件則換行 left = 0; top += view.getMeasuredHeight(); } } }
至此,一個(gè)功能類似Houseparty的demo就完成了,github地址:
https://github.com/uncleleonf...
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/70046.html
作者:聲網(wǎng)Agora用戶,資深A(yù)ndroid開發(fā)者吳東洋。本系列文章分享了基于Agora SDK 2.1實(shí)現(xiàn)多人視頻通話的實(shí)踐經(jīng)驗(yàn)。 自從2016年,鼓吹互聯(lián)網(wǎng)寒冬的論調(diào)甚囂塵上,2017年亦有愈演愈烈之勢(shì)。但連麥直播、在線抓娃娃、直播問(wèn)答、遠(yuǎn)程狼人殺等類型的項(xiàng)目卻異軍突起,成了投資人的風(fēng)口,創(chuàng)業(yè)者的藍(lán)海和用戶的必裝App,而這些方向的項(xiàng)目都有一個(gè)共同的特點(diǎn)——都依賴視頻通話和全互動(dòng)直播技術(shù)。 聲...
閱讀 893·2021-11-15 11:38
閱讀 2526·2021-09-08 09:45
閱讀 2828·2021-09-04 16:48
閱讀 2574·2019-08-30 15:54
閱讀 941·2019-08-30 13:57
閱讀 1629·2019-08-29 15:39
閱讀 506·2019-08-29 12:46
閱讀 3530·2019-08-26 13:39