超视距遥操(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 应用推流时,建议:

  1. AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .voiceChat, ...)
  2. 用 AVAudioEngine 挂 AVAudioMixerNode → installTap(onBus:...) 拿 AVAudioPCMBuffer;
  3. 每 20 ms 一片,转成 s16le PCM,Data.base64EncodedString();
  4. 传给 teleopRequest.appendAudioDelta(eventId:audioDelta:)。

采样率与 base64 分片规约与 Java / Android 一致;参考 Java 示例的注释部分。