0%

janus.js的使用

之前我在《janus前端核心库源码分析》一文中已经向你详细分析了janus.js文件的实现,但对于大多同学来说,知道如何使用它才是最重要的。那具体我们该如何使用它呢?janus中的videoroomtest.js是一个不错的例子,今天就来分析一下videoroomtest.js,看看它是是如何使用janus.js的。

通过分析videoroomtest.js文件,你会发现使用janus.js的步骤很简单,只要下面三步即可:

  • 初始化janus.js
  • 创建Janus对象
  • attach 音视频流

下面我就按上面的顺序向你逐一介绍videoroomtest.js是如何使用janus.js的。

janus的初始化

我们使用janus.js之前,第一步就是调用Janus方法init来初始化janus.js。方法的原型如下:

1
2
3
Janus.init = function(options){
...
}

init方法需要一个JSON格式的输入参数,它包括两个: debug和一个回调函数callback。这样当init执行完成后,就可以通过callback将结果返回到应用层了。videoroomtest.js调用Janus.init的代码如下:

1
2
3
4
5
6
7
8
...
Janus.init(
{
debug: "all",
callback: function(){...}
}
);
...

上面的代码非常简单,关键的一点是我们要知道传入的参数是JSON 格式的,它有两个域debugcallback即可。知道如何调用了,接下来我们再来看看init函数的实现。

janus.jsinit方法实现了什么功能呢?我们把Janus.init方法中的主干逻辑抽取出来,代码如下:

1
2
3
4
5
6
7
8
9
10
11
Janus.init = function(options) {
...
if(Janus.initDone) {
options.callback();
} else {
...
Janus.initDone = true;
options.callback();
}
...
};

上面代码中的initDone表示的是之前是否初始化过janus.js?而optionsinit方法的输入参数,通过它可以回调应用层。通过上面的代码我们可以知道init方法的功能其实蛮简单的,就是将initDone置位,并回调应用层。

接下来我们看一下init回调应用层后,在应用层又做了什么事儿。 输入参数optionscallback方法在videoroomtest.js中是一个匿名函数,其主干逻辑代码如下:

1
2
3
4
5
...
callback: function() {
$('#start').one('click', function() { ... } )
}
...

正如你上面看到的,callback函数只有一行关键代码,即给start 键钮绑定了一个click方法。当用户点击start时执行该方法。那么应用层在start方法中又做了哪些事儿呢?

创建Janus对象

下面的代码就是start方法的主逻辑,从中我们可以知道start方法中完成了使用janus.js的第二个关键步骤,即创建Janus对象。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
if(!Janus.isWebrtcSupported()) {
...
return;
}

janus = new Janus(
{
server: server,
success: function() { ... },
error: function(error) { ... },
destroyed: function() { ... }
}
);

在该方法中首先判断浏览器是否支持WebRTC,如果不支持则直接退出,否则创建Janus对象。在创建Janus对象时,需要给它传入了一个JSON格式的参数,该JSON对象包括以下几个

  • server,Janus服务器地址
  • success,连接成功后执行的回调函数
  • error,连接失败后执行的回调函数
  • destroyed,连接销毁时的回调函数

在上述几个中,最关键的是success回调函数。它的含义是当Janus对象创建成功后,回调该函数到应用层。但对于这个回调函的详细介绍我们先暂时放一放,现在我们先来看看创建Janus对象时都做了哪些事儿,然后再来分析success回调函数。

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
function Janus(gatewayCallbacks) {
...
createSession(gatewayCallbacks);
...
this.attach = function(callbacks) { createHandle(callbacks); };
function handleEvent(json, skipTimeout) { ... };
function keepAlive() {{ ... };

//创建Session
function createSession(callbacks) {
...
//与服务器相连
Janus.httpAPICall(server,
{
...
//处理服务器发过来的消息
success: handleEvent(...){ ... };
...
});
...
};

//绑定媒体流
function createHandle(callbacks) {...}
...

通过上面的代码我们可以知道,在创建Janus对象时它会调用createSession函数。而在createSession函数内部会通过Janus方法httpAPICall与服务端建立HTTP连接。连接建立成功后,会回调success函数,也就是类中的handleEvent方法。

handleEvent的功能我在《janus前端核心库源码分析》一文中已经介绍过,它用于处理服务端发来的消息,并根据不同的消息类型做不同的逻辑处理。

现在我们可以总结一下创建Janus对象所做的事儿啦,其实就两件事儿:一与janus服务器建立连接(HTTP/WebSocket/…);二处理janus服务器发来的各种消息。

接下来我们再来讨论一下Janus对象创建成功后,回调success干了些什么吧!

attach 绑定媒体流

正如上面所说,Janus创建成功后会回调输入参数中的success函数。这个函数特别重要,下面我们来看看在该函数中实现了什么逻辑吧。

实际上,success 回调函数做的事儿也很简单,只是调用了 janus.attach 方法,而attach正是我们使用janus.js的第三步。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
success: function() {
janus.attach(
{
plugin: "janus.plugin.videoroom",
opaqueId: opaqueId,
success: function(pluginHandle) { ... },
error: function(error) { ... },
consentDialog: function(on) { ... },
iceState: function(state) { ... },
mediaState: function(medium, on) { ... },
webrtcState: function(on) { ... },
onmessage: function(msg, jsep) { ... },
onlocalstream: function(stream) { ... },
onremotestream: function(stream) { ... },
oncleanup: function() { ... }
}
);
}

同样,该方法的作用在《janus前端核心库源码分析》一文中也已经做过介绍,该方法可以让浏览器与服务端的videoroom插件绑定,以便获取媒体流。

这里绑定的真实含义是建立WEBRTC连接。

在调用attach方法时,也要传一个JSON格式的对象。在该对像包含了很多属性,这些属性的含义如下:

  • plugin,要绑定的janus插件,这里要绑定插件为janus.plugin.videoroom
  • opaqueId,一个随机值,插件的唯一ID。
  • success,attach方法执行成功后的回调函数。
  • error,attach方法执行失败后的回调函数。
  • consentDialog,
  • iceState,可以通过该函数更新ICE状态。在videoroomtest.js中没有做任何处理。
  • mediaState,可以通过该函数更新媒体状态。该方法也没有做任你可事儿。
  • webrtcState,更改WebRTC状态的回调函数。
  • onmessage,收到事件消自己的回调函数。
  • onlocalstream,收到本地流时的回调函数。
  • onremotestream,收到远端流时的回调函数。
  • oncleanup,销毁时的回调函数。

在上面属性中,比较关键的是successwebrtceStateonmessageonlocalstreamonremotestream 。这几个属性都是回调函数,在不同的情况下janus.js会调不同的回调函数。下面我们就对这几个回调函数做一下详细分析。

success回调函数

当调用attach方法成功与janus服务器插件绑定之后,janus.js会回调success函数。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
this.attach = function(callbacks) { createHandle(callbacks); };
...

function createHandle(callbacks) {
...
Janus.httpAPICall( server + "/" + sessionId,
{
verb: 'POST',
withCredentials: withCredentials,
body: request,
success: function() {
var pluginHandle = { ... };
callbacks.success(pluginHandle);
},
error: function() { ... }
}
);
...
}

...

上面代码的执行过程如下,janus.jsjanus服务器发送attach请求,服务器收到attach请求后进行处理,然后根据处理结果返回successerror。如果浏览器收到的是服务器返回的success响应,那么就会回调success函数。

接下来我们看一下success回调到应用层videoroomtest.js后做了哪些事儿吧! 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
...
janus.attach(
{
...
success: function(pluginHandle) {
...
sfutest = pluginHandle;
...
}
...
}
);
...

从上面的代码中我们可以看到,应用层success的逻辑非常简单,只是将janus.js层创建的pluginHanle保存起来以备后用,剩下的其它的一些代码是与界面相关的,我们这里就不介绍了。

onmessage

onmessagee实现的是对janus服务端返回事件的处理逻辑。我们依然还是先看看在janus.js中是如何调用该函数的。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function handleEvent(json, skipTimeout) {
...
else if(json["janus"] === "event") {
...
var sender = json["sender"];
var pluginHandle = pluginHandles[sender];
...
var plugindata = json["plugindata"];
...
var jsep = json["jsep"];
var callback = pluginHandle.onmessage;
if(callback) {
callback(data, jsep);
} else {
...
}
...
}
...
}

上面就是janus.js中的handleEvent函数的主逻辑,当janus.js收到janus服务端发来的event事件后,从中取出必要的信息,然后调用pluginHandle.onmessage方法回调应用层。

应用层的onmessage函数非常重要,可以说它是应用层最核心的代码。它根据从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
...
onmessage: function(msg, jsep) {
var event = msg["videoroom"];
if(event) {
if(event === "joined") {
...
publishOwnFeed(true);
...
} else if(event === "destroyed") {
...
} else if(event === "event") {
if(msg["publishers"]) {
...
} else if(msg["leaving"]) {
...
} else if(msg["unpublished"]) {
...
}else if(msg["error"]) {
...
}
}
}
if(jsep) {
...
sfutest.handleRemoteJsep({ jsep: jsep });
}
}
...

通过上面的代码我们可以看到,消息包括以下3种:

  • joined,表示作为发布者加入成功
  • destroyed, 表示用户成功销毁房间
  • event,子事件,event又有四个子事件:
    • publishers,发布者列表
    • leaving,有用户离开了
    • unpublished,用户取消发布流
    • error,出错

onmessage函数中会对3种事件消息做处理,下面我们分别对这几个事件消息做一下分析。

joined 事件

joined消息的含义我在《janus的videoroom插件》一文中已经向你做过介绍了,即当你作为发布者加入到房间里时,如果成功则会收到joined事件消息。

此时你可以从该消息中取出publishers列表,该列表中的每一个publisher都是会中的一个发布者(或称为一路流)。由于发布者也是房间的订阅者,所以你应该订阅每个发布者的媒体流,因此你要与列表中的每个publisher进行一次媒体协商,并进行attach。这样就可以接收发布者的媒体流啦。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
if(event === "joined") {
...
publishOwnFeed(true); //创建 Offer
if(msg["publishers"]) {
var list = msg["publishers"];
for(var f in list) {
var id = list[f]["id"];
var display = list[f]["display"];
var audio = list[f]["audio_codec"];
var video = list[f]["video_codec"];
newRemoteFeed(id, display, audio, video); //attach插件,并向插件发送 join 消息
}
}
}
...

从代码中我们可以看到,因为它是一个发布者所以它首先执行publishOwnFeed(true)创建Offer与janus服务端作媒体协商。之后遍历publishers列表,取出每个publisher执行newRemoteFeed方法,在该方法中又会调用attach方法获取其它发布者的媒体流。

publishers 事件

当有发布者加入到房间后,房间内的所有订阅者都会收到publishers事件消息。浏览器收到该消息后,应该对消息中的每个未绑定的publisher都进行attach操作,这样就可以接收发布者的媒体流了。 其主逻辑代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
...
if(msg["publishers"]) {
var list = msg["publishers"];
for(var f in list) {
var id = list[f]["id"];
var display = list[f]["display"];
var audio = list[f]["audio_codec"];
var video = list[f]["video_codec"];
newRemoteFeed(id, display, audio, video);//attach插件
}
}
...

这段代码与joined消息的处理逻辑几乎是一样的,只不过订阅者此时不需要与janus服务器进行媒体协商,所以它没有调用publishOwnFeed方法,其它的逻辑都是一样的了。

leaving/unpublished 事件

当一个发布者取消发布时,janus会向房间内的其它用户发送unpublished消息。另外,当有用户离开房间时,janus会给房间内的其它人发送leaving消息。另外,如果离开的用户是一个发布者的话,它同时也会发unpublished消息,所以这里我们将两个消息放在一起讨论。

这两个消息处理的核心逻辑如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...

var remoteFeed = null;
for(var i=1; i<6; i++) {
if(feeds[i] && feeds[i].rfid == unpublished) {
remoteFeed = feeds[i];
break;
}
}

if(remoteFeed != null) {
...
feeds[remoteFeed.rfindex] = null;
remoteFeed.detach();
}
...

上面这段代码有点不太好理解,我这里解释一下你就清楚了。首先我们来看看循环中的固定数字6,表示什么含义呢?实际上我在《janus的videoroom插件》一文中有解释过,在janus中一个房间内最多可以支持6路流,这个6表过的正是这个意思。所以当收到leavingunpublished消息时,客户端会遍历全局变量feeds(保存所有订阅的流),如果在feeds中找到了该流,则需要与该流进行detach操作。

现在我们再读这段代码时是不是就理解它要表达的意思了呢?

onlocalstream 消息

当收到onlocalstream消息时,说明本地流已经准备就绪了,此时我们需要让本地流的视频在浏览器里显示出来。代码如下:

1
2
3
4
5
6
7
8
9
10
...
onlocalstream: function(stream) {
mystream = stream;
...
Janus.attachMediaStream($('#myvideo').get(0), stream);
$("#myvideo").get(0).muted = "muted";
...
var videoTracks = stream.getVideoTracks();}
//显示video标签
...

这段代码的含议非常简单了,就是获取本地址,然后从HTML中拿到一个video标签,最后将video与本地流绑定到一起,这样就可以看到捕获的本地视频了。

onremotestream 消息

onremotestream消息与onlocalstream是类似的,只不过它表示的是远端的视频流。当收到远端的视频流时,我们也同样在本地创建一个video,然后将远端视频流与video绑定即可。

小结

本文我向你详细介绍了janus中的videoroom是如何使用janus.js文件的,其总的步骤是先调用Janus.init方法进行初始化;然后创建Janus对象,即与janus服务器建立连接,并接收来自服务器端的消息; 最后调用attach与业务服务器绑定,再根据消息进行推流或拉流。

总的来看,使用janus.js库大大减轻了我们开发webrtc应用程序的工作量,提高了工作效率。

参考

《janus的videoroom插件》
《janus前端核心库源码分析》

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