RPC 機能

概要

RPC 機能は、 DataChannel 経由で JSON-RPC 2.0 による Sora の一部 HTTP API 呼び出しを行うための仕組みです。 Sora の RPC 機能については Sora のドキュメント RPC 機能 をご確認ください。

設定

RPC 機能は DataChannel 経由のシグナリングが有効な場合に利用できます。 Sora iOS SDK では Configuration.dataChannelSignaling を true に設定してください。

var config = Configuration(url: soraURL,
                           channelId: soraChannelId,
                           role: role)

// DataChannel 経由のシグナリングを有効にする
config.dataChannelSignaling = true

RPC 用の DataChannel は Sora が label: "rpc" を持つチャネルとして作成します。 Configuration.dataChannels に rpc を指定する必要はありません。

利用できるメソッドの確認

利用可能な RPC メソッドは onReceiveSignaling で受信する Signaling.offer の rpcMethods から確認できます。 rpcMethods は [String]? で、メソッド名の文字列配列です。

mediaChannel.handlers.onReceiveSignaling = { signaling in
  switch signaling {
  case let .offer(offer):
    guard let rpcMethods = offer.rpcMethods else {
      return
    }
    // 例: ["2025.2.0/RequestSimulcastRid", ...]
    print(rpcMethods)
  default:
    break
  }
}

RPC を送信する

ユーザーは MediaChannel.rpc を利用して RPC を送信します。 MediaChannel.rpc は label: "rpc" の DataChannel が OPEN になった後に利用できます。 Sora iOS SDK では mediaChannel.handlers.onDataChannelOpened で label == "rpc" を受け取った時点から利用可能です。

mediaChannel.handlers.onDataChannelOpened = { mediaChannel, label in
  guard label == "rpc" else {
    return
  }

  Task {
    do {
      let params = RequestSimulcastRidParams(
        rid: .r1,
        senderConnectionId: "EKNQ103WRD4ZZ74B6TKRM9YK78")
      _ = try await mediaChannel.rpc(
        method: RequestSimulcastRid.self,
        params: params)
    } catch {
      // 予期しないエラー
      _ = error
    }
  }
}

MediaChannel.rpc の戻り値は RPCResponse<M.Result>? であり、 M.Result は呼び出したメソッドに対応した型になります。rpc 呼び出し時に is_notification_request を true に設定した場合は返り値が nil になります。

RPC 機能のメソッドと SDK が提供する型の対応

Sora iOS SDK では以下の RPC メソッドが型として提供されます。

  • 2025.2.0/RequestSimulcastRid に対応した RequestSimulcastRid
  • 2025.2.0/RequestSpotlightRid に対応した RequestSpotlightRid
  • 2025.2.0/ResetSpotlightRid に対応した ResetSpotlightRid
  • 2025.2.0/PutSignalingNotifyMetadata に対応した PutSignalingNotifyMetadata<Metadata>
  • 2025.2.0/PutSignalingNotifyMetadataItem に対応した PutSignalingNotifyMetadataItem<Metadata, Value>

各メソッド型は RPCMethodProtocol を実装しており、メソッド固有のパラメータ型と結果型を持ちます。その型を用いて MediaChannel.rpc を呼んでください。

以下は RequestSimulcastRid と PutSignalingNotifyMetadataItem を呼び出すサンプルコードです。

RequestSimulcastRid の例

import Sora

func requestSimulcastRid(mediaChannel: MediaChannel) async throws {
  // RequestSimulcastRidParams:
  //   rid: Rid
  //   senderConnectionId: String?
  let params = RequestSimulcastRidParams(
    rid: .r0,
    senderConnectionId: "YOUR-SENDER-CONNECTION-ID")

  do {
    guard
      let response = try await mediaChannel.rpc(
        method: RequestSimulcastRid.self,
        params: params,
        is_notification_request: false,
        timeout: 5.0)
    else {
      // RPC レスポンスが nil の場合
      return
    }
    // response.result は RequestSimulcastRidResult で、プロパティで直接アクセス可能
    let channelId = response.result.channelId
    let receiverConnectionId = response.result.receiverConnectionId
    let rid = response.result.rid
    let senderConnectionId = response.result.senderConnectionId
    _ = (channelId, receiverConnectionId, rid, senderConnectionId)
  } catch SoraError.rpcUnavailable(let reason) {
    // DataChannel が利用できない
    _ = reason
  } catch SoraError.rpcTimeout {
    // 応答がタイムアウトした
  } catch SoraError.rpcServerError(let detail) {
    // Sora から JSON-RPC のエラー応答が返った
    _ = detail
  } catch {
    // 予期しないエラー
    _ = error
  }
}

PutSignalingNotifyMetadataItem の例

import Sora

func putSignalingNotifyMetadataItem(mediaChannel: MediaChannel) async throws {
  // メタデータアイテムの構造を定義
  struct NewItem: Codable {
    let foo: String
  }

  // メタデータ全体の構造を定義
  struct Metadata: Codable {
    let oldItem: String
    let newItem: NewItem

    enum CodingKeys: String, CodingKey {
      case oldItem = "old_item"
      case newItem = "new_item"
    }
  }

  // PutSignalingNotifyMetadataItemParams<Value>:
  //   key: String
  //   value: Value
  //   push: Bool?
  let params = PutSignalingNotifyMetadataItemParams(
    key: "new_item",
    value: NewItem(foo: "bar"))

  do {
    guard
      let response = try await mediaChannel.rpc(
        method: PutSignalingNotifyMetadataItem<Metadata, NewItem>.self,
        params: params,
        is_notification_request: false,
        timeout: 5.0)
    else {
      // RPC レスポンスが nil の場合
      return
    }
    // response.result は Metadata 型で、更新後のメタデータ全体が返される
    let updated = response.result
    _ = updated.oldItem
    _ = updated.newItem
  } catch SoraError.rpcUnavailable(let reason) {
    // DataChannel が利用できない
    _ = reason
  } catch SoraError.rpcTimeout {
    // 応答がタイムアウトした
  } catch SoraError.rpcServerError(let detail) {
    // Sora から JSON-RPC のエラー応答が返った
    _ = detail
  } catch {
    // 予期しないエラー
    _ = error
  }
}

MediaChannel.rpc のレスポンスが不要な場合

レスポンスが不要な場合は is_notification_request を true にします。 この場合は JSON-RPC 2.0 の Notification として送信され、レスポンスを返しません。

do {
  let params = RequestSimulcastRidParams(
    rid: .r1,
    senderConnectionId: "EKNQ103WRD4ZZ74B6TKRM9YK78")
  _ = try await mediaChannel.rpc(
    method: RequestSimulcastRid.self,
    params: params,
    is_notification_request: true,
    timeout: 5.0)
} catch {
  // 予期しないエラー
  _ = error
}

MediaChannel.rpc について

MediaChannel.rpc はジェネリクスを使用します。

func rpc<M: RPCMethodProtocol>(
  method: M.Type,
  params: M.Params,
  is_notification_request: Bool = false,
  timeout: TimeInterval = 5.0
) async throws -> RPCResponse<M.Result>?

RPCMethodProtocol は以下のように定義されており、 Params と Result を RPC の各メソッドと関連付ける役割を持ちます。

public protocol RPCMethodProtocol {
  associatedtype Params: Encodable
  associatedtype Result: Decodable
  static var name: String { get }
}

この関連付けにより、利用者は呼び出したメソッドに対応した結果型を扱えます。

例えば RequestSimulcastRid メソッドに対応する RequestSimulcastRid を指定した場合のレスポンスの型は RPCResponse<RequestSimulcastRidResult>? になります。

SDK で未提供の RPC メソッド型を定義する

Sora に新しい RPC メソッドが追加され、SDK 側でまだ型が提供されていない場合は、 RPCMethodProtocol を実装した型をユーザーが定義することで対応することができます。

Params は JSON-RPC 2.0 リクエストオブジェクトの params に入る値を表す型で、 Result は JSON-RPC 2.0 レスポンスオブジェクトの result に入る値を表す型です。 name には Sora に存在する RPC メソッド名を指定してください。

// リクエストパラメーター {"value": "hello"} を送信し
// {"accepted": true} のようにリザルトを受信する例です
struct CustomRPCParams: Encodable {
  let value: String
}

struct CustomRPCResult: Decodable {
  let accepted: Bool
}

struct CustomRPCMethod: RPCMethodProtocol {
  typealias Params = CustomRPCParams
  typealias Result = CustomRPCResult
  static let name = "2025.2.0/CustomRPCMethod"
}

let params = CustomRPCParams(value: "hello")
let response = try await mediaChannel.rpc(
  method: CustomRPCMethod.self,
  params: params)
_ = response?.result.accepted

Swift 6 言語モードに対応する Sendable な RPC を利用する

MediaChannel.sendableRPC(method:params:isNotificationRequest:timeout:) は、 MediaChannel.rpc と同じ挙動で、 Swift 6 言語モードの検査に対応した RPC メソッドを呼び出します。 引数と、 jsonrpc / id / result の意味、 notification で nil が返ること、失敗時に SoraError が投げられることは MediaChannel.rpc と共通です。

MediaChannel.sendableRPC は MediaChannel.rpc に対して以下の 2 点が異なります。

  • 呼び出せるのは SendableRPCMethodProtocol に準拠したメソッドだけです
  • 戻り値は SendableRPCResponse<M.Result>? です

SendableRPCResponse は Result が Sendable な場合だけ Sendable に準拠できるため、応答を actor 境界や Task の @Sendable クロージャへ渡せます。 そのため、 params と result を actor 境界や @Sendable クロージャへ渡す場合は MediaChannel.sendableRPC を使ってください。

MediaChannel.sendableRPC は Swift 5 言語モードでも呼び出せます。 ただし、 actor 境界や @Sendable クロージャを越える値の検査が有効になるのは Swift 6 言語モードです。

利用者定義のメソッドを呼び出す

SDK で未提供のメソッドは、 Sendable な Params / Result を関連付けた SendableRPCMethodProtocol 準拠型を定義して呼び出します。 SendableRPCMethodProtocol は RPCMethodProtocol を継承し、 Params と Result に Sendable を要求します。

// リクエストパラメーター {"value": "hello"} を送信し
// {"accepted": true} のようにリザルトを受信する例です

// Params と Result が Sendable のため SendableRPCMethodProtocol へ準拠できる
struct CustomRPCParams: Encodable, Sendable {
  let value: String
}

struct CustomRPCResult: Decodable, Sendable {
  let accepted: Bool
}

enum CustomRPCMethod: SendableRPCMethodProtocol {
  typealias Params = CustomRPCParams
  typealias Result = CustomRPCResult
  static let name = "2025.2.0/CustomRPCMethod"
}

let response = try await mediaChannel.sendableRPC(
  method: CustomRPCMethod.self,
  params: CustomRPCParams(value: "hello"))

Sendable ではない Params / Result を使う既存の利用者定義メソッドは、これまでどおり MediaChannel.rpc を利用してください。 SendableRPCMethodProtocol へ準拠させるには、非 Sendable な型を Sendable な型へ変更する必要があります。

SendableRPCResponse を actor へ渡す

SendableRPCResponse<CustomRPCResult> は Sendable のため、応答を actor 境界や Task の @Sendable クロージャへそのまま渡せます。

actor RPCResultStore {
  private var accepted: Bool?

  func store(_ response: SendableRPCResponse<CustomRPCResult>) {
    // response.result は CustomRPCResult で、プロパティで直接アクセス可能
    accepted = response.result.accepted
  }
}

組み込みメソッドを呼び出す

RequestSimulcastRid / RequestSpotlightRid / ResetSpotlightRid は新旧両方の protocol へ準拠しているため、 MediaChannel.rpc と MediaChannel.sendableRPC のどちらからも呼び出せます。 RequestSimulcastRid を MediaChannel.sendableRPC で呼び出した場合のレスポンスの型は SendableRPCResponse<RequestSimulcastRidResult>? になります。

let params = RequestSimulcastRidParams(
  rid: .r0,
  senderConnectionId: "YOUR-SENDER-CONNECTION-ID")

guard
  let response = try await mediaChannel.sendableRPC(
    method: RequestSimulcastRid.self,
    params: params,
    isNotificationRequest: false,
    timeout: 5.0)
else {
  // 応答が返らなかった場合
  return
}

// response.result は RequestSimulcastRidResult で、プロパティで直接アクセス可能
let channelId = response.result.channelId

ジェネリックな組み込みメソッドを呼び出す

PutSignalingNotifyMetadata / PutSignalingNotifyMetadataItem は型パラメータに Sendable を要求しないため、 SendableRPCMethodProtocol へ準拠できません。 actor 境界や @Sendable クロージャへ渡す場合は、型パラメータに Sendable を要求する SendablePutSignalingNotifyMetadata / SendablePutSignalingNotifyMetadataItem を使ってください。 RPC メソッド名は既存の PutSignalingNotifyMetadata / PutSignalingNotifyMetadataItem と同じです。

  • SendablePutSignalingNotifyMetadata<Metadata> は Metadata が Codable & Sendable の場合に利用します
  • SendablePutSignalingNotifyMetadataItem<Metadata, Value> は Metadata が Decodable & Sendable 、 Value が Encodable & Sendable の場合に利用します
struct NewItem: Codable, Sendable {
  let foo: String
}

struct Metadata: Codable, Sendable {
  let oldItem: String
  let newItem: NewItem
}

let params = PutSignalingNotifyMetadataItemParams(
  key: "new_item",
  value: NewItem(foo: "bar"))

guard
  let response = try await mediaChannel.sendableRPC(
    method: SendablePutSignalingNotifyMetadataItem<Metadata, NewItem>.self,
    params: params,
    isNotificationRequest: false,
    timeout: 5.0)
else {
  // 応答が返らなかった場合
  return
}

// response.result は Metadata 型で、更新後のメタデータ全体が返される
let updated = response.result

任意の JSON を送受信する

参照型や可変状態を持つ型は Sendable を満たせないため、任意の JSON を送受信する場合は Sendable へ準拠した JSONValue を利用します。 JSONValue は SendableRPCMethodProtocol の Params / Result としてそのまま利用できます。

// JSONValue.object は [String: JSONValue] のため、任意の JSON を Sendable のまま組み立てられる
let metadata: JSONValue = .object(["app_name": .string("sora-ios-sdk")])

guard
  let response = try await mediaChannel.sendableRPC(
    method: SendablePutSignalingNotifyMetadata<JSONValue>.self,
    params: PutSignalingNotifyMetadataParams(metadata: metadata))
else {
  // 応答が返らなかった場合
  return
}

// response.result は JSONValue のため、case 分岐で読み出す
if case .object(let fields) = response.result {
  _ = fields["app_name"]
}

notification を送る

isNotificationRequest に true を指定すると JSON-RPC 2.0 の Notification として送信され、 Sora は応答を返さないため戻り値は nil になります。 この挙動も MediaChannel.rpc と共通です。

// notification は応答を待たないため、戻り値の SendableRPCResponse? は nil になる
let response = try await mediaChannel.sendableRPC(
  method: RequestSimulcastRid.self,
  params: params,
  isNotificationRequest: true,
  timeout: 5.0)

エラーを扱う

失敗時に SoraError が投げられることも MediaChannel.rpc と共通です。 SoraError.rpcServerError が運ぶ RPCErrorDetail は Sendable で、 RPCErrorDetail.data は JSON-RPC 2.0 の error.data を表す JSONValue? です。 error.data が省略された場合は nil 、 null の場合は .null になります。

do {
  _ = try await mediaChannel.sendableRPC(
    method: RequestSimulcastRid.self,
    params: params,
    isNotificationRequest: false,
    timeout: 5.0)
} catch SoraError.rpcServerError(let detail) {
  // detail.code と detail.message は JSON-RPC 2.0 の error.code と error.message になる
  _ = detail.code
  _ = detail.message
  // detail.data は error.data が省略された場合は nil、null の場合は .null になる
  switch detail.data {
  case .some(.object(let errorData)):
    _ = errorData
  case .some(let value):
    _ = value
  case .none:
    break
  }
} catch {
  // 予期しないエラー
  _ = error
}

Sendable な RPC のサンプルコード

import Sora

// MARK: - 利用者定義の RPC メソッド

/// 利用者定義の RPC メソッドのパラメーター。
/// Params と Result が Sendable のため、SendableRPCMethodProtocol へ準拠できる。
struct CustomRPCParams: Encodable, Sendable {
  let value: String
}

/// 利用者定義の RPC メソッドの実行結果。
struct CustomRPCResult: Decodable, Sendable {
  let accepted: Bool
}

/// 利用者定義の RPC メソッド。
/// SendableRPCMethodProtocol は RPCMethodProtocol を継承し、Params と Result に Sendable を要求する。
enum CustomRPCMethod: SendableRPCMethodProtocol {
  typealias Params = CustomRPCParams
  typealias Result = CustomRPCResult
  static let name = "2025.2.0/CustomRPCMethod"
}

// MARK: - SendableRPCResponse を actor へ渡す

/// SendableRPCResponse を受け取る actor。
///
/// SendableRPCResponse<CustomRPCResult> は Sendable のため、actor 境界を越えて渡せる。
actor RPCResultStore {
  private var accepted: Bool?

  func store(_ response: SendableRPCResponse<CustomRPCResult>) {
    // response.result は CustomRPCResult で、プロパティで直接アクセス可能
    accepted = response.result.accepted
  }

  func currentAccepted() -> Bool? {
    accepted
  }
}

/// MediaChannel 自体は Sendable ではないため、チャネルを actor 境界や Task へ渡す場合は
/// nonisolated(unsafe) などの隔離が必要になる。これは rpc / sendableRPC のどちらでも同じ。
/// sendableRPC が不要にするのは params / result / SendableRPCResponse を渡す際の回避策で、
/// ここでは Sendable な SendableRPCResponse だけを actor へ渡すのが要点になる。
final class SendableRPCExample {
  nonisolated(unsafe) let mediaChannel: MediaChannel
  let store = RPCResultStore()

  init(mediaChannel: MediaChannel) {
    self.mediaChannel = mediaChannel
  }

  /// SendableRPCMethodProtocol 準拠の利用者定義メソッドを sendableRPC で呼び出す。
  func callCustomMethod() async throws {
    do {
      guard
        let response = try await mediaChannel.sendableRPC(
          method: CustomRPCMethod.self,
          params: CustomRPCParams(value: "hello"),
          isNotificationRequest: false,
          timeout: 5.0)
      else {
        // 応答が返らなかった場合
        return
      }
      // response は SendableRPCResponse<CustomRPCResult> です。 RPCResponse と同じ挙動で、
      // Swift 6 言語モードの検査に対応した型のため、actor 境界や @Sendable クロージャへ渡せます。
      await store.store(response)
    } catch SoraError.rpcUnavailable(let reason) {
      // DataChannel が利用できない
      _ = reason
    } catch SoraError.rpcTimeout {
      // 応答がタイムアウトした
    } catch SoraError.rpcServerError(let detail) {
      // Sora から JSON-RPC のエラー応答が返った
      _ = detail
    } catch {
      // 予期しないエラー
      _ = error
    }
  }
}

// MARK: - 組み込みメソッドを timeout 付きで呼び @Sendable クロージャへ渡す

/// 組み込みの RequestSimulcastRid は SendableRPCMethodProtocol へも準拠しているため、
/// sendableRPC からも呼び出せる。戻り値は SendableRPCResponse<RequestSimulcastRidResult>? になる。
func requestSimulcastRidSendable(mediaChannel: MediaChannel) async throws {
  // RequestSimulcastRidParams:
  //   rid: Rid
  //   senderConnectionId: String?
  let params = RequestSimulcastRidParams(
    rid: .r0,
    senderConnectionId: "YOUR-SENDER-CONNECTION-ID")

  do {
    guard
      let response = try await mediaChannel.sendableRPC(
        method: RequestSimulcastRid.self,
        params: params,
        isNotificationRequest: false,
        timeout: 5.0)
    else {
      // 応答が返らなかった場合
      return
    }
    // response.result は RequestSimulcastRidResult で、プロパティで直接アクセス可能
    let channelId = response.result.channelId
    let receiverConnectionId = response.result.receiverConnectionId
    let rid = response.result.rid
    let senderConnectionId = response.result.senderConnectionId
    _ = (channelId, receiverConnectionId, rid, senderConnectionId)

    // @Sendable クロージャへ渡す場合は、クロージャの外側の値をキャプチャできるかが検査される。
    // SendableRPCResponse は Sendable のため、そのままキャプチャできる。
    await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
      Task { @Sendable in
        _ = response.result.channelId
        continuation.resume()
      }
    }
  } catch SoraError.rpcUnavailable(let reason) {
    // DataChannel が利用できない
    _ = reason
  } catch SoraError.rpcTimeout {
    // 応答がタイムアウトした
  } catch SoraError.rpcServerError(let detail) {
    // Sora から JSON-RPC のエラー応答が返った
    _ = detail
  } catch {
    // 予期しないエラー
    _ = error
  }
}

// MARK: - notification として送信する

/// isNotificationRequest に true を指定すると JSON-RPC 2.0 の Notification として送信され、
/// Sora は応答を返さないため戻り値は nil になる。この挙動も rpc と共通になる。
func requestSimulcastRidAsNotification(mediaChannel: MediaChannel) async throws {
  let params = RequestSimulcastRidParams(
    rid: .r0,
    senderConnectionId: "YOUR-SENDER-CONNECTION-ID")

  do {
    // notification は応答を待たないため、戻り値の SendableRPCResponse? は nil になる
    let response = try await mediaChannel.sendableRPC(
      method: RequestSimulcastRid.self,
      params: params,
      isNotificationRequest: true,
      timeout: 5.0)
    _ = response
  } catch SoraError.rpcUnavailable(let reason) {
    // DataChannel が利用できない
    _ = reason
  } catch SoraError.rpcTimeout {
    // 応答がタイムアウトした
  } catch SoraError.rpcServerError(let detail) {
    // Sora から JSON-RPC のエラー応答が返った
    _ = detail
  } catch {
    // 予期しないエラー
    _ = error
  }
}

// MARK: - ジェネリックな組み込みメソッドを Sendable 版の型で呼び出す

/// シグナリング通知メタデータのアイテムを設定する。
/// ジェネリックな組み込みメソッドは、actor 境界や @Sendable クロージャへ渡す場合に Sendable 版の型を使う。
func putSignalingNotifyMetadataItemSendable(mediaChannel: MediaChannel) async throws {
  // メタデータアイテムの構造を定義する。Value は Encodable & Sendable が必要になる
  struct NewItem: Codable, Sendable {
    let foo: String
  }

  // メタデータ全体の構造を定義する。Metadata は Decodable & Sendable が必要になる
  struct Metadata: Codable, Sendable {
    let oldItem: String
    let newItem: NewItem

    enum CodingKeys: String, CodingKey {
      case oldItem = "old_item"
      case newItem = "new_item"
    }
  }

  // PutSignalingNotifyMetadataItemParams<Value>:
  //   key: String
  //   value: Value
  //   push: Bool?
  let params = PutSignalingNotifyMetadataItemParams(
    key: "new_item",
    value: NewItem(foo: "bar"))

  do {
    guard
      let response = try await mediaChannel.sendableRPC(
        method: SendablePutSignalingNotifyMetadataItem<Metadata, NewItem>.self,
        params: params,
        isNotificationRequest: false,
        timeout: 5.0)
    else {
      // 応答が返らなかった場合
      return
    }
    // response.result は Metadata 型で、更新後のメタデータ全体が返される
    let updated = response.result
    _ = updated.oldItem
    _ = updated.newItem
  } catch SoraError.rpcUnavailable(let reason) {
    // DataChannel が利用できない
    _ = reason
  } catch SoraError.rpcTimeout {
    // 応答がタイムアウトした
  } catch SoraError.rpcServerError(let detail) {
    // Sora から JSON-RPC のエラー応答が返った
    _ = detail
  } catch {
    // 予期しないエラー
    _ = error
  }
}

// MARK: - 辞書を受け取る組み込みメソッドを JSONValue で呼び出す

/// 参照型や可変状態を持つ型は Sendable を満たせないため、任意の JSON を送受信する場合は
/// Sendable へ準拠した JSONValue を使う。ここでは辞書をメタデータとして送信する。
func putSignalingNotifyMetadataWithJSONValue(mediaChannel: MediaChannel) async throws {
  // JSONValue.object は [String: JSONValue] のため、任意の JSON を Sendable のまま組み立てられる
  let metadata: JSONValue = .object(["app_name": .string("sora-ios-sdk")])

  do {
    guard
      let response = try await mediaChannel.sendableRPC(
        method: SendablePutSignalingNotifyMetadata<JSONValue>.self,
        params: PutSignalingNotifyMetadataParams(metadata: metadata),
        isNotificationRequest: false,
        timeout: 5.0)
    else {
      // 応答が返らなかった場合
      return
    }
    // response.result は JSONValue のため、case 分岐で読み出す
    if case .object(let fields) = response.result {
      _ = fields["app_name"]
    }
  } catch SoraError.rpcUnavailable(let reason) {
    // DataChannel が利用できない
    _ = reason
  } catch SoraError.rpcTimeout {
    // 応答がタイムアウトした
  } catch SoraError.rpcServerError(let detail) {
    // Sora から JSON-RPC のエラー応答が返った
    _ = detail
  } catch {
    // 予期しないエラー
    _ = error
  }
}

// MARK: - エラー応答から JSON-RPC 2.0 のエラー詳細を取り出す

/// 失敗時に SoraError が投げられることも rpc と共通になる。
/// SoraError.rpcServerError が運ぶ RPCErrorDetail は Sendable で、
/// detail.data は JSON-RPC 2.0 の error.data を表す JSONValue? になる。
func describeSendableRPCError(mediaChannel: MediaChannel) async throws {
  let params = RequestSimulcastRidParams(
    rid: .r0,
    senderConnectionId: "YOUR-SENDER-CONNECTION-ID")

  do {
    _ = try await mediaChannel.sendableRPC(
      method: RequestSimulcastRid.self,
      params: params,
      isNotificationRequest: false,
      timeout: 5.0)
  } catch SoraError.rpcServerError(let detail) {
    // detail.code と detail.message は JSON-RPC 2.0 の error.code と error.message になる
    _ = detail.code
    _ = detail.message
    // detail.data は error.data が省略された場合は nil、null の場合は .null になる
    switch detail.data {
    case .some(.object(let errorData)):
      _ = errorData
    case .some(let value):
      _ = value
    case .none:
      // error.data が省略された場合
      break
    }
  } catch {
    // 予期しないエラー
    _ = error
  }
}