HappyOyster iOS SDKのエントリポイントは、プロセスレベルのシングルトンであるHappyOysterEngine.sharedです。ビジネスメソッドはすべてasync throwsであり、失敗時にはOysterSDKErrorをスローします。単一の体験はOysterTravelハンドルによって管理され、UIKitとSwiftUIの両方で、アドベンチャー、ディレクション、アクティングの3つのモードにまたがります。
注記
- 本文書は、Happy Oyster iOS SDKの外部機能説明 + インターフェースリファレンスです。各パブリック型とメソッドのパラメータ、タイミング、使用方法、および短い例を1つずつ説明しています。
- 完全な統合フロー(プロジェクトのセットアップ、依存関係の設定、サーバー側の連携、エンドツーエンドの実行)については、サンプルプロジェクトのドキュメントを参照してください。ここでは繰り返しません。
1. コアコンセプト
まず、以降の内容を読みやすくするためにいくつかの用語を説明します。
概念 | 説明 |
|---|---|
token | HTTP 認証トークン(Bailian 一時 API キー)。サーバーが Bailian API を介して交換し、配信します。 |
ticket | ワンタイムトライアルクレデンシャル。サーバーがオープンプラットフォームの |
World | キャラクターやシーンを含むAIワールド。サーバーによって作成および管理され、SDKは関与しません。 |
Travel | 1つの |
セッションステータス |
|
モード |
|
2. 概要
この SDK を統合すると、アプリは AI によってリアルタイムに生成される「ワールド」に入り、3つのモード(adventure、directing、または acting)でリアルタイムインタラクティブビデオ体験ができます。体験の開始 → リアルタイム再生 → リアルタイムインタラクション → プロセス制御(一時停止/再開/巻き戻し/終了) → ステータスおよびエラーコールバック。
各モードは異なる機能セットをサポートしています。start()によって返されるmodeに基づいてUIを制御してください:
機能 |
|
|
|
|---|---|---|---|
| はい | いいえ | いいえ |
| いいえ | はい | はい |
| いいえ | はい | はい |
| いいえ | はい | いいえ — エントリポイントを非表示にする |
| はい | はい | はい |
| 適用される | 無視される | 無視される |
注記actingワールドはポートレート優先です:start()はセッション用にaspectRatio(9:16 / 16:9)を返します。ストリームをプルする前に、これを使用してプレーヤーの向きとコンテナサイズを選択してください(§8を参照)。
import HappyOysterSDK。コアとなるエントリポイントは2つの型です。
HappyOysterEngine — オーケストレーションのエントリポイントであり、プロセスレベルのシングルトンHappyOysterEngine.shared。
メソッド / プロパティ | 説明 |
|---|---|
| ランタイムを初期化し、リアルタイムエンジンを自動的に登録します( |
| HTTP認証トークンの注入 / 更新 |
| ワンタイム認証情報でセッションハンドルを作成 |
| リソースを解放( |
| 現在のSDKバージョン |
OysterTravel — createTravel(ticket:)によって作成される、1回の体験用のセッションハンドル。
メソッド / プロパティ | 説明 |
|---|---|
| 再生ビュー(UIKit / SwiftUI) |
| 状態変更 + エラーのプッシュストリーム / 観測可能な現在の状態 |
| 接続して再生(オプションでアドベンチャー体験の最大時間をリクエスト) |
| ディレクティングモードのテキスト指示 |
| アドベンチャーモードの制御 |
| 一時停止 / 再開( |
| 巻き戻し(一時停止時のみ) |
| 終了(冪等性あり、必ず呼び出してください) |
| マイクの一時的な譲渡 / 復元 |
いくつかのヘルパーエクスポートもあります:SDK の内部ログを引き継ぐための OysterLog、バージョンを読み取るための HappyOysterEngine.version、OysterVideoView は SwiftUI 再生ビューです(videoView と同等)。使用方法は後述します。
ライフサイクル:初期化 → トークンの注入 → セッションの作成 → ビデオのマウント + イベントの購読 → 再生の開始 → インタラクション → 終了。SDK はワールドの作成や管理(サーバー側で行われます)を担当せず、低レベルのリアルタイム通信の詳細を公開することもありません。
3. Quick Start
初期化から終了までの完全なフローを、ステップごとのコメント付きで示します。各APIの詳細は§6にあります。
import HappyOysterSDK
// 1) Initialize (as early as possible after app launch, once before createTravel)
let engine = HappyOysterEngine.shared
engine.initialize(config: OysterConfig(
apiHost: "[workspace-id].[region].maas.aliyuncs.com",// For the API host, refer to the Bailian documentation https://www.alibabacloud.com/help/en/model-studio/base-url
model: "happyoyster-1.0-adventure" // Versioned model name enabled for your account, required; see the Happy Oyster model documentation
))
// 2) (Optional) Take over logging: set level + custom output
OysterLog.setMinimumLevel(.info)
OysterLog.setHandler { level, tag, message in
print("[Oyster][\(level)][\(tag)] \(message)")
}
// 3) Inject the HTTP auth token (Bailian temporary API Key, delivered by your server)
engine.updateToken(temporaryApiKey)
// 4) Create a session handle with a one-time ticket (not yet connected)
let travel = try engine.createTravel(ticket: ticket)
Task { @MainActor in
// 5) Mount the video (UIKit; for SwiftUI use OysterVideoView(travel:))
containerView.addSubview(travel.videoView)
// 6) Subscribe to events before start, to avoid missing early statuses
let eventTask = Task {
for await event in travel.events {
switch event {
case .statusChanged(let status): render(status) // §7 status table
case .error(let error): handle(error) // §9 error.code / error.kind
}
}
}
do {
// 7) Connect and play
let data = try await travel.start()
// 8) Interact based on mode
if data.mode == .directing {
_ = try await travel.sendInstruct(content: "Suddenly it starts to pour")
} else {
travel.sendCommand(OysterAdventureCommand(translation: .front))
}
} catch let error as OysterSDKError {
handle(error)
}
// 9) Funnel every exit path into a single end()
_ = try? await travel.end()
eventTask.cancel()
}
// Exit the SDK / switch gateway: await engine.cleanup()
注記プロジェクトのセットアップ、依存関係の設定(必要なAliRTCアダプタとベンダーバイナリを含む)、サーバー側の連携、およびエンドツーエンドの実行については、サンプルプロジェクトのドキュメントを参照してください。
4. 要件
項目 | Requirement |
|---|---|
最小OS | iOS 15.0+(すべてのパブリック型には |
Language | Swift ( |
並行処理 | メインスレッドアクセス(エントリ型には |
Network | パブリックネットワークアクセスが必要 |
権限 |
|
マイクの権限:リアルタイムインタラクティブビデオ体験には、双方向(アップリンク + ダウンリンク)のリアルタイムオーディオ/ビデオチャネル が必要なため、SDK は実行中にローカルマイクを占有します。これは録音ではありません。Info.plist には NSMicrophoneUsageDescription を指定する必要があります。そうしないと、リアルタイムキャプチャの開始時にクラッシュします。マイクへの排他アクセスが必要な場合(例:音声認識)、pauseLocalAudioCapture() / resumeLocalAudioCapture() を使用して一時的に譲渡および復元します(§6.2 を参照)。
5. 統合と認証
5.1 統合と依存関係
コードレベルでは import HappyOysterSDK だけで十分です。SDK はコンパイル済みのバイナリ(xcframework)として CocoaPods の subspecs を介して配布され、パブリックな CocoaPods Trunk に公開されています。Podfile に依存関係を宣言してください:
# HappyOysterSDK / AliVCSDK_ARTC are both published on the public CocoaPods source.
pod 'HappyOysterSDK', '1.0.3' # Aggregate entry point (Core + World)
pod 'HappyOysterSDK/UI', '1.0.3' # Optional default UI components (video view, control HUD)
pod 'HappyOysterSDK/StreamAliRTC', '1.0.3' # Video stream + AliRTC engine adapter (already depends on Stream)
# RTC vendor binary: weak-linked by the SDK, not redistributed with it — bring your own.
pod 'AliVCSDK_ARTC', '7.11.0'
注記HappyOysterSDK/StreamAliRTCをプルインする際は、常に**AliVCSDK_ARTCが必要です**。これが欠落している場合、SDKはサイレントにLoopbackにフォールバックします。接続してrunningに到達することはできますが、エラーなしで黒い画面が表示されます。
プロジェクトのセットアップとエンドツーエンドの初期化フローはサンプルプロジェクトで扱われています。サンプルプロジェクトのドキュメントを参照してください。本文書はインターフェース自体に焦点を当てています。
5.2 認証モデル
SDKはトークンの取得や更新を行わず、軽量に保たれています。認証には2つのレイヤーがあり、インテグレーターがそのライフサイクルを管理します:
- HTTP 認証トークン(Bailian 一時 API キー):サーバーが Bailian API を介して交換し、配信します。
updateToken(_:)を通じて注入されます。一部の内部 SDK サービスはBailian ゲートウェイを直接呼び出し、認証のためにこのトークンを保持します。したがって、これは独自のビジネスサービスのトークンではなく、Bailian が発行した一時 API キーである必要があります。SDK は最新のもののみを保持し、永続化や更新は行いません。有効期限が切れた後、再交換して再注入します。 - ワンタイムトライアルクレデンシャル
ticket:サーバーがオープンプラットフォームのget-travel-credentialを介して交換します(プレフィックスtk_、有効期間 30 分、使い捨て)。createTravel(ticket:)の引数として使用され、体験が(正常または異常に)終了するか、有効期限が切れると無効になり、再利用できません。
注記AK、署名キー、その他の高権限クレデンシャルはサーバー上にのみ存在し、クライアントSDKがそれらに触れることはありません。クライアントが受け取るのは常に短命な一時APIキーです。
注記機能ゲート(リモートスイッチ/強制アップグレード):サーバーは SDK をリモートで無効化したり、サポートされる最低バージョンを設定したりできます。無効化されている間、ゲートされた呼び出し(start / pause / resume / rewind / sendInstruct / sendCommand)は 108001 で拒否され、実行中の体験は SDK によって終了されます(§9 を参照)。OysterSDKError.raw には人間が読める理由が含まれます。バージョンが低すぎる場合は、ユーザーにアップグレードを促します。
トークンの有効期限切れへの対応:上記の HTTP 認証トークンの有効期限が切れた後、サーバーに新しいトークンを即座に要求し、updateToken(_:) を介して注入します。トークンの有効期限が切れているかどうかを判定する必要がある箇所は2つあります:
engine.createTravelやtravel.startなどのAPIを呼び出す際は、エラーを処理し、トークンの期限切れ/無効のエラータイプ(101001/101002)を確認してください。新しいトークンを注入した後、対応するAPIを再度呼び出してください。OysterTravelEventの.errorイベントをリッスンする際、トークンの期限切れ/無効のエラータイプを確認し、トークンを再要求して注入してください。
6. APIリファレンス
エントリ型は2つあり、どちらも@MainActorおよび@available(iOS 15.0, *)です。ビジネスメソッドはasync throwsであり、失敗時にはOysterSDKErrorをスローします。戻り値を持つメソッドはすべて@discardableResultとマークされています。
6.1 HappyOysterEngine
プロセスレベルのシングルトンであり、オーケストレーションのエントリポイントです。initは非パブリックです。常にHappyOysterEngine.sharedを使用してください。自分でインスタンス化しないでください(基盤となるリアルタイムエンジンもプロセスシングルトンです)。
@MainActor @available(iOS 15.0, *)
public final class HappyOysterEngine {
public static let shared: HappyOysterEngine // the unique process-level instance
public static let version: String // current SDK version
@discardableResult
public func initialize(config: OysterConfig) -> Bool // initialize (once before createTravel)
public func updateToken(_ token: String) // inject/update the HTTP auth token
public func createTravel(ticket: String) throws -> OysterTravel // create a session handle with a one-time credential
public func cleanup() async // release resources (can initialize again)
}
initialize(config:)
- 目的:ランタイムを初期化し、リアルタイムエンジンを自動的に登録します(ホスト側での手動登録は不要)。
- パラメータ:
config.apiHostはBailianゲートウェイURLであり、ビジネスサーバーではありません。これは必須です。プレリリース/トライアル環境では、対応するゲートウェイを明示的に渡す必要があり、そうしないとリクエストは失敗します(例:106001、ドメインが解決できない)。config.modelはアカウントで有効化されたバージョン付きモデル名であり、同様にデフォルト値なしで必須です。Happy Oysterはモードごとのサブモデルに分割されているため、SDKはどれを使用するか推測できません。両方とも、注入されたトークンと同じアカウントおよびリージョンに属している必要があります。その他のフィールドは§8OysterConfigを参照してください。 - **1つのモデルは1つの
mode**に対応します。モードごとのモデルはそれぞれ独自のゲートウェイルートを持つため、**1回のinitializeはその1つのmode**のワールドにのみ対応します。アプリが複数のモードのワールドを提供する場合、異なるモードのワールドに入る前に、一致するモデルを指定してinitialize()を再度呼び出すだけです。アイドル状態では最新の構成が優先され、cleanup()は不要で、注入されたトークンは保持されます。Travelの実行中は呼び出しが無視されるため、事前にend()を実行してください。ワールドのmodeと一致しないモデルは、ゲートウェイによってAccessDeniedとして拒否され、106003に正規化されます。 - 使用タイミング:アプリ起動後できるだけ早い段階で、
createTravelの前に1回呼び出します。 - 戻り値:
Bool— 渡したconfigが有効になったかどうか。以下の2つのケースではfalseになります:設定が無効である(apiHost/modelが空白、または有効なゲートウェイ URL を形成していない)、またはトラベルが実行中であるため呼び出しが無視された場合。どちらのケースでも、ランタイムは変更されません。 - 注意:アイドル状態では、再度呼び出すと新しい構成でランタイムが再構成されます(
apiHost/modelの切り替えにcleanup()は不要で、注入されたトークンは保持されます)。Travelの実行中は警告のみを出力するno-opとなるため、事前にend()を実行してください。無効な構成ではランタイムは変更されません。再initializeが成功したかどうかを判断するためにisReadyを使用しないでください。拒否された場合、以前の構成が引き続き有効であり、isReadyはtrueのままです。isReadyは「現在エンジンが使用可能か」に答え、戻り値は「渡した構成が反映されたか」に答えます。
OysterLog
- 目的:SDKの内部ログを設定および引き継ぎ、独自のログモジュールに出力します。レベルを設定するための
setMinimumLevel(_:)と、カスタム出力用のsetHandler(_:)を提供します(§3の例を参照)。
updateToken(_:)
- 目的:HTTP認証トークン(Bailian一時APIキー、§5.2)を注入/更新します。
- 使用タイミング:
initializeの後、いつでも呼び出し可能です。トークンの有効期限が切れた後、または認証関連のエラー(101001/101002)を受け取った後に、トークンを再交換して再度呼び出してください。 - 注釈:
initializeされていない場合、警告と共に何もしません。
createTravel(ticket:)
- 目的:ワンタイムの
ticketを使用して単一のセッションハンドルを作成します。 - パラメータ:
ticketはワンタイムクレデンシャルです。作成されると、この体験のために占有されたとみなされます。 - 使用タイミング:新しい体験の前に呼び出します。返されるハンドルはまだ接続されておらず、その後
travel.start()を呼び出す必要があります。ビデオは返されたハンドルから取得されます(§6.2を参照)。 - 注(同期
throws):initializeされていない場合は100001をスローします。前のTravelがend()される前に再度呼び出された場合は103004をスローします(各エンジンは一度に1つのアクティブなTravelのみを許可します)。
cleanup()
- 目的:SDKリソースを解放します(アクティブなtravel、ランタイム設定、トークンを終了)。
- 使用タイミング:SDKを完全に終了するとき、または
configを変更する必要があるとき。 - 注:
async— 内部的には、まず現在のアクティブなtravelを確実にend()し、その後ランタイムを破棄するため、fire-and-forgetにはなりません。解放後は再度initializeできます。
6.2 OysterTravel
createTravel によって作成されるセッションハンドル。使い捨てであり、終端状態(end / サーバー側の終了 / 失敗)に達すると無効化され、エンジン経由で新しい createTravel が必要になります。これは ObservableObject(@Published status、SwiftUI を直接駆動可能)でもあります。
@MainActor @available(iOS 15.0, *)
public final class OysterTravel: ObservableObject {
@Published public private(set) var status: OysterTravelStatus // current external status (observable)
public var isEnded: Bool { get } // whether a terminal state is reached (synchronously readable)
public var videoView: UIView { get } // UIKit playback view; for SwiftUI use OysterVideoView(travel:)
public var events: AsyncStream<OysterTravelEvent> { get } // status change + error, multi-subscribe
@discardableResult public func start() async throws -> OysterStartTravelData // connect and play
@discardableResult public func start(maxExperienceTimeSec: Int?) async throws -> OysterStartTravelData // connect and play + request max experience duration (adventure mode only)
@discardableResult public func pause() async throws -> OysterTravelStateData // pause (directing / acting)
@discardableResult public func resume() async throws -> OysterTravelStateData // resume
@discardableResult public func rewind(toSec: TimeInterval) async throws -> OysterRewindTravelData // rewind (paused only)
@discardableResult public func end() async throws -> OysterEndTravelData // end (idempotent, be sure to call)
@discardableResult public func sendInstruct(content: String) async throws -> OysterSendInstructData // directing-mode text instruction
public func sendCommand(_ command: OysterAdventureCommand) // adventure-mode control (fire-and-forget)
public func flushCommands() // manually send an already-submitted pending command
public func pauseLocalAudioCapture() async // temporarily yield the microphone
public func resumeLocalAudioCapture() async // restore microphone occupation
}
ビデオビュー videoView / OysterVideoView(travel:)
- 目的:リモート画像のレンダリングエントリ。「SDKがビューを提供し、ホストがそれを配置する」 UIKitの場合は
travel.videoViewを取得し、SwiftUIの場合はOysterVideoView(travel:)を使用します。 - 使用タイミング:ハンドルが作成されるとすぐに利用可能になります(繰り返しアクセスしても同じビューが返されます)。任意の階層にマウントでき、エンジンが準備できれば自動的にレンダリングされます。
start()の前後どちらでマウントしても機能し、黒画面にはなりません。 - 注:セッションが終了すると、SDKは自動的にレンダリングバインディングを解放します。必要に応じてビューを階層から削除してください。
イベントとステータス events / status / isEnded
- 目的:
eventsは状態変更 + エラーのプッシュストリームです。statusは観測可能な現在の外部ステータスです。isEndedは終端状態の同期的に読み取り可能なフラグです。 - 使用タイミング:初期のステータスを見逃さないように、**
start()の前にevents**の消費を開始することをお勧めします。 - 注意:
eventsへの各アクセスは独立したストリームを返し、複数購読をサポートします。購読解除 =for awaitイテレーションの終了(または保持されているTaskの破棄)を意味します。SwiftUI では、@StateObject/@ObservedObjectを使用してstatusを直接監視できます(エラーは引き続きeventsを通じて通知されます)。§7 を参照してください。
start() / start(maxExperienceTimeSec:)
- 目的:作成時にキャプチャした
ticketを使用して、トラベル + RTC 参加設定と交換し、接続して再生します。成功すると、SDK は自動的にリアルタイム接続を確立し、内部ステータスポーリングを開始します。ステータスはeventsを通じて表面化されます。 - パラメータ:
maxExperienceTimeSec(オプション)— **このアドベンチャー体験の最大持続時間(秒単位)**をリクエストします。値はそのままサーバーに送信されます。許可される値、実際の有効持続時間、および自動終了のタイミングはすべてサーバーによって決定されます。SDKはローカルでの検証を行いません。nilを渡す(またはパラメータなしのstart()を呼び出す)と、サーバーのデフォルト持続時間が使用されます。**ディレクションモードではこのパラメータは無視されます。**時間が終了すると、サーバーが体験を終了し、ホストはeventsを介してended終了状態を受信します(サーバー側の終了と同じ、§7を参照)。 - 戻り値:
OysterStartTravelData(mode/version/encryptedTravelIdなど、§8を参照)。これによりインタラクションUIが決定されます。 - エラー:
401010/401011(クレデンシャルが無効/使用済み)、403002(ワールドの準備ができていない)、403007(サービス仕様が有効化されていない、例:acting)、429001/429002(同時実行制限/容量枯渇)、500001(リソース/サーバー障害)、103004(同時開始)。 ticketのワールドmodeは、initializeに渡されたmodelと一致している必要があります(呼び出し元の責任)。モードごとのモデルでは、各モデルが独自のゲートウェイルートを持ち、start()はticketを現在初期化されているモデルのルートに送信します。SDKはこれを事前に検証しませんし、検証することもできません****。modeは、このstart()呼び出し自体の応答(OysterStartTravelData.mode)によって提供されます。呼び出し前、SDKは不透明なticketとモデル名のみを保持しており、比較対象のモードはなく、モデル名からモードを推測することはサーバー固有の命名規則を推測することになり、SDKはそれを行いません。したがって、異なるモードのワールドに入る前に、一致するモデルを指定してinitialize()を再度呼び出してください(アイドル状態では最新の構成が優先され、cleanup()は不要でトークンは保持されます。Travelの実行中は呼び出しが無視されるため、事前にend()を実行してください)。不一致の場合、このstart()はゲートウェイで失敗します。診断時は、まず現在のmodelとticketのワールドのmodeが一致していることを確認し、上記の認証情報エラーコードを参照してください。- 注意(ストリームなし時の自動終了):サーバーは「ストリームなしタイムアウト」(デフォルト約 30 秒)を送信します。接続後、その期間内にストリームが受信されない場合(
runningに到達しない場合)、SDK は自動的に体験を終了し、failedに遷移し、eventsの.errorを通じて105006を表面化させます(致命的。開始前の画面に戻ることで対応し、自分でタイミングを計る必要はありません)。
pause() / resume()
- 目的:体験の一時停止/再開(冪等性あり)。
- 使用タイミング:
directingおよびactingワールドでサポートされていますが、adventureではサポートされていません。start()によって返されるmodeを使用して、一時停止ボタンを表示するかどうかを事前に決定します。pauseは現在の状態がrunningである必要があります。resumeはpausedである必要があります。 - エラー:
103001(アクティブな体験なし)、103002(状態/バージョンが許可されていない)、103003(モードの不一致)。
rewind(toSec:)
- 目的:指定された秒数まで巻き戻します。成功すると、SDKは元のrtcConfigで自動的に再参加し、再生に戻ります。
- 使用タイミング:****
paused状態でのみ開始でき、directingワールドによってのみ実行できます。actingとadventureは巻き戻しをサポートしていないため、これらのモードでは巻き戻しエントリポイントを非表示にし、呼び出さないでください。 - エラー:
103001、103002。
end()
- 目的:体験を終了します(冪等性があり、繰り返し呼び出し可能)。呼び出しが成功するか、異常終了した後、SDK は自動的にリアルタイム接続を切断し、ポーリングを停止し、すべてのセッションリソースを解放します。同時に
ticketが無効化され、ハンドルは終端状態に入ります。 - 使用タイミング / 注意:ユーザーが能動的に終了する場合でも、体験が受動的に終了する場合でも(タイマーの満了、
.ended/.failedの受信、ページの破棄)、必ず単一のend()に到達するようにしてください。そうしないと、リモートリソースが速やかに解放されない可能性があります。すべての終了パスを同じ冪等なクリーンアップメソッドに集約することをお勧めします。
sendInstruct(content:)(ディレクティングモード)
- 目的:ストーリーを進行させるためのテキスト指示を送信します。
- 使用タイミング:ディレクティングモード。
runningのときは直接送信され、pausedのときはキャッシュされ、再接続によりrunningに再開した後の最初のフレームで再送信されます。 - エラー:
103001、103002、103003(アドベンチャーモードで呼び出された場合)、403004(コンテンツモデレーション)、404000(travelが見つからない)。
sendCommand(_:) / flushCommands()(アドベンチャーモード)
sendCommand:方向/視点/アクション制御コマンドを送信します(§8OysterAdventureCommandを参照。ファイアアンドフォーゲット、戻り値なし、スローなし)。runningの状態にある adventure モード でのみ有効です。外部入力は毎フレーム高頻度になる可能性があり、SDK は内部でスロットリングを行います(最新優先のサンプリング、RTC ラインレートでのフレームマージ)。ホスト側でスロットリングを行う必要はありません。サーバーのコマンドへの応答自体に遅延があるため、実際の有効時間は固定されていない点に注意してください。flushCommands:「キーリリース/入力リリース」の瞬間に呼び出され、キュー内ですでに待機している最後のコマンドを即座に再送信します。保留中のコマンドがない場合は純粋な no-op となり、新しいコマンドは生成されません。- エラー(すべて
throwsではなくeventsの.errorを介して表面化):アクティブな体験なし103001、ディレクティングモード103003で呼び出された、リアルタイムチャネルの準備ができていない / 送信失敗105004。
pauseLocalAudioCapture() / resumeLocalAudioCapture()
- 目的:ローカルマイクキャプチャに対するSDKの占有を一時的に解放/復元します。
- 使用タイミング:音声認識などがマイクへの排他アクセスを必要とする場合、まず
pauseし、その後resumeします。
7. イベントとステータス
イベントは OysterTravel.events にアタッチされており、SDK からユーザーへのアクティブプッシュチャネルであり、独自の呼び出しによってトリガーされない状況(内部で管理されるリアルタイム接続やステータスポーリングの問題など)を表面化させるために使用されます。
var events: AsyncStream<OysterTravelEvent> { get }
@available(iOS 15.0, *)
public enum OysterTravelEvent {
case statusChanged(OysterTravelStatus)
// The SDK's internal flow errored, e.g. an internal API request error or a streaming error; note you must check for token expiration here and re-request the token
case error(OysterSDKError)
}
消費スタイルは2種類あります。いずれかを選択してください:
- SwiftUI:
OysterTravelはObservableObjectです。@StateObject/@ObservedObjectを直接使用してstatusを監視し、UIを制御してください。エラーは引き続きeventsから発生します。 - 命令型 / UIKit:
Task内でfor await event in travel.events { ... }を実行し、.statusChanged/.errorに対してswitchを行います。完了したら保持しているTaskをキャンセルしてください。
let task = Task {
for await event in travel.events {
switch event {
case .statusChanged(let status): render(status) // see table below
case .error(let error): handle(error) // error.code / error.kind, see §9
}
}
}
// When done: task.cancel()
OysterTravelStatus(5つのプロセス状態 + 2つの終端状態):
ステータス | 説明 | 一般的な処理 |
|---|---|---|
| 作成後、開始前 | — |
| 接続中 / 再接続中(内部の接続 / 再接続) | 接続中 / 再接続中のヒントを表示 |
| ストリーム準備完了、インタラクティブ(内部再生中) | 画像とコントロールを表示 |
| 一時停止が受け付けられました、サーバーの確認待ち | 「一時停止中…」を表示 |
| 一時停止(確認済み) | 一時停止状態を表示( |
| 終了(アクティブな終了またはサーバー側の終了)。終端 | 終了処理を行いページを閉じる |
| 失敗。終端 | エラーを表示して終了処理 |
注記ended / failed に入ると、セッションは終了し、すべてのセッション操作(pause/resume/sendCommand…)はそれ以降効果を発揮しません。コールバックはメインスレッドでトリガーされる可能性があるため、UI を直接更新できます。
注記内部ステータスポーリング中、SDK はクライアントのプル/再生状態のハートビートを自動的に報告します(接続中/再生中/一時停止/再接続中/切断)。これにより、サーバーはストリーム側の状態とクライアント側の状態を区別できます。これは純粋に SDK 内部の動作であり、ホストが認識したり関与したりする必要はありません。
8. データモデル
注記すべてのパブリック型には @available(iOS 15.0, *) が付けられています。以下の戻り値/パラメータは SDK 出力 であり、ネイティブな Swift 型(Date / TimeInterval / OysterTravelStatus)を使用して内部でインプレース構築されます。これらは **** Codable ではなく、ワイヤ(snake_case)の詳細を公開しません。ワイヤのデコードは SDK 内部で行われます。
// Configuration
public struct OysterConfig: Sendable {
public let apiHost: String // Bailian gateway URL (not a business server), required, passed explicitly by the host
public let model: String // Versioned model name (e.g. "happyoyster-1.0-adventure"), required, no default
public let logLevel: OysterLogLevel? // .debug/.info/.warning/.error; defaults to .warning when nil
public let callbackTimeoutMs: Int // global callback timeout, default 30000; surfaced as 105005 on timeout
public init(apiHost: String, model: String, logLevel: OysterLogLevel? = nil, callbackTimeoutMs: Int = 30_000)
}
public enum OysterLogLevel: Int, Comparable, CaseIterable, Sendable {
case debug, info, warning, error
}
// External session status (§7)
public enum OysterTravelStatus: String, Equatable, Sendable, CustomStringConvertible {
case idle // after create, before start
case prepare // connecting / reconnecting
case running // stream ready, interactive
case pausing // pause accepted, awaiting server confirmation
case paused // paused (confirmed)
case ended // terminal: active end or server-side end
case failed // terminal: failed
}
// Open string value (preserves unknown values for server-side extension; encoded/decoded as a bare JSON string "running")
public struct OysterRawValue: RawRepresentable, Equatable, Hashable, Codable, Sendable {
public let rawValue: String
public init(rawValue: String) { self.rawValue = rawValue }
}
public typealias OysterModeValue = OysterRawValue
public extension OysterModeValue {
static let adventure = OysterModeValue(rawValue: "adventure")
static let directing = OysterModeValue(rawValue: "directing")
static let acting = OysterModeValue(rawValue: "acting")
}
// start() return (SDK output, not Codable)
public struct OysterStartTravelData: Equatable, Sendable {
public let encryptedTravelId: String // identifier for this experience
public let encryptedWorldId: String
public let mode: OysterModeValue // adventure / directing / acting
public let playUrl: String? // currently always returned as null by the server
public let firstFrame: String? // first-frame image URL, may be null (produced asynchronously)
public let version: String // world version identifier (diagnostics; drive interaction UI from `mode`)
public let aspectRatio: String? // "9:16" / "16:9"; only set for acting worlds, nil otherwise
}
// Control API returns (SDK output, not Codable; status is the external enum OysterTravelStatus)
public struct OysterTravelStateData: Equatable, Sendable { public let encryptedTravelId: String; public let status: OysterTravelStatus }
public struct OysterRewindTravelData: Equatable, Sendable { public let encryptedTravelId: String; public let status: OysterTravelStatus; public let resumedAtSec: TimeInterval }
public struct OysterEndTravelData: Equatable, Sendable { public let encryptedTravelId: String; public let status: OysterTravelStatus; public let endedAt: Date; public let duration: TimeInterval }
public struct OysterSendInstructData: Equatable, Sendable { public let encryptedTravelId: String; public let content: String; public let accepted: Bool }
// Adventure-mode control command (strongly typed enums; values aligned with the internal WorldControlParams)
public struct OysterAdventureCommand: Equatable, Sendable {
public enum Translation: String { case none, front, back, left, right, frontLeft, frontRight, backLeft, backRight }
public enum Rotation: String { case none, mouseUp, mouseDown, mouseLeft, mouseRight, mouseUpLeft, mouseUpRight, mouseDownLeft, mouseDownRight }
public enum Interaction: String { case none, jump, attack, crouch, sprint }
public let translation: Translation // movement for this command; submit every frame while held, then stop
public let rotation: Rotation // view rotation for this command; submit every frame while held, then stop
public let interaction: Interaction // one-shot action for this command; submit once
public init(translation: Translation = .none, rotation: Rotation = .none, interaction: Interaction = .none)
public init(_ params: WorldControlParams) // convenient bridge from the default control WorldControlParams (consistent rawValue)
}
// Unified error (§9)
public struct OysterSDKError: Error {
public let code: Int
public let raw: Any? // the SDK's internal raw error info; structure is not guaranteed stable, for logging/diagnostics only
public var kind: OysterErrorKind // typed view of the numeric code, for exhaustive switch (computed property)
public init(code: Int, raw: Any? = nil)
// Error code constants are in the nested OysterSDKError.Code (e.g. .notInitialized = 100001)
}
// Typed semantic view of error codes: local codes are named cases; server-side 4xxxxx/5xxxxx converge to .server(code:)
public enum OysterErrorKind: Equatable, Sendable {
case notInitialized, tokenMissing, tokenInvalid, noActiveTravel, invalidState, sendCommandInDirecting
case concurrentTravel, realtimeConnectFailed, realtimeJoinTimeout, firstFrameTimeout
case channelNotReady, callbackTimeout, localNetwork, responseDecodeFailed
case streamAutoEnd, proxyOrUnrecognized, featureGateDisabled
case server(code: Int), unknown(code: Int)
}
注記
- 注:コマンドのrawValueはローワーキャメルケースです(例:
front/mouseLeft/jump)。上記の列挙値が正式なものです。 - 注:
modeは外部ではadventure(wander) /directing(ストーリー) /acting(ロールプレイング)となります。OysterModeValueの定義が正式なものです。 - 注:
aspectRatioはオープンな文字列です(現在は9:16/16:9ですが、今後追加される可能性があります)。既知の値と一致させるのではなく、width:heightとしてパースして比率を比較してください。
9. エラーコード
SDK はエラーを一様に OysterSDKError として報告し、その型は常に code によって区別されます。「エラーがどのパスから来たか」で型を判定しないでください。同じ code がビジネスメソッド(async throws)によってスローされることもあれば、events の .error を通じて表面化されることもあります。型付きマッチングには error.kind を使用します(§8 OysterErrorKind を参照)。
エラーコード:サーバー 4xxxxx/5xxxxx、クライアントローカル 1xxxxx。
サーバーエラーコード(共通)
code | 意味 | 推奨される処理 |
|---|---|---|
| 無効なパラメータ(無効な列挙値など) | リクエストパラメータまたはSDKバージョンを確認 |
| 体験クレデンシャル( | サーバーに認証情報を再発行させる |
| 体験クレデンシャル( | 使い捨ての認証情報、再発行が必要 |
| ワールドが存在しない、削除された、または現在の開発者に属していない(クレデンシャル発行後に削除されたワールドを含む) | 有効なWorldを再度選択 |
| ワールドが準備未完了 | 開始前にワールドの準備が完了するのを待つ |
| このAPIはプライマリAPIキーのみ許可します | このAPIには一時キーを使用できません |
| コンテンツモデレーションによって入力コンテンツが拒否されました。 | 入力を変更して再試行 |
| リクエストされたサービス仕様が有効になっていません | 容量がいっぱいであるかのように再試行しないでください。有効な仕様に切り替えてください(通常:アカウントにacting仕様がない) |
| キャパシティ構成が一時的に利用できません | 後ほど再試行 |
| リソースが存在しません(world/wanderの所有権またはアーティファクトなし) | ID / ステータスの確認 |
| リクエストが現在のリソース状態と競合しています | 体験の状態を確認 |
| この仕様の並行処理制限に達しました | 既存のセッションが終了した後に再試行してください( |
| 利用可能なキャパシティが不足しています | 後ほど再試行 |
| 内部システムエラー | 後ほど再試行 / 報告 |
| 推論リソースの割り当てまたは内部サービス障害 | 後ほど再試行 |
クライアントローカルエラーコード
code | 意味 | SDKがセッションを自動終了 | 推奨される処理 |
|---|---|---|---|
| SDK初期化前に呼び出された場合。 | いいえ(同期的にスロー、この呼び出しを拒否) | まず |
| HTTP認証トークンが注入されていません | いいえ |
|
| HTTP認証トークンが無効 / 拒否されました | いいえ | トークンを再交換してから |
| 現在アクティブな体験はありません | いいえ(この呼び出しを拒否) | まず |
| 現在の状態/バージョンではこの操作は許可されていません | いいえ(この呼び出しを拒否) | 体験の状態 / |
| モードの不一致(例:非 | いいえ(この呼び出しを拒否) |
|
| 体験の同時作成 / 開始 | いいえ(同期的にスロー) | 呼び出しを直列化し、まず古いセッションを |
| リアルタイム接続に失敗 | はい | 終了して再開 |
| リアルタイム参加タイムアウト | はい | 終了して再開 |
| 最初のビデオフレーム待機中のタイムアウト | はい | 終了して再開 |
| リアルタイムチャネルが準備未完了 / 送信失敗 | 場合による(アクティブな送信失敗、ハートビートは報告のみ) |
|
| コールバックタイムアウト(デフォルト30秒) | いいえ | 再試行し、必要に応じて |
| 参加後にストリームがありません。SDKが体験を自動終了します | はい | 終了して再開 |
| ローカルネットワークエラー | いいえ | 再試行可能 |
| レスポンスの解析に失敗 | 依存する | SDKのアップグレード / 報告 |
| 認識可能なエラーコードがない / プロキシ文字列エラーコード | いいえ | 再試行可能 |
| サーバーの機能スイッチによってリモートで無効化されています(完全シャットダウンまたはバージョンが低すぎる。理由は | はい |
|
致命性の判定方法:致命性はブール型フィールドとしては公開されなくなりました(OysterSDKError には isFatal がありません)。意味的に、「fatal」は具体的にSDK がセッションを積極的に終了させるかどうか(RTC の切断、セッション全体の解放)を指します—
- セッションを自動終了させるエラー(例:
105001/105002/105003/105006/108001):ホストはこれをステートマシンの終端状態(status → failed、eventsの.statusChangedを通じて表面化)から認識し、それに応じて「体験開始」前の画面に戻ります。致命性自体を判定する必要はありません。 - 呼び出し拒否エラー(
100001/103001/103002/103003/103004):同期的にスローされる / 能動的に呼び出したときに呼び出しが拒否される。これらによってセッションが終了することはありません。 - その他の非致命的エラー(例:
101001/101002/105005/106001/106003):セッションは終了しません。提案に従って再試行するか、トークンを再注入後に続行してください。
注記**106001**には2つの原因が考えられます。ローカルネットワークエラー、または不正なapiHostです。再試行してもリクエストが回復しない場合は、apiHostが正しく設定されているか確認してください。
注記model を設定した後に 106003 が表示される場合、通常はモデル名/バージョンが間違っているか、アカウントで有効化されていないことを意味します。モデル、apiHost、およびトークンを一緒に確認してください。これらはすべて同じアカウントとリージョンに属している必要があります。不一致の場合、ゲートウェイは AccessDenied で拒否し、このコードに正規化されます。