WebRTC简介
WebRTC (Web Real-Time Communications) 是一项实时通讯技术,它允许网络应用或者站点,在不借助中间媒介的情况下,建立点对点(Peer-to-Peer)的连接,实现视频流和(或)音频流或者其他任意数据的传输。
WebRTC的实现是完全开源的,其核心代码用C++编写,实现了WebRTC标准里定义的API。代码库里包含了Android和iOS/macOS平台的SDK封装,分别提供Java和Objective-C接口,便于这些平台的开发者调用,Windows和Linux平台则可以直接调用WebRTC的C++接口进行开发。
基本流程
架构图
2. 基础概念
- Signaling Servier:用于实现用户的管理,以及WebSocket连接消息。
- ICE Server:用于下发STUN/TURN Server的url、用户名、密码信息。
- TURN Server:ICE协议里的TURN Server,供客户端获取公网IP,也能提供媒体数据中转服务。
客户端会依次访问这3中服务器,完成加入会话、获取TURN Server配置、执行ICE协议过程。
通话过程
步骤1:发起端创建本地PeerConnection(简称PC)对象,并创建offer
步骤2:发起端通过Signaling Server把offer发送到应答端
步骤3:应答端创建本地PC对象,把发起端的offer设置给PC,然后获得answer
步骤4:应答端通过Signaling Server把answer发给发起端
步骤5:发起端把应答端的answer设置给PC
步骤6:两端都收集本地PC的ICE Candidate(包括访问TURN Server),通过Signaling Server发送给对端,对端把ICE Candidate设置给本地的PC
步骤7:两端开始建立P2P的socket,并首发音视频和数据。
名词解释
- offer、answer和SDP
offer、answer都属于SDP(Session Description Protocol),是用于描述连接的多媒体内容的标准,例如分辨率、格式、编解码器、加密等,以便在数据传输时双方都可以相互理解。这本质上是描述内容的元数据,而不是媒体内容本身。
一个典型的SDP消息:
v=0
o=- 2883690436762308021 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE video data
a=msid-semantic: WMS
m=video 9 UDP/TLS/RTP/SAVPF 96 98 100 127 97 99 101
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:fj2x
a=ice-pwd:GmiInyo0PRBeJcP+KU6PkmQS
a=ice-options:trickle
a=fingerprint:sha-256 23:AF:7C:FC:03:36:50:93:E2:A4:B7:9F:85:2A:FC:0B:A0:69:F1:67:81:73:10:E8:A4:C3:51:CB:83:2E:2A:4C
a=setup:active
a=mid:video
a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:4 urn:3gpp:video-orientation
a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=recvonly
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:96 VP8/90000
a=rtcp-fb:96 goog-remb
a=rtcp-fb:96 transport-cc
a=rtcp-fb:96 ccm fir
a=rtcp-fb:96 nack
a=rtcp-fb:96 nack pli
a=rtpmap:98 VP9/90000
a=rtcp-fb:98 goog-remb
a=rtcp-fb:98 transport-cc
a=rtcp-fb:98 ccm fir
a=rtcp-fb:98 nack
a=rtcp-fb:98 nack pli
a=fmtp:98 profile-id=0
a=rtpmap:100 red/90000
a=rtpmap:127 ulpfec/90000
a=rtpmap:97 rtx/90000
a=fmtp:97 apt=96
a=rtpmap:99 rtx/90000
a=fmtp:99 apt=98
a=rtpmap:101 rtx/90000
a=fmtp:101 apt=100
m=application 9 DTLS/SCTP 5000
c=IN IP4 0.0.0.0
a=ice-ufrag:fj2x
a=ice-pwd:GmiInyo0PRBeJcP+KU6PkmQS
a=ice-options:trickle
a=fingerprint:sha-256 23:AF:7C:FC:03:36:50:93:E2:A4:B7:9F:85:2A:FC:0B:A0:69:F1:67:81:73:10:E8:A4:C3:51:CB:83:2E:2A:4C
a=setup:active
a=mid:data
a=sctpmap:5000 webrtc-datachannel 1024
- ICE
Interactive Connectivity Establishment,是一个实现P2P连接的框架。由于Peer A与Peer B的直连需要绕过防火墙,如果设备没有公共IP,它会提供一个唯一的IP。如果路由器不支持直接与对等方连接,则需要通过服务器中继数据。ICE使用STUN和/或TURN完成此操作。 - STUN
Session Traversal Utilities for NAT,是一种协议,用于发现您的公共地址并确定路由器中任何会阻止与对等方直接连接的限制。客户端将向 Internet 上的 STUN 服务器发送请求,该服务器将回复客户端的公共地址以及客户端是否可以通过路由器的 NAT 访问。
- NAT
Network Address Translation,用于为您的设备提供公共 IP 地址。路由器将有一个公共 IP 地址,连接到路由器的每个设备都将有一个私有 IP 地址。请求将从设备的私有 IP 转换为具有唯一端口的路由器的公共 IP。这样您就不需要为每台设备设置一个唯一的公共 IP,但仍然可以在 Internet 上被发现。 - TURN
Traversal Using Relays around NAT,一些使用 NAT 的路由器采用称为“对称 NAT”的限制。这意味着路由器将只接受来自您之前连接的对等方的连接。通过打开与 TURN 服务器的连接并通过该服务器中继所有信息来绕过对称 NAT 限制。终端将创建与 TURN 服务器的连接,并告诉所有对等方将数据包发送到服务器,然后将其转发给终端。这会带来一些开销,因此只有在没有其他选择的情况下才使用它。
数据示例
offer:
{
"from": "JX121110700032_APP",
"to": "JX121110700032_WEB",
"message": {
"data": {
"description": "v=0\r\no=- 578772191408603257 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE video\r\na=msid-semantic: WMS ARDAMS\r\nm=video 9 UDP/TLS/RTP/SAVPF 96 98 100 127 97 99 101\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:9dh8\r\na=ice-pwd:+NzqON1cRjJfJ0Sdf9AP2hn5\r\na=ice-options:trickle renomination\r\na=fingerprint:sha-256 32:2D:6D:0B:FF:96:01:88:40:4A:D4:7F:74:82:4D:54:B2:D6:BF:9F:C6:44:7E:75:91:50:85:B3:AB:4A:24:D1\r\na=setup:actpass\r\na=mid:video\r\na=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:4 urn:3gpp:video-orientation\r\na=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r\na=sendrecv\r\na=rtcp-mux\r\na=rtcp-rsize\r\na=rtpmap:96 VP8/90000\r\na=rtcp-fb:96 ccm fir\r\na=rtcp-fb:96 nack\r\na=rtcp-fb:96 nack pli\r\na=rtcp-fb:96 goog-remb\r\na=rtcp-fb:96 transport-cc\r\na=rtpmap:98 VP9/90000\r\na=rtcp-fb:98 ccm fir\r\na=rtcp-fb:98 nack\r\na=rtcp-fb:98 nack pli\r\na=rtcp-fb:98 goog-remb\r\na=rtcp-fb:98 transport-cc\r\na=rtpmap:100 red/90000\r\na=rtpmap:127 ulpfec/90000\r\na=rtpmap:97 rtx/90000\r\na=fmtp:97 apt=96\r\na=rtpmap:99 rtx/90000\r\na=fmtp:99 apt=98\r\na=rtpmap:101 rtx/90000\r\na=fmtp:101 apt=100\r\na=ssrc-group:FID 2579110363 988490045\r\na=ssrc:2579110363 cname:XnDr/Jk7DTRP7WWu\r\na=ssrc:2579110363 msid:ARDAMS ARDAMSv0\r\na=ssrc:2579110363 mslabel:ARDAMS\r\na=ssrc:2579110363 label:ARDAMSv0\r\na=ssrc:988490045 cname:XnDr/Jk7DTRP7WWu\r\na=ssrc:988490045 msid:ARDAMS ARDAMSv0\r\na=ssrc:988490045 mslabel:ARDAMS\r\na=ssrc:988490045 label:ARDAMSv0\r\n",
"label": 0,
"type": "offer"
},
"type": "offer"
}
}
answer:
{
"from": "JX121110700032_WEB",
"to": "JX121110700032_APP",
"message": {
"data": {
"description": "v=0\r\no=- 7563495410314281449 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE video\r\na=msid-semantic: WMS\r\nm=video 9 UDP/TLS/RTP/SAVPF 96 98 100 127 97 99 101\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:2G/F\r\na=ice-pwd:oUee5+4N4Dzg3bXWOEtMGZIR\r\na=ice-options:trickle\r\na=fingerprint:sha-256 7F:A7:28:67:77:F2:F4:C3:54:69:E8:3C:61:1B:7F:C2:A4:D5:1E:38:DC:86:F5:CB:8A:24:B8:87:25:DE:E6:0A\r\na=setup:active\r\na=mid:video\r\na=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:4 urn:3gpp:video-orientation\r\na=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r\na=recvonly\r\na=rtcp-mux\r\na=rtcp-rsize\r\na=rtpmap:96 VP8/90000\r\na=rtcp-fb:96 goog-remb\r\na=rtcp-fb:96 transport-cc\r\na=rtcp-fb:96 ccm fir\r\na=rtcp-fb:96 nack\r\na=rtcp-fb:96 nack pli\r\na=rtpmap:98 VP9/90000\r\na=rtcp-fb:98 goog-remb\r\na=rtcp-fb:98 transport-cc\r\na=rtcp-fb:98 ccm fir\r\na=rtcp-fb:98 nack\r\na=rtcp-fb:98 nack pli\r\na=fmtp:98 profile-id=0\r\na=rtpmap:100 red/90000\r\na=rtpmap:127 ulpfec/90000\r\na=rtpmap:97 rtx/90000\r\na=fmtp:97 apt=96\r\na=rtpmap:99 rtx/90000\r\na=fmtp:99 apt=98\r\na=rtpmap:101 rtx/90000\r\na=fmtp:101 apt=100\r\n",
"type": "answer"
},
"type": "answer"
}
}
candidate:
{
"from": "JX121110700032_WEB",
"to": "JX121110700032_APP",
"message": {
"data": {
"candidate": "candidate:369914011 1 udp 2122260223 10.13.159.140 62792 typ host generation 0 ufrag 3y6A network-id 1 network-cost 10",
"label": 0,
"id": "video"
},
"type": "candidate"
}
}
应用实践
架构图
模块说明
MainActivity:App主页,通过Binder与录屏Service和悬浮Service通信
录屏Service:是一个前台服务,用于实现录屏、socket通信、P2P连接,包含VideoCapture、WebSocket、Peer、多个RTC流程模块。
悬浮Service:用于承载悬浮窗,app退出时,仍可悬浮于其他应用之上。
P2P连接时序图
WebRTC的使用
- WebRTC配置
gradle dependencies配置:
implementation "org.java-websocket:Java-WebSocket:1.5.2"
implementation files('libs/audio_device_java.jar')
implementation files('libs/autobanh.jar')
implementation files('libs/base_java.jar')
implementation files('libs/libjingle_peerconnection.jar')
混淆配置:
-keep class org.webrtc.** { *; }
权限配置:
<uses-permission android:name="android.permission.INTERNET" />
-
WebRTC的使用
客户端和服务端通信,采用WebSocket,wss协议。 -
创建WebSocket
String host = "ws://ws.xx.com/server/xx";
URI serverURI = URI.create(host);
mWebSocketClient = new WebSocketClient(serverURI) {
@Override public void onOpen(ServerHandshake handshakedata) {
Log.d(TAG, "socket state connect");
}
@Override public void onMessage(String message) {
Log.d(TAG, "web socket onMessage:" + message);
try {
TestBean bean = new Gson().fromJson(message, TestBean.class);
if (bean != null && bean.message != null) {
if ("init".equals(bean.message.type)) {
Log.d(TAG, "初始化成功");
if (!mIsReady) {
mIsTerminalEnd = false;
mIsReady = true;
messageHandler.onMessage("ready", id, payload);
}
} else if ("answer".equals(bean.message.type)) {
Log.d("收到answer");
if (bean.message.data != null) {
JSONObject payload = new JSONObject();
payload.put("type", bean.message.data.type);
payload.put("description", bean.message.data.description);
messageHandler.onMessage("answer", id, payload);
}
} else if ("candidate".equals(bean.message.type)) {
Log.d("收到candidate");
if (bean.message.data != null) {
JSONObject payload = new JSONObject();
payload.put("id", bean.message.data.id);
payload.put("label", bean.message.data.label);
payload.put("candidate", bean.message.data.candidate);
messageHandler.onMessage("candidate", id, payload);
}
}
}
} catch (Exception e) {
Log.e(TAG, "WebSocket连接异常:" + e.getLocalizedMessage());
}
}
@Override public void onClose(int code, String reason, boolean remote) {
Log.d(TAG, "web socket onClose code:" + code + " reason:" + reason + " remote:" + remote);
}
@Override public void onError(Exception ex) {
Log.d(TAG, "web socket onError:" + ex.getLocalizedMessage());
}
};
mWebSocketClient.connect();
- 创建PeerConnectionFactory
PeerConnectionFactory.initializeAndroidGlobals(mContext, params.videoCodecHwAcceleration);
factory = new PeerConnectionFactory(null);
- 初始化音视频
WebScoket建立连接,接收到Web端连接成功的消息后,初始化本地音视频,并开始录屏。
private void initScreenCaptureStream() {
mLocalMediaStream = factory.createLocalMediaStream("ARDAMS");
mVideoSource = factory.createVideoSource(videoCapturer);
// 开始录屏
videoCapturer.startCapture(mPeerConnParams.videoWidth, mPeerConnParams.videoHeight, mPeerConnParams.videoFps);
// 添加VideoTrack
mLocalMediaStream.addTrack(factory.createVideoTrack("ARDAMSv0", mVideoSource));
// 添加AudioTrack
//AudioSource audioSource = factory.createAudioSource(new MediaConstraints());
//mLocalMediaStream.addTrack(factory.createAudioTrack("ARDAMSa0", audioSource));
}
- 创建DataChannel
P2P连接建立后,两端通过DataChannel通信,不再依赖WebSocket。收到Web端连接成功的消息后,客户端创建Peer,并创建DataChannel。
public Peer(String id, int endPoint) {
Log.d(TAG, "new Peer: " + id + " " + endPoint);
this.pc = factory.createPeerConnection(iceServers, mPeerConnConstraints, this);
this.dataChannel = pc.createDataChannel("testDataChannel", new DataChannel.Init());
this.dataChannel.registerObserver(dataObserver);
this.id = id;
this.endPoint = endPoint;
pc.addStream(mLocalMediaStream); //, new MediaConstraints()
}
DataChannel.Observer dataObserver = new DataChannel.Observer() {
@Override public void onBufferedAmountChange(long l) {
}
@Override public void onStateChange() {
}
@Override public void onMessage(DataChannel.Buffer buffer) {
Charset charset = StandardCharsets.UTF_8;
CharBuffer charBuffer = charset.decode(buffer.data);
String data = charBuffer.toString();
try {
JSONObject jsonObject = new JSONObject(data);
String type = jsonObject.optString("key");
String operate = jsonObject.optString("value");
// TODO 执行adb命令
... ...
} catch (JSONException e) {
e.printStackTrace();
}
}
};
业务指令
建立P2P连接后,Web端的操作,通过DataChannel发送事件。客户端通过adb命令模拟点击、触屏、滑动事件。
点击事件:
示例:
客户端响应:adb shell input keyevent 3
触摸事件:
示例:
客户端响应:adb shell input tap 100 200
滑动事件:
示例:
客户端响应:adb shell input swipe 100 200 300 200
注:客户端执行adb命令,需系统签名,否则无权限。