2025年6月9日月曜日

janusのadmin apiを利用してiceの接続状態を確認する


背景

janus(やぬす)とはWebRTCの通信の中継を行ってくれるサーバーのプログラムです。
janusには管理用としてadmin apiがあり、それを利用すると利用状況を確認できます。
janusのフォーラムで不具合時の情報収集にadmin apiを利用して状況確認を促されている投稿を何度か見かけたので、使い方を大まかに把握しました。
ice(Interactive Connectivity Establishment)の接続状況確認を例にして、備忘録として記事に関連情報を残します。

主に下記のサイトを参考にしました。
Janus gatewayのAdmin APIを使ってみる
Admin/Monitor API

使ったもの

  • janus 1.3.1
  • python 3
  • websockets 15.0.1
    pipでインストールしました。
    pip install websockets

janusでadmin api有効化

admin apiは標準設定では有効化されていないので設定を変更します。

janus.jcfgのgeneralでパスワードを設定します。
janus.jcfg
general: {
admin_secret = "admin_secret_for_testing" # change this
}

今回はwebsocketを介してadmin apiを使うので、websocketの設定でadminを有効にします。
janus.transport.websockets.jcfg
admin: {
admin_ws = true
admin_ws_port = 7188
admin_wss = false
}
janusを利用してtls対応するなら、上記の設定に加えてそれ用の設定や鍵の配置が必要です。
自分はtls(wss)化はプロキシサーバー(nginx)に任せているので、ここではtls無しの設定のみ紹介しています。

上記の設定を施しjanusを再起動すると、admin apiが利用可能になります。

pythonのwebsocketsで情報を取得

下記のプログラムを実行すると、localhostで実行しているjanus情報を取得できます。
public ipを割り当てたサーバーで動かすjanusの様子を見たい場合は janus_admin_url と janus_admin_secret を書き換えてください。
main.py
#!/usr/bin/python

from websockets.sync.client import connect
from websockets.typing import Subprotocol
import json
import random
import string
from copy import copy

janus_admin_url = 'ws://localhost:7188/janus'
janus_admin_secret = "admin_secret_for_testing"

# janus_admin_url = 'wss://your-server-domain.com:7989/janus'
# janus_admin_secret = 'your-password-of-admin-secret-on-server'

def create_random_string(length, seq=string.digits):
sr = random.SystemRandom()
return ''.join([sr.choice(seq) for i in range(length)])

# https://janus.discourse.group/t/admin-api-via-websockets/1584/3
# https://janus.conf.meetecho.com/docs/admin.html
websock = connect(janus_admin_url, subprotocols=[Subprotocol("janus-admin-protocol")])

transaction = create_random_string(8)
data = {"janus":'list_sessions', "admin_secret": janus_admin_secret, "transaction": transaction}
websock.send(json.dumps(data))
received_json = websock.recv()
print(received_json)
decoded_json = json.loads(received_json)
# print(decoded_json['sessions'])
for session_id in decoded_json['sessions']:
data_for_session = copy(data)
data_for_session["janus"] = "list_handles"
data_for_session["session_id"] = session_id
print("session_id: %ld" % session_id)
websock.send(json.dumps(data_for_session))
received_json = websock.recv()
print(received_json)
decoded_json = json.loads(received_json)
for handle_id in decoded_json['handles']:
data_for_handle = copy(data_for_session)
data_for_handle["janus"] = "handle_info"
data_for_handle["handle_id"] = handle_id
print("handle_id: %ld" % handle_id)
websock.send(json.dumps(data_for_handle))
received_json = websock.recv()
print(received_json)
print(json.dumps(json.loads(received_json)["info"]["webrtc"]["ice"], indent=2))

上記のプログラムを実行すると、それぞれの要求で取得した情報と、handleに含まれるiceの情報が表示されます。
下記は取得できる情報の例です。
内容は後ほど解説します。
{
"stream_id": 1,
"component_id": 1,
"state": "ready",
"gathered": 34493264,
"connected": 34816217,
"local-candidates": [
"1 1 udp 2015363327 172.28.0.4 52189 typ host"
],
"remote-candidates": [
"remote1 1 udp 1845501695 172.28.0.1 52776 typ prflx raddr 172.28.0.1 rport 52776\r\n"
],
"selected-pair": "172.28.0.4:52189 [host,udp] <-> 172.28.0.1:52776 [prflx,udp]",
"ready": 1
}

要所を解説します。

websocketの接続はadmin apiの説明で指示されている通り、subprotocolとしてjanus-admin-protocolを設定します。
websock = connect(janus_admin_url, subprotocols=[Subprotocol("janus-admin-protocol")])


iceの状況はsessionのhandleに含まれる info -> webrtc -> iceで見れるので、下記の順序で情報を取得します。
  1. session一覧を取得
  2. sessionのhandle一覧を取得
  3. handleからiceの情報を取得
data = {"janus":'list_sessions', "admin_secret": janus_admin_secret, "transaction": transaction}
websock.send(json.dumps(data))
received_json = websock.recv()
decoded_json = json.loads(received_json)
for session_id in decoded_json['sessions']:
data_for_session["janus"] = "list_handles"
websock.send(json.dumps(data_for_session))
received_json = websock.recv()
decoded_json = json.loads(received_json)
for handle_id in decoded_json['handles']:
data_for_handle["janus"] = "handle_info"
websock.send(json.dumps(data_for_handle))
received_json = websock.recv()
print(json.dumps(json.loads(received_json)["info"]["webrtc"]["ice"], indent=2))

websocketの接続なものの要求の度にパスワードとtransactionの入力や、要求の階層が深くなれば関連する情報(session_idなど)が必要なので、親要素のobjectをコピーして使っています
data = {"janus":'list_sessions', "admin_secret": janus_admin_secret, "transaction": transaction}
websock.send(json.dumps(data))
for session_id in decoded_json['sessions']:
data_for_session = copy(data)
data_for_session["janus"] = "list_handles"
data_for_session["session_id"] = session_id
websock.send(json.dumps(data_for_session))
for handle_id in decoded_json['handles']:
data_for_handle = copy(data_for_session)
data_for_handle["janus"] = "handle_info"
data_for_handle["handle_id"] = handle_id
websock.send(json.dumps(data_for_handle))

ice接続情報の内容

取得したiceの情報を残します。

localhostで動かすjanus

janus.jcfg: 設定なし

local-candidatesとremote-candidatesとselected-pairに謎の172始まりのアドレスが記されていました。
PCのLANのIPは192始まりなので謎ですが、この状態で同一PCでjanusの起動、ffmpegを利用したwebカメラの配信、ブラウザでの閲覧ができました。
{
"stream_id": 1,
"component_id": 1,
"state": "ready",
"gathered": 34493264,
"connected": 34816217,
"local-candidates": [
"1 1 udp 2015363327 172.28.0.4 52189 typ host"
],
"remote-candidates": [
"remote1 1 udp 1845501695 172.28.0.1 52776 typ prflx raddr 172.28.0.1 rport 52776\r\n"
],
"selected-pair": "172.28.0.4:52189 [host,udp] <-> 172.28.0.1:52776 [prflx,udp]",
"ready": 1
}

public ipで動かすjanus

接続通信成功時

janus.jcfg: natで nat_1_1_mapping と ignore_mdns を設定。
janus.jcfg
nat: {
nat_1_1_mapping = "3.130.222.222" # public ip of your server
ignore_mdns = true
}
ignore_mdnsは無くても動くのですが無効化していないとmdnsが扱えなかった警告がログに出るので、public ipで使う場合は無効化しておくと不要なログを減らせて良いです。

admin apiで祝した情報には、janus.jcfgで設定したpublic ipがlocal-candidatesのudpのアドレスとして指定されたと分かります。
{
"stream_id": 1,
"component_id": 1,
"state": "ready",
"gathered": 29417410098,
"connected": 29418227869,
"local-candidates": [
"1 1 udp 2015363327 3.130.222.222 5077 typ host"
],
"remote-candidates": [
"remote1 1 udp 1845501695 210.157.193.13 30547 typ prflx raddr 210.157.193.13 rport 30547\r\n",
"3953914067 1 udp 1677729535 210.157.193.13 15140 typ srflx raddr 0.0.0.0 rport 0 generation 0 ufrag m+mG network-cost 999",
"3480043913 1 udp 1677732095 240b:c010:602:384a:9de8:8e0b:8b:dd6b 58682 typ srflx raddr :: rport 0 generation 0 ufrag m+mG network-cost 999"
],
"selected-pair": "3.130.222.222:5077 [prflx,udp] <-> 210.157.193.13:30547 [prflx,udp]",
"ready": 1
}

接続失敗時

janus.jcfg: nat_1_1_mapping 未設定

下記のログに悩まされたときの状態です。
[WARN] [1611011328540253] ICE failed for component 1 in stream 1, but let's give it some time... (trickle received, answer received, alert not set)

取得したiceの情報にpublic ipがどこにも含まれていないため、nat_1_1_mapping でのpublic ipの指定が出来ていないと分かります。
{
"stream_id": 1,
"component_id": 1,
"state": "connecting",
"gathered": 12869731,
"local-candidates": [
"1 1 udp 2015363327 172.18.0.4 5006 typ host"
],
"remote-candidates": [
"3441168726 1 udp 1677732095 240b:c010:602:384a:9de8:8e0b:8b:dd6b 54470 typ srflx raddr :: rport 0 generation 0 ufrag l8me network-cost 999",
"3205426597 1 udp 1677729535 210.157.193.13 15141 typ srflx raddr 0.0.0.0 rport 0 generation 0 ufrag l8me network-cost 999"
],
"ready": 1
}

janusはjcfgに不備があっても不備以後を無視して起動する仕様なので、jcfgファイルに nat_1_1_mapping を記述しているにも関わらず反映されてない場合は記述箇所かそれより前に不備がある可能性があります。

不備があったら起動時のログに示されるので、下記のようなエラーが発生していないか確認するのが良いです。
下記ののログは「/opt/janus/etc/janus/janus.jcfg」の81行目に異常があることを知らせています。
[ERR] [config.c:janus_config_parse:205] Error parsing config file at line 81: syntax error
Failed to load /opt/janus/etc/janus/janus.jcfg, trying the INI instead...
[ERR] [config.c:janus_config_parse:191] -- Error reading configuration file 'janus.cfg'... error 2 (No such file or directory)
Error reading/parsing the configuration file in /opt/janus/etc/janus, going on with the defaults and the command line arguments

おわり

janusのadmin apiを利用してiceの接続状態を確認できました。
期待するように動かない場合は不具合特定のいとぐちになるかもしれません。

参考

Janus gatewayのAdmin APIを使ってみる
Admin/Monitor API
Admin API via Websockets

0 件のコメント :