超视距遥操(Teleop)示例 · iOS
超视距遥操(Teleop)示例 · iOS
⚠️ 尚未上线:本能力为 v1.5.0-SNAPSHOT 开发中特性,接口与协议可能调整,正式发布前请勿用于生产环境。
v1.5.0 新增。iOS 端最小示例,完整源码在
agentsdk_for_ios/Examples/TeleopExample/main.swift。此外agentsdk_for_ios/Examples/TeleopSimulator/main.swift提供一个更长的运行时压力测试样例。
TeleopExample/main.swift
swift
import Foundation
import LinksoulAgentSDK
// 遥操:enter → startAudio → appendAudioDelta → doneAudio → exit
let url = "wss://open.agibot.com/api/V1/open-portal/app/wss/agent-sdk"
let appId = "您通过灵心开放平台创建的应用获取的appId"
let appKey = "您通过灵心开放平台创建的应用获取的appKey"
let appSecret = "您通过灵心开放平台创建的应用获取的appSecret"
let agentId = "您通过灵心平台创建的二开智能体获取的agentId"
let agentSdk = AgentSdk.create(url: url, appId: appId, appKey: appKey, appSecret: appSecret)
final class AuthCallback: AgentAuthCallback {
func onAuthState(appId: String, code: Int, msg: String) {
print("onAuthState => appId: \(appId), code: \(code), msg: \(msg)")
}
}
agentSdk.registerAuth(AuthCallback())
agentSdk.initialize()
Thread.sleep(forTimeInterval: 2.0)
// 注册遥操请求
let teleopId = IdGenerator.generateTeleopId()
let teleopRequest = TeleopRequest(agentSdk: agentSdk, agentId: agentId, teleopId: teleopId)
agentSdk.registerTeleop(teleopRequest)
// 进入遥操
final class EnterCallback: TeleopEnterCallback {
override func onEnterAck(teleopId: String, eventId: String, code: Int, msg: String?) {
print("onEnterAck => teleopId \(teleopId), eventId \(eventId), code \(code), msg \(msg ?? "")")
}
}
let enterParam = AgentParam.create()
teleopRequest.enter(param: enterParam, enterCallback: EnterCallback(), timeout: 2000)
// 开始说话
let startParam = AgentParam.create()
.setString("role", "male") // male/female
.setInt("threshold", 0) // -14 - 14
let eventId = teleopRequest.startAudio(param: startParam)
// 持续说话(audio delta 为 base64 编码字符串)
teleopRequest.appendAudioDelta(eventId: eventId, audioDelta: "")
// 结束说话
teleopRequest.doneAudio(eventId: eventId)
// 检查机器人是否在线(详见「定时探活」小节,遥操期间应每 10s 探活一次)
final class KeepaliveCallback: TeleopKeepaliveCallback {
override func onKeepaliveAck(teleopId: String, eventId: String, code: Int, msg: String?) {
print("onKeepaliveAck => teleopId \(teleopId), eventId \(eventId), code \(code), msg \(msg ?? "")")
}
}
teleopRequest.keepalive(keepaliveCallback: KeepaliveCallback(), timeout: 2000)
// 退出遥操
final class ExitCallback: TeleopExitCallback {
override func onExitAck(teleopId: String, eventId: String, code: Int, msg: String?) {
print("onExitAck => teleopId \(teleopId), eventId \(eventId), code \(code), msg \(msg ?? "")")
}
}
teleopRequest.exit(exitCallback: ExitCallback(), timeout: 2000)
RunLoop.main.run()
定时探活(keepalive)
enter 拿到 code == 0 之后,业务侧应每 10 秒发起一次 keepalive 探活;onKeepaliveAck 收到 code != 0 即视为机器人离线,需停止定时、退出遥操、注销资源。
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 { ... }。
常见改写要点
| 想做 | 改哪里 |
|---|---|
| 一次遥操中间多次讲话 | 每次讲话都重新 startAudio(param:) 拿到新的 eventId,appendAudioDelta / doneAudio 使用该 eventId;无需 exit 再 enter |
| 切换音色 / 灵敏度 | 每一段 startAudio 时重新填 AgentParam.role / threshold;不影响前一段音频 |
| 中途取消遥操 | 直接 exit(exitCallback:timeout:),不再补发 doneAudio |
| 感知 SDK 鉴权 / 断连 | 在 AgentAuthCallback.onAuthState 里维护一个 authed 标志;enter 前 double-check;或在 onEnterAck 收到 code == 1000 后触发重连 |
| 多机器人同时遥操 | 每个 agentId 建一个 TeleopRequest;SDK 按 teleopId 索引,互不冲突 |
关于 RunLoop.main.run()
命令行 target 需要显式保活主线程,否则进程会在最后一行代码执行完之后立刻退出。iOS App target 由 UIApplication 自带 runloop,无需此调用;SwiftUI / UIKit 页面直接把回调 UI 更新切到 DispatchQueue.main.async 即可。
麦克风采集(App 场景)
iOS 应用推流时,建议:
AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .voiceChat, ...)- 用
AVAudioEngine挂AVAudioMixerNode→installTap(onBus:...)拿AVAudioPCMBuffer; - 每 20 ms 一片,转成 s16le PCM,
Data.base64EncodedString(); - 传给
teleopRequest.appendAudioDelta(eventId:audioDelta:)。
采样率与 base64 分片规约与 Java / Android 一致;参考 Java 示例的注释部分。