大多数浏览器和
Developer App 均支持流媒体播放。
-
利用 AVFoundation 和 Metal 在 EDR 中显示 HDR 视频
了解如何利用 AVFoundation 和 Metal 来构建高效的 EDR 管道。跟着我们一起学习如何使用 AVPlayer 显示具有 EDR 的 HDR 视频,在 App 视图中添加播放功能,使用 Metal 进行渲染,以及使用 Core Image 或自定义 Metal 着色器添加视频效果,如抠像或颜色管理等。无论您在开发游戏还是专业 App,我们都将帮助您确定要使用哪些框架,分享选择传输、颜色空间和像素缓冲区格式的最佳实践。
资源
相关视频
WWDC23
WWDC22
Tech Talks
WWDC21
WWDC20
-
下载
♪ ♪
Ken Greenebaum: 大家好 欢迎来到 WWDC 2022 我是 Ken Greenebaum 是 Apple 的 显示和色彩技术团队的成员 很高兴今年能举行三次 EDR 演讲 希望您有机会观看 “探索 iOS 上的 EDR” 我们在里面宣布了 对 iOS 的 EDR API 支持 还有 “借助 Core Image、Metal 和 SwiftUI 显示 EDR 内容” 一些开发者可能 也看过我去年的 EDR 演讲 我们在里面演示了 如何通过使用 EDR 来使用 AVPlayer 播放 HDR 视频
在这次演讲中 我们将深入探讨 如何使用 Core Media 接口来实现 不只是 EDR 播放 而且还有如何解码和播放 HDR 视频 使其进入您的 EDR 层或视图中
然后 我们将继续讨论如何通过 Core Video 的显示链接实时访问 解码的视频帧 将这些帧发送到 CoreImage Filter 或 Metal Shader 以添加颜色管理 视觉效果 或应用其他信号处理 最后 将生成的帧 发送到 Metal 进行渲染 首先 我们回顾一下 与 EDR 兼容的视频媒体框架 帮助您决定哪种最符合您的 App 要求
接下来 我们将简要讨论高级 AVKit 和 AVFoundation framework 如果您的 App 需要直接播放 这样可以完成 播放 HDR 视频的所有工作
最后 我们将讨论 在 EDR 播放 编辑或图像处理引擎中 利用 Core Video 和 Metal 使用解码视频帧的最佳实践
首先 我们快速了解一下 Apple 的视频框架 从最高级别的接口开始 这些是最容易使用的 并继续使用提供更多机会的底部框架 但代价是增加代码的复杂性 最好使用最高级别的框架 以充分利用自动提供的优化 这为我们进入演讲的主体内容 做了准备 我们将探索多种场景 从简单的 EDR 播放 到更复杂的解码视频帧管道 再到 CoreImage 或 Metal 的实时处理 最高级别 是 AVKit 使用 AVKit 您可以创建 用于媒体播放的用户界面 完成传输控件 章节导航 画中画功能支持 以及字幕和隐藏式字幕的显示 AVKit 可以将 HDR 内容 作为 EDR 播放 我们将使用 AVPlayerViewController 进行演示 但如果您的 App 需要 进一步处理视频帧 则必须使用媒体框架 这样才能使您更好地控制管道 接下来是 AVFoundation AVFoundation 是一个 功能全面的框架 用于在 Apple 平台上 处理基于时间的视听媒体 使用 AVFoundation 您可以轻松播放 创建 和编辑 QuickTime 影片 和 MPEG 4 文件 播放 HLS 流 并在您的 App 中 构建强大的媒体功能 在本次演讲中 我们将探讨 AVPlayer 和相关的 AVPlayerLayer 接口的使用 Core Video 是一个为 数字视频提供管道模型的框架 它通过将流程划分为多个独立的步骤 简化处理视频的方式 Core Video 还使您可以 更轻松地访问和操作 单个帧 而不必担心 数据类型之间的转换 或显示同步 我们将用 Core Image 来演示 DisplayLink 和使用 Core Image 的 CVPixelBuffer 以及使用 Metal 的 CVMetalTextureCache 接下来是 Video Toolbox 这是一个底层框架 可以直接访问 硬件编码器和解码器 Video Toolbox 提供 视频压缩和解压缩服务 以及存储在 Core Video 像素缓冲区的 光栅图像格式之间的转换服务 VTDecompressionSession 是一个功能强大的底层接口 超出了本次演讲的范围 但高级开发者可能想要进一步研究 最后是 Core Media 该框架定义了 AVFoundation 和其他高级媒体框架 使用的媒体管道 您可以始终使用 Core Media 的 底层数据类型和接口 来有效地处理媒体样本 和管理媒体数据队列 在接下来的演讲中 我们将演示如何以及何时 在您的 App 中使用这些框架 首先 如何使用 AVKit 和 AVFoundation 来轻松播放渲染为 EDR 的 HDR 视频 然后是 AVPlayer 的一系列 更复杂的 App 渲染到您自己的层 通过 CADisplayLink 访问单独解码的帧 并将生成的 CVPixelBuffer 发送到 Core Image 进行处理 最后 通过 CVMetalTextureCache 以 Metal 纹理方式 访问解码帧 以便在 Metal 中进行处理和渲染 现在我们已经对 Apple 平台上的 视频媒体层有了大致的了解 我们将重点介绍 AVKit 和 AVFoundation framework 首先 我们来讨论一下 使用 AVFoundation 的 AVPlayer 接口 播放 HDR 视频内容 AVPlayer 是一个控制器对象 用于管理媒体资源的播放和计时 AVPlayer 接口可用于 高性能 HDR 视频播放 在可能的情况下 自动将结果渲染为 EDR 使用 AVPlayer 您可以播放 本地和远程基于文件的媒体 如 QuickTime 影片 以及使用 HLS 提供的流媒体 本质上 AVPlayer 用于 一次播放一个媒体资源 您可以重用播放器实例 来连续播放其他媒体资源 甚至可以创建多个实例 来同时播放一个以上的资源 但 AVPlayer 一次只管理 单个媒体资源的播放 AVFoundation framework 还提供了一个名为 AVQueuePlayer 的 AVPlayer 子类 您可以使用该子类 创建和管理 连续 HDR 媒体资源的排队和播放 如果您的 App 需要简单地播放 渲染到 EDR 的 HDR 视频媒体 那么带有 AVPlayerViewController 的 AVPlayer 可能是最好的方法 使用 AVPlayer 和 AVPlayerLayer 在 iOS 或 macOS 上 播放您自己的视图
这些是使用 AVPlayer 最简单的方法 我们来看看这两者的示例 首先 我们将介绍如何将 AVFoundation 的 AVPlayer 接口 与 AVKit 的 AVPlayerViewController 结合使用 这里 我们从媒体的 URL 实例化 AVPlayer 开始
接下来 我们创建一个 AVPlayerViewController 然后将我们的查看器控制器的 播放器属性设置为 我们刚刚从媒体的 URL 创建的播放器
并以模式呈现视图控制器 以开始播放视频 AVKit 为您管理所有细节 并将在支持 EDR 的显示器上 自动播放 HDR 视频作为 EDR 正如我提到的 一些 App 需要在 自己的视图中 播放 HDR 视频媒体 让我们看看如何使用 AVPlayer 和 AVPlayerLayer 来实现这一点 为了在您自己的视图中 以 EDR 的形式播放 HDR 视频媒体 我们再次开始使用媒体的 URL 创建一个 AVPlayer 然而 这次我们用刚刚创建的播放器 实例化了一个 AVPlayerLayer 接下来 我们需要在播放器层上 设置边界 这是我们从视图中获得的 既然播放器层有了视图的边界 我们可以将播放器层 作为子层添加到视图中 最后 为了播放 HDR 视频媒体 我们调用了 AVPlayer 的播放方法 那就是使用 AVPlayer 和 AVPlayerLayer 在您自己的层中将 HDR 视频媒体 作为 EDR 播放所需的全部内容 我们刚刚使用 AVPlayer 探索了 两种最简单的 HDR 视频播放工作流程 然而 许多 App 需要的 不仅仅是简单的媒体播放
例如 App 可能需要对视频 进行图像处理 如颜色分级或色度键控 我们来探索一个从 AVPlayer 获取解码视频帧的工作流程 应用 CoreImage filter 或 Metal shader 实时处理 并将结果渲染为 EDR 我们将演示如何使用 AVPlayer 和 AVPlayerItem 从您的 HDR 视频媒体解码 EDR 帧 从 Core Video 显示链接访问解码帧 将生成的像素缓冲区发送到 Core Image 或 Metal 进行处理 然后在具有 EDR 支持的显示器上 以 EDR 的形式在 CAMetalLayer 中渲染结果 考虑到这一点 我们将首先演示 在 CAMetalLayer 上 设置几个关键属性 这些属性是确保 HDR 媒体 正确渲染为 EDR 所必需的 首先 我们需要获得 CAMetalLayer 然后将 HDR 视频内容渲染到它上面 在该层上 我们通过将 wantsExtendedDynamicRangeContent 标志设置为真来选择 EDR
请确保使用支持 Extended Dynamic Range 内容的像素格式 对于下面的 AVPlayer 示例 我们将把 CAMetalLayer 设置为 使用半浮点像素格式 但是结合 PQ 或 HLG 传递函数 使用的 10 位格式也可以 为了避免将结果限制为 SDR 我们还需要将层设置为 EDR 兼容的扩展范围色彩空间
在我们的示例中 我们将半浮动 Metal 纹理设置为 扩展的线性 Display P3 色彩空间 我们只是初步了解了 EDR 色彩空间和像素缓冲区格式 您可能想查看我去年的讲座 “HDR rendering with EDR” 以及今年的 “探索 iOS 上的 EDR” 以了解更多信息
现在 我们已经设置了 CAMetalLayer 的基本属性 我们还要继续演示 使用 Core Image 或 Metal shader 添加实时图像处理 我们将结合 AVPlayer 使用显示链接 来实时访问解码的视频帧
对于此工作流程 首先 从 AVPlayerItem 创建 AVPlayer 接下来 实例化 AVPlayerItemVideoOutput 为 EDR 配置适当的 像素缓冲区格式和色彩空间 然后创建并配置 Display Link 最后 运行 Display Link 将像素缓冲区发送到 Core Image 或 Metal 进行处理 我们将演示在 iOS 上 使用的 CADisplayLink 在为 macOS 开发时 请使用等效的 CVDisplayLink 这一次 我们选择从媒体的 URL 创建一个 AVPlayerItem 并用我们刚刚创建的 AVPlayerItem 实例化 AVPlayer 现在我们创建一对字典 来指定解码帧的 色彩空间和像素缓冲区格式 第一个字典 videoColorProperties 是指定色彩空间和传递函数的地方 在本例中 我们要求的是 Display P3 色彩空间 该色彩空间对应于大多数 Apple 显示器的色彩空间 以及线性传递函数 该传递函数允许 AVFoundation 保持 EDR 所需的扩展范围值
第二个字典 outputVideoSettings 指定了像素缓冲区格式的特征 并为我们刚刚创建的 视频色彩属性字典 提供了参考 在本例中 我们请求广色域 和半浮点像素缓冲区格式 AVPlayerItemVideoOutput 非常有用 它不仅可以将视频解码为 我们在输出设置字典中 指定的像素缓冲区格式 还可以通过 像素传输会话自动执行所需的 任何色彩转换 回想一下 一个视频可能包含多个剪辑 可能具有不同的色彩空间 AVFoundation 会自动 为我们管理这些 正如我们即将演示的那样 这种行为还允许将解码后的视频帧 发送到 Metal 等底层框架 而这些框架本身并不会 自动将色彩空间转换为 显示器的色彩空间 现在 我们使用 outputVideoSettings 字典 创建 AVPlayerItemVideoOutput 第三步 我们设置 Display Link 用于实时访问解码的帧 CADisplayLink 在每次 显示更新时执行回调 在我们的例子中 我们调用了一个局部函数 来获取 CVPixelBuffer 我们将把它发送给 CoreImage 进行处理 接下来 我们创建一个 视频播放器项目观察器 以允许我们处理 对指定 Player Item 属性的更改
我们的示例将在 每次 Player Item 的状态更改时 执行此代码
当播放器项目的状态更改为 readyToPlay 我们将我们的 AVPlayerItemVideoOutput 添加到刚刚返回的 新 AVPlayerItem 中 注册 CADisplayLink 将主运行循环设置为普通模式 并通过调用视频播放器上的播放 来启动 HDR 视频的实时解码
最后 我们来看看 CADisplayLink 回调实现的例子 我们之前称之为 displayLinkCopyPixelBuffer 的局部函数 HDR 视频开始播放后 每次刷新显示时都会调用 CADisplayLink 回调函数 例如 对于典型的显示 它可能每秒被调用 60次 如果有一个新的 CVPixelBuffer 我们的代码 就有机会更新显示的帧 在每次显示回调时 我们尝试复制一个 CVPixelBuffer 其中包含要在当前挂钟时间显示的 解码视频帧 然而 复制像素缓冲区调用可能会失败 因为在每次显示器刷新时 并不总是有新的 CVPixelBuffer 可用 尤其是当屏幕刷新率超过 正在播放的视频的刷新率时 如果没有新的 CVPixelBuffer 则调用失败 我们跳过渲染 这会使前一帧保持在屏幕上 以进行另一次显示刷新 但如果复制成功 那么我们在 CVPixelBuffer 中 就有一个新的视频帧 我们可以通过多种方式 处理和渲染这个新的帧 一个方法是将 CVPixelBuffer 发送到 Core Image 进行处理 Core Image 可以将一个或多个 CIFilter 串在一起 以向视频帧提供 GPU 加速的图像处理
请注意 并非所有 CIFilter 都与 EDR 兼容 并且可能在处理 HDR 内容时 遇到问题 包括钳位到 SDR 或更糟 Core Image 提供了许多 EDR 兼容的滤镜 通过 CICategoryHighDynamicRange 调用 filterNames 来枚举 与 EDR 兼容的 CoreImage 滤镜 在我们的示例中 我们将添加一个 简单的色调效果 现在 我们回到例子中 并集成 Core Image 在每个生成新 CVPixelBuffer 的 Display Link 回调上 从该像素缓冲区创建 CIImage
实例化 CIFilter 以实现所需的效果 我之所以使用色调滤镜 是因为它没有参数 非常简单 但系统中内置了许多 CIFilter 您也可以直接编写自己的滤镜 将 CIFilter 的输入图像设置为 我们刚刚创建的 CIImage
处理后的视频结果将出现在 滤镜的输出图像中 根据需要将尽可能多的 CIFilter 连接在一起 以实现您想要的效果 然后使用 CIRenderDestination 将结果图像渲染到 App 的视图代码中
请参考 WWDC 2020 的演讲 “Applying CI Effects to Video” 进一步了解有关此工作流程的信息 另一个方法是使用 Metal 和自定义 Metal shader 来处理和渲染新的 CVPixelBuffer 我们将简要描述 将 CVPixelBuffer 转换为 Metal 纹理的过程 然而 实现这种转换并保持最佳性能 是一个有深度的话题 最好留待下次讨论 我们建议从 CoreVideo 的 Metal 纹理缓存中 导出 Metal 纹理 并将此过程作为 本次演讲的最后一个示例 一般而言 该过程是从 CVPixelBuffer 中获取 IOSurface 创建一个 MetalTextureDescriptor 然后使用 newTextureWithDescriptor 从该 Metal 设备 创建一个 Metal 纹理
但如果没有小心处理锁 纹理可能会被 重复使用和过度绘制 此外 并不是所有的像素缓冲区格式 都被 Metal 纹理支持 这就是为什么我们在本例中 使用半浮点 由于这些复杂性 我们建议直接 从 CoreVideo 获取 Metal 纹理 正如我们现在将要演示的 我们来进一步探索 Core Video 和 Metal 如前所述 CVMetalTextureCache 是使用 CVPixelBuffer 和 Metal 的 一种简单而有效的方法 CVMetalTextureCache 很方便 因为您可以直接 从缓存中获得 Metal 纹理 而不需要进一步的转换 CVMetalTextureCache 自动在 CVPixelBuffer 和 Metal 纹理 之间建立桥梁 从而简化代码并保持快速运行 与 CVPixelBufferPool 结合使用时 CVMetalTextureCache 还通过保持 Metal 纹理到 IOSurface 的实时映射 来提供性能优势
最后 使用 CVMetalTextureCache 消除了手动跟踪 IOSurface 的需要 现在是我们演讲的最后一个例子 如何使用 CVMetalTextureCache 直接从 Core Video 中 提取 Metal 纹理 这里 我们从获取 系统默认 Metal 设备开始 我们用它来创建一个 Metal 纹理缓存 然后实例化一个 与 Metal 纹理缓存关联的 Core Video Metal 纹理缓存 然后可以使用它来访问 解码的视频帧作为 Metal 纹理 可以方便地直接在我们的 Metal 引擎中使用 在本例中 我们创建并使用 Metal 系统默认设备 接下来 我们使用 CVMetalTextureCacheCreate 创建 CVMetalTextureCache 指定我们刚刚创建的 Metal 设备 我们得到了创建 Core Video Metal 纹理 所需的 CVPixelBuffer 的 高度和宽度 然后 我们调用 CVMetalTextureCacheCreateTextureFromImage 来实例化一个 CVMetalTexture 对象 并将其与 CVPixelBuffer 相关联 最后 我们调用 CVMetalTextureGetTexture 以获得所需的 Metal 纹理 Swift App 应使用 CVMetalTexture 的强引用 但是 当使用对象 C 时 您必须确保 Metal 在您释放 CVMetalTextureRef. 之前已经完成了您的纹理 这可以使用 metal 命令缓冲区完成处理程序来完成
就是这些了 各位 回顾一下 我们探讨了一些 将 HDR 视频媒体渲染到 EDR 的工作流程 用于播放 编辑或图像处理
大家学习了如何从 AVPlayer 切换到 AVKit 的 AVPlayerViewController 以播放 HDR 媒体 大家还了解了如何使用 AVPlayer 和 AVPlayerLayer 在您自己的视图上显示 HDR 媒体 最后 我们探讨了 如何在播放过程中添加实时效果 将 AVFoundation 的 AVPlayer 连接到 CoreVideo 然后连接到 Metal 进行渲染 以及使用 CoreImage 滤镜 和 metal shader 应用实时效果
如您想深入了解 我会推荐几个 与创建视频工作流程 以及将 HDR 媒体与 EDR 整合 相关的 WWDC 讲座 我强烈推荐大家观看讲座 “Edit and Playback HDR video with AVFoundation” 这期讲座探讨了 AVVideoComposition 的使用 通过 applyingCIFiltersWithHandler 将效果应用到 HDR 媒体 在本期讲座中 您还会学到 如何使用自定义合成器 当每个视频帧可供处理时 它可以与 CVPixelBuffer 一起使用 正如我在开始时提到的 今年我们还将举办 另外两场关于 EDR 的讲座 “探索 iOS 上的 EDR” 我们在里面宣布了 EDR API 支持已扩展到包括 iOS 以及 “借助 Core Image、 Metal 和 SwiftUI 显示 EDR 内容“ 这节讲座进一步探索 EDR 与其他媒体框架的集成 希望您能把 HDR 视频整合到 macOS 和现在 iOS 上的开启了 EDR 的 App 中 感谢收看
-
-
6:58 - Playing media using AVPlayerViewController
// Playing media using AVPlayerViewController let player = AVPlayer(URL: videoURL) // Creating a player view controller var playerViewController = AVPlayerViewController() playerViewController.player = player self.presentViewController(playerViewController, animated: true) { playerViewController.player!.play() }
-
7:38 - Playing media using AVPlayer and AVPlayerLayer
// Playing media using AVPlayer and AVPlayerLayer let player = AVPlayer(URL: videoURL) var playerLayer = AVPlayerLayer(player: player) playerLayer.frame = self.view.bounds self.view.layer.addSublayer(playerLayer) player.play()
-
9:28 - CAMetalLayer Properties
// Opt into using EDR let layer: CAMetalLayer layer.wantsExtendedDynamicRangeContent = true // Use half-float pixel format layer.pixelFormat = MTLPixelFormatRGBA16Float // Use extended linear display P3 color space layer.colorspace = kCGColorSpaceExtendedLinearDisplayP3
-
11:33 - Create an AVPlayerItemVideoOutput
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] // Create a player item video output let videoPlayerItemOutput = AVPlayerItemVideoOutput(outputSettings: outputVideoSettings)
-
13:02 - Create a display link
// Create a display link lazy var displayLink: CADisplayLink = CADisplayLink(target: self, selector: #selector(displayLinkCopyPixelBuffers(link:))) var statusObserver: NSKeyValueObservation? statusObserver = videoPlayerItem.observe(\.status, options: [.new, .old], changeHandler: { playerItem, change in if playerItem.status == .readyToPlay { playerItem.add(videoPlayerItemOutput) displayLink.add(to: .main, forMode: .common) videoPlayer?.play() } }) }
-
14:16 - Run DisplayLink to get pixel buffers
@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 output = filter.outputImage ?? CIImage.empty() // use context to render to you CIRenderDestination } } }
-
15:53 - Integrate 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 output = filter.outputImage ?? CIImage.empty() // use context to render to your CIRenderDestination } } }
-
19:13 - Using CVMetalTextureCache
// Create a CVMetalTextureCacheRef let mtlDevice = MTLCreateSystemDefaultDevice() var mtlTextureCache: CVMetalTextureCache? = nil CVMetalTextureCacheCreate(allocator: kCFAllocatorDefault, cacheAttributes: nil, metalDevice: mtlDevice, textureAttributes: nil, cacheOut: &mtlTextureCache) // Create a CVMetalTextureRef using metalTextureCache and our pixelBuffer 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) // In Obj-C, release CVMetalTextureRef in Metal command buffer completion handlers
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。