超视距遥操(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))。

text
              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 快速代码

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 值说明
.teleopEnteragentsdk.teleop.enter进入遥操(SDK → 网关)
.teleopEnterAckagentsdk.teleop.enter_ack进入回执
.teleopAudioStartagentsdk.teleop_audio.start音频段开始(可携带 param)
.teleopAudioDeltaagentsdk.teleop_audio.delta音频增量,audio 字段为 base64 字符串,可选 gain 为音频增益倍数(默认 1.0)
.teleopAudioDoneagentsdk.teleop_audio.done音频段结束
.teleopKeepaliveagentsdk.teleop.keepalive机器人在线探活(SDK → 网关)
.teleopKeepaliveAckagentsdk.teleop.keepalive_ack探活回执(网关 → SDK)
.teleopExitagentsdk.teleop.exit退出遥操
.teleopExitAckagentsdk.teleop.exit_ack退出回执

所有遥操消息 wire 结构:

json
{
  "type":      "<event type>",
  "agentId":   "<目标机器人 agentId>",
  "teleopId":  "teleop_xxxxxxxxxxxxxxxxxxxxx",
  "eventId":   "event_xxxxxxxxxxxxxxxxxxxxx",
  "agentMode": "teleop"
}

agentMode 恒为 "teleop"(对应 AgentPolicy.teleop)。

startAudio 参数

startAudio(param:) 支持在每段音频开始时携带一份 AgentParam:

键类型取值示例说明
roleString"male" / "female"遥操音色选择
thresholdInt-14 ~ 14VAD 灵敏度阈值

Swift 构造示例:

swift
let startParam = AgentParam.create()
    .setString("role", "male")
    .setInt("threshold", 0)
let eventId = teleopRequest.startAudio(param: startParam)

机器人在线检测(keepalive)

遥操会话通常持续较长,期间机器人可能因网络中断 / 电量耗尽 / 现场断电等原因掉线。SDK 提供 TeleopRequest.keepalive(...) 一次性探活方法,业务侧应定时调用(建议每 10 秒一次)判断机器人是否仍在线。

接口与回调

swift
@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:本地占位超时(< 50 clamp 到 50,语义同 enter/exit);
  • 返回本次消息的 eventId;
  • 每次调用发送一条 agentsdk.teleop.keepalive,网关按 teleopId 转发到目标机器人;机器人上线则回 success == true。

code 含义(与 enter / exit 一致)

code含义
0机器人在线且响应正常
-1服务端返回失败,msg 携带 errorMsg——表示机器人已离线,业务侧必须处理该异常
1000SDK 端 WebSocket 已关闭,本次未真正下发

定时探活 + 掉线自动退出

任何一次 code != 0,都应视为机器人已离线——立即停止定时任务、exit 退出遥操、unregisterTeleop 释放资源,避免后续 appendAudioDelta 继续下发无效音频。

swift
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 等基类字段。

常见问题

  1. onEnterAck 拿到 code == 1000:底层 WebSocket 已断开或尚未建立。等 AgentAuthCallback.onAuthState(code == 0) 之后再 enter;断连后 SDK 会自动重连,重连成功再次 enter 即可。
  2. 每片 audio delta 建议多长:与被动交互侧一致 —— 20 ms ~ 40 ms 的 PCM/Opus 编码为 base64 后 append;太大容易堵塞 WS 单帧,太小则消息数过多。
  3. enter 之后是否需要立即 startAudio:不需要。enter 拿到 ACK 只表示遥操会话已就绪,业务侧可以在任意时刻推音频(例如等操作员按下发送按钮)。
  4. 一次遥操可以调用几次 startAudio / doneAudio:任意多次。每一段讲话都是 startAudio → appendAudioDelta × N → doneAudio,不需要 exit 再 enter。多段之间使用不同的 eventId(startAudio 的返回值)。