本文面向集成方 Android 开发者,覆盖安装、鉴权、完整接入流程、事件处理与最佳实践,说明如何稳定接入 HappyOyster Android SDK。
HappyOyster 当前仅提供 Adventure 模式;Directing 与 Acting 暂不支持。本文涉及 Directing / Acting 模式的内容仅供 SDK 能力参考。
能做什么
- 开始一次体验(Travel):用一次性凭证进入一个已就绪的世界,SDK 自动建立实时视频连接。
- 实时播放:SDK 返回一个视频 View,你挂载到布局即可播放 AI 实时生成的画面。
- 实时互动:
- 实时导演模式(directing):发送文本指令驱动剧情。
- 角色演绎(acting):同样发送文本指令;暂停 / 恢复可用;不要回溯,不要
sendCommand。用进房回包的aspectRatio定播放器方向(详见模式适配)。 - 世界探索模式(adventure):发送方向 / 视角 / 动作控制指令与世界交互。
- 过程控制:暂停 / 恢复(实时导演与角色演绎)、回溯(仅实时导演)、结束(三种模式均可)。
- 状态与错误回调:通过事件监听实时感知体验状态与异常。
SDK 不负责世界的创建与管理,也不直接暴露底层实时通信细节——这些由你的服务端或 SDK 内部处理。你只需聚焦「开始体验 → 播放 → 互动 → 结束」。
安装
HappyOyster SDK(cn.happyoyster:opensdk)发布在 Maven Central;其底层实时通信引擎(阿里云 ARTC)发布在阿里云 Maven。两个仓库都需要声明。
环境要求
项 | 要求 |
|---|---|
minSdk | 24(Android 7.0)及以上 |
compileSdk | 36 |
JDK | 11 字节码目标(宿主工具链建议 JDK 11 及以上) |
语言 | Kotlin(协程 |
ABI |
|
网络 | 需要可访问公网 |
settings.gradle.kts 中加入仓库:
<version> 替换为该具体版本号,在模块 build.gradle.kts 中添加依赖:
SDK 以瘦 AAR 形式发布,不内嵌任何第三方依赖;实时通信引擎等传递依赖在解析时自动从上述仓库拉取,因此阿里云 Maven 仓库必不可少,缺失会导致
com.aliyun.aio:AliVCSDK_ARTC 解析失败。权限
SDK 库自身仅声明 INTERNET。实时通信引擎会自动合并少量网络 / 蓝牙 / 音频设置类权限,库 manifest 不包含录音与摄像头权限。视频画面为纯订阅播放,SDK 不向远端发送真实音频。
宿主必须自行声明:当你的 targetSdk 为 33 及以上时,需在宿主 AndroidManifest.xml 声明通知权限(实时通信引擎包含前台服务):
sendCommand)建议声明录音权限:世界探索模式的实时控制指令通过一路静音音频流建立「推流者身份」并承载上行通道,SDK 不会录制或上传你的真实音频。RECORD_AUDIO 并非 DataChannel 的硬前置——即使未授予,静音流仍以推流者身份存在、上行通道通常可用。但为兼顾各机型的稳定性,仍建议:如果你的 App 会用到 sendCommand,在宿主 AndroidManifest.xml 声明该权限,并在调用前于运行时申请。
105004:105004 表示实时通道未就绪 / 发送失败(如 DataChannel 连接中断或实时通道异常),并非缺少权限的必然结果——它不由未授予 RECORD_AUDIO 直接触发。实时导演模式(sendInstruct)与视频播放不受影响。若你的 App 不使用世界探索模式,则无需该权限。
如需裁剪合并进来的权限,可用 tools:node="remove"。
鉴权模型
SDK 不获取、不刷新 token,保持轻量。鉴权分两层:
- 千问AI平台网关 API Key:由你的 App 通过
updateToken(token)注入为 Bearer token。SDK 只保存最新一个,不持久化、不刷新;Key 变化后由你再次注入。网关要求包括startTravel在内的 SDK 请求均使用此 Bearer。Bearer Key 过期或无效时,SDK 抛出SDKError(101002);此时应重新获取并注入 Bearer Key(updateToken),而非重新换取 ticket。验证 / Demo 阶段可直接用主千问AI平台 API Key 作为 Bearer(updateToken)跑通流程。生产环境务必改用你的服务端签发的短期 token,切勿把长期 API Key 打包进 App 分发。 - 一次性体验凭证
ticket:由你的服务端调用开放平台换取并下发给客户端,仅用于一次startTravel;体验结束(正常或异常)后即失效,不可复用。ticket 级别的凭证错误由六位服务端 code 标识(如401010= ticket 无效 / 过期,401011= ticket 已使用),SDK 原样透传。
SDKConfig.apiHost 传入你账号的千问AI平台 API Host(即 maas.qianwenaiapi.com),并通过无默认值的必填参数 SDKConfig.model 传入拆分后的模型名(happyoyster-1.0-directing / happyoyster-1.0-acting / happyoyster-1.0-adventure,与所用 Open API 入口一致)。SDK 将请求地址补全为 https://{apiHost}/api/v2/apps/{model}/openapi/v1/{endpoint},你无需自行拼接。
构造 SDKConfig 时遗漏 model 会编译失败;传入空白 model 时,initialize 同步抛出 SDKError(100002)(raw = "SDKConfig.model must not be blank"),不会创建或替换 runtime,也不会发起网络请求。API Host、model 与注入的 API Key 必须匹配所需的账号和模型授权,否则网关通常返回 AccessDenied(运行期以 SDKError(106003) 抛出,AccessDenied 原文见 SDKError.raw;排查清单见 API Reference 错误码表 106003 行)。出于安全,强烈建议客户端注入服务端签发的短期 token作为 Bearer,而非把长期 API Key 打包进 App 或写入代码仓库。
快速开始
事件订阅与错误处理
用 onStatusChanged 驱动宿主状态机与互动能力门控;用 onError 统一接收运行时错误。事件接口与完整错误码见 HappyOyster Android SDK API Reference。
必须
- 在
HappyOyster.initialize(...)成功返回后立即注册监听器;addListener/removeListener必须在initialize之后调用(在初始化前调用会抛出SDKError(100001))。 - 以
onStatusChanged(Running)为门控,running后才允许互动调用;sendCommand(世界探索模式)仅running有效。 - 同时处理
onError,不要只 catchstartTravel;致命错误同时终止体验(见 API Reference 错误码节)。
- 在合适的生命周期(如
onDestroy)调用removeListener,避免内存泄露。
- 重复注册监听但不 removeListener。
指令发送:sendInstruct 与 sendCommand
sendInstruct(实时导演 / 角色演绎)
sendInstruct 用于实时导演(directing)与角色演绎下发送文本指令驱动画面,在 running 或 paused 状态均可调用。paused 态下 SDK 不会自动 resume——由宿主决定是否先调用 resumeTravel 再发 instruct(节流、状态校验等契约细节见 API Reference)。角色演绎世界的 creationModel 恒为 Simple,因此 ScriptList 限制只作用于实时导演。
sendCommand(世界探索模式)
sendCommand 用于世界探索模式(adventure)下发送方向 / 视角 / 动作控制指令,仅 running 有效。SDK 内置 42 ms(24fps)latest-wins 节流,宿主可按游戏帧率高频调用,SDK 自动合并;无需宿主手动限速。单次动作调用一次即可;持续动作需按帧续调、松手时显式发一次 None 复位(单次 / 持续最佳实践、节流 / 失败透出等契约细节见 API Reference)。
推荐做法:在宿主的世界探索模式互动界面提供三组独立指令入口(移动方向 + 视角方向 + 动作交互),维护三组当前按下状态,并在每次调用时发送完整的三字段快照。未按下的维度填 "None";如果不同维度同时按下,必须保留并发送各维度当前值,不能因为更新一个维度就把其他维度重置为 "None"。
暂停 / 恢复 / 回溯
适用模式:暂停 / 恢复适用于实时导演与角色演绎,两者行为完全一致(含下面的 3 s 屏障);回溯仅实时导演。世界探索模式下调用 pauseTravel / resumeTravel / rewindTravel 会被 SDK 以 103003 拒绝。此外服务端下发的 version 必须是本模式的 v2 标识(实时导演 storyV2、角色演绎 actingV2),否则暂停 / 恢复返回 103002。
暂停是异步的:pauseTravel 方法返回仅代表受理,真正暂停以 onStatusChanged(Paused) 为准。在「调用 pauseTravel」到「收到 Paused 回调」之间,宿主可将本地体验状态标记为 pausing;只有收到 Paused 后,才应允许调用 resumeTravel 或依赖 paused 态的 rewindTravel。
SDK 内部 pause→reopen 屏障:收到 Paused 后即可按契约调用 resumeTravel / rewindTravel;若调用发生在暂停确认后的 3 s settle 窗口内,SDK 的 suspend 方法会先等待剩余时间,再向服务端发送会重新打开实时房间的请求。宿主无需另加 pause→resume 延时;自然等待已满 3 s 时 SDK 不增加延迟。
宿主侧调用冷却(建议):建议宿主在 resumeTravel 返回成功后的 3 s 内暂缓再次发起 pauseTravel,避免过于频繁的切换。SDK 本身不强制这个冷却。
回溯:rewindTravel 仅在 paused 状态可用;回溯后服务端自动 resume,SDK 自动重连 RTC,无需宿主干预。典型序列:pause → wait:paused → rewindTravel(sec)。回溯秒数 rewindToSec 应取 4 的整数倍(如 4、8、12),非整数倍由服务端向下取整(如 7→4),实际生效秒数以返回的 resumedAtSec 为准。仅实时导演支持回溯:在角色演绎与世界探索模式下调用 rewindTravel,会被 SDK 在本地以 103003 拒绝,不会发出任何 HTTP 请求;宿主应隐藏回溯入口,而不是只把按钮置灰。
异步语义、3× 重试退避(resumeTravel 退避 1 s / 2 s / 3 s)等契约细节见 HappyOyster Android SDK API Reference。
日志
SDK Logcat tag:HappyOysterSDK。SDK 提供两路互不影响的日志输出:
- 内置 Logcat(默认关闭):仅当
SDKConfig.logcatEnabled = true时 SDK 才写 Logcat(默认false,完全静默)。SDKConfig.logLevel过滤其最低等级,默认INFO已含重建会话所需的生命周期锚点(初始化、Travel 起止 / 状态迁移、RTC 入会 / 首帧);仅看报错可降为WARN,深入排障可升到DEBUG/VERBOSE。logLevel不影响logHandler。 - 宿主回调
logHandler(推荐):接收 SDK 每一条LogRecord(全量 firehose),与logLevel/logcatEnabled完全独立,可转发到宿主自有日志系统(Logcat、文件、崩溃平台等)。回调须快速、非阻塞且禁止在回调内回调 SDK;回调抛出的异常被静默捕获;LogRecord.message已脱敏,不含 Bearer token、ticket、RTC token 等明文。
travelId(即 encryptedTravelId)以全值输出,是发往服务端的会话键——排查某次会话或提工单时附上它,即可对齐客户端与服务端日志。每条 HTTP 响应日志还带服务端 requestId(reqId= 字段,缺失时为 -)用于定位单次请求;该 ID 仅出现在日志中,不进入任何公开返回值。
生命周期与内存
- 在
Application.onCreate调用initialize,全局一次。 - idle 状态下再次调用
initialize(例如切换 API Host或model)会关闭上一个 idle runtime 并重置已注册监听器、Feature Gate 状态;返回后请重新addListener。Travel 正在 starting、active 或 ending 时再次初始化会以103004拒绝;务必先等待endTravel()完成。initialize不会再丢弃进行中的 Travel。 - 体验与宿主生命周期绑定:在
Activity/Fragment的onDestroy(或 ViewModelonCleared)中调用endTravel,确保实时连接与资源释放。 attachVideo()返回的 View 在结束时从布局移除(container.removeAllViews()),并removeListener。- SDK 只持有 application context,你也不要把 Activity 传给 SDK。
协程与线程
- 业务方法是
suspend,可在任意 coroutine context 的lifecycleScope/viewModelScope中调用;SDK 会在内部主 dispatcher 协调状态与 RTC 操作,HTTP 仍不阻塞主线程。 - 调用方取消 coroutine 时,SDK 会取消该调用仍在途的 HTTP 请求(如有),并原样传播
CancellationException,不会转换为SDKError;不要把它当成业务错误捕获或吞掉。取消不代表服务端已经受理的请求会被回滚。 attachVideo()、sendCommand()在主线程调用;离主线程调用会同步抛出IllegalStateException。
token 管理
- Bearer API Key 有效期有限,建议在进入体验前确保 token 新鲜;收到
onError(101002)(token 过期)时重新获取 Bearer Key 并updateToken,无需重新换取 ticket。
错误恢复
- 对致命错误:清理本次体验状态(包括移除视频 View)、提示用户、允许重新开始。
- 对网络抖动(
106001)和上游服务临时异常(106003):可做有限次重试。 - 初始化同步抛出
100002时,为model空白;填写完整模型名称及版本后重新初始化。非空model的名称 / 版本错误、模型已下线、尚未发布或未授权时,网关通常返回 AccessDenied 并映射为106003;结合SDKError.raw检查model、API Host、API Key 的账号是否匹配。
模式适配
- 用
startTravel返回的mode决定可开放的交互能力:实时导演与角色演绎使用文本指令sendInstruct(角色演绎额外用aspectRatio定播放器方向,隐藏回溯),世界探索模式使用控制指令sendCommand。 - 世界探索模式(adventure)可选用重载
startTravel(ticket, maxExperienceTimeSec)限制本次体验最大时长(秒,到时自动结束);合法档位由服务端配置(当前60/90/120,默认60),实时导演与角色演绎 忽略该值(服务端忽略并回显null)。传入不支持的值时服务端返回400000且本次启动失败。参数细节见 API Reference。
角色演绎:按 aspectRatio 定播放器方向
StartTravelData.aspectRatio 仅角色演绎(acting)非空:"9:16"(竖屏,服务端创建世界时的默认值)或 "16:9"(横屏);世界探索与实时导演均为 null。画幅在创建世界时(由你的服务端通过 Open API 指定)就已确定,对客户端是只读结果。SDK 原样保留未识别的取值,宿主须把不认识的值等同 null 处理,回退到自己的默认方向。
时机:在 startTravel() 返回之后、调用 attachVideo() 把返回的 SurfaceView 挂进布局之前确定播放容器方向。SDK 只有在宿主挂载视图后才开始绑定并渲染远端流,所以此刻定方向仍然赶在首帧之前。该值随 startTravel() 的返回值交付,不保证在 SDK 加入实时通信频道之前完成——只需保证在 attachVideo() 挂载前定好方向即可。
后果:远端视图以裁剪填充(clip-to-fill)方式绑定——容器方向与 aspectRatio 不一致会裁掉画面(例如把 9:16 竖屏流放进 16:9 容器,上下会被切掉大半),而不是留黑边。
账号未开通角色演绎(acting)规格(或该规格总闸关闭)时,创建世界与进房都会被服务端拒绝:
startTravel 以 403007 失败(进房前拒绝,不创建 Travel),SDK 原样透传。该码不可按容量不足重试,应提示联系开通。完整示例(ViewModel + Activity 片段)
DevOps 排障指南
当集成过程中出现问题(无法进入 Travel、黑屏无画面、中途断流、暂停 / 恢复异常等),开放 SDK 日志是最快的定位手段。本章给出一套标准排障流程,便于你自查,也便于向我们反馈时一次性提供足够信息。
开启日志
排障时按需选择日志输出(详见日志):快速自查用 logcatEnabled = true 配合 adb logcat,排障建议把 logLevel 升到 DEBUG 或 VERBOSE(默认 INFO 已含完整生命周期时间线,仅关心报错可降到 WARN);接入自有系统则用 logHandler 接收全量 LogRecord(不受 logLevel 影响),写入你的文件或崩溃平台。
会话相关 ID:travelId
SDK 日志中的 travelId(即 StartTravelData.encryptedTravelId)以全值输出,是贯穿一次会话的唯一标识,也是发往服务端的会话键。它是把客户端日志与服务端会话记录对齐的关键——反馈问题时请务必带上出问题会话的 travelId。
采集日志
复现问题时,把 SDK 日志落盘:
反馈问题时请提供
为避免多轮往返,提交问题时请一并附上:
项 | 说明 |
|---|---|
SDK 版本 |
|
会话 | 出问题会话的 |
发生时间 | 出问题的大致时间点(精确到分钟即可) |
错误码 | 捕获到的 |
复现步骤 | 操作路径 + 期望结果 + 实际结果 |
运行环境 | 设备型号、Android 版本、网络环境(WiFi/蜂窝) |
日志文件 | 「采集日志」步骤采集的 |
关于脱敏与日志可分享性
SDK 日志在设计上即为可安全分享:凭据类信息(Bearer token、Travel ticket、RTC token)、RTC 内部标识与媒体地址在写入日志前已脱敏,不会出现明文;travelId 属会话标识(非凭据),保留全值仅用于对账。即便如此,仍建议通过可信渠道传输日志文件,不要公开粘贴到不受控的平台。