管理 API と SDK

SENCA を操作する HTTP の管理 API(/api/v1)の使い方です。認証、権限、Idempotency-Key、ETag、ページング、エラーといった共通のルールと、OpenAPI の定義、イベント(SSE)、JavaScript の SDK、Rust のクライアントを説明します。

API と SDK

このページの見出し
  1. 共通のルール
  2. OpenAPI の定義
  3. イベント(SSE)
  4. JavaScript の SDK(@senca/sdk)
  5. Rust のクライアント(senca-client)
  6. 関連するページ

SENCA のノードは、HTTP の管理 API(/api/v1)で操作します。管理画面(/console/)も裏では同じ API を呼んでいます。 ここでは、どのエンドポイントにも共通する決まりごとと、イベント(SSE)、JavaScript の SDK(@senca/sdk)、Rust のクライアント(senca-client)の使い方を説明します。

エンドポイントごとのメソッド・パス・必要な権限は API リファレンス に、ルームのシグナリング(WebSocket のメッセージ形式)は シグナリング にあります。 API の定義ファイルは docs/api/openapi.yaml で、ノードの実際の動作に合わせて書いています。 リソースごとの使い方は、それぞれのページを見てください。配信と再生は 取り込みと再生、録画は 録画、 入力・出力は 入力と出力、パイプラインは パイプライン、ノードとグリッドは クラスターとグリッド、ルームは リアルタイムのルーム、メディアパイプラインは メディアパイプライン です。

このページの要点:

  • すべての /api/v1 に Authorization: Bearer が必要です。テナントは認証情報から決まります。権限(スコープ)が足りなければ 403 になります。
  • 未知のフィールドや未知のクエリを送ると 400 になります(黙って無視することはしません)。
  • 作成の再送には Idempotency-Key、更新の衝突防止には ETag と If-Match(PATCH では必須)を使います。
  • エラーは problem+json で返り、プログラムでは code で判別します。retryable が true なら再送してかまいません。
  • 変更は GET /api/v1/events(SSE)で受け取れます。JavaScript と Rust のクライアントを使えば、これらの決まりを自動で処理してくれます。

共通のルール

基本

項目値
ベースパス/api/v1(--http で指定したアドレスで待ち受けます。--https を指定すれば TLS でも同じパスです)
本文JSON(UTF-8)。時刻は UTC の RFC 3339(2026-10-25T00:00:00.000Z)
本文の上限1 MiB。超えると、problem+json ではなく text/plain の 413 を返して接続を閉じます
応答ヘッダーすべての応答に X-Request-Id と Cache-Control: no-store が付きます
X-Request-Idリクエストの X-Request-Id が 64 文字以内の英数字と - _ . であれば、その値を使います。そうでなければ req_... の形の値を作ります
パス空の区切り(//)・.・.. を含むパスは 404 になります
メソッド既知のリソースに定義されていないメソッド(HEAD を含む)は 405(METHOD_NOT_ALLOWED)になります。未知のパスは 404 です
CORS/api/v1 は CORS ヘッダーを返しません。ブラウザーからは、同じオリジン(管理画面など)か、アプリのサーバーを経由して呼び出してください

認証

すべての /api/v1(/capabilities を含む)に Authorization: Bearer <値> ヘッダーが必要です。/healthz・/readyz・署名付きの再生パス (/s/...)には不要です。

値が senca_ で始まれば API トークン、それ以外は JWT として扱います。値の長さは 8192 バイトまでです。 認証に失敗すると 401(AUTH_FAILED、WWW-Authenticate: Bearer)が返ります。期限切れ・未知のトークンといった失敗の理由は応答に含まれず、ノードのログにだけ記録されます。

API トークン

最初にノードを起動したとき、トークンが 1 つも無ければ、ノードは tenant:admin と node:admin を持つトークン(名前 bootstrap-admin、 テナント default、期限なし)を作り、値を {data-dir}/control/bootstrap-admin-token(パーミッション 0600)に書き出します。クラスターでは ID が最も小さいノードがこれを作ります。

以降のトークンは、このトークンを使って POST /api/v1/tokens(tenant:admin)で作ります。値(token)が返るのは作成時の応答だけで、サーバーにはハッシュしか残りません。

TOKEN=$(cat data/control/bootstrap-admin-token)
curl -s -X POST http://127.0.0.1:8080/api/v1/tokens \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"ci","scopes":["stream:read","stream:write"],"expires_at":"2027-01-01T00:00:00Z"}'
フィールドルール
name1〜64 文字(違反すると 400 INVALID_CONFIG)
scopes1 つ以上。未知の権限を指定すると 400 になります。自分が持っていない権限は付与できません(403 FORBIDDEN)
expires_at任意。RFC 3339 で指定します。過去の時刻は 400 になります。期限を過ぎたトークンで認証すると 401 になります

一覧と取得ではトークンの値は返りません。不要になったトークンは DELETE /tokens/{id} で失効させます。ただし、テナントで最後の tenant:admin トークンは削除できません(409 CONFLICT)。 トークンの保管と入れ替えの手順は セキュリティ にあります。

JWT(外部の認証基盤のトークンをそのまま使う場合)

senca-node --jwks /etc/senca/jwks.json --jwt-issuer https://idp.example/ --jwt-audience senca [--jwt-tenant-claim tenant]

--jwks を使うときは、--jwt-issuer と --jwt-audience も指定してください。検証用の鍵はノード上の JWKS ファイルからだけ読み込み、トークンに書かれた URL へ鍵を取りに行くことはありません。

使えるアルゴリズムは EdDSA(Ed25519)・ES256・RS256(RSA は 2048 ビット以上)です。アルゴリズムは鍵で決まり、ヘッダーの alg が鍵と合わなければ拒否します。 JWKS の鍵には kid が必須で、use を書くなら sig にします。alg=none・HS 系・crit 付きのトークンは受け付けません。

トークンでは、kid が JWKS にあること、exp(必須)、nbf、iss の一致、aud の一致(文字列または配列)を検査します。時計のずれは 60 秒まで許容します。 テナントはクレーム tenant(名前は --jwt-tenant-claim で変えられます)で決まり、無いか空なら拒否します。操作した人(主体)は sub で表します。 権限は scope(空白区切り)か scp(文字列または配列)から読み、未知の権限は無視します。

権限とテナント

権限主な操作
stream:readストリーム・予約・入力・出力・パイプライン・ルーム・メディアパイプラインの読み取り、POST /streams/{id}/plan、POST /schedules/check、POST /pipelines/{id}/validate、SSE
stream:write上のリソースの作成・更新・削除、鍵の再発行、録画の開始・停止・PATCH /recordings/{id}、ルームの参加トークンの発行・参加者の操作・メッセージ送信
subscribePOST /streams/{id}/playback-url(ライブの署名付き URL)
recording:read録画の一覧・取得・VOD の URL・サムネイル・MP4 のエクスポート
recording:deleteDELETE /recordings/{id}
tenant:adminトークン・シークレット(/secrets)の管理。node:admin 以外のすべての権限を含みます
node:adminテナントをまたぐ運用操作(/nodes・/devices・/grid、ドレイン、デバイスの停止と再開)と、SSE のテナントに属さないイベントの受信
publishWHIP での配信(POST /whip/{stream_id} で、ストリームキーの代わりに使えます。同じテナントのストリームだけが対象です)

操作ごとに必要な権限は、OpenAPI の拡張フィールド x-senca-scope に書いてあります。null は認証だけで足りるという意味で、GET /capabilities がこれに当たります。 権限が足りなければ 403(FORBIDDEN)になり、title に必要な権限の名前が入ります。

テナントは認証情報から決まります。 API トークンなら作成者のテナント、JWT ならテナントのクレームです。本文でテナントを指定するフィールドはありません。 他のテナントのリソースは 404 に見えます。API トークンは自分のテナントにしか作れないので、新しいテナントを使い始めるには、JWT のテナントのクレームに新しい名前を入れます。

本文の検査

本文のあるリクエストには Content-Type: application/json を付けてください(; charset=... が付いていてもかまいません)。無ければ 415(UNSUPPORTED_MEDIA_TYPE)です。

フィールド名の綴りを間違えると、黙って無視されるのではなく 400 になります(INVALID_REQUEST。details.reason に解析エラーの内容が入ります)。型の誤りも同じ扱いです。 値の範囲・組み合わせ・参照先の誤りは、ほとんどが 400 INVALID_CONFIG になります。

Idempotency-Key

ストリームを作る POST を送ったところでタイムアウトした。届いたのかどうかわからないまま再送すると、ストリームが 2 つできてしまうかもしれません。 これを防ぐのが Idempotency-Key ヘッダーです。POST に付けておくと、24 時間は同じリクエストに同じ応答を返し、処理は 1 回しか行いません。

キーは 1〜255 文字の表示可能な ASCII(空白なし)で、違反すると 400 です。キーはテナントごとに区別されます。 同じリクエストかどうかは、メソッド・パス・クエリ・本文の SHA-256 で判定し、ヘッダーは見ません。 同じキーで中身の違うリクエストを送ったときや、別の主体が同じキーを使ったときは、409(IDEMPOTENCY_CONFLICT)になります。

再送への応答は、保存しておいたステータスコード・本文・Location・ETag に Idempotency-Replayed: true を付けたものです。 4xx の応答も保存するので、同じキーで再送すれば同じエラーが返ります。一方 5xx は保存しないので、再送すると処理をやり直します。256 KiB を超える応答も保存しません。

保存する本文からは秘密情報を消し、stream_key・token・secret を null にして、代わりに stream_key_redacted: true などを入れます。 つまり秘密情報は最初の応答でしか見られず、再送で得た応答から取り出すことはできません。

同じキーのリクエストはノードの中で順番に処理されるので、同時に再送しても処理は 1 回だけです。

ETag と If-Match(楽観的排他制御)

2 人がほぼ同時に同じストリームを更新すると、後から書いた方が先の変更を知らずに上書きしてしまいます。 SENCA はこれを楽観的排他制御で防ぎます。「読んだときから誰も変更していなければ更新する」という方式です。

リソースの応答(作成・取得・更新)に付く ETag は "{generation}"(例 "3")で、W/"3" の形も受け付けます。 PATCH では、この値を If-Match に入れて送るのが必須です。 無ければ 428(PRECONDITION_REQUIRED)、形式が違うか古ければ 412(PRECONDITION_FAILED)になります。

DELETE の If-Match は任意です。検査するのは /streams・/recordings・/schedules だけで、付けた値が古ければ 412 になります。 入力・出力・シークレット・パイプライン・ルーム・メディアパイプライン・トークンの DELETE では、If-Match を見ません。

# 取得してから If-Match を付けて更新する
ETAG=$(curl -s -D - -o /dev/null -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8080/api/v1/streams/$ID | awk -F': ' 'tolower($1)=="etag"{print $2}' | tr -d '\r')
curl -s -X PATCH http://127.0.0.1:8080/api/v1/streams/$ID \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -H "If-Match: $ETAG" \
  -d '{"metadata":{"owner":"studio-a"}}'

412 が返ったら、リソースを取得し直してから変更をやり直してください。

一覧とページング

一覧の応答は {"items": [...], "next_cursor": "..." | null} で、ID の昇順に並びます。 1 ページの件数は limit(1〜1000、既定 100)で決め、次のページは前の応答の next_cursor を cursor に入れて取ります。next_cursor が null なら最後のページです。 範囲外の limit は 400 です。

一覧でも、未知のクエリパラメーターは 400 になります。使える絞り込みは、GET /recordings の stream・state、GET /schedules の stream・state、GET /media-pipelines の room だけです。 ページングの無い一覧もあり、GET /secrets は全件を返して next_cursor: null、GET /nodes は items だけを返します。

curl -s -H "Authorization: Bearer $TOKEN" 'http://127.0.0.1:8080/api/v1/streams?limit=50'
curl -s -H "Authorization: Bearer $TOKEN" 'http://127.0.0.1:8080/api/v1/streams?limit=50&cursor=<next_cursor>'

エラー(problem+json)

エラーは Content-Type: application/problem+json(RFC 9457 の形式)で返します。

{
  "type": "urn:senca:error:precondition-failed",
  "title": "資源が更新されています(If-Match が古い)",
  "status": 412,
  "code": "PRECONDITION_FAILED",
  "request_id": "req_...",
  "retryable": false
}

title は人が読むための日本語の説明なので、プログラムでは code で判別してください。details はコードごとの補足情報で、無ければ省略されます。

retryable が true なら、同じリクエストを再送してかまいません。503・429・504 で retryable が true のときは Retry-After が付きます (429 なら次に受け付けられるまでの秒数、それ以外は 1)。リクエストの数の上限による 429 は、どの /api/v1 の操作でも返ることがあります(セキュリティ)。

/api/v1 以外のバージョン(/api/v2/... など)を指定すると、422 UNSUPPORTED_PROTOCOL_VERSION(details.supported: ["v1"])になります。 不具合を問い合わせるときは、request_id(= X-Request-Id)を添えてください。

codeステータスretryable主な場面
INVALID_CONFIG400false値の検査エラー(範囲・組み合わせ・参照先)
INVALID_REQUEST400false本文の解析エラー・未知のフィールド・未知のクエリ・limit の範囲外・Idempotency-Key の形式
UNSUPPORTED_PROTOCOL_VERSION422false/api/v1 以外のバージョンのパス
UNSUPPORTED_CODEC422false静止画のデコード(GET /streams/{id}/snapshot)ができない、ルームの処理が 422 を返した、E2EE のルームやそこからエクスポートしたストリームをサーバーで処理しようとした(details.reason: "e2ee")
UNSUPPORTED_MEDIA_TYPE415falseContent-Type が application/json でない
AUTH_FAILED401false認証の失敗
FORBIDDEN403false権限が足りない、持っていない権限をトークンに付与しようとした
NOT_FOUND404false存在しない、他のテナントのリソースである、未知のパス
METHOD_NOT_ALLOWED405falseリソースに無いメソッドを使った
CONFLICT409false現在の状態と矛盾する(使用中のシークレットの削除、最後の管理者トークンの削除など)
IDEMPOTENCY_CONFLICT409false同じ Idempotency-Key で別のリクエストを送った
PRECONDITION_FAILED412falseIf-Match が古い、または形式が違う
PRECONDITION_REQUIRED428falsePATCH に If-Match が無い
QUOTA_EXCEEDED403false上限に達した(同じストリームで同時に録画できるのは 8 本まで、など)
RATE_LIMITED429trueリクエストの数の上限を超えた(details.limit は ip・token・auth_failures・connect)
CAPACITY_UNAVAILABLE503ほとんどは trueクラスターが変更を受け付けられない(リーダーの交代中や混雑時)、ルームが別のノードにある、--webrtc-udp が無い、コーデックワーカーが無い(このときは false)
INPUT_TIMEOUT504true静止画を求めたが、配信のキーフレームが 5 秒以内に届かない
OWNERSHIP_LOST409falseクラスターで、別のノードが配信を受信しているストリームの録画の開始や静止画を求めた(details.owner_api のノードにリクエストを送り直してください)
PAYLOAD_TOO_LARGE413false録画が大きすぎて MP4 にエクスポートできない、クラスターに記録する変更が大きすぎる
INTERNAL_ERROR500trueコントロールプレーンへの書き込みに失敗したなど

表の retryable は既定値で(true なのは RATE_LIMITED・CAPACITY_UNAVAILABLE・INPUT_TIMEOUT・INTERNAL_ERROR)、個々のエラーで変わることがあります。type は urn:senca:error:{code を小文字にし、_ をハイフンにしたもの} です。

OpenAPI の定義

docs/api/openapi.yaml(OpenAPI 3.1)に、すべての操作・スキーマ・ステータスコードを書いています。説明は日本語です。 このサイトの API リファレンス は、この定義からエンドポイントを分類ごとにまとめたものです。

ノード自身は定義ファイルを配信しないので、リポジトリのファイルを OpenAPI 3.1 に対応したビューアーで開いてください。たとえば Docker の Swagger UI なら次のとおりです。

docker run --rm -p 127.0.0.1:8081:8080 -e SWAGGER_JSON=/spec/openapi.yaml \
  -v "$PWD/docs/api:/spec:ro" swaggerapi/swagger-ui
# http://127.0.0.1:8081/ を開く

ただし /api/v1 は CORS ヘッダーを返さないため、ビューアーの「Try it out」から別オリジンのノードへリクエストを送ることはできません。実際に試すときは curl か SDK を使ってください。

定義ファイルには、独自の拡張フィールドが 3 つあります。

フィールド意味
x-senca-scopeその操作に必要な権限(null は認証だけが必要)
x-senca-undefined-method-status: 405定義されていないメソッドへの応答
x-senca-event-dataSSE の data のスキーマ(SseEventData)

今のところ対応していない機能(非同期処理の状態を表す operation リソースなど)は、定義ファイルの冒頭にまとめてあります。 JavaScript の SDK と Rust のクライアントの型は、この定義ファイルから生成しています。

イベント(SSE)

GET /api/v1/events は、変更を Server-Sent Events(text/event-stream。サーバーから送り続ける HTTP の通知)で配信します。必要な権限は stream:read です。

curl -sN -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8080/api/v1/events
event: stream.created
id: 42
data: {"type":"stream.created","resource_id":"...","occurred_at":"2026-11-03T01:02:03.000Z","data":{"name":"studio-a"}}

流れてくるイベントの多くは、クラスターで合意された(単一ノードでは保存された)設定の変更から作られる「制御のイベント」です。 SSE の id は変更の記録(制御ログ)の中での位置を表す整数で、data は {type, resource_id, occurred_at, data}、occurred_at は送信した時刻です。

届くのは自分のテナントのイベントだけです。テナントに属さないイベント(ノード・グリッド・stream.owner)は、node:admin を持つ主体にだけ届きます。 サーバーにはイベントの種類で絞り込む機能が無いので、必要なものだけ扱いたいときはクライアント側で捨ててください。SDK はこれを自動で行います(下の JavaScript・Rust の説明)。

接続を保つため、15 秒ごとにコメント行(keep-alive)が送られます。401・403 は他の API と同じく problem+json で返り、403 の title は「権限 stream:read が必要です」です。

Last-Event-ID と resync

ノードは直近のイベントを残しています(既定 4096 件、--events-retain)。単一ノードでは、再起動した後も、制御ログのスナップショットより後の分を読み直して残します。

接続が切れたら、最後に受け取った id を Last-Event-ID に付けて再接続してください。その位置より後のイベントのうちノードが残している分が先に届き、続けて新しいイベントが届きます。 Last-Event-ID を付けなければ、接続した後のイベントだけが届きます。

id はそれまでに適用した変更の数で、クラスターでは同じイベントがどのノードでも同じ id になります。そのため、別のノードへ再接続しても同じ Last-Event-ID で続きを受け取れます。 接続したノードの適用が Last-Event-ID に追いついていなければ、ノードは 3 秒まで待ちます。

指定した位置がノードの残している範囲より古いときは、最初に event: resync(id は現在の位置、data: {})が届き、その後は新しいイベントが続きます。 受信側の読み取りが遅れて通知があふれたときも、残している分から続きを送ります。残している分にも無ければ、event: resync を送って接続を閉じます。

resync を受け取ったら、一覧の API で状態を取り直し、受け取った id を Last-Event-ID にして再接続してください。

イベントの一覧

制御のイベント(id あり)は次のとおりです。

種類テナントdata
stream.createdあり{name}
stream.updatedあり{generation}
stream.deletedあり{}
stream.ownerなし(node:admin){node}(ストリームを担当するノードが替わった。クラスターとグリッド)
recording.updatedあり{stream, state, generation}
recording.deletedあり{}
schedule.created・schedule.updated・schedule.deletedあり{}
schedule.runあり{state, occurrence, run_state, recording}
node.updatedなし(node:admin){draining}
input.updated・output.updatedあり{stream, generation, enabled}
input.deleted・output.deletedあり{}
secret.createdあり{name}
secret.deletedあり{}
pipeline.updatedあり{name, desired_state}
pipeline.deletedあり{}
room.updatedあり{auto}(ルームの作成・自動作成・更新)
room.deletedあり{}
media_pipeline.updatedあり{room, desired_state}(作成・更新)
media_pipeline.deletedあり{}
node.down・node.upなし(node:admin){}(resource_id はノードの ID)
device.down・device.upなし(node:admin){device}(定期的な検出でわかった状態の変化)
device.disabled・device.enabledなし(node:admin){device}(手動での停止・再開)
grid.decisionsなし(node:admin){decisions}(配置の決定。resource_id は空)

ルームのイベント(リアルタイムのルーム)は次のとおりです。

種類Webhook での名前data.data
room.startedroom.created{settings, auto_created}
room.stoppedroom.destroyed{reason}
room.connection.createdconnection.created{participant}
room.connection.updatedconnection.updated{connection_id, metadata}
room.connection.destroyedconnection.destroyed{connection_id, client_id?, reason, duration_ms}
room.track.publishedtrack.published{track}
room.track.unpublishedtrack.unpublished{track_id, connection_id}
room.spotlight.changedspotlight.changed{focused}

ルームのイベントは制御のイベントと違い、合意ログに記録されません。SSE の id を持たないので、Last-Event-ID で再開できず、取りこぼしても再送されません。 また、届くのはそのイベントが起きたノードに接続している購読者だけです(カスケードの扱いは リアルタイムのルーム)。 クラスターのすべてのルームのイベントを受け取りたいなら、各ノードの /api/v1/events を購読してください。

data は {type, resource_id(ルームの ID), event_id(イベントの ID。重複の除去に使います), node(イベントを出したノードの ID), occurred_at, data} です。 内側の data には上の表の内容だけが入り、Webhook での名前の type は入りません。

ルームのイベントもテナントで絞り込まれ、node:admin でも他のテナントのルームのイベントは届きません。ルームの設定の変更は、制御のイベント room.updated で通知されます。

event: room.connection.created
data: {"type":"room.connection.created","resource_id":"daily","event_id":"...","node":"...","occurred_at":"...","data":{"participant":{"connection_id":"...","client_id":"alice",...}}}

JavaScript の SDK(@senca/sdk)

apps/sdk-js にあります。ブラウザーで使うリアルタイムのルームの機能(Senca.connect())と、ブラウザーと Node.js 20 以上で使える管理 API のクライアント(SencaClient)が入っています。 実行時に必要な依存パッケージはありません。ソースは JSDoc で型を付けた ES modules で、ビルドせずにそのまま動きます。型定義は types/、API の一覧は apps/sdk-js/API.md、詳しい説明は apps/sdk-js/README.md にあります。

npm には公開していないので、リポジトリからインストールしてください(例: npm install /path/to/senca/apps/sdk-js)。 読み込むときは、すべてを使うなら @senca/sdk、管理 API だけなら @senca/sdk/rest、シグナリングの型だけなら @senca/sdk/protocol を指定します。

管理 API(SencaClient)

import { SencaClient, ApiError } from '@senca/sdk';

const senca = new SencaClient({ baseUrl: 'https://node.example', token: process.env.SENCA_TOKEN });

const s = await senca.streams.create({ name: 'studio-a' });     // Idempotency-Key を自動で付ける
for await (const x of senca.streams.list({ limit: 100 })) console.log(x.name);   // cursor を自動でたどる

const cur = await senca.streams.get(s.id);
await senca.streams.update(s.id, { metadata: { a: 'b' } }, { ifMatch: cur });     // ETag・generation・リソースのどれでも指定できる
await senca.streams.modify(s.id, (c) => ({ metadata: { ...c.metadata, n: '1' } })); // 412 なら取得し直してやり直す(既定 3 回)

try {
  await senca.streams.update(s.id, { name: 'x' }, { ifMatch: 1 });
} catch (e) {
  if (e instanceof ApiError && e.code === 'PRECONDITION_FAILED') { /* 他のクライアントが先に更新した */ }
}

SencaClient に渡せるオプション(ClientOptions)は次のとおりです。

項目既定意味
baseUrl(必須)https://node.example(/api/v1 を補います)または https://node.example/api/v1
tokenなしAPI トークンまたは JWT。関数を渡すと、リクエストのたびに呼び出します
retrytrue再試行の設定(maxAttempts 3(初回を含む)・initialDelayMs 200・maxDelayMs 5000・factor 2・jitter 0.2)。false で再試行しません
timeoutMs300001 回のリクエストのタイムアウト(SSE には適用しません)
headers{}すべてのリクエストに付けるヘッダー
idempotencytruePOST に Idempotency-Key(UUID v4)を自動で付けます
fetchglobalThis.fetchfetch の実装を差し替えるときに使います
  • 使えるリソースとメソッド: streams(rotateKey・playbackUrl・startRecording・plan・snapshot・snapshots.list/get/delete)・ recordings(stop・playbackUrl・thumbnail・exportMp4。作成は streams.startRecording)・schedules(check・runs)・ tokens・inputs・outputs(create(streamId, body)・list(streamId))・secrets・pipelines(validate・control)・ nodes(drain・undrain・devices.disable/enable)・rooms(createToken・participants(id).list/get/updateMetadata/disconnect・ sendMessage・stats)・mediaPipelines(list({room})・start・stop)。クライアント自体のメソッドとして capabilities()・devices()・ grid()・events() があります。型の無いリクエストは senca.request(method, path, opts) で送れます。
  • 共通のメソッド: create・list(Paginator。for await で全件を取得)・listPage(1 ページ)・get・update(ifMatch が必須。無いと TypeError)・modify・delete(ifMatch は任意)。 通信の失敗、408・429・502・503・504、retryable: true のエラーのときは、Retry-After に従い、待ち時間を倍々に延ばしながら再試行します。 ただし応答が無かった失敗を再試行するのは、GET・DELETE・PUT と、Idempotency-Key 付きの POST だけです。PATCH は 429・503 のときだけ再試行します。

problem+json のエラーは ApiError(status・code・title・type・requestId・retryable・retryAfterMs・details・problem)として投げられます。 通信の失敗では status: 0、code: 'NETWORK_ERROR' | 'TIMEOUT' です。

ほかに、SencaClient.etagOf(resource)、SencaClient.newIdempotencyKey()(プロセスをまたいで同じキーで再送するときに使います)、 SencaClient.operations(operationId からメソッド・パス・権限を調べられる表)も用意しています。

SSE

for await (const ev of senca.events({ types: ['stream.*', 'room.connection.*'], signal })) {
  if (ev.type === 'resync') await reloadAll();
  else console.log(ev.type, ev.data.resource_id);
}

ブラウザーの EventSource は Authorization ヘッダーを送れないので、SDK は fetch でイベントを読み取ります。 types には、完全一致か、末尾の * による前方一致で種類を書きます。絞り込みはクライアント側で行い、resync は絞り込みに関係なく必ず届きます。

接続が切れると、SDK は最後の id を Last-Event-ID にして再接続します。間隔は 1 秒から倍々に延ばして最大 30 秒で、reconnect: false にすれば再接続しません。 401・403・404 のときは再接続せず、ApiError で終わります。

リアルタイムのルーム(Senca.connect()、ブラウザー)

import { Senca } from '@senca/sdk';

const camera = await navigator.mediaDevices.getUserMedia({ video: { width: 1280, height: 720 }, audio: true });
const session = await Senca.connect({
  url: signalingUrl,                 // 参加トークンの応答に含まれる signaling_url
  room: 'daily',
  token: () => fetch('/my-backend/room-token').then((r) => r.text()),   // 再接続のたびに取得し直す
  publish: { stream: camera, simulcast: true },
  dataChannels: ['#chat'],
});

for await (const track of session.remoteTracks()) {
  document.body.append(track.attach(document.createElement('video')));
}

参加トークンは、アプリのサーバーが POST /api/v1/rooms/{id}/tokens(senca.rooms.createToken(room, {role, client_id, metadata}))で発行し、 token と signaling_url だけをブラウザーへ渡します。管理 API のトークンは、決してブラウザーに渡さないでください。

Session には、participants・tracks(随時更新される Map)、events()・remoteTracks()・messages()(非同期イテレーター)、waitFor()、 publish()・send()・stats()・close() があります。 シグナリング v1 の SDP のやり取り、受信スロットの管理、再ネゴシエーション、ping、ICE の再起動、リダイレクト、再接続(0.5 秒から倍々に延ばして最大 10 秒、10 回まで)は SDK が自動で行います。 エラーは SencaError(code・fatal・closeCode・request)として届きます。

ルームの機能と制約は リアルタイムのルーム、プロトコルは シグナリング を見てください。

Rust のクライアント(senca-client)

crates/senca-client にある、tokio で動く非同期のクライアントです。HTTP/1.1 と TLS には SENCA 自身の実装を使うので、外部の HTTP クライアントには依存しません。 crates.io には公開していないため、Cargo.toml の依存関係に path か git で追加してください。詳しくは crates/senca-client/README.md にあります。

use senca_client::{EventsOptions, ListOptions, ProblemCode, SencaClient, ServerEvent};
use senca_client::models::{StreamCreate, StreamPatch};

let client = SencaClient::new("https://senca.example", std::env::var("SENCA_TOKEN")?)?;

let s = client.streams().create(&StreamCreate { name: "studio-a".into(), ..Default::default() }).await?;
let key = s.ingest.stream_key.clone();   // ストリームキーはこの応答でしか受け取れない

let patch = StreamPatch { name: Some("studio-b".into()), ..Default::default() };
let s = match client.streams().update(&s.id, &patch, &s).await {   // 前の応答(ETag)をそのまま If-Match に使う
    Ok(v) => v,
    Err(e) if e.code() == Some(ProblemCode::PreconditionFailed) => { /* 取得し直して再試行 */ todo!() }
    Err(e) => return Err(e.into()),
};

let mut it = client.streams().list_with(ListOptions::default().limit(100));
while let Some(x) = it.next().await {
    println!("{}", x?.name);
}

let mut events = client.events(EventsOptions::default().types(["stream.*", "recording.*"]));
while let Some(ev) = events.next().await {
    match ev? {
        ServerEvent::Event(e) => println!("{} {}", e.event, e.data.resource_id),
        ServerEvent::Resync { .. } => { /* 一覧を取得し直す */ }
    }
}

client.streams().delete(&s.id, s.if_match()).await?;

Rust のクライアントで使えるリソースとメソッドは次のとおりです。

リソースメソッド
streams()create・create_with・list・list_with・list_page・get・update・delete・rotate_key・playback_url・start_recording・plan・snapshot・snapshots・snapshot_by_id・delete_snapshot
recordings()list・list_with(ListOptions::stream)・list_page・get・update・delete・stop・playback_url・thumbnail・export_mp4
schedules()create・create_with・list・list_with・list_page・check・get・update・delete・runs
tokens()create・create_with・list・list_with・list_page・get・delete
inputs()・outputs()create(stream_id, ..)・create_with・list(stream_id)・list_with・get・update・delete
secrets()create・create_with・list・delete
pipelines()create・create_with・list・list_with・list_page・get・update・delete・validate・control
nodes()list・get・drain・undrain・disable_device・enable_device
rooms()create・create_with・list・list_with・list_page・get・update・delete・issue_token・send_message・stats・participants・participants_with・participant・update_participant・disconnect
media_pipelines()create・create_with・list・list_with(ListOptions::room)・list_page・get・update・set_state・delete
クライアントcapabilities・grid・devices・events・healthz・readyz・fetch_signed(署名付きの再生パス。307 をたどります)・request_json(型の無いリクエスト)
  • バージョン情報付きの応答: get・create・update は Versioned<T>(value・etag・replayed・request_id。Deref で中身を参照できます)を返します。update の if_match には、&Versioned<T>・IfMatch::Etag・IfMatch::Generation(n)・u64 のどれでも渡せます。
  • Idempotency-Key: POST には毎回新しいキーを付け、再試行でも同じキーを使います。create_with(.., RequestOptions::idempotency_key("...")) でキーを指定すれば、プロセスをまたいだ再送でも同じ応答が返ります(Versioned::replayed が true になります)。
  • エラー: API のエラーは Error::Api(ApiError) になります。ApiError には code(ProblemCode。未知の値は Unknown になり、元の値は raw_code に入ります)・title・status・retryable・ request_id・details・retry_after があります。ほかに Connect・Transport・Timeout・Decode・InvalidUrl・InvalidArgument のエラーがあります。
  • 再試行(RetryPolicy): 既定では、最初のリクエストの後に最大 3 回再試行します。待ち時間は 200 ms から倍々に延ばし(上限 10 秒、±20 % のゆらぎ)、Retry-After があればそれ以上待ちます。 ClientBuilder::retry(RetryPolicy::NONE) で再試行しなくなります。PATCH は retryable・429・503 のときだけ再試行します。
  • SSE: EventsOptions で、types・last_event_id・reconnect(既定 true)・initial_backoff(既定 1 秒)・max_backoff(既定 30 秒)を指定します。 接続が切れたら Last-Event-ID を付けて再接続します。401・403 では再接続しません。
  • クライアントの構築: SencaClient::builder(url).token(..).timeout(..)(既定 30 秒).user_agent(..).root_certificates_pem(pem).build()。 TLS では、既定で OS の認証局を使います。
  • モデル: senca_client::models の型は、openapi.yaml から生成しています。応答に含まれる未知のフィールドは 読み飛ばし、未知の enum の値は Unknown になります。*Patch の null を取りうる項目は Option<Option<T>> です(None = 変更しない、 Some(None) = null を送って値を消す)。

関連するページ

この文書の原文は、リポジトリの docs/guide/ja/15-api-and-sdks.md にあります。内容は現在の版(v0.0.1)に合わせています。