The HappyOyster iOS SDK entry point is the process-level singleton HappyOysterEngine.shared . Business methods are all async throws , throwing OysterSDKError on failure. A single experience is carried by an OysterTravel handle, spanning three modes — adventure, directing, and acting — on both UIKit and SwiftUI.
Note
- This document is the external feature description + interface reference for the Happy Oyster iOS SDK: it explains the parameters, timing, usage, and short examples of each public type and method, one by one.
- For the complete integration flow (project setup, dependency configuration, server-side coordination, end-to-end run), see the sample project documentation; it is not repeated here.
1. Core Concepts
A few terms first, to make the rest easier to read.
Concept | Description |
|---|---|
token | HTTP auth token (Bailian temporary API Key). Your server exchanges it via the Bailian API and delivers it down; it is injected through |
ticket | One-time trial credential. Your server exchanges it via the open platform |
World | An AI world, containing characters and scenes. Created and managed by your server; the SDK is not involved. |
Travel | A single real-time experience, corresponding to one |
Session status |
|
Mode |
|
2. Overview
After integrating this SDK, your app can enter "worlds" generated in real time by AI, and have a real-time interactive video experience in three modes — adventure, directing, or acting: start the experience → real-time playback → real-time interaction → process control (pause/resume/rewind/end) → status and error callbacks.
Each mode supports a different set of capabilities. Drive your UI from the mode returned by start():
Capability |
|
|
|
|---|---|---|---|
| Yes | No | No |
| No | Yes | Yes |
| No | Yes | Yes |
| No | Yes | No — hide the entry point |
| Yes | Yes | Yes |
| Applied | Ignored | Ignored |
Noteacting worlds are portrait-first: start() returns an aspectRatio (9:16 / 16:9) for the session. Use it to pick the player orientation and container size before pulling the stream (see §8).
import HappyOysterSDK; the core entry points are two types.
HappyOysterEngine — the orchestration entry point, a process-level singleton HappyOysterEngine.shared.
Method / Property | Description |
|---|---|
| Initialize the runtime and automatically register the real-time engine (once, before |
| Inject / update the HTTP auth token |
| Create a session handle with a one-time credential |
| Release resources (can |
| The current SDK version |
OysterTravel — the session handle for one experience, created by createTravel(ticket:).
Method / Property | Description |
|---|---|
| Playback view (UIKit / SwiftUI) |
| Status-change + error push stream / observable current status |
| Connect and play (optionally request the maximum duration of an adventure experience) |
| Directing-mode text instruction |
| Adventure-mode control |
| Pause / resume ( |
| Rewind (paused only) |
| End (idempotent, be sure to call it) |
| Temporarily yield / restore the microphone |
There are also a few helper exports: OysterLog for taking over the SDK's internal logging; HappyOysterEngine.version for reading the version; OysterVideoView is the SwiftUI playback view (equivalent to videoView). Usage is described later.
Lifecycle: initialize → inject token → create session → mount video + subscribe to events → start playback → interact → end. The SDK is not responsible for creating or managing worlds (done by your server), nor does it expose low-level real-time communication details.
3. Quick Start
A complete flow from initialization to ending, with step-by-step comments. Details of each API are in §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?spm=a2c4g.11186623.help-menu-2400256.d_0_0_4.171c56fftIRQRt&scm=20140722.H_3042998._.OR_help-T_cn~zh-V_1
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()
NoteFor project setup, dependency configuration (including the required AliRTC adapter and vendor binary), server-side coordination, and an end-to-end run, see the sample project documentation.
4. Requirements
Item | Requirement |
|---|---|
Minimum OS | iOS 15.0+ (all public types are marked |
Language | Swift ( |
Concurrency | Main-thread access (entry types are marked |
Network | Public network access required |
Permissions |
|
Microphone permission: A real-time interactive video experience needs a bidirectional (uplink + downlink) real-time audio/video channel, so the SDK occupies the local microphone while running — this is not recording. Info.plist must provide NSMicrophoneUsageDescription, otherwise starting real-time capture will crash. When you need exclusive microphone access (e.g. speech recognition), use pauseLocalAudioCapture() / resumeLocalAudioCapture() to temporarily yield and restore it (see §6.2).
5. Integration and Authentication
5.1 Integration and Dependencies
import HappyOysterSDK is all you need at the code level. The SDK is distributed as a precompiled binary (xcframework) via CocoaPods subspecs, published to the public CocoaPods Trunk — declare the dependencies in your 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'
NoteAliVCSDK_ARTC is required whenever you pull in HappyOysterSDK/StreamAliRTC: if it's missing, the SDK silently falls back to Loopback — it can connect and reach running, but shows a black screen with no error.
Project setup and the end-to-end initialization flow are handled by the sample project; see the sample project documentation. This document focuses on the interface itself.
5.2 Authentication Model
The SDK does not obtain or refresh tokens, keeping things lightweight. Authentication has two layers, and the integrator manages their lifecycles:
- HTTP auth token (Bailian temporary API Key): Your server exchanges it via the Bailian API and delivers it down; it is injected through
updateToken(_:). Some internal SDK services call the Bailian gateway directly, carrying this token for authentication — therefore it must be a temporary API Key issued by Bailian, not a token from your own business service. The SDK keeps only the latest one, does not persist or refresh it; after it expires, you re-exchange and re-inject it. - One-time trial credential
ticket: Your server exchanges it via the open platformget-travel-credential(prefixtk_, valid for 30 minutes, single-use), used as thecreateTravel(ticket:)argument; it becomes invalid once the experience ends (normally or abnormally) or expires, and cannot be reused.
NoteAK, signing keys, and other high-privilege credentials exist only on your server; the client SDK never touches them. What the client receives is always a short-lived temporary API Key.
NoteFeature Gate (remote switch / forced upgrade): The server can remotely disable the SDK or set a minimum supported version. While disabled, gated calls (start / pause / resume / rewind / sendInstruct / sendCommand) are rejected with 108001 and any in-flight experience is terminated by the SDK (see §9). OysterSDKError.raw carries a human-readable reason; prompt the user to upgrade when the version is too low.
Handling token expiration: After the HTTP auth token above expires, immediately request a new token from your server and inject it via updateToken(_:). There are two places where you need to determine whether the token has expired:
- When calling APIs such as
engine.createTravelortravel.start, handle the error and check for the token-expired/invalid error types (101001/101002); after injecting a new token, re-call the corresponding API. - When listening to the
.errorevent ofOysterTravelEvent, check for the token-expired/invalid error types, re-request the token, and inject it.
6. API Reference
There are two entry types, both @MainActor and @available(iOS 15.0, *). Business methods are async throws and throw OysterSDKError on failure; methods with return values are all marked @discardableResult.
6.1 HappyOysterEngine
A process-level singleton, the orchestration entry point. init is non-public — always use HappyOysterEngine.shared; do not instantiate it yourself (the underlying real-time engine is also a process singleton).
@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:)
- Purpose: Initialize the runtime and automatically register the real-time engine (no manual registration needed by the host).
- Parameters:
config.apiHostis the Bailian gateway URL, not your business server, and is required; in pre-release/trial environments you must explicitly pass the corresponding gateway, otherwise requests fail (e.g.106001, domain cannot be resolved).config.modelis the versioned model name enabled for your account and is likewise required with no default — Happy Oyster is split into per-mode sub-models, so the SDK cannot infer which one to use. Both must belong to the same account and region as the injected token. Other fields are in §8OysterConfig. - One model serves one
mode: each per-mode model is its own gateway route, so a singleinitializeonly serves worlds of that onemode. If your app offers worlds in several modes, just callinitialize()again with the matching model before entering a world of a different mode — while idle the latest config wins, nocleanup()is needed and the injected token is kept; while a Travel is in flight the call is ignored, soend()it first. A model that does not match the world'smodeis rejected by the gateway withAccessDenied, normalized to106003. - When to use: Call once before
createTravel, as early as possible after app launch. - Returns:
Bool— whether theconfigyou passed took effect. It isfalsein two cases: the config is invalid (apiHost/modelblank or not forming a valid gateway URL), or a Travel is in flight so the call was ignored. In both cases the runtime is left unchanged. - Note: While idle, calling it again re-configures the runtime with the new config (switching
apiHost/modelneeds nocleanup(), and the injected token is kept); it is a no-op with a warning only while a Travel is in flight, soend()it first. An invalid config leaves the runtime unchanged. Do not useisReadyto tell whether a re-initializesucceeded — if it was rejected the previous config is still in effect andisReadyremainstrue;isReadyanswers "is the engine usable now", the return value answers "did the config I just passed take effect".
OysterLog
- Purpose: Configure and take over the SDK's internal logging, printing it into your own logging module. Provides
setMinimumLevel(_:)to set the level andsetHandler(_:)for custom output (see the §3 example).
updateToken(_:)
- Purpose: Inject / update the HTTP auth token (Bailian temporary API Key, §5.2).
- When to use: After
initialize, callable at any time; re-exchange and call again after the token expires or after receiving an auth-related error (101001/101002). - Note: A no-op with a warning when not
initialized.
createTravel(ticket:)
- Purpose: Create a single session handle with a one-time
ticket. - Parameter:
ticketis a one-time credential; once created, it is considered occupied for this experience. - When to use: Call before each new experience; the returned handle is not yet connected and you must call
travel.start()afterwards. The video is taken from the returned handle (see §6.2). - Note (synchronous
throws): throws100001if notinitialized; throws103004if called again before the previous Travel hasend()ed (each engine allows only one active Travel at a time).
cleanup()
- Purpose: Release SDK resources (end the active travel, runtime config, token).
- When to use: When fully exiting the SDK or needing to change the
config. - Note:
async— internally it deterministicallyend()s the current active travel first, then tears down the runtime, leaving no fire-and-forget. After release you caninitializeagain.
6.2 OysterTravel
The session handle created by createTravel; single-use, invalidated once a terminal state (end / server-side end / failure) is reached, requiring a fresh createTravel via the engine. It is also an ObservableObject (@Published status, can directly drive 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
}
Video view videoView / OysterVideoView(travel:)
- Purpose: The rendering entry for the remote picture — "the SDK provides the view, the host places it". For UIKit, take
travel.videoView; for SwiftUI, useOysterVideoView(travel:). - When to use: Available as soon as the handle is created (repeated access returns the same view); mount it into any hierarchy, and once the engine is ready it renders automatically. Mounting before or after
start()both work, with no black screen. - Note: When the session ends, the SDK automatically releases the rendering binding; remove the view from the hierarchy as needed.
Events and status events / status / isEnded
- Purpose:
eventsis the push stream of status changes + errors;statusis the observable current external status;isEndedis a synchronously readable flag for the terminal state. - When to use: It is recommended to start consuming
eventsbeforestart(), to avoid missing early statuses. - Note: Each access to
eventsreturns an independent stream, supporting multi-subscription; unsubscribing = ending thefor awaititeration (or destroying the heldTask). In SwiftUI you can directly observestatuswith@StateObject/@ObservedObject(errors still come throughevents). See §7.
start() / start(maxExperienceTimeSec:)
- Purpose: Use the
ticketcaptured at create time to exchange for travel + RTC join configuration and connect to play. On success the SDK automatically establishes the real-time connection and begins internal status polling, surfacing status throughevents. - Parameter:
maxExperienceTimeSec(optional) — requests the maximum duration (in seconds) of this adventure experience. The value is sent to the server as-is; the allowed values, the actual effective duration, and the auto-end timing are all decided by the server — the SDK performs no local validation. Passingnil(or calling the parameterlessstart()) uses the server's default duration. Directing mode ignores this parameter. When the time is up, the server ends the experience and the host receives theendedterminal state viaevents(same as a server-side end, see §7). - Returns:
OysterStartTravelData(mode/version/encryptedTravelId, etc., see §8), which determines the interaction UI. - Errors:
401010/401011(credential invalid/used),403002(world not ready),403007(service specification not enabled, e.g. acting),429001/429002(concurrency limit / capacity exhausted),500001(resource/server failure),103004(concurrent start). - The
ticket's worldmodemust match themodelpassed toinitialize(caller's responsibility): with per-mode models, each model is its own gateway route, andstart()sends theticketto the route of the currently initialized model. The SDK does not and cannot verify this for you beforehand —modeis delivered by the response to this verystart()call (OysterStartTravelData.mode); before the call the SDK holds only an opaqueticketand a model name, with no mode to compare against, and inferring the mode from the model name would be guessing at a server-owned naming scheme, which the SDK does not do. So: before entering a world of a different mode, callinitialize()again with the matching model (while idle the latest config wins — nocleanup()needed and the token is kept; while a Travel is in flight the call is ignored, soend()it first). On a mismatch thisstart()fails at the gateway; when diagnosing, first check that the currentmodeland themodeof theticket's world belong together, then consult the credential error codes above. - Note (auto-end on no stream): The server sends a "no-stream timeout" (default ~30s). If, after connecting, no stream is received within that duration (it never reaches
running), the SDK automatically ends the experience, transitions tofailed, and surfaces105006via the.errorofevents(fatal; handle by returning to the pre-start screen, no need to time it yourself).
pause() / resume()
- Purpose: Pause / resume the experience (idempotent).
- When to use: Supported by
directingandactingworlds; not supported byadventure. Use themodereturned bystart()to decide in advance whether to show a pause button.pauserequires the current state to berunning;resumerequirespaused. - Errors:
103001(no active experience),103002(state/version not allowed),103003(mode mismatch).
rewind(toSec:)
- Purpose: Rewind to the specified seconds. On success the SDK automatically rejoins with the original rtcConfig and returns to playback.
- When to use: Can only be initiated in the
pausedstate, and only bydirectingworlds —actingandadventuredo not support rewind, so hide the rewind entry point in those modes and do not call it. - Errors:
103001,103002.
end()
- Purpose: End the experience (idempotent, can be called repeatedly). After a successful call or an abnormal exit, the SDK automatically disconnects the real-time connection, stops polling, and releases all session resources; the
ticketis invalidated at the same time, and the handle enters a terminal state. - When to use / Note: Whether the user exits actively or the experience ends passively (timer expiry, receiving
.ended/.failed, page destruction), make sure a singleend()is reached, otherwise remote resources may not be released promptly. It is recommended to funnel all exit paths into the same idempotent cleanup method.
sendInstruct(content:) (directing mode)
- Purpose: Send a text instruction to drive the storyline.
- When to use: Directing mode; sent directly when
running, cached whenpausedand resent with the first frame after resuming back torunningvia reconnect. - Errors:
103001,103002,103003(called in adventure mode),403004(content moderation),404000(travel not found).
sendCommand(_:) / flushCommands() (adventure mode)
sendCommand: Send direction/view/action control commands (see §8OysterAdventureCommand; fire-and-forget, no return, does not throw). Effective only in adventure mode whenrunning. External input can be high-frequency every frame, and the SDK throttles internally (latest-wins sampling, frame merging at RTC line rate); the host does not need to throttle itself. Note that the server's response to commands itself has latency, so the actual effective time is not fixed.flushCommands: Called at the moment of "key release / input release", immediately resending the last command already waiting in the queue; a pure no-op when there is no pending command, generating no new command.- Errors (all surfaced via the
.errorofevents, notthrows): no active experience103001; called in directing mode103003; real-time channel not ready / send failed105004.
pauseLocalAudioCapture() / resumeLocalAudioCapture()
- Purpose: Temporarily release / restore the SDK's occupation of local microphone capture.
- When to use: When something like speech recognition needs exclusive microphone access,
pausefirst andresumeafterwards.
7. Events and Status
Events are attached to OysterTravel.events and are the SDK's active push channel to you, used to surface situations not triggered by your own calls (e.g. problems with the internally managed real-time connection or status polling).
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)
}
There are two consumption styles, pick one:
- SwiftUI:
OysterTravelis anObservableObject; directly use@StateObject/@ObservedObjectto observestatusand drive the UI; errors still come fromevents. - Imperative / UIKit: in a
Task,for await event in travel.events { ... }, andswitchover.statusChanged/.error; cancel the heldTaskwhen done.
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 process states + 2 terminal states):
Status | Description | Typical handling |
|---|---|---|
| after create, before start | — |
| connecting / reconnecting (internal connecting / reconnecting) | show connecting / reconnecting hint |
| stream ready, interactive (internal playing) | show picture and controls |
| pause accepted, awaiting server confirmation | show "pausing…" |
| paused (confirmed) | show paused state (only |
| ended (active end or server-side end). Terminal | wrap up and close the page |
| failed. Terminal | show error and wrap up |
NoteAfter entering ended / failed, the session has terminated, and all session operations (pause/resume/sendCommand…) no longer take effect. Callbacks may be triggered on the main thread, so you can update the UI directly.
NoteDuring internal status polling, the SDK automatically reports client pull/playback-state heartbeats (connecting / playing / paused / reconnecting / disconnected), so the server can distinguish stream-side from client-side states — purely internal SDK behavior, which the host does not need to be aware of or participate in.
8. Data Models
NoteAll public types are marked @available(iOS 15.0, *). The return values/parameters below are SDK outputs, constructed in place internally with native Swift types (Date / TimeInterval / OysterTravelStatus); they are not Codable and do not expose wire (snake_case) details — wire decoding happens inside the 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)
}
Note
- Note: command rawValues are lower camelCase (e.g.
front/mouseLeft/jump); the enum values above are authoritative. - Note:
modeis externallyadventure(wander) /directing(story) /acting(role playing); theOysterModeValuedefinition is authoritative. - Note:
aspectRatiois an open string (currently9:16/16:9, more may be added). Parse it aswidth:heightand compare the ratio instead of matching known values.
9. Error Codes
The SDK reports errors uniformly as OysterSDKError, and the type is always distinguished by code — do not judge the type by "which path the error came from": the same code may be thrown by a business method (async throws) or surfaced via the .error of events. For typed matching, use error.kind (see §8 OysterErrorKind).
Error codes: server 4xxxxx/5xxxxx, client-local 1xxxxx.
Server Error Codes (common)
code | Meaning | Suggested handling |
|---|---|---|
| Invalid parameters (invalid enum value, etc.) | Check the request parameters or the SDK version |
| Experience credential ( | Have the server re-issue the credential |
| Experience credential ( | Single-use credential; re-issue |
| World does not exist, was deleted, or does not belong to the current developer (including a world deleted after the credential was issued) | Pick a valid World again |
| World state not ready | Wait for the world to be ready before starting |
| The API only allows the primary API Key | A temporary Key cannot be used for this API |
| Input content rejected by content moderation; applies to | Change the input and retry |
| The requested service specification is not enabled | Do not retry as if capacity were full; switch to an enabled specification (typically: the account has no acting specification) |
| Capacity configuration temporarily unavailable | Retry later |
| Resource does not exist (world/wander ownership or no artifact) | Verify ID / status |
| The request conflicts with the current resource state | Check the experience state |
| Concurrency limit reached for this specification | Retry after an existing session ends (do not confuse with |
| Not enough available capacity | Retry later |
| Internal system error | Retry later / report it |
| Inference resource allocation or internal service failure | Retry later |
Client-Local Error Codes
code | Meaning | SDK auto-terminates session | Suggested handling |
|---|---|---|---|
| Called before SDK initialization; also covers an | No (thrown synchronously, rejecting this call) |
|
| HTTP auth token not injected | No | retry after |
| HTTP auth token invalid / rejected | No | re-exchange the token then retry |
| No active experience currently | No (rejecting this call) |
|
| Current state/version does not allow this operation | No (rejecting this call) | check experience state / |
| Mode mismatch (e.g. | No (rejecting this call) | pick the right API by |
| Concurrent create/start of experience | No (thrown synchronously) | serialize calls, |
| Real-time connection failed | Yes | end and restart |
| Real-time join timeout | Yes | end and restart |
| Timeout waiting for the first video frame | Yes | end and restart |
| Real-time channel not ready / send failed | depends (active send failure; heartbeat is report-only) | send after |
| Callback timeout (default 30s) | No | retry, and increase |
| No stream after joining; SDK auto-ends the experience | Yes | end and restart |
| Local network error | No | retryable |
| Response parsing failed | depends | upgrade the SDK / report |
| No recognizable error code / proxy string error code | No | retryable |
| Remotely disabled by the server feature switch (full shutdown or version too low; reason in | Yes | Follow the reason in |
How to judge fatality: Fatality is no longer exposed as a boolean field (OysterSDKError has no isFatal). Semantically, "fatal" specifically means whether the SDK actively terminates the session (disconnect RTC, release the whole session) —
- Errors that auto-terminate the session (e.g.
105001/105002/105003/105006/108001): the host perceives this from the state-machine terminal state (status → failed, surfaced via the.statusChangedofevents), and returns to the screen before "start experience" accordingly, with no need to judge fatality itself. - Call-rejection errors (
100001/103001/103002/103003/103004): thrown synchronously / the call is rejected when you actively call; they do not terminate the session. - Other non-fatal errors (e.g.
101001/101002/105005/106001/106003): they do not terminate the session; retry as suggested or continue after re-injecting the token.
Note106001 may have two causes: a local network error, or an incorrect apiHost. If retrying does not recover the request, check whether apiHost is configured correctly.
Note106003 appearing after you set model usually means the model name/version is wrong or not enabled for your account: check the model together with the apiHost and token, which must all belong to the same account and region — the gateway rejects a mismatch with AccessDenied, normalized to this code.