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 は、state、 accessory、trackingButtonEnabled を含む DockAccessory.StateChange 値を発行します。名前、 カテゴリ、UUID の取得には accessory.identifier を使用します。ハードウェアの詳細は firmwareVersion と hardwareModel から取得できます。

システム追尾

システム追尾は DockKit のデフォルトモードです。有効にすると、システムは 組み込みの ML 推論を通じてカメラフレームを解析し、顔や 体を検出して、被写体がフレーム内に収まるようにモーターを動かします。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([])

カスタム追尾

カスタム ML モデルや Vision フレームワークを使用する場合は、システム追尾を 無効にし、独自の観測結果を提供します。

観測結果の提供

推論結果から DockAccessory.Observation 値を作成し、10~30 fps で アクセサリに渡します。

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対象を左側の3分の1にフレーミングします
.right対象を右側の3分の1にフレーミングします

システムのデフォルトの動作では、主要な対象が中央に配置されることがよくありますが、.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:) の呼び出しは、1秒あたり2回以下にしてください。呼び出し頻度がこれを超えると、.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:) を1秒あたり2回より多く呼び出すと、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)
やってはいけないこと:シミュレータで DockKit を使用する

DockKit には、DockKit 対応の物理アクセサリが必要です。初期化時に利用可能かどうかを確認し、アクセサリがない場合のフォールバック動作を用意してください。

レビューチェックリスト

  • 必要な箇所に import DockKit が記述されている
  • ドックへの接続・取り外しイベントを検出するため、accessoryStateChanges を購読している
  • .docked と .undocked の両方の状態を処理している
  • カスタムトラッキングやモーター制御を行う前に、システムトラッキングを無効にしている
  • アニメーション完了後に、システムトラッキングを復元している
  • カスタム観測データが 10~30 fps で提供されている
  • animate コマンドと setOrientation コマンドの呼び出しを、毎秒 2 回までに制限している
  • 観測データの rect で、正規化された座標(左下原点)を使用している
  • カメラ情報を、アクティブな AVCaptureDevice と現在のサンプルバッファーからインラインで構築している
  • 観測タイプの選択で .humanFace、.humanBody、.object を指定している
  • DockKit の列挙型を扱うすべての switch 文で @unknown default を処理している
  • アクセサリの可動範囲を制限する場合、モーション制限を設定している
  • アプリがフォアグラウンドに戻った後、トラッキング状態を再適用している
  • accessoryEvents を #available(iOS 17.4, *) でガードしている
  • trackingStates と batteryStates を #available(iOS 18.0, *) でガードしている
  • バッテリー UI で、複数バッテリー搭載ドックに対応するため BatteryState.name を保持している
  • Simulator ビルドで DockKit のコードパスを実行していない

参考資料

コメント