AppRTC-Android 源码导读

课程地址:零声学院 WebRTC入门与提高 https://ke.qq.com/course/435382?tuin=137bb271

技术支持QQ群:782508536

image

源地址:AppRTC-Android 源码导读
注:本博主有修改

WebRTC 的安卓 demo 工程—— AppRTC-Android 的源码导读

概览

让我们先搞清楚 WebRTC 的几个核心类以及它们之间的关系,首先是三大核心类

  • MediaStream,获取 Audio/Video 数据;
  • PeerConnection,交换 Audio/Video 数据;
  • DataChannel,收发任意数据;

MediaStream 中包含多个 Track(AudioTrack,VideoTrack),用于采集数据,PeerConnection 则包含多个 Stream。【本地数据叫采集,是 Stream 的事,那远端的数据呢?在本地也是一个 Stream 吗?】而数据则通过 DataChannel 收发。【音视频数据也通过 DataChannel 收发?】

WebRTC 的代码量不小,一次性看明白不太现实,在本文中,我将试图搞清楚三个问题:

  1. 客户端之间如何建立连接?
  2. 客户端之间如何协商音视频相关的配置?
  3. 音视频数据的采集、预览、编码、传输、解码、渲染完整流程。

当然,限于篇幅和实际需求,细节之处我不会展开,但把握住了主线之后,日后再有具体需求,我们就可以快速找到相关代码了。

建立连接

连接的建立涉及到roomserver和signal server。

  • 首先是房间创建者(initiator)向 Room Server 发起请求,获取房间配置信息,各种 URL 信息;
  • 创建者获取到相关信息之后,创建 offer sdp,发送给 Room Server;
  • 接着是房间加入者向 Room Server 发起请求,除了获取房间配置信息,还会获取到 offer sdp;
  • 加入者创建 answer sdp,利用 Signal Server(长连接)发送给创建者,同时开始建立 P2P 连接;
  • 创建者收到 answer sdp 之后,也开始建立 P2P 连接;

下面是一张简单的示意图(懒得画高大上的流程图了):


下面是创建者关键的代码位置:

  • 入口代码是 CallActivity#startCall,调用 appRtcClient.connectToRoom,这里我们配置了 server url,所以 appRtcClient 是 WebSocketRTCClient;
  • WebSocketRTCClient#connectToRoomInternal,我们利用 RoomParametersFetcher 获取配置信息;(这里是向room server的请求)
  • RoomParametersFetcher#makeRequest,我们利用 AsyncHttpURLConnection 发出 POST 请求,请求返回后,我们在 roomHttpResponseParse 中解析数据,解析完毕后,回调到 WebSocketRTCClient;
  • WebSocketRTCClient#signalingParametersReady,其中保存了相关信息后,回调到 CallActivity;
  • CallActivity#onConnectedToRoomInternal,在这里我们就要区分自己的角色了,我们是创建者,则开始创建 offer sdp;

涉及到 sdp 之后,就从 app 进入到 WebRTC 的库里面了。
创建 offer 在库里面的调用流程:

  • PeerConnection#createOffer
    【native 调用】
  • SdpObserver#onCreateSuccess,也就是 PeerConnectionClient.SDPObserver#onCreateSuccess,其中我们会设置 local sdp(offer);
    PeerConnection#setLocalDescription
    【native 调用】
  • SdpObserver#onSetSuccess,也就是 PeerConnectionClient.SDPObserver#onSetSuccess,此时我们还没有 remote sdp(answer),则我们会回调到 CallActivity;
  • CallActivity#onLocalDescription,我们是创建者,则调用 appRtcClient.sendOfferSdp 发送 offer sdp,然后等待加入者的 answer;

接下来是加入者关键代码的位置:

  • 从入口代码到获取房间配置,代码路径都和创建者一致,但在 CallActivity#onConnectedToRoomInternal 中,我们已经获取到了 remote sdp(offer),所以我们先设置 remote sdp;
  • PeerConnection#setRemoteDescription
    【native 调用】
  • SdpObserver#onSetSuccess,也就是 PeerConnectionClient.SDPObserver#onSetSuccess,此时我们还没有 local sdp(answer),但我们会马上创建 answer sdp;
  • PeerConnection#createAnswer
    【native 调用】
  • SdpObserver#onCreateSuccess,也就是 PeerConnectionClient.SDPObserver#onCreateSuccess,其中我们会设置 local sdp(answer);
  • PeerConnection#setLocalDescription
    【native 调用】
  • SdpObserver#onSetSuccess,也就是 PeerConnectionClient.SDPObserver#onSetSuccess,此时我们已经有了 local sdp(answer),则我们会回调到 CallActivity,并且开始添加 ICE candidate;
  • 最后在 CallActivity#onLocalDescription 中,我们调用 appRtcClient.sendAnswerSdp 把 answer sdp 发送给创建者;

接下来是创建者收到 answer 之后的处理流程:

  • WebSocketRTCClient#onWebSocketMessage 中,我们收到 answer 之后,会回调到 CallActivity;
  • CallActivity#onRemoteDescription 中我们设置 remote sdp;
  • PeerConnection#setRemoteDescription
    【native 调用】
  • SdpObserver#onSetSuccess,也就是 PeerConnectionClient.SDPObserver#onSetSuccess,此时我们已经有了 remote sdp(answer),则开始添加 ICE candidate;

上面 setLocalDescription/setRemoteDescription 有一段较长的调用链,它们会分别回调到 onCreateSuccess/onSetSuccess。

【ICE candidate 怎么生成的?怎么使用的?添加 ICE candidate 会开始建立 P2P 连接?】这块没有太大的需求,先不看了... (P2P是内部进行?)

SDP

这块没有太大的需求,先不看了...

音视频全流程

我们先看视频流程,WebRTC 支持多种数据源,Camera1、Camera2、录屏,我们以 Camera1 采集为例,整个流程包括以下环节:

  • 相机到预览;
  • 相机到编码;
  • 编码到传输;
  • 传输到解码;
  • 解码到渲染;

相机到预览

我们可以采取两头夹逼的方式来探究,采集是 Camera1,那就是 Camera1Capturer,预览则是 SurfaceViewRenderer,整个数据流动的关键环节如下:

  • 帧数据从 Camera 出来,形式是 SurfaceTexture
  • SurfaceTextureHelper#tryDeliverTextureFrame
  • Camera1Session#listenForTextureFrames 中构造的匿名 SurfaceTextureHelper.OnTextureFrameAvailableListener
  • CameraCapturer#onTextureFrameCaptured
  • AndroidVideoTrackSourceObserver#onTextureFrameCaptured,从这里开始,数据就要从 Java 层进入到 native 层了
  • webrtc/sdk/android/src/jni/androidvideotracksource.ccAndroidVideoTrackSource::OnTextureFrameCaptured
  • webrtc/media/base/adaptedvideotracksource.ccAdaptedVideoTrackSource::OnFrame
  • webrtc/media/base/videobroadcaster.ccVideoBroadcaster::OnFrame
  • webrtc/sdk/android/src/jni/video_renderer_jni.ccJavaVideoRendererWrapper::OnFrame,从这里开始,数据又要从 native 层回到 Java 层了
  • VideoRenderer.Callbacks#renderFrame,也就是 SurfaceViewRenderer#renderFrame

而这个数据流的各个环节是怎么一步步建立起来的呢?就在创建 PeerConnection 的过程中:PeerConnectionClient#createPeerConnectionInternal,其中调用了 PeerConnectionClient#createVideoTrack,创建好了 VideoTrack,这一数据流就建立起来了:

  • 创建一个 AndroidVideoTrackSourceObserver,它实现了 VideoCapturer.CapturerObserver 接口,在相机输出帧数据的时候,其 onTextureFrameCaptured 会被调用,这就是上面提到的数据从 Java 层进入到 native 层的起点;
  • 调用 VideoCapturer#initialize,把 Observer 和 Capturer 绑定起来;
  • 创建 VideoSource,它对应于 webrtc/sdk/android/src/jni/androidvideotracksource.h 中定义的 AndroidVideoTrackSource
  • 调用 VideoCapturer#startCapture,开始采集;
  • 用 VideoSource 创建 VideoTrack;
  • 创建 VideoRenderer,它对应于 webrtc/media/base/videosinkinterface.h 中定义的 VideoSinkInterface,这里我们实际创建的 native 对象是 webrtc/sdk/android/src/jni/video_renderer_jni.h 中定义的 JavaVideoRendererWrapper,这就是上面提到的数据从 native 层回到 Java 层的起点;
  • 为 VideoTrack 增加 VideoRenderer,其对应的 native 部分就会为 native 的 Track 增加 Sink 了;

至此,视频数据从相机到预览的流动过程就已经清晰了,而这个流程是怎么建立起来的,应该也已经很清楚了。

这里有一个 NDK 开发的技巧,对于 Java 对象把调用转发到 native 层的场景,我们需要把 Java 对象和 native 对象关联起来,通常做法是把 native 对象的指针作为 Java 对象的一个 long 类型的 nativeHandle 成员,native 函数都传入这个 handle,这样就可以调用 native 对象的方法了。

最后,如果你想看图,下面是手写的调用步骤 :)

【图一】

【图二】

相机到编码

  • webrtc/video/vie_encoder.ccViEEncoder::OnFrame
  • ViEEncoder::EncodeTask::Run
  • ViEEncoder::EncodeVideoFrame
  • webrtc/modules/video_coding/video_sender.ccVideoSender::AddVideoFrame
  • webrtc/modules/video_coding/generic_sender.ccVCMGenericEncoder::Encode
  • webrtc/sdk/android/src/jni/androidmediaencoder_jni.ccMediaCodecVideoEncoder::Encode -> EncodeTexture
  • MediaCodecVideoEncoder#encodeTexture:把 texture 内容输入到 MediaCodec 中
  • webrtc/sdk/android/src/jni/androidmediaencoder_jni.ccMediaCodecVideoEncoder::EncodeTask::Run
  • MediaCodecVideoEncoder::DeliverPendingOutouts:调用 Java 层接口,消费 MediaCodec 输出数据

剥离 WebRTC 采集、预览、编码

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

推荐阅读更多精彩内容