Highlight
iPadOS 17 让应用可以通过 USB-C 接口使用 Apple Studio Display 内置摄像头、USB 网络摄像头和 HDMI 切换器等外部视频设备,并引入了自动摄像头选择 API 和
AVCaptureDeviceRotationCoordinator来正确处理视频旋转。
核心内容
Stage Manager 让 iPad 可以扩展到多个屏幕。当你把 iPad 接到 Apple Studio Display 上时,FaceTime 会自动使用显示器上的摄像头,因为那个角度更适合视频通话。iPadOS 17 把这个能力开放给了所有应用。
(00:28)但外接摄像头和内置摄像头有一个本质区别:它们可以独立于 iPad 移动。旋转 iPad 时,外接摄像头的朝向不会改变。这让视频旋转变得复杂。
发现外接摄像头
(04:10)外接摄像头通过 AVCaptureDevice.DeviceType.external 标识。它们的位置是 .unspecified,因为可以独立于设备移动。
以前用内置摄像头的应用,只需要少量改动就能支持外接摄像头。但外接摄像头随时可能被拔掉,应用必须处理连接和断开事件。
自动摄像头选择
(08:38)手动处理摄像头切换逻辑会变得很复杂。iPadOS 17 引入了自动摄像头选择 API:
userPreferredCamera:记录用户选择的摄像头,系统会跨启动保存这个偏好systemPreferredCamera:系统根据用户偏好和当前连接的设备,智能推荐最佳摄像头
当用户连接一个新的外接摄像头时,系统会认为用户想使用它,自动将其设为首选。
视频旋转
(17:55)AVCaptureVideoOrientation 在 iPadOS 17 中被废弃了。它假设摄像头随设备一起旋转,这对外接摄像头不适用。
新的 AVCaptureDeviceRotationCoordinator 提供了两个角度:
videoRotationAngleForHorizonLevelPreview:用于预览,让画面相对于重力保持水平videoRotationAngleForHorizonLevelCapture:用于拍摄,让照片和视频在查看时保持 upright
外接麦克风
(27:38)外接摄像头自带的麦克风也可以被应用使用。iPadOS 17 改进了 USB-C 外接麦克风的支持,包括语音隔离(Voice Isolation)和回声消除的优化调参。
详细内容
发现外接摄像头
(04:10)使用 AVCaptureDeviceDiscoverySession 发现外接摄像头:
let discoverySession = AVCaptureDevice.DiscoverySession(
deviceTypes: [.external],
mediaType: .video,
position: .unspecified
)
let externalCameras = discoverySession.devices
关键点:
- 外接摄像头的
deviceType是.external .externalUnknown在 iPadOS 上已被废弃position为.unspecified
自动摄像头选择
(09:11)使用系统推荐的摄像头:
// 首次启动时设置默认偏好
if UserDefaults.standard.object(forKey: "hasSetInitialCamera") == nil {
let discoverySession = AVCaptureDevice.DiscoverySession(
deviceTypes: [.builtInWideAngleCamera],
mediaType: .video,
position: .back
)
if let backCamera = discoverySession.devices.first {
AVCaptureDevice.userPreferredCamera = backCamera
}
UserDefaults.standard.set(true, forKey: "hasSetInitialCamera")
}
// 获取系统推荐的摄像头
let preferredCamera = AVCaptureDevice.systemPreferredCamera
监听摄像头变化:
// 使用 KVO 监听 systemPreferredCamera
var observation: NSKeyValueObservation?
observation = AVCaptureDevice.observe(\.systemPreferredCamera, options: [.new]) { device, change in
guard let newCamera = change.newValue else { return }
// 切换到新摄像头,但不要在录制中打断
if !isRecording {
switchToCamera(newCamera)
}
}
关键点:
userPreferredCamera是读写属性,设置后系统会记住用户偏好systemPreferredCamera是只读属性,综合用户偏好和当前可用设备- 用户连接新摄像头时,系统会自动推荐它
- 切换摄像头时不要打断正在进行的录制
处理连接和断开
(06:40)外接摄像头随时可能被拔掉,应用需要优雅处理:
// 监听特定设备的连接状态
var connectionObservation: NSKeyValueObservation?
connectionObservation = camera.observe(\.isConnected, options: [.new]) { device, change in
if let connected = change.newValue, !connected {
// 摄像头断开,切换到内置摄像头
DispatchQueue.main.async {
switchToBuiltInCamera()
}
}
}
// 或者监听 DiscoverySession 的设备列表变化
let discoverySession = AVCaptureDevice.DiscoverySession(
deviceTypes: [.external, .builtInWideAngleCamera],
mediaType: .video,
position: .unspecified
)
discoverySession.addObserver(self, forKeyPath: "devices", options: [.new], context: nil)
关键点:
- KVO 回调和通知都在后台队列执行
- 需要同步到
AVCaptureSession队列和主线程 - 同一物理设备重新连接后,会获得一个新的
AVCaptureDevice实例
视频旋转
(19:00)使用 AVCaptureDeviceRotationCoordinator 处理旋转:
// 创建旋转协调器
var rotationCoordinator: AVCaptureDeviceRotationCoordinator?
func setupRotationCoordinator(for device: AVCaptureDevice, previewLayer: AVCaptureVideoPreviewLayer) {
rotationCoordinator = AVCaptureDeviceRotationCoordinator(
device: device,
previewLayer: previewLayer
)
// 设置初始预览旋转
updatePreviewRotation()
// 监听预览旋转角度变化
previewObservation = rotationCoordinator?.observe(
\.videoRotationAngleForHorizonLevelPreview,
options: [.new]
) { coordinator, change in
DispatchQueue.main.async {
self.updatePreviewRotation()
}
}
}
func updatePreviewRotation() {
guard let coordinator = rotationCoordinator else { return }
let angle = coordinator.videoRotationAngleForHorizonLevelPreview
// 应用到预览连接的 videoRotationAngle
if let connection = previewLayer.connection,
connection.isVideoRotationAngleSupported(angle) {
connection.videoRotationAngle = angle
}
}
// 拍摄时应用捕获旋转
func applyCaptureRotation(for output: AVCapturePhotoOutput) {
guard let coordinator = rotationCoordinator else { return }
let angle = coordinator.videoRotationAngleForHorizonLevelCapture
if let connection = output.connection(with: .video),
connection.isVideoRotationAngleSupported(angle) {
connection.videoRotationAngle = angle
}
}
关键点:
AVCaptureDeviceRotationCoordinator适用于所有平台(iOS、macOS、tvOS)- 预览旋转在主队列更新,用于 UI 同步
- 拍摄旋转确保照片和视频在查看时保持 upright
- 切换摄像头时要创建新的 rotation coordinator
AVCaptureVideoOrientation已废弃,应迁移到新 API
自定义预览旋转(使用 VideoDataOutput)
(23:54)如果使用 VideoDataOutput 做自定义预览,不要通过 connection 旋转,而是旋转 CALayer:
// 不推荐:会导致帧传输中断
// videoDataOutput.connection?.videoRotationAngle = angle
// 推荐:旋转显示层
sampleBufferDisplayLayer.setAffineTransform(
CGAffineTransform(rotationAngle: angle * .pi / 180)
)
关键点:
- 通过
AVCaptureConnection旋转VideoDataOutput会导致帧传输中断 - 旋转
AVSampleBufferDisplayLayer更平滑 - 使用
AVAssetWriter录制时,通过transform属性设置旋转,而不是逐帧旋转
外接麦克风
(27:38)使用外接摄像头自带的麦克风:
// 查找当前使用的麦克风
let microphone = AVCaptureDevice.default(.microphone, for: .audio, position: .unspecified)
// 使用 AVAudioSession 更精细地控制音频路由
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playAndRecord, mode: .voiceChat)
try audioSession.setActive(true)
// 设置首选输入为外接麦克风的设备
if let preferredInput = audioSession.availableInputs?.first(where: { input in
input.portType == .usbAudio
}) {
try audioSession.setPreferredInput(preferredInput)
}
关键点:
- 新的
.microphone设备类型替代了已废弃的.builtInMicrophone - 系统会自动切换到最后连接的麦克风
localizedName会随当前使用的麦克风变化- 语音隔离(Voice Isolation)支持外接麦克风
相机切换按钮逻辑
(15:13)处理外接摄像头的切换逻辑:
func changeCamera() {
let currentPosition = currentDevice.position
switch currentPosition {
case .back:
// 从后置切换到外部摄像头(如果有),否则切换到前置
let externalSession = AVCaptureDevice.DiscoverySession(
deviceTypes: [.external],
mediaType: .video,
position: .unspecified
)
if let externalCamera = externalSession.devices.first {
AVCaptureDevice.userPreferredCamera = externalCamera
} else {
let frontSession = AVCaptureDevice.DiscoverySession(
deviceTypes: [.builtInWideAngleCamera],
mediaType: .video,
position: .front
)
if let frontCamera = frontSession.devices.first {
AVCaptureDevice.userPreferredCamera = frontCamera
}
}
case .unspecified, .front:
// 从前置/外部切换到后置
let backSession = AVCaptureDevice.DiscoverySession(
deviceTypes: [.builtInWideAngleCamera],
mediaType: .video,
position: .back
)
if let backCamera = backSession.devices.first {
AVCaptureDevice.userPreferredCamera = backCamera
}
default:
break
}
}
关键点:
- 外接摄像头位置为
.unspecified - 每次切换后设置
userPreferredCamera,让系统学习用户偏好 - 根据应用类型设计不同的切换策略
核心启发
1. 为视频通话应用添加外接摄像头自动切换
做什么:当用户把 iPad 接到带摄像头的显示器上时,视频通话应用自动切换到显示器的摄像头,获得更好的拍摄角度。
为什么值得做:Apple Studio Display 的摄像头位置比 iPad 前置摄像头更适合桌面视频通话。自动切换让用户无需手动操作。
怎么开始:监听 systemPreferredCamera 的 KVO 通知,当外接摄像头连接时自动切换。在通话中保持当前摄像头不变,通话结束后再切换。
2. 构建支持 HDMI 输入的直播应用
做什么:利用 iPadOS 对 UVC 设备的支持,让应用可以接收 HDMI 切换器的视频输入,用于直播或录制。
为什么值得做:HDMI 切换器符合 UVC 规范即可被识别为外接摄像头。这让 iPad 可以作为专业直播的监视器和录制设备。
怎么开始:使用 AVCaptureDevice.DiscoverySession 发现 .external 类型设备,配置 AVCaptureSession 接收视频流。注意 HDMI 输入不需要镜像预览。
3. 实现正确的多方向视频预览
做什么:使用 AVCaptureDeviceRotationCoordinator 替换旧的 AVCaptureVideoOrientation,确保外接摄像头在 iPad 旋转时预览始终正确。
为什么值得做:旧的旋转 API 假设摄像头和 iPad 一起旋转,这对外接摄像头完全错误。新 API 让画面始终相对于重力保持水平。
怎么开始:创建设备切换时同时创建新的 AVCaptureDeviceRotationCoordinator,监听 videoRotationAngleForHorizonLevelPreview 变化,应用到 preview layer 的 connection。
4. 为专业拍摄应用添加外接麦克风支持
做什么:在录制视频时,当用户连接带麦克风的外接摄像头或 USB 麦克风时,自动切换音频输入源。
为什么值得做:外接麦克风通常比内置麦克风音质更好,且支持语音隔离等高级功能。自动切换让用户专注于内容创作。
怎么开始:监听 AVAudioSession 的 route change 通知,当检测到新的 USB 音频输入时,使用 setPreferredInput 切换。同时更新 UI 显示当前使用的麦克风名称。
关联 Session
- Create a more responsive camera experience — iOS 17 相机响应性新 API:延迟处理、零快门延迟、响应式拍摄
- What’s new in the Photos picker — 系统照片选择器的新改进,支持嵌入和连续选择
- What’s new in Background Assets — 后台下载资源,减少用户等待
- Meet SwiftUI for spatial computing — SwiftUI 在空间计算平台上的全新能力
评论
GitHub Issues · utterances