关于 https://github.com/xiejinpeng007/WebRTC-Android-Server 的代码梳理
1. 点击设定按钮,本地预览画面创建
创建WebRtcClient对象
在WebRtcClient的init() 初始化代码块中:
- 首先初始化 PeerConnectionFactory
// 初始化 PeerConnectionFactory
PeerConnectionFactory.initialize(
PeerConnectionFactory
.InitializationOptions
.builder(app)
.createInitializationOptions()
- 创建PeerConnectionFactory的工厂对象 factory
这个工厂类非常重要,在后续创建连接及音视频采集/编解码中,需要其为我们生成各种重要的组件。如:PeerConnection、VideoSource、VideoTrack等…采用Builder模式对其进行初始化,可方便对其进行编解码器的设置
private val factory: PeerConnectionFactory
factory = PeerConnectionFactory.builder()
// 视频编码器
.setVideoDecoderFactory(
DefaultVideoDecoderFactory(eglContext)
)
// 视频解码器
.setVideoEncoderFactory(
DefaultVideoEncoderFactory(
eglContext, true, true
)
)
.createPeerConnectionFactory()
- 初始化 socket-io 通信(个人理解,这个通过任意长连接都可以,例如webSocket、Netty等)
主要是沟通传递信令消息,完成应答机制的(offer、answer)
val messageHandler = MessageHandler()
socket = IO.socket(url)
socket?.on("id", messageHandler.onId)
socket?.on("message", messageHandler.onMessage)
socket?.on("ids", messageHandler.onIdsChanged)
socket?.connect()
// 消息护理类MessageHandler
private inner class MessageHandler {
val onMessage = Emitter.Listener { args ->
val data = args[0] as JSONObject
try {
val from = data.getString("from")
val type = data.getString("type")
var payload: JSONObject? = null
if (type != "init") {
payload = data.getJSONObject("payload")
}
//用于检查是否 PC 是否已存在已经是否达到最大的2个 PC 的限制
if (!peers.containsKey(from)) {
val endPoint = findEndPoint()
if (endPoint == MAX_PEER) return@Listener
else addPeer(from, endPoint)
}
//根据不同的指令类型和数据响应相应步骤的方法
when (type) {
"init" -> createOffer(from)
"offer" -> createAnswer(from, payload)
"answer" -> setRemoteSdp(from, payload)
"candidate" -> addIceCandidate(from, payload)
}
} catch (e: JSONException) {
e.printStackTrace()
}
}
val onId = Emitter.Listener { args ->
val id = args[0] as String
webrtcListener.onCallReady(id)
}
val onIdsChanged = Emitter.Listener { args ->
Log.d(TAG, args.toString())
val ids = args[0] as JSONArray
webrtcListener.onOnlineIdsChanged(ids)
}
}
项目配套的node.js服务器,会在socket-io建立连接后,发送【id】 和【ids】两个消息,对应messageHandler的【onId】和【onIdsChanged】。
- 在【onId】响应中,最终在WebRtcClient的监听方法:onCallReady()里,调用了设置本地预览画面的代码
- 在【ids】响应中,最终回调到WebRtcClient的监听方法:onOnlineIdsChanged(),刷新用户列表
2. 点击用户列表中的call,发起呼叫的过程
* 发送消息
// 在call按钮的点击事件中
webRtcClient?.callByClientId(it)
fun callByClientId(clientId: String) {
sendMessage(clientId, "init", JSONObject())
}
private fun sendMessage(to: String, type: String, payload: JSONObject) {
val message = JSONObject()
message.put("to", to)
message.put("type", type)
message.put("payload", payload)
socket?.emit("message", message)
}
上述代码,发送了一条如下json字符串:
{
to : clientId, // call的那个人的userId
type:"init",
payload:{}
}
* 发起call请求后,init的消息响应
在MessageHandler 的 【onMessage】 监听里,收到一条来自信令服务器的json消息:
{"type":"offer","payload":{"type":"offer","sdp":"v=0\r\no=- 2982456486120911597 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE audio video\r\na=msid-semantic: WMS LOCALMEDIASTREAM\r\nm=audio 9 UDP\/TLS\/RTP\/SAVPF 111 103 104 9 102 0 8 106 105 13 110 112 113 126\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:B59x\r\na=ice-pwd:gdQfA+AmFDbGXrM7\/eM2vADD\r\na=ice-options:trickle renomination\r\na=fingerprint:sha-256 E4:09:A7:58:0D:62:AF:60:94:3A:21:E1:F1:50:09:A1:D9:B0:2F:B8:2C:77:25:20:73:30:A2:6E:4A:22:C3:0C\r\na=setup:actpass\r\na=mid:audio\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:2 http:\/\/www.ietf.org\/id\/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=recvonly\r\na=rtcp-mux\r\na=rtpmap:111 opus\/48000\/2\r\na=rtcp-fb:111 transport-cc\r\na=fmtp:111 minptime=10;useinbandfec=1\r\na=rtpmap:103 ISAC\/16000\r\na=rtpmap:104 ISAC\/32000\r\na=rtpmap:9 G722\/8000\r\na=rtpmap:102 ILBC\/8000\r\na=rtpmap:0 PCMU\/8000\r\na=rtpmap:8 PCMA\/8000\r\na=rtpmap:106 CN\/32000\r\na=rtpmap:105 CN\/16000\r\na=rtpmap:13 CN\/8000\r\na=rtpmap:110 telephone-event\/48000\r\na=rtpmap:112 telephone-event\/32000\r\na=rtpmap:113 telephone-event\/16000\r\na=rtpmap:126 telephone-event\/8000\r\nm=video 9 UDP\/TLS\/RTP\/SAVPF 96 97 98 99 100 101 127 124 125\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:B59x\r\na=ice-pwd:gdQfA+AmFDbGXrM7\/eM2vADD\r\na=ice-options:trickle renomination\r\na=fingerprint:sha-256 E4:09:A7:58:0D:62:AF:60:94:3A:21:E1:F1:50:09:A1:D9:B0:2F:B8:2C:77:25:20:73:30:A2:6E:4A:22:C3:0C\r\na=setup:actpass\r\na=mid:video\r\na=extmap:14 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:13 http:\/\/www.webrtc.org\/experiments\/rtp-hdrext\/abs-send-time\r\na=extmap:3 urn:3gpp:video-orientation\r\na=extmap:2 http:\/\/www.ietf.org\/id\/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:5 http:\/\/www.webrtc.org\/experiments\/rtp-hdrext\/playout-delay\r\na=extmap:6 http:\/\/www.webrtc.org\/experiments\/rtp-hdrext\/video-content-type\r\na=extmap:7 http:\/\/www.webrtc.org\/experiments\/rtp-hdrext\/video-timing\r\na=extmap:8 http:\/\/tools.ietf.org\/html\/draft-ietf-avtext-framemarking-07\r\na=extmap:9 http:\/\/www.webrtc.org\/experiments\/rtp-hdrext\/color-space\r\na=sendrecv\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:97 rtx\/90000\r\na=fmtp:97 apt=96\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=rtpmap:99 rtx\/90000\r\na=fmtp:99 apt=98\r\na=rtpmap:100 H264\/90000\r\na=rtcp-fb:100 goog-remb\r\na=rtcp-fb:100 transport-cc\r\na=rtcp-fb:100 ccm fir\r\na=rtcp-fb:100 nack\r\na=rtcp-fb:100 nack pli\r\na=fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\na=rtpmap:101 rtx\/90000\r\na=fmtp:101 apt=100\r\na=rtpmap:127 red\/90000\r\na=rtpmap:124 rtx\/90000\r\na=fmtp:124 apt=127\r\na=rtpmap:125 ulpfec\/90000\r\na=ssrc-group:FID 4194079609 283063961\r\na=ssrc:4194079609 cname:99ClzwXGhIkyvqMM\r\na=ssrc:4194079609 msid:LOCALMEDIASTREAM LOCALMEDIASTREAM\r\na=ssrc:4194079609 mslabel:LOCALMEDIASTREAM\r\na=ssrc:4194079609 label:LOCALMEDIASTREAM\r\na=ssrc:283063961 cname:99ClzwXGhIkyvqMM\r\na=ssrc:283063961 msid:LOCALMEDIASTREAM LOCALMEDIASTREAM\r\na=ssrc:283063961 mslabel:LOCALMEDIASTREAM\r\na=ssrc:283063961 label:LOCALMEDIASTREAM\r\n"},"from":"0hqg_yi2bF0LtDr6AAAk"}
其中type为offer,进入socket的offer事件中
// 个人理解,应答和代表连接的类
private val peers = HashMap<String, Peer>()
// from 是信令服务器传来的,其实是选择呼叫的那个userId
// endPoint是个int,代表下标
addPeer(from,endPoint)
when (type) { // type 是offer,进入 createOffer()中
"init" -> createOffer(from)
"offer" -> createAnswer(from, payload)
"answer" -> setRemoteSdp(from, payload)
"candidate" -> addIceCandidate(from, payload)
}
* 创建offer
private fun createOffer(peerId: String) {
Log.d(TAG, "CreateOfferCommand")
val peer = peers[peerId]
peer?.pc?.createOffer(peer, pcConstraints)
}
pc 是 在Peer里维护的一个PeerConnection对象,在Peer创建的时候,创建。
PeerConnection :顾名思义,这个类代表点对点之间的连接,可以从远端获取音视频流等数据。在创建之前可以通过RTCConfiguration对连接进行详细的配置,最后通过createPeerConnection()方法完成创建。
通过PeerConnection.createOffer()方法,创建offer后,会在SdpObserver中收到onCreateSuccess回调,此时调用setLocalDescription()方法将该Offer保存到本地Local域,然后将Offer发送给对方。
在本项目里,SdpObserver即Peer类,该类的onCreateSuccess()回调如下:
override fun onCreateSuccess(sdp: SessionDescription) {
// TODO: modify sdp to use pcParams prefered codecs
try {
val payload = JSONObject()
payload.put("type", sdp.type.canonicalForm())
payload.put("sdp", sdp.description)
// 将offer发送给对方
sendMessage(id, sdp.type.canonicalForm(), payload)
pc!!.setLocalDescription(this@Peer, sdp)
} catch (e: JSONException) {
e.printStackTrace()
}
}
上述代码中,type是:ANSWER;
sdp.description是:
v=0
o=- 3764191716774981901 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE audio video
a=msid-semantic: WMS LOCALMEDIASTREAM
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 102 0 8 106 105 13 110 112 113 126
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:ecOU
a=ice-pwd:sj7eXDXOmULhV/JaqK2dbsTH
a=ice-options:trickle renomination
a=fingerprint:sha-256 01:39:F5:A9:FA:1E:D3:1D:8B:B1:C2:71:BF:22:95:F7:78:61:A0:41:69:8D:FF:AC:AF:0E:F4:38:F4:60:A6:5E
a=setup:active
a=mid:audio
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:2 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=inactive
a=rtcp-mux
a=rtpmap:111 opus/48000/2
a=rtcp-fb:111 transport-cc
a=fmtp:111 minptime=10;useinbandfec=1
a=rtpmap:103 ISAC/16000
a=rtpmap:104 ISAC/32000
a=rtpmap:9 G722/8000
a=rtpmap:102 ILBC/8000
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:106 CN/32000
a=rtpmap:105 CN/16000
a=rtpmap:13 CN/8000
a=rtpmap:110 telephone-event/48000
a=rtpmap:112 telephone-event/32000
a=rtpmap:113 telephone-event/16000
a=rtpmap:126 telephone-event/8000
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 127 124 125
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:ecOU
a=ice-pwd:sj7eXDXOmULhV/JaqK2dbsTH
a=ice-options:trickle renomination
a=fingerprint:sha-256 01:39:F5:A9:FA:1E:D3:1D:8B:B1:C2:71:BF:22:95:F7:78:61:A0:41:69:8D:FF:AC:AF:0E:F4:38:F4:60:A6:5E
a=setup:active
a=mid:video
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
a=extmap:13 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 urn:3gpp:video-orientation
a=extmap:2 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
a=extmap:8 http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07
a=extmap:9 http://www.webrtc.org/experiments/rtp-hdrext/color-space
a=sendrecv
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:97 rtx/90000
a=fmtp:97 apt=96
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=rtpmap:99 rtx/90000
a=fmtp:99 apt=98
a=rtpmap:100 H264/90000
a=rtcp-fb:100 goog-remb
a=rtcp-fb:100 transport-cc
a=rtcp-fb:100 ccm fir
a=rtcp-fb:100 nack
a=rtcp-fb:100 nack pli
a=fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=rtpmap:101 rtx/90000
a=fmtp:101 apt=100
a=rtpmap:127 red/90000
a=rtpmap:124 rtx/90000
a=fmtp:124 apt=127
a=rtpmap:125 ulpfec/90000
a=ssrc-group:FID 1341000078 3749448991
a=ssrc:1341000078 cname:vvbLxysQ9u9PyfZN
a=ssrc:1341000078 msid:LOCALMEDIASTREAM LOCALMEDIASTREAM
a=ssrc:1341000078 mslabel:LOCALMEDIASTREAM
a=ssrc:1341000078 label:LOCALMEDIASTREAM
a=ssrc:3749448991 cname:vvbLxysQ9u9PyfZN
a=ssrc:3749448991 msid:LOCALMEDIASTREAM LOCALMEDIASTREAM
a=ssrc:3749448991 mslabel:LOCALMEDIASTREAM
a=ssrc:3749448991 label:LOCALMEDIASTREAM
至此,offer创建完毕。
备注:应答方answer的创建放在后面单独梳理。整个过程如下图
上述代码中:
// 将offer发送给对方
sendMessage(id, sdp.type.canonicalForm(), payload)
发送消息给信令服务器后,对方的处理流程暂略,会回到socket的【onMessage]监听中,返回的json消息为:
{"type":"candidate","payload":{"label":0,"id":"audio","candidate":"candidate:3061417881 1 udp 2122260223 192.168.4.141 38266 typ host generation 0 ufrag YmYk network-id 6 network-cost 10"},"from":"YgJQ_NYLzH5_EisjAAAs"}
from是呼叫的那个人的userId,type 是candidate ,会走到MessageHandler的【onMessage】,再到addIceCandidate()方法
private fun addIceCandidate(peerId: String, payload: JSONObject?) {
Log.d(TAG, "AddIceCandidateCommand")
val pc = peers[peerId]!!.pc
if (pc!!.remoteDescription != null) {
val candidate = IceCandidate(
payload!!.getString("id"),
payload.getInt("label"),
payload.getString("candidate")
)
pc.addIceCandidate(candidate)
}
}
信令服务器传过来的json里就有IceCandidate 对象(这是被呼叫方传来的他的IceCandidate)的信息,在本地创建IceCandidate,把json中的数据设置进去,在调用PeerConnection.addIceCandidate()把IceCandidate添加进去。
(至此,点对点连接完成)
备注:
在双方offer、answer 的媒体协商过程结束后,被呼叫方的PeerConnection.Observer会回调onIceCandidate()方法并提供IceCandidate对象,这个时候我们把他组装为candidate的SDP信令发送到信令服务器,透传给另外一端(也就是上方收到的IceCandidate)。关于应答方,在下方有具体流程,这里为避免理解有误,把应答方的点对点连接建立做的事情也一并说了。
* 绘制远端画面
当点对点连接建立起来后,我们就可以开始获取音视频流数据了。之前在createPeerConnection()中传入的PeerConnection.Observer会回调onAddStream()方法(注意此方法会在收到远端SDP并调用setRemoteDescription()后,就会回调了,不用等连接真正建立,与onAddTrack()一致),并提供MediaStream对象,其中包含远端的音视频轨AudioTracks与VideoTracks。前面我们添加了一条录屏的视频轨,因此直接获取第一条VideoTrack对象即可,然后跟之前一样通过addSink()即可与SurfaceViewRenderer的绑定,从而渲染出视频流。
override fun onAddStream(mediaStream: MediaStream) {
Log.d(TAG, "onAddStream " + mediaStream.id)
// remote streams are displayed from 1 to MAX_PEER (0 is localStream)
webrtcListener.onAddRemoteStream(mediaStream, endPoint + 1)
}
// webrtcListener.onAddRemoteStream(),最终会调用Activity里创建WebRtcClient时,设置的监听对象的onAddRemoteStream()方法里
// 在Activity
override fun onAddRemoteStream(remoteStream: MediaStream, endPoint: Int) {
// 显示远端画面
remoteStream.videoTracks[0].addSink(binding.remoteRenderer)
}