前言 (閑聊)
之前在上移動平臺開發(fā)課的過程中,對android的開發(fā)算是有一個大概的初步了解,但是知之甚淺。印象最深刻的就是但凡遇到圖片視頻方面的處理就會變得非常復(fù)雜以及容易出錯。那時對于我這個小白來說想調(diào)用一個視頻播放器來播放一小段視頻都是一個"大"工程了,至于什么實時的視頻對話想都不想去想,因為太復(fù)雜且麻煩!!!
但是有了功能齊全的SDK ,這次的實時視頻開發(fā),卻是與以前完全不同的體驗。直觀感受就好像是我這種菜雞做機器學(xué)習(xí)模型有了Python的sklearn庫,菜雞的大雄有了多啦A夢那樣,擁有了一個萬能的百寶箱。當(dāng)你想實現(xiàn)一個想著就十分復(fù)雜的功能時例如直播的推拉流之類的,這里面就已經(jīng)集成了對應(yīng)的函數(shù)。
所用SDK介紹
關(guān)于SDK的安裝本文不做過多描述,我使用的是ZEGO EXPRESS SDK,相應(yīng)的安裝詳細(xì)過程請直接看鏈接
https://doc-zh.zego.im/zh/215.html,同時記得按照步驟申請對應(yīng)的AppID以及AppSign.
使用此SDK的優(yōu)點:代碼簡單易懂,文檔內(nèi)容較為全面,實現(xiàn)簡單。
本文實際內(nèi)容可以有些長,所以可以根據(jù)目錄篩選查看內(nèi)容
目錄
一.所實現(xiàn)項目的功能
1.項目實現(xiàn)核心截圖
2.所實現(xiàn)的功能
3.適用的應(yīng)用場景
二.實現(xiàn)流程
1.布局的設(shè)計
2.核心邏輯代碼
2.1推流和拉流的概念
2.2正式開始及全局變量的聲明
2.2.1 onCreate函數(shù)內(nèi)操作
1.申請AppID
2.初始化SDK
3.初始化用戶及登錄房間
4.獲取所在房間內(nèi)的所有推流
2.2.2 點擊事件
1.推拉流
2.麥克風(fēng)按鈕處理
3.與本地相機美顏、改變攝像頭前后置等本地擴展功能
4.退出按鈕
三.源代碼
四.不足以及可以繼續(xù)開發(fā)的地方
一.所實現(xiàn)項目的功能
1.項目實現(xiàn)核心截圖
登錄界面
核心視頻UI
2.所實現(xiàn)的功能
(1).從登錄界面到視頻界面的跳轉(zhuǎn),以及傳遞所輸入的房間號ID。以及在核心視頻UI界面的退出到登錄界面的跳轉(zhuǎn).
(2) 四人可同時正常視頻通話,且對應(yīng)每個流所呈現(xiàn)的畫面進(jìn)行打開關(guān)閉收音.
(3).實時的將同一房間號的正在推流的流ID全部顯示在第二排視頻下的TextView上(在該房間的用戶可根據(jù)次項流ID內(nèi)容)
(4).實現(xiàn)對本地界面中的前后置攝像頭進(jìn)行切換,美顏效果的實現(xiàn)。
3.適用的應(yīng)用場景
家庭聊天,同事或者同學(xué)聊天以及簡單的面對面會議。
二.實現(xiàn)流程
**1.**先說簡單說一下界面布局的設(shè)計。這個相對簡單,看上述的界面也大概也能明白一些,說一下我做的過程中遇到的問題吧。
第一個因為只需要輸入房間ID,所以登陸界面很簡單,線性布局垂直方向, 再加上TextView + EditText + Button就解決了的最簡單登陸界面。實在不懂可以直接看第三部分的源代碼。
第二個界面對于新手來說還是有些困難的,首先由于四人視頻所占空間很大,一個屏幕的大小不能輕易放下,這種需要滾動條的情況就可以采用三種手段來解決分別是RecyclerView,ListView以及ScrollView來解決。本文采用的是相對來說最簡單的ScrollView來解決。這里就要注意了,**ScrollView當(dāng)中只有一個子元素**,所以如果你想像我一樣,把ScrollView作為最外層的話,需要在內(nèi)部再嵌套一個線性布局,這樣才能用。
相連視頻的畫面采用的是相對布局,這樣更加方便做微調(diào),視頻本身使用TextureView來做。
我代碼的整體設(shè)計布局框架如下(要注意的是本文對于核心視頻UI的布局上圖片的點擊事件,都是在圖片的屬性中添加的android:onClick處理解決方法,這里看不懂沒關(guān)系,后面也會說。)
```xml
...(省略拉流1和2的TextView+LinearLayout的組合,寫法類似下面這個框架的)
```
更詳盡的代碼見第三部分的全部源碼
**2.**下面就是核心邏輯代碼的實現(xiàn)了
**注意:以下只展示核心的代碼,并不能直接運行,具體操作請看第三部分。**
**2.1**首先,如果不理解**推流**和**拉流**的概念的話,首先要快速理解一下。
實際上我們可以用一個簡單過程來幫助理解。
首先來說,推流就是你發(fā)送出去一串代碼(流ID)以及你本地的照相機所拍攝的實時畫面(所謂的流) 上網(wǎng)絡(luò)且到達(dá)服務(wù)器并存儲。且如果你不停止推流就要一直發(fā)送,就好像水流一樣源源不斷,但是服務(wù)器里面就好像是有門堵著的,水是不能輕易漏出來。
別人想在這個服務(wù)器上看你的視頻咋辦? 他就要拉你對應(yīng)的流,咋拉呢?就通過你發(fā)的那個推出去的流ID來拉。如果他知道這個流ID ,就好像是找到了服務(wù)器對應(yīng)一扇門的一把鎖鑰匙似的,把你不斷發(fā)送到服務(wù)器的"流"大門打開,水流涌出來了一直到他的手機上,這樣他就看到你的畫面了。
而你如果停止了推流,水就沒了,他自然就接收不到畫面了。他停止拉流,等于是把之前那扇門又關(guān)上了,他手機上也不會再接收你的畫面。
**2.2**理解之后,如果你的SDK已經(jīng)按照最頂上鏈接集成完畢后,開發(fā)過程就可以正式開始了!
以下要用到很多的全局變量,首先展示一下他們的聲明以及初始值,如果下面的有些看不懂的可以返回過來看一看這里所寫的內(nèi)容,以及注釋。
```java
public static ZegoExpressEngine engine = null;
boolean publishMicEnable = true; // 初始的自己麥克風(fēng)為開著的
boolean playStreamMute = true; //其余屏幕人的初始狀態(tài)都為靜音
boolean playStreamMute2 = true;
boolean playStreamMute3 = true;
boolean isBeauty = false;//初始無美顏
boolean isFrontCamera = true; // 初始為前置攝像頭
ImageButton ib_local_mic; //本地麥克風(fēng)
ImageButton ib_remote_stream_audio;//拉流1外部視角的音量
ImageButton ib_remote_stream_audio2;//拉流2外部視角的音量
ImageButton ib_remote_stream_audio3;//拉流3外部視角的音量
ImageButton ib_beauty; //美顏按鍵
String LocalStreamID; //本地推流ID
String RemoteStreamID; //拉流1 ID
String RemoteStreamID2; //拉流2 ID
String RemoteStreamID3;//拉流3 ID
ArrayList
private String userID;//用戶ID
String roomID;//房間ID
//寫好自己的ID和sign,以下為我所申請的ID,如果要自己使用或者商用請自行申請并修改
long appID = ;? // 請通過官網(wǎng)注冊獲取,格式為 123456789L
String appSign = "";? //64個字符,請通過官網(wǎng)注冊獲取,格式為"0123456789012345678901234567890123456789012345678901234567890123"
```
**2.2.1** onCreate函數(shù)部分
**(1).**申請AppID,這一步如果不做的話根本后面做不了!!所以要先申請一各APPID,可以看我最上方附的那個鏈接?
**(2)**根據(jù)你的appID以及appsign進(jìn)行初始化SDK,使用測試環(huán)境,通用場景接入。如果這一步成功了,那么恭喜你,你已經(jīng)獲得了一個強大的神奇engine,他功能強大,后面所有所有都是依靠他來實現(xiàn)的,什么推流拉流就是一行代碼的事情.
```java
engine = ZegoExpressEngine.createEngine(appID, appSign, true, ZegoScenario.GENERAL, getApplication(), null);
```
**(3).**初始化用戶,并將用戶登錄至房間內(nèi)。這步也是在進(jìn)行視頻通話之前的必須一步,我們每個人都是在服務(wù)器上一個獨立的個體,想要實現(xiàn)特定用戶群體之間的交流。房間是很好的一個工具。這個userid和name在全局中不能有任何重復(fù),最好有一定意義,但我面向的場景主要是家庭場景,不太需要,如果是商用開發(fā)還是很有必要開發(fā)的。為了防止重復(fù),我采用的是生成隨機數(shù),這樣的話重復(fù)的概率就小很多了。
我這里的roomID,從登錄界面時的intent的所傳遞的數(shù)據(jù)來定義的
```java
//用戶注冊
String randomSuffix = String.valueOf(new Date().getTime() % (new Date().getTime() / 1000));
userID = "user" + randomSuffix;
ZegoUser user = new ZegoUser(userID);
...
//房間登錄
ntent intent = getIntent();
roomID = intent.getStringExtra("room_id");//getXxxExtra方法獲取Intent傳遞過來的roomID
engine.loginRoom(roomID, user);//有了房間號,將用戶登錄到該房間
```
**(4).**獲取所在房間內(nèi)的所有推流。這里面主要就是要用到監(jiān)聽房間相關(guān)事件回調(diào)來實現(xiàn)主要用到的是回調(diào)中的onRoomStreamUpdate 要注意的是:這里的流更新指的是房間內(nèi)其他用戶的,用戶自己的流產(chǎn)生變化,自己的這個回調(diào)函數(shù)是沒有反應(yīng)的。
想實現(xiàn)此功能,首先要創(chuàng)建一個ArrayList來記錄房間內(nèi)存在的所有的流ID。可以看到如下的RoomStreamList我是在全局變量的地方事先聲明過了。(其他全局變量的聲明我會放到后面函數(shù)部分說明) 這里放入的第一個元素的目的是為了方便在后面講ArrayList中所有元素連接成為一行具體的內(nèi)容.
```java
RoomStreamList = new ArrayList
RoomStreamList.add("當(dāng)前房間內(nèi)的推流有:");
```
onRoomStreamUpdate的具體寫法如下所示,看起來好像挺長,實際上思路就是如果有一個流ID狀態(tài)發(fā)生改變,我就看我的列表當(dāng)中是不是有這個流ID如果有那就去掉,如果沒有就加入進(jìn)去。實在不懂,根據(jù)注釋也能看個大概,這里面需要提一下的是sentenceId += 這個并不是真正的加法,而是java中的字符串拼接,將ArrayList中所有元素拼成一句話,并找到要顯示的TextView并顯示出來。
```java
engine.setEventHandler(new IZegoEventHandler() {
...
public void onRoomStreamUpdate(String roomID, ZegoUpdateType updateType, ArrayList
/* 流狀態(tài)更新,登陸房間后,當(dāng)房間內(nèi)有用戶新推送或刪除音視頻流時,SDK會通過該回調(diào)通知 */
//自己的推流不會被記入
for (int i = 0; i < streamList.size(); i++)//加入或退出房間流的所有推流id全都遍歷一遍
{
Toast.makeText(getApplicationContext(), streamList.get(i).streamID + " room stream changed", Toast.LENGTH_LONG).show();
if (RoomStreamList.contains(streamList.get(i).streamID)) {//如果現(xiàn)有列表中包含這個,就移除
RoomStreamList.remove(streamList.get(i).streamID);
} else {//如果現(xiàn)有列表中不包含這個就加入
RoomStreamList.add(streamList.get(i).streamID);
}
}
String SentenceId = "";// 用于記錄下當(dāng)前房間內(nèi)還有的流ID
for (int i = 0; i < RoomStreamList.size(); i++) {
SentenceId += RoomStreamList.get(i) + " ";//利用字符串拼接,將當(dāng)前房間還在的所有流ID全部記下
}
TextView ViewIdlist = findViewById(R.id.stream_id_list);//找到用于顯示流ID的TextView
ViewIdlist.setText(SentenceId);//設(shè)置文字信息在TextView上體現(xiàn)出來
}
});
```
**(5).**動態(tài)權(quán)限申請,代碼如下。若需要申請更多權(quán)限則自行添加。
```java
String[] permissionNeeded = {
"android.permission.CAMERA",
"android.permission.RECORD_AUDIO"};
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(this, "android.permission.CAMERA") != PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(this, "android.permission.RECORD_AUDIO") != PackageManager.PERMISSION_GRANTED) {
requestPermissions(permissionNeeded, 101);}
}
```
**2.2.2** 接下來要做的就是逐步完成每個在視圖中注冊的點擊事件,其中包括四個部分,分別是1.推拉流 2.麥克風(fēng)按鈕處理
**3.**與本地相機美顏、改變攝像頭前后置等本地擴展功能 4.退出按鈕
**(1)**.推拉流
這也是視頻通話最為主要的部分。但是卻是十分簡單的,核心的代碼就只需要調(diào)用兩個接口也就是兩行代碼就可以解決。但是還是有些要注意的事項。如下為推流**核心**代碼,有所省略,具體實現(xiàn)詳見第三部分。 實際上可以看出來,核心的邏輯就是判斷推流按鈕上的字是否是"推流",如果是的話就就行推流再把文字設(shè)置稱為"停止推流"。
其中也包含了核心的接口就是startPublishingStream,stopPublishingStream以及startpreview和stoppreview來獲取本地圖像。
```java
public void ClickPublish(View view) {
...
if (button.getText().equals("推流")) {//若上面的文字是推流,則說明還未推流。
/* 開始推流 */
EditText et = findViewById(R.id.ed_publish_stream_id);//找到,旁邊的EditText的實例
LocalStreamID = et.getText().toString();//獲取其文字內(nèi)容,并賦值給全局變量LocalStreamID
engine.startPublishingStream(LocalStreamID);//推流
/* 開始預(yù)覽并設(shè)置本地預(yù)覽視圖 */
/* Start preview and set the local preview view. */
View local_view = findViewById(R.id.local_view);//獲取預(yù)覽圖像的TextureView實例
engine.startPreview(new ZegoCanvas(local_view));//開始預(yù)覽
button.setText("停止推流");//文字從推流改變?yōu)橥V雇屏?/p>
} else {//若上面文字不是推流
/* 停止推流 */
engine.stopPublishingStream();//停止推流
/* 停止本地預(yù)覽 */
/* Start stop preview */
engine.stopPreview();//停止預(yù)覽
button.setText("推流");//文字變?yōu)橥屏?/p>
}
}
```
以拉流1按鈕為例,拉流的實現(xiàn)實際上與推流十分類似甚至還簡單一些。核心邏輯也是相似的判斷按鈕上的字是否為拉流1。核心的接口就是startPlayingStream和stopPlayingStream兩個。
要注意的是,我這里首先對要拉的流默認(rèn)是先靜音的。這里的playStreamMute是一個全局變量默認(rèn)值為True.
```java
public void ClickPlay(View view) {
...
if (button.getText().equals("拉流1")) {//若文字為拉流1
/* 開始拉流 */
/* Begin to play stream */
EditText et = findViewById(R.id.ed_play_stream_id);//獲取拉流旁的EditText實例
RemoteStreamID = et.getText().toString();//獲取其字符串,作為1號拉流ID
View play_view = findViewById(R.id.remote_view);//獲取播放實例
engine.startPlayingStream(RemoteStreamID, new ZegoCanvas(play_view));//開始拉流
engine.mutePlayStreamAudio(RemoteStreamID, playStreamMute);//首先對各用戶采取靜音
button.setText("停止拉流");//文字變?yōu)橥V估?/p>
} else {
/* 停止拉流 */
/* Begin to stop play stream */
engine.stopPlayingStream(RemoteStreamID);//停止拉流
button.setText("拉流1");//文字轉(zhuǎn)變?yōu)槔?
}
}
```
**(2)**.麥克風(fēng)按鈕處理
首先來說本地話筒,如下的publishMicEnable是一個bool類型的全局變量,初始值為true,根據(jù)注釋可以大概看懂。
要注意的就是核心接口engine.muteMicrophone(!publishMicEnable);這里括號中是對于publishMicEnable進(jìn)行了取反的,原因可以根據(jù)變量的名字就能看出來,他**mute**Microphone我們是publishMic**Enable**,二者意義本身就相反,所以取反之后才能體現(xiàn)原本意義。
```java
//本地麥克風(fēng)
public void enableLocalMic(View view) {
publishMicEnable = !publishMicEnable;//將bool變量先取反,即狀態(tài)改變
if (publishMicEnable) {//本地麥克風(fēng)經(jīng)取反后為真,那么就把圖標(biāo)變?yōu)殚_啟狀態(tài)的圖標(biāo)
ib_local_mic.setBackgroundDrawable(getResources().getDrawable(R.drawable.ic_bottom_microphone_on));
} else {//反之,則變?yōu)殛P(guān)閉狀態(tài)的圖標(biāo)
ib_local_mic.setBackgroundDrawable(getResources().getDrawable(R.drawable.ic_bottom_microphone_off));
}
/* Enable Mic*/
engine.muteMicrophone(!publishMicEnable);//因為這個函數(shù)是mute,而我們是enable,所以取反才與本義相同
}
```
其次對于遠(yuǎn)端拉流麥克風(fēng)的控制,以拉流1所收畫面的麥克風(fēng)為例。與本地麥克風(fēng)相似,核心函數(shù)有所不同。此處的核心函數(shù)為engine.mutePlayStreamAudio(RemoteStreamID, playStreamMute),這里面的兩個參數(shù)也都是全局變量,playStreamMute也是一個bool類型的變量,初始值為true。而這個RemoteStreamID這個全局變量在之前的拉流時所進(jìn)行賦值的。
```java
public void enableRemoteMic(View view) {
playStreamMute = !playStreamMute;//先將此bool變量取反,即狀態(tài)改變
if (playStreamMute) {//若此時該bool變量為真,則說明是靜音狀態(tài),則圖標(biāo)變?yōu)殛P(guān)閉狀態(tài)圖標(biāo)
ib_remote_stream_audio.setBackgroundDrawable(getResources().getDrawable(R.drawable.ic_bottom_microphone_off));
} else {//反之則變?yōu)殚_啟狀態(tài)
ib_remote_stream_audio.setBackgroundDrawable(getResources().getDrawable(R.drawable.ic_bottom_microphone_on));
}
/* Enable Mic*/
engine.mutePlayStreamAudio(RemoteStreamID, playStreamMute);//此處因為bool變量實際意義與函數(shù)本義相同,故不用取反
}
```
**(3)**.與本地相機美顏、改變攝像頭前后置等本地擴展功能?
這部分功能就比較簡單了,實現(xiàn)的邏輯與麥克風(fēng)相似,也是對于一個bool型全局變量進(jìn)行判斷。
相機美顏實現(xiàn)如下,核心的接口就是engine.enableBeautify(WHITEN);代碼當(dāng)中的isBeauty為一個全局變量的bool值。
這里我只使用了美白參數(shù)進(jìn)行使用,還可以添加其他的參數(shù),詳情見頂部連接中點開API文檔。
```java
public void enableBeauty(View view) {
isBeauty = !isBeauty;//取反
//更換圖標(biāo)
if (isBeauty) {//若現(xiàn)在為真,則變?yōu)槭褂妹李亴?yīng)圖標(biāo)
ib_beauty.setBackgroundDrawable(getResources().getDrawable(R.drawable.beauty_ps));
} else {//若現(xiàn)在為假,則對應(yīng)普通狀態(tài)圖標(biāo)
ib_beauty.setBackgroundDrawable(getResources().getDrawable(R.drawable.normal));
}
if (isBeauty) {//如果處于美顏狀態(tài)
//這里只采用一個較為明顯的美白功能
engine.enableBeautify(WHITEN);
} else {//反之則關(guān)閉所有美顏設(shè)置
engine.enableBeautify(NONE);
}
}
```
改變攝像頭方向,因為不需要更換圖標(biāo)所以更為簡單,核心接口是engine.useFrontCamera(isFrontCamera);其中isFront為bool類型的全局變量
```java
public void frontCamera(View view)
{
isFrontCamera = !isFrontCamera;//先去反
engine.useFrontCamera(isFrontCamera);//根據(jù)現(xiàn)有布爾值帶入是否使用前置攝像頭的函數(shù)中
}
```
**(4)**.退出按鈕
這部分要注意的是,退出按鈕要同時考慮到退出之后還要回到房間登錄界面,以及若當(dāng)前是推流狀態(tài)要停止當(dāng)前推流,以及退出用戶對于房間的登錄。實現(xiàn)起來還是比較簡單的代碼如下,其中使用了顯示intent來進(jìn)行活動的啟動,roomID為一個全局變量。在初始化的主函數(shù)中就以及賦值為了從之前登錄界面所帶來的roomID.
```java
public void Logout(View view) {
Intent intent = new Intent(this, Login.class);//設(shè)置一個從當(dāng)前活動到Login活動的intent
engine.stopPublishingStream();//停止推流
engine.logoutRoom(roomID);//退出該房間
startActivity(intent);//重新進(jìn)入房間的登錄界面
finish();//結(jié)束當(dāng)前活動
}
```
這樣就寫完了,怎么樣是不是非常簡單!真正要自己去想的也就是一些邏輯的處理,大大節(jié)省了開發(fā)的時間。
三.源代碼
**1.**兩組layout
登錄界面layout
```xml
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
android:layout_width="match_parent"
android:layout_height="557dp"
android:gravity="center_vertical"
android:orientation="vertical">
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="請輸入房間號"
android:textSize="22dp"
android:layout_gravity="center_horizontal"
android:textAlignment="center"
/>
android:id="@+id/room_login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical" />
```
核心視頻UI的layout (有點長。。)
```xml
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="1dp">
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_width="match_parent"
android:layout_height="273dp"
android:background="#8D8B8B"
android:orientation="horizontal">
android:id="@+id/view"
android:layout_width="3dp"
android:layout_height="match_parent"
android:layout_centerHorizontal="true"
/>
android:id="@+id/local_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginBottom="3dp"
android:layout_marginEnd="4dp"
android:layout_marginLeft="3dp"
android:layout_marginRight="4dp"
android:layout_marginStart="3dp"
android:layout_marginTop="3dp"
android:layout_toLeftOf="@id/view"
android:layout_toStartOf="@id/view" />
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="33dp"
android:layout_marginEnd="0dp"
android:layout_marginRight="0dp"
android:layout_toLeftOf="@id/view"
android:layout_toStartOf="@id/view"
android:gravity="center"
android:text="LOCAL"
android:textColor="#ffffff"
/>
android:id="@+id/ib_local_camera_change"
android:layout_width="33dp"
android:layout_height="33dp"
android:layout_alignParentBottom="true"
android:layout_marginRight="77dp"
android:layout_marginBottom="7dp"
android:layout_toStartOf="@id/view"
android:layout_toLeftOf="@id/view"
android:background="@drawable/arrow"
android:onClick="frontCamera" />
android:id="@+id/ib_local_beauti"
android:layout_width="33dp"
android:layout_height="33dp"
android:layout_alignParentBottom="true"
android:layout_marginRight="42dp"
android:layout_marginBottom="7dp"
android:layout_toStartOf="@id/view"
android:layout_toLeftOf="@id/view"
android:background="@drawable/normal"
android:onClick="enableBeauty" />
android:id="@+id/ib_local_mic"
android:layout_width="33dp"
android:layout_height="33dp"
android:layout_alignParentBottom="true"
android:layout_marginRight="7dp"
android:layout_marginBottom="7dp"
android:layout_toStartOf="@id/view"
android:layout_toLeftOf="@id/view"
android:background="@drawable/ic_bottom_microphone_on"
android:onClick="enableLocalMic" />
android:id="@+id/remote_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginBottom="3dp"
android:layout_marginEnd="3dp"
android:layout_marginLeft="6dp"
android:layout_marginRight="3dp"
android:layout_marginStart="6dp"
android:layout_marginTop="3dp"
android:layout_toEndOf="@id/view"
android:layout_toRightOf="@id/view" />
android:id="@+id/textView2"
android:layout_width="match_parent"
android:layout_height="33dp"
android:layout_toEndOf="@id/view"
android:layout_toRightOf="@id/view"
android:gravity="center"
android:text="REMOTE"
android:textColor="#ffffff" />
android:id="@+id/ib_remote_mic"
android:layout_width="33dp"
android:layout_height="33dp"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:layout_marginEnd="7dp"
android:layout_marginRight="7dp"
android:layout_marginBottom="7dp"
android:background="@drawable/ic_bottom_microphone_off"
android:onClick="enableRemoteMic" />
android:layout_width="match_parent"
android:layout_height="273dp"
android:background="#8D8B8B"
android:orientation="horizontal">
android:id="@+id/view2"
android:layout_width="3dp"
android:layout_height="match_parent"
android:layout_centerHorizontal="true"
/>
android:id="@+id/remote_view2"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginBottom="3dp"
android:layout_marginEnd="5dp"
android:layout_marginLeft="3dp"
android:layout_marginRight="5dp"
android:layout_marginStart="3dp"
android:layout_marginTop="3dp"
android:layout_toLeftOf="@id/view2"
android:layout_toStartOf="@id/view2" />
android:id="@+id/textView3"
android:layout_width="match_parent"
android:layout_height="33dp"
android:layout_marginEnd="0dp"
android:layout_marginRight="0dp"
android:layout_toLeftOf="@id/view2"
android:layout_toStartOf="@id/view2"
android:gravity="center"
android:text="REMOTE2"
android:textColor="#ffffff"
/>
android:id="@+id/ib_remote_mic2"
android:layout_width="33dp"
android:layout_height="33dp"
android:layout_alignParentBottom="true"
android:layout_marginRight="7dp"
android:layout_marginBottom="7dp"
android:layout_toStartOf="@id/view2"
android:layout_toLeftOf="@id/view2"
android:background="@drawable/ic_bottom_microphone_off"
android:onClick="enableRemoteMic2" />
android:id="@+id/remote_view3"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginBottom="3dp"
android:layout_marginEnd="3dp"
android:layout_marginLeft="0dp"
android:layout_marginRight="3dp"
android:layout_marginStart="0dp"
android:layout_marginTop="3dp"
android:layout_toEndOf="@id/view2"
android:layout_toRightOf="@id/view2" />
android:id="@+id/textView4"
android:layout_width="match_parent"
android:layout_height="33dp"
android:layout_toEndOf="@id/view2"
android:layout_toRightOf="@id/view2"
android:gravity="center"
android:text="REMOTE3"
android:textColor="#ffffff" />
android:id="@+id/ib_remote_mic3"
android:layout_width="33dp"
android:layout_height="33dp"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:layout_marginEnd="7dp"
android:layout_marginRight="7dp"
android:layout_marginBottom="7dp"
android:background="@drawable/ic_bottom_microphone_off"
android:onClick="enableRemoteMic3" />
android:id="@+id/stream_id_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="當(dāng)前房間內(nèi)的推流有:"
android:textSize="15dp"
/>
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="本地ID"
android:layout_gravity="center_horizontal"
android:textAlignment="center"
/>
android:layout_width="wrap_content"
android:layout_height="wrap_content">
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="一號遠(yuǎn)端ID"
android:layout_gravity="center_horizontal"
android:textAlignment="center"
/>
android:layout_width="wrap_content"
android:layout_height="wrap_content">
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="二號遠(yuǎn)端ID"
android:layout_gravity="center_horizontal"
android:textAlignment="center"
/>
android:layout_width="wrap_content"
android:layout_height="wrap_content">
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="三號遠(yuǎn)端ID"
android:layout_gravity="center_horizontal"
android:textAlignment="center"
/>
android:layout_width="wrap_content"
android:layout_height="wrap_content">
```
**2.**活動java源代碼
登錄界面.java
```java
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
public class Login extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);//設(shè)置布局
Button login = findViewById(R.id.btn_login);//獲取登錄按鈕實例
login.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {//匿名類實現(xiàn)監(jiān)聽功能
EditText roomIDx = findViewById(R.id.room_login);//獲取用于輸入的EditText的實例
String roomID = roomIDx.getText().toString().trim();//獲取其中的文字,也就是對應(yīng)的roomID
if (roomID.equals("")) {//檢查此ID是否為空,為空則彈出,請輸入信息。
Toast.makeText(Login.this, "請輸入roomID", Toast.LENGTH_LONG).show();
}
else {//反之啟動活動UI
Intent intent =new Intent(Login.this, UI.class);//創(chuàng)建一個顯式intent
intent.putExtra("room_id", roomID);//并將房間號作為夸活動傳輸?shù)臄?shù)據(jù)傳輸?shù)経I活動當(dāng)中
startActivity(intent);//啟動Activity
finish();//結(jié)束活動
}
}
});
}
}
```
核心視頻UI.java
```java
import android.content.pm.PackageManager;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import im.zego.zegoexpress.ZegoExpressEngine;
import im.zego.zegoexpress.constants.ZegoRoomState;
import im.zego.zegoexpress.constants.ZegoUpdateType;
import im.zego.zegoexpress.entity.ZegoCanvas;
import im.zego.zegoexpress.entity.ZegoStream;
import im.zego.zegoexpress.entity.ZegoUser;
import im.zego.zegoexpress.callback.IZegoEventHandler;
import im.zego.zegoexpress.constants.ZegoScenario;
import android.content.Intent;
import android.os.Build;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
// 導(dǎo)入對應(yīng)美顏參數(shù)的常量值
import static im.zego.zegoexpress.constants.ZegoBeautifyFeature.*;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Date;
public class UI extends AppCompatActivity {
public static ZegoExpressEngine engine = null;
boolean publishMicEnable = true; // 初始的自己麥克風(fēng)為開著的
boolean playStreamMute = true; //其余屏幕人的初始狀態(tài)都為靜音
boolean playStreamMute2 = true;
boolean playStreamMute3 = true;
boolean isBeauty = false;//初始無美顏
boolean isFrontCamera = true; // 初始為前置攝像頭
ImageButton ib_local_mic; //本地麥克風(fēng)
ImageButton ib_remote_stream_audio;//拉流1外部視角的音量
ImageButton ib_remote_stream_audio2;//拉流2外部視角的音量
ImageButton ib_remote_stream_audio3;//拉流3外部視角的音量
ImageButton ib_beauty; //美顏按鍵
String LocalStreamID; //本地推流ID
String RemoteStreamID; //拉流1 ID
String RemoteStreamID2; //拉流2 ID
String RemoteStreamID3;//拉流3 ID
ArrayList
private String userID;//用戶ID
String roomID;//房間ID
//寫好自己的ID和sign,以下為我所申請的ID,如果要自己使用或者商用請自行申請并修改
long appID = ;? // 請通過官網(wǎng)注冊獲取,格式為 123456789L
String appSign = ;? //64個字符,請通過官網(wǎng)注冊獲取,格式為"0123456789012345678901234567890123456789012345678901234567890123"
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
/* 填寫 appID 和 appSign */
/* 初始化SDK,使用測試環(huán)境,通用場景接入,此為自動初始化,無需點擊按鈕*/
engine = ZegoExpressEngine.createEngine(appID, appSign, true, ZegoScenario.GENERAL, getApplication(), null);
setContentView(R.layout.activity_main);
//登錄
/* 創(chuàng)建用戶 */
/* 生成隨機的用戶ID,避免不同手機使用時用戶ID沖突,相互影響 */
/* Generate random user ID to avoid user ID conflict and mutual influence when different mobile phones are used */
String randomSuffix = String.valueOf(new Date().getTime() % (new Date().getTime() / 1000));
userID = "user" + randomSuffix;
ZegoUser user = new ZegoUser(userID);
//初始化房間內(nèi)流id數(shù)組
RoomStreamList = new ArrayList
RoomStreamList.add("當(dāng)前房間內(nèi)的推流有:");
/* 開始登陸房間 */
//房間狀態(tài)改變,時間處理
engine.setEventHandler(new IZegoEventHandler() {
/** 以下為常用的房間相關(guān)回調(diào) */
public void onRoomStateUpdate(String roomID, ZegoRoomState state, int errorCode, JSONObject extendedData) {
//房間狀態(tài)改變,提示信息
Toast.makeText(getApplicationContext(), "room state changed", Toast.LENGTH_SHORT).show();
}
public void onRoomUserUpdate(String roomID, ZegoUpdateType updateType, ArrayList
/* 用戶狀態(tài)更新,登陸房間后,當(dāng)房間內(nèi)有用戶新增或刪除時,SDK會通過該回調(diào)通知 */
//....
//用戶加入提示信息
Toast.makeText(getApplicationContext(), userList.get(0) + "加入房間", Toast.LENGTH_LONG).show();
}
public void onRoomStreamUpdate(String roomID, ZegoUpdateType updateType, ArrayList
/* 流狀態(tài)更新,登陸房間后,當(dāng)房間內(nèi)有用戶新推送或刪除音視頻流時,SDK會通過該回調(diào)通知 */
//自己的推流不會被記入
for (int i = 0; i < streamList.size(); i++)//加入或退出房間流的所有推流id全都遍歷一遍
{
Toast.makeText(getApplicationContext(), streamList.get(i).streamID + " room stream changed", Toast.LENGTH_LONG).show();
if (RoomStreamList.contains(streamList.get(i).streamID)) {//如果現(xiàn)有列表中包含這個,就移除
RoomStreamList.remove(streamList.get(i).streamID);
} else {//如果現(xiàn)有列表中不包含這個就加入
RoomStreamList.add(streamList.get(i).streamID);
}
}
String SentenceId = "";// 用于記錄下當(dāng)前房間內(nèi)還有的流ID
for (int i = 0; i < RoomStreamList.size(); i++) {
SentenceId += RoomStreamList.get(i) + " ";//利用字符串拼接,將當(dāng)前房間還在的所有流ID全部記下
}
TextView ViewIdlist = findViewById(R.id.stream_id_list);//找到用于顯示流ID的TextView
ViewIdlist.setText(SentenceId);//設(shè)置文字信息在TextView上體現(xiàn)出來
}
});
//房間ID為Login活動傳遞過來的
Intent intent = getIntent();
roomID = intent.getStringExtra("room_id");//getXxxExtra方法獲取Intent傳遞過來的roomID
engine.loginRoom(roomID, user);//有了房間號,將用戶登錄到該房間
// 麥克風(fēng)
ib_local_mic = findViewById(R.id.ib_local_mic);//找到本地麥克風(fēng)圖標(biāo)
/* 音頻播放是否靜音的開關(guān) */
/* Switch for mute audio output */
ib_remote_stream_audio = findViewById(R.id.ib_remote_mic);//找到拉流1麥克風(fēng)圖標(biāo)并賦值給之前定義的全局變量
ib_remote_stream_audio2 = findViewById(R.id.ib_remote_mic2);//找到拉流2麥克風(fēng)圖標(biāo)并賦值給之前定義的全局變量
ib_remote_stream_audio3 = findViewById(R.id.ib_remote_mic3);//找到拉流3麥克風(fēng)圖標(biāo)并賦值給之前定義的全局變量
ib_beauty = findViewById(R.id.ib_local_beauti);//找到美顏圖標(biāo)
//動態(tài)申請權(quán)限
String[] permissionNeeded = {
"android.permission.CAMERA",
"android.permission.RECORD_AUDIO"};
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(this, "android.permission.CAMERA") != PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(this, "android.permission.RECORD_AUDIO") != PackageManager.PERMISSION_GRANTED) {
requestPermissions(permissionNeeded, 101);
}
}
}
// Part I 推拉流按鈕處理
/*點擊推流按鈕進(jìn)行推流 */
/*
Click Publish Button
*/
public void ClickPublish(View view) {
if (engine == null) {//之前自動初始化的SDK若未初始化成功則會彈出以下內(nèi)容
Toast.makeText(this, "please initiate your sdk first!", Toast.LENGTH_SHORT).show();
return;
}
Button button = (Button) view;//獲取推流這個按鈕的實例
if (button.getText().equals("推流")) {//若上面的文字是推流,則說明還未推流。
EditText et = findViewById(R.id.ed_publish_stream_id);//找到,旁邊的EditText的實例
LocalStreamID = et.getText().toString();//獲取其文字內(nèi)容,并賦值給全局變量LocalStreamID
/* 開始推流 */
/* Begin to publish stream */
engine.startPublishingStream(LocalStreamID);//推流
Toast.makeText(this, "published", Toast.LENGTH_SHORT).show();//推流成功文字提示
/* 開始預(yù)覽并設(shè)置本地預(yù)覽視圖 */
/* Start preview and set the local preview view. */
View local_view = findViewById(R.id.local_view);//獲取預(yù)覽圖像的TextureView實例
engine.startPreview(new ZegoCanvas(local_view));//開始預(yù)覽
Toast.makeText(this, "preview is set", Toast.LENGTH_SHORT).show();//提示預(yù)覽設(shè)置成功
button.setText("停止推流");//文字從推流改變?yōu)橥V雇屏?/p>
} else {//若上面文字不是推流
/* 停止推流 */
/* Begin to stop publish stream */
engine.stopPublishingStream();//停止推流
/* 停止本地預(yù)覽 */
/* Start stop preview */
engine.stopPreview();//停止預(yù)覽
Toast.makeText(this, "publishing has stopped", Toast.LENGTH_SHORT).show();//提示停止已成功
button.setText("推流");//文字變?yōu)橥屏?/p>
}
}
/* 點擊拉流1按鈕*/
/*
Click Play Button
*/
//由于如下三個按鈕,實現(xiàn)代碼大同小異所以就只 詳寫 此按鈕注釋,其他實現(xiàn)原理一致
public void ClickPlay(View view) {
if (engine == null) {//之前自動初始化的SDK若未初始化成功則會彈出以下內(nèi)容
Toast.makeText(this, "please initiate your sdk first!", Toast.LENGTH_SHORT).show();
return;
}
Button button = (Button) view;//獲取按鈕實例
if (button.getText().equals("拉流1")) {//若文字為拉流1
EditText et = findViewById(R.id.ed_play_stream_id);//獲取拉流旁的EditText實例
RemoteStreamID = et.getText().toString();//獲取其字符串,作為1號拉流ID
/* 開始拉流 */
/* Begin to play stream */
View play_view = findViewById(R.id.remote_view);//獲取播放實例
engine.startPlayingStream(RemoteStreamID, new ZegoCanvas(play_view));//開始拉流
engine.mutePlayStreamAudio(RemoteStreamID, playStreamMute);//首先對各用戶采取靜音
Toast.makeText(this, "Remote1 played successfully", Toast.LENGTH_SHORT).show();//提示拉流畫面播放成功
button.setText("停止拉流");//文字變?yōu)橥V估?/p>
} else {
/* 停止拉流 */
/* Begin to stop play stream */
engine.stopPlayingStream(RemoteStreamID);//停止拉流
Toast.makeText(this, "Remote1 stopped successfully", Toast.LENGTH_SHORT).show();//提示停止拉流成功
button.setText("拉流1");//文字轉(zhuǎn)變?yōu)槔?
}
}
/* 點擊拉流2按鈕 */
/*
Click Play Button
*/
//與拉流1按鈕相似
public void ClickPlay2(View view) {
if (engine == null) {//之前自動初始化的SDK若未初始化成功則會彈出以下內(nèi)容
Toast.makeText(this, "please initiate your sdk first!", Toast.LENGTH_SHORT).show();
return;
}
Button button = (Button) view;
if (button.getText().equals("拉流2")) {
EditText et = findViewById(R.id.ed_play_stream_id2);
RemoteStreamID2 = et.getText().toString();
/* 開始拉流 */
/* Begin to play stream */
View play_view = findViewById(R.id.remote_view2);
engine.startPlayingStream(RemoteStreamID2, new ZegoCanvas(play_view));
engine.mutePlayStreamAudio(RemoteStreamID2, playStreamMute2);
Toast.makeText(this, "Remote2 played successfully", Toast.LENGTH_SHORT).show();
button.setText("停止拉流");
} else {
/* 停止拉流 */
/* Begin to stop play stream */
engine.stopPlayingStream(RemoteStreamID2);
Toast.makeText(this, "Remote2 stopped successfully", Toast.LENGTH_SHORT).show();
button.setText("拉流2");
}
}
/* 點擊拉流按鈕3 */
/*
Click Play Button
*/
//與拉流1按鈕相似
public void ClickPlay3(View view) {
if (engine == null) {//之前自動初始化的SDK若未初始化成功則會彈出以下內(nèi)容
Toast.makeText(this, "please initiate your sdk first!", Toast.LENGTH_SHORT).show();
return;
}
Button button = (Button) view;
if (button.getText().equals("拉流3")) {
EditText et = findViewById(R.id.ed_play_stream_id3);
RemoteStreamID3 = et.getText().toString();
View play_view = findViewById(R.id.remote_view3);
/* 開始拉流 */
/* Begin to play stream */
engine.startPlayingStream(RemoteStreamID3, new ZegoCanvas(play_view));
engine.mutePlayStreamAudio(RemoteStreamID3, playStreamMute3);
Toast.makeText(this, "Remote3 played successfully", Toast.LENGTH_SHORT).show();
button.setText("停止拉流");
} else {
/* 停止拉流 */
/* Begin to stop play stream */
EditText et = findViewById(R.id.ed_play_stream_id3);
engine.stopPlayingStream(RemoteStreamID3);
Toast.makeText(this, "Remote3 stopped successfully", Toast.LENGTH_SHORT).show();
button.setText("拉流3");
}
}
// Part II 麥克風(fēng)按鈕處理
//本地麥克風(fēng)
public void enableLocalMic(View view) {
if (engine == null) {//之前自動初始化的SDK若未初始化成功則會彈出以下內(nèi)容
Toast.makeText(this, "please initiate your sdk first!", Toast.LENGTH_SHORT).show();
return;
}
publishMicEnable = !publishMicEnable;//將bool變量先取反,即狀態(tài)改變
if (publishMicEnable) {//本地麥克風(fēng)經(jīng)取反后為真,那么就把圖標(biāo)變?yōu)殚_啟狀態(tài)的圖標(biāo)
ib_local_mic.setBackgroundDrawable(getResources().getDrawable(R.drawable.ic_bottom_microphone_on));
} else {//反之,則變?yōu)殛P(guān)閉狀態(tài)的圖標(biāo)
ib_local_mic.setBackgroundDrawable(getResources().getDrawable(R.drawable.ic_bottom_microphone_off));
}
/* Enable Mic*/
engine.muteMicrophone(!publishMicEnable);//因為這個函數(shù)是mute,而我們是enable,所以取反才與本義相同
}
//一號拉流麥克風(fēng)處理,二三號也大同小異
public void enableRemoteMic(View view) {
if (engine == null) {//之前自動初始化的SDK若未初始化成功則會彈出以下內(nèi)容
Toast.makeText(this, "please initiate your sdk first!", Toast.LENGTH_SHORT).show();
return;
}
playStreamMute = !playStreamMute;//先將此bool變量取反,即狀態(tài)改變
if (playStreamMute) {//若此時該bool變量為真,則說明是靜音狀態(tài),則圖標(biāo)變?yōu)殛P(guān)閉狀態(tài)圖標(biāo)
ib_remote_stream_audio.setBackgroundDrawable(getResources().getDrawable(R.drawable.ic_bottom_microphone_off));
} else {//反之則變?yōu)殚_啟狀態(tài)
ib_remote_stream_audio.setBackgroundDrawable(getResources().getDrawable(R.drawable.ic_bottom_microphone_on));
}
/* Enable Mic*/
engine.mutePlayStreamAudio(RemoteStreamID, playStreamMute);//此處因為bool變量實際意義與函數(shù)本義相同,故不用取反
}
//二號拉流麥克風(fēng)處理,與一號類似
public void enableRemoteMic2(View view) {
if (engine == null) {//之前自動初始化的SDK若未初始化成功則會彈出以下內(nèi)容
Toast.makeText(this, "please initiate your sdk first!", Toast.LENGTH_SHORT).show();
return;
}
playStreamMute2 = !playStreamMute2;
if (playStreamMute2) {
ib_remote_stream_audio2.setBackgroundDrawable(getResources().getDrawable(R.drawable.ic_bottom_microphone_off));
} else {
ib_remote_stream_audio2.setBackgroundDrawable(getResources().getDrawable(R.drawable.ic_bottom_microphone_on));
}
/* Enable Mic*/
engine.mutePlayStreamAudio(RemoteStreamID2, playStreamMute2);
}
//三號拉流麥克風(fēng)處理,與一號類似
public void enableRemoteMic3(View view) {
if (engine == null) {//之前自動初始化的SDK若未初始化成功則會彈出以下內(nèi)容
Toast.makeText(this, "please initiate your sdk first!", Toast.LENGTH_SHORT).show();
return;
}
playStreamMute3 = !playStreamMute3;
if (playStreamMute3) {
ib_remote_stream_audio3.setBackgroundDrawable(getResources().getDrawable(R.drawable.ic_bottom_microphone_off));
} else {
ib_remote_stream_audio3.setBackgroundDrawable(getResources().getDrawable(R.drawable.ic_bottom_microphone_on));
}
/* Enable Mic*/
engine.mutePlayStreamAudio(RemoteStreamID3, playStreamMute3);
}
// Part III 本地相機美顏、改變攝像頭前后置等擴展功能
//美顏功能
public void enableBeauty(View view) {
if (engine == null) {//之前自動初始化的SDK若未初始化成功則會彈出以下內(nèi)容
Toast.makeText(this, "please initiate your sdk first!", Toast.LENGTH_SHORT).show();
return;
}
isBeauty = !isBeauty;//取反
//更換圖標(biāo)
if (isBeauty) {//若現(xiàn)在為真,則變?yōu)槭褂妹李亴?yīng)圖標(biāo)
ib_beauty.setBackgroundDrawable(getResources().getDrawable(R.drawable.beauty_ps));
} else {//若現(xiàn)在為假,則對應(yīng)普通狀態(tài)圖標(biāo)
ib_beauty.setBackgroundDrawable(getResources().getDrawable(R.drawable.normal));
}
if (isBeauty) {//如果處于美顏狀態(tài)
//這里只采用一個較為明顯的美白功能
engine.enableBeautify(WHITEN);
} else {//反之則關(guān)閉所有美顏設(shè)置
engine.enableBeautify(NONE);
}
}
//調(diào)用后置攝像頭
public void frontCamera(View view)
{
if (engine == null) {//之前自動初始化的SDK若未初始化成功則會彈出以下內(nèi)容
Toast.makeText(this, "please initiate your sdk first!", Toast.LENGTH_SHORT).show();
return;
}
isFrontCamera = !isFrontCamera;//先去反
engine.useFrontCamera(isFrontCamera);//根據(jù)現(xiàn)有布爾值帶入是否使用前置攝像頭的函數(shù)中
}
// Part IV 退出按鈕
public void Logout(View view) {
Intent intent = new Intent(this, Login.class);//設(shè)置一個從當(dāng)前活動到Login活動的intent
engine.stopPublishingStream();//停止推流
engine.logoutRoom(roomID);//退出該房間
startActivity(intent);//重新進(jìn)入房間的登錄界面
finish();//結(jié)束當(dāng)前活動
}
}
```
四.功能上的不足以及可以繼續(xù)開發(fā)的地方
1.為了更加方便人的使用,可以將對應(yīng)房間的推流id自動進(jìn)行拉流。
2.擴展功能可以增加一些其他其他的,類似像是只聽見其他用戶的聲音關(guān)閉其畫面
3.利用其他技術(shù),實現(xiàn)手機端的屏幕共享。
4.可以通過RecyclerView等或其他方式,從而實現(xiàn)房間內(nèi)一個用戶界面能顯示更多畫面,使得最多拉的流數(shù)>3
5.當(dāng)前拉的某個流停止時,畫面靜止,可以考慮讓其轉(zhuǎn)換為黑屏。
評論
查看更多