音視頻流媒體開發(fā)-目錄
iOS知識點(diǎn)-目錄
Android-目錄
Flutter-目錄
數(shù)據(jù)結(jié)構(gòu)與算法-目錄
uni-pp-目錄
6 實(shí)現(xiàn)音視頻一對一通話
1. 語法補(bǔ)充 =>
=>是es6語法中的arrow function
06/6.1 arrow.html
<html>
<head>
<title>arrow</title>
</head>
<body>
<script>
console.log("普通函數(shù)方式");
var arr1 = [1, 2, 3, 4, 5];
arr1.forEach(function(e) {
console.log(e);
});
console.log("箭頭函數(shù)方式");
var arr2 = [1, 2, 3, 4, 5];
arr2.forEach((e) => {
console.log(e);
});
</script>
</body>
</html>
2. 語法補(bǔ)充promise
promise的then是異步執(zhí)行,但鏈路的then/catch是順序執(zhí)行,我們直接看范例
代碼:06/6.1 promise.html
<html>
<head>
<title>arrow</title>
</head>
<body>
<script>
function taskA() {
console.log("Task A");
}
function taskB() {
console.log("Task B");
throw new Error("taskB掉坑里了");
}
function onRejected(error) {
console.log("onRejected catch Error: A or B", error);
}
function finalTask() {
console.log("Final Task");
}
var promise = Promise.resolve();
promise
.then(taskA)
.then(taskB)
.catch(onRejected)
.then(finalTask);
</script>
</body>
</html>
代碼流程

6.1 一對一通話原理
對于我們WebRTC應(yīng)用開發(fā)人員而言,主要是關(guān)注RTCPeerConnection類,我們以(1)信令設(shè)計;(2)媒體協(xié)商;(3)加入Stream/Track;(4)網(wǎng)絡(luò)協(xié)商 四大塊繼續(xù)講解通話原理

6.1.1 信令協(xié)議設(shè)計
采用json封裝格式
- join 加入房間
- resp-join 當(dāng)join房間后發(fā)現(xiàn)房間已經(jīng)存在另一個人時則返回另一個人的uid;如果只有自己則不返回
- leave 離開房間,服務(wù)器收到leave信令則檢查同一房間是否有其他人,如果有其他人則通知他有人離開
- new-peer 服務(wù)器通知客戶端有新人加入,收到new-peer則發(fā)起連接請求
- peer-leave 服務(wù)器通知客戶端有人離開
- offer 轉(zhuǎn)發(fā)offer sdp
- answer 轉(zhuǎn)發(fā)answer sdp
- candidate 轉(zhuǎn)發(fā)candidate sdp
join
var jsonMsg = {
'cmd': 'join',
'roomId': roomId,
'uid': localUserId,
};
resp--join
jsonMsg = {
'cmd': 'resp‐join',
'remoteUid': remoteUid
};
leave
var jsonMsg = {
'cmd': 'leave',
'roomId': roomId,
'uid': localUserId,
};
new--peer
var jsonMsg = {
'cmd': 'new‐peer',
'remoteUid': uid
};
peer--leave
var jsonMsg = {
'cmd': 'peer‐leave',
'remoteUid': uid
};
offer
var jsonMsg = {
'cmd': 'offer',
'roomId': roomId,
'uid': localUserId,
'remoteUid':remoteUserId,
'msg': JSON.stringify(sessionDescription)
};
answer
var jsonMsg = {
'cmd': 'answer',
'roomId': roomId,
'uid': localUserId,
'remoteUid':remoteUserId,
'msg': JSON.stringify(sessionDescription)
};
candidate
var jsonMsg = {
'cmd': 'candidate',
'roomId': roomId,
'uid': localUserId,
'remoteUid':remoteUserId,
'msg': JSON.stringify(candidateJson)
};
6.1.2 媒體協(xié)商

- createOffer
基本格式
aPromise = myPeerConnection.createOffer([options])? - [options]
var options = {
offerToReceiveAudio: true, // 告訴另一端,你是否想接收音頻,默認(rèn)true
offerToReceiveVideo: true, // 告訴另一端,你是否想接收視頻,默認(rèn)true
iceRestart: false, // 是否在活躍狀態(tài)重啟ICE網(wǎng)絡(luò)協(xié)商
};
ICE Restart (webrtc.github.io)
iceRestart:只有在處于活躍的時候,iceRestart=false才有作用。
createAnswer
基本格式
aPromise = RTCPeerConnection .createAnswer([ options ])? 目前createAnswer的options是無效的。setLocalDescription
基本格式
aPromise = RTCPeerConnection .setLocalDescription(sessionDescription);setRemoteDescription
基本格式
aPromise = pc.setRemoteDescription(sessionDescription);
6.1.3 加入Stream/Track
- addTrack
基本格式
rtpSender = rtcPeerConnection .addTrack(track,stream ...);
track:添加到RTCPeerConnection中的媒體軌(音頻track/視頻track)
stream:getUserMedia中拿到的流,指定track所在的stream
6.1.4 網(wǎng)絡(luò)協(xié)商
addIceCandidate
基本格式
aPromise = pc.addIceCandidate(候選人);-
candidate

注意Android和Web端的不同。
6.2 RTCPeerConnection補(bǔ)充
6.2.1 構(gòu)造函數(shù)
語法
pc = new RTCPeerConnection([ configuration ]);
configuration可選
bundlePolicy 一般用max-bundle
banlanced:音頻與視頻軌使用各自的傳輸通道
max-compat:每個軌使用自己的傳輸通道
max-bundle:都綁定到同一個傳輸通道iceTransportPolicy 一般用all
指定ICE的傳輸策略
relay:只使用中繼候選者
all:可以使用任何類型的候選者iceServers
其由RTCIceServer組成,每個RTCIceServer都是一個ICE代理的服務(wù)器

- rtcpMuxPolicy 一般用require
rtcp的復(fù)用策略,該選項(xiàng)在收集ICE候選者時使用

6.2.2 重要事件
- onicecandidate 收到候選者時觸發(fā)的事件
- ontrack 獲取遠(yuǎn)端流
- onconnectionstatechange PeerConnection的連接狀態(tài),參考
pc.onconnectionstatechange = function(event) {
switch(pc.connectionState) {
case "connected":
// The connection has become fully connected
break;
case "disconnected":
case "failed":
// One or more transports has terminated unexpectedly or in an error
break;
case "closed":
// The connection has been closed
break;
}
}
- oniceconnectionstatechange ice連接事件 具體參考
6.3 實(shí)現(xiàn)WebRTC音視頻通話
開發(fā)步驟
1. 客戶端顯示界面
2. 打開攝像頭并顯示到頁面
3. websocket連接
4. join、new-peer、resp-join信令實(shí)現(xiàn)
5. leave、peer-leave信令實(shí)現(xiàn)
6. offer、answer、candidate信令實(shí)現(xiàn)
7. 綜合調(diào)試和完善
6.3.1 客戶端顯示界面
步驟:創(chuàng)建html頁面
主要是input、button、video控件的布局。
6.3.2 打開攝像頭并顯示到頁面
需要通過
6.3.3 websocket連接
6.3.4 join、new-peer、resp-join信令實(shí)現(xiàn)
思路:(1)點(diǎn)擊加入開妞;(2)響應(yīng)加入按鈕事件;(3)將join發(fā)送給服務(wù)器;(4)服務(wù)器 根據(jù)當(dāng)前房間的人數(shù)做處理,如果房間已經(jīng)有人則通知房間里面的人有新人加入(new-peer),并通知自己房間里面是什么人(respjoin)。
6.3.5 leave、peer-leave信令實(shí)現(xiàn)
思路:(1)點(diǎn)擊離開按鈕;(2)響應(yīng)離開按鈕事件;(3)將leave發(fā)送給服務(wù)器;(4)服務(wù)器處理leave,將發(fā)送者刪除并通知房間(peer-leave)的其他人;(5)房間的其他人在客戶端響應(yīng)peer-leave事件。
6.3.6 offer、answer、candidate信令實(shí)現(xiàn)
思路:
(1)收到new-peer (handleRemoteNewPeer處理),作為發(fā)起者創(chuàng)建RTCPeerConnection,綁定事件響應(yīng)函數(shù),加入本地流;
(2)創(chuàng)建offer sdp,設(shè)置本地sdp,并將offer sdp發(fā)送到服務(wù)器;
(3)服務(wù)器收到offer sdp 轉(zhuǎn)發(fā)給指定的remoteClient;
(4)接收者收到offer,也創(chuàng)建RTCPeerConnection,綁定事件響應(yīng)函數(shù),加入本地流;
(5)接收者設(shè)置遠(yuǎn)程sdp,并創(chuàng)建answer sdp,然后設(shè)置本地sdp并將answer sdp發(fā)送到服務(wù)器;
(6)服務(wù)器收到answer sdp 轉(zhuǎn)發(fā)給指定的remoteClient;
(7)發(fā)起者收到answer sdp,則設(shè)置遠(yuǎn)程sdp;
(8)發(fā)起者和接收者都收到ontrack回調(diào)事件,獲取到對方碼流的對象句柄;
(9)發(fā)起者和接收者都開始請求打洞,通過onIceCandidate獲取到打洞信息(candidate)并發(fā)送給對方
(10)如果P2P能成功則進(jìn)行P2P通話,如果P2P不成功則進(jìn)行中繼轉(zhuǎn)發(fā)通話。
6.3.7 綜合調(diào)試和完善
思路:
(1)點(diǎn)擊離開時,要將RTCPeerConnection關(guān)閉(close);
(2)點(diǎn)擊離開時,要將本地攝像頭和麥克風(fēng)關(guān)閉;
(3)檢測到客戶端退出時,服務(wù)器再次檢測該客戶端是否已經(jīng)退出房間。
(4)RTCPeerConnection時傳入ICE server的參數(shù),以便當(dāng)在公網(wǎng)環(huán)境下可以進(jìn)行正常通話。

啟動coturn
# nohup是重定向命令,輸出都將附加到當(dāng)前目錄的 nohup.out 文件中; 命令后加 & ,后臺執(zhí)行起來后按ctr+c,不會停止
sudo nohup turnserver ‐L 0.0.0.0 ‐a ‐u lqf:123456 ‐v ‐f ‐r nort.gov &
# 前臺啟動
sudo turnserver ‐L 0.0.0.0 ‐a ‐u lqf:123456 ‐v ‐f ‐r nort.gov
#然后查看相應(yīng)的端口號3478是否存在進(jìn)程
sudo lsof ‐i:3478
設(shè)置configuration,先設(shè)置為relay中繼模式,只有relay中繼模式可用的時候,才能部署到公網(wǎng)去(部署到公網(wǎng)后也先測試relay)。
var defaultConfiguration = {
bundlePolicy: "max‐bundle",
rtcpMuxPolicy: "require",
iceTransportPolicy:"relay",//relay
// 修改ice數(shù)組測試效果,需要進(jìn)行封裝
iceServers: [
{
"urls": [
"turn:192.168.221.134:3478?transport=udp",
"turn:192.168.221.134:3478?transport=tcp" // 可以插入多個進(jìn)行備選
],
"username": "lqf",
"credential": "123456"
},
{
"urls": [
"stun:192.168.221.134:3478"
]
}
]
};
pc = new RTCPeerConnection(defaultConfiguration);
relay中繼網(wǎng)絡(luò)狀況

局域網(wǎng)P2P

6.4 部署到公網(wǎng)
公網(wǎng)防火墻問題,比如 coturn涉及到的3478端口是否開放
啟動coturn
sudo nohup turnserver -L 0.0.0.0 -a -u lqf:123456 -v -f -r nort.gov &
# nohup是重定向命令,輸出都將附加到當(dāng)前目錄的 nohup.out 文件中; 命令后加 & ,后臺執(zhí)行起來后按ctr+c,不會停止
sudo nohup turnserver ‐L 0.0.0.0 ‐a ‐u lqf:123456 ‐v ‐f ‐r nort.gov &
# 前臺啟動
sudo turnserver ‐L 0.0.0.0 ‐a ‐u lqf:123456 ‐v ‐f ‐r nort.gov
#然后查看相應(yīng)的端口號3478是否存在進(jìn)程
sudo lsof ‐i:3478
編譯和啟動nginx
sudo apt‐get update
#安裝依賴:gcc、g++依賴庫
sudo apt‐get install build‐essential libtool
#安裝 pcre依賴庫(http://www.pcre.org/)
sudo apt‐get install libpcre3 libpcre3‐dev
#安裝 zlib依賴庫(http://www.zlib.net)
sudo apt‐get install zlib1g‐dev
#安裝ssl依賴庫
sudo apt‐get install openssl
#下載nginx 1.15.8版本
wget http://nginx.org/download/nginx‐1.15.8.tar.gz
tar xvzf nginx‐1.15.8.tar.gz
cd nginx‐1.15.8/
# 配置,一定要支持https
./configure ‐‐with‐http_ssl_module
# 編譯
make
#安裝
sudo make install
默認(rèn)安裝目錄:/usr/local/nginx
啟動:sudo /usr/local/nginx/sbin/nginx
停止:sudo /usr/local/nginx/sbin/nginx -s stop
重新加載配置文件:sudo /usr/local/nginx/sbin/nginx -s reload
產(chǎn)生證書
mkdir ‐p ~/cert
cd ~/cert
# CA私鑰
openssl genrsa ‐out key.pem 2048
# 自簽名證書
openssl req ‐new ‐x509 ‐key key.pem ‐out cert.pem ‐days 1095
配置web服務(wù)器
- 配置自己的證書
ssl_certificate /home/lqf/cert/cert.pem? // 注意證書所在的路徑
ssl_certificate_key /home/lqf/cert/key.pem? - 配置主機(jī)域名或者主機(jī)IP
server_name 192.168.221.134? - web頁面所在目錄
root /mnt/hgfs/ubuntu/ubuntu/module/webrtc/src/06/6.4/client?
完整配置文件:/usr/local/nginx/conf/conf.d/webrtc-https.conf
server {
listen 443 ssl;
ssl_certificate /home/lqf/cert/cert.pem;
ssl_certificate_key /home/lqf/cert/key.pem;
charset utf‐8;
# ip地址或者域名
server_name 192.168.221.134;
location / {
add_header 'Access‐Control‐Allow‐Origin' '*';
add_header 'Access‐Control‐Allow‐Credentials' 'true';
add_header 'Access‐Control‐Allow‐Methods' '*';
add_header 'Access‐Control‐Allow‐Headers' 'Origin, X‐Requested‐With, Content‐Type,Accept';
# web頁面所在目錄
root /mnt/hgfs/ubuntu/ubuntu/module/webrtc/src/06/6.4/client;
index index.php index.html index.htm;
}
}
編輯nginx.conf文件,在末尾}之前添加包含文件
include /usr/local/nginx/conf/conf.d/*.conf;
}
配置websocket代理
ws 不安全的連接 類似http
wss是安全的連接,類似https
https不能訪問ws,本身是安全的訪問,不能降級做不安全的訪問。

ws協(xié)議和wss協(xié)議兩個均是WebSocket協(xié)議的SCHEM,兩者一個是非安全的,一個是安全的。也是統(tǒng)一的資源標(biāo)志符。就好比HTTP協(xié)議和HTTPS協(xié)議的差別。
Nginx主要是提供wss連接的支持,https必須調(diào)用wss的連接。
完整配置文件:/usr/local/nginx/conf/conf.d/webrtc-websocket-proxy.conf
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream websocket {
server 192.168.221.134:8099;
}
server {
listen 8098 ssl;
#ssl on;
ssl_certificate /home/lqf/cert/cert.pem;
ssl_certificate_key /home/lqf/cert/key.pem;
server_name 192.168.221.134;
location /ws {
proxy_pass http://websocket;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
wss://192.168.221.134:8098/ws 端口是跟著IP后面
信令服務(wù)器后臺執(zhí)行
sudo nohup node ./signal_server.js &
解決websocket自動斷開
我們在通話時,出現(xiàn)60秒后客戶端自動斷開的問題,是因?yàn)榻?jīng)過nginx代理時,如果websocket長時間沒有收發(fā)消息則該websocket將會被斷開。我們這里可以修改收發(fā)消息的時間間隔。
proxy_connect_timeout :后端服務(wù)器連接的超時時間_發(fā)起握手等候響應(yīng)超時時間
proxy_read_timeout:連接成功后等候后端服務(wù)器響應(yīng)時間其實(shí)已經(jīng)進(jìn)入后端的排隊(duì)之中等候處理(也可以說是后端服務(wù)器處理請求的時間)
proxy_send_timeout :后端服務(wù)器數(shù)據(jù)回傳時間_就是在規(guī)定時間之內(nèi)后端服務(wù)器必須傳完所有的數(shù)據(jù)nginx使用proxy模塊時,默認(rèn)的讀取超時時間是60s。
完整配置文件:/usr/local/nginx/conf/conf.d/webrtc-websocket-proxy.conf
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream websocket {
server 192.168.221.134:8099;
}
server {
listen 8098 ssl;
ssl_certificate /home/lqf/cert/cert.pem;
ssl_certificate_key /home/lqf/cert/key.pem;
server_name 192.168.221.134;
location /ws {
proxy_pass http://websocket;
proxy_http_version 1.1;
proxy_connect_timeout 4s; #配置點(diǎn)1
proxy_read_timeout 6000s; #配置點(diǎn)2,如果沒效,可以考慮這個時間配置長一點(diǎn)
proxy_send_timeout 6000s; #配置點(diǎn)3
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
客戶端 - 服務(wù)器 信令:心跳包
keeplive 間隔5秒發(fā)送一次給信令服務(wù)器,說明客戶端一直處于活躍的狀態(tài)。
6.5 Web和Android實(shí)現(xiàn)通話
本章主要內(nèi)容
- 獲取權(quán)限和引入庫(WebRTC、websocket)
- 信令處理
- Android WebRTC框架分析
- Android實(shí)戰(zhàn)-走讀代碼
6.5.1 獲取權(quán)限和引入庫
涉及到
- camera權(quán)限
- audio訪問權(quán)限
- 網(wǎng)絡(luò)訪問權(quán)限
使用Android studio 3.2 開發(fā)
1 Android權(quán)限管理
申請靜態(tài)權(quán)限
AndroidManifest.xml文件配置
<uses‐permission android:name="android.permission.CAMERA" />
<uses‐permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses‐permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses‐permission android:name="android.permission.RECORD_AUDIO" />
<uses‐permission android:name="android.permission.INTERNET" />
<uses‐permission android:name="android.permission.ACCESS_NETWORK_STATE" />
動態(tài)申請權(quán)限
void requestPermissions(
@NonNull Activity host, @NonNull String rationale,
int requestCode, @Size(min = 1) @NonNull String... perms)
申請范例
String[] perms = {Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO};
if (!EasyPermissions.hasPermissions(this, perms)) {
EasyPermissions.requestPermissions(this, "Need permissions for camera &
microphone", 0, perms);
}
2 引入庫
// WebRTC庫
implementation 'org.webrtc:google‐webrtc:1.0.+'
// websocket庫
implementation "org.java‐websocket:Java‐WebSocket:1.4.0"
// 處理權(quán)限庫
implementation 'pub.devrel:easypermissions:1.1.3'
6.5.2 信令處理


和js代碼一致,我們重點(diǎn)關(guān)注代碼的基本流程。
通過 RTCSignalClient類
配置websocket地址(RTCSignalClient類)(一定要根據(jù)自己的IP地址):
private static final String WS_URL = "ws://192.168.2.112:8099";
主動調(diào)用函數(shù)
- joinRoom
- leaveRoom
- sendOffer
- sendAnswer
- sendCandidate
回調(diào)函數(shù)
public interface OnSignalEventListener {
void onConnected();
void onConnecting();
void onDisconnected();
void onClosse();
void onRemoteNewPeer(JSONObject message); // 新人加入
void onResponseJoin(JSONObject message); // 加入回應(yīng)
void onRemotePeerLeave(JSONObject message);
void onRemoteOffer(JSONObject message);
void onRemoteAnswer(JSONObject message);
void onRemoteCandidate(JSONObject message);
}
6.5.3 Android WebRTC框架分析
配置coturn地址(CallActivity類):
private static MyIceServer[] iceServers = {
new MyIceServer("stun:192.168.2.112:3478"),
new MyIceServer("turn:192.168.2.112:3478?transport=udp",
"lqf",
"123456"),
new MyIceServer("turn:192.168.2.112:3478?transport=tcp",
"lqf",
"123456")
};
Android端需要使用addstream的方式添加audiotrack 和videotrack,否則會出現(xiàn)web端聽不到Android端的的聲音。


web端和Android端的candidate格式是有一定的區(qū)別。
(1)發(fā)送傳輸
Android

web

(2)接收處理
Android 端

接口:

Web端

web端和Android端的sdp有區(qū)別。
6.5.4 Android實(shí)戰(zhàn)-走讀代碼
權(quán)限
在 manifests文件中添加權(quán)限庫
在module的gradle中添加依賴庫收發(fā)信令
實(shí)現(xiàn)Activity的切換
編寫signal類使用websocket收發(fā)信令創(chuàng)建PeerConnection
音視頻數(shù)據(jù)采集
創(chuàng)建PeerConnection媒體協(xié)商
協(xié)商媒體能力網(wǎng)絡(luò)協(xié)商
candidate連通檢測視頻渲染
6.5.5 Web和Android通話總結(jié)
Web客戶端、Android客戶端、Nginx服務(wù)器一定要按照自己的IP去設(shè)置相關(guān)的連接,比如websocket和coturn地址。
要啟動的服務(wù)器:
(1)nginx
(2)信令服務(wù)器 signal_server
(3)打洞服務(wù)器 coturn (stun+turn)

