mcpField
D
dockkit

dockkit

github.com/dpearson2699开发1.2k3.1k 次安装13 周前更新
代码仓库

关于 dockkit

DockKit 技能介绍如何使用 Apple 的 DockKit 框架,将兼容的电动云台与 iPhone 相机应用连接,实现主体跟踪和电机控制。内容涵盖系统跟踪、自定义检测、构图及配件事件处理。

主要功能

  1. 01发现并监听配件连接
  2. 02启用系统主体跟踪
  3. 03提交自定义检测结果
  4. 04设置构图模式与感兴趣区域
  5. 05控制电机运动和动画
  6. 06读取跟踪、电池与按键事件

适用场景

  1. 01开发跟随人物的相机应用
  2. 02用自定义 ML 模型跟踪主体
  3. 03为视频画面设置构图区域
  4. 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 代码路径

参考资料

评论

支持 Markdown