WWDC Quick Look 💓 By SwiftGGTeam
Support external cameras in your iPadOS app

Support external cameras in your iPadOS app

观看原视频

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:55AVCaptureVideoOrientation 在 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

评论

GitHub Issues · utterances