0%

janus的videoroom插件

videoroom

在Janus的众多插件中,大家最感兴趣的恐怕就是VideoRoom插件了。因为它实现的是一个音视频会议的场景,这正是大多数同学所需要的。而且在Janus众多的插件中VideoRoom应该也是最复杂的一个,如果你们撑握了它,再去看其它插件的实现就容易多了。

VideoRoom中,包括了很多API,这些API是我们打开VideoRoom的一把钥匙,所以本文的重点就是讲解这些API。我相信当你把这些API都撑握之后,再去看VideoRoom插件的代码时就会更加游刃有余了。

VideoRoom插件

VideoRoom是Janus的一个插件,实现了一个SFU(Selective Forwarding Unit)型的音视频会议。如果你从数据转发的角度看,也可以把它认为是一个音视频路由器

VideoRoom实现的音视频会议是基于发布/订阅模式。每个参与方都可以发布自己的实时音视频流,因此它可以实现几种不同的场景,比如泛娱乐化直播或多人的实时互动产品(如音视频会议、在线教育小班课等)。

考虑到此插件允许一个参与方可以打开多个WebRTC PeerConnection(如每个参与方可以有1个用于推流的PeerConnection和N个拉流的PeerConnection),所以每个参与方需要为订阅不同的流attachVideoRoom插件几次(每attach一次就会生成一个Handle,每个Handle就是一个上下文)。

因此,对于每个参与方至少要有一个Handle用于管理与插件的关系(如加入一个房间,离开一个房间,静音/取消静音,发布,接收事件)。

每当参与方需要订阅另一个参与方发布的音视频流时,它需要创建一个新的Handle。新创建的Handle在逻辑上属于**”从”Handle,它不能像“主”Handle一样可以做取消房间静音这样的操作。因此,从**Handle唯一目的是提供一个上下文,在该上下文中创建一个recvonly类型的PeerConnection来订阅发布者的音视频流。

通过上面的描述我们可以知道,主Handle用于管理,而从Handle用于订阅音视频流。

注意,现在WebRTC已经实现了SSRC复用(Unified Plan),这意味着你可以使用相同的Janus HandlePeerConnection同时接收多路音视频流。

VideoRoom插件功能非常强大,也很灵活,它有很多的配置项,你可以通过conf/janus.plugin.videoroom.jcfg来修改它们。当然Janus也支持动态API修改配置,如通过API创建房间等。

要增加更多房间或修改现有房间信息,你可以向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
room- <唯一的房间ID>:{
description = 房间的描述信息
is_private = true | false(是否是私有房间? 如果创建的是私有房间,则无法通过list指令进行查看)
secret = <可选项,操作房间所需的密码,如果设置了,则像做销毁房间这样的操作时你要带上它才行>
PIN = <可选项,加入会议房间的密码>
require_pvtid = true | false(是否订阅音视频流时,需要提供一个与发布者相关的有效private_id, 默认为false)
publishers = <房间内发布者的最大数>(例如,一个视频会议可以有6个发布者,而广播只有一个,默认= 3)
bitrate = <房间里发布者发送数据的最大比特率>(例如128000)
fir_freq = <向发布者发送FIR指令的频率>(0 =禁用)
audiocodec = opus | g722 | pcmu | pcma | isac32 | isac16(发布者可以使用的音频编解码器列表,默认为opus。编码器按优先顺序以逗号分隔)
videocodec = vp8 | vp9 | h264 | av1 | h265(发布者可以使用的视频编解码器列表,默认为vp8。可以按优先级顺序用逗号分隔,例如,vp9,vp8,h264)
vp9_profile = VP9首选的profile("2" 表示 "profile-id = 2" )
h264_profile = H.264首选的profile("42e01f" 表示 "profile-level-id = 42e01f" )
opus_fec = true | false(是否使用带内FEC;仅适用于Opus,默认为false)
video_svc = true | false(是否启用SVC支持;仅适用于VP9,默认为false)
audiolevel_ext = true | false(对于发布者是否使用RTP扩展ssrc-audio-level?默认为 true)
audiolevel_event = true | false(是否将audiolevel事件发送给其他用户)
audio_active_packets = 100(音频保活包个数,默认值= 100,2秒)
audio_level_average = 25(音频音量级别的平均值,127 =静音,0 ='太大声',默认= 25)
videoorient_ext = true | false(发布者是否使用RTP扩展video-orientation? 默认= true)
playoutdelay_ext = true | false(发布者是否使用RTP扩展playout-delay? 默认= true)
transport_wide_cc_ext = true | false(发布者是否使用RTP扩展 transport-wide-cc? 默认= true)
record = true | false(该房间是否启录制?默认= false)
rec_dir = <启用录制后,录制文件存放的目录>
lock_record = true | false(是否锁定录制状态? 默认= false)
notify_joining = true | false(可选,当有新的参与方加入房音后,是否通知房间里的所有参与者?
Videoroom插件默认仅通知发布者,启用此功能可能会导致额外的通知传输。
该功能与require_pvtid一起启用时,对管理员管理仅收听的参与者特别有用。默认= false)
require_e2ee = true | false(是否启用端到端加密? 默认= false)
}

Video Room 可以使用的API

VideoRoom 插件支持很多API。这些API中,一些是同步请求,一些则是异步请求。但无论是同步还是异步请求,当遇到无效的JSON格式或无效的请求时,都使用同步进行错误响应。

接下来,我们首先看看都有那些同步请求API。createdestroyeditexistslistallowedkicklistparticipants是同步请求API。create允许您动态创建一个新的音视频房间;edit允许您动态编辑房间的属性(例如 修改PIN码);destroy首先释放视频资源,然后踢除房间里的所有用户,最后销毁音视频房间;exists检查指定的音视频房间是否存在;list列出所有有效的音视频房间; listparticipants列出指定房间中所有激活的参与者及其详细信息。

异步请求API有:joinjoinandconfigureconfigurepublishunpublishstartpauseswitchleavejoin允许你加入指定的音视频房间;configure可用于修改某些属性(例如,比特率范围);joinandconfigure的含义是将前两个请求合并为一个请求(该请求仅适用于发布者);publish发布媒体流给所有订阅者; unpublish正好与publish相反;start允许你开始接收订阅的媒体流;pause暂停发送媒体流;switch更改指定PeerConnection的媒体源(例如,你正在看A,现在改为看B),但无需为此创建新的Handle;leave离开视频房间。

下面咱们对上面提到的API做一下详细分析,首先看一下createAPI,它用于创建新的音视频房间,其格式如下:

1
2
3
4
5
6
7
8
9
10
11
{
"request":"create",
"room":<可选,房间ID。如果不填,则由插件随机生成>,
"permanent":<true | false,是否创建永久房间,默认= false>,
"description":"<可选,房间的名称>",
"secret":"<可选,编辑/销毁房间时用的密码>",
"pin":"<可选,加入房间的密码>",
"is_private":<true | false,是否是私有房间?如果是私有房间则不会出现在房间列表中>,
"allowed":[可选,用户加入房间的token数组],
...
}

上面的说明已经非常清楚了,这里我就不做简赘述了。

如果create成功,则会返回created响应,格式如下:

1
2
3
4
5
{
"videoroom":“created",
"room":<房间ID>,
"permanent":<是否是创建的永久房间?是则为true,否则为false>
}

注意,如果你请求创建一个永久房间,但permanet返回的是false,很可能是因为权限的问题导致的。

如果create请求失败,则返回错误信息,格式如下:

1
2
3
4
5
{
"videoroom":"event",
"error_code":<错误码,每个错误码的含义需要看插件实现代码中的宏定义>,
"error":"<错误描述字符串>"
}

这里需要注意的是,所有请求的错误响应格式都与上面一样。

默认情况下,所有用户都可以创建房间,但你可以通过在VideoRoom插件的配置文件中增加admin_key项来限制此功能。此时,只有带了正确的admin_key值的create请求才能成功创建房间。你也可以选择将此功能扩展到RTP转发,只转发受信任的客户端的RTP包。

房间创建好后,您可以用editAPI编辑其中的部分(但不是全部)属性。edit允许你修改房间描述,密码,PIN码以及是否为私有。但你将无法修改他的静态属性,例如房间ID,采样率,与扩展相关的内容等。如果你有兴趣更改ACL,还需要查看allowed是否允许。

一个edit请求格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"request":"edit",
"room":<房间ID>,
"secret":"<房间密码>",
"new_description":"<房间的新名称,可选>",
"new_secret":"<房间的新密码,可选>",
"new_pin":"<新PIN码,可选>",
"new_is_private":<true | false,房间是否为私有房间?>,
"new_require_pvtid":<true | false,房间是否要求订阅者提供private_id>,
"new_bitrate":<比特率>,
"new_fir_freq":<发送PLI请求关键帧的时间间隔>,
"new_publishers":<房间里发布者的最大数>,
"new_lock_record":<true | false,如否可以改变录制状态>,
"permanent":<true | false,该房间是否是永久房间?默认= false>
}

edit请求成功,刚收到edited响应:

1
2
3
4
{
"videoroom":"edited",
"room":<房间ID>
}

接下来我们来看看destroyAPI,无论你是通过动态创建的还是静态创建的房间,均可使用destroy销毁它,其格式如下:

1
2
3
4
5
6
{
"request":"destroy",
"room":<房间ID>,
"secret":"<房间密码>",
"permanent":<true | false,是否是永久房间,默认= false>
}

成功销毁房间后将收到destroyed响应,其格式如下:

1
2
3
4
{
"videoroom":"destroyed",
"room":<房间ID>
}

销毁房间后,在房间内的所有参与者都会收到destroyed事件,如下所示:

1
2
3
4
{
"videoroom":"destroyed",
"room":<房间ID>
}

Janus中还提供了existsAPI,来检查房间是否存在,该请求的格式如下:

1
2
3
4
{
"request":"exists",
"room":<房间ID>
}

请求成功将收到success响应:

1
2
3
4
5
{
"videoroom":“success",
"room":<房间ID>,
“exists":<true | false 房间是否存在>
}

allowedAPI可以打开/关闭对令牌的检测,它还可以增加/删除允许的用户,其请求格式如下:

1
2
3
4
5
6
7
8
9
{
"request":"allowed",
"secret":"<房间密码,如果已配置,则是必需的>",
"action":"enable | disable | add | remove",
"room":<房间ID>,
"allowed":[
//字符串数组
]
}

成功请求将返回success响应:

1
2
3
4
5
6
7
{
"videoroom":“success",
"room":<房间ID>,
“allowed":[
//更新后完整的令牌列表
]
}

如果你是房间管理员(即你创建了该房间并可以加密访问),则你可以使用kickAPI踢除房间内的用户。

注意,这只会将用户踢出房间,但并不能阻止他们重新加入。要禁止他们加入,你需要先从授权用户列表中删除他们(请参阅allowed请求),然后再将其踢掉。kick请求的格式如下:

1
2
3
4
5
6
{
"request":"kick",
"secret":"<房间密码>",
"room":<房间ID>,
"id":<被踢用户ID>
}

请求成功将收到success响应:

1
2
3
{
"videoroom":"success",
}

你还可以通过listAPI获取可用房间的列表(不包括配置或创建为私有的房间),其格式如下:

1
2
3
{
"request":“list"
}

请求成功将返回success响应,响应中会带有有效的房间列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"videoroom":“成功",
"rooms":[//房间对象数组
{// 第一个房间
"room":<房间ID>,
"description":"<房间名称>",
"pin_required":<true | false,是否需要输入PIN吗才能加入此房间>,
"max_publishers":<房间内发布者最大数量,>
"bitrate":<发布者使用的(通过REMB)比特率上限>,
"bitrate_cap":<true | false,上述上限是否可以动态更改?>,
"fir_freq":<发送PLI/FIR请求关键帧的时间间隔>,
"audiocodec":"<音频编解码器列表,每个编码器以逗号分隔>",
"videocodec":"<视频编解码器列表,每个编码器以逗号分隔>",
"record":<true | false,是否打开了录制功能>,
"record_dir":"<如果开启了录掉,.mjr文件保存的路径>",
"lock_record":<true | false,是否只能通过密码才能更改房间记录状态>,
"num_participants":<房间内参与人的个数>
},
//其他房间
]
}

当然,你要获取特定房间中的参与者列表,可以使用listparticipants请求,其格式如下:

1
2
3
4
{
"request":"listparticipants",
"room":<房间ID>
}

请求成功将返回一个participants响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"videoroom":"participants",
"room":<房间ID>,
“participants":[//参与者对象的数组
{//参与者#1
"id":<用户ID>,
"display":"<用户名;可选>",
"publisher":"<true | false,用户是否是房间的发布者>",
"talking":<true | false,用户是否可以说话(仅当使用音频级别时)>
},
//其他参与者
]
}

上面是Janus中的同步API。异步API都是与参与者有关,即参与者如何发布,订阅或管理他们正在发送或接收的媒体流。

VideoRoom 发布者

在VideoRoom中,发布者是指那些能够在房间中发布媒体流的参与者。

当你以发布者的身份加入到房间里时,您应该发送join请求,并且将ptype设置为publisher。请求的具体格式如下:

1
2
3
4
5
6
7
8
{
"request":"join",
"ptype":"pbulisher",
"room":<房间ID>,
"id":<发布者ID;可选,如果缺少,将由插件选择>,
"display":"<发布者名称;可选>",
"token":"<邀请令牌,如果房间有ACL时需要该字段;可选>"
}

join请求成功将收到joined事件,其中包含当前激活的发布者列表,以及任选的参加者列表。joined事件格式如下:

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
{
"videoroom":"joined",
"room":<房间ID>,
"description":<房间名,如果有的话>,
"id":<用户ID>,
"private_id":<与参与者相关联的不同唯一ID;打算是私人的>,
“publishers":[
{
"id":<活动发布者#1的唯一ID>,
"display":"<发布者#1的名称,如果有的话>",
"audio_codec":"<发布者#1使用的音频编解码器,如果有的话>",
"video_codec":"<发布者#1使用的视频编解码器,如果有的话>",
"simulcast":"<如果发布者使用simulcast,则为true(仅VP8和H.264)>",
"talking":<true | false,发布者开启语音聊天(仅在使用音频级别的情况下)>,
},
//其他活跃的发布者
],
"attendees":[//仅当房间的notify_joining设置为TRUE时存在
{
"id":<与会者#1的唯一ID>,
"display":"<与会者#1的名称,如果有的话>"
},
//其他参加者
]
}

注意,如果房间中当前没有人,则发布者列表为空。上面格式中的private_id属性只有在用户订阅时才起作用。

对于房间里的订阅者来说,会收到event通知。

1
2
3
4
5
6
7
8
{
"videoroom":"event",
"room":<房间ID>,
“joining":{
"id":<参与者ID>,
"display":"<参与者名称>"
}
}

如果你想成为发布者,则发送publish请求。该请求必须跟着一个JSEP SDP Offer,用于协商新的PeerConnection。插件会将其与房间配置进行匹配(例如,确保房间中使用协商的编解码器),并使用JSEP SDP answer进行答复从而完成PeerConnection的设置。建立PeerConnection后,发布者立即处于活动状态,其他参与者就可以订阅它发布的流啦。

publish请求格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"request":"publish",
"audio":<true | false,是否应该转发音频;默认为true>,
"video":<true | false,是否应该转发视频;默认为true>,
"data":<true | false,是否应该转发数据;默认为true>,
"audiocodec":"<在协商协议中首选的音频编解码器;可选>",
"videocodec":"<在协商协议中首选的视频编解码器;可选>",
"bitrate":<通过REMB返回的比特率上限;可选,如果存在则覆盖全局房间值>,
"record":<true | false,是否应该记录此发布者;可选>
"filename":"<录制文件名;可选>",
"display":"<用户名称;可选>",
"audio_level_average":"<音频音量平均值,此设置覆盖房间的audio_level_average;可选>",
"audio_active_packets":"<音频保活包数,此设置覆盖房间audio_active_packets;可选>
}

此请求应该与发布者的JSEP SDP Offer一起提供,插件收到此消息后,将协商与之匹配的JSEP SDP Answer。如果成功,configured事件将被返回,其格式如下:

1
2
3
4
{
"videoroom":"event",
"configured":“ok"
}

该事件将与准备好的JSEP SDP Answer一起发送给客户端。

你也可以用configure请求代替publish。两者的功能在发布上是等效的,但从语义的角度来看,publish是发布时要发送的正确消息。configure请求也可以用于更新发布者会话的某些属性,在这种情况下,就不能用publish请求了。

需要注意的是,如果用户已经发送过publish了,再发送publish将导致失败。

其实,您可以将joinpublish两个API合并为一个API请求。比如你一开始以参与者的身份加入,随后变为发布者,这时你就可以将他们合并。你可以使用joinandconfigure请求来做到这一点,该请求将这两个请求(join与publish)结合在一起。如果成功,则响应一个joined事件,并且将JSEP SDP Answer一起发送出去。

一旦PeerConnection设置成功,且发布者处于激活状态,event就会被发向房间中的所有参与者。其格式如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"videoroom":"event",
"room":<房间ID>,
“publishers":[
{
"id":<新发布者的唯一ID>,
"display":"<新发布者的名称,如果有的话>",
"audio_codec":"<新布者使用的音频编解码器,如果有的话>",
"video_codec":"<新发布使用的视频编解码器,如果有的话>",
"simulcast":"<如果发布者使用simucast,则为true(仅VP8和H.264)>",
"talking":<true | false,发布者是否在讲话(仅在使用音频级别的情况下)>,
}
]
}

要停止发布并删除相关的PeerConnection,可以使用该unpublish请求:

1
2
3
{
"request":“unpublish"
}

当插件收到这条请求后,它会删除对应的PeerConnection,并将发布者从活动列表中删除。如果成功,响应如下所示:

1
2
3
4
{
"videoroom":"event",
“unpublish":“ok"
}

PeerConnection删除后,插件还将向所有其他参与者通知该流不再可用的消息:

1
2
3
4
5
{
"videoroom":"event",
"room":<房间ID>,
"unpublished":<发布者的ID>
}

注意,不光收到unpublish消息会触发上面的事件通知,其实无论什么情况下,只要发布者提供的流消失了(例如,句柄已关闭或用户失去连接),都会发同样的事件。此外,你可以使用同一句柄的上下文多次执行发布取消发布操作。

正如我们上面讲过的,你可以使用configure请求调整发布者会话的某些属性。该请求的格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"request":"configure",
"audio":<true | false,取决于是否应该转发音频;默认为true>,
"video":<true | false,取决于是否应该转发视频;默认为true>,
"data":<true | false,取决于是否应该转发数据;默认为true>,
"bitrate":<比特率上限;可选,如果存在则覆盖全局房间值(除非设置了bitrate_cap)>,
"keyframe":<true | false,是否向发布者发送关键帧请求>,
"record":<true | false,是否开启录制;可选>
"filename":"<如果开启了录制,指明录制路径/文件;可选>",
"display":"<用户名称;可选>",
"audio_active_packets":"<音频保活包个数,audio_active_packets;可选>",
"audio_level_average":"<音频音量平均值,audio_level_average;可选>",
}

configure基本上与publish的属性相同。这就是为什么两个请求都可以用来开始发布的原因。如果configure成功,则返回configured事件,格式如下:

1
2
3
4
{
"videoroom":"event",
"configured":“ok"
}

当发送configure请求RTP扩展ssrc-audio-level时,如果audiolevel_event设置为true ,则可能会向所有发布者发送一些临时事件。这些事件将具有以下格式:

1
2
3
4
5
6
{
"videoroom":<"talking"|"stopped-talking",是否发布者开始或停止发言>,
"room":<房间的唯一ID>,
"id":<发布者的唯一ID>,
"audio-level-dBov-avg":<音平音量的平均值,127 =静音,0 ='太大声'>
}

VideoRoom插件的主要目的是从WebRTC源(发布者)获取媒体,并将其转发到WebRTC目的地(订阅者),但实际上存在几种方案,可以将媒体转发给外部(不一定与WebRTC兼容)组件。例如,用于媒体处理,外部录制,转码,级联等等。rtp_forward顾名思义,就是将发布者发送的RTP包(普通或加密)实时转发到远程后端。

您可以使用rtp_forward请求为现有发布者添加新的RTP转发器,其格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"request":"rtp_forward",
"room":<房间ID>,
"publisher_id":<发布者ID>,
"host":"<将RTP和数据包转发到的host主机IP地址>",
"host_family":"<ipv4 | ipv6,使用IPv4还是IPv6;默认情况下,无论我们得到什么>",
"audio_port":<音频RTP数据包转发到的端口>,
"audio_ssrc":<音频SSRC,用于流式传输;可选>
"audio_pt":<音频有效负载类型;可选>
"audio_rtcp_port":<接收方接收音频RTCP反馈端口;可选,当前未用于音频>,
"video_port":<将视频RTP数据包转发到的端口>,
"video_ssrc":<视频 SSRC;可选>
"video_pt":<视频有效载荷类型;可选>
"video_rtcp_port":<接收方接收视频RTCP反馈端口;可选>
"video_port_2":<如果simulcast,则视频第二个的RTP数据端口>,
"video_ssrc_2":<如果simulcast,则视频第二个的SSRC;可选>
"video_pt_2":<如果simulcast,则视频第二个的有效载荷类型;可选>
"video_port_3":<如果simulcast,则视频第三个RTP数据包端口>,
"video_ssrc_3":<如果simulcast,则视频第三个SSRC;可选>
"video_pt_3":<如果simulcast,则视频第三个的有效载荷类型;可选>
"data_port":<数据通道消息端口>,
"srtp_suite":<身份验证标签的长度(32或80);可选>
"srtp_crypto":"<用作加密的密钥(如SDES中的base64编码的密钥;可选>"
}

注意,如上所述,如果您配置了admin_key属性,则在请求中也需要提供它,否则未授权的请求将被拒绝。默认情况下,没有对rtp_forward进行限制。

如果请求成功则返回rtp_forward响应,其中格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"videoroom":"rtp_forward",
"room":<房间ID>,
"publisher_id":<发布者ID>
"rtp_stream":{
"host":"<接收流的主机IP,如果未解析,则与请求相同>",
"audio":<音频RTP端口,与请求相同(如果已配置)>,
"audio_rtcp":<音频RTCP端口,与请求相同(如果已配置)>,
"audio_stream_id":<分配给音频RTP转发器的唯一数字ID,如果有的话,>
"video":<视频RTP端口,与请求相同(如果已配置)>,
"video_rtcp":<视频RTCP端口,如果配置,则与请求相同,>
"video_stream_id":<分配给主视频RTP转发器的唯一数字ID,如果有的话,>
"video_2":<第二个视频端口,与请求相同(如果已配置)>,
"video_stream_id_2":<分配给第二层视频RTP转发器的唯一数字ID,如果有的话,>
"video_3":<第三个视频端口,与请求相同(如果已配置)>,
"video_stream_id_3":<分配给第三个视频RTP转发器的唯一数字ID,如果有,>
"data":<数据端口,与请求相同(如果已配置)>,
"data_stream_id":<分配给数据通道消息转发器的唯一数字ID(如果有)>
}
}

要停止以前创建的RTP转发器,可以使用stop_rtp_forward请求,其格式如下:

1
2
3
4
5
6
{
"request":"stop_rtp_forward",
"room":<房间ID>,
"publisher_id":<发布者ID>,
"stream_id":<RTP转发器ID>
}

请求成功,则返回stop_rtp_forward响应:

1
2
3
4
5
6
{
"videoroom":"stop_rtp_forward",
"room":<房间ID>,
"publisher_id":<发布者ID,与请求相同,>
"stream_id":<流ID,与请求相同>
}

如果要获取特定房间中所有转发器的列表,可以使用listforwarders请求,其格式如下:

1
2
3
4
5
{
"request":"listforwarders",
"room":<房间的唯一数字ID>,
"secret":"<房间密码;如果已配置,则是必需的>"
}

请求成功,则返回forwarders响应,其中包括RTP转发器列表:

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
{
"videoroom":"forwarders",
"room":<房间的唯一ID>,
"rtp_forwarders":[//具有RTP转发器的发布者数组
{//发布者#1
"publisher_id":<发布者#1的唯一数字ID>,
"rtp_forwarders":[// RTP转发器数组
{// RTP转发器#1
"audio_stream_id":<音频RTP转发器的唯一ID,如果有的话>,
"video_stream_id":<视频RTP转发器的唯一ID,如果有的话>,
"data_stream_id":<数据通道消息转发器的唯一ID(如果有)>,
"ip":"<接收端IP>",
"port":<接收端端口>,
"rtcp_port":<接收端RTCP端口,如果有的话>,
"ssrc":<转发器正在使用的SSRC,如果有的话>,
"pt":<转发器正在使用的有效负载类型>,
"substream":<视频子流,如果有>,
"srtp":<true | false,RTP流是否已加密>
},
//此发布者的其他转发器
],
},
//其他发布者
]
}

在会议进行期间启用或禁用录制,您可以使用enable_recording请求,该请求的格式如下:

1
2
3
4
5
6
{
"request":"enable_recording",
"room":<房间ID>,
"secret":"<房间密码;如果已配置,则是必需的>"
"record":<true | false,是否自动记录此会议室中的参与者>,
}

注意,参与者通常也可以通过configure请求来更改自己的录制状态:这样做是为了获得最大的灵活性,您可能希望单独记录一些流,而不是全局或自动记录一些内容,到特定文件。就是说,如果你希望确保在启用全局录制后参与者不能停止其录制,或者在不应该录制该会议室的情况下启动它,那么您应该确保在创建会议室时使用lock_record属性,将其设置为true。这样,只有在提供了房间密码的情况下,才能更改录制状态,从而确保只有管理员才能执行此操作。

最后,您可以使用leave请求离开会议室。如果您是会议室中的活动发布者,这也将隐式取消你的发布。该leave请求如下所示:

1
2
3
{
"request":"leave"
}

如果成功,响应将如下所示:

1
2
3
4
{
"videoroom":"event",
"leaving":"ok"
}

其他参与者将收到”leaving”事件,格式如下:

1
2
3
4
5
{
"videoroom":"event",
"room":<房间ID>,
"leaving:<离开的参与者的唯一ID>
}

如果您是活跃的发布者,则其他用户也将收到相应的unpublish事件,以通知他们该流不再可用。如果您只是潜伏而不是发布者,则其他参与者将仅收到”leave”事件。

VideoRoom 订阅者

订阅者在加入房间时,join请求的ptype属性应该设置为subscriber,并指定要订阅的确切的媒体流。该请求的确切语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"request":"join",
"ptype":"subscriber",
"room":<房间ID>,
"feed":<发布者ID;强制性>,
"private_id":<发起此请求的用户ID;可选的,除非房间配置要求>
"close_pc":<true | false,发布者离开时是否应自动关闭PeerConnection;默认为true>,
"audio":<true | false,是否转发音频;默认为true>,
"video":<true | false,是否转发视频;默认为true>,
"data":<true | false,是否转发数据;默认为true>,
"offer_audio":<true | false; 是否应该协商音频;如果发布者的音频>,默认为true
"offer_video":<true | false; 是否应该协商视频;如果发布者的视频>,默认为true
"offer_data":<true | false; 是否应该协商数据通道;如果发布者的datachannels>为默认值,则为true
"substream":<启用了simulcast情况下,要接收的子流(0-2);可选>
"temporal":<启用simulcast情况下,要接收的时间层(0-2);可选>
"fallback":<多少时间(在我们这里,默认为250000)没有接收到数据包将使我们下降到下面的子流>,
"spatial_layer":<启用VP9-SVC时要接收的空间层(0-2);可选>
"temporal_layer":<启用VP9-SVC时要接收的时间层(0-2);可选>
}

如您所见,只要指定好要订阅的发布者ID,并在需要时指定好private_id(订阅者ID),其它的都可以不设置。不过请求中的offer_audiooffer_videooffer_data特别有意思,你可以通过它们订阅媒体的一个子集(音频\视频\数据)。

默认情况下,发送join请求时会导致插件层创建SDP Offer,用以协商发布者提供那些媒体。此外,如果发布者发布的是simulcastVP9 SVC,那么你还可以订阅你感兴趣的子流,例如,获得最佳质量的中间质量。更有意思的是,你可以使用configure请求随时动态更改这些设置。

上面的请求如果成功,将生成一个新的JSEP SDP Offer,并伴随一个attached事件:

1
2
3
4
5
6
{
"videoroom":"attached",
"room":<房间ID>,
"feed":<发布者ID>,
"display":"<发布者的名称,如果有的话>"
}

在此阶段,为了完成PeerConnection的设置,订阅者应将JSEP SDP Answer发送回插件。此操作是通过start请求来完成的,在这种情况下,请求必须与JSEP SDP Answer相关联,但是不需要任何参数:

1
2
3
{
"request":“start"
}

如果成功,此请求将返回一个started事件:

1
2
3
4
{
"videoroom":"event",
"started":"ok"
}

完成此操作后,所需要做的就是等待WebRTC PeerConnection建立成功。一旦PeerConnection建立成功,Streaming插件就可以开始向订阅的观众转发媒体了。

注意,在需要重新协商(例如出于ICE重启目的)的情况下,您也可以使用我们刚经历的相同步骤(watch请求,然后插件创建JSEP Offer,最后客户端发送start请求和JSEP Answer)。

作为订阅者,您可以发送pause临时暂停或发送start恢复整个媒体的传送(在这种情况下,不附带任何JSEP SDP Answer)。因为上下文中已经有了相关信息,所以不需要重新进行协商。

1
2
3
{
"request":"pause"
}
1
2
3
{
"request":"start"
}

当然,它们会分别导致paused和started事件:

1
2
3
4
{
"videoroom":"event",
"paused":"ok"
}
1
2
3
4
{
"videoroom":"event",
"started":"ok"
}

configure请求可以对订阅做更多深入操作。该请求允许订阅者动态更改与媒体订阅有关的某些属性,configure请求的格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"request":"configure",
"audio":<true | false,是否应该转发音频;可选>
"video":<true | false,是否应该转发视频;可选>
"data":<true | false,是否转发数据;可选>
"substream":<启用simulcast情况下,要接收的子流(0-2);可选>
"temporal":<启用simulcast,要接收的时间层(0-2);可选>
"fallback":<多少时间(在我们这里,默认为250000)没有接收到数据包将使我们下降到下面的子流>,
"spatial_layer":<启用VP9-SVC时要接收的空间层(0-2);可选>
"temporal_layer":<启用VP9-SVC时要接收的时间层(0-2);可选>
"audio_level_average":"<如果提供,将覆盖此用户的房间audio_level_average;可选>",
"audio_active_packets":"<如果提供,将覆盖此用户的房间audio_active_packets;可选>
}

正如你所看到的audio,video和data属性可以用作媒体级的暂停/恢复功能,而pause与start只是简单地暂停/恢复所有数据流。

下面来说说switch,switch 请求格式如下:

1
2
3
4
5
6
7
{
"request":"switch",
"feed":<要切换到的新发布者的唯一ID;强制性>,
"audio":<true | false,取决于是否应该中继音频;可选>
"video":<true | false,取决于是否应该中继视频;可选>
"data":<true | false,取决于是否应该中继数据通道消息;可选>
}

如果成功,您将退订之前的发布者,然后订阅新的发布者。确认切换成功的事件如下所示:

1
2
3
4
5
6
{
"videoroom":"event",
"switched":"ok",
"room":<房间ID>,
"id":<新发布者的唯一ID>
}

最后,要停止订阅并删除相关的PeerConnection,可以使用该leave请求。由于上下文是隐式的,因此不需要其他参数:

1
2
3
{
"request":"leave"
}

如果成功,该插件将尝试拆除PeerConnection,并发送回一个left事件:

1
2
3
4
{
"videoroom":"event",
"left":"ok",
}

小结

VideoRoom插件是Janus的一个特别重要的插件,对于该插件的理解对于我们理解整个Janus有至关重要的意义。本文说细分析了VideoRoom插件中所有的信令,大体上我们可以将它们人成两在类,一类是房间管理信令,另一类是用户信令。

这些信令设计的非常巧妙,对我们研发自己的SFU会议系统是一个很好的借鉴。

参考

多人实时互动之各WebRTC流媒体服务器比较
janus前端核心库源码分析

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