目标:Janus is an open source, general purpose, WebRTC server designed and developed by Meetecho.
技术栈:C,jcfg(libconfig)
代码结构:
描述:janus由Core实现了WebRTC所支持的所有协议(SDP, ICE, DTLS-SRTP, RTP/RTCP),并且管理插件的初始化,管理,关闭等。我们可以看到具体业务功能有Plugin提供,Core本身不能进行任何具体业务实现。为了详细了解这个系统,我们可以带着问题来了解:
- 系统的编译依赖,及编译先后顺序是怎么样的?
- 系统的Core的启动,及Plugin启动都做了那些事情?
- 简单的单路通话(VideoCall)实现原理?
- 多路通话(VideoRoom) 实现原理?
1. 系统的编译依赖,及编译先后顺序是怎么样的?
我们来看右Doxygen生成的头文件依赖
我们看到依赖很简单:
- plugins/plugin.h
- transports/transport.h
- loggers/logger.h
- events/eventhandler.h
所以在编译时,Core是依赖到Plugins,Transports, Loggers,Events模块的。
分析 Makefile.am 知道:
janus_SOURCES 包含了 plugins/plugin.c, transports/transport.c这两个实现。所以Core只是把plugin.c, transport.c放到了这两个子目录中,实际他俩是Core的源文件。
然后看Plugin的编辑脚本,发现一个很神奇的事情,Plugin居然不依赖Core,不管是头文件还是链接。这是不是跟接口编程一样,框架提供接口,找一个现成的库适配了这个接口就能在这个库里面使用,现在厉害的在于,适配的时候都不依赖框架。
所以可以很明确的知道,他们的编译可以分开,即先编译Core,然后并行编译Plugins,Transports,EventHandlers。
2. 系统的Core的启动,及Plugin启动都做了那些事情?
我们来分析main函数:
- 首先在解析命令行参数,和配置文件。
- 然后调用
janus_log_init
初始化日志模块,这里面启用了一个日志打印现成,并维护了一个log_buffer链表,及log_buffer pool。 - 加载外部logger的so,并且初始化,最后调用
janus_log_set_loggers
关联到janus_log中去管理。 - 调用
janus_auth_init
初始化授权系统。 - 调用
janus_recorder_init
初始化路由系统。 - 调用
janus_ice_init
初始化ICE协议栈。 - 调用
SSL_library_init
,SSL_load_error_strings
,OpenSSL_add_all_algorithms
初始化OpenSSL环境。janus_dtls_srtp_init
初始化DTLS功能。 - 调用
janus_sctp_init
初始化SCTP功能,多播通道。 - 初始化Core的全局变量。
sessions
系统会话hash表,及sessions_watchdog_context
,并启动session watchdog
线程。requests
系统请求异步队列,并启动请求分配线程。tasks
系统任务线程池,用来分配请求。 - 加载EventHandlers的so,并初始化。 调用
janus_events_init
关联到管理中。 - 加载外部Plugins的so,并初始化。
- 加载外部Transports的so,并初始化。
- 给系统发送第一个Event,janus started。
- 启动主线程循环
g_main_loop_run
3. 简单的单路通话(VideoCall)实现原理?
首先我们抛开系统来说,一个通话是由Alice(终端A)发起,然后服务器路由到Bob(终端B),这其中附带SDP(Session Description Protocol)。我们先说最简单的方式
Server
/ \
Alice <--rtp--> Bob
其中SDP携带了Alice RTP/RTCP 的端口及IP,然后Bob回给服务器信令也携带了自己的SDP,同样服务器把这个信令路由到Alice。Alice收到后,对着Bob给的端口RTP/RTCP通信。这样就建立起来了Session,相互的数据互通了。
现在我们开始研究Janus怎么把这个过程实现的。
我们先查看Websocket Transport + VideoCall Plugin。
第一步,Alice和Bob怎么和Janus建立信令通道呢?
1. Websocket Transport 用 libwebsockets 建立 ws/wss 服务器。
2. Alice和Bob建立webscoket连接,libwebsockets会分配一个ws_client
,并回调WST(Websocket Transport)。WST在ws_client
上建立messages
队列和用janus_transport_session_create
建立Core Session。最后给Core发送一个connected
消息,这个消息啥也没干。
3. Alice建立连接后,会立马发送一个create
请求,WST收到请求后,把请求发送到Core。Core会立即创建一个Core Session
,同时发送一个 Session
类型的created
消息。然后回复Alice建立连接成功。
4. 然后,Alice发送attach
请求。Core会立即创建一个ICE Handle
,准备处理RTP会话,同时发送一个Handle
类型的attach
消息。然后回复成功。这一步很关键,create
只是建立Core Session
,并没有做任何路由关联。而attach
则告诉Core,我要用的Plugin
。这样就在ICE Handle
中建立了Plugin
指针,并调用Plugin
创建一个Plugin Session
, 方便后续的message
直接给Plugin
。
5. 至此连接就建立了。当然Janus还有一步,注册用户名,方便呼叫。Alice发送一个message
请求,并且带上body:{request:'register", username:"Alice"}
。Core根据attach
的信息,把消息给 VideoCall Plugin
,VideoCall
则建立一个用户名对应Session
的hash表。
第二步,Alice和Bob怎么建立呼叫会话呢?(注意这里的会话与代码中的会话不是一个意思)
1. Alice发送一个{janus:"message",body:{request:"call",username:"Bob"},jseq:{sdp:"xxx",type:"offer"}}
的消息。这个是message
消息,Core会直接给Plugin
处理。VideoCall Plugin
先回复Boback
,再根据username
找到Bob的Session
,然后验证sdp,给两个Session
做上通话标记与关联。然后给Bob发送一个叫incomingcall
的event
,带上Alice发送过来的SDP。
2. Bob回一个{janus:"message",body:{request:"accept"},jseq:{sdp:"xxx",type:"answer"}}
的消息。VideoCall Plugin
先回复Boback
,再解析sdp,提取相关信息。然后给Alice发送一个叫accept
的event
,带上Bob发送过来的SDP。
3. 这样就建立一个通话。
第三步,Alice或者Bob如何挂机呢?
1. Bob发送一个{janus:"message",body:{request:"hangup"}}
的消息。VideoCall Plugin
收到消息后,先回复Bob ack
,然后清除现场,然后给Alice发送发送一个hangup
的event
。
2. 这样电话就挂断了。
上面的都是信令层面如何交互,涉及RTP/RTCP的只有SDP,那么SDP如何在ICE框架下,把RTP会话建立起来的呢?
4. 多路通话(VideoRoom) 实现原理?
VideoRoom Plugin
实现了SFU(Selective Forwarding Unit)功能。它实现了会议功能,基于发布/订阅模式,任何一个参会者都可以推送自己的流,并订阅别人的流。
我们先看看信令交互:
第一步,Alice和Bob怎么和Janus建立信令通道呢?
1. 同单路通话一样,建立起来用户通道。只是最后一步注册用户名不一样了。它把这一步直接整合进后面的join
请求中去了,因为会议不需要像单路通话一样去找对方,而是给一个会议id,用其它方式告知对方后,让他加入进来。
第二步,如何创建会议,并且加入会议呢?
1. 调式发现,VideoRoom
的示例并没有实现创建会议的功能,而是直接加入了一个叫1234
的默认会议。
2. Alice发送{janus:"message",body:{request:"join",room:1234,ptype:"publisher",display:"Alice"}}
的消息,VideoRoom Plugin
收到消息后,会建立一个Publisher
,已用户id
作为key,并且与Core Session
关联起来。这样就Alice就加入了会议
3. 设置自己发布的推流,发送{janus:"message",jseq:{sdp:"xxx",type:"offer"},body:{request:"configure",audio:true,video:true}}
的消息,VideoRoom Plugin
根据参数设置一些参数,然后通知其它参会者。
4. 退出会议。Alice发送一个{janus:"destory"}
的请求。Core拿到消息后,首先调用WST结束连接。然后清理Core Session
,然后清理ICE Handle
。这里有个问题,Plugin Session
并没有清理,在哪里,什么时机做的清理呢?这里用的是 ICE 框架的事件做的处理。
至此,我们了解了视频通话,主要信令的交互过程,如何创建连接,如何建立会话,如何销毁等等。