2023-03-02 Android WebRTC流程梳理

关于 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】。

    1. 在【onId】响应中,最终在WebRtcClient的监听方法:onCallReady()里,调用了设置本地预览画面的代码
    1. 在【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-answer.png

上述代码中:

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

推荐阅读更多精彩内容