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.
- 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 (QwenCloud temporary API Key). Your server exchanges it via the QwenCloud 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 |
acting 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 |
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.
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 |
|
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:
AliVCSDK_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.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 (QwenCloud temporary API Key): Your server exchanges it via the QwenCloud API and delivers it down; it is injected through
updateToken(_:). Some internal SDK services call the QwenCloud gateway directly, carrying this token for authentication — therefore it must be a temporary API Key issued by QwenCloud, 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.
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.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).
initialize(config:)
- Purpose: Initialize the runtime and automatically register the real-time engine (no manual registration needed by the host).
- Parameters:
config.apiHostis the QwenCloud 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 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 (QwenCloud 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).
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 / 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).
- 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.
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 |
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.8. Data Models
@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.- 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 |
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.
106001 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.106003 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 — the gateway rejects a mismatch with AccessDenied, normalized to this code.