Controlling a DockKit accessory using your camera app
Follow subjects in real time using an iPhone that you mount on a DockKit accessory.
Overview
This sample code project shows you how to use your camera app with a DockKit accessory to frame and track subjects in real time. It demonstrates how DockKit system tracking works for your camera app, and how you can override system tracking to frame and track specific subjects using custom machine learning signals. It also shows you how to integrate physical buttons on your DockKit device with camera controls.
The sample uses SwiftUI and the features of Swift concurrency to build a responsive camera app with DockKit control. See AVCam: Building a camera app for more details about the camera implementation design. This sample code project uses the sample app from that project as a starting point to write a basic camera app. The following diagram depicts the app’s design:
[Image]
The sample app defines two key services:
CaptureServiceis an actor that manages the interactions with the AVFoundation capture APIs. This object configures the capture pipeline and manages its life cycle, and it defines an asynchronous interface to capture videos. It also delegates handling of those operatons to the app’sMovieCaptureobject.DockControlServiceis an actor that manages interactions with a DockAccessory using DockKit APIs. This object listens toDockAccessoryconnection/disconnection events, manages subscriptions to the connectedDockAccessory, and controls its movements using an asynchronous interface. It also delegates camera control in response toDockAccessoryevents to theCameraModelobject.
Configure the sample code project
Because Simulator doesn’t have access to device cameras and can’t connect to a DockKit device, it isn’t suitable for running the sample app. To run the app, you need an iPhone with iOS 18 or later.
Write a basic camera app to take photos
See AVCam: Building a camera app to learn how to write a basic camera app to capture videos using an iPhone’s front and rear cameras.
Configure the DockKit accessory manager
AVCaptureSession is a singleton class that provides connection and disconnection notifications with a DockKit accessory by subscribing to the accessoryStateChanges API.
The dock control service subscribes to accessoryStateChanges in its setUp(features: DockAccessoryFeatures) method.
// Subscribe to accessory state changes.
for await stateEvent in try DockAccessoryManager.shared.accessoryStateChanges {
// Save the DockKit accessory when docked (connected).
if let newAccessory = stateEvent.accessory, stateEvent.state == .docked {
dockkitAccessory = newAccessory
await setupAccessorySubscriptions(for: newAccessory)
}
}When an accessory connects, DockKit sets it up to use system tracking, and to listen to accessory events and battery states in setupAccessorySubscriptions(for accesory: DockAccessory).
func setupAccessorySubscriptions(for accesory: DockAccessory) async {
// Enable system tracking on the first connection.
try await DockAccessoryManager.shared.setSystemTrackingEnabled(true)
// Start the necessary subscriptions to accessory events and battery states.
subscribeToAccessoryEvents(for: accesory)
toggleBatterySummary(to: true, for: accesory)
}Change the tracking mode
The app provides a tracking mode menu to switch between system tracking, custom tracking, and manual tracking. The default is system tracking, which the app sets by calling setSystemTrackingEnabled(_:) to true.
func updateTrackingMode(to trackingMode: TrackingMode) async {
self.trackingMode = trackingMode
// Call `systemTrackingEnabled` with `true` to enable the system tracking mode.
try await DockAccessoryManager.shared.setSystemTrackingEnabled(trackingMode == .system ? true : false)
}The app provides various menus and options to configure the selected subjects, selected frame, region to track, and more. All menus and buttons primarily live in the main DockKit menu.
Tap to track the subject
The app provides a tap-to-track toggle to enable or disable selecting a specific subject to track by tapping the camera view. When the tap-to-track toggle is enabled, the selectSubject(at point: CGPoint?) method allows people to select tapped subjects.
func selectSubject(at point: CGPoint?) async -> Bool {
if let point = point {
// Select a specific subject at the point.
try await accessory.selectSubject(at: point)
} else {
// Clear the selected subjects.
try await accessory.selectSubjects([])
}
}Set the region of interest
The app provides a region-of-interest toggle to enable or disable setting a region of interest to frame the selected subjects by holding and dragging the camera view. When toggling the region of interest, the setRegionOfInterest(to region: CGRect) method allows setting a region CGRect in the camera view. The dock accessory keeps the subjects framed in the selected region.
func setRegionOfInterest(to region: CGRect) async {
try await accessory.setRegionOfInterest(region)
}Set the framing mode
The app provides a framing mode menu to select a DockAccessory.FramingMode.
func updateFraming(to framing: FramingMode) async -> Bool {
try await accessory.setFramingMode(dockKitFramingMode(from: framing))
}The app uses the helper function dockKitFramingMode(from: framing) to map a local FramingMode enumeration to DockAccessory.FramingMode.
func dockKitFramingMode(from framingMode: FramingMode) -> DockAccessory.FramingMode {
switch framingMode {
case .auto:
return DockAccessory.FramingMode.automatic
case .center:
return DockAccessory.FramingMode.center
case .left:
return DockAccessory.FramingMode.left
case .right:
return DockAccessory.FramingMode.right
}
}Implement manual control using actuator velocities
When someone sets the TrackingMode to TrackingMode.manual, the app provides chevrons to move DockAccessory up, left, right, and down by using the setAngularVelocity(_:) API.
func handleChevronTapped(chevronType: ChevronType, speed: Double = 0.2) async {
var velocity = Vector3D()
switch chevronType {
case .tiltUp:
velocity.x = -speed
break
case .tiltDown:
velocity.x = speed
break
case .panLeft:
velocity.y = -speed
break
case .panRight:
velocity.y = speed
break
}
try await dockkitAccessory.setAngularVelocity(velocity)
}Run the default animations
The app provides buttons to run the four default animations that DockAccessory provides. Before running the animation, the app disables system tracking. When the animation is complete, the app restores system tracking to its prior value.
// Disable the system tracking before running the animation.
try await DockAccessoryManager.shared.setSystemTrackingEnabled(false)
// Run the animation and wait for it to finish.
let progress = try await dockkitAccessory.animate(motion: dockKitAnimation(from: animation))
while(!progress.isCancelled && !progress.isFinished) {
try await Task.sleep(nanoseconds: NSEC_PER_SEC/10) // 0.1 sec
}
// Restore the system tracking after running the animation.
try await DockAccessoryManager.shared.setSystemTrackingEnabled(trackingMode == .system ? true : false)The app uses the helper function dockKitAnimation(from animation: Animation) to map a local animation enumeration to DockAccessory.Animation.
func dockKitAnimation(from animation: Animation) -> DockAccessory.Animation {
switch animation {
case .yes:
return DockAccessory.Animation.yes
case .nope:
return DockAccessory.Animation.nope
case .wakeup:
return DockAccessory.Animation.wakeup
case .kapow:
return DockAccessory.Animation.kapow
}
}The DockKit menu provides toggles to subscribe to various states, like battery and tracking, and displays them in the app’s UI.
Implement the battery state
The dock control service subscribes to batteryStates to acquire the current battery state of the accessory. The current battery state includes the battery level, charging indicator, and so forth.
for await batterySummaryState in try dockkitAccessory.batteryStates {
battery = .available(percentage: batterySummaryState.batteryLevel, charging: batterySummaryState.chargeState == .charging)
}Implement the tracking states
The dock control service subscribes to DockAccessory.TrackingStates to get a list of tracked subjects with attributes like saliency and speaking confidence. The dock control service delegates the handling of the conversion from a normalized subject rectangle to camera view space coordinates to the CameraModel, which uses the capture service for the operation. The app uses these states, along with the transformed subject rectangle, to show an overlay on the faces of the subjects.
for await trackingSummaryState in try dockkitAccessory.trackingStates {
for subject in trackingSummaryState.trackedSubjects {
switch subject {
case .person(let person):
if let rect = await cameraCaptureDelegate?.convertToViewSpace(from: person.rect) {
// Create a `DockAccessoryTrackedPerson` object from `TrackingState`.
trackedPersons.append(DockAccessoryTrackedPerson(saliency: person.saliencyRank, rect: rect,
speaking: person.speakingConfidence, looking: person.lookingAtCameraConfidence))
}
default:
// Do nothing.
break
}
}
}Implement camera control using accessory events
The dock control service subscribes to an async stream of DockAccessory.AccessoryEvents. A physical input on the DockAccessory triggers an accessory event. When the app receives an accessory event, it delegates handling of the event to the CameraModel, which uses the capture service to perform camera operations.
for await event in try accesory.accessoryEvents {
switch (event) {
case let .button(id, pressed):
break
case .cameraZoom(factor: let factor):
let zoomType = factor > 0 ? CameraZoomType.increase : CameraZoomType.decrease
// Implement the camera zoom.
cameraCaptureDelegate?.zoom(type: zoomType, factor: 0.2)
break
case .cameraShutter:
if (Date.now.timeIntervalSince(lastShutterEventTime) > 0.2) {
// Implement the camera start capture or stop capture.
cameraCaptureDelegate?.startOrStartCapture()
lastShutterEventTime = .now
}
break
case .cameraFlip:
// Implement the camera flip.
cameraCaptureDelegate?.switchCamera()
break
default: break
}
}CameraModel implements the sample’s CameraCaptureDelegate protocol and provides the helper methods to control the camera.
Implement the camera zoom
The capture service implements the updateMagnification(for zoomType: CameraZoomType, by scale: Double = 0.2) method in response to a zoom event from the accessory.
func updateMagnification(for zoomType: CameraZoomType, by scale: Double = 0.2) {
try? currentDevice.lockForConfiguration()
let magnification = (zoomType == .increase ? 1.0 : -1.0) * scale
var newZoomFactor = currentDevice.videoZoomFactor + magnification
newZoomFactor = max(min(newZoomFactor, self.maxZoomFactor), self.minZoomFactor)
newZoomFactor = Double(round(10 * newZoomFactor) / 10)
currentDevice.videoZoomFactor = newZoomFactor
currentDevice.unlockForConfiguration()
self.zoomFactor = newZoomFactor
}Implement the camera shutter
The capture service implements the startRecording() method in response to a start-capture shutter event, and a stopRecording() method in response to a stop-capture shutter event.
func startRecording() {
movieCapture.startRecording()
}
func stopRecording() async throws -> Movie {
try await movieCapture.stopRecording()
}Implement the camera flip
The capture service implements the selectNextVideoDevice() method in reponse to the camera flip event.
func selectNextVideoDevice() {
// Change the session's active capture device.
changeCaptureDevice(to: nextDevice)
}