# RPC 機能

## 概要

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

> **警告**
>
> この機能は実験的機能のため、正式版では仕様が変更される可能性があります。

## 設定

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

```swift
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]?` で、メソッド名の文字列配列です。

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

> **注釈**
>
> is_notification_request を true に設定すると Notification として利用できます。
> これは Sora 側がレスポンスを返さないことを意味します。
> Notification に関する詳細は   をご確認ください。

### 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 の例

```swift
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 の例

```swift
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 として送信され、レスポンスを返しません。

```swift
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` はジェネリクスを使用します。

```swift
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 の各メソッドと関連付ける役割を持ちます。

```swift
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 メソッド名を指定してください。

```swift
// リクエストパラメータ {"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
```
