WWDC Quick Look 💓 By SwiftGGTeam
Create camera extensions with Core Media IO

Create camera extensions with Core Media IO

观看原视频

Highlight

macOS 12.3 用基于 SystemExtensions 的 Camera Extensions 取代 DAL plug-ins,让第三方软件摄像头、硬件摄像头和创意摄像头以沙盒 daemon 形式被 FaceTime 等所有相机 App 使用。


核心内容

在 macOS 上做一个第三方摄像头,过去常用 DAL plug-in。它从 macOS 10.7 开始存在,可以驱动硬件摄像头,也可以发布虚拟摄像头。

问题在安全边界。DAL plug-in 会把第三方代码直接加载到 App 进程里。插件有 bug,App 可能崩溃;插件有恶意行为,App 也会暴露在攻击面里。因此 FaceTime、QuickTime Player、Photo Booth 不支持它,许多第三方相机 App 也只有在关闭 library validation 或系统完整性保护后才会加载它(00:56)。

Camera Extensions 是 macOS 12.3 引入的新架构。扩展代码运行在独立的沙盒 daemon 进程中,buffer 在交给 App 前会被验证,框架负责扩展进程和 App 之间的 IPC,也负责把 buffer 分发给多个客户端(01:50)。

对用户来说,它仍然像普通摄像头。Camera Extension 会通过现有 AVFoundation capture API 暴露出来,在相机 App 里显示得像内建摄像头,也可以出现在 FaceTime 的相机选择器里(03:36)。

Session 把用法分成三类。

第一类是纯软件摄像头。它可以生成 color bars、测试图案、不同帧率和分辨率的程序化画面,或播放预渲染内容来测试音视频同步(04:02)。

第二类是硬件摄像头驱动。它可以配合 DriverKit Extension 访问硬件,也可以在确有需要时走 legacy IOVideoFamily kext。对于非标准 USB Video Class 摄像头,扩展还可以覆盖 Apple 的默认 UVC extension,声明特定 vendor ID 和 product ID(04:30)。

第三类是创意摄像头。扩展读取另一台物理摄像头的画面,加滤镜、合成多路画面,再把结果发布成一个新的摄像头 stream。演示里的 CIFilterCam 就是这种模式:配置 App 选择源摄像头和 Core Image 滤镜,FaceTime 和 Photo Booth 同时看到更新后的虚拟摄像头画面(06:05)。

详细内容

1. 安装模型:扩展跟随 App 分发

Camera Extension 建在 SystemExtensions 框架之上。扩展可执行文件放在 App bundle 里。App 调用 SystemExtensions 框架来安装、升级、降级扩展;删除 App 时,SystemExtensions 也会为所有用户卸载这个 Camera Extension(07:29)。

这个模型省掉了单独的安装器,也可以通过 App Store 分发。

概念流程:安装按钮触发 system extension 激活请求
1. 找到 App bundle 内嵌的 Camera Extension
2. 创建用于 activate extension 的 OSSystemExtensionRequest
3. 设置 OSSystemExtensionRequestDelegate 记录安装状态
4. 把请求交给 SystemExtensions 框架执行

关键点:

  • OSSystemExtensionRequest 对应 Session 中安装和卸载按钮创建的激活/停用请求。
  • 请求目标是嵌入在 App bundle 里的 Camera Extension。
  • OSSystemExtensionRequestDelegate 用来接收安装状态,Session 的示例在 delegate 方法里记录结果。
  • 这里写的是概念流程;具体 identifier 和错误处理要按你的 target 配置填写。

开发时有一个容易踩的坑:system extension 只能由位于 /Applications 的 App 安装。Session 演示中直接运行 App 后安装失败,把 App 移到 /Applications 后才成功(12:57)。

Info.plist 里也有关键配置。Camera Extension 的 Info.plist 通过 CMIOExtensionMachServiceName 标识自己;CoreMedia IO 的 registerassistant 不会启动缺少这个键的扩展。扩展的 entitlements 需要启用 App Sandbox,并且 App Group 前缀要匹配 Mach service name,才能通过校验(11:01)。

概念配置清单:
- Info.plist: CMIOExtensionMachServiceName
- Info.plist: 系统扩展用途描述
- Entitlements: App Sandbox = YES
- Entitlements: App Group 前缀匹配 MachServiceName

关键点:

  • CMIOExtensionMachServiceName 是 registerassistant 查找并启动扩展的入口。
  • 用途描述会出现在系统扩展授权流程中。
  • App Sandbox 是扩展运行的前提。
  • App Group 命名不匹配会导致扩展验证失败。

2. Provider、Device、Stream:三个对象组成一台摄像头

CoreMedia IO Extension 的主要类是 Provider、Device 和 Stream。Provider 拥有 Device,Device 拥有 Stream,三者都可以有 properties(22:04)。

创建它们时,你分别提供 ProviderSourceDeviceSourceStreamSource。Source 对象负责协议实现,框架负责样板代码。

概念流程:Provider 是服务入口
1. 实现一个符合 CMIOExtensionProviderSource 的 source 对象
2. 用这个 source 创建 CMIOExtensionProvider
3. 调用 Provider 的 startService,并传入 provider instance
4. 在 provider 下按需发布 device 和 stream

关键点:

  • CMIOExtensionProviderSource 是你实现扩展行为的 source 协议之一。
  • CMIOExtensionProvider 是扩展的底层对象,负责管理设备发布。
  • startService 是启动服务的入口,Session 明确指出要把 provider 传给它。
  • 真正项目中还要创建 device source、stream source,并把 stream 加到 device 上。

Provider 可以按需添加和移除设备,例如处理热插拔事件。它也会收到客户端进程连接通知,你可以借此决定是否向某些 App 发布设备(22:35)。

Device 负责管理 stream。它有本地化名称、UUID 形式的 device ID,以及可选的 legacy ID。localizedName 会一路传到 AVFoundation,成为 AVCaptureDevice.localizedName;device ID 会成为 AVCaptureDevice.uniqueIdentifier。如果你在迁移旧 DAL plug-in,并且要维持已经发布过的 identifier,可以提供 legacy device ID(23:40)。

概念配置:Device 层需要稳定的名称和标识
- localizedName: 用户在相机选择器中看到的名称
- deviceID: UUID 形式的设备标识
- legacyDeviceID: 迁移 DAL plug-in 时保留旧 uniqueIdentifier

关键点:

  • localizedName 会影响用户在相机选择器中看到的名称。
  • deviceID 是新架构下的稳定设备标识。
  • legacyDeviceID 只在迁移旧 DAL plug-in 且需要保持兼容时使用。
  • AVFoundation 只处理第一个 input stream,多个输入 stream 不会都被它消费。

Stream 是真正发送 sample buffer 的对象。它发布 CMIOExtensionStreamFormat,定义有效帧率,配置 active format,使用标准 clock 或自定义 clock 来驱动 buffer timing,并把 sample buffer 送给客户端(25:44)。

概念配置:Stream 层描述格式、帧率和时间
- stream formats: 发布给客户端的可用视频格式
- active format index: 当前使用的视频格式
- frame duration: 最大帧率
- max frame duration: 最小帧率
- clock: 驱动每个 sample buffer 的时间来源

关键点:

  • CMIOExtensionStreamFormat 会映射成 AVCaptureDeviceFormat
  • 客户端可以读写 active format index 来切换当前格式。
  • frame duration 对应最大帧率,max frame duration 对应最小帧率。
  • sample buffer 的时间由标准 clock 或自定义 clock 驱动。

3. 创意摄像头:配置 App 通过 property 控制扩展

CIFilterCam 演示展示了创意摄像头的交互方式。配置 App 里有源摄像头选择、滤镜选择、bypass 按钮和实时预览。预览视图由 AVCaptureVideoPreviewLayer 支撑(15:02)。

真正有用的部分在扩展。FaceTime 和 Photo Booth 都选择 CIFilterCam 后,配置 App 改变滤镜,两个 App 同时看到新效果;配置 App 改变源摄像头,所有使用 CIFilterCam 的 App 都跟着变化(16:29)。

概念流程:配置 App 控制创意摄像头
1. 用户在配置 App 中选择源摄像头
2. 用户选择 Core Image 滤镜
3. 配置 App 通过 CoreMedia IO C property interface 写入扩展属性
4. 扩展用新属性处理后续 buffer
5. 所有正在使用 CIFilterCam 的 App 同步看到变化

关键点:

  • 源摄像头选择对应演示里“use this camera”的配置变化。
  • 滤镜选择对应演示里“use this other filter”的配置变化。
  • 这是概念流程;Session 没有提供可直接复制的 property 写入代码。
  • Session 明确说明 AVFoundation 不支持自定义 properties,配置 App 要走 CoreMedia IO C API。

CMIOExtension 没有提供 DAL controls 的替代物。自动曝光、亮度、锐化、平移缩放等能力都应建模成 property。自定义 property 可以桥接到旧的 CoreMedia IO C property interface:名称以 4cc_ 开头,后面把 selector、scope、element 编码进去,中间用下划线分隔(26:46)。

概念示例:自定义 property 命名
4cc_cust_glob_0000

关键点:

  • 4cc_ 表示这是桥接到旧 C property address 的自定义名称。
  • cust 是四字符 selector,Session 用它举例表示 custom。
  • glob 表示 global scope;scope 也可以是 input 或 output。
  • 0000 表示 element;main element 通常是 0。

如果创意摄像头要在扩展里访问另一台硬件摄像头,需要 macOS Ventura。扩展 entitlements 里要加入 com.apple.security.device.camera,Info.plist 里要提供 NSCameraUsageDescription,用户也会收到授权提示(17:31)。

4. 安全边界:daemon、registerassistantservice 和 TCC

Camera Extension 的扩展代码不会进入 App 进程。Apple 的 registerassistantservice 会通过 CMIOExtensionMachServiceName 识别扩展,并以 _cmiodalassistants 这个 role user 启动它。sandboxd 会套用为摄像头场景定制的 sandbox profile(19:25)。

这个 sandbox profile 允许常见硬件通信:USB、Bluetooth、Wi-Fi 客户端模式、FireWire,也允许读写扩展自己的 container 和 tmp。它比普通 App 更严格:不能 fork、exec、posix spawn 子进程,不能访问 window server,不能连接前台用户账号,也不能在全局命名空间注册自己的 Mach services(19:59)。

Camera Extension 运行边界:
App process
  -> CoreMedia IO / AVFoundation compatibility layers
  -> registerassistantservice
  -> sandboxed camera extension daemon

关键点:

  • App 仍通过 AVFoundation 或 CoreMedia IO 看见摄像头。
  • registerassistantservice 是 App 和扩展之间的代理服务。
  • 扩展 daemon 被沙盒隔离,不能把第三方代码加载进 App。
  • buffer 在交给 App 前会经过验证和权限控制。

registerassistantservice 还负责透明度、同意和控制策略。某个 App 第一次使用摄像头时,系统会询问用户;这个 consent 对内建摄像头和第三方摄像头都适用。用户拒绝授权时,代理服务会阻止 buffer 传给该 App。它还负责 attribution,让系统知道某个 App 正在使用某个摄像头,从而把 daemon 消耗的电量归因到使用它的 App(21:04)。

5. Output device:CoreMedia IO 也能消费视频

DAL plug-in 还有一个较少被提到的能力:发布 output device。它和摄像头相反,不向 App 提供画面,而是实时消费 App 送来的画面。典型场景是 print-to-tape,或者把实时预览送到带 SDI 输入的专业 deck(28:44)。

Output device 没有 AVFoundation 等价 API。要向 output device 送帧,客户端必须直接使用 CoreMedia IO C API(29:25)。

概念流程:sink stream 消费视频
1. stream direction 设置为 sink
2. 客户端把 sample buffer 放入简单队列
3. 扩展通过 consumeSampleBuffer 消费 buffer
4. 消费完成后调用 notifyScheduledOutputChanged 通知客户端

关键点:

  • source stream 像摄像头一样产生数据。
  • sink stream 像输出设备一样消费数据。
  • consumeSampleBuffer 是 Session 提到的扩展侧消费调用。
  • notifyScheduledOutputChanged 用来通知客户端队列状态变化。

Session 最后给出迁移信号:macOS 12.3 已经把 DAL plug-ins 标记为 deprecated,构建时会出现 warning。Apple 计划在 macOS Ventura 之后的下一个 major release 中完全禁用 DAL plug-ins。仍在维护 DAL plug-in 的团队,需要开始迁移到 Camera Extension(30:28)。

核心启发

  • 做什么:做一个会议专用虚拟摄像头。 为什么值得做:Camera Extension 可以出现在 FaceTime 等所有相机 App 中,配置 App 改一次滤镜,所有客户端都看到同一结果。 怎么开始:用软件摄像头模板建立 Provider、Device、Stream,再用 Core Image 处理源摄像头 buffer;配置 App 通过 CoreMedia IO 自定义 property 写入滤镜名称。

  • 做什么:为非标准 USB 摄像头做 macOS 驱动。 为什么值得做:Apple 的 UVC extension 适合标准 UVC 设备;非标准协议或 UVC 之外的额外功能可以用 Camera Extension 覆盖默认 UVC extension。 怎么开始:阅读 “Overriding the default USB video class extension”,在最小 DEXT bundle 的 Info.plist 里声明对应 vendor ID 和 product ID。

  • 做什么:做一台自动播放测试图案的软件摄像头。 为什么值得做:纯软件摄像头可以生成固定图案、不同帧率和分辨率,用于验证视频会议、录制或音视频同步流程。 怎么开始:从 Xcode 的 Camera Extension target 模板开始,保留软件 camera stream,替换 sample buffer 生成逻辑。

  • 做什么:把旧 DAL plug-in 迁移成 Camera Extension。 为什么值得做:DAL plug-ins 已被 deprecated,并计划在 Ventura 之后的 major release 中禁用;Camera Extension 还能被 Apple App 使用。 怎么开始:复用旧设备的 unique identifier 作为 legacy device ID,逐步把 DAL controls 映射成 provider、device 或 stream properties。

关联 Session

评论

GitHub Issues · utterances