15、Janus网关:Janus架构、插件机制、视频会议插件使用

聊到WebRTC的服务端,很多人第一个想到的就是Janus。说实话,我在刚接触WebRTC那会儿,也被各种SFU、MCU搞得一头雾水。后来真正上手Janus,才发现它的设计思路其实非常清晰——说白了,它就是一个“插件化”的WebRTC网关。

我个人习惯把Janus比作一个“乐高底座”。它本身不提供具体的业务功能,但你可以往上面插各种“积木”(插件),来实现视频会议、直播、录音、推流等等。这种设计让Janus非常灵活,也是它能在众多WebRTC服务器中脱颖而出的原因。

15.1 Janus的整体架构

先来看一张我画的架构图,帮你快速建立整体认知:

Janus 网关核心架构 Janus Core(核心层) 会话管理 · ICE/DTLS/SRTP · 媒体路由 传输层 WebSocket / HTTP / RabbitMQ 插件层 VideoRoom / AudioBridge / Streaming VideoRoom(视频会议) AudioBridge(音频混音) Streaming(直播推流) EchoTest(回音测试) Web/App 客户端 图:Janus 三层架构(传输层 → 核心层 → 插件层)

Janus的架构分为三层:

  • 传输层:负责与客户端通信。支持WebSocket、HTTP(REST API)、RabbitMQ等多种方式。我个人最常用的是WebSocket,延迟低,双向通信方便。
  • 核心层(Janus Core):这是Janus的心脏。它处理所有WebRTC的底层细节——ICE连接、DTLS握手、SRTP加密、媒体流的转发。嗯,这里要注意,Core本身不关心你传的是视频还是音频,它只负责把数据从A点搬到B点。
  • 插件层:真正的业务逻辑都在这里。每个插件是一个独立的动态库(.so文件),Core通过标准接口调用它们。你想做视频会议?加载VideoRoom插件。想做直播?加载Streaming插件。

核心要点:Janus的Core和插件之间通过JSON消息通信。插件不直接接触WebRTC的底层细节,它只需要告诉Core“我要发什么数据给谁”,剩下的ICE/DTLS/SRTP都由Core搞定。

15.2 插件机制详解

插件机制是Janus的灵魂。我刚开始研究Janus源码时,最困惑的就是插件到底怎么工作的。后来自己动手写了一个简单的Echo插件,才算真正搞明白。

15.2.1 插件的生命周期

每个Janus插件都要实现一组固定的回调函数。Core会在不同时机调用它们:

回调函数 触发时机 说明
init() Janus启动时 插件初始化,加载配置
create_session() 客户端创建会话时 分配插件级别的资源
handle_message() 收到客户端消息时 处理业务逻辑,比如加入房间、发布流
setup_media() 媒体通道就绪时 开始接收/发送媒体数据
destroy_session() 会话结束时 清理资源

说白了,插件就是一个“事件处理器”。客户端发来一个JSON请求,插件解析后做出响应,然后通过Core把结果推回给客户端。

小技巧:调试插件时,可以开启Janus的debug日志。在启动命令加上 -d 4(日志级别4),就能看到插件和Core之间交互的完整JSON消息。我曾经靠这个定位过一个诡异的“房间创建失败”问题——最后发现是插件配置文件的JSON格式写错了。

15.2.2 插件与客户端的通信模型

客户端和插件之间的通信,走的是“请求-响应”+“事件推送”混合模式:

  • 请求-响应:客户端发一个{ "request": "join", "room": 1234 },插件处理完后回复{ "janus": "success", ... }
  • 事件推送:当房间里有其他人加入/离开,或者有人开始/停止推流时,插件主动向相关客户端推送事件。比如{ "event": "joined", "id": 5678 }

这种模型的好处是——客户端不需要轮询,实时性很好。坏处是——你得自己维护好会话状态,不然事件推送给谁都不知道。

15.3 视频会议插件(VideoRoom)实战

VideoRoom是Janus最常用的插件之一。它实现了一个SFU(Selective Forwarding Unit)模式的视频会议系统。每个参与者把自己的音视频流推送到服务器,服务器再转发给房间里的其他人。

我在项目中遇到过一个问题:当房间里有20个人同时开摄像头时,客户端的带宽直接爆了。后来才发现,VideoRoom默认会把所有人的视频流都转发给每个人。解决办法是开启simulcast(联播)或者VP9-SVC,让客户端根据自身带宽选择接收不同质量的流。

15.3.1 创建房间

首先,客户端需要向VideoRoom插件发送一个创建房间的请求:

{
  "janus": "message",
  "body": {
    "request": "create",
    "room": 1234,
    "description": "技术分享会议室",
    "publishers": 10,
    "bitrate": 1024000,
    "fir_freq": 10,
    "audiocodec": "opus",
    "videocodec": "vp8"
  }
}

参数说明:

  • room:房间号,唯一标识。
  • publishers:最大发布者数量。超过这个数,新人就进不来了。
  • bitrate:每个发布者的视频码率上限(单位bps)。
  • fir_freq:关键帧请求频率。值越小,延迟越低,但带宽消耗越大。

避坑指南:我曾经把publishers设成100,以为能支持100人同时开摄像头。结果Janus直接OOM了。后来查文档才发现,这个值不是房间总人数,而是同时推流的人数。如果你只需要少数人发言,其他人只观看,建议把publishers设小一点(比如5~10),然后通过subscribers参数控制观看人数。

15.3.2 加入房间并推流

创建好房间后,客户端就可以加入了。加入时需要指定自己的角色:

{
  "janus": "message",
  "body": {
    "request": "join",
    "room": 1234,
    "ptype": "publisher",
    "display": "张三"
  }
}

这里的ptype有两个选项:

  • publisher:发布者,把自己的音视频流推送到服务器。
  • subscriber:订阅者,只接收别人的流,自己不推流。

加入成功后,插件会返回一个id,这是你在房间里的唯一标识。然后你就可以开始推流了——其实就是标准的WebRTC流程:创建PeerConnection,添加音视频轨道,设置ICE候选,等待连接建立。

15.3.3 订阅别人的流

如果你是订阅者,或者想同时观看别人的流,需要发送订阅请求:

{
  "janus": "message",
  "body": {
    "request": "join",
    "room": 1234,
    "ptype": "subscriber",
    "feed": 5678
  }
}

feed就是你要订阅的那个发布者的id。VideoRoom会为每个订阅者创建一个独立的PeerConnection,专门用来接收这个发布者的流。

嗯,这里有个细节要注意:每个订阅者只能订阅一个发布者。如果你想同时看多个人,就得创建多个订阅者会话。这也是SFU的典型做法——每个订阅关系都是一条独立的媒体通道。

15.3.4 事件监听

作为客户端,你需要监听插件推送的事件。常见的事件有:

事件类型 触发条件 数据示例
joined 有人加入房间 { "id": 5678, "display": "李四" }
leaving 有人离开房间 { "id": 5678 }
unpublished 有人停止推流 { "id": 5678 }
talking 有人开始/停止说话 { "id": 5678, "talking": true }

这些事件非常有用。比如你可以根据talking事件,在UI上高亮当前说话的人。或者根据joined/leaving事件,动态更新参会者列表。

个人经验:我在做视频会议项目时,发现talking事件有时候会误触发(比如背景噪音大)。后来我在客户端加了一个“能量阈值”判断——只有当检测到的音频能量持续超过某个值超过500ms,才认为真的有人在说话。这样用户体验好了很多。

15.4 部署与配置要点

最后聊几个部署Janus时的实用配置。说实话,这些坑我都是踩过一遍才记住的。

15.4.1 核心配置(janus.jcfg)

general: {
  configs_folder = "/etc/janus"      # 配置文件目录
  plugins_folder = "/usr/lib/janus/plugins"  # 插件目录
  debug_level = 4                    # 调试级别(0-7)
}

nat: {
  stun_server = "stun.l.google.com"
  stun_port = 19302
  turn_server = "turn.example.com"
  turn_port = 3478
  turn_type = "udp"
  turn_user = "myuser"
  turn_pwd = "mypass"
}

media: {
  rtp_port_range = "20000-40000"     # RTP端口范围
  dtls_mtu = 1200                    # DTLS MTU,避免分片
}

这里重点说下rtp_port_range。Janus默认会占用大量UDP端口(每个媒体流至少需要2个端口)。如果你的服务器在NAT后面,记得在防火墙上开放这个端口范围。我曾经因为忘了开端口,折腾了一整天才发现是防火墙的问题。

15.4.2 VideoRoom插件配置(janus.plugin.videoroom.jcfg)

videoroom: {
  string_ids = true                  # 使用字符串ID
  admin_key = "supersecret"          # 管理员密钥
  lock_room = false                  # 是否锁定房间
  default_bitrate = 1024000          # 默认码率
  bitrate_cap = 5120000              # 码率上限
}

string_ids这个选项我强烈建议开启。默认情况下,房间号和用户ID都是数字,但数字ID在调试时很难一眼看出是谁。开启字符串ID后,你可以用“room_tech_share”这样的名字,清晰多了。

15.5 小结

Janus的架构设计非常优雅——Core负责脏活累活,插件专注业务逻辑。VideoRoom插件则是SFU模式的一个典型实现,适合中小规模的视频会议场景(我个人经验是50人以内效果不错,再往上就需要考虑集群部署了)。

如果你打算在生产环境使用Janus,建议先搭一个EchoTest插件测试网络连通性,再逐步切换到VideoRoom。这样排查问题会快很多。


公众号:蓝海资料掘金营,微信deep3321