HappyOyster iOS SDK 入口为进程级单例 HappyOysterEngine.shared ,业务方法均为 async throws ,失败时抛出 OysterSDKError 。一次体验由 OysterTravel 句柄承载,支持 UIKit 与 SwiftUI。
核心概念
先建立几个名词,便于阅读后文。
概念 | 说明 |
|---|---|
token | HTTP 鉴权 token(千问AI平台临时 API Key)。由你的服务端调用千问AI平台接口换取后下发,经 |
ticket | 一次性体验凭证。由你的服务端调用开放平台 |
World | AI 世界,含角色、场景。由你的服务端创建与管理,SDK 不涉及。 |
Travel | 一次实时体验,对应一个 |
会话状态 |
|
模式 |
|
概览
集成本 SDK 后,你的 App 可进入由 AI 实时生成的「世界」,以世界探索模式(adventure)、实时导演模式(directing)或角色演绎模式(acting)进行实时互动视频体验:开始体验 → 实时播放 → 实时互动 → 过程控制(暂停/恢复/回溯/结束)→ 状态与错误回调。
各模式可用的能力不同,请按 start() 返回的 mode 决定 UI:
能力 | adventure | directing | acting |
|---|---|---|---|
| 支持 | 不支持 | 不支持 |
| 不支持 | 支持 | 支持 |
| 不支持 | 支持 | 支持 |
| 不支持 | 支持 | 不支持,请隐藏入口 |
| 支持 | 支持 | 支持 |
| 生效 | 忽略 | 忽略 |
acting 世界为竖屏优先:start() 返回的 aspectRatio 给出本次体验的画幅(9:16 / 16:9),建议在拉流前据此决定播放器方向与容器尺寸(见"数据模型"章节)。import HappyOysterSDK,核心入口是两个类型。
HappyOysterEngine —— 编排入口,进程级单例 HappyOysterEngine.shared。
方法 / 属性 | 说明 |
|---|---|
| 初始化运行时并自动注册实时引擎( |
| 注入 / 更新 HTTP 鉴权 token |
| 用一次性凭证创建会话句柄 |
| 释放资源(可再 |
| 当前 SDK 版本号 |
OysterTravel —— 一次体验的会话句柄,由 createTravel(ticket:) 创建。
方法 / 属性 | 说明 |
|---|---|
| 播放视图(UIKit / SwiftUI) |
| 状态变更 + 错误推送流 / 可观察的当前状态 |
| 建连播放(可选请求世界探索模式体验的最大时长) |
| 实时导演模式文本指令 |
| 世界探索模式操控 |
| 暂停 / 恢复( |
| 回溯(仅 paused) |
| 结束(幂等,务必调用) |
| 临时让出 / 恢复麦克风 |
OysterLog 用于接管 SDK 内部日志;HappyOysterEngine.version 读取版本号;OysterVideoView 是 SwiftUI 播放视图(与 videoView 等价)。用法见后文。
生命周期:初始化 → 注入 token → 创建会话 → 挂载视频 + 订阅事件 → start 播放 → 互动 → end。SDK 不负责世界的创建与管理(由你的服务端完成),也不暴露底层实时通信细节。
快速上手
一段从初始化到结束的完整流程,分步注释。各 API 的细节见"API 参考"章节。
环境要求
项 | 要求 |
|---|---|
最低系统 | iOS 15.0+(公开类型均标 |
语言 | Swift( |
并发 | 主线程访问(入口类型标 |
网络 | 需可访问公网 |
权限 |
|
Info.plist 必须提供 NSMicrophoneUsageDescription,否则启动实时采集时会崩溃。当你需要独占麦克风(如语音识别)时,用 pauseLocalAudioCapture() / resumeLocalAudioCapture() 临时让出与恢复(见"OysterTravel"章节)。
引入与鉴权
引入与依赖
代码层面 import HappyOysterSDK 即可。SDK 以预编译二进制(xcframework)按 CocoaPods subspec 分发,已发布到 CocoaPods 公开 Trunk——在 Podfile 中声明依赖:
HappyOysterSDK/StreamAliRTC 时 AliVCSDK_ARTC 为必需:缺失时 SDK 会静默回落 Loopback——能连上、状态走到 running,但黑屏不报错。鉴权模型
SDK 不获取、不刷新 token,保持轻量。鉴权分两层,集成方负责生命周期管理:
- HTTP 鉴权 token(千问AI平台临时 API Key):由你的服务端调用千问AI平台接口换取后下发,经
updateToken(_:)注入。SDK 内部部分服务直接调用千问AI平台网关,携带该 token 鉴权——因此它必须是千问AI平台侧签发的临时 API Key,而非你自有业务服务的 token。SDK 只保存最新一个,不持久化、不刷新;过期后由你重新换取并再次注入。 - 一次性体验凭证
ticket:由你的服务端调用开放平台get-travel-credential换取(前缀tk_,有效期 30 分钟,一次性),作为createTravel(ticket:)入参;体验结束(正常或异常)或过期后即失效,不可复用。
start / pause / resume / rewind / sendInstruct / sendCommand)会拒绝并透出 108001,进行中的体验会被 SDK 主动终止(见"错误码"章节);OysterSDKError.raw 里带可读的禁用原因,版本过低时请引导用户升级。updateToken(_:) 注入。有两处需判断 token 是否过期:
- 调用
engine.createTravel、travel.start等 API 时,处理 error,判断 token 过期/无效的 error 类型(101001/101002),注入新 token 后重新调用对应 API。 - 监听
OysterTravelEvent的.error事件时,判断 token 过期/无效的 error 类型,重新请求 token 并注入。
API 参考
入口为两个类型,均 @MainActor、@available(iOS 15.0, *)。业务方法为 async throws,失败抛 OysterSDKError;有返回值的方法均标 @discardableResult。
HappyOysterEngine
进程级单例,编排入口。init 非 public——统一用 HappyOysterEngine.shared,不要自行实例化(底层实时引擎也是进程单例)。
initialize(config:)
初始化运行时并自动注册实时引擎(无需宿主手动注册)。
使用时机:createTravel 前调用一次,App 启动后尽早调用。
注意:空闲时再调即以新 config 重新装配(换 apiHost / model 都不必先 cleanup(),已注入的 token 保留);仅当有进行中的 Travel 时为 no-op 并告警,需先 end()。config 非法时保持现状,已生效的运行时不受影响。
签名
返回值
Bool —— 本次传入的 config 是否已生效。false 有两种情况:config 非法(apiHost / model 为空或拼不出合法网关 URL),或有进行中的 Travel 导致本次调用被忽略;两种情况下运行时都保持原状。
isReady 判断成败:重新 initialize 若被拒,之前那份 config 仍在生效,isReady 照样是 true。isReady 回答"引擎现在可用吗",本返回值回答"我刚传的这份 config 生效了吗"。参数
字段 | 类型 | 是否必填 | 描述 |
|---|---|---|---|
|
| 是 |
|
mode 一一对应:按模式拆分后,每个模型是一条独立的网关路由,一次 initialize 只服务一种 mode 的世界。若你的 App 同时提供多种模式的世界,在进入不同模式的世界前用对应模型再 initialize() 一次即可——空闲时以最新 config 为准,不需要 cleanup(),已注入的 token 也保留;有进行中的 Travel 时该调用被忽略,需先 end()。模型与世界 mode 不匹配时,网关以 AccessDenied 拒绝并归一为 106003。OysterLog
配置并接管 SDK 内部的日志,打印到你自己的日志模块中。提供 setMinimumLevel(_:) 设置等级、setHandler(_:) 自定义打印(见"快速上手"示例)。
updateToken(_:)
注入 / 更新 HTTP 鉴权 token(千问AI平台临时 API Key,见"鉴权模型"章节)。
使用时机:initialize 之后、可随时调用;token 过期或收到鉴权类错误(101001 / 101002)后重新换取并再次调用。
注意:未 initialize 时为 no-op 并告警。
签名
参数
字段 | 类型 | 是否必填 | 描述 |
|---|---|---|---|
|
| 是 | HTTP 鉴权 token(千问AI平台临时 API Key,见"鉴权模型"章节)。 |
createTravel(ticket:)
用一次性 ticket 创建一次会话句柄。
使用时机:每次开始新体验前调用;返回的句柄尚未建连,需再调 travel.start()。视频从返回句柄取(见"OysterTravel"章节)。
注意(同步 throws):未 initialize 抛 100001;上一个 Travel 未 end() 前再次调用抛 103004(每个 engine 同时只允许一个 active Travel)。
签名
参数
字段 | 类型 | 是否必填 | 描述 |
|---|---|---|---|
|
| 是 | 一次性凭证,创建后即视为本次体验占用。 |
返回值
返回OysterTravel 会话句柄;尚未建连,需再调 start()。
错误
code | 说明 |
|---|---|
| 未 |
| 上一个 Travel 未 |
cleanup()
释放 SDK 资源(结束 active travel、运行时配置、token)。
使用时机:彻底退出 SDK 或需要更换 config 时。
注意:async——内部先确定性地 end() 当前 active travel,再拆运行时,不留 fire-and-forget。释放后可再次 initialize。
签名
OysterTravel
由 createTravel 创建的会话句柄;单次使用,到达终态(end / 服务端结束 / 失败)后失效,需经 engine 重新 createTravel。同时是 ObservableObject(@Published status,可直接驱动 SwiftUI)。
videoView / OysterVideoView(travel:)
远端画面渲染入口,「SDK 出视图、宿主摆放」。UIKit 取 travel.videoView;SwiftUI 用 OysterVideoView(travel:)。
使用时机:句柄创建后即可取(多次访问返回同一视图),挂进任意层级,引擎就绪自动渲染,start() 前后挂载均可、不黑屏。
注意:会话结束时 SDK 自动释放渲染绑定,你按需把视图移出层级。
签名
events / status / isEnded
events 是状态变更 + 错误的推送流;status 是可观察的当前对外状态;isEnded 同步可读是否终态。
使用时机:events 建议在 start() 之前就开始消费,避免漏掉早期状态。
注意:每次访问 events 返回一条独立流,支持多订阅;取消订阅 = 结束 for await 迭代(或销毁持有的 Task)。SwiftUI 可直接 @StateObject/@ObservedObject 观察 status(错误仍走 events)。详见"事件与状态"章节。
签名
start() / start(maxExperienceTimeSec:)
用 create 时捕获的 ticket 换取 travel + RTC 入会配置并建连播放。成功后 SDK 自动建立实时连接并开始内部状态轮询,状态经 events 透出。
注意(无推流自动结束):服务端下发「无推流超时」(默认约 30s)。若建连后在该时长内仍未收到推流(迟迟不进 running),SDK 自动结束本次体验,状态转 failed、经 events 的 .error 透出 105006(致命,按回到开始前界面处理,无需自行计时)。
签名
参数
字段 | 类型 | 是否必填 | 描述 |
|---|---|---|---|
|
| 否 | (可选)——请求本次世界探索(adventure)体验的最大持续时长(秒)。参数原样发给服务端,允许值、实际生效时长与到期自动结束时机均由服务端决定,SDK 不做本地校验;传 |
返回值
OysterStartTravelData(mode / version / encryptedTravelId 等,见"数据模型"章节),据此决定互动 UI。
ticket 的世界 mode 必须与 initialize 传入的 model 匹配(调用方保证):模型按 mode 拆分后,每个模型是一条独立的网关路由,start() 是把 ticket 发往当前模型那条路由。SDK 不会也无法在调用前替你校验这一点——mode 由本次 start() 的响应下发(即 OysterStartTravelData.mode),调用前 SDK 手里只有不透明的 ticket 和模型名,没有可比对的 mode;而从模型名反推 mode 属于对服务端命名的猜测,SDK 不做。因此:换一种 mode 的世界之前,用对应模型再 initialize() 一次(空闲时以最新 config 为准,无需 cleanup(),token 保留;有进行中的 Travel 时该调用被忽略,需先 end())。不匹配时本次 start() 会在网关侧失败,排查时请优先核对"当前 model 与本次 ticket 所属世界的 mode 是否成套",再看下表的凭证类错误码。错误
code | 说明 |
|---|---|
| 凭证无效(也包括:凭证有效,但所属世界的 |
| 凭证已用 |
| 世界未就绪 |
| 当前规格未开通(如角色演绎(acting)规格) |
| 并发已满 / 可用容量不足 |
| 资源/服务端失败 |
| 并发开始 |
pause() / resume()
暂停 / 恢复体验(幂等)。
使用时机:directing(实时导演)与 acting(角色演绎)世界支持,adventure 不支持。可用 start() 返回的 mode 提前决定是否展示暂停按钮。pause 要求当前 running;resume 要求 paused。
签名
返回值
返回OysterTravelStateData(字段见"数据模型"章节)。
错误
code | 说明 |
|---|---|
| 无活跃体验 |
| 状态/版本不允许 |
| 模式不匹配 |
rewind(toSec:)
回溯到指定秒数。成功后 SDK 自动用原始 rtcConfig 重新入会回到播放。
使用时机:仅 paused 状态可发起,且只有 directing(实时导演)世界支持——acting 与 adventure 均不支持回溯,请在这两种模式下隐藏回溯入口,不要调用。
签名
参数
字段 | 类型 | 是否必填 | 描述 |
|---|---|---|---|
|
| 是 | 回溯到的目标秒数。 |
返回值
返回OysterRewindTravelData(字段见"数据模型"章节)。
错误
code | 说明 |
|---|---|
| 无活跃体验 |
| 状态不允许 |
end()
结束体验(幂等,可重复调用)。调用成功或异常退出后,SDK 自动断开实时连接、停止轮询、释放全部会话资源,ticket 同时失效,句柄进入终态。
使用时机 / 注意:无论用户主动退出还是被动结束(计时到期、收到 .ended/.failed、页面销毁),都要确保走到一次 end(),否则远端资源释放可能不及时。建议把所有退出路径收口到同一个幂等清理方法。
签名
返回值
返回OysterEndTravelData(字段见"数据模型"章节)。
sendInstruct(content:)(实时导演模式)
发送文本指令驱动剧情。
使用时机:实时导演模式;running 时直发,paused 时缓存、待 resume 重连进 running 后随首帧补发。
签名
参数
字段 | 类型 | 是否必填 | 描述 |
|---|---|---|---|
|
| 是 | 要发送的文本指令内容。 |
返回值
返回OysterSendInstructData(字段见"数据模型"章节)。
错误
code | 说明 |
|---|---|
| 无活跃体验 |
| 状态不允许 |
| 在世界探索模式下调用 |
| 输入内容违规(内容安审) |
| Travel 不存在 |
sendCommand(_:) / flushCommands()(世界探索模式)
sendCommand:发送方向/视角/动作控制指令(见"数据模型"章节OysterAdventureCommand,fire-and-forget,无返回、不 throws)。仅世界探索模式、running时有效。外部可每帧高频持续输入,SDK 内部做节流(latest-wins 采样、按 RTC 线速合帧),宿主无需自行节流。注意服务端对指令的响应本身有延迟,实际生效时间不固定。flushCommands:「松键 / 输入释放」瞬间调用,立即补发队列里已等待的最后一条指令;无待发指令时为纯 no-op,不生成新指令。
签名
错误
code | 说明 |
|---|---|
| 无活跃体验 |
| 在实时导演模式下调用 |
| 实时通道未就绪/发送失败 |
events 的 .error 透出,非 throws。)
pauseLocalAudioCapture() / resumeLocalAudioCapture()
临时释放 / 恢复 SDK 对本地麦克风采集的占用。
使用时机:语音识别等需要独占麦克风时先 pause,结束后 resume。
签名
事件与状态
事件挂在 OysterTravel.events 上,是 SDK 对你的主动推送通道,用于透出非你主动调用触发的情况(如内部自动维护的实时连接或状态轮询出现问题)。
- SwiftUI:
OysterTravel是ObservableObject,直接@StateObject/@ObservedObject观察status驱动 UI;错误仍从events取。 - 命令式 / UIKit:在
Task中for await event in travel.events { ... },switch处理.statusChanged/.error;结束时取消持有的Task。
OysterTravelStatus(5 个过程态 + 2 个终态):
状态 | 说明 | 典型处理 |
|---|---|---|
| create 后、start 前 | — |
| 建连 / 重连中(内部 connecting / reconnecting) | 展示连接 / 重连提示 |
| 流已就绪、可交互(内部 playing) | 显示画面与操控 |
| 暂停已受理、等待服务端确认 | 展示「暂停中…」 |
| 已暂停(确认) | 展示暂停态(仅 |
| 已结束(主动 end 或服务端结束)。终态 | 收尾并关闭页面 |
| 失败。终态 | 展示错误并收尾 |
ended / failed 后会话已终止,所有会话操作(pause/resume/sendCommand…)都不再生效。回调可能在主线程触发,更新 UI 可直接使用。数据模型
@available(iOS 15.0, *)。下列返回值/参数是 SDK 出参,由内部就地构造、用 Swift 原生类型(Date / TimeInterval / OysterTravelStatus),不是 Codable、不暴露 wire(snake_case)细节——wire 解码发生在 SDK 内部。- 操控指令 rawValue 为小写驼峰(如
front/mouseLeft/jump),以上述枚举取值为准。 mode对外取值为adventure(世界探索)/directing(实时导演)/acting(角色演绎),以OysterModeValue定义为准。aspectRatio为开放字符串(当前9:16/16:9,后续可能扩展)。判断横竖屏请按宽:高解析比值,不要穷举已知取值。
错误码
SDK 统一以 OysterSDKError 报错,类型一律通过 code 区分,不要依据「从哪条路径拿到的错误」判断类型——同一个 code 既可能从业务方法(async throws)抛出、也可能经 events 的 .error 透出。需要类型化匹配时用 error.kind(见"数据模型"章节 OysterErrorKind)。
错误码:服务端4xxxxx/5xxxxx,客户端本地1xxxxx。
服务端错误码(常见)
code | 含义 | 建议处理 |
|---|---|---|
| 参数非法(枚举非法等) | 检查请求参数或 SDK 版本 |
| 体验凭证( | 让服务端重新下发凭证 |
| 体验凭证( | 凭证一次性,重新下发 |
| World 不存在、已删除或不属于当前开发者(含凭证内世界已删除) | 重新选择有效 World |
| 世界状态非就绪 | 等世界就绪后再开始 |
| 当前接口仅允许主 API Key | 临时 Key 不可用于该接口 |
| 输入内容违规(内容安审);适用于 | 修改输入内容后重试 |
| 当前规格未开通 | 不可按容量满重试;换已开通规格或联系开通(典型:账号未开通角色演绎(acting)规格) |
| 容量配置暂不可用 | 稍后重试 |
| 资源不存在(世界 / Travel 归属或无产物) | 核对 ID / 状态 |
| 请求与当前资源状态冲突 | 检查体验状态 |
| 当前规格并发已满 | 等已有会话结束后重试(勿与 |
| 当前可用容量不足 | 稍后重试 |
| 系统内部错误 | 稍后重试 / 反馈 |
| 推理资源分配/服务内部失败 | 稍后重试 |
客户端本地错误码
code | 含义 | SDK 是否自动终止会话 | 建议处理 |
|---|---|---|---|
| SDK 未初始化即调用;也包括 | 否(同步抛出,拒绝该次调用) | 先 |
| 未注入 HTTP 鉴权 token | 否 |
|
| HTTP 鉴权 token 无效 / 被拒 | 否 | 重新换取 token 后 |
| 当前无活跃体验 | 否(拒绝该次调用) | 先 |
| 当前状态/版本不允许该操作 | 否(拒绝该次调用) | 检查体验状态 / |
| 模式不匹配(如非 | 否(拒绝该次调用) | 按 |
| 并发创建/开始体验 | 否(同步抛出) | 串行化调用,先 |
| 实时连接失败 | 是 | 结束并重新开始 |
| 实时入会超时 | 是 | 结束并重新开始 |
| 等待视频首帧超时 | 是 | 结束并重新开始 |
| 实时通道未就绪/发送失败 | 按场景(主动发送失败;心跳仅上报) | 待 |
| 回调超时(默认 30s) | 否 | 重试,必要时调大 |
| 入会后无推流,SDK 自动结束体验 | 是 | 结束并重新开始 |
| 本地网络错误 | 否 | 可重试 |
| 响应解析失败 | 按场景 | 升级 SDK / 反馈 |
| 未携带可识别错误码 / 代理字符串错误码 | 否 | 可重试 |
| 被服务端功能开关远程禁用(整体关停或版本过低;原因见 | 是 | 按 |
OysterSDKError 没有 isFatal)。语义上「致命」专指 SDK 是否主动终止会话(断开 RTC、释放整次会话)——
- 会自动终止会话的错误(如
105001/105002/105003/105006/108001):宿主从状态机终态感知(status → failed,经events的.statusChanged透出),据此回到「开始体验」前的界面即可,无需自行判定致命性。 - 调用拒绝类错误(
100001/103001/103002/103003/103004):在你主动调用时同步抛出 / 拒绝该次调用,不终止会话。 - 其余非致命错误(如
101001/101002/105005/106001/106003):不终止会话,按建议重试或重新注入 token 后继续。
106001 可能有两种原因:本地网络错误,或 apiHost 配置错误。若重试无法恢复,检查 apiHost 是否配置正确。106003 若在填写 model 后出现,优先检查模型名称及版本是否正确、是否已为当前账号开通,并确认 apiHost、token 与模型属于同一账号——不匹配时网关以 AccessDenied 拒绝并归一到本码。