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 内容会被压缩到 SDRMTLPixelFormatRGBA16Float是半浮点格式,能存储 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 是
AVPlayerItemVideoOutput和CIFilter。 -
HDR 视频抠像合成:用 Metal 着色器实现色度键(Chroma Key)抠像,把绿幕视频与背景图层实时合成。EDR 保证合成后的 HDR 内容不会丢失亮度信息。入口 API 是
CVMetalTextureCache和自定义 Metal 片段着色器。 -
专业视频播放器的自定义渲染引擎:放弃 AVPlayerViewController,用 CADisplayLink + Metal 构建自己的播放渲染管线,支持逐帧精确控制和特效叠加。适合剪辑软件或专业播放器。入口 API 是
CADisplayLink和CAMetalLayer。 -
HDR 截图与帧导出:在 CADisplayLink 回调中,把处理后的 CVPixelBuffer 保存为 HEIF/PNG,保留 HDR 元数据。可以做成视频逐帧导出工具。入口 API 是
CVPixelBuffer和CIContext.writeHEIFRepresentation。
关联 Session
- Explore EDR on iOS — EDR API 首次支持 iOS 平台
- Display EDR content with Core Image, Metal, and SwiftUI — 用 SwiftUI 展示 EDR 内容
- Edit and play back HDR video with AVFoundation — 使用 AVVideoComposition 处理 HDR 视频
- Optimize the Core Image pipeline for your video app — Core Image 视频处理管线优化
评论
GitHub Issues · utterances