WebRTC学习记录以及以Janus-gateway流程增进理解

发布时间 2023-04-14 12:33:04作者: J0K3Rzz

这篇文章是我按照我的学习习惯记录的文章,借鉴了许多大佬的学习框架,以及独自去验证正确性的一个过程
Web 实时通信(Real-Time Communication)

概述
https://webrtcforthecurious.com/zh/docs/01-what-why-and-how/

看完只有一个感受 :为什么音视频要扯上web,其中的协议大部分都来自web的世界,思考的泉水都是起源于web的思考框架,webrtc去掉音视频 也是一个很好的p2p 框架,加上音视频的难度就在于传输的内容必须是低延时性和交互性(Real-Time),这里反而是webrtc最引人注目的地方,也是webrtc创新的地方。

信令 sdp 
连接 nat,利用udp,stun or turn   ice
安全 DTLS 
传输 RTP,RTCP,SRTP(利用DTLS的秘钥),SRTCP(利用DTLS的秘钥),SCTP(keep-alive)

这里利用开源框架 janus-gateway 了解实实现细节,网上吹嘘这个的也挺多,咱来一探究竟,
首先把demo建立起来,打开日志,在echotest的demo下,将相应的日志文件获取,获取日志文件阅读过程,将整个源码的流程理清。

从janus的main函数开始,其实主要功能注释都有提供,主要有两个方面可以探讨,一个是框架设计(如何将WebRTC的思路集成至框架中并且考量未来扩展),再一个就是WebRTC的代码设计细节

加载插件的思路实际就是从配置文件读取对应插件的名字以及路径,使用动态库链接,并解使用统一的接口进行初始化。

  1. config 阶段
    分为四类 janus eventhandler(将消息发布出去所使用的组件) transport(传输) plugin

  2. 根据config创建 守护进程

  3. 初始化logger 加载日志插件

  4. 处理剩余config

  5. 处理 特殊ip

  6. 获取本地ip

  7. 客户端安全验证 配置应用

  8. 配置ice所需的stun 和turn服务器地址,一开始我的理解webrtc 是客户端的端到端,为啥网关也需要呢,假如该gateway部署在nat之后就需要了,后面理解了gateway本身也可以是客户端,网关只是起到一个转发的作用。

  9. sessions( HashTable  ) 超时watchdog(房间session超时等回调)

  10. janus_requests 分发队列 初始化 janus的request就同步处理  其它request 异步处理丢进线程池异步处理

  11. 读取配置文件加载event handler ,获取所有event handler 
    文档详细描述:https://janus-legacy.conf.meetecho.com/docs/eventhandlers.html
    event handler抽象结构声明 janus_eventhandler 具体实现由对应eventhandler动态库里的加载执行
    这里以websocket的eventhandler 分析一下这个事件处理器的主要工作内容
    读取相应配置 -> 启动一个websocket thread 处理ws ,启动一个消息处理线程 (janus core广播消息的回调就是将该消息放到该队列当中
    event -> 进去event thread 解析出message 并丢进message的队列,websocket 线程将该消息丢给ws 传递出去
    如何处理事件,事件需要广播,初始化event事件队列,开启新线程处理event(分发给每个handler)

  12. 读取配置文件 加载plugin
    这里需要注意插件是如何和客户端通信的 配合这篇博文食用更佳 https://blog.csdn.net/cgs1999/article/details/94436089
    这里找echo-test 最简单的插件分析,可以发现插件和core的交流基本上和业务没有什么关系,所以这里可以抽离出来,这里分析抽象的plugin行为应该是怎么样的
    具体行为不在main函数里,看一下对plugins 的操作 ,可以看到plugin的sessionid是由从第10步中的janus_process_incoming_request 传递的。
    从sessionid入手,发现sessionid是janus_request的message传递。 而janus_request的message是由transport 传进来的。那transport中的message的来源呢?从第13步的transport即可知道。来源知道了,剩下去处。
    session的开始是由create开始,如果json消息里没有session且是create消息,就会根据逻辑创建session。
    以下几个阶段:


    create(创建janus会话)
    通过客户端生成的transfaction 创建core_session,transfaction确保是同一个core_session,后续的操作都需要带上这个sessionid
    attach(创建ICE会话,上下文包括插件以及janus_session)
    以插件编写的get_package 的返回值为key 找已注册的插件,验证token,并创建一个handle,这个handle的定义为janus_ice_handle (用于标识一个ICE会话,封装了ICE的传输信息,用于一个通道实际的数据收发,一个core_session上可以有若干janus_ice_handle,实际就是attach多个插件的意思);janus_ice_handle_attach_plugin 利用core_session 和 handle 和插件建立plugin_session(具体由插件实现,和插件的业务相关),并补充handle的信息,这里详细介绍下handle 中的几个关键点。主要创建一个新的上下文并启动事件循环和一个消息队列(janus_ice_outgoing_traffic)以及线程资源用于协商ice会话或者其它事项.
    webrtc过程
    SDP交换过程
    根据WebRTC的协议定义,这个过程就是需要想办法吧sdp提供给两边。
    客户端创建sdp 并用上述会话以janus message( janus的message 需要带core session和 handle 的id才能确定具体的handle )承载sdp信息传给janus-gateway,janus-gateway属于sdp(signal-server的作用)
    解析sdp->基于libnice库和janus建立ice链接(Setup ICE locally)初始化会话后续所步骤需要的资源,等待客户端建立ice连接->插件处理sdp并获取本地的sdp以event消息传递给客户端。这里需要理解为什么SDP需要在插件中返回,把能否进行媒体传输的决定权移交给上层业务(插件)。
    trickle 消息,用于交换ice的candidate,双方的消息都是trickle
    这里有个问题就是trikle的消息都是两边的库生成candidate信息所调用的回调向对方发送trikle消息。存在一个问题就是服务端的trikle消息可能比服务端的sdp消息要快?万一sdp失败了怎么办 Todo ICE通道的建立和SDP(没有candidate)没有冲突,只需要考虑sdp的异常case关闭session即可
    客户端利用webrtcsdk收集candidate,给janus发送消息,janus利用attach阶段建立的消息循环向ice层设置客户端的candidates
    janus利用nice_agent_gather_candidates,给客户端发送消息,客户端接收到trickle消息直接在peerConnection.addIceCandidate设置
    这里是服务端需要处理多个会话所以需要有特殊消息循环机制(消息如下图)和多线程处理,而客户端只有一个会话,所以客户端逻辑稍微简单
    获取candidate结束后会在message中添加complete标识。

     

     


    NAT
    这里我们只需要知道libnice实现了nat即可,这里引入的libnice库很重要,janus的和客户端的p2p连接接就是这个库做到的,NAT的过程的具体需要知道libnice的大概实现和调用逻辑,一边更熟悉webrtc 建立连接的过程。,当libnice 发射new-selected-pair信号的时候,就是已经确认nat通道已经确认,如果需要webrtc的安全传输则在这时候开始建立dtls通道。
    DTLS
    利用openssl bio建立,是一个简单的小状态机。(这里要是被攻击不就会创建n多线程?)

    SRTP
    协商SRTP的秘钥,即从上一步建立的DTLS中设置相关的秘钥即可。
    创建处理SRTCP的事件源
    协商完成就是说明通道已经协商完毕可以传输了,随后发送event

    webrtcup消息
    Janus发送webrtcup通知ICE通道建立
    rtppacket传输
    ice通道收到rtp消息交给插件转发rtp即可,这里需要注意如何把rtp的包转发给别人。
    这里比较关键的函数如下,只需要几个参数就可以修改rtp的 头

    随后根据插件的plugin_session获取core_session使用该core_session的ice通道的handle转发rtp,echotest的handle是本身,所以使用原来的即可

     

     


  13. 读取配置文件 加载transport,
    以transport 为例。websocket 比较好理解,需要定义janus子协议。

  14. wertc 的断开,客户端主动发送destory的消息,或者janus在websocket 会有监听的心跳,如果在一段时间的心跳(keepalive的msg)结束了,就表示断开连接了

配置至本地demo测试:
学习完大致流程后,利用网上大佬们的Android Demo https://github.com/lesliebeijing/JanusAndroidDemo.git  直接跑一边熟悉流程
大致原理就是利用transport来交流网关业务,同时双端配合webrtc 通过webrtc来实现网关和客户端的视频转发

感触:其实难度在于没人告诉你WebRTC为什么这样设计?这也是我苦恼的地方,我不认识熟悉Janus-gateway的人,所有遇到问题都是强迫自己去看代码寻找答案,这对于初学者来说确实是很痛苦的一件事情。但是分析下来其实代码很精简,每块代码都有它存在的意义。耗时两个星期才读了个大概。。。还有很多~ c真的很难读 擦,后续的就是插件的业务和如何断开连接的分析,

后面发现比较新的流媒体服务器都是使用单进程单线程模型,就是为了解决C10K问题。同时也没有相应的文章去描述如何把janus应用到负载均衡上,对于如何在流媒体服务做负载均衡这一块我也是很有疑问的,这对于做到低延时很有破坏效果(如果动态切换网关ip的话,还得再学习一下