通知短信+运营短信,5秒速达,支持群发助手一键发送🚀高效触达和通知客户 广告
# 1.概要      在Janus的众多插件中,有一个房间插件videoRoom,由于它实现视频会议sfu功能,通过改造能适合我们很多业务场景。而且在Janus众多的业务插件中`VideoRoom`应该也是最复杂的一个,如果你们撑握了它,就轻而易举了解其他插件。      janus 提供了很多管理房间的api接口,包括房间的创建,修改,删除,转推等操作。学习这些api有助于了解janus房间管理,同时也可以学习房间管理需要提供哪些标准的api接口。 ## 1.VideoRoom插件 `   VideoRoom`是Janus的一个插件,实现了一个SFU(Selective Forwarding Unit)型的音视频会议。如果你从数据转发的角度看,也可以把它认为是一个音视频`路由器`。 `   VideoRoom`实现的音视频会议是基于`发布/订阅`模式。每个`参与方`都可以发布自己的实时音视频流,因此它可以实现几种不同的场景,比如泛娱乐化直播或多人的实时互动产品(如音视频会议、在线教育小班课等)。m/n          76          主handle是发布handle。考虑到此插件允许一个`参与方`可以打开多个WebRTC `PeerConnection`(如每个`参与方`可以有1个用于推流的`PeerConnection`和N个拉流的`PeerConnection`),所以每个`参与方`需要为订阅不同的流`attach`到`VideoRoom`插件几次(每`attach`一次就会生成一个`Handle`,每个`Handle`就是一个上下文)。因此,对于每个`参与方`至少要有一个`Handle`用于管理与插件的关系(如加入一个房间,离开一个房间,静音/取消静音,发布,接收事件)。         每当`参与方`需要订阅另一个参与方发布的音视频流时,它需要创建一个新的`Handle`。新创建的`Handle`在逻辑上属于**“slave”**`Handle`,它不能像**“master”**`Handle`一样可以做取消房间静音这样的操作。因此,**从**`Handle`唯一目的是提供一个上下文,在该上下文中创建一个`recvonly`类型的`PeerConnection`来订阅发布者的音视频流。 通过上面的描述我们可以知道,主Handle用于管理,而从Handle用于订阅音视频流。 > 注意,现在`WebRTC`已经实现了SSRC复用(Unified Plan),这意味着你可以使用相同的`Janus Handle` 和`PeerConnection`同时接收多路音视频流。 ## 2.房间参数         VideoRoom插件功能非常强大,也很灵活,它有很多的配置项,你可以通过`conf/janus.plugin.videoroom.jcfg`来修改它们。当然Janus也支持动态API修改配置,如通过API创建房间等。 ### 2.1 创建房间 要增加更多房间或修改现有房间信息,你可以向Janus发送下面格式的请求: ~~~ 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)} ~~~ ## 3\. Video Room API接口 `  VideoRoom` 插件支持很多API操作。这些API中,一些是同步请求,一些则是异步请求。但无论是同步还是异步请求,当遇到无效的JSON格式或无效的请求时,都使用同步进行错误响应。      接下来,我们首先看看都有那些同步请求API。`create`,`destroy`,`edit`,`exists`,`list`,`allowed`,`kick`和`listparticipants`是同步请求API。`create`允许您动态创建一个新的音视频房间;`edit`允许您动态编辑房间的属性(例如 修改PIN码);`destroy`首先释放视频资源,然后踢除房间里的所有用户,最后销毁音视频房间;`exists`检查指定的音视频房间是否存在;`list`列出所有有效的音视频房间; `listparticipants`列出指定房间中所有激活的参与者及其详细信息。      异步请求API有:`join`,`joinandconfigure`,`configure`,`publish`,`unpublish`,`start`,`pause`,`switch`和`leave`。`join`允许你加入指定的音视频房间;`configure`可用于修改某些属性(例如,比特率范围);`joinandconfigure`的含义是将前两个请求合并为一个请求(该请求仅适用于发布者);`publish`发布媒体流给所有订阅者; `unpublish`正好与`publish`相反;`start`允许你开始接收订阅的媒体流;`pause`暂停发送媒体流;`switch`更改指定`PeerConnection`的媒体源(例如,你正在看A,现在改为看B),但无需为此创建新的Handle;`leave`离开视频房间。 下面咱们对上面提到的API做一下详细分析,首先看一下`create`API,它用于创建新的音视频房间,其格式如下: ~~~ { "request":"create", "room":<可选,房间ID。如果不填,则由插件随机生成>, "permanent":<true | false,是否创建永久房间,默认= false>, "description":"<可选,房间的名称>", "secret":"<可选,编辑/销毁房间时用的密码>", "pin":"<可选,加入房间的密码>", "is_private":<true | false,是否是私有房间?如果是私有房间则不会出现在房间列表中>, "allowed":[可选,用户加入房间的token数组], ...} ~~~ 上面的说明已经非常清楚了,这里我就不做简赘述了。 如果`create`成功,则会返回`created`响应,格式如下: ~~~ { "videoroom":“created", "room":<房间ID>, "permanent":<是否是创建的永久房间?是则为true,否则为false>} ~~~ > 注意,如果你请求创建一个永久房间,但`permanet`返回的是false,很可能是因为权限的问题导致的。 如果`create`请求失败,则返回错误信息,格式如下: ~~~ { "videoroom":"event", "error_code":<错误码,每个错误码的含义需要看插件实现代码中的宏定义>, "error":"<错误描述字符串>"} ~~~ > 这里需要注意的是,所有请求的错误响应格式都与上面一样。 默认情况下,所有用户都可以创建房间,但你可以通过在`VideoRoom`插件的配置文件中增加`admin_key`项来限制此功能。此时,只有带了正确的`admin_key`值的`create`请求才能成功创建房间。你也可以选择将此功能扩展到RTP转发,只转发受信任的客户端的RTP包。 房间创建好后,您可以用`edit`API编辑其中的部分(但不是全部)属性。`edit`允许你修改房间描述,密码,PIN码以及是否为私有。但你将无法修改他的静态属性,例如房间ID,采样率,与扩展相关的内容等。如果你有兴趣更改ACL,还需要查看`allowed`是否允许。 一个`edit`请求格式如下: ~~~ { "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`响应: ~~~ { "videoroom":"edited", "room":<房间ID>} ~~~ 接下来我们来看看`destroy`API,无论你是通过动态创建的还是静态创建的房间,均可使用`destroy`销毁它,其格式如下: ~~~ { "request":"destroy", "room":<房间ID>, "secret":"<房间密码>", "permanent":<true | false,是否是永久房间,默认= false>} ~~~ 成功销毁房间后将收到`destroyed`响应,其格式如下: ~~~ { "videoroom":"destroyed", "room":<房间ID>} ~~~ 销毁房间后,在房间内的所有参与者都会收到`destroyed`事件,如下所示: ~~~ { "videoroom":"destroyed", "room":<房间ID>} ~~~ Janus中还提供了`exists`API,来检查房间是否存在,该请求的格式如下: ~~~ { "request":"exists", "room":<房间ID>} ~~~ 请求成功将收到success响应: ~~~ { "videoroom":“success", "room":<房间ID>, “exists":<true | false 房间是否存在>} ~~~ `allowed`API可以打开/关闭对令牌的检测,它还可以增加/删除允许的用户,其请求格式如下: ~~~ { "request":"allowed", "secret":"<房间密码,如果已配置,则是必需的>", "action":"enable | disable | add | remove", "room":<房间ID>, "allowed":[ //字符串数组 ]} ~~~ 成功请求将返回success响应: ~~~ { "videoroom":“success", "room":<房间ID>, “allowed":[ //更新后完整的令牌列表 ]} ~~~ 如果你是房间管理员(即你创建了该房间并可以加密访问),则你可以使用`kick`API踢除房间内的用户。 > 注意,这只会将用户踢出房间,但并不能阻止他们重新加入。要禁止他们加入,你需要先从授权用户列表中删除他们(请参阅allowed请求),然后再将其踢掉。`kick`请求的格式如下: ~~~ { "request":"kick", "secret":"<房间密码>", "room":<房间ID>, "id":<被踢用户ID>} ~~~ 请求成功将收到success响应: ~~~ { "videoroom":"success",} ~~~ 你还可以通过`list`API获取可用房间的列表(不包括配置或创建为私有的房间),其格式如下: ~~~ { "request":“list"} ~~~ 请求成功将返回success响应,响应中会带有有效的房间列表: ~~~ { "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`请求,其格式如下: ~~~ { "request":"listparticipants", "room":<房间ID>} ~~~ 请求成功将返回一个`participants`响应: ~~~ { "videoroom":"participants", "room":<房间ID>, “participants":[//参与者对象的数组 {//参与者#1 "id":<用户ID>, "display":"<用户名;可选>", "publisher":"<true | false,用户是否是房间的发布者>", "talking":<true | false,用户是否可以说话(仅当使用音频级别时)> }, //其他参与者 ]} ~~~ 上面是Janus中的同步API。异步API都是与`参与者`有关,即`参与者`如何发布,订阅或管理他们正在发送或接收的媒体流。 ## 4\. VideoRoom 发布者 在VideoRoom中,`发布者`是指那些能够在房间中发布`媒体流`的参与者。 当你以`发布者`的身份加入到房间里时,您应该发送`join`请求,并且将ptype设置为`publisher`。请求的具体格式如下: ~~~ { "request":"join", "ptype":"pbulisher", "room":<房间ID>, "id":<发布者ID;可选,如果缺少,将由插件选择>, "display":"<发布者名称;可选>", "token":"<邀请令牌,如果房间有ACL时需要该字段;可选>"} ~~~ `joid`请求成功将收到`joined`事件,其中包含当前激活的`发布者`列表,以及任选的`参加者`列表。`joined`事件格式如下: ~~~ { "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`通知。 ~~~ { "videoroom":"event", "room":<房间ID>, “joining":{ "id":<参与者ID>, "display":"<参与者名称>" }} ~~~ 如果你想成为`发布者`,则发送`publish`请求。该请求必须跟着一个`JSEP SDP Offer`,用于协商新的`PeerConnection`。插件会将其与房间配置进行匹配(例如,确保房间中使用协商的编解码器),并使用`JSEP SDP answer`进行答复从而完成`PeerConnection`的设置。建立`PeerConnection`后,发布者立即处于活动状态,其他参与者就可以订阅它发布的流啦。 `publish`请求格式如下: ~~~ { "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`事件将被返回,其格式如下: ~~~ { "videoroom":"event", "configured":“ok"} ~~~ 该事件将与准备好的`JSEP SDP Answer`一起发送给客户端。 你也可以用`configure`请求代替`publish`。两者的功能在`发布`上是等效的,但从语义的角度来看,`publish`是发布时要发送的正确消息。`configure`请求也可以用于更新发布者会话的某些属性,在这种情况下,就不能用`publish`请求了。 > 需要注意的是,如果用户已经发送过`publish`了,再发送`publish`将导致失败。 其实,您可以将`join`和`publish`两个API合并为一个API请求。比如你一开始以`参与者`的身份加入,随后变为`发布者`,这时你就可以将他们合并。你可以使用`joinandconfigure`请求来做到这一点,该请求将这两个请求(join与publish)结合在一起。如果成功,则响应一个`joined`事件,并且将`JSEP SDP Answer`一起发送出去。 一旦`PeerConnection`设置成功,且发布者处于激活状态,`event`就会被发向房间中的所有`参与者`。其格式如下所示: ~~~ { "videoroom":"event", "room":<房间ID>, “publishes":[ { "id":<新发布者的唯一ID>, "display":"<新发布者的名称,如果有的话>", "audio_codec":"<新布者使用的音频编解码器,如果有的话>", "video_codec":"<新发布使用的视频编解码器,如果有的话>", "simulcast":"<如果发布者使用simucast,则为true(仅VP8和H.264)>", "talking":<true | false,发布者是否在讲话(仅在使用音频级别的情况下)>, } ]} ~~~ 要停止发布并删除相关的`PeerConnection`,可以使用该`unpublish`请求: ~~~ { "request":“unpublish"} ~~~ 当插件收到这条请求后,它会删除对应的`PeerConnection`,并将发布者从活动列表中删除。如果成功,响应如下所示: ~~~ { "videoroom":"event", “unpublish":“ok"} ~~~ 当`PeerConnection`删除后,插件还将向所有其他`参与者`通知该流不再可用的消息: ~~~ { "videoroom":"event", "room":<房间ID>, "unpublished":<发布者的ID>} ~~~ > 注意,不光收到`unpublish`消息会触发上面的事件通知,其实无论什么情况下,只要`发布者`提供的流消失了(例如,句柄已关闭或用户失去连接),都会发同样的`事件`。此外,你可以使用同一句柄的上下文多次执行`发布`或`取消发布`操作。 正如我们上面讲过的,你可以使用`configure`请求调整发布者会话的某些属性。该请求的格式如下: ~~~ { "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`事件,格式如下: ~~~ { "videoroom":"event", "configured":“ok"} ~~~ 当发送`configure`请求RTP扩展`ssrc-audio-level`时,如果`audiolevel_event`设置为true ,则可能会向所有发布者发送一些临时事件。这些事件将具有以下格式: ~~~ { "videoroom":<"talking"|"stopped-talking",是否发布者开始或停止发言>, "room":<房间的唯一ID>, "id":<发布者的唯一ID>, "audio-level-dBov-avg":<音平音量的平均值,127 =静音,0 ='太大声'>} ~~~ ## 5\. VideoRoom 级联 `  VideoRoom`插件的主要目的是从WebRTC源(发布者)获取媒体,并将其转发到WebRTC目的地(订阅者),但实际上存在几种方案,可以将媒体转发给外部(不一定与WebRTC兼容)组件。例如,用于媒体处理,外部录制,转码,级联等等。`rtp_forward`顾名思义,就是将发布者发送的RTP包(普通或加密)实时转发到远程后端。 您可以使用`rtp_forward`请求为现有发布者添加新的RTP转发器,其格式如下: ~~~ { "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`响应,其中格式如下: ~~~ { "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`请求,其格式如下: ~~~ { "request":"stop_rtp_forward", "room":<房间ID>, "publisher_id":<发布者ID>, "stream_id":<RTP转发器ID>} ~~~ 请求成功,则返回`stop_rtp_forward`响应: ~~~ { "videoroom":"stop_rtp_forward", "room":<房间ID>, "publisher_id":<发布者ID,与请求相同,> "stream_id":<流ID,与请求相同>} ~~~ 如果要获取特定房间中所有转发器的列表,可以使用`listforwarders`请求,其格式如下: ~~~ { "request":"listforwarders", "room":<房间的唯一数字ID>, "secret":"<房间密码;如果已配置,则是必需的>"} ~~~ 请求成功,则返回forwarders响应,其中包括RTP转发器列表: ~~~ { "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`请求,该请求的格式如下: ~~~ { "request":"enable_recording", "room":<房间ID>, "secret":"<房间密码;如果已配置,则是必需的>" "record":<true | false,是否自动记录此会议室中的参与者>,} ~~~ > 注意,参与者通常也可以通过configure请求来更改自己的录制状态:这样做是为了获得最大的灵活性,您可能希望单独记录一些流,而不是全局或自动记录一些内容,到特定文件。就是说,如果你希望确保在启用全局录制后参与者不能停止其录制,或者在不应该录制该会议室的情况下启动它,那么您应该确保在创建会议室时使用lock\_record属性,将其设置为true。这样,只有在提供了房间密码的情况下,才能更改录制状态,从而确保只有管理员才能执行此操作。 最后,您可以使用`leave`请求离开会议室。如果您是会议室中的活动发布者,这也将隐式取消你的发布。该leave请求如下所示: ~~~ { "request":"leave"} ~~~ 如果成功,响应将如下所示: ~~~ { "videoroom":"event", "leave":"ok"} ~~~ 其他参与者将收到”leave”事件,格式如下: ~~~ { "videoroom":"event", "room":<房间ID>, "leave:<离开的参与者的唯一ID>} ~~~ 如果您是活跃的发布者,则其他用户也将收到相应的`unpublish`事件,以通知他们该流不再可用。如果您只是潜伏而不是发布者,则其他参与者将仅收到”leave”事件。 ## VideoRoom 订阅者 订阅者在加入房间时,join请求的ptype属性应该设置为`subscriber`,并指定要订阅的确切的媒体流。该请求的确切语法如下: ~~~ { "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_audio`,`offer_video`和`offer_data`特别有意思,你可以通过它们订阅媒体的一个子集(音频\\视频\\数据)。 默认情况下,发送`join`请求时会导致插件层创建`SDP Offer`,用以协商发布者提供那些媒体。此外,如果发布者发布的是`simulcast`或`VP9 SVC`,那么你还可以订阅你感兴趣的子流,例如,获得最佳质量的中间质量。更有意思的是,你可以使用`configure`请求随时动态更改这些设置。 上面的请求如果成功,将生成一个新的`JSEP SDP Offer`,并伴随一个attached事件: ~~~ { "videoroom":"attached", "room":<房间ID>, "feed":<发布者ID>, "display":"<发布者的名称,如果有的话>"} ~~~ 在此阶段,为了完成`PeerConnection`的设置,订阅者应将`JSEP SDP Answer`发送回插件。此操作是通过`start`请求来完成的,在这种情况下,请求必须与`JSEP SDP Answer`相关联,但是不需要任何参数: ~~~ { "request":“start"} ~~~ 如果成功,此请求将返回一个started事件: ~~~ { "videoroom":"event", "started":"ok"} ~~~ 完成此操作后,所需要做的就是等待WebRTC `PeerConnection`建立成功。一旦`PeerConnection`建立成功,Streaming插件就可以开始向订阅的观众转发媒体了。 > 注意,在需要重新协商(例如出于ICE重启目的)的情况下,您也可以使用我们刚经历的相同步骤(watch请求,然后插件创建`JSEP Offer`,最后客户端发送`start`请求和`JSEP Answer`)。 作为`订阅者`,您可以发送`pause`临时暂停或发送`start`恢复整个媒体的传送(在这种情况下,不附带任何JSEP SDP Answer)。因为上下文中已经有了相关信息,所以不需要重新进行协商。 ~~~ { "request":"pause"} { "request":"start"} ~~~ 当然,它们会分别导致paused和started事件: ~~~ { "videoroom":"event", "paused":"ok"} { "videoroom":"event", "started":"ok"} ~~~ `configure`请求可以对`订阅`做更多深入操作。该请求允许`订阅者`动态更改与媒体订阅有关的某些属性,`configure`请求的格式如下: ~~~ { "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 请求格式如下: ~~~ { "request":"switch", "feed":<要切换到的新发布者的唯一ID;强制性>, "audio":<true | false,取决于是否应该中继音频;可选> "video":<true | false,取决于是否应该中继视频;可选> "data":<true | false,取决于是否应该中继数据通道消息;可选>} ~~~ 如果成功,您将退订之前的发布者,然后订阅新的发布者。确认切换成功的事件如下所示: ~~~ { "videoroom":"event", "switched":"ok", "room":<房间ID>, "id":<新发布者的唯一ID> } ~~~ 最后,要停止订阅并删除相关的PeerConnection,可以使用该leave请求。由于上下文是隐式的,因此不需要其他参数: ~~~ { "request":"leave" } ~~~ 如果成功,该插件将尝试拆除PeerConnection,并发送回一个left事件: ~~~ { "videoroom":"event", "left":"ok", } ~~~ ## 小结 `VideoRoom`插件是Janus的一个特别重要的插件,对于该插件的理解对于我们理解整个Janus有至关重要的意义。本文说细分析了`VideoRoom`插件中所有的信令,大体上我们可以将它们人成两在类,一类是房间管理信令,另一类是用户信令。 这些信令设计的非常巧妙,对我们研发自己的SFU会议系统是一个很好的借鉴。