0%

janus前端核心库源码分析

Hello,大家好!今天我们继续来分析janus。相信现在大家应该对janus 都比较熟悉了,它是一套完整的音视频会议系统,包括了WebRTC流媒体服务器和客户端API两大部分。

其中,客户端又包括Android、iOS以及浏览器端。今天我们要分析的内容就是浏览器端中的janus.js文件。之所以要分析它,是因为它是浏览器端最关键的一个文件,了解了它我们基本上就将浏览器端的逻辑全部撑握了。

从大的方面说,janus.js主要完成两方面的事儿。一是封装了浏览器与janus流媒体服务器之间的业务接口,使得在浏览器端开发音视频会议系统变得特别简单;二是对WebRTC的API做了封装,这样用户不用再理会WebRTC底层API该何使用了。

总之一句话,就是大大的减了少JS用户使用janus的难度。

在阅读本文之前你应该已经熟悉了JavaScript语法,且对浏览器下调用WebRTC的API十分精通,否则你应该先去补齐相关知识再来阅读本文。 这里有一些参考资料仅供参考:《js类的探究》,在这篇文章中有介绍ES5与ES6之间的区别。《WebRTC入门与实战课程》,该课程详细讲解了在浏览器下该如何使用WebRTC。

下面我们开始janus.js源码分析。

核心类Janus

对于janus源码的目录结构我已经在之前的文章中向你介绍过了,如果你还没看过,可以到这里看一下。通过目录结构我们可以知道,janus.js就在janus源码的html目录下。

大体浏览一下janus.js你会发现,整个文件有3000多行代码,但只有一个,即Janus类。该类中实现了很多方法,然而核心代码量并不大,经抽丝拨茧,你会发现下面几个方法是比较关键的。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
function Janus(gatewayCallbacks) {

...

//创建一个Session,私有方法
function createSession(callbacks);

//消毁Session,私有方法
function destroySession(callbacks);

//用于处理Janus信令
function eventHandler()

//信令事件处理函数,私有方法
function handleEvent(json, skipTimeout);

//发送消息,私有方法
function sendMessage(handleId, callbacks);

//创建插件处理函数,私有方法
function createHandle(callbacks);

//消毁插件处理方法
function destroyHandle(handleId, callbacks);

//发送candidate,私有方法
function sendTrickleCandidate(handleId, candidate);

//创建PC
function streamsDone(handleId, jsep, media, callbacks, stream);

//准备WebRTC
function prepareWebrtc(handleId, offer, callbacks);

//接收远端的SDP,并设备远端描述符
function prepareWebrtcPeer(handleId, callbacks);

//创建Offer
function createOffer(handleId, media, callbacks);

//创建answer
function createAnswer(handleId, media, callbacks);

//发送SDP
function sendSDP(handleId, callbacks);

...
}

对上面的这些方法,我们可以按类别将其划分成以下几类:

  • Session相关

    • createSession
    • destorySession
  • 信令处理

    • handleEvent
    • eventHandler
    • sendMessage
  • Plugin相关

    • createHandle
    • destoryHandle
  • WebRTC相关

    • prepareWebrtc
    • prepareWebrtcPeer
    • createOffer
    • createAnswer
    • sendSDP
    • streamDone

接下来,我们就对这几个函数做下详细介绍,整体的介绍思路是:首先介绍一下它的主要功能是什么,然后再讨论一下它是怎么实现的。

Session 相关

首先我们来看看 Session 的作用是什么。Session表示的是一个客户端与janus服务器之间建立的一个信令通道。janus客户端与服务器之间就是通过这个信令通道传输信令的。

Session是如何创建的呢?下面我们就来看一下createSession函数的处理逻辑。在createSession中,首先创建了一个JSON对象request,该对象中包括了以下几个信息:

  • janus,代表一个信令,create表示要创建一个session
  • token,唯一标识,用于鉴权。
  • apisecret,API调用码密,用于安全访问。

request对象构建好后,createSession函数会根据server地址的类型(如ws://、http://)判断是使用 websocket 接口还是使用 HTTP RESTFUL接口。判断逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
...
if(!server && Janus.isArray(servers)) {
// We still need to find a working server from the list we were given
server = servers[serversIndex];
if(server.indexOf("ws") === 0) {
websockets = true;
Janus.log("Server #" + (serversIndex+1) + ": trying WebSockets to contact Janus (" + server + ")");
} else {
websockets = false;
Janus.log("Server #" + (serversIndex+1) + ": trying REST API to contact Janus (" + server + ")");
}
}
...

如果createSession判断server使用的是websocket接口,它就会走到websocket的处理逻辑分支。

1
2
3
if(websockets){
...
}

在这个分支中,首先通过Janus.newWebSocket方法与server(janus服务器)建立连接。然后向websocket注册侦听事件,当websocket接收到不同的事件后就跳到对应事件的处理函数中执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
...
ws = Janus.newWebSocket(server, 'janus-protocol');
wsHandlers = {
'error': ...,
'open': ...,
'message': ...,
'close': ...
}

for(var eventName in wsHandlers) {
ws.addEventListener(eventName, wsHandlers[eventName]);
}
...

在上代码中,注册到 websocket 中的 open 和 message 事件特别重要。websocket收到open事件说明与janus服务器之间已经成功建立了连接,此时我们就可以将之前准备好的 request 发送出去了。

1
2
3
4
...
transactions[transaction] = function()...;
ws.send(JSON.stringify(request));
...

当websocket连接创建好后,janus.js首先在transctons中创建一个新的transaction,然后将之前准备好的request发送给服务器。

这里需要注意的是transaction,它表示一个事务或称之为上下文,当一件事儿由多个步骤多阶段组合完成时,我们一般都使用这种设计方案。

服务端收到消息后进行逻辑处理,之后通过上面建立的连接将处理结果返回给janus.js,此时就会触发我们上面注册的message事件。在message事件中,对所有接收到的服务端的消息都由handleEvent函数进行处理。对于该函数我们后面还会做详细介绍,这里就不过多讲解了。

至此,createSession 函数的主要作用我们就分析完了,而destorySessioncreateSession的反函数,用于销毁createSession创建的资源,大家自己去看代码就好了,我这里不再做过多描述了。

接下来我们来看一下janus.js是如何处理从服务端接收到的信令的。

消息处理

上面讲解Session的创建时,我已经向你介绍了janus默认提供了两种传输信令的接口,即websockethttp。janus会根所用户访问地址的协议头来自动判断使用那种协议进行信令的传输。

对于服务端来讲,这两种接口的实现是在janus源码目录下的 transports 目录下,对应的实现文件为janus_websockets.c和janus_http.c文件,通过文件名我们也可以知道他们分别是websocket和http接口的实现了。

当然janus不光支持这两种接口,它还支持好几种接口,但需要你手工配置。如果你不进行任何配置的话,它默认只支持这两种接口。

下面是janus信令处理的简化架构图,我们通过这张图先从整体上了解一下janus是如何处理信令的。

上图将janus分成了两大部分,服务端和客户端。我们分别来介绍一下,首先来看看服务端的处理过程。

服务端接收消息

通过上图我们可以看到,janus服务端的信令处理是由transports完成的,transports中包括很多插件,图中展示的websockethttp就是其中的两个。

这两个transport插件是在janus服务启动时加载起来的。以websocket插件为例,当该插件被加载后,websocket服务随即开启。此时,janus.js(JS客户端)就可以向该websocket服务发送消息了,同时janus.js也可以通过websocket连接接收来自服务端的信息。

当在服务端通过websocket收到消息后,最终会调用janus_websockets.c中的janus_websockets_common_callback方法将收到的消息传给janus core。janus core 收到消息后再根据消息类型做相应的处理。关于这块的逻辑我们先暂时放一放,待以有时间了我再说细介绍。

接下来我们再看客户端janus.js是如何处理的。

客户端接收消息

客户端是如何处理消息的呢?我们还是从Session创建之后讲起。在创建Session一节中我已经介绍了,janus在websocket上侦听了message事件,每当websocket收到服务端发来的消息时,就会触发该事件。

janus.js对该事件的处理方法是也比较简单,不管三七二十一直接将event中带来的数据交收handleEvent处理。handleEvent又是如何做的呢?

1
2
3
4
5
6
7
8
9
10
11
12
function handleEvent(json, skipTimeout) {
...
if(json["janus"] === "keepalive") {
...
} else if(json["janus"] === "ack") {
...
} else if(json["janus"] === "success") {
...
} else if(...

...
}

handleEvent处理逻辑就如上面所示,对消息类型做判断,根据不同的类型做不同的处理。它能处理的消息包括以下几种:

  • keepalive,心跳消息。
  • ack,确认消息。也就是说之前客户端发送了一个信令给服务端,服务端收到之后给客户端回了一个ack,确认服务端已经收到该消息了。
  • success,消息处理成功。该消息与 ack 消息是类似的,当服务器完成了客户端的命令后会返回该消息。
  • trickle,收集候选者用的消息。里边存放着 candidate,janus.js收到该消息后,需要将Candidate解析出来。
  • webrtcup,表示一个peer上线了,此时要找到以应的业务插件(plugin)做业务处理。
  • hangup,用户挂断,找到对应的plugin,进行挂断操作。
  • detached,某个插件要求与Janus Core之间断开连接。
  • media,开始或停信媒体流。
  • slowlink,限流?
  • error,错误消息
  • event,插件发磅的事件消息。
  • timeout,超时。

对于janus.js来讲,上面这些消息有些是不需要再做处理的,有些是需要修改状态的,还有一些是与业务插件有关的,需要交由pluginHandle做进一步处理。 关于pluginHandle后面我们再做介绍。

以上就是janus客户端对从服务端接收到的消息的处理过程。当然在你阅读代码时还会看到eventHandler函数,这个函数是对handleEvent函数简单的封装,用在http接口上。由于websocket接口是长连接,所以直接使用的handleEvent函数,我们清楚这两个函数的联系与区别就OK了。

客户端发送消息

上面我们主要介绍了从服务端来的消息janus是如何处理的,那客户端是如何发送消息的呢?

janus.js中为上层应用封装了一个发送消息的方法,即config.send()。这个函数实际调用的是janus的sendMessage方法。我们来看一下它的大体实现吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function sendMessage(handleId, callbacks) {
...
var pluginHandle = pluginHandles[handleId];
...
var message = callbacks.message;
...
if(pluginHandle.token)
request["token"] = pluginHandle.token;
...
var request = { "janus": "message", "body": message, "transaction": transaction };
...
if(websockets){
ws.send(JSON.stringify(request));
}
...
}

通过上面的代码我们可以发现,在sendMessage中主要是构造request对象,然后将构造好的request消息通过websocket发送出去。

消息发送给janus服务器,服务器处理好后又会给客户端返回消息。消息返回到客户端后,后会触发websocket的message事件,这样就又回到了我们上面介绍的handleEvent处理函数。

Plugin相关

janus.js中,Plugin相关函数的主要作用是,在客户端创建一个pluginHandle对象,并让该对象与janus服务端的某个插件关联。

所谓的关联就是在pluginHandle对象中保存着可以访问janus服务端插件的信息。该对象中存放着很多的信息,如session、plugin、webrtc等信息。下面我抽取了一些比较关键的信息,我们来详细分析一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
session : that,
plugin : plugin,
...
token : handleToken,
detached : false,
webrtcStuff : {
...
mySdp : null,
mediaConstraints : null,
pc : null,
trickle : true,
iceDone : false,
...
}
...

上面的字段就是pluginHandle对象的一些重要信息,在这些信息中包括了两在部分,基础信息部分和WebRTC信息部分。

首先我们看一下基础信息。session中保存的是janus核心类对象;plugin指明我们要与服务端那个个插件建立联接,例如vidoeroom插件;token用于安全访问;detached表明是否已经与服务端对应的plugin建立了联系。

接下来webrtcStuff域是与webrtc相关的参数。mySdp中保存的是本地SDP信息;mediaConstrains存放用于采集音视频数据的限制参数;pc表示PeerConnection;trickle指明在使用WebRTC时是否使用trickle机制;iceDone表明是否ICE建立成功了。

该对象中的内容是由createHandle函数创建和填充的,下面我们就来看一下createHandle函数做了哪些事儿?

在该函数中,它首先构造request对象,该对象包括以下几个信息:

  • janus,表示信令类型, 在createHandle函数中,该域填的内容为attach,表示与某个plugin 进行绑定。
  • plugin,指明要绑定的具体pluginjanus.plugin.videoroom
  • opaque_id,一个唯一的ID。
  • transaction,表示一个事务ID。
  • token,用于与服务器连接的合法标识
  • apisecret,API密码。
  • sessionID,session的唯一标识。

request对象创建好后,通过websocket发送给服务端,这样就在客户端与服务端的plugin插件建立了联接。

除了创建request对象外,该函数还创建了一个transaction对象,并将它存放在 transactions 数组中(transactions[transaction])。

实际上 transaction 是一个函数,该函数中会创建一个pluginHandle对象,pluginHandle创建好后,也会被保存起来放到pluginHandles里以备后面使用。

除了createHandle函数之外,在janus.js中还有destoryHandle函数,它是createHandle的反函数,用于做释放操作。

以上就是janus.js中处理Plugin相关的函数。

Webrtc相关

janus.js中WebRTC相关的方法是最多的,也是最为重要的。其中尤以prepareWebRTC最为重要。下面我们就来详细介绍一下这个函数。

这个函数的作用是什么呢?说来起它还是蛮复杂的,我们来一项一项来介绍。一、它要过浏览器采集音视频数据,以便可以将数据上传给远端;二要与按照WebRTC的规规范进行媒体协商;三协商成功后要与远端建立连接;最后把采集的数据压缩编码后传到远端;

这个函数代码量非常大,我抽取了函数中重要的逻辑,这样更有利于我们撑握整个函数的流程脉络。代码整理如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
...
if(isAudioSendEnabled(media) || isVideoSendEnabled(media)) {
...
//media为空,或者media.video不为屏幕,说明现在想采集视频数据
if(!media || media.video !== 'screen') {
//遍历每个设备
navigator.mediaDevices.enumerateDevices().then( function(devices) {

//如果是音频输入设备
var audioExist = devices.some(function(device) {
return device.kind === 'audioinput';
}),
//如果是视频输入设置
videoExist = isScreenSendEnabled(media) || devices.some(function(device) {
return device.kind === 'videoinput';
});
...
//设置采集数据的限制条件
var gumConstraints = {
audio: (audioExist && !media.keepAudio) ? audioSupport : false,
video: (videoExist && !media.keepVideo) ? videoSupport : false
};

...
//采集数据
navigator.mediaDevices.getUserMedia(gumConstraints)
.then(function(stream) {
...
//
streamsDone(handleId, jsep, media, callbacks, stream);
}).catch(function(error) {...});

}).catch(...);
}
...
}

通过代码的梳理我们可以看到prepareWebrtc函数首先遍历所有设备,找出可用的设备,之外调用`getUserMedia函数去采集音视频数据。之后像媒体协商、Candidate的收集都在 streamDone 函数中完成。

接下来我们继续分析streamsDone 函数。下面是streamsDone函数的主要脉络代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function streamsDone(handleId, jsep, media, callbacks, stream) {
...
var config = pluginHandle.webrtcStuff;
...
//创建PeerConnection
f(!config.pc) {
var pc_config = {"iceServers": iceServers, "iceTransportPolicy": iceTransportPolicy, "bundlePolicy": bundlePolicy};
...
var pc_constraints = {
"optional": [{"DtlsSrtpKeyAgreement": true}]
};
...
config.pc = new RTCPeerConnection(pc_config, pc_constraints);
config.pc.oniceconnectionstatechange = ...;
config.pc.onicecandidate = ...;
config.pc.ontrack = ...;
}

//将本地track添加到流中
if(addTracks && stream) {
...
config.pc.addTrack(track, stream);
...
}

//创建offer进行媒体协商
if(!jsep) {
createOffer(handleId, media, callbacks);
}else{...}
}

在该函数中首先会根据限制条件创建一个PeerConnection。PeerConnection简称PC,它是浏览器下使用WebRTC的核心,想了解这块知识的同学可以看一下我的课程《WebRTC入门与实战》,这里我就不过多讲解这部分内容了。

PC创建好后,需要将之前从prepareWebrtc中获取的本地音视频轨添加到PC中,为媒体协商做好准备。最后调用createOffer函数生成媒体协商中的OfferSDP与远端交换从成完成媒体协商。

现在你应该了解streamsDone函数的作用了吧?同时也应该清楚prepareWebrtc函数是干什么的了。

当我们将prepareWebrtc函数的功能搞清楚之后,对于其它的WebRTC相关API就比较容易理解了,由于篇幅的原因我就不在这里一一做介绍了。

小结

上面我对janus.js文件做了全面的剖析,通过本文你应该知道janus.js的API可以分成四大类,分别是Session相关,信令相关,Plugin相关以及WebRTC相关的API。同时你也应该清楚,janus中的Session表示的是客户端与服务端之间的网络连接;客户端与服务器之间的信令是如何交互的,以及包括了哪些信令;pluginHandle的作用是用来保存访问远端插件的信息用的,同时它也保存了操作WebRTC相关的信息;而WebRTC相关的API是janus.js中最关键,最为复杂的。尤其是prepareWebrtc函数是最核心的API。

参考

欢迎关注我的其它发布渠道