如何使用 AVFoundation 编辑和播放 HDR 视频。

Posted 老司机技术周报

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何使用 AVFoundation 编辑和播放 HDR 视频。相关的知识,希望对你有一定的参考价值。

作者: libkern, ios 开发者

Sessions:

https://developer.apple.com/videos/play/wwdc2020/10009/

https://developer.apple.com/videos/play/wwdc2020/10010/

  1. 一些相关的基础概念

  2. 确定设备是否支持HDR编辑能力

  3. 使用AVMutableVideoComposition与内置合成器来编辑HDR内容

  4. 通过Core Image创建和使用自定义合成器来启用HDR编辑。

  5. 如何导出HDR视频

基本概念

HDR是什么

HRD也可称作HDRI,即High Dynamic Range Imaging,中文可译为“高动态范围成像”。是一种在计算机图形学和电影摄影术中,用来实现比普通数位图像技术更大曝光动态范围(即更大的明暗差别)的一组技术。高动态范围成像的目的就是要正确地表示真实世界中从太阳光直射到最暗的阴影这样大的范围亮度。相比SDR,拥有更大的亮度范围,更广的色域,更大的比特深度。

HDR视频带有HLG (混合对数伽玛 Hybrid Log-Gamma) 或 PQ (感知量化 Perceptual Quantizer) 传输功能, Apple支持两种类型的传递函数。

(图片来自:https://zhuanlan.zhihu.com/p/97421136)

SDR视频每平方米最高不得超过100坎德拉(通常也称为尼特)。而HDR视频可以达到10,000尼特,大了两个数量级。因此HDR视频可以显示更亮的白色和更深的黑色,同时HDR还可以提供更好的对比度,从而在阴影和高光方面提供更多细节。HDR通常与“广色域”隐式配对,色域与动态范围相结合,定义了色彩量或可以描述的色彩总数。为了适应更大的亮度和色彩范围,HDR通常还与更高的位深度相关联,通常10-bit或更高。

传输函数

HDR与传递函数有关,传递函数描述场景线性光如何映射到非线性代码值,然后再返回显示光。

EOTF-电光传递函数,数学传递函数,描述如何将数字值转换为显示设备上的光。

OETF –光电传递函数。数学传递函数,描述通常在相机内如何将光值转换为数字值。

需要注意的是,EOTF不一定是OETF直接逆向得来的。

PQ(或感知量化器)是一种传递函数,旨在表示HDR设备中的宽亮度范围(高达10,000 Nits),根据人类视觉系统的工作原理而设计的,特别是在对比度灵敏度方面。

HLG(或混合对数伽马)是一种传递函数,旨在代表HDR设备的宽亮度范围。是一种场景相关的系统,这意味着编码的代码值是相对于规范化场景光的。HLG与SDR范围内的现有SDR设备完全兼容。

HDR metadata (元数据)

HDR metadata是设计又来改善编码视频的光显示映射关系的,可以用来:

  1. 描述视频模板环境与当前播放环境的差异。

  2. 包含视频流、场景和视频帧相关的统计数据。

No metadata 无元数据

HLG

Static metadata 静态元数据

HDR10,整个视频过程中元数据是静态唯一的。

Dynamic metadata 动态元数据

Dolby Vision (杜比视觉)

不同的视频帧或者场景中元数据是不同的

广色域

上面提到过,HDR通常与广色域有关。

此图说明了色域的差异。

第一个三角形代表BT.709和sRGB色彩空间,它们最初是为标清广播电视行业开发的。

下一个较大的三角形代表P3色彩空间,该色彩空间由Digital Cinema Initiatives为电影行业提供。

P3可以代表比BT.709更细微的颜色。最后一个也是最大的三角形代表BT.2020,它提供了更多的颜色。BT.2020被开发为超高清电视的标准。

广色域与HDR传输功能和10bit-format结合使用,可以使视频更加生动,并代表原始场景。

视频编辑工作流程

如果在此你对视频编辑不熟悉,可以查看以往的WWDC视频 Advanced Editing with AV Foundation 和 Working with Media in AV Foundation

AVPlayerItem and AVAssetExportSeesion workflow

在此流程中,AVPlayerItem用于传递给AVPlayer做视频预览,使用AVAssetExportSession用于视频导出。AVComposition用于对齐源视频时间轨道,AVVideoComposition 用于指定在任何给定时间点的几何变换和轨迹混合。

static func loadAsCompositions() -> (AVComposition, AVVideoComposition) {
        guard let url = Bundle.main.url(forResource: "HDRMovie", withExtension: "mov") else {
            fatalError("The required video asset wasn't found in the app bundle.")
        }
        
        let asset = AVAsset(url: url)
        
        let avComposition = AVMutableComposition()
        
        let timeRange = CMTimeRange(start: .zero, duration: asset.duration)
        let videoTrack = avComposition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)
        if let sourceTrack = asset.tracks(withMediaType: .video).first {
            try? videoTrack?.insertTimeRange(timeRange, of: sourceTrack, at: .zero)
        }
        
        let videoComposition = AVVideoCompositionBuilder.buildVideoComposition(asset: asset, avComposition: avComposition)
        
        return (avComposition, videoComposition)
    }

AVAssetReader and AVAssetWriter

使用更底层一些的AVAssetReader,此路径下仅仅可以用于视频的导出,无法进行视频的预览。

此流程中我们首先使用AVAssetReader读取videoAsset,然后调用addOutput添加AVAssetReaderVideoCompositionOutput的实例,随后调用startReading开始持续读入。AVAssetReaderVideoCompositionOutput实例对象包含一个AVVideoComposition对象,我们可以通过这个Composition对视频内容做处理,最终将处理过后的视频帧发送到AVAssetWriter,最终写到影片文件中。

启用HDR进行视频编辑

上面大致介绍了视频的编辑流程,由此我们可以得知,对视频编辑的关键在于构造特定的AVVideoComposition,下面将介绍三种构造AVVideoComposition的方式 ,其能力和效果也存在不同。

AVMutableVideoComposition构建,使用内置合成器(Bultin-in Compositor)

此方式适合混合视频的多个或者进行帧级别的几何变换(例如裁剪,缩放,平移和旋转),最常见的用例是在两个视频片段之间的过渡效果,例如滑动,淡入,淡出等。

如果要应用一些过滤效果(Filter Effect,例如模糊,色彩或某些花哨的艺术效果),则此方法不适用。

static func buildVideoComposition(asset: AVAsset, avComposition: AVComposition) -> AVVideoComposition {
        
        let videoTracks = avComposition.tracks(withMediaType: .video)
        
        guard !videoTracks.isEmpty, let videoTrack = videoTracks.first else {
            fatalError("The specified asset has no video tracks.")
        }
        
        let assetSize = videoTrack.naturalSize
        let timeRange = videoTrack.timeRange
        
        var instructionLayers = [AVMutableVideoCompositionLayerInstruction]( "AVMutableVideoCompositionLayerInstruction")
        let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)
        let identityAffine = CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0)
        let verticalFlipAffine = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: assetSize.height)
        var transferDuration = CMTime(value: 85, timescale: 30)
        
        if asset.duration < transferDuration {
            transferDuration = asset.duration
        }
        let transferTimeRange = CMTimeRange(start: .zero, duration: transferDuration )
        layerInstruction.setTransformRamp(fromStart: identityAffine, toEnd: verticalFlipAffine, timeRange: transferTimeRange)
        instructionLayers.append(layerInstruction)
        
        let compositionInstruction = AVMutableVideoCompositionInstruction()
        compositionInstruction.timeRange = timeRange
        compositionInstruction.layerInstructions = instructionLayers
        
        let videoComposition = AVMutableVideoComposition()
        videoComposition.instructions = [compositionInstruction]
        videoComposition.frameDuration = CMTime(value: 1, timescale: 30)
        videoComposition.renderSize = assetSize
        return videoComposition
    }

AVMutableVideoComposition构建,使用applyingCIFiltersWithHandler

通过此方式可以对视频帧附加filter效果,比如官方文档中应用高斯模糊的例子

let filter = CIFilter(name: "CIGaussianBlur")!
let composition = AVVideoComposition(asset: asset, applyingCIFiltersWithHandler: { request in

        // Clamp to avoid blurring transparent pixels at the image edges
    let source = request.sourceImage.imageByClampingToExtent()
    filter.setValue(source, forKey: kCIInputImageKey)

        // Vary filter parameters based on video timing
    let seconds = CMTimeGetSeconds(request.compositionTime)
    filter.setValue(seconds * 10.0, forKey: kCIInputRadiusKey)

        // Crop the blurred output to the bounds of the original image
    let output = filter.outputImage!.imageByCroppingToRect(request.sourceImage.extent)

        // Provide the filter output to the composition
    request.finishWithImage(output, context: nil)
})

注意,此方法不能使你混合多个图层,上述实例代码中使用的CIFilter只会应用于该VideoAsset的第一个已启用视频轨道的帧的处理过程中。也就是说只有VideoAsset的第一个启用的视频轨道会被Filter过滤并通过VideoComposition输出。

你可以在AVAsynchronousCIImageFilteringRequest回调的处理过程中使用多个内置CIFilter来实现更复杂的过滤效果。除了内置的CIFilter,我们同样可以通过编写自定义的CoreImage Metal内核代码来构建自定义的CIFilter。

需要了解的是,如果你在使用内置的CIFilter,那么你不需要关心如何开启HDR的处理能力,因为系统已经帮你处理好了。如果你使用上面所说的自行构建的非内置Filter来处理视频帧,那么你需要了解HDR附带的扩展动态范围。

我们看下面给出的CoreImage Metal代码

#include <metal_stdlib>
#include <CoreImage/CoreImage.h> // includes CIKernelMetalLib.h
using namespace metal;

extern "C" float4 HDRHighlight(coreimage::sample_t s, float time, coreimage::destination dest) 
{
    float diagLine = dest.coord().x + dest.coord().y;
    float patternWidth = 40;
    float zebra = fract(diagLine/patternWidth + time*2.0);
    
    if ((zebra > 0.5) && (s.r > 1 || s.g > 1 || s.b > 1))
        return float4(2.0, 0.0, 0.0, 1.0);
    else
        return s;
}

此Metal代码段中知道扩展的动态范围。此过滤器的目的是突出显示超出正常的0到1动态范围的任何像素。如果R G或B通道强度中的任何一个超过1,都会将该像素固定为超亮红色(R = 2.0)。你可能已经注意到,此代码引入了CoreImage,并且语法与我们所见的常规Metal代码有些不同。这种类型的代码称为Metal CoreImage内核,了解更多相关信息可以查阅WWDC20相关课题。

AVMutableVideoComposition,使用Custom Compositor

此种方式最为灵活,通过这个方式可以在多个视频层上分别进行帧级别的几何变换,而且,可以针对每个视频层应用不同的Filter effect。当然,更大的灵活性,也就意味着需要我们自己处理更多的细节。

class SampleCustomCompositor: NSObject, AVVideoCompositing {
    private let filter = HDRIndicatorFilter()
    private let coreImageContext = CIContext(options: [CIContextOption.cacheIntermediates: false])
    
    var sourcePixelBufferAttributes: [String: Any]? = [String(kCVPixelBufferPixelFormatTypeKey): [kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange]]
    var requiredPixelBufferAttributesForRenderContext: [String: Any] =
        [String(kCVPixelBufferPixelFormatTypeKey): [kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange]]
    
    var supportsWideColorSourceFrames = true
    
    var supportsHDRSourceFrames = true
    
    func renderContextChanged(_ newRenderContext: AVVideoCompositionRenderContext) {
        return
    }
    
    func startRequest(_ request: AVAsynchronousVideoCompositionRequest) {
        
        guard let outputPixelBuffer = request.renderContext.newPixelBuffer() else {
            print("No valid pixel buffer found. Returning.")
            request.finish(with: CustomCompositorError.ciFilterFailedToProduceOutputImage)
            return
        }
        
        guard let requiredTrackIDs = request.videoCompositionInstruction.requiredSourceTrackIDs, !requiredTrackIDs.isEmpty else {
            print("No valid track IDs found in composition instruction.")
            return
        }
        
        let sourceCount = requiredTrackIDs.count
        
        if sourceCount > 1 {
            request.finish(with: CustomCompositorError.notSupportingMoreThanOneSources)
            return
        }
        
        if sourceCount == 1 {
            let sourceID = requiredTrackIDs[0]
            let sourceBuffer = request.sourceFrame(byTrackID: sourceID.value(of: Int32.self)!)!
            let sourceCIImage = CIImage(cvPixelBuffer: sourceBuffer)
            filter.inputImage = sourceCIImage
            filter.inputTime = Float(request.compositionTime.seconds)
            if let outputImage = filter.outputImage {
                let renderDestination = CIRenderDestination(pixelBuffer: outputPixelBuffer)
                do {
                    try coreImageContext.startTask(toRender: outputImage, to: renderDestination)
                } catch {
                    print("Error starting request: \\(error)")
                }
            }
        }
        
        request.finish(withComposedVideoFrame: outputPixelBuffer)
    }
}

实现一个自定义的Compositor,继承自NSObject并且遵循AVVideoCompositing协议,实现协议中必要的方法,来完成整个合成工作。

大多数HDR源都带有10位像素格式,那么如何在Custom Compositor中开启HDR的能力?首先,刚才实现的Custom Compositor所做的实际工作需要具有HDR功能,你还需要了解HDR源带来的扩展动态范围。

然后设置输入和输出,使其都支持10位或更高位深度的像素格式,这样系统就清楚你有能力执行HDR相关的处理逻辑。

    var sourcePixelBufferAttributes: [String: Any]? = [String(kCVPixelBufferPixelFormatTypeKey): [kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange]]
    var requiredPixelBufferAttributesForRenderContext: [String: Any] =
        [String(kCVPixelBufferPixelFormatTypeKey): [kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange]]

其次,重写两个参数supportsWideColorSourceFrames和supportsHDRSourceFrames以表示当前Custom Compositor可以处理HDR输入帧。supportsWideColorSourceFrames代表可以处理宽色域的视频帧,因为HDR视频通常带有较宽的色彩。而supportsHDRSourceFrames则是AVVideoCompositing中新增的属性,表示是否支持处理HDR视频输入帧。两者必须确保设置为true。

    static func buildVideoComposition(asset: AVAsset, avComposition: AVComposition) -> AVVideoComposition {
        
        let videoTracks = avComposition.tracks(withMediaType: .video)
        
        guard !videoTracks.isEmpty, let videoTrack = videoTracks.first else {
            fatalError("The specified asset has no video tracks.")
        }
        
        let assetSize = videoTrack.naturalSize
        let timeRange = videoTrack.timeRange
        
        var instructionLayers = [AVMutableVideoCompositionLayerInstruction]( "AVMutableVideoCompositionLayerInstruction")
        let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)
        instructionLayers.append(layerInstruction)
        
        let compositionInstruction = AVMutableVideoCompositionInstruction()
        compositionInstruction.timeRange = timeRange
        compositionInstruction.layerInstructions = instructionLayers
        
        let videoComposition = AVMutableVideoComposition()
        videoComposition.instructions = [compositionInstruction]
        videoComposition.frameDuration = CMTime(value: 1, timescale: 30)
        videoComposition.renderSize = assetSize
        videoComposition.customVideoCompositorClass = SampleCustomCompositor.self
        return videoComposition
    }

通过AVMutableVideoComposition的customVideoCompositorClass字段来设定上面实现的自定义合成器类。

视频合成控制

Apple支持两种类型的HDR视频,这些视频按其传输函数分类。即HLG和PQ。因此,您有三种可能的输入类型:HLG,PQ和SDR。

在没有设置Composition Color的相关字段(colorPrimaries,colorTransferFunction和colorYCbCrMatrix)的情况下,默认使用HLG。

  • colorPrimaries 视频原色,可选值有AVVideoColorPrimaries_P3_D65、AVVideoColorPrimaries_SMPTE_C、AVVideoColorPrimaries_EBU_3213、AVVideoColorPrimaries_ITU_R_709_2、AVVideoColorPrimaries_ITU_R_2020。如果为nil,则使用视频源的原色。

  • colorTransferFunction 传输函数,可选值有AVVideoTransferFunction_ITU_R_709_2、AVVideoTransferFunction_SMPTE_240M_1995、AVVideoTransferFunction_SMPTE_ST_2084_PQ

  • colorYCbCrMatrix 用于视频合成的YCbCr矩阵,可选值有AVVideoYCbCrMatrix_ITU_R_601_4、AVVideoYCbCrMatrix_ITU_R_709_2、AVVideoYCbCrMatrix_SMPTE_240M_1995、AVVideoYCbCrMatrix_ITU_R_2020

有些场景下,我们拥有HDR视频源,但当我们需要传输到不支持HDR播放的一方时,我们可以将颜色属性设置为SDR来节省计算能力。

我们可以通过AVPlayer.eligibleForHDRPlayback来检查当前设备和系统是否能够播放HDR视频,需要注意的是,不支持播放HDR的设备并不意味着其不支持导出HDR的视频。

视频导出

AVFoundation通过HLG和HDR10格式支持HDR,这既是行业标准,也受到消费类显示器的广泛支持。包括本地支持HDR的Apple设备(例如iPhone,iPad,MacBook或iMac)或者是连接到外部HDR显示器或电视上。

Apple在2017年时就已经在专业工具上提供了HDR支持,例如 Final Cut Pro X 和 Compressor。

通过AVAssetExportSession

先来了解下工作流,看下图

AVAssetExportSession设计的初衷是为了方便导出,支持单个或者多个视频混合过后导出。AVAssetExportSession的导出效果是基于预设参数的。

  • 支持H.264, HEVC 还有 Apple ProRes 编码

  • 不同的视频最大分辨率

  • 不同的视频质量级别

那么如何进行HDR视频的导出,如果使用HEVC或者Apple ProRes预设,则不需要任何额外的更改。Apple ProRes预设还将保留视频源的HDR格式。

需要注意的是,带有Alpha通道的HEVC当前不支持HDR。

如果需要进行HDR到SDR的转化,建议使用H.264预设,这样可以最大程度的保持向后兼容。

混合HDR和SDR导出

AVAssetExportSession会遍历所有Assets,搜索支持HDR的视频源。一旦找到任何一个HDR视频源后,当前混合导出的文件将为HDR,并且合成中的任何SDR资源都将转换为HDR。

如果混合使用不同的HDR格式,则导出的默认策略是优先使用HLG而不是PQ格式(AVAssetExportSession知道如何在PQ和HLG格式之间正确转换),优先使用有元数据的HDR格式而不是没有元数据的HDR格式。

规则如下图所示:

通过AVAssetWriter

AVAssetWriter两个基本工作流如所述

  1. 使用AVAssetReader读取,数据可以选择通过AVComposition和/或AVVideoComposition编辑,然后通过AVAssetWriter导出。

  2. 使用AVAssetWriter导出相机捕获的帧的应用程序。

AVAssetWriter可以完全控制导出过程,通过明确指定视频编解码器,比特率,帧频,视频帧尺寸,色彩空间和动态范围还有用于导出的视频编码器。

使用sourceFromatHint

使用AVOutputSettingsAssistant

HDR导出

相关key值

  1. AVVideoCodecKey,AVFoundation通过两个编解码器支持HDR:HEVC和Apple ProRes。

HEVC是HDR的一种非常常见的分发格式,提供了良好的压缩效率,因此导出的文件大小是可控的。

Apple ProRes是用于专业视频工作流程的夹层格式。

  1. AVVideoColorPropertiesKey,此key对应的是一个包含三个key的Dictionary,分别是:传递函数(AVVideoTransferFunctionKey),颜色原色(AVVideoColorPrimariesKey)和YCbCr-Matrix(AVVideoYCbCrMatrixKey)。

传递函数就是transfer-function,比如HLG和PQ。

颜色原色一般设置为广色域格式,比如AVVideoColorPrimaries_ITU_R_2020 (BT.2020,非恒定亮度系统),这在HDR内容中很常用。

YCbCr-Matrix-Key表示如何在YUV和RGB颜色表示之间进行转换。

  1. AVVideoCompressionPropertiesKey是一个Dictionary,大多数我们需要自定义设置的参数都在这里。包括比特率,B帧传输,I帧间隔和编解码器质量等等,可以使用supportedOutputSettingsKeys来查询支持设置的key值。

其中唯一相关的HDR key是AVVideoProfileLevelKey,在使用HEVC编解码器导出HDR时,必须将其设置为HEVC_Main10_AutoLevel。请注意,不支持8位HEVC HDR,并且此密钥不适用于ProRes导出。

重要Key值对应关系,比如HLG,PQ。

HLG设置

HDR10设置

支持的设备

iOS:

在装有Apple A10 Fusion芯片或更高版本的设备上支持HEVC硬件编码。2018 iPhone7及以后。

Mac:

HEVC和Apple ProRes软件编码器可在所有Mac上使用。

HEVC硬件编码在2017年和运行新macOS的更新Mac上可用。当然,硬件编码将大大加快导出速度。

总结

  • 通过AVFoundation可以进行HDR视频编辑了,在此过程中AVVideoComposition是整个流程的中心。

  • 如果使用自定义的Compositor,记得设置10-bit pixel formats,并且设置supportsHDRSourceFrames = true

  • 通过color space等参数控制合成,使用eligibleForHDRPlayback检查播放能力。

  • 使用AVAssetExportSession或AVAssetWriter来导出HDR,选择HEVC或Apple ProRes编解码器或预设。

  • 使用AVAssetWriter时,传递函数设置为HLG或PQ。对于HEVC HDR导出,AVVideoProfileLevelKey设置为HEVC_Main10_AutoLevel,广色域最好是ITU_R_2020。

关注我们

我们是「老司机技术周报」,每周会发布一份关于 iOS 的周报,也会定期分享一些和 iOS 相关的技术。欢迎关注。

关注有礼,关注【老司机技术周报】,回复「2020」,领取学习大礼包。

支持作者

这篇文章的内容来自于 《WWDC20 内参》。在这里给大家推荐一下这个专栏,专栏目前已经创作了 108 篇文章,只需要 29.9 元。点击【阅读原文】,就可以购买继续阅读 ~

WWDC 内参 系列是由老司机周报、知识小集合以及 SwiftGG 几个技术组织发起的。已经做了几年了,口碑一直不错。 主要是针对每年的 WWDC 的内容,做一次精选,并号召一群一线互联网的 iOS 开发者,结合自己的实际开发经验、苹果文档和视频内容做二次创作。

以上是关于如何使用 AVFoundation 编辑和播放 HDR 视频。的主要内容,如果未能解决你的问题,请参考以下文章

AVFoundation自己定义音视频频播放

IOS开发中AVFoundation中AVAudioPlayer的使用

关于使用 AVAudioPlayer 自动录制和自动播放 AVAudioRecorder

AVFoundation 初解

如何使用 AVFoundation 以正确的音高播放不同采样率的音频文件?

如何防止使用 AVFoundation 录制视频中断当前正在播放的任何全局音频(Swift)?