音视频流媒体开发【七十九】- WebRTC6-音视频一对一通话

音视频流媒体开发-目录
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封装格式

  1. join 加入房间
  2. resp­join 当join房间后发现房间已经存在另一个人时则返回另一个人的uid;如果只有自己则不返回
  3. leave 离开房间,服务器收到leave信令则检查同一房间是否有其他人,如果有其他人则通知他有人离开
  4. new­peer 服务器通知客户端有新人加入,收到new­peer则发起连接请求
  5. peer­leave 服务器通知客户端有人离开
  6. offer 转发offer sdp
  7. answer 转发answer sdp
  8. 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 一般用max­bundle
    banlanced:音频与视频轨使用各自的传输通道
    max­compat:每个轨使用自己的传输通道
    max­bundle:都绑定到同一个传输通道

  • 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;
   }
}

6.3 实现WebRTC音视频通话

开发步骤
1. 客户端显示界面
2. 打开摄像头并显示到页面
3. websocket连接
4. join、new­peer、resp­join信令实现
5. leave、peer­leave信令实现
6. offer、answer、candidate信令实现
7. 综合调试和完善

6.3.1 客户端显示界面

步骤:创建html页面
主要是input、button、video控件的布局。

6.3.2 打开摄像头并显示到页面

需要通过

6.3.3 websocket连接
6.3.4 join、new­peer、resp­join信令实现

思路:(1)点击加入开妞;(2)响应加入按钮事件;(3)将join发送给服务器;(4)服务器 根据当前房间的人数做处理,如果房间已经有人则通知房间里面的人有新人加入(new­peer),并通知自己房间里面是什么人(respjoin)。

6.3.5 leave、peer­leave信令实现

思路:(1)点击离开按钮;(2)响应离开按钮事件;(3)将leave发送给服务器;(4)服务器处理leave,将发送者删除并通知房间(peer­leave)的其他人;(5)房间的其他人在客户端响应peer­leave事件。

6.3.6 offer、answer、candidate信令实现

思路:
(1)收到new­peer (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服务器
  1. 配置自己的证书
    ssl_certificate /home/lqf/cert/cert.pem; // 注意证书所在的路径
    ssl_certificate_key /home/lqf/cert/key.pem;
  2. 配置主机域名或者主机IP
    server_name 192.168.221.134;
  3. 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,本身是安全的访问,不能降级做不安全的访问。

image.png

ws协议和wss协议两个均是WebSocket协议的SCHEM,两者一个是非安全的,一个是安全的。也是统一的资源标志符。就好比HTTP协议和HTTPS协议的差别。

Nginx主要是提供wss连接的支持,https必须调用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后面

信令服务器后台执行

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/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; #配置点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实现通话

本章主要内容

  1. 获取权限和引入库(WebRTC、websocket)
  2. 信令处理
  3. Android WebRTC框架分析
  4. Android实战­走读代码
6.5.1 获取权限和引入库

涉及到

  1. camera权限
  2. audio访问权限
  3. 网络访问权限
    使用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";
主动调用函数
  1. joinRoom
  2. leaveRoom
  3. sendOffer
  4. sendAnswer
  5. 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)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,817评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,329评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,354评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,498评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,600评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,829评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,979评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,722评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,189评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,519评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,654评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,940评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,762评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,993评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,382评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,543评论 2 349

推荐阅读更多精彩内容