コールバック¶
Sora iOS SDK には WebRTC API イベントや Sora 固有の機能のイベントをハンドリングするためのコールバックを MediaChannelHandlers で提供しています。
ここではそのコールバックの利用方法について説明します。
コールバックと同じイベントは AsyncStream でも購読できます。詳しくは Swift 6 言語モードに対応する Sendable なイベントを購読する をご確認ください。
onConnect¶
Sora との接続時に呼ばれるコールバックです。
error が nil ではない場合は接続に失敗しています。
let config = Configuration(url: url,
channelId: channelId,
role: .sendrecv)
config.mediaChannelHandlers.onConnect = { [weak self] error in
guard let self = self else { return }
// error が nil でない場合は接続に失敗しているのでエラーハンドリングを行ってください
if let error = error {
// エラー処理
}
}
onDisconnect¶
Sora との接続を解除したときに呼ばれるコールバックです。
event は SoraCloseEvent という enum で、以下の 2 つのケースがあります。
- ok: Sora から正常に切断された場合
- error: Sora との接続に異常が発生した結果切断された場合
ok の例
- クライアントから切断した場合
- Sora の API を利用して切断された場合
- 認証エラーで Sora から切断された場合
error の例
- 接続中にネットワークエラーが起こった場合
- WebSocket 切断時に異常が起こった場合
let config = Configuration(url: url,
channelId: channelId,
role: .sendrecv)
config.mediaChannelHandlers.onDisconnect = { [weak self] event in
guard let self = self else { return }
switch event {
case .ok(let code, let reason):
// 正常に切断された場合の処理
print("切断コード: \(code)")
print("切断理由: \(reason)")
case .error(let error):
// エラー内容を表示
print("エラー内容: \(error.localizedDescription)")
}
}
SoraCloseEvent の定義は以下のようになっています。
enum SoraCloseEvent {
// 正常系
case ok(code: Int, reason: String)
// 異常系
case error(Error)
}
onAddStream¶
ストリームが追加されたときに呼ばれるコールバックです。
自分自身のストリームも入ってくるため、Configuration.publisherStreamId を控えておくことでリモートのストリームを判別することができます。
let config = Configuration(url: url,
channelId: channelId,
role: .sendrecv)
let publisherStreamId = config.publisherStreamId
config.mediaChannelHandlers.onAddStream = { [weak self] stream in
guard let self = self else { return }
// 例: リモートストリームの videoRenderer に receiverVideoView をセットする
if stream.streamId != publisherStreamId {
stream.videoRenderer = self.receiverVideoView
}
}
onRemoveStream¶
ストリームが削除されたときに呼ばれるコールバックです。
let config = Configuration(url: url,
channelId: channelId,
role: .sendrecv)
let publisherStreamId = config.publisherStreamId
config.mediaChannelHandlers.onRemoveStream = { [weak self] stream in
guard let self = self else { return }
// 例: リモートストリーム用の videoRenderer をクリアする
if stream.streamId != publisherStreamId {
self.receiverVideoView.stop()
self.receiverVideoView.clear()
}
}
onReceiveSignalingJSON¶
Sora からシグナリングメッセージを受信したときに呼ばれるコールバックです。
シグナリングメッセージは JSON 文字列として提供されます。
let config = Configuration(url: url,
channelId: channelId,
role: .sendrecv)
config.mediaChannelHandlers.onReceiveSignalingJSON = { json in
guard let data = json.data(using: .utf8) else { return }
do {
guard let signaling = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let type = signaling["type"] as? String
else {
return
}
switch type {
case "notify":
guard let eventType = signaling["event_type"] as? String else { return }
switch eventType {
case "connection.created":
// メタデータに含まれる JSON 値 (Any) を任意の型にキャストする
if let metadata = signaling["metadata"] as? [String: Any] {
print("metadata => \(metadata)")
}
if let authnMetadata = signaling["authn_metadata"] as? [String: Any] {
print("authn => \(authnMetadata)")
}
if let authzMetadata = signaling["authz_metadata"] as? [String: Any] {
print("authz => \(authzMetadata)")
}
...
// 他のイベント種別でも同様に処理する
case "connection.created":
...
default:
...
}
case "push":
if let pushData = signaling["data"] as? [String: Any] {
print("data => \(pushData)")
}
...
default:
...
}
} catch {
print("decode error => \(error)")
}
}
onReceiveSignaling¶
Sora からシグナリングメッセージを受信したときに呼ばれるコールバックです。
Sora から受信する type: notify メッセージなどをアプリケーションで利用したい場合にこのコールバックを利用します。
let config = Configuration(url: url,
channelId: channelId,
role: .sendrecv)
mediaChannel.handlers.onReceiveSignaling = { signaling in
switch signaling {
// type: notify
case .notify(let notify):
switch notify.eventType {
case "connection.created":
// メタデータに含まれる JSON 値 (Any) を任意の型にキャストする
if let metadata = notify.metadata as? [String: Any] {
print("metadata => \(metadata)")
}
if let authnMetadata = notify.authnMetadata as? [String: Any] {
print("authn => \(authnMetadata)")
}
if let authzMetadata = notify.authzMetadata as? [String: Any] {
print("authz => \(authzMetadata)")
}
...
// 他のイベント種別でも同様に処理する
case "connection.created":
...
default:
...
}
// type: push
case .push(let push):
if let data = push.data as? [String: Any] {
print("data => \(data)")
}
...
default:
...
}
}
onDataChannel¶
DataChannel シグナリングに切り替わったタイミングで呼ばれるコールバックです。
let config = Configuration(url: url,
channelId: channelId,
role: .sendrecv)
config.mediaChannelHandlers.onDataChannel = { [weak self] mediaChannel in
// ここに DataChannel が利用可能になった際の処理を書いてください
}
onDataChannelOpened¶
DataChannel がラベルごとに OPEN になったときに呼ばれるコールバックです。
特定のラベルが利用可能になった時点で処理したい場合は、 onDataChannelOpened を利用してください。
たとえばリアルタイムメッセージングで #spam ラベルの利用開始を待ちたい場合や、 RPC で rpc ラベルの利用開始を待ちたい場合に利用できます。
let config = Configuration(url: url,
channelId: channelId,
role: .sendrecv)
config.mediaChannelHandlers.onDataChannelOpened = { [weak self] mediaChannel, label in
guard let self = self else { return }
switch label {
case "#spam":
// #spam ラベルが利用可能になった後の処理
break
case "rpc":
// rpc ラベルが利用可能になった後の処理
break
default:
break
}
}
onDataChannelMessage¶
DataChannel のメッセージ受信時に呼ばれるコールバックです。
詳しい使い方は リアルタイムメッセージング機能 をご確認ください。
Swift 6 言語モードに対応する Sendable なイベントを購読する¶
SoraHandlers / MediaChannelHandlers のコールバックと同じイベントを AsyncStream で購読できます。
Sora.subscribeEvents(bufferingPolicy:) は Sora オブジェクトのイベントを配送し、 MediaChannel.subscribeEvents(bufferingPolicy:) は接続ごとのイベントを配送します。
どちらも購読者ごとに独立した AsyncStream<SoraEvent> を返します。
イベントは不変で Sendable な SoraEvent として配送されるため、 actor 境界や Task の @Sendable クロージャへそのまま渡せます。
payload は MediaChannel 、 MediaStream 、 raw WebRTC object を含まず、すべて値として確定しているため、配送後も保持できます。
この API の追加によって、既存のコールバックの型、配送スレッド、配送順序、呼び出し回数は変わりません。
Swift 6 言語モードを利用しない場合は、従来のコールバックをそのまま利用できます。
コールバックは非推奨ではないため、購読 API へ置き換える必要はありません。
購読 API は Swift 5 言語モードでも呼び出せますが、 actor 境界や @Sendable クロージャを越える値の検査が有効になるのは Swift 6 言語モードです。
コールバックとの対応¶
購読 API は、コールバックと同じ配送点から次のイベントを配送します。
コールバック |
イベント |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
イベントを購読する実装サンプル¶
// 接続の前に購読を開始する (mediaChannelAdded と接続の結果を受け取るため)
let soraEvents = Sora.shared.subscribeEvents()
Task {
for await event in soraEvents {
switch event.kind {
case .connected:
print("connected: \(event.connectionId ?? "-")")
default:
// SDK が種別を追加しても、 default があるため既存のコードはビルドできます
break
}
}
}
_ = Sora.shared.connect(configuration: config) { mediaChannel, error in
guard let mediaChannel, error == nil else { return }
// 購読を開始してから Task へ渡す (購読を開始する前のイベントは届かない)
let channelEvents = mediaChannel.subscribeEvents()
Task {
for await event in channelEvents {
switch event.kind {
case .streamAdded:
print("stream added: \(event.streamId ?? "-")")
case .dataChannelMessage:
print("message: \(event.dataChannelMessage?.count ?? 0) bytes")
case .disconnected:
print("disconnected")
default:
break
}
}
}
}
購読を解除する場合は、購読している Task を cancel するか、 AsyncStream への参照を解放します。
配送されるイベント¶
SoraEvent.kind でイベントの種別を判定します。配送される種別は購読する側によって異なります。
Sora オブジェクトのイベント¶
Sora.subscribeEvents(bufferingPolicy:) が配送する種別です。
mediaChannelAddedmediaChannelRemovedaudioRouteChanged- このインスタンスが開始した接続の
connected、connectFailed、disconnected
MediaChannel のイベント¶
MediaChannel.subscribeEvents(bufferingPolicy:) が配送する種別です。
connected/connectFailedstreamAdded/streamRemovedvideoEnabledChanged/audioEnabledChangedsignalingReceivedJSONdataChannelOpened、dataChannelAvailable、dataChannelMessagedisconnected
イベントの内容¶
種別ごとに該当する property だけが設定され、それ以外は nil です。
種別 |
配送元 |
設定される property |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
世代 ( transportEpoch ) は接続ごとの値で、接続の開始時は 0 です。 "type": "redirect" を受信するたびに増えるため、通常は 0 (redirect なし) か 1 (redirect あり) になります。
同じ接続のイベント列で古い世代のイベントを識別するために使い、別の接続の世代とは比較できません (増加の上限も保証しないため、判定は「0 か、1 以上か」で行ってください)。
DataChannel で受信したメッセージの扱いは リアルタイムメッセージング機能 をご確認ください。
buffer と通し番号¶
bufferingPolicy で購読者ごとに buffer の件数と drop 方針を指定します。指定する値は AsyncStream の BufferingPolicy と同じです。
bufferingPolicy |
挙動 |
|---|---|
|
最新の |
|
最も古い |
|
件数を制限しません (消費が遅れるとその分のメモリを使い続けます) |
イベントが破棄されるのは、配送に対して購読者の消費が追いつかない場合です。配送そのものは成功しており、 buffer のあふれた分だけが破棄されます。
最新の状態だけを扱いたい場合は .bufferingNewest(1) を、イベントを 1 件も破棄したくない場合は .unbounded を指定します。
// 最新の 1 件だけを保持する (消費が遅れても常に最新のイベントを扱える)
let events = mediaChannel.subscribeEvents(bufferingPolicy: .bufferingNewest(1))
Task {
for await event in events {
// ここでイベントを処理する
}
}
SoraEvent.sequence は購読元ごとの通し番号です。値の飛びからイベントの欠落を検出できます。
購読を開始する前に配送されたイベントにも採番されるため、購読者が最初に受け取る値は 1 とは限りません。
BufferingPolicy の詳細については Swift ドキュメント をご確認ください。
購読の開始と終端¶
- 購読を開始する前に配送されたイベントは届きません。 buffer は購読ごとに作られるためです
- 接続の前に購読を開始すれば、
mediaChannelAddedと接続の結果を取り逃しません - 購読の解除は、購読している
Taskの cancel、購読に使ったAsyncStreamへの参照の解放、接続の終了 (MediaChannel)、Soraインスタンスの解放です MediaChannelの購読は、接続の終了でdisconnectedを配送してから終端します。終端後の購読には終端済みのAsyncStreamを返します- 購読している
Taskの loop の中からconnectionIdやdisconnect(error:)などの同期 API を呼べます - イベントの配送スレッドはイベントの発生元によって異なります。
AsyncStreamの再開後に実行される購読者のコードは、購読しているTaskの executor 上で動きます。 UI 更新は main actor / main queue へ束ねてください - 同時に配送されたイベントは、配送順と
sequenceの順序が一致しないことがあります
Sendable なイベントのサンプルコード¶
import Sora
// MARK: - SoraEvent を actor へ渡す
/// イベントを受け取る actor です。
///
/// `Sendable` な `SoraEvent` は actor 境界を越えてそのまま渡せます。payload はすべて値として
/// 確定しているため、受け取ったあとも保持できます。
actor EventStore {
private var events: [SoraEvent] = []
func append(_ event: SoraEvent) {
events.append(event)
}
/// `streamAdded` のイベントからストリーム ID を取り出します。
func addedStreamIds() -> [String] {
events.filter { $0.kind == .streamAdded }.compactMap(\.streamId)
}
}
// MARK: - Sora インスタンスのイベントを購読する
/// `Sora` インスタンスのイベントを購読します。
///
/// 購読を開始する前に配送されたイベントは届かないため、`mediaChannelAdded` と接続の結果を
/// 受け取る場合は接続の前に購読を開始します。
/// 購読を解除する場合は、戻り値の `Task` を cancel します。
func subscribeSoraEvents(store: EventStore) -> Task<Void, Never> {
// 購読者ごとに独立した AsyncStream が返ります
let events = Sora.shared.subscribeEvents()
return Task {
for await event in events {
// SoraEvent は Sendable のため、actor 境界へそのまま渡せます
// 従来の SoraHandlers の onConnect、onDisconnect、onAddMediaChannel、onRemoveMediaChannel、
// onChangeAudioRoute と、Sora.connect の handler に対応するイベントが届きます
await store.append(event)
}
}
}
// MARK: - 接続ごとのイベントを購読する
/// 接続ごとのイベントを購読します。
///
/// 購読を解除する場合は、戻り値の `Task` を cancel します。
/// `MediaChannel` は `Sendable` ではないため、チャネルを actor 境界や `Task` の
/// `@Sendable` クロージャへ渡す場合は `nonisolated(unsafe)` などの隔離が必要です。
/// `MediaChannel.subscribeEvents` が返す `AsyncStream<SoraEvent>` は `Sendable` のため、イベントを渡すだけの場合は
/// 隔離は必要ありません。
func subscribeChannelEvents(mediaChannel: MediaChannel, store: EventStore) -> Task<Void, Never> {
// 購読を開始してから Task へ渡します (購読を開始する前のイベントは届きません)
let events = mediaChannel.subscribeEvents()
return Task {
for await event in events {
switch event.kind {
case .connected:
// 従来の MediaChannelHandlers.onConnect に対応します
await store.append(event)
case .dataChannelMessage:
// 従来の MediaChannelHandlers.onDataChannelMessage に対応します
// (dataChannelLabel と dataChannelMessage を持ちます)
await store.append(event)
case .disconnected:
// 従来の MediaChannelHandlers.onDisconnect に対応します
// (このイベントを配送したあとに購読が終端します)
await store.append(event)
default:
// SDK が種別を追加しても、default があるため既存のコードはビルドできます
// (その他の種別は SoraEventKind を参照してください)
break
}
}
}
}
// MARK: - 購読を解除する
/// 購読した `Task` を保持して、不要になった時点で解除するサンプル実装です。
///
/// 購読を解除する場合は、購読している `Task` を cancel します。`Task` を保持しないと解除する
/// 手段が無くなるため、購読を開始したら保持します。
/// `Task` を cancel すると `for await` のループが終わり、購読者も解除されます。
///
/// `Sora` インスタンスの購読も同じです ( `subscribeSoraEvents` が返す `Task` を保持して cancel します)。
/// `MediaChannel` の購読は、接続が終了すると `disconnected` を配送したあとに購読が終端するため、
/// 接続の終了に合わせて解除する必要はありません。
///
/// `MediaChannel` 自体は `Sendable` ではないため、チャネルを actor 境界や `Task` の
/// `@Sendable` クロージャへ渡す場合は `nonisolated(unsafe)` などの隔離が必要です。
final class ChannelEventSubscription {
nonisolated(unsafe) let mediaChannel: MediaChannel
let store = EventStore()
private var task: Task<Void, Never>?
init(mediaChannel: MediaChannel) {
self.mediaChannel = mediaChannel
}
/// 購読を開始し、解除に使う `Task` を保持します。
func start() {
task = subscribeChannelEvents(mediaChannel: mediaChannel, store: store)
}
/// 購読を解除します。
func stop() {
task?.cancel()
task = nil
}
}