WWDC Quick Look 💓 By SwiftGGTeam
Display HDR video in EDR with AVFoundation and Metal

Display HDR video in EDR with AVFoundation and Metal

观看原视频

Highlight

Apple 在 iOS 上开放了 EDR API,开发者可以通过 AVFoundation 和 Metal 构建完整的 HDR 视频播放与实时处理管线,从简单的 AVPlayer 播放到逐帧提取、Core Image 滤镜处理、Metal 着色器渲染均可实现。

核心内容

选择合适的视频框架层级

播放 HDR 视频时,开发者面临一个选择:用高层框架快速实现,还是用底层框架获得更大控制权。Apple 的视频框架从高层到低层依次是 AVKit、AVFoundation、Core Video、Video Toolbox 和 Core Media。原则是能用高层就不用低层,因为高层框架自带优化。

AVKit 提供完整的播放 UI,包括进度条、章节导航、画中画、字幕。如果只需要播放 HDR 视频,AVPlayerViewController 一行代码就能搞定 EDR 渲染。

但许多应用需要实时处理视频帧,比如调色、抠像、叠加特效。这时就要往下走,用 AVFoundation 提取解码后的帧,再交给 Core Image 或 Metal 处理。

简单播放:AVPlayer 的两种方式

最基础的 HDR 播放有两种方案。

第一种用 AVPlayerViewController,适合需要完整播放控件的场景。(06:58

let player = AVPlayer(url: videoURL)
var playerViewController = AVPlayerViewController()
playerViewController.player = player
self.present(playerViewController, animated: true) {
    playerViewController.player!.play()
}

关键点:

  • AVPlayerViewController 自动处理所有播放 UI 和 EDR 渲染
  • 适合不需要自定义处理的纯播放场景

第二种用 AVPlayerLayer,适合把视频嵌入自定义视图。(07:38

let player = AVPlayer(url: videoURL)
var playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = self.view.bounds
self.view.layer.addSublayer(playerLayer)
player.play()

关键点:

  • AVPlayerLayer 将视频渲染到自定义 CALayer
  • 同样自动支持 EDR,无需额外配置
  • 适合需要将视频作为视图一部分的场景

实时处理:从解码帧到 Metal 渲染

当需要在播放时添加特效,流程变成:AVPlayer 解码 -> CADisplayLink 提取帧 -> Core Image 或 Metal 处理 -> CAMetalLayer 渲染。

这个流程的关键是正确配置 CAMetalLayer。必须启用 EDR、使用半浮点像素格式、设置扩展色域。(09:28

然后创建 AVPlayerItemVideoOutput,指定输出格式和色彩空间。AVFoundation 会自动处理不同片段之间的色彩转换。(11:33

CADisplayLink 每帧回调时,从 VideoOutput 复制最新的 CVPixelBuffer。注意屏幕刷新率可能高于视频帧率,所以 copyPixelBuffer 不一定每次都成功,失败时保持上一帧即可。(13:02

详细内容

配置 EDR 渲染层

let layer: CAMetalLayer

// 启用 EDR
layer.wantsExtendedDynamicRangeContent = true

// 使用半浮点像素格式
layer.pixelFormat = MTLPixelFormatRGBA16Float

// 使用扩展线性 Display P3 色域
layer.colorspace = kCGColorSpaceExtendedLinearDisplayP3

关键点:

  • wantsExtendedDynamicRangeContent 必须设为 true,否则 HDR 内容会被压缩到 SDR
  • MTLPixelFormatRGBA16Float 是半浮点格式,能存储 EDR 所需的扩展亮度值
  • kCGColorSpaceExtendedLinearDisplayP3 是 Apple 设备最常用的广色域,线性传递函数保留扩展范围值

创建视频输出与显示链接

let videoColorProperties = [
    AVVideoColorPrimariesKey: AVVideoColorPrimaries_P3_D65,
    AVVideoTransferFunctionKey: AVVideoTransferFunction_Linear,
    AVVideoYCbCrMatrixKey: AVVideoYCbCrMatrix_ITU_R_2020
]

let outputVideoSettings = [
    AVVideoAllowWideColorKey: true,
    AVVideoColorPropertiesKey: videoColorProperties,
    kCVPixelBufferPixelFormatTypeKey as String: NSNumber(value: kCVPixelFormatType_64RGBAHalf)
] as [String : Any]

let videoPlayerItemOutput = AVPlayerItemVideoOutput(outputSettings: outputVideoSettings)

关键点:

  • AVVideoColorPrimaries_P3_D65 指定 Display P3 色域
  • AVVideoTransferFunction_Linear 使用线性传递函数,这是 EDR 的关键,避免亮度值被压缩
  • kCVPixelFormatType_64RGBAHalf 是 64 位 RGBA 半浮点格式
  • AVPlayerItemVideoOutput 会自动处理色彩转换,即使视频包含多个不同色域的片段
lazy var displayLink: CADisplayLink = CADisplayLink(
    target: self,
    selector: #selector(displayLinkCopyPixelBuffers(link:))
)

var statusObserver: NSKeyValueObservation?

statusObserver = videoPlayerItem.observe(\.status, options: [.new, .old]) { playerItem, change in
    if playerItem.status == .readyToPlay {
        playerItem.add(videoPlayerItemOutput)
        displayLink.add(to: .main, forMode: .common)
        videoPlayer?.play()
    }
}

关键点:

  • 观察 status 属性,等播放器准备好后再添加输出和启动显示链接
  • CADisplayLink 与主运行循环的 common mode 关联,确保滚动时也能回调
  • macOS 上应使用 CVDisplayLink 替代 CADisplayLink

逐帧处理:Core Image 滤镜

@objc func displayLinkCopyPixelBuffers(link: CADisplayLink) {
    let currentTime = videoPlayerItemOutput.itemTime(forHostTime: CACurrentMediaTime())

    if videoPlayerItemOutput.hasNewPixelBuffer(forItemTime: currentTime) {
        if let buffer = videoPlayerItemOutput.copyPixelBuffer(
            forItemTime: currentTime,
            itemTimeForDisplay: nil
        ) {
            let image = CIImage(cvPixelBuffer: buffer)

            let filter = CIFilter.sepiaTone()
            filter.inputImage = image
            let output = filter.outputImage ?? CIImage.empty()

            // 使用 context 渲染到 CIRenderDestination
        }
    }
}

关键点:

  • itemTime(forHostTime:) 将主机时间映射到视频时间线
  • hasNewPixelBuffer 检查是否有新帧,避免不必要的复制
  • copyPixelBuffer 可能失败(视频帧率低于屏幕刷新率),失败时跳过渲染
  • CIImage(cvPixelBuffer:) 直接从像素缓冲区创建图像
  • 注意:不是所有 CIFilter 都支持 EDR,应使用 CICategoryHighDynamicRange 筛选兼容滤镜

逐帧处理:Metal 纹理

let mtlDevice = MTLCreateSystemDefaultDevice()

var mtlTextureCache: CVMetalTextureCache? = nil
CVMetalTextureCacheCreate(
    allocator: kCFAllocatorDefault,
    cacheAttributes: nil,
    metalDevice: mtlDevice,
    textureAttributes: nil,
    cacheOut: &mtlTextureCache
)

let width = CVPixelBufferGetWidth(pixelBuffer)
let height = CVPixelBufferGetHeight(pixelBuffer)

var cvTexture: CVMetalTexture? = nil
CVMetalTextureCacheCreateTextureFromImage(
    allocator: kCFAllocatorDefault,
    textureCache: mtlTextureCache,
    sourceImage: pixelBuffer,
    textureAttributes: nil,
    pixelFormat: MTLPixelFormatRGBA16Float,
    width: width,
    height: height,
    planeIndex: 0,
    textureOut: &cvTexture
)

let texture = CVMetalTextureGetTexture(cvTexture)

关键点:

  • CVMetalTextureCache 是连接 Core Video 和 Metal 的桥梁,避免手动管理 IOSurface
  • 缓存会保持 MTLTexture 到 IOSurface 的映射,提升性能
  • 使用半浮点像素格式是因为 Metal 纹理原生支持这种格式
  • Objective-C 中需要在 Metal 命令缓冲区完成回调中释放 CVMetalTextureRef

核心启发

  • 实时视频调色工具:利用 AVPlayerItemVideoOutput + Core Image 滤镜链,做一个支持 HDR 的实时调色面板。用户可以滑动滑块调整色温、饱和度,滤镜直接作用于视频帧。入口 API 是 AVPlayerItemVideoOutputCIFilter

  • HDR 视频抠像合成:用 Metal 着色器实现色度键(Chroma Key)抠像,把绿幕视频与背景图层实时合成。EDR 保证合成后的 HDR 内容不会丢失亮度信息。入口 API 是 CVMetalTextureCache 和自定义 Metal 片段着色器。

  • 专业视频播放器的自定义渲染引擎:放弃 AVPlayerViewController,用 CADisplayLink + Metal 构建自己的播放渲染管线,支持逐帧精确控制和特效叠加。适合剪辑软件或专业播放器。入口 API 是 CADisplayLinkCAMetalLayer

  • HDR 截图与帧导出:在 CADisplayLink 回调中,把处理后的 CVPixelBuffer 保存为 HEIF/PNG,保留 HDR 元数据。可以做成视频逐帧导出工具。入口 API 是 CVPixelBufferCIContext.writeHEIFRepresentation

关联 Session

评论

GitHub Issues · utterances