音视频流媒体开发-目录
iOS知识点-目录
Android-目录
Flutter-目录
数据结构与算法-目录
uni-pp-目录
6 实现音视频一对一通话
1. 语法补充 =>
=>是es6语法中的arrow function
06/6.1 arrow.html
<html>
<head>
<title>arrow</title>
</head>
<body>
<script>
console.log("普通函数方式");
var arr1 = [1, 2, 3, 4, 5];
arr1.forEach(function(e) {
console.log(e);
});
console.log("箭头函数方式");
var arr2 = [1, 2, 3, 4, 5];
arr2.forEach((e) => {
console.log(e);
});
</script>
</body>
</html>
2. 语法补充promise
promise的then是异步执行,但链路的then/catch是顺序执行,我们直接看范例
代码: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应用开发人员而言,主要是关注RTCPeerConnection类,我们以(1)信令设计;(2)媒体协商;(3)加入Stream/Track;(4)网络协商 四大块继续讲解通话原理
6.1.1 信令协议设计
采用json封装格式
- join 加入房间
- respjoin 当join房间后发现房间已经存在另一个人时则返回另一个人的uid;如果只有自己则不返回
- leave 离开房间,服务器收到leave信令则检查同一房间是否有其他人,如果有其他人则通知他有人离开
- newpeer 服务器通知客户端有新人加入,收到newpeer则发起连接请求
- peerleave 服务器通知客户端有人离开
- offer 转发offer sdp
- answer 转发answer sdp
- candidate 转发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 媒体协商
- createOffer
基本格式
aPromise = myPeerConnection.createOffer([options]); - [options]
var options = {
offerToReceiveAudio: true, // 告诉另一端,你是否想接收音频,默认true
offerToReceiveVideo: true, // 告诉另一端,你是否想接收视频,默认true
iceRestart: false, // 是否在活跃状态重启ICE网络协商
};
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 网络协商
addIceCandidate
基本格式
aPromise = pc.addIceCandidate(候选人);-
candidate
注意Android和Web端的不同。
6.2 RTCPeerConnection补充
6.2.1 构造函数
语法
pc = new RTCPeerConnection([ configuration ]);
configuration可选
bundlePolicy 一般用maxbundle
banlanced:音频与视频轨使用各自的传输通道
maxcompat:每个轨使用自己的传输通道
maxbundle:都绑定到同一个传输通道iceTransportPolicy 一般用all
指定ICE的传输策略
relay:只使用中继候选者
all:可以使用任何类型的候选者iceServers
其由RTCIceServer组成,每个RTCIceServer都是一个ICE代理的服务器
- rtcpMuxPolicy 一般用require
rtcp的复用策略,该选项在收集ICE候选者时使用
6.2.2 重要事件
- onicecandidate 收到候选者时触发的事件
- ontrack 获取远端流
- onconnectionstatechange PeerConnection的连接状态,参考
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 实现WebRTC音视频通话
开发步骤
1. 客户端显示界面
2. 打开摄像头并显示到页面
3. websocket连接
4. join、newpeer、respjoin信令实现
5. leave、peerleave信令实现
6. offer、answer、candidate信令实现
7. 综合调试和完善
6.3.1 客户端显示界面
步骤:创建html页面
主要是input、button、video控件的布局。
6.3.2 打开摄像头并显示到页面
需要通过
6.3.3 websocket连接
6.3.4 join、newpeer、respjoin信令实现
思路:(1)点击加入开妞;(2)响应加入按钮事件;(3)将join发送给服务器;(4)服务器 根据当前房间的人数做处理,如果房间已经有人则通知房间里面的人有新人加入(newpeer),并通知自己房间里面是什么人(respjoin)。
6.3.5 leave、peerleave信令实现
思路:(1)点击离开按钮;(2)响应离开按钮事件;(3)将leave发送给服务器;(4)服务器处理leave,将发送者删除并通知房间(peerleave)的其他人;(5)房间的其他人在客户端响应peerleave事件。
6.3.6 offer、answer、candidate信令实现
思路:
(1)收到newpeer (handleRemoteNewPeer处理),作为发起者创建RTCPeerConnection,绑定事件响应函数,加入本地流;
(2)创建offer sdp,设置本地sdp,并将offer sdp发送到服务器;
(3)服务器收到offer sdp 转发给指定的remoteClient;
(4)接收者收到offer,也创建RTCPeerConnection,绑定事件响应函数,加入本地流;
(5)接收者设置远程sdp,并创建answer sdp,然后设置本地sdp并将answer sdp发送到服务器;
(6)服务器收到answer sdp 转发给指定的remoteClient;
(7)发起者收到answer sdp,则设置远程sdp;
(8)发起者和接收者都收到ontrack回调事件,获取到对方码流的对象句柄;
(9)发起者和接收者都开始请求打洞,通过onIceCandidate获取到打洞信息(candidate)并发送给对方
(10)如果P2P能成功则进行P2P通话,如果P2P不成功则进行中继转发通话。
6.3.7 综合调试和完善
思路:
(1)点击离开时,要将RTCPeerConnection关闭(close);
(2)点击离开时,要将本地摄像头和麦克风关闭;
(3)检测到客户端退出时,服务器再次检测该客户端是否已经退出房间。
(4)RTCPeerConnection时传入ICE server的参数,以便当在公网环境下可以进行正常通话。
启动coturn
# nohup是重定向命令,输出都将附加到当前目录的 nohup.out 文件中; 命令后加 & ,后台执行起来后按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
#然后查看相应的端口号3478是否存在进程
sudo lsof ‐i:3478
设置configuration,先设置为relay中继模式,只有relay中继模式可用的时候,才能部署到公网去(部署到公网后也先测试relay)。
var defaultConfiguration = {
bundlePolicy: "max‐bundle",
rtcpMuxPolicy: "require",
iceTransportPolicy:"relay",//relay
// 修改ice数组测试效果,需要进行封装
iceServers: [
{
"urls": [
"turn:192.168.221.134:3478?transport=udp",
"turn:192.168.221.134:3478?transport=tcp" // 可以插入多个进行备选
],
"username": "lqf",
"credential": "123456"
},
{
"urls": [
"stun:192.168.221.134:3478"
]
}
]
};
pc = new RTCPeerConnection(defaultConfiguration);
relay中继网络状况
局域网P2P
6.4 部署到公网
公网防火墙问题,比如 coturn涉及到的3478端口是否开放
启动coturn
sudo nohup turnserver L 0.0.0.0 a u lqf:123456 v f r nort.gov &
# nohup是重定向命令,输出都将附加到当前目录的 nohup.out 文件中; 命令后加 & ,后台执行起来后按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
#然后查看相应的端口号3478是否存在进程
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
默认安装目录:/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
产生证书
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服务器
- 配置自己的证书
ssl_certificate /home/lqf/cert/cert.pem; // 注意证书所在的路径
ssl_certificate_key /home/lqf/cert/key.pem; - 配置主机域名或者主机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/webrtchttps.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协议和wss协议两个均是WebSocket协议的SCHEM,两者一个是非安全的,一个是安全的。也是统一的资源标志符。就好比HTTP协议和HTTPS协议的差别。
Nginx主要是提供wss连接的支持,https必须调用wss的连接。
完整配置文件:/usr/local/nginx/conf/conf.d/webrtcwebsocketproxy.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后面
信令服务器后台执行
sudo nohup node ./signal_server.js &
解决websocket自动断开
我们在通话时,出现60秒后客户端自动断开的问题,是因为经过nginx代理时,如果websocket长时间没有收发消息则该websocket将会被断开。我们这里可以修改收发消息的时间间隔。
proxy_connect_timeout :后端服务器连接的超时时间_发起握手等候响应超时时间
proxy_read_timeout:连接成功后等候后端服务器响应时间其实已经进入后端的排队之中等候处理(也可以说是后端服务器处理请求的时间)
proxy_send_timeout :后端服务器数据回传时间_就是在规定时间之内后端服务器必须传完所有的数据nginx使用proxy模块时,默认的读取超时时间是60s。
完整配置文件:/usr/local/nginx/conf/conf.d/webrtcwebsocketproxy.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; #配置点1
proxy_read_timeout 6000s; #配置点2,如果没效,可以考虑这个时间配置长一点
proxy_send_timeout 6000s; #配置点3
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
客户端 服务器 信令:心跳包
keeplive 间隔5秒发送一次给信令服务器,说明客户端一直处于活跃的状态。
6.5 Web和Android实现通话
本章主要内容
- 获取权限和引入库(WebRTC、websocket)
- 信令处理
- Android WebRTC框架分析
- Android实战走读代码
6.5.1 获取权限和引入库
涉及到
- camera权限
- audio访问权限
- 网络访问权限
使用Android studio 3.2 开发
1 Android权限管理
申请静态权限
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" />
动态申请权限
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"
// 处理权限库
implementation 'pub.devrel:easypermissions:1.1.3'
6.5.2 信令处理
和js代码一致,我们重点关注代码的基本流程。
通过 RTCSignalClient类
配置websocket地址(RTCSignalClient类)(一定要根据自己的IP地址):
private static final String WS_URL = "ws://192.168.2.112:8099";
主动调用函数
- joinRoom
- leaveRoom
- sendOffer
- sendAnswer
- sendCandidate
回调函数
public interface OnSignalEventListener {
void onConnected();
void onConnecting();
void onDisconnected();
void onClosse();
void onRemoteNewPeer(JSONObject message); // 新人加入
void onResponseJoin(JSONObject message); // 加入回应
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,否则会出现web端听不到Android端的的声音。
web端和Android端的candidate格式是有一定的区别。
(1)发送传输
Android
web
(2)接收处理
Android 端
接口:
Web端
web端和Android端的sdp有区别。
6.5.4 Android实战走读代码
权限
在 manifests文件中添加权限库
在module的gradle中添加依赖库收发信令
实现Activity的切换
编写signal类使用websocket收发信令创建PeerConnection
音视频数据采集
创建PeerConnection媒体协商
协商媒体能力网络协商
candidate连通检测视频渲染
6.5.5 Web和Android通话总结
Web客户端、Android客户端、Nginx服务器一定要按照自己的IP去设置相关的连接,比如websocket和coturn地址。
要启动的服务器:
(1)nginx
(2)信令服务器 signal_server
(3)打洞服务器 coturn (stun+turn)