All Products
Search
Document Center

Alibaba Cloud Model Studio:HappyOyster iOS SDK API Reference

Last Updated:Sep 18, 2026

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 updateToken(_:), and the SDK carries it as an HTTP Bearer when requesting the gateway. The SDK keeps only the latest one, does not persist or refresh it; after it expires, you re-exchange and re-inject it.

ticket

One-time trial credential. Your server exchanges it via the open platform get-travel-credential (prefix tk_, valid for 30 minutes, single-use), used as the createTravel(ticket:) argument; it becomes invalid once the experience ends or expires, and cannot be reused.

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 OysterTravel handle. Single-use; invalidated once a terminal state is reached, requiring a fresh createTravel via the engine.

Session status

OysterTravelStatus: idle → prepare → running → pausing → paused (paused returns to prepare → running via reconnect), plus two terminal states ended / failed (see §7).

Mode

adventure (direction/view/action commands), directing (text-driven storyline), or acting (text-driven character performance). The mode returned by start() determines the UI accordingly; see §2 for what each mode supports.

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

adventure

directing

acting

sendCommand (direction/view/action)

Yes

No

No

sendInstruct (text instruction)

No

Yes

Yes

pause() / resume()

No

Yes

Yes

rewind(toSec:)

No

Yes

No — hide the entry point

end()

Yes

Yes

Yes

start(maxExperienceTimeSec:)

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(config:)

Initialize the runtime and automatically register the real-time engine (once, before createTravel)

updateToken(_:)

Inject / update the HTTP auth token

createTravel(ticket:)

Create a session handle with a one-time credential

cleanup()

Release resources (can initialize again)

version

The current SDK version

OysterTravel — the session handle for one experience, created by createTravel(ticket:).

Method / Property

Description

videoView / OysterVideoView(travel:)

Playback view (UIKit / SwiftUI)

events / status

Status-change + error push stream / observable current status

start() / start(maxExperienceTimeSec:)

Connect and play (optionally request the maximum duration of an adventure experience)

sendInstruct(content:)

Directing-mode text instruction

sendCommand(_:) / flushCommands()

Adventure-mode control

pause() / resume()

Pause / resume (directing and acting)

rewind(toSec:)

Rewind (paused only)

end()

End (idempotent, be sure to call it)

pauseLocalAudioCapture() / resumeLocalAudioCapture()

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 @available(iOS 15.0, *))

Language

Swift (async/await)

Concurrency

Main-thread access (entry types are marked @MainActor)

Network

Public network access required

Permissions

Info.plist must include NSMicrophoneUsageDescription (see below)

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:

  1. 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.
  2. One-time trial credential ticket: Your server exchanges it via the open platform get-travel-credential (prefix tk_, valid for 30 minutes, single-use), used as the createTravel(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:

  1. When calling APIs such as engine.createTravel or travel.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.
  2. When listening to the .error event of OysterTravelEvent, 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.apiHost is 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.model is 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 §8 OysterConfig.
  • One model serves one mode: each per-mode model is its own gateway route, so a single initialize only serves worlds of that one mode. If your app offers worlds in several modes, just call initialize() again with the matching model before entering a world of a different mode — while idle the latest config wins, no cleanup() is needed and the injected token is kept; while a Travel is in flight the call is ignored, so end() it first. A model that does not match the world's mode is rejected by the gateway with AccessDenied, normalized to 106003.
  • When to use: Call once before createTravel, as early as possible after app launch.
  • Returns: Bool — whether the config you passed took effect. It is false in two cases: the config is invalid (apiHost / model blank 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 / model needs no cleanup(), and the injected token is kept); it is a no-op with a warning only while a Travel is in flight, so end() it first. An invalid config leaves the runtime unchanged. Do not use isReady to tell whether a re-initialize succeeded — if it was rejected the previous config is still in effect and isReady remains true; isReady answers "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 and setHandler(_:) 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: ticket is 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): throws 100001 if not initialized; throws 103004 if called again before the previous Travel has end()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 deterministically end()s the current active travel first, then tears down the runtime, leaving no fire-and-forget. After release you can initialize again.

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, use OysterVideoView(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: events is the push stream of status changes + errors; status is the observable current external status; isEnded is a synchronously readable flag for the terminal state.
  • When to use: It is recommended to start consuming events before start(), to avoid missing early statuses.
  • Note: Each access to events returns an independent stream, supporting multi-subscription; unsubscribing = ending the for await iteration (or destroying the held Task). In SwiftUI you can directly observe status with @StateObject/@ObservedObject (errors still come through events). See §7.

start() / start(maxExperienceTimeSec:)

  • Purpose: Use the ticket captured 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 through events.
  • 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. Passing nil (or calling the parameterless start()) 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 the ended terminal state via events (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 world mode must match the model passed to initialize (caller's responsibility): with per-mode models, each model is its own gateway route, and start() sends the ticket to the route of the currently initialized model. The SDK does not and cannot verify this for you beforehand — mode is delivered by the response to this very start() call (OysterStartTravelData.mode); before the call the SDK holds only an opaque ticket and 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, call initialize() again with the matching model (while idle the latest config wins — no cleanup() needed and the token is kept; while a Travel is in flight the call is ignored, so end() it first). On a mismatch this start() fails at the gateway; when diagnosing, first check that the current model and the mode of the ticket'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 to failed, and surfaces 105006 via the .error of events (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 directing and acting worlds; not supported by adventure. Use the mode returned by start() to decide in advance whether to show a pause button. pause requires the current state to be running; resume requires paused.
  • 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 paused state, and only by directing worldsacting and adventure do 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 ticket is 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 single end() 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 when paused and resent with the first frame after resuming back to running via 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 §8 OysterAdventureCommand; fire-and-forget, no return, does not throw). Effective only in adventure mode when running. 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 .error of events, not throws): no active experience 103001; called in directing mode 103003; real-time channel not ready / send failed 105004.

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, pause first and resume afterwards.

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: OysterTravel is an ObservableObject; directly use @StateObject/@ObservedObject to observe status and drive the UI; errors still come from events.
  • Imperative / UIKit: in a Task, for await event in travel.events { ... }, and switch over .statusChanged / .error; cancel the held Task when 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

idle

after create, before start

prepare

connecting / reconnecting (internal connecting / reconnecting)

show connecting / reconnecting hint

running

stream ready, interactive (internal playing)

show picture and controls

pausing

pause accepted, awaiting server confirmation

show "pausing…"

paused

paused (confirmed)

show paused state (only directing shows the rewind entry)

ended

ended (active end or server-side end). Terminal

wrap up and close the page

failed

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: mode is externally adventure (wander) / directing (story) / acting (role playing); the OysterModeValue definition is authoritative.
  • Note: aspectRatio is an open string (currently 9:16 / 16:9, more may be added). Parse it as width:height and 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 codedo 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

400000

Invalid parameters (invalid enum value, etc.)

Check the request parameters or the SDK version

401010

Experience credential (ticket) invalid or expired

Have the server re-issue the credential

401011

Experience credential (ticket) already used

Single-use credential; re-issue

403001

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

403002

World state not ready

Wait for the world to be ready before starting

403003

The API only allows the primary API Key

A temporary Key cannot be used for this API

403004

Input content rejected by content moderation; applies to sendInstruct text instructions

Change the input and retry

403007

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)

403008

Capacity configuration temporarily unavailable

Retry later

404000

Resource does not exist (world/wander ownership or no artifact)

Verify ID / status

409000

The request conflicts with the current resource state

Check the experience state

429001

Concurrency limit reached for this specification

Retry after an existing session ends (do not confuse with 500001)

429002

Not enough available capacity

Retry later

500000

Internal system error

Retry later / report it

500001

Inference resource allocation or internal service failure

Retry later

Client-Local Error Codes

code

Meaning

SDK auto-terminates session

Suggested handling

100001

Called before SDK initialization; also covers an initialize that did not take effect because apiHost/model was blank or invalid

No (thrown synchronously, rejecting this call)

initialize first, and verify both apiHost and model are set correctly

101001

HTTP auth token not injected

No

retry after updateToken

101002

HTTP auth token invalid / rejected

No

re-exchange the token then retry updateToken

103001

No active experience currently

No (rejecting this call)

createTravel + start first

103002

Current state/version does not allow this operation

No (rejecting this call)

check experience state / version

103003

Mode mismatch (e.g. sendCommand called on a non-adventure world)

No (rejecting this call)

pick the right API by mode, see the §2 capability table

103004

Concurrent create/start of experience

No (thrown synchronously)

serialize calls, end the old session first

105001

Real-time connection failed

Yes

end and restart

105002

Real-time join timeout

Yes

end and restart

105003

Timeout waiting for the first video frame

Yes

end and restart

105004

Real-time channel not ready / send failed

depends (active send failure; heartbeat is report-only)

send after running

105005

Callback timeout (default 30s)

No

retry, and increase callbackTimeoutMs if needed

105006

No stream after joining; SDK auto-ends the experience

Yes

end and restart

106001

Local network error

No

retryable

106002

Response parsing failed

depends

upgrade the SDK / report

106003

No recognizable error code / proxy string error code

No

retryable

108001

Remotely disabled by the server feature switch (full shutdown or version too low; reason in raw)

Yes

Follow the reason in raw; guide the user to upgrade when the version is too low

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 .statusChanged of events), 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.