摘要:叔想做個直播很久了,最近終于得空,做了一個視頻群聊,以饗觀眾。主界面在主界面,我們需要檢查先和權限,以適配及以上版本。但提供了相關可以直接實現前置攝像頭預覽的功能。最多支持六人同時聊天。直接繼承,根據不同的顯示模式來完成孩子的測量和布局。
叔想做個直播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。
主界面(MainActivity)6ffa586315ed49e6a8cdff064ad8a0b0
在主界面,我們需要檢查先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. 初始化RtcEngineZ
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() { /**
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個老鐵開趴,界面切分成六分屏
由于之前已經處理了新進用戶就會創建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/uncleleonf...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/70046.html
作者:聲網Agora用戶,資深Android開發者吳東洋。本系列文章分享了基于Agora SDK 2.1實現多人視頻通話的實踐經驗。 自從2016年,鼓吹互聯網寒冬的論調甚囂塵上,2017年亦有愈演愈烈之勢。但連麥直播、在線抓娃娃、直播問答、遠程狼人殺等類型的項目卻異軍突起,成了投資人的風口,創業者的藍海和用戶的必裝App,而這些方向的項目都有一個共同的特點——都依賴視頻通話和全互動直播技術。 聲...
閱讀 882·2021-11-15 11:38
閱讀 2512·2021-09-08 09:45
閱讀 2812·2021-09-04 16:48
閱讀 2563·2019-08-30 15:54
閱讀 929·2019-08-30 13:57
閱讀 1617·2019-08-29 15:39
閱讀 495·2019-08-29 12:46
閱讀 3519·2019-08-26 13:39