Skip to content

AudioBridge ​

janus.plugin.audiobridge is an audio conference bridge: every participant sends one Opus stream to the server, the server mixes the streams, and each participant receives the mix of everyone else. You use it for voice conferences that need one stream down per listener instead of one per speaker, and to forward a mixed room to another program (a recorder, a broadcaster) over RTP.

The mixer does not send audio back yet

In this port a participant can join, appear in listparticipants, mute and leave. The mix does not come back to it: the browser's inbound-rtp stays at 0 packets (see the join transcript). Room management and RTP forwarders work. Read Known gaps before you build on it.

Enable and configure ​

Build with the plugin-audiobridge feature. The plugin reads janus.plugin.audiobridge.jcfg; each room-<id> block is a room that exists at startup:

text
general: {
}
room-1234: {
	description = "Demo Room"
	secret = "adminpwd"
	sampling_rate = 16000
	record = false
	use_limiter = false
}

sampling_rate is mandatory in a static room: a room without it is skipped with the log line Can't add the AudioBridge room, missing mandatory information.... Valid values are 8000, 12000, 16000, 24000 and 48000 (create answers 499 Unsupported sampling rate 12345 for others). The other keys, including pin, spatial_audio, allow_rtp_participants, mjrs and the rtp_forward_* keys that start a forwarder with the room, are in the AudioBridge config reference.

Try it ​

Manage rooms (answered at once) ​

list, exists, create, edit, allowed, listparticipants, rtp_forward, listforwarders, mute_room, kick, play_file, destroy and the other management requests answer in the same HTTP reply. The transcript also shows the requests the port does not implement yet (stop_rtp_forward, enable_recording) and a permanent room.

jsonc
// AudioBridge requests that are answered directly (synchronous): no PeerConnection needed.

// Room 1234 comes from janus.plugin.audiobridge.jcfg (secret adminpwd, sampling_rate 16000).


// → POST /janus
{
  "janus": "create",
  "transaction": "t-create"
}

// ← 200 OK
{
  "janus": "success",
  "transaction": "t-create",
  "data": {
    "id": 4100001234567908
  }
}

// → POST /janus/4100001234567908
{
  "janus": "attach",
  "plugin": "janus.plugin.audiobridge",
  "transaction": "t-attach"
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-attach",
  "data": {
    "id": 4100002469135799
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-1",
  "body": {
    "request": "list"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-1",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "success",
      "list": [
        {
          "room": 1234,
          "description": "Demo Room",
          "sampling_rate": 16000,
          "spatial_audio": false,
          "use_limiter": false,
          "pin_required": false,
          "record": false,
          "muted": false,
          "num_participants": 0
        }
      ]
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-2",
  "body": {
    "request": "exists",
    "room": 1234
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-2",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "success",
      "room": 1234,
      "exists": true
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-3",
  "body": {
    "request": "listparticipants",
    "room": 1234
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-3",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "participants",
      "room": 1234,
      "participants": []
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-4",
  "body": {
    "request": "create",
    "room": 5678,
    "description": "Docs bridge",
    "secret": "s3cret",
    "pin": "1111",
    "sampling_rate": 16000,
    "is_private": false
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-4",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "created",
      "room": 5678,
      "permanent": false
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-5",
  "body": {
    "request": "create",
    "room": 5678
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-5",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "event",
      "error_code": 486,
      "error": "Room 5678 already exists"
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-6",
  "body": {
    "request": "list"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-6",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "success",
      "list": [
        {
          "room": 1234,
          "description": "Demo Room",
          "sampling_rate": 16000,
          "spatial_audio": false,
          "use_limiter": false,
          "pin_required": false,
          "record": false,
          "muted": false,
          "num_participants": 0
        },
        {
          "room": 5678,
          "description": "Docs bridge",
          "sampling_rate": 16000,
          "spatial_audio": false,
          "use_limiter": false,
          "pin_required": true,
          "record": false,
          "muted": false,
          "num_participants": 0
        }
      ]
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-7",
  "body": {
    "request": "edit",
    "room": 5678,
    "secret": "s3cret",
    "new_description": "Renamed bridge"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-7",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "edited",
      "room": 5678,
      "permanent": false
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-8",
  "body": {
    "request": "edit",
    "room": 5678,
    "secret": "wrong",
    "new_description": "x"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-8",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "event",
      "error_code": 489,
      "error": "Unauthorized (wrong secret)"
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-9",
  "body": {
    "request": "allowed",
    "room": 5678,
    "secret": "s3cret",
    "action": "add",
    "allowed": [
      "token-a"
    ]
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-9",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "success",
      "room": 5678,
      "allowed": [
        "token-a"
      ]
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-10",
  "body": {
    "request": "rtp_forward",
    "room": 5678,
    "secret": "s3cret",
    "host": "127.0.0.1",
    "port": 5002,
    "codec": "opus",
    "ptype": 100,
    "ssrc": 1234
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-10",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "success",
      "room": 5678,
      "stream_id": 2007654332,
      "host": "127.0.0.1",
      "port": 5002
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-11",
  "body": {
    "request": "listforwarders",
    "room": 5678,
    "secret": "s3cret"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-11",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "forwarders",
      "room": 5678,
      "rtp_forwarders": [
        {
          "stream_id": 2007654332,
          "ip": "127.0.0.1",
          "port": 5002,
          "ssrc": 1234,
          "codec": "opus",
          "ptype": 100,
          "always_on": false
        }
      ]
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-12",
  "body": {
    "request": "stop_rtp_forward",
    "room": 5678,
    "secret": "s3cret",
    "stream_id": 1
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-12",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "event",
      "error_code": 499,
      "error": "Request 'stop_rtp_forward' is not implemented by this port"
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-13",
  "body": {
    "request": "mute_room",
    "room": 5678,
    "secret": "s3cret"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-13",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "success",
      "room": 5678
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-14",
  "body": {
    "request": "unmute_room",
    "room": 5678,
    "secret": "s3cret"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-14",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "success",
      "room": 5678
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-15",
  "body": {
    "request": "kick",
    "room": 5678,
    "secret": "s3cret",
    "id": 99
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-15",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "event",
      "error_code": 492,
      "error": "No such user 99 in room 5678"
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-16",
  "body": {
    "request": "listannouncements",
    "room": 5678,
    "secret": "s3cret"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-16",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "announcements",
      "room": 5678,
      "announcements": []
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-17",
  "body": {
    "request": "play_file",
    "room": 5678,
    "secret": "s3cret",
    "filename": "/nonexistent.opus",
    "file_id": "intro"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-17",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "success",
      "room": 5678,
      "file_id": "intro"
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-18",
  "body": {
    "request": "enable_recording",
    "room": 5678,
    "secret": "s3cret",
    "record": true
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-18",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "event",
      "error_code": 499,
      "error": "Request 'enable_recording' is not implemented by this port"
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-19",
  "body": {
    "request": "resetdecoder"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-19",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "event",
      "error_code": 487,
      "error": "Can't reset (not in a room)"
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-20",
  "body": {
    "request": "destroy",
    "room": 5678,
    "secret": "s3cret"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-20",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "destroyed",
      "room": 5678,
      "permanent": false
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-21",
  "body": {
    "request": "destroy",
    "room": 5678,
    "secret": "s3cret"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-21",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "event",
      "error_code": 485,
      "error": "No such room (5678)"
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-22",
  "body": {
    "request": "bogus"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-22",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "event",
      "error_code": 482,
      "error": "Unknown request 'bogus'"
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-23",
  "body": {
    "request": "create",
    "room": 5679,
    "permanent": true
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-23",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "event",
      "error_code": 499,
      "error": "No configuration file, can't create permanent room"
    }
  }
}

Join with a browser ​

join, configure, changeroom and leave are asynchronous: you get an ack, then an event with the same transaction. The browser's SDP offer goes in the jsep of join (or of a later configure), and the plugin answers in the joined event. The transcript was recorded with two real PeerConnections in headless Chrome. Look at the end: the last note is the browser's own getStats() after the join.

jsonc
// Two browsers join room 1234 with an audio-only offer. SDP is shortened.

// join carries the offer. The plugin answers with `joined` and the JSEP answer in one event.


// → POST /janus
{
  "janus": "create",
  "transaction": "t-create"
}

// ← 200 OK
{
  "janus": "success",
  "transaction": "t-create",
  "data": {
    "id": 4100001234567908
  }
}

// → POST /janus/4100001234567908
{
  "janus": "attach",
  "plugin": "janus.plugin.audiobridge",
  "transaction": "t-attach"
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-attach",
  "data": {
    "id": 4100002469135799
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-1",
  "body": {
    "request": "join",
    "room": 1234,
    "display": "Alice",
    "muted": false
  },
  "jsep": {
    "type": "offer",
    "sdp": "v=0\r\no=- 6100000000001000000 2 IN IP4 127.0.0.1\r\ns=-\r\n…"
  }
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 4100001234567908,
  "transaction": "t-1"
}

// ⇠ event, long poll GET /janus/<session>
{
  "janus": "event",
  "session_id": 4100001234567908,
  "transaction": "t-1",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "joined",
      "room": 1234,
      "id": 4100003703703690,
      "participants": []
    }
  },
  "jsep": {
    "type": "answer",
    "sdp": "v=0\r\no=- 4100004938271581 1 IN IP4 192.0.2.10\r\ns=AudioBridge 1234\r\n…"
  }
}

// ⇠ event, long poll GET /janus/<session>
{
  "janus": "webrtcup",
  "session_id": 4100001234567908,
  "sender": 4100002469135799
}

// → POST /janus/4100001234567908
{
  "janus": "attach",
  "plugin": "janus.plugin.audiobridge",
  "transaction": "t-attach"
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-attach",
  "data": {
    "id": 4100006172839472
  }
}

// → POST /janus/4100001234567908/4100006172839472
{
  "janus": "message",
  "transaction": "t-1",
  "body": {
    "request": "join",
    "room": 1234,
    "display": "Bob"
  },
  "jsep": {
    "type": "offer",
    "sdp": "v=0\r\no=- 6100000000002000000 2 IN IP4 127.0.0.1\r\ns=-\r\n…"
  }
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 4100001234567908,
  "transaction": "t-1"
}

// ⇠ event, long poll GET /janus/<session>
{
  "janus": "event",
  "session_id": 4100001234567908,
  "transaction": "t-1",
  "sender": 4100006172839472,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "joined",
      "room": 1234,
      "id": 4100007407407363,
      "participants": [
        {
          "id": 4100003703703690,
          "display": "Alice",
          "setup": true,
          "muted": false,
          "talking": false
        }
      ]
    }
  },
  "jsep": {
    "type": "answer",
    "sdp": "v=0\r\no=- 4100008641975254 1 IN IP4 192.0.2.10\r\ns=AudioBridge 1234\r\n…"
  }
}

// ⇠ event, long poll GET /janus/<session>
{
  "janus": "event",
  "session_id": 4100001234567908,
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "joined",
      "room": 1234,
      "participants": [
        {
          "id": 4100007407407363,
          "display": "Bob",
          "setup": false,
          "muted": false
        }
      ]
    }
  }
}

// → POST /janus/4100001234567908/4100006172839472
{
  "janus": "message",
  "transaction": "t-2",
  "body": {
    "request": "listparticipants",
    "room": 1234
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-2",
  "sender": 4100006172839472,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "participants",
      "room": 1234,
      "participants": [
        {
          "id": 4100007407407363,
          "display": "Bob",
          "setup": false,
          "muted": false,
          "talking": false
        },
        {
          "id": 4100003703703690,
          "display": "Alice",
          "setup": true,
          "muted": false,
          "talking": false
        }
      ]
    }
  }
}

// → POST /janus/4100001234567908/4100006172839472
{
  "janus": "message",
  "transaction": "t-3",
  "body": {
    "request": "configure",
    "muted": true
  }
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 4100001234567908,
  "transaction": "t-3"
}

// ⇠ event, long poll GET /janus/<session>
{
  "janus": "event",
  "session_id": 4100001234567908,
  "transaction": "t-3",
  "sender": 4100006172839472,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "event",
      "result": "ok"
    }
  }
}

// ⇠ event, long poll GET /janus/<session>
{
  "janus": "event",
  "session_id": 4100001234567908,
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "event",
      "room": 1234,
      "participants": [
        {
          "id": 4100007407407363,
          "display": "Bob",
          "setup": false,
          "muted": true
        }
      ]
    }
  }
}

// → POST /janus/4100001234567908/4100006172839472
{
  "janus": "message",
  "transaction": "t-4",
  "body": {
    "request": "leave"
  }
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 4100001234567908,
  "transaction": "t-4"
}

// ⇠ event, long poll GET /janus/<session>
{
  "janus": "event",
  "session_id": 4100001234567908,
  "transaction": "t-4",
  "sender": 4100006172839472,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "left",
      "room": 1234,
      "id": 4100007407407363
    }
  }
}

// ⇠ event, long poll GET /janus/<session>
{
  "janus": "event",
  "session_id": 4100001234567908,
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "event",
      "room": 1234,
      "leaving": 4100007407407363
    }
  }
}


// Browser stats for alice a few seconds after the join (RTCPeerConnection.getStats):

//   outbound-rtp packetsSent = 165, inbound-rtp packetsReceived = 0

// A working mixer sends the mix back, so packetsReceived would grow. Here it stays 0 (known gap).


// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-2",
  "body": {
    "request": "leave"
  }
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 4100001234567908,
  "transaction": "t-2"
}

// ⇠ event, long poll GET /janus/<session>
{
  "janus": "event",
  "session_id": 4100001234567908,
  "transaction": "t-2",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "left",
      "room": 1234,
      "id": 4100003703703690
    }
  }
}

// Join errors: wrong PIN, no such room, and a plain RTP join (no PeerConnection) in a room that allows it and in one that does not.


// → POST /janus/4100001234567908
{
  "janus": "attach",
  "plugin": "janus.plugin.audiobridge",
  "transaction": "t-attach"
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-attach",
  "data": {
    "id": 4100009876543145
  }
}

// → POST /janus/4100001234567908/4100009876543145
{
  "janus": "message",
  "transaction": "t-1",
  "body": {
    "request": "create",
    "room": 6000,
    "description": "Pinned bridge",
    "pin": "1111",
    "secret": "s3cret"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-1",
  "sender": 4100009876543145,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "created",
      "room": 6000,
      "permanent": false
    }
  }
}

// → POST /janus/4100001234567908/4100009876543145
{
  "janus": "message",
  "transaction": "t-2",
  "body": {
    "request": "create",
    "room": 6001,
    "description": "Plain RTP bridge",
    "allow_rtp_participants": true
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-2",
  "sender": 4100009876543145,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "created",
      "room": 6001,
      "permanent": false
    }
  }
}

// → POST /janus/4100001234567908
{
  "janus": "attach",
  "plugin": "janus.plugin.audiobridge",
  "transaction": "t-attach"
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-attach",
  "data": {
    "id": 4100011111111036
  }
}

// → POST /janus/4100001234567908/4100011111111036
{
  "janus": "message",
  "transaction": "t-1",
  "body": {
    "request": "join",
    "room": 6000,
    "display": "Carol",
    "pin": "wrong"
  }
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 4100001234567908,
  "transaction": "t-1"
}

// ⇠ event, long poll GET /janus/<session>
{
  "janus": "event",
  "session_id": 4100001234567908,
  "transaction": "t-1",
  "sender": 4100011111111036,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "event",
      "error_code": 489,
      "error": "Unauthorized (wrong pin)"
    }
  }
}

// → POST /janus/4100001234567908/4100011111111036
{
  "janus": "message",
  "transaction": "t-2",
  "body": {
    "request": "join",
    "room": 99
  }
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 4100001234567908,
  "transaction": "t-2"
}

// ⇠ event, long poll GET /janus/<session>
{
  "janus": "event",
  "session_id": 4100001234567908,
  "transaction": "t-2",
  "sender": 4100011111111036,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "event",
      "error_code": 485,
      "error": "No such room (99)"
    }
  }
}

// → POST /janus/4100001234567908/4100011111111036
{
  "janus": "message",
  "transaction": "t-3",
  "body": {
    "request": "join",
    "room": 1234,
    "display": "Carol",
    "rtp": {
      "ip": "127.0.0.1",
      "port": 6000,
      "payload_type": 100
    }
  }
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 4100001234567908,
  "transaction": "t-3"
}

// ⇠ event, long poll GET /janus/<session>
{
  "janus": "event",
  "session_id": 4100001234567908,
  "transaction": "t-3",
  "sender": 4100011111111036,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "event",
      "error_code": 489,
      "error": "Plain RTP participants not allowed in this room"
    }
  }
}

// → POST /janus/4100001234567908/4100011111111036
{
  "janus": "message",
  "transaction": "t-4",
  "body": {
    "request": "join",
    "room": 6001,
    "display": "Carol",
    "rtp": {
      "ip": "127.0.0.1",
      "port": 6000,
      "payload_type": 100
    }
  }
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 4100001234567908,
  "transaction": "t-4"
}

// ⇠ event, long poll GET /janus/<session>
{
  "janus": "event",
  "session_id": 4100001234567908,
  "transaction": "t-4",
  "sender": 4100011111111036,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "joined",
      "room": 6001,
      "id": 4100012345678927,
      "participants": []
    }
  }
}

The steps:

  1. Create the PeerConnection, add an audio track, create the offer (wait for ICE gathering or send trickle candidates).
  2. Send {"request": "join", "room": 1234, "display": "Alice"} with jsep set to the offer.
  3. Apply the jsep answer in the joined event. The event also carries your participant id and the list of participants already in the room.
  4. After webrtcup, send configure (without a jsep) to mute and unmute, and leave to leave. The other participants get joined, event (with participants for a mute, or leaving for a leave) notifications.

To make the plugin send the offer instead, put "generate_offer": true in join, apply its offer, and send your answer in a configure. This is verified: the joined event then carries a JSEP offer.

Forward the mix over RTP ​

rtp_forward sends the room's mix to any UDP address as RTP, which is how you feed ffmpeg or GStreamer. This transcript starts a UDP listener on this machine, adds a forwarder with always_on, and counts what arrives.

jsonc
// rtp_forward sends the mix of a room to a UDP address. A UDP socket on 127.0.0.1:5002 (a stand-in for ffmpeg or GStreamer) listens.

// always_on makes the forwarder send silence while the room is empty.


// → POST /janus
{
  "janus": "create",
  "transaction": "t-create"
}

// ← 200 OK
{
  "janus": "success",
  "transaction": "t-create",
  "data": {
    "id": 4100001234567908
  }
}

// → POST /janus/4100001234567908
{
  "janus": "attach",
  "plugin": "janus.plugin.audiobridge",
  "transaction": "t-attach"
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-attach",
  "data": {
    "id": 4100002469135799
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-1",
  "body": {
    "request": "rtp_forward",
    "room": 1234,
    "secret": "adminpwd",
    "host": "127.0.0.1",
    "port": 5002,
    "codec": "opus",
    "ptype": 100,
    "ssrc": 1234,
    "always_on": true
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-1",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "success",
      "room": 1234,
      "stream_id": 2007654332,
      "host": "127.0.0.1",
      "port": 5002
    }
  }
}

// The listener received 100 packets in 2 seconds (an empty room, so silence).

// First packet: 20 bytes, RTP version 2, payload type 100, sequence 1, SSRC 1234.


// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-2",
  "body": {
    "request": "listforwarders",
    "room": 1234,
    "secret": "adminpwd"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-2",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.audiobridge",
    "data": {
      "audiobridge": "forwarders",
      "room": 1234,
      "rtp_forwarders": [
        {
          "stream_id": 2007654332,
          "ip": "127.0.0.1",
          "port": 5002,
          "ssrc": 1234,
          "codec": "opus",
          "ptype": 100,
          "always_on": true
        }
      ]
    }
  }
}

Reference ​

All requests have "request": "<name>". The reply key is audiobridge. Parameters not marked required are optional. Rows that no transcript above shows are taken from the C documentation and the Rust request table; they were not all run.

Synchronous requests ​

RequestParametersReply
createroom (number, random if missing), permanent, description, secret, pin, is_private, allowed (tokens), sampling_rate (default 16000), spatial_audio, use_limiter, audiolevel_ext, audiolevel_event, audio_active_packets, audio_level_average, default_expectedloss, default_bitrate, denoise, record, record_file, record_dir, mjrs, mjrs_dir, allow_rtp_participants, groups, admin_key{"audiobridge": "created", "room", "permanent"}
editroom, secret, new_description, new_secret, new_pin, new_is_private, new_record_dir, new_mjrs_dir, permanent{"audiobridge": "edited", "room", "permanent"}
destroyroom, secret, permanent{"audiobridge": "destroyed", "room", "permanent"}; participants get a destroyed event
existsroom{"audiobridge": "success", "room", "exists"}
listnone{"audiobridge": "success", "list": [{room, description, sampling_rate, spatial_audio, use_limiter, pin_required, record, muted, num_participants}]}
listparticipantsroom{"audiobridge": "participants", "room", "participants": [{id, display, setup, muted, talking}]}
allowedroom, secret, action (enable, disable, add, remove), allowed (tokens){"audiobridge": "success", "room", "allowed": […]}
kick, kick_allroom, secret, id (for kick){"audiobridge": "success"}
mute, unmuteroom, secret, id{"audiobridge": "success"}
mute_room, unmute_roomroom, secret{"audiobridge": "success", "room"}
suspend, resumeroom, secret, id, plus pause_events, stop_record (suspend) or record, filename (resume){"audiobridge": "success"}
resetdecodernone (on a joined handle){"audiobridge": "success"}; error 487 if you are not in a room
rtp_forwardroom, secret (or admin_key if configured), host, port, codec (opus, pcma, pcmu), ptype, ssrc, group, host_family, srtp_suite, srtp_crypto, always_on{"audiobridge": "success", "room", "stream_id", "host", "port"}
listforwardersroom, secret{"audiobridge": "forwarders", "room", "rtp_forwarders": [{stream_id, ip, port, ssrc, codec, ptype, always_on}]}
stop_rtp_forwardroom, secret, stream_idnot implemented, see below
play_file, is_playing, stop_file, stop_all_files, listannouncementsroom, secret, plus file_id, filename, loop, groupsuccess, announcements; see below
enable_recording, enable_mjrs, denoise_enable, denoise_disablesee the C docsenable_recording, denoise_enable and denoise_disable are not implemented

listannouncements and play_file need the room secret when the room has one: without it the answer is 483 Missing mandatory element (secret).

Asynchronous requests ​

RequestParametersEvents
joinroom (required), id (unique participant id; random if missing), display, pin, token, muted, suspended, pause_events, codec (opus, pcma, pcmu), bitrate, quality (0 to 10), expected_loss (0 to 20), volume (percent), spatial_position (0 to 100), denoise, secret (makes you an admin: not muted by mute_room), group, record, filename, generate_offer, rtp (plain RTP participant, see below), audio_level_average, audio_active_packetsto you: {"audiobridge": "joined", "room", "id", "participants": […]} with the JSEP answer; to the others: joined with the new participant in participants
configuremuted, display, bitrate, quality, expected_loss, volume, spatial_position, denoise, record, filename, group; with a jsep for a renegotiation or ICE restartto you: {"audiobridge": "event", "result": "ok"}; to the others, if muted changed: event with participants
changeroomlike join, no jsep and no generate_offerto you: roomchanged; to both rooms: leave and join notifications
leavenoneto you: {"audiobridge": "left", "room", "id"}; to the others: {"audiobridge": "event", "room", "leaving": id}

The joined event has no display of its own: you chose it. Other participants appear in participants with id, display, setup (has a PeerConnection), muted and, in listparticipants, talking.

Error codes ​

CodeMeaning
480 to 484No message, invalid JSON, unknown request, missing element, invalid element
485, 486No such room, room exists
487, 488Not in a room, libopus error
489Unauthorized (wrong secret, PIN or token; also a plain RTP join into a room that does not allow it)
490, 491, 492Participant id exists, already in the room, no such participant
493, 494Invalid SDP, no such group
499Other error, including "not implemented by this port"

Behaviour you may not expect ​

  • list answers with the key list. The C doxygen text says rooms; the C server and this port both send list.
  • In the recorded listparticipants replies each entry has id, display, setup, muted and talking. list includes muted, the room-wide mute from mute_room.
  • rtp_forward sends from the moment the forwarder exists: 50 packets per second, payload type 100, 20 bytes each for silence. It does not need a participant if always_on is true.
  • play_file with a path that does not exist still answers success with the file_id: the reply does not check the file.
  • A room created with "permanent": true answers 499 No configuration file, can't create permanent room, because writing the room back to janus.plugin.audiobridge.jcfg is not ported.

Known gaps ​

  • fails The mixer sends 0 packets to participants. Verified: after a join, getStats() in Chrome shows outbound-rtp growing and no inbound-rtp (transcript above). The mixer, decoder and encoder exist in the crate (plugins/audiobridge/src/frame.rs, opus.rs), but the mix is not sent on the participant's PeerConnection. Janus Bench audiobridge.mix fails for this reason.
  • gap Plain RTP participants. A join with an rtp object into a room created with allow_rtp_participants: true answers joined, but the event has no rtp object (the address and port to send to): the plain RTP sockets are not ported, so nothing can be sent or received.
  • gap Not implemented requests answer error 499 Request '<name>' is not implemented by this port: stop_rtp_forward, enable_recording, denoise_enable, denoise_disable. A forwarder cannot be stopped except by destroying the room.
  • gap Permanent rooms (above), and RNNoise denoising.
  • Opus FEC decoding and some encoder details are open in parity/status.json.
  • Static rtp_forward_* keys are read; room recording to WAV and per-participant MJR files are ported in the crate but not exercised here.

Test it in Janus Bench ​

Open the AudioBridge page, or run audiobridge.mix from Run all checks. It joins room 1234, lists participants, mutes and leaves, and waits for mixed audio. Result today: fails at the step "mixed audio arrives" (0 packets received).

GPL-3.0-or-later, like the Janus gateway it ports.