关于 dockkit
DockKit 技能介绍如何使用 Apple 的 DockKit 框架,将兼容的电动云台与 iPhone 相机应用连接,实现主体跟踪和电机控制。内容涵盖系统跟踪、自定义检测、构图及配件事件处理。
主要功能
- 01发现并监听配件连接
- 02启用系统主体跟踪
- 03提交自定义检测结果
- 04设置构图模式与感兴趣区域
- 05控制电机运动和动画
- 06读取跟踪、电池与按键事件
适用场景
- 01开发跟随人物的相机应用
- 02用自定义 ML 模型跟踪主体
- 03为视频画面设置构图区域
- 04响应云台按键并监控电池
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill dockkit安装后在 Claude Code 中输入/dockkit确认已加载。
附带文件evals/evals.jsonreferences/dockkit-patterns.md
详细介绍
DockKit
用于与电动相机支架和云台集成的框架,可通过旋转 iPhone 实际跟踪拍摄对象。DockKit 负责电机控制、对象检测和取景,因此相机应用无需额外代码,即可实现 360 度水平旋转和 90 度俯仰跟踪。应用可以覆盖系统跟踪并提供自定义观测结果、直接控制电机,或调整取景。iOS 17+、Swift 6.3。
目录
设置
导入 DockKit:
import DockKit
DockKit 需要实体 DockKit 兼容配件和真机。模拟器无法连接底座硬件。
DockKit 本身不需要特殊权限或 DockKit 专用的 Info.plist 键。使用设备摄像头的相机应用仍需进行常规的相机隐私处理,包括 NSCameraUsageDescription。该框架会通过 DockKit 系统守护进程自动与已配对的配件通信。
应用必须使用 AVFoundation 相机 API。DockKit 会接入相机管线,分析帧以实现系统跟踪。
发现配件
使用 DockAccessoryManager.shared 观察底座连接状态:
import DockKit
func observeAccessories() async throws {
for await stateChange in try DockAccessoryManager.shared.accessoryStateChanges {
switch stateChange.state {
case .docked:
guard let accessory = stateChange.accessory else { continue }
// Accessory is connected and ready
configureAccessory(accessory)
case .undocked:
// iPhone removed from dock
handleUndocked()
@unknown default:
break
}
}
}
accessoryStateChanges 会发出 DockAccessory.StateChange 值,其中包含 state、accessory 和 trackingButtonEnabled。使用 accessory.identifier 获取名称、类别和 UUID;硬件详情可通过 firmwareVersion 和 hardwareModel 获取。
系统跟踪
系统跟踪是 DockKit 的默认模式。启用后,系统会通过内置机器学习推理分析相机帧、检测人脸和人体,并驱动电机使拍摄对象保持在画面中。任何使用 AVFoundation 相机 API 的应用都会自动受益。
启用或停用
// Enable system tracking (default)
try await DockAccessoryManager.shared.setSystemTrackingEnabled(true)
// Disable system tracking for custom control
try await DockAccessoryManager.shared.setSystemTrackingEnabled(false)
系统跟踪状态不会在应用终止、设备重启或前后台切换后保留。当应用需要特定状态时,请明确设置。
轻点以选择对象
允许用户通过轻点选择特定对象:
// Select the subject at a unit point in video-frame coordinates
try await accessory.selectSubject(at: CGPoint(x: 0.5, y: 0.5))
// Select specific subjects by identifier
try await accessory.selectSubjects([subjectUUID])
// Clear selection (return to automatic selection)
try await accessory.selectSubjects([])
自定义跟踪
使用自定义机器学习模型或 Vision 框架时,请停用系统跟踪并提供自己的观测结果。
提供观测结果
根据推理输出构造 DockAccessory.Observation 值,并以每秒 10 到 30 帧的频率传递给配件:
import DockKit
import AVFoundation
func processFrame(
_ sampleBuffer: CMSampleBuffer,
accessory: DockAccessory,
activeDevice: AVCaptureDevice
) async throws {
let cameraInfo = DockAccessory.CameraInformation(
captureDevice: activeDevice.deviceType,
cameraPosition: activeDevice.position,
orientation: .corrected,
cameraIntrinsics: frameIntrinsics(from: sampleBuffer),
referenceDimensions: frameDimensions(from: sampleBuffer)
)
let detection = try await detector.detect(sampleBuffer)
let observationType: DockAccessory.Observation.ObservationType = switch detection.kind {
case .face: .humanFace
case .body: .humanBody
case .object: .object
}
let observation = DockAccessory.Observation(
identifier: detection.id,
type: observationType,
rect: detection.rect, // normalized, lower-left origin
faceYawAngle: detection.faceYawAngle
)
try await accessory.track([observation], cameraInformation: cameraInfo)
}
观测类型
审核自定义跟踪时,请从仅支持的 ObservationType 情况中明确选择:.humanFace、.humanBody 和 .object。如果可能检测到人体或物体,请勿只回答 .humanFace。
rect 使用归一化坐标,原点位于左下角(与 Vision 框架使用相同的坐标系,无需转换)。
相机信息
DockAccessory.CameraInformation 描述当前活动相机;不要硬编码占位设备、内参或帧尺寸值。如果坐标已经相对于左下角,请将方向设为 .corrected。在审核答复中,应拒绝不透明的可选 cameraInfo 占位值,并展示如何根据当前 AVCaptureDevice 和当前 CMSampleBuffer 构造该值。
跟踪相关的方法也接受 [AVMetadataObject],而不只是观测结果。当 DockKit 应将观测结果或元数据与捕获的图像缓冲区合并时,请使用 image: CVPixelBuffer 重载;这些重载中必须提供 image 参数。
取景和感兴趣区域
取景模式
控制系统如何为跟踪对象取景:
try await accessory.setFramingMode(.automatic) // documented default
try await accessory.setFramingMode(.center) // explicit opt-in
| 模式 | 行为 |
|---|---|
.automatic | 文档所述的默认模式;由系统决定最佳取景方式 |
.center | 显式选择此模式可使对象保持居中 |
.left | 将对象置于画面左侧三分之一处 |
.right | 将对象置于画面右侧三分之一处 |
系统默认行为通常会将主要对象置于画面中央,但 .center 绝不是类似默认值的模式;默认模式是 .automatic。当图形叠加层占据画面部分区域时,请使用 .left 或 .right。
感兴趣区域
将跟踪范围限制在视频画面的特定区域:
// Normalized coordinates, origin at upper-left
let squareRegion = CGRect(x: 0.25, y: 0.0, width: 0.5, height: 1.0)
try await accessory.setRegionOfInterest(squareRegion)
裁剪为非标准宽高比时(例如用于视频会议的方形视频),请使用感兴趣区域,以便对象保持在可见区域内。
电机控制
直接控制电机前,请先停用系统跟踪。
角速度
以弧度/秒为单位设置连续旋转速度:
import Spatial
// Pan right at 0.2 rad/s, tilt down at 0.1 rad/s
let velocity = Vector3D(x: 0.1, y: 0.2, z: 0.0)
try await accessory.setAngularVelocity(velocity)
// Stop all motion
try await accessory.setAngularVelocity(Vector3D())
坐标轴:
x-- 俯仰(倾斜)。在 iOS 上,正值表示向下倾斜。y-- 偏航(水平转动)。正值表示向右转动。z-- 滚转(如果硬件支持)。
设置方向
在指定时长内移动到特定位置:
let target = Vector3D(x: 0.0, y: 0.5, z: 0.0) // Yaw 0.5 rad
let progress = try accessory.setOrientation(
target,
duration: .seconds(2),
relative: false
)
也接受用于基于四元数设置方向的 Rotation3D。设置 relative: true 可相对于当前位置移动。返回的 Progress 对象用于跟踪完成进度。
运动状态
监控配件的当前位置和速度:
for await state in try accessory.motionStates {
let positions = state.angularPositions // Vector3D
let velocities = state.angularVelocities // Vector3D
let time = state.timestamp
if let error = state.error {
// Motor error occurred
}
}
设置限制
限制每个轴的运动范围和最大速度:
let yawLimit = try DockAccessory.Limits.Limit(
positionRange: -1.0 ..< 1.0, // radians
maximumSpeed: 0.5 // rad/s
)
let limits = DockAccessory.Limits(yaw: yawLimit, pitch: nil, roll: nil)
try accessory.setLimits(limits)
动画
内置的角色动画可让底座做出富有表现力的动作:
// Disable system tracking before animating
try await DockAccessoryManager.shared.setSystemTrackingEnabled(false)
let progress = try await accessory.animate(motion: .kapow)
// Wait for completion
while !progress.isFinished && !progress.isCancelled {
try await Task.sleep(for: .milliseconds(100))
}
// Restore system tracking
try await DockAccessoryManager.shared.setSystemTrackingEnabled(true)
| 动画 | 效果 |
|---|---|
.yes | 点头动作 |
.no | 摇头动作 |
.wakeup | 启动风格的动作 |
.kapow | 夸张的钟摆式摆动 |
动画从配件当前位置开始,并以异步方式执行。动画完成后,请务必恢复跟踪状态。animate(motion:) 和 setOrientation(_:duration:relative:) 的调用频率每秒不得超过两次;调用频率过高会抛出 .frameRateTooHigh。
跟踪状态和对象选择
iOS 18+ 通过会抛出错误的 trackingStates 异步序列公开基于 ML 的跟踪信号。每个状态都有 time 和 trackedSubjects(.person 或 .object);人物还包含 identifier、rect、speakingConfidence、lookingAtCameraConfidence 和 saliencyRank(排名越低,显著性越高)。
if #available(iOS 18.0, *) {
for await state in try accessory.trackingStates {
var speaker: UUID?
var engaged: UUID?
var salient: (id: UUID, rank: Int)?
for subject in state.trackedSubjects {
switch subject {
case .person(let person):
let id = person.identifier, rect = person.rect
let speaking = person.speakingConfidence
let looking = person.lookingAtCameraConfidence
let rank = person.saliencyRank
updateSubjectOverlay(id: id, rect: rect)
if let speaking, speaking > 0.7 { speaker = id }
if let looking, looking > 0.7 { engaged = id }
if let rank, salient == nil || rank < salient!.rank { salient = (id, rank) }
case .object(let object):
let id = object.identifier, rect = object.rect
let rank = object.saliencyRank
updateSubjectOverlay(id: id, rect: rect)
if let rank, salient == nil || rank < salient!.rank { salient = (id, rank) }
}
}
if let id = speaker ?? engaged ?? salient?.id { try await accessory.selectSubjects([id]) }
}
}
使用 selectSubjects(_:) 按 UUID 锁定跟踪;传入 [] 可恢复自动选择。使用 speakingConfidence 查找说话者,使用 lookingAtCameraConfidence 判断参与度,使用 rect 设置叠加层,并将较低的 saliencyRank 值用作后备判断依据。
在评审答复中,请在代码中使用 lookingAtCameraConfidence 和 rect,而不只是用文字描述。
配件事件
底座上的实体按钮会通过会抛出错误的 accessoryEvents 异步序列触发事件(iOS 17.4+):
if #available(iOS 17.4, *) {
for await event in try accessory.accessoryEvents {
switch event {
case .cameraShutter: break
case .cameraFlip: break
case .cameraZoom(factor: let factor): break
case .button(id: let id, pressed: let pressed): break
@unknown default: break
}
}
}
第三方应用会接收这些事件,并通过 AVFoundation 实现相应行为。
电池监控
通过会抛出错误的 batteryStates 异步序列监控底座的电池状态(iOS 18+)。底座可能会报告多个电池,每个电池都由 name 标识:
if #available(iOS 18.0, *) {
var batteryRows: [String: (Double, DockAccessory.BatteryChargeState, Bool)] = [:]
for await battery in try accessory.batteryStates {
batteryRows[battery.name] = (battery.batteryLevel, battery.chargeState, battery.lowBattery)
}
}
常见错误
不要:未停用系统跟踪就控制电机
// WRONG -- system tracking fights manual commands
try await accessory.setAngularVelocity(velocity)
// CORRECT -- disable system tracking first
try await DockAccessoryManager.shared.setSystemTrackingEnabled(false)
try await accessory.setAngularVelocity(velocity)
不要:假设跟踪状态会在生命周期事件之间保留
// WRONG -- state may have reset after backgrounding
func applicationDidBecomeActive() {
// Assume custom tracking is still active
}
// CORRECT -- re-set tracking state on foreground
func applicationDidBecomeActive() {
Task {
try await DockAccessoryManager.shared.setSystemTrackingEnabled(false)
}
}
不要:超出建议频率调用 track()
// WRONG -- calling once per second is too slow
try await accessory.track(observations, cameraInformation: cameraInfo)
// (called at 1 fps)
// CORRECT -- call at 10-30 fps
// Hook into AVCaptureVideoDataOutputSampleBufferDelegate for per-frame calls
不要:频繁调用方向或动画接口
如果每秒调用 animate(motion:) 或 setOrientation(_:duration:relative:) 超过两次,DockKit 可能会抛出 .frameRateTooHigh。设置好运动轨迹后,请观察其 Progress,并避免在紧密循环中发送命令。
不要:动画结束后忘记恢复跟踪
// WRONG -- tracking stays disabled after animation
try await DockAccessoryManager.shared.setSystemTrackingEnabled(false)
let progress = try await accessory.animate(motion: .kapow)
// CORRECT -- restore tracking when animation completes
try await DockAccessoryManager.shared.setSystemTrackingEnabled(false)
let progress = try await accessory.animate(motion: .kapow)
while !progress.isFinished && !progress.isCancelled {
try await Task.sleep(for: .milliseconds(100))
}
try await DockAccessoryManager.shared.setSystemTrackingEnabled(true)
不要:在 Simulator 中使用 DockKit
DockKit 需要实体的 DockKit 兼容配件。请在初始化时加入防护,并在配件不可用时提供回退行为。
审查清单
- 在需要的位置包含
import DockKit - 订阅
accessoryStateChanges以检测扩展坞连接/断开事件 - 处理
.docked和.undocked两种状态 - 在使用自定义跟踪或控制电机前禁用系统跟踪
- 动画完成后恢复系统跟踪
- 以 10-30 fps 提供自定义观察结果
- 将
animate和setOrientation命令限制为每秒最多调用 2 次 - 观察结果中的
rect使用归一化坐标(原点位于左下角) - 根据处于活动状态的
AVCaptureDevice和当前样本缓冲区内联构建摄像头信息 - 观察类型选择使用名称
.humanFace、.humanBody和.object - 在所有针对 DockKit 枚举的 switch 语句中处理
@unknown default - 如果要限制配件的运动范围,请设置运动限制
- 应用返回前台后重新应用跟踪状态
- 使用
#available(iOS 17.4, *)对accessoryEvents进行保护 - 使用
#available(iOS 18.0, *)对trackingStates和batteryStates进行保护 - 电池 UI 保留
BatteryState.name,以支持多电池扩展坞 - 在 Simulator 构建中不执行任何 DockKit 代码路径