前言 (閑聊)
之前在上移動(dòng)平臺(tái)開(kāi)發(fā)課的過(guò)程中,對(duì)android的開(kāi)發(fā)算是有一個(gè)大概的初步了解,但是知之甚淺。印象最深刻的就是但凡遇到圖片視頻方面的處理就會(huì)變得非常復(fù)雜以及容易出錯(cuò)。那時(shí)對(duì)于我這個(gè)小白來(lái)說(shuō)想調(diào)用一個(gè)視頻播放器來(lái)播放一小段視頻都是一個(gè)"大"工程了,至于什么實(shí)時(shí)的視頻對(duì)話想都不想去想,因?yàn)樘珡?fù)雜且麻煩!!!
但是有了功能齊全的SDK ,這次的實(shí)時(shí)視頻開(kāi)發(fā),卻是與以前完全不同的體驗(yàn)。直觀感受就好像是我這種菜雞做機(jī)器學(xué)習(xí)模型有了Python的sklearn庫(kù),菜雞的大雄有了多啦A夢(mèng)那樣,擁有了一個(gè)萬(wàn)能的百寶箱。當(dāng)你想實(shí)現(xiàn)一個(gè)想著就十分復(fù)雜的功能時(shí)例如直播的推拉流之類的,這里面就已經(jīng)集成了對(duì)應(yīng)的函數(shù)。
所用SDK介紹
關(guān)于SDK的安裝本文不做過(guò)多描述,我使用的是ZEGO EXPRESS SDK,相應(yīng)的安裝詳細(xì)過(guò)程請(qǐng)直接看鏈接
https://doc-zh.zego.im/zh/215.html,同時(shí)記得按照步驟申請(qǐng)對(duì)應(yīng)的AppID以及AppSign.
使用此SDK的優(yōu)點(diǎn):代碼簡(jiǎn)單易懂,文檔內(nèi)容較為全面,實(shí)現(xiàn)簡(jiǎn)單。
本文實(shí)際內(nèi)容可以有些長(zhǎng),所以可以根據(jù)目錄篩選查看內(nèi)容
目錄
一.所實(shí)現(xiàn)項(xiàng)目的功能
1.項(xiàng)目實(shí)現(xiàn)核心截圖
2.所實(shí)現(xiàn)的功能
3.適用的應(yīng)用場(chǎng)景
二.實(shí)現(xiàn)流程
1.布局的設(shè)計(jì)
2.核心邏輯代碼
2.1推流和拉流的概念
2.2正式開(kāi)始及全局變量的聲明
2.2.1 onCreate函數(shù)內(nèi)操作
1.申請(qǐng)AppID
2.初始化SDK
3.初始化用戶及登錄房間
4.獲取所在房間內(nèi)的所有推流
2.2.2 點(diǎn)擊事件
1.推拉流
2.麥克風(fēng)按鈕處理
3.與本地相機(jī)美顏、改變攝像頭前后置等本地?cái)U(kuò)展功能
4.退出按鈕
三.源代碼
四.不足以及可以繼續(xù)開(kāi)發(fā)的地方
一.所實(shí)現(xiàn)項(xiàng)目的功能
1.項(xiàng)目實(shí)現(xiàn)核心截圖
登錄界面

核心視頻UI

2.所實(shí)現(xiàn)的功能
(1).從登錄界面到視頻界面的跳轉(zhuǎn),以及傳遞所輸入的房間號(hào)ID。以及在核心視頻UI界面的退出到登錄界面的跳轉(zhuǎn).
(2) 四人可同時(shí)正常視頻通話,且對(duì)應(yīng)每個(gè)流所呈現(xiàn)的畫(huà)面進(jìn)行打開(kāi)關(guān)閉收音.
(3).實(shí)時(shí)的將同一房間號(hào)的正在推流的流ID全部顯示在第二排視頻下的TextView上(在該房間的用戶可根據(jù)次項(xiàng)流ID內(nèi)容)
(4).實(shí)現(xiàn)對(duì)本地界面中的前后置攝像頭進(jìn)行切換,美顏效果的實(shí)現(xiàn)。
3.適用的應(yīng)用場(chǎng)景
家庭聊天,同事或者同學(xué)聊天以及簡(jiǎn)單的面對(duì)面會(huì)議。
二.實(shí)現(xiàn)流程
**1.**先說(shuō)簡(jiǎn)單說(shuō)一下界面布局的設(shè)計(jì)。這個(gè)相對(duì)簡(jiǎn)單,看上述的界面也大概也能明白一些,說(shuō)一下我做的過(guò)程中遇到的問(wèn)題吧。
第一個(gè)因?yàn)橹恍枰斎敕块gID,所以登陸界面很簡(jiǎn)單,線性布局垂直方向, 再加上TextView + EditText + Button就解決了的最簡(jiǎn)單登陸界面。實(shí)在不懂可以直接看第三部分的源代碼。
第二個(gè)界面對(duì)于新手來(lái)說(shuō)還是有些困難的,首先由于四人視頻所占空間很大,一個(gè)屏幕的大小不能輕易放下,這種需要滾動(dòng)條的情況就可以采用三種手段來(lái)解決分別是RecyclerView,ListView以及ScrollView來(lái)解決。本文采用的是相對(duì)來(lái)說(shuō)最簡(jiǎn)單的ScrollView來(lái)解決。這里就要注意了,**ScrollView當(dāng)中只有一個(gè)子元素**,所以如果你想像我一樣,把ScrollView作為最外層的話,需要在內(nèi)部再嵌套一個(gè)線性布局,這樣才能用。
相連視頻的畫(huà)面采用的是相對(duì)布局,這樣更加方便做微調(diào),視頻本身使用TextureView來(lái)做。
我代碼的整體設(shè)計(jì)布局框架如下(要注意的是本文對(duì)于核心視頻UI的布局上圖片的點(diǎn)擊事件,都是在圖片的屬性中添加的android:onClick處理解決方法,這里看不懂沒(méi)關(guān)系,后面也會(huì)說(shuō)。)
```xml
...(省略拉流1和2的TextView+LinearLayout的組合,寫(xiě)法類似下面這個(gè)框架的)
```
更詳盡的代碼見(jiàn)第三部分的全部源碼
**2.**下面就是核心邏輯代碼的實(shí)現(xiàn)了
**注意:以下只展示核心的代碼,并不能直接運(yùn)行,具體操作請(qǐng)看第三部分。**
**2.1**首先,如果不理解**推流**和**拉流**的概念的話,首先要快速理解一下。
實(shí)際上我們可以用一個(gè)簡(jiǎn)單過(guò)程來(lái)幫助理解。
首先來(lái)說(shuō),推流就是你發(fā)送出去一串代碼(流ID)以及你本地的照相機(jī)所拍攝的實(shí)時(shí)畫(huà)面(所謂的流) 上網(wǎng)絡(luò)且到達(dá)服務(wù)器并存儲(chǔ)。且如果你不停止推流就要一直發(fā)送,就好像水流一樣源源不斷,但是服務(wù)器里面就好像是有門堵著的,水是不能輕易漏出來(lái)。
別人想在這個(gè)服務(wù)器上看你的視頻咋辦? 他就要拉你對(duì)應(yīng)的流,咋拉呢?就通過(guò)你發(fā)的那個(gè)推出去的流ID來(lái)拉。如果他知道這個(gè)流ID ,就好像是找到了服務(wù)器對(duì)應(yīng)一扇門的一把鎖鑰匙似的,把你不斷發(fā)送到服務(wù)器的"流"大門打開(kāi),水流涌出來(lái)了一直到他的手機(jī)上,這樣他就看到你的畫(huà)面了。
而你如果停止了推流,水就沒(méi)了,他自然就接收不到畫(huà)面了。他停止拉流,等于是把之前那扇門又關(guān)上了,他手機(jī)上也不會(huì)再接收你的畫(huà)面。
**2.2**理解之后,如果你的SDK已經(jīng)按照最頂上鏈接集成完畢后,開(kāi)發(fā)過(guò)程就可以正式開(kāi)始了!
以下要用到很多的全局變量,首先展示一下他們的聲明以及初始值,如果下面的有些看不懂的可以返回過(guò)來(lái)看一看這里所寫(xiě)的內(nèi)容,以及注釋。
```java
public static ZegoExpressEngine engine = null;
boolean publishMicEnable = true; // 初始的自己麥克風(fēng)為開(kāi)著的
boolean playStreamMute = true; //其余屏幕人的初始狀態(tài)都為靜音
boolean playStreamMute2 = true;
boolean playStreamMute3 = true;
boolean isBeauty = false;//初始無(wú)美顏
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
//寫(xiě)好自己的ID和sign,以下為我所申請(qǐng)的ID,如果要自己使用或者商用請(qǐng)自行申請(qǐng)并修改
long appID = ;? // 請(qǐng)通過(guò)官網(wǎng)注冊(cè)獲取,格式為 123456789L
String appSign = "";? //64個(gè)字符,請(qǐng)通過(guò)官網(wǎng)注冊(cè)獲取,格式為"0123456789012345678901234567890123456789012345678901234567890123"
```
**2.2.1** onCreate函數(shù)部分
**(1).**申請(qǐng)AppID,這一步如果不做的話根本后面做不了!!所以要先申請(qǐng)一各APPID,可以看我最上方附的那個(gè)鏈接?
**(2)**根據(jù)你的appID以及appsign進(jìn)行初始化SDK,使用測(cè)試環(huán)境,通用場(chǎng)景接入。如果這一步成功了,那么恭喜你,你已經(jīng)獲得了一個(gè)強(qiáng)大的神奇engine,他功能強(qiáng)大,后面所有所有都是依靠他來(lái)實(shí)現(xiàn)的,什么推流拉流就是一行代碼的事情.
```java
engine = ZegoExpressEngine.createEngine(appID, appSign, true, ZegoScenario.GENERAL, getApplication(), null);
```
**(3).**初始化用戶,并將用戶登錄至房間內(nèi)。這步也是在進(jìn)行視頻通話之前的必須一步,我們每個(gè)人都是在服務(wù)器上一個(gè)獨(dú)立的個(gè)體,想要實(shí)現(xiàn)特定用戶群體之間的交流。房間是很好的一個(gè)工具。這個(gè)userid和name在全局中不能有任何重復(fù),最好有一定意義,但我面向的場(chǎng)景主要是家庭場(chǎng)景,不太需要,如果是商用開(kāi)發(fā)還是很有必要開(kāi)發(fā)的。為了防止重復(fù),我采用的是生成隨機(jī)數(shù),這樣的話重復(fù)的概率就小很多了。
我這里的roomID,從登錄界面時(shí)的intent的所傳遞的數(shù)據(jù)來(lái)定義的
```java
//用戶注冊(cè)
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傳遞過(guò)來(lái)的roomID
engine.loginRoom(roomID, user);//有了房間號(hào),將用戶登錄到該房間
```
**(4).**獲取所在房間內(nèi)的所有推流。這里面主要就是要用到監(jiān)聽(tīng)房間相關(guān)事件回調(diào)來(lái)實(shí)現(xiàn)主要用到的是回調(diào)中的onRoomStreamUpdate 要注意的是:這里的流更新指的是房間內(nèi)其他用戶的,用戶自己的流產(chǎn)生變化,自己的這個(gè)回調(diào)函數(shù)是沒(méi)有反應(yīng)的。
想實(shí)現(xiàn)此功能,首先要?jiǎng)?chuàng)建一個(gè)ArrayList來(lái)記錄房間內(nèi)存在的所有的流ID。可以看到如下的RoomStreamList我是在全局變量的地方事先聲明過(guò)了。(其他全局變量的聲明我會(huì)放到后面函數(shù)部分說(shuō)明) 這里放入的第一個(gè)元素的目的是為了方便在后面講ArrayList中所有元素連接成為一行具體的內(nèi)容.
```java
RoomStreamList = new ArrayList
RoomStreamList.add("當(dāng)前房間內(nèi)的推流有:");
```
onRoomStreamUpdate的具體寫(xiě)法如下所示,看起來(lái)好像挺長(zhǎng),實(shí)際上思路就是如果有一個(gè)流ID狀態(tài)發(fā)生改變,我就看我的列表當(dāng)中是不是有這個(gè)流ID如果有那就去掉,如果沒(méi)有就加入進(jìn)去。實(shí)在不懂,根據(jù)注釋也能看個(gè)大概,這里面需要提一下的是sentenceId += 這個(gè)并不是真正的加法,而是java中的字符串拼接,將ArrayList中所有元素拼成一句話,并找到要顯示的TextView并顯示出來(lái)。
```java
engine.setEventHandler(new IZegoEventHandler() {
...
public void onRoomStreamUpdate(String roomID, ZegoUpdateType updateType, ArrayList
/* 流狀態(tài)更新,登陸房間后,當(dāng)房間內(nèi)有用戶新推送或刪除音視頻流時(shí),SDK會(huì)通過(guò)該回調(diào)通知 */
//自己的推流不會(huì)被記入
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)有列表中包含這個(gè),就移除
RoomStreamList.remove(streamList.get(i).streamID);
} else {//如果現(xiàn)有列表中不包含這個(gè)就加入
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)出來(lái)
}
});
```
**(5).**動(dòng)態(tài)權(quán)限申請(qǐng),代碼如下。若需要申請(qǐng)更多權(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** 接下來(lái)要做的就是逐步完成每個(gè)在視圖中注冊(cè)的點(diǎn)擊事件,其中包括四個(gè)部分,分別是1.推拉流 2.麥克風(fēng)按鈕處理
**3.**與本地相機(jī)美顏、改變攝像頭前后置等本地?cái)U(kuò)展功能 4.退出按鈕
**(1)**.推拉流
這也是視頻通話最為主要的部分。但是卻是十分簡(jiǎn)單的,核心的代碼就只需要調(diào)用兩個(gè)接口也就是兩行代碼就可以解決。但是還是有些要注意的事項(xiàng)。如下為推流**核心**代碼,有所省略,具體實(shí)現(xiàn)詳見(jiàn)第三部分。 實(shí)際上可以看出來(lái),核心的邏輯就是判斷推流按鈕上的字是否是"推流",如果是的話就就行推流再把文字設(shè)置稱為"停止推流"。
其中也包含了核心的接口就是startPublishingStream,stopPublishingStream以及startpreview和stoppreview來(lái)獲取本地圖像。
```java
public void ClickPublish(View view) {
...
if (button.getText().equals("推流")) {//若上面的文字是推流,則說(shuō)明還未推流。
/* 開(kāi)始推流 */
EditText et = findViewById(R.id.ed_publish_stream_id);//找到,旁邊的EditText的實(shí)例
LocalStreamID = et.getText().toString();//獲取其文字內(nèi)容,并賦值給全局變量LocalStreamID
engine.startPublishingStream(LocalStreamID);//推流
/* 開(kāi)始預(yù)覽并設(shè)置本地預(yù)覽視圖 */
/* Start preview and set the local preview view. */
View local_view = findViewById(R.id.local_view);//獲取預(yù)覽圖像的TextureView實(shí)例
engine.startPreview(new ZegoCanvas(local_view));//開(kāi)始預(yù)覽
button.setText("停止推流");//文字從推流改變?yōu)橥V雇屏?/p>
} else {//若上面文字不是推流
/* 停止推流 */
engine.stopPublishingStream();//停止推流
/* 停止本地預(yù)覽 */
/* Start stop preview */
engine.stopPreview();//停止預(yù)覽
button.setText("推流");//文字變?yōu)橥屏?/p>
}
}
```
以拉流1按鈕為例,拉流的實(shí)現(xiàn)實(shí)際上與推流十分類似甚至還簡(jiǎn)單一些。核心邏輯也是相似的判斷按鈕上的字是否為拉流1。核心的接口就是startPlayingStream和stopPlayingStream兩個(gè)。
要注意的是,我這里首先對(duì)要拉的流默認(rèn)是先靜音的。這里的playStreamMute是一個(gè)全局變量默認(rèn)值為True.
```java
public void ClickPlay(View view) {
...
if (button.getText().equals("拉流1")) {//若文字為拉流1
/* 開(kāi)始拉流 */
/* Begin to play stream */
EditText et = findViewById(R.id.ed_play_stream_id);//獲取拉流旁的EditText實(shí)例
RemoteStreamID = et.getText().toString();//獲取其字符串,作為1號(hào)拉流ID
View play_view = findViewById(R.id.remote_view);//獲取播放實(shí)例
engine.startPlayingStream(RemoteStreamID, new ZegoCanvas(play_view));//開(kāi)始拉流
engine.mutePlayStreamAudio(RemoteStreamID, playStreamMute);//首先對(duì)各用戶采取靜音
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)按鈕處理
首先來(lái)說(shuō)本地話筒,如下的publishMicEnable是一個(gè)bool類型的全局變量,初始值為true,根據(jù)注釋可以大概看懂。
要注意的就是核心接口engine.muteMicrophone(!publishMicEnable);這里括號(hào)中是對(duì)于publishMicEnable進(jìn)行了取反的,原因可以根據(jù)變量的名字就能看出來(lái),他**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)殚_(kāi)啟狀態(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);//因?yàn)檫@個(gè)函數(shù)是mute,而我們是enable,所以取反才與本義相同
}
```
其次對(duì)于遠(yuǎn)端拉流麥克風(fēng)的控制,以拉流1所收畫(huà)面的麥克風(fēng)為例。與本地麥克風(fēng)相似,核心函數(shù)有所不同。此處的核心函數(shù)為engine.mutePlayStreamAudio(RemoteStreamID, playStreamMute),這里面的兩個(gè)參數(shù)也都是全局變量,playStreamMute也是一個(gè)bool類型的變量,初始值為true。而這個(gè)RemoteStreamID這個(gè)全局變量在之前的拉流時(shí)所進(jìn)行賦值的。
```java
public void enableRemoteMic(View view) {
playStreamMute = !playStreamMute;//先將此bool變量取反,即狀態(tài)改變
if (playStreamMute) {//若此時(shí)該bool變量為真,則說(shuō)明是靜音狀態(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)殚_(kāi)啟狀態(tài)
ib_remote_stream_audio.setBackgroundDrawable(getResources().getDrawable(R.drawable.ic_bottom_microphone_on));
}
/* Enable Mic*/
engine.mutePlayStreamAudio(RemoteStreamID, playStreamMute);//此處因?yàn)閎ool變量實(shí)際意義與函數(shù)本義相同,故不用取反
}
```
**(3)**.與本地相機(jī)美顏、改變攝像頭前后置等本地?cái)U(kuò)展功能?
這部分功能就比較簡(jiǎn)單了,實(shí)現(xiàn)的邏輯與麥克風(fēng)相似,也是對(duì)于一個(gè)bool型全局變量進(jìn)行判斷。
相機(jī)美顏實(shí)現(xiàn)如下,核心的接口就是engine.enableBeautify(WHITEN);代碼當(dāng)中的isBeauty為一個(gè)全局變量的bool值。
這里我只使用了美白參數(shù)進(jìn)行使用,還可以添加其他的參數(shù),詳情見(jiàn)頂部連接中點(diǎn)開(kāi)API文檔。
```java
public void enableBeauty(View view) {
isBeauty = !isBeauty;//取反
//更換圖標(biāo)
if (isBeauty) {//若現(xiàn)在為真,則變?yōu)槭褂妹李亴?duì)應(yīng)圖標(biāo)
ib_beauty.setBackgroundDrawable(getResources().getDrawable(R.drawable.beauty_ps));
} else {//若現(xiàn)在為假,則對(duì)應(yīng)普通狀態(tài)圖標(biāo)
ib_beauty.setBackgroundDrawable(getResources().getDrawable(R.drawable.normal));
}
if (isBeauty) {//如果處于美顏狀態(tài)
//這里只采用一個(gè)較為明顯的美白功能
engine.enableBeautify(WHITEN);
} else {//反之則關(guān)閉所有美顏設(shè)置
engine.enableBeautify(NONE);
}
}
```
改變攝像頭方向,因?yàn)椴恍枰鼡Q圖標(biāo)所以更為簡(jiǎn)單,核心接口是engine.useFrontCamera(isFrontCamera);其中isFront為bool類型的全局變量
```java
public void frontCamera(View view)
{
isFrontCamera = !isFrontCamera;//先去反
engine.useFrontCamera(isFrontCamera);//根據(jù)現(xiàn)有布爾值帶入是否使用前置攝像頭的函數(shù)中
}
```
**(4)**.退出按鈕
這部分要注意的是,退出按鈕要同時(shí)考慮到退出之后還要回到房間登錄界面,以及若當(dāng)前是推流狀態(tài)要停止當(dāng)前推流,以及退出用戶對(duì)于房間的登錄。實(shí)現(xiàn)起來(lái)還是比較簡(jiǎn)單的代碼如下,其中使用了顯示intent來(lái)進(jìn)行活動(dòng)的啟動(dòng),roomID為一個(gè)全局變量。在初始化的主函數(shù)中就以及賦值為了從之前登錄界面所帶來(lái)的roomID.
```java
public void Logout(View view) {
Intent intent = new Intent(this, Login.class);//設(shè)置一個(gè)從當(dāng)前活動(dòng)到Login活動(dòng)的intent
engine.stopPublishingStream();//停止推流
engine.logoutRoom(roomID);//退出該房間
startActivity(intent);//重新進(jìn)入房間的登錄界面
finish();//結(jié)束當(dāng)前活動(dòng)
}
```
這樣就寫(xiě)完了,怎么樣是不是非常簡(jiǎn)單!真正要自己去想的也就是一些邏輯的處理,大大節(jié)省了開(kāi)發(fā)的時(shí)間。
三.源代碼
**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="請(qǐng)輸入房間號(hào)"
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 (有點(diǎn)長(zhǎng)。。)
```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="一號(hào)遠(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="二號(hào)遠(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="三號(hào)遠(yuǎn)端ID"
android:layout_gravity="center_horizontal"
android:textAlignment="center"
/>
android:layout_width="wrap_content"
android:layout_height="wrap_content">
```
**2.**活動(dòng)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);//獲取登錄按鈕實(shí)例
login.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {//匿名類實(shí)現(xiàn)監(jiān)聽(tīng)功能
EditText roomIDx = findViewById(R.id.room_login);//獲取用于輸入的EditText的實(shí)例
String roomID = roomIDx.getText().toString().trim();//獲取其中的文字,也就是對(duì)應(yīng)的roomID
if (roomID.equals("")) {//檢查此ID是否為空,為空則彈出,請(qǐng)輸入信息。
Toast.makeText(Login.this, "請(qǐng)輸入roomID", Toast.LENGTH_LONG).show();
}
else {//反之啟動(dòng)活動(dòng)UI
Intent intent =new Intent(Login.this, UI.class);//創(chuàng)建一個(gè)顯式intent
intent.putExtra("room_id", roomID);//并將房間號(hào)作為夸活動(dòng)傳輸?shù)臄?shù)據(jù)傳輸?shù)経I活動(dòng)當(dāng)中
startActivity(intent);//啟動(dòng)Activity
finish();//結(jié)束活動(dòng)
}
}
});
}
}
```
核心視頻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)入對(duì)應(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)為開(kāi)著的
boolean playStreamMute = true; //其余屏幕人的初始狀態(tài)都為靜音
boolean playStreamMute2 = true;
boolean playStreamMute3 = true;
boolean isBeauty = false;//初始無(wú)美顏
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
//寫(xiě)好自己的ID和sign,以下為我所申請(qǐng)的ID,如果要自己使用或者商用請(qǐng)自行申請(qǐng)并修改
long appID = ;? // 請(qǐng)通過(guò)官網(wǎng)注冊(cè)獲取,格式為 123456789L
String appSign = ;? //64個(gè)字符,請(qǐng)通過(guò)官網(wǎng)注冊(cè)獲取,格式為"0123456789012345678901234567890123456789012345678901234567890123"
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
/* 填寫(xiě) appID 和 appSign */
/* 初始化SDK,使用測(cè)試環(huán)境,通用場(chǎng)景接入,此為自動(dòng)初始化,無(wú)需點(diǎn)擊按鈕*/
engine = ZegoExpressEngine.createEngine(appID, appSign, true, ZegoScenario.GENERAL, getApplication(), null);
setContentView(R.layout.activity_main);
//登錄
/* 創(chuàng)建用戶 */
/* 生成隨機(jī)的用戶ID,避免不同手機(jī)使用時(shí)用戶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)的推流有:");
/* 開(kāi)始登陸房間 */
//房間狀態(tài)改變,時(shí)間處理
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)有用戶新增或刪除時(shí),SDK會(huì)通過(guò)該回調(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)有用戶新推送或刪除音視頻流時(shí),SDK會(huì)通過(guò)該回調(diào)通知 */
//自己的推流不會(huì)被記入
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)有列表中包含這個(gè),就移除
RoomStreamList.remove(streamList.get(i).streamID);
} else {//如果現(xiàn)有列表中不包含這個(gè)就加入
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)出來(lái)
}
});
//房間ID為L(zhǎng)ogin活動(dòng)傳遞過(guò)來(lái)的
Intent intent = getIntent();
roomID = intent.getStringExtra("room_id");//getXxxExtra方法獲取Intent傳遞過(guò)來(lái)的roomID
engine.loginRoom(roomID, user);//有了房間號(hào),將用戶登錄到該房間
// 麥克風(fēng)
ib_local_mic = findViewById(R.id.ib_local_mic);//找到本地麥克風(fēng)圖標(biāo)
/* 音頻播放是否靜音的開(kāi)關(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)
//動(dòng)態(tài)申請(qǐng)權(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 推拉流按鈕處理
/*點(diǎn)擊推流按鈕進(jìn)行推流 */
/*
Click Publish Button
*/
public void ClickPublish(View view) {
if (engine == null) {//之前自動(dòng)初始化的SDK若未初始化成功則會(huì)彈出以下內(nèi)容
Toast.makeText(this, "please initiate your sdk first!", Toast.LENGTH_SHORT).show();
return;
}
Button button = (Button) view;//獲取推流這個(gè)按鈕的實(shí)例
if (button.getText().equals("推流")) {//若上面的文字是推流,則說(shuō)明還未推流。
EditText et = findViewById(R.id.ed_publish_stream_id);//找到,旁邊的EditText的實(shí)例
LocalStreamID = et.getText().toString();//獲取其文字內(nèi)容,并賦值給全局變量LocalStreamID
/* 開(kāi)始推流 */
/* Begin to publish stream */
engine.startPublishingStream(LocalStreamID);//推流
Toast.makeText(this, "published", Toast.LENGTH_SHORT).show();//推流成功文字提示
/* 開(kāi)始預(yù)覽并設(shè)置本地預(yù)覽視圖 */
/* Start preview and set the local preview view. */
View local_view = findViewById(R.id.local_view);//獲取預(yù)覽圖像的TextureView實(shí)例
engine.startPreview(new ZegoCanvas(local_view));//開(kāi)始預(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>
}
}
/* 點(diǎn)擊拉流1按鈕*/
/*
Click Play Button
*/
//由于如下三個(gè)按鈕,實(shí)現(xiàn)代碼大同小異所以就只 詳寫(xiě) 此按鈕注釋,其他實(shí)現(xiàn)原理一致
public void ClickPlay(View view) {
if (engine == null) {//之前自動(dòng)初始化的SDK若未初始化成功則會(huì)彈出以下內(nèi)容
Toast.makeText(this, "please initiate your sdk first!", Toast.LENGTH_SHORT).show();
return;
}
Button button = (Button) view;//獲取按鈕實(shí)例
if (button.getText().equals("拉流1")) {//若文字為拉流1
EditText et = findViewById(R.id.ed_play_stream_id);//獲取拉流旁的EditText實(shí)例
RemoteStreamID = et.getText().toString();//獲取其字符串,作為1號(hào)拉流ID
/* 開(kāi)始拉流 */
/* Begin to play stream */
View play_view = findViewById(R.id.remote_view);//獲取播放實(shí)例
engine.startPlayingStream(RemoteStreamID, new ZegoCanvas(play_view));//開(kāi)始拉流
engine.mutePlayStreamAudio(RemoteStreamID, playStreamMute);//首先對(duì)各用戶采取靜音
Toast.makeText(this, "Remote1 played successfully", Toast.LENGTH_SHORT).show();//提示拉流畫(huà)面播放成功
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)槔?
}
}
/* 點(diǎn)擊拉流2按鈕 */
/*
Click Play Button
*/
//與拉流1按鈕相似
public void ClickPlay2(View view) {
if (engine == null) {//之前自動(dòng)初始化的SDK若未初始化成功則會(huì)彈出以下內(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();
/* 開(kāi)始拉流 */
/* 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");
}
}
/* 點(diǎn)擊拉流按鈕3 */
/*
Click Play Button
*/
//與拉流1按鈕相似
public void ClickPlay3(View view) {
if (engine == null) {//之前自動(dòng)初始化的SDK若未初始化成功則會(huì)彈出以下內(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);
/* 開(kāi)始拉流 */
/* 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) {//之前自動(dòng)初始化的SDK若未初始化成功則會(huì)彈出以下內(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)殚_(kāi)啟狀態(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);//因?yàn)檫@個(gè)函數(shù)是mute,而我們是enable,所以取反才與本義相同
}
//一號(hào)拉流麥克風(fēng)處理,二三號(hào)也大同小異
public void enableRemoteMic(View view) {
if (engine == null) {//之前自動(dòng)初始化的SDK若未初始化成功則會(huì)彈出以下內(nèi)容
Toast.makeText(this, "please initiate your sdk first!", Toast.LENGTH_SHORT).show();
return;
}
playStreamMute = !playStreamMute;//先將此bool變量取反,即狀態(tài)改變
if (playStreamMute) {//若此時(shí)該bool變量為真,則說(shuō)明是靜音狀態(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)殚_(kāi)啟狀態(tài)
ib_remote_stream_audio.setBackgroundDrawable(getResources().getDrawable(R.drawable.ic_bottom_microphone_on));
}
/* Enable Mic*/
engine.mutePlayStreamAudio(RemoteStreamID, playStreamMute);//此處因?yàn)閎ool變量實(shí)際意義與函數(shù)本義相同,故不用取反
}
//二號(hào)拉流麥克風(fēng)處理,與一號(hào)類似
public void enableRemoteMic2(View view) {
if (engine == null) {//之前自動(dòng)初始化的SDK若未初始化成功則會(huì)彈出以下內(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);
}
//三號(hào)拉流麥克風(fēng)處理,與一號(hào)類似
public void enableRemoteMic3(View view) {
if (engine == null) {//之前自動(dòng)初始化的SDK若未初始化成功則會(huì)彈出以下內(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 本地相機(jī)美顏、改變攝像頭前后置等擴(kuò)展功能
//美顏功能
public void enableBeauty(View view) {
if (engine == null) {//之前自動(dòng)初始化的SDK若未初始化成功則會(huì)彈出以下內(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)槭褂妹李亴?duì)應(yīng)圖標(biāo)
ib_beauty.setBackgroundDrawable(getResources().getDrawable(R.drawable.beauty_ps));
} else {//若現(xiàn)在為假,則對(duì)應(yīng)普通狀態(tài)圖標(biāo)
ib_beauty.setBackgroundDrawable(getResources().getDrawable(R.drawable.normal));
}
if (isBeauty) {//如果處于美顏狀態(tài)
//這里只采用一個(gè)較為明顯的美白功能
engine.enableBeautify(WHITEN);
} else {//反之則關(guān)閉所有美顏設(shè)置
engine.enableBeautify(NONE);
}
}
//調(diào)用后置攝像頭
public void frontCamera(View view)
{
if (engine == null) {//之前自動(dòng)初始化的SDK若未初始化成功則會(huì)彈出以下內(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è)置一個(gè)從當(dāng)前活動(dòng)到Login活動(dòng)的intent
engine.stopPublishingStream();//停止推流
engine.logoutRoom(roomID);//退出該房間
startActivity(intent);//重新進(jìn)入房間的登錄界面
finish();//結(jié)束當(dāng)前活動(dòng)
}
}
```
四.功能上的不足以及可以繼續(xù)開(kāi)發(fā)的地方
1.為了更加方便人的使用,可以將對(duì)應(yīng)房間的推流id自動(dòng)進(jìn)行拉流。
2.擴(kuò)展功能可以增加一些其他其他的,類似像是只聽(tīng)見(jiàn)其他用戶的聲音關(guān)閉其畫(huà)面
3.利用其他技術(shù),實(shí)現(xiàn)手機(jī)端的屏幕共享。
4.可以通過(guò)RecyclerView等或其他方式,從而實(shí)現(xiàn)房間內(nèi)一個(gè)用戶界面能顯示更多畫(huà)面,使得最多拉的流數(shù)>3
5.當(dāng)前拉的某個(gè)流停止時(shí),畫(huà)面靜止,可以考慮讓其轉(zhuǎn)換為黑屏。
電子發(fā)燒友App








































評(píng)論