超视距遥操(Teleop)指南 · iOS
超视距遥操(Teleop)指南 · iOS
⚠️ 尚未上线:本能力为 v1.5.0-SNAPSHOT 开发中特性,接口与协议可能调整,正式发布前请勿用于生产环境。
v1.5.0 新增。Teleop = 超视距遥操:远端操作员通过 SDK 在线接管机器人的语音交互。iOS 端 Swift API 与 Java 一一对应,示例源码在
agentsdk_for_ios/Examples/TeleopExample/main.swift。
使用场景
- 机器人在现场无人守卫或光线 / 网络不佳的位置,需要远端操作员对特定请求进行接管应答;
- 二开 App 需要旁路 LLM,直接由人工语音接管一段窗口;
- 操作员的音频通过 SDK 上行到网关,网关按
agentId下发到目标机器人执行。
iOS 端与其他语言 SDK 一致,不承担视频拉流;若要在 iOS 侧同时看到机器人现场画面,请由 App 自行集成第三方 WebRTC 拉流方案(例如 Tencent TRRO 的 iOS SDK 或其他标准 WebRTC 客户端)。
调用流程
严格顺序:enter → 任意次 startAudio / appendAudioDelta / doneAudio → exit。
遥操存续期间可任意频次调用 keepalive 探活(推荐每 10 秒一次,见下文 机器人在线检测(keepalive))。
enter exit
┌──────────────────────┐ ┌──────────────────────┐
│ TeleopRequest.enter │ │ TeleopRequest.exit │
│ (onEnterAck) │ │ (onExitAck) │
└──────────┬───────────┘ └──────────┬───────────┘
│ 任意次 (fire-and-forget) │
▼ │
┌──────────────────────┐ │
│ startAudio(param:) │ │
│ appendAudioDelta(*) │───────────────────┘
│ doneAudio │
└──────────────────────┘
┌──────────────────────────┐
│ TeleopRequest.keepalive │ ← 建议每 10 秒一次;code != 0 视为机器人离线
│ (onKeepaliveAck) │
└──────────────────────────┘
enter/keepalive/exit都有 ACK 回调;相应on*Ack收到code == 0视为成功;startAudio(param:)/appendAudioDelta/doneAudio是 fire-and-forget,本地不排队等 ACK;- 若发送瞬间 SDK 连接已关闭,
enter/keepalive/exit回调会在原线程同步以code == 1000+msg == "Agentsdk connection is closed"触发一次失败回执。
Swift 快速代码
import LinksoulAgentSDK
// 1. 创建 SDK
let agentSdk = AgentSdk.create(url: url, appId: appId, appKey: appKey, appSecret: appSecret)
// 2. 注册鉴权
final class AuthCallback: AgentAuthCallback {
func onAuthState(appId: String, code: Int, msg: String) {
// code == 0 后再进入遥操
}
}
agentSdk.registerAuth(AuthCallback())
agentSdk.initialize()
Thread.sleep(forTimeInterval: 2.0)
// 3. 注册遥操会话
let teleopId = IdGenerator.generateTeleopId()
let teleopRequest = TeleopRequest(agentSdk: agentSdk, agentId: agentId, teleopId: teleopId)
agentSdk.registerTeleop(teleopRequest)
// 4. 进入
final class EnterCallback: TeleopEnterCallback {
override func onEnterAck(teleopId: String, eventId: String, code: Int, msg: String?) {
// code == 0 成功;-1 服务端拒绝;1000 本地连接关闭
}
}
let enterParam = AgentParam.create()
teleopRequest.enter(param: enterParam, enterCallback: EnterCallback(), timeout: 2000)
// 5. 开麦
let startParam = AgentParam.create()
.setString("role", "male") // male / female
.setInt("threshold", 0) // -14 ~ 14
let eventId = teleopRequest.startAudio(param: startParam)
// 6. 20ms 一片,base64 后 append(伪代码)
teleopRequest.appendAudioDelta(eventId: eventId, audioDelta: base64Chunk)
// 7. 关麦
teleopRequest.doneAudio(eventId: eventId)
// 8. 退出
final class ExitCallback: TeleopExitCallback {
override func onExitAck(teleopId: String, eventId: String, code: Int, msg: String?) {
// ...
}
}
teleopRequest.exit(exitCallback: ExitCallback(), timeout: 2000)
agentSdk.unregisterTeleop(teleopId: teleopId)
事件族与协议
AgentEventType 枚举里的遥操事件:
| Swift 枚举成员 | wire 值 | 说明 |
|---|---|---|
.teleopEnter | agentsdk.teleop.enter | 进入遥操(SDK → 网关) |
.teleopEnterAck | agentsdk.teleop.enter_ack | 进入回执 |
.teleopAudioStart | agentsdk.teleop_audio.start | 音频段开始(可携带 param) |
.teleopAudioDelta | agentsdk.teleop_audio.delta | 音频增量,audio 字段为 base64 字符串,可选 gain 为音频增益倍数(默认 1.0) |
.teleopAudioDone | agentsdk.teleop_audio.done | 音频段结束 |
.teleopKeepalive | agentsdk.teleop.keepalive | 机器人在线探活(SDK → 网关) |
.teleopKeepaliveAck | agentsdk.teleop.keepalive_ack | 探活回执(网关 → SDK) |
.teleopExit | agentsdk.teleop.exit | 退出遥操 |
.teleopExitAck | agentsdk.teleop.exit_ack | 退出回执 |
所有遥操消息 wire 结构:
{
"type": "<event type>",
"agentId": "<目标机器人 agentId>",
"teleopId": "teleop_xxxxxxxxxxxxxxxxxxxxx",
"eventId": "event_xxxxxxxxxxxxxxxxxxxxx",
"agentMode": "teleop"
}
agentMode 恒为 "teleop"(对应 AgentPolicy.teleop)。
startAudio 参数
startAudio(param:) 支持在每段音频开始时携带一份 AgentParam:
| 键 | 类型 | 取值示例 | 说明 |
|---|---|---|---|
role | String | "male" / "female" | 遥操音色选择 |
threshold | Int | -14 ~ 14 | VAD 灵敏度阈值 |
Swift 构造示例:
let startParam = AgentParam.create()
.setString("role", "male")
.setInt("threshold", 0)
let eventId = teleopRequest.startAudio(param: startParam)
机器人在线检测(keepalive)
遥操会话通常持续较长,期间机器人可能因网络中断 / 电量耗尽 / 现场断电等原因掉线。SDK 提供 TeleopRequest.keepalive(...) 一次性探活方法,业务侧应定时调用(建议每 10 秒一次)判断机器人是否仍在线。
接口与回调
@discardableResult
public func keepalive(keepaliveCallback: TeleopKeepaliveCallback, timeout: Int64) -> String
open class TeleopKeepaliveCallback: Timestamped {
open func onKeepaliveAck(teleopId: String, eventId: String, code: Int, msg: String?)
}
timeout:本地占位超时(< 50clamp 到 50,语义同enter/exit);- 返回本次消息的
eventId; - 每次调用发送一条
agentsdk.teleop.keepalive,网关按teleopId转发到目标机器人;机器人上线则回success == true。
code 含义(与 enter / exit 一致)
code | 含义 |
|---|---|
0 | 机器人在线且响应正常 |
-1 | 服务端返回失败,msg 携带 errorMsg——表示机器人已离线,业务侧必须处理该异常 |
1000 | SDK 端 WebSocket 已关闭,本次未真正下发 |
定时探活 + 掉线自动退出
任何一次 code != 0,都应视为机器人已离线——立即停止定时任务、exit 退出遥操、unregisterTeleop 释放资源,避免后续 appendAudioDelta 继续下发无效音频。
import Foundation
import LinksoulAgentSDK
final class KeepaliveWatcher {
private let teleopRequest: TeleopRequest
private let agentSdk: AgentSdk
private let queue = DispatchQueue(label: "teleop.keepalive")
private var timer: DispatchSourceTimer?
private var online = true
private let lock = NSLock()
init(teleopRequest: TeleopRequest, agentSdk: AgentSdk) {
self.teleopRequest = teleopRequest
self.agentSdk = agentSdk
}
/// enter 成功后调用;每 10 秒探活一次。
func start(intervalSec: Int = 10) {
let t = DispatchSource.makeTimerSource(queue: queue)
t.schedule(deadline: .now() + .seconds(intervalSec), repeating: .seconds(intervalSec))
t.setEventHandler { [weak self] in self?.tick() }
t.resume()
self.timer = t
}
func stop() {
timer?.cancel()
timer = nil
}
private func tick() {
lock.lock(); let stillOnline = online; lock.unlock()
guard stillOnline else { return }
teleopRequest.keepalive(keepaliveCallback: Callback(owner: self), timeout: 2000)
}
private final class Callback: TeleopKeepaliveCallback {
weak var owner: KeepaliveWatcher?
init(owner: KeepaliveWatcher) { self.owner = owner; super.init() }
override func onKeepaliveAck(teleopId: String, eventId: String, code: Int, msg: String?) {
guard let self = owner else { return }
if code == 0 { return }
self.lock.lock()
if !self.online { self.lock.unlock(); return }
self.online = false
self.lock.unlock()
print("robot offline => code=\(code), msg=\(msg ?? "")")
self.stop()
// 掉线自动退出遥操
self.teleopRequest.exit(exitCallback: ExitCb(owner: self), timeout: 2000)
}
}
private final class ExitCb: TeleopExitCallback {
weak var owner: KeepaliveWatcher?
init(owner: KeepaliveWatcher) { self.owner = owner; super.init() }
override func onExitAck(teleopId: String, eventId: String, code: Int, msg: String?) {
owner?.agentSdk.unregisterTeleop(teleopId: teleopId)
}
}
}
业务侧主动调用 exit 后,应同步调用 watcher.stop() 停止定时器,避免继续发送探活消息。
回调线程:
onKeepaliveAck在 SDK 的消息分发线程回调(非主线程)。如果需要更新 UI,请DispatchQueue.main.async { ... }切回主线程。
生命周期与清理
- SDK 内部按
teleopId维护活跃 request;调用enter前必须先registerTeleop(_:); - 每次 request 方法调用会刷新内部
updateTs;超过 7200s 未活动的 request 会被后台清理线程回收; - 调用完
exit或不再需要遥操时,调用agentSdk.unregisterTeleop(teleopId:)显式回收; AgentSdk.release()会连带清理该实例名下的所有 Teleop request。
回调线程约定
onEnterAck/onKeepaliveAck/onExitAck在 SDK 的消息分发线程回调(非主线程);如果要更新 UI,请自行切到DispatchQueue.main;TeleopEnterCallback/TeleopKeepaliveCallback/TeleopExitCallback是open class(不是protocol),需要继承 +override使用,方便共享timeout等基类字段。
常见问题
onEnterAck拿到code == 1000:底层 WebSocket 已断开或尚未建立。等AgentAuthCallback.onAuthState(code == 0)之后再enter;断连后 SDK 会自动重连,重连成功再次enter即可。- 每片 audio delta 建议多长:与被动交互侧一致 —— 20 ms ~ 40 ms 的 PCM/Opus 编码为 base64 后 append;太大容易堵塞 WS 单帧,太小则消息数过多。
enter之后是否需要立即startAudio:不需要。enter拿到 ACK 只表示遥操会话已就绪,业务侧可以在任意时刻推音频(例如等操作员按下发送按钮)。- 一次遥操可以调用几次
startAudio/doneAudio:任意多次。每一段讲话都是startAudio → appendAudioDelta × N → doneAudio,不需要exit再enter。多段之间使用不同的eventId(startAudio的返回值)。