WWDC 2017 Session 511 Working with HEIF and HEVC

前情提要

什么是HEVC、苹果为什么选择HEVC,之前在503 hevc introduce已经介绍过了,总的来说就是为了4K甚至更大的分辨率、高比特深度(high depths)比如10-bit、更宽的色彩空间,做这些都需要更高的压缩率来支撑,hevc相对H.264压缩率提高了40%,对于相机采集的场景,相比H.264压缩率提升了50%

今天讲的主要是如何应用,分四个主题:

  • Access
  • Playback
  • Capture
  • Export

Access

PhotoKit的PHImageManager请求HEVC内容播放

1
2
3
4
5
6
// PHImageManager
manager.requestPlayerItem(forVideo: asset, options: nil){(playerItem, dictionary) in // use AVPlayerItem
}
manager.requestLivePhoto(for: asset, targetSize: size, contentMode: .default, options: nil){(livePhoto, dictionary) in // use PHLivePhoto
}

PHAssetResourceManager请求HEVC的Assets,可访问对应文件,进行转码

1
2
3
4
5
// PHImageManager
manager.requestExportSession(forVideo: asset, options: nil, exportPreset: preset){(session, dictionary) in // use AVAssetExportSession
}
manager.requestAVAsset(forVideo: asset, options: nil){(asset, audioMix, dictionary) in // use AVAsset
}

用PHAssetResourceManager访问HEVC数据

1
2
3
4
// PHAssetResourceManager
resourceManager.requestData(for: assetResource, options: nil, dataReceivedHandler:{(data) in // use Data
},{(error) in // handle Error
})

Playback

  1. HEVC支持AVKit、AVFoundation、VideoToolBox
  2. HEVC支持HLS、支持边下边播、本地文件播放
  3. MP4,MOV容器格式支持
  4. API是适配好的,不用修改调用点

播放一个Video的API对于H.264和HEVC是一样的:

1
2
let player = AVPlayer(url: URL(fileURLWithPath: "MyAwesomeMovie.mov"))
player.play()

对于解码能力,想要判断iOS系统是否支持解码一个assetTrack用这isDecodable属性

1
assetTrack.isDecodable

对于播放能力,用isPlayable属性,不是所有内容都能实时播放、不同设备能力不一样
硬解码可以得到较好的功耗、最好的解码性能,下面这句用于判断是否具备硬解能力,这个API不光可以用于判断HEVC,对于其他Codec也适用

1
let hardwareDecodeSupported = VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC)

我们该如何选择Codec,H.264还是HEVC?如果你更重视全平台兼容性,选H.264,这东西有十年了,被产业广泛接受,第三方库对他的兼容性很好,如果你要更小的文件或更高的清晰度,选HEVC

Capture

接下来看看Capture,我们可以用AVFoundation来采集HEVC视频,视频容器格式支持mp4,mov,最低配需要A10 CPU也就是iPhone 7
我们看看大家比较熟悉的结构图

这个图不再赘述,录制H.264 4K视频的常规代码如下

1
2
3
4
5
6
7
8
9
let session = AVCaptureSession()
session.sessionPreset = .hd4K3840x2160
let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: nil, position: .back)
let input = try! AVCaptureDeviceInput(device: camera!)
session.addInput(input)
let movieFileOutput = AVCaptureMovieFileOutput()
session.addOutput(movieFileOutput)
session.startRunning()
movieFileOutput.startRecording(to: url, recordingDelegate: self)

录制HEVC怎么做呢?

1
2
3
4
5
6
7
8
let connection = movieFileOutput.connection(with: .video)
if movieFileOutput.availableVideoCodecTypes.contains(.hevc) {
outputSetings = [AVVideoCodecKey: AVVideoCodecType.hevc]
}
else {
outputSetings = [AVVideoCodecKey: AVVideoCodecType.h264]
}
movieFileOutput.setOutputSettings(outputSetings, for: connection!)

采集HEVC的LivePhoto,结构与视频类似,但Output换成了AVCapturePhotoOutput

我们先看看LivePhoto有哪些更新:

  • 视频稳定性,播放时画面不再颤抖
  • 在播放LivePhoto时候不再停掉音乐播放
  • 更加流畅,帧率支持30fps

录制HEVC的LivePhoto
iOS11提供了新API判断是否支持某个codec,如果不做这个判断赋值,默认是.hevc

1
2
3
4
5
6
let photoSettings = AVCapturePhotoSettings()
photoSettings.livePhotoMovieFileURL = URL(fileURLWithPath: myFilePath)
if photoOutput.availableLivePhotoVideoCodecTypes.contains(.hevc) {
photoSettings.livePhotoVideoCodecType = .hevc
}
photoOutput.capturePhoto(with: photoSettings, delegate: self)

下面是定制性较强的采集流程图,用到了AVAssetWriter,可以想加一层滤镜处理。

配置AVAssetWritterInput有两种方式,建议用新API

1
2
3
4
// iOS 7
vdo.recommendedVideoSettingsForAssetWriter(writingTo: .mov)
// iOS 11
vdo.recommendedVideoSettings(forVideoCodecType: .hevc, assetWriterOutputFileType: .mov)

Export

导出和转码HEVC

  • AVFoundation和VideoToolbox支持转码到HEVC
  • mp4、mov容器格式支持
  • 需要条件判断的代码支持(iPhone7+才能HEVC encode)

对于AVAssetExportSession苹果引入了三个新的Preset,可将其他codec比如H.264转码成HEVC,以节省存储空间

1
2
3
AVAssetExportPresetHEVC1920x1080
AVAssetExportPresetHEVC3840x2160
AVAssetExportPresetHEVCHighestQuality

AVAssetExportSession是更高层的类,向下一层用AVAssetWriter来做转码的话,用下面两种方式来设置Preset

1
2
3
4
//Specify HEVC with output settings for AVAssetWriterInput
settings = [AVVideoCodecKey: AVVideoCodecType.hevc]
//Convenient output settings with AVOutputSettingsAssistant
AVOutputSettingsPreset.hevc1920x1080 AVOutputSettingsPreset.hevc3840x2160

不是所有编码器都能适配你的Output Settings选项,需要用一个函数验证你的输出是否支持你设置的编码codec,encoderID和properties是验证后OK的编码器ID和合法的输出设置选项

1
2
3
4
let error = VTCopySupportedPropertyDictionaryForEncoder( 3840, 2160,
kCMVideoCodecType_HEVC, encoderSpecification, &encoderID, &properties)
if error == kVTCouldNotFindVideoEncoderErr { // no HEVC encoder
}

向下一层来看VideoToolBox相关的编码API,最只要的类是VTCompressionSession

高级主题:

Bit Depth

日出日落在现实中和在电影里是不尽相同的,比如左边的渐变是电影中我们看到的渐变的细节,现象的原因是没有足够的精度去表达这个渐变色阶梯间的细微差别,而10-bit编码可以做到这样的精度,色彩渐变更细腻

10-bit的另一个好处是相比8-bit更加节省存储空间,session没提 http://x264.nl/x264/10bit_02-ateme-why_does_10bit_save_bandwidth.pdf
对于8-bit和10-bit 的支持多说一句,在 iOS 平台, 8-bit的 HEVC 硬编码需要至少 A10 Fusion 芯片才能支持;而iOS的 10-bit HEVC 硬编,由于现有iPhone CPU Level不够,估计要等 A11 芯片了,猜测也许10月的iPhone 8;而 8 位及 10 位 HEVC 的软编码则是MacOS全平台支持。HEVC的硬编/软编的平台支持和8/10-bit的平台支持这个session说的有点乱,请没看[Session 503]的同学直接看下面总结的表格会清晰点:

HEVC Encode Support

  • 8-bit hardware encode: iOS devices with A10 Fusion chip and over | macOS devices with 6th Generation Intel Core and over
  • 10-bit software encode: All Macs running macOS
    HEVC Capture Support
  • 8-bit hardware encode: iOS devices with A10 Fusion chip and over [iPhone 7 Plus, iPhone 7, 10.5-inch iPad Pro, 12.5-inch iPad Pro (2017)]

HEVC Decode Support

  • 8-bit hardware decode: iOS devices with A9 chip and over | macOS devices with 6th Generation Intel Core and over
  • 10-bit hardware decode: iOS devices with A9 chip and over | macOS devices with 7th Generation Intel Core
  • 8-bit software decode: All iOS devices | All Macs
  • 10-bit software decode: All iOS devices | All Macs

那么如何设置10-bit呢?通过设置这个选项kVTCompressionPropertyKey_ProfileLevel 为 kVTProfileLevel_HEVC_Main10_AutoLevel,注意检查是否支持10-bit

1
2
3
4
5
Set profile via kVTCompressionPropertyKey_ProfileLevel
// Check VTSessionCopySupportedPropertyDictionary() for support
kVTProfileLevel_HEVC_Main10_AutoLevel
//CoreVideo pixel buffer format
kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange // 10-bit 4:2:0

Hierarchical Frame Encoding

视频帧基本类型介绍,I帧关键帧,P前向参考帧,B双向参考帧,I P B越往右压缩率越高,计算开销越大,这方面基础知识一大堆,想深入了解的同学自己搜搜就好,不再赘述
现在假设我们有个只能解码30fps的解码器,但有个240帧的内容(李安的比利林恩中场战事120帧。。),很显然,这个假想的解码器必须丢帧,丢帧原则是丢掉其他帧“不参考的帧”,比如IPPPPP这种帧序列可以丢最后一个P帧,没有帧参考他来做diff,同理也可以丢B帧。

我们来看一个低端设备播放240fps内容的真实案例,每隔7个“droppable frame”有一个“Non-droppable frame”(droppable 和 non-droppable可理解成I/P帧的泛化),为了实现droppable frame可丢,让他们全都参考non-dropable frame,而不是他们之间相互参考,但这么做压缩率掉下去了,原因是不能参考邻居,导致每个drappable frame diff出的大小太大不省空间, 这是问题之一

由于假想的解码器只能解30fps的内容,开始丢尝试丢帧,每8个丢4个Droppable,丢到120fps

然后解码器说还是解不了那么多,继续丢到60fps

No way,继续减半丢到30fps

然而,这样的结构,我们很难决策是间隔的丢、还是丢前一半或者后一半。
解决办法是引入时间等级(temporal level),方便我们决策哪些帧先被丢掉比较合适,这样就可以一个level一个level的丢了,就不用纠结怎么丢的问题了;

再来看参考关系,是不是更紧凑了,不是都参考non-dropable了,也会参考邻居droppable了,diff变小提高了压缩率
PS: 隐含的是High Level只参考 low level

同样面对只支持30fps的decoder开始丢帧,过程就变成了丢level


总结下分层编码解决了什么问题:

  • 优化了时间伸缩策略,播放丢帧时候不再用猜
  • 优化了运动补偿,使得参考关系变得更紧凑,提高了压缩率
  • 还有下面这个,没查到什么意思,谁了解给科普下。

怎么用分层编码呢?设置两个属性

1
// Check VTSessionCopySupportedPropertyDictionary() for support kVTCompressionPropertyKey_BaseLayerFrameRate // temporal level 0 frame rate kVTCompressionPropertyKey_ExpectedFrameRate // frame rate of content

继续举上面的🌰,
kVTCompressionPropertyKey_BaseLayerFrameRate设成30,kVTCompressionPropertyKey_ExpectedFrameRate设置成240,
BaseLayer是必须被解码的关键Level,然后其他Level是用来解码或者丢弃的,level从高到低的丢。

HEIF的正确打开姿势

HEIF的发音

德国人🇩🇪读Hife,法国🇫🇷读Eff,俄罗斯🇷🇺读Heef,
但诺基亚的研究人员对这个标准贡献最大,所以为了尊重他们,读成芬兰🇫🇮口音Hafe,尽管他们是少数

Why HEIF

  • 画质一样情况下,HEIF比起20多岁的JPEG平均小两倍,注意是平均,不是最多小两倍
  • HEIF允许将图片分成若干区块,这对于大图的增量式解码很有帮助的,这里提到的区块(Tile)千万别和503中讲的CTU(coding tree unit)搞混了,Tile是帧内可以独立进行解码的矩形区域,包含多个按矩形排列的CTU(数目不要求一定相同),再形象点,Tile相当于华容道棋盘,CTU相当于华容道棋子,棋子大小不一定相同,定义这一结构的初衷是增强编解码的并行处理性能,类似高德地图或浏览器加载大网页的懒加载技术。
  • HEIF支持了透明度、对比度、深度信息,现代图像格式优势尽显

    对于iPhone 的双摄像头如何感知深度,session未涉及,但大体原理其实是很简单的光学+几何原理,下面从知乎盗张图,上过初中的你一定能看懂,图中两个假象的摄像头距离越远测距精度越高,高大上的VR技术以此为前提

    深度信息可以辅助做很多图片编辑的效果,比如下面的背景部分加了Noir black and white filter

原图

前景加了fade filter,讲师说小女孩的裤子还是粉色没变,像素眼们怎么看

还可以这样,小女孩手里的花景深最小,保持原有色彩,其他深度部分都黑白

也可以控制前景背景的亮度,比如下图,要下雨了,小女孩还在诡异地笑

苹果工程师还是很诚实的,注意细节,这图绝对特么没P过,绝对Demo跑出来的

想深入了解HEIF的depth map的同学,可以看507、508
HEIF不光支持静态图片,也支持图片序列,比如这种长曝光

为了演示HEIF的超高压缩率和区块懒加载(前文解释过的Tile技术)有多平滑,演讲人展示了在自己iPhone7照片里找了个全景图作为Demo,这个全景图是一个91k by 32k pixels的全景图,如果用RGB色彩空间TIFF容器的图片格式存储这个图片是2GB大小,对于HEIF只有160MB,而且JPEG对图片大小限制在64k by 64k pixels,甚至是不能用来存储这张图的,注意这就是你桌面那个叫Yosemite的破石头山。

为了展示HEIF大小不受限制,而且有分区块的增量加载不会爆掉内存,演讲者一路放大照片,最后看到了照片中山路的限速标志

讲真,这个Demo太东施效颦,和4年前Nokia发布Lumia 1020在草丛里找针的那个Zoom in Demo如出一辙

Low Level Access To HEIF

读写一个图片的最底层的API来自ImageIO的CGImageSource和CGImageDestination,用法和以前读一个JPEG一样,只是换个扩展名,另一个区别是支持硬解的机器6s+确实会解码速度更快一些

1
2
3
4
5
6
7
// Read a jpeg image from file
let inputURL = URL(fileURLWithPath: "/tmp/image.heic")
let source = CGImageSourceCreateWithURL(inputURL as CFURL, nil)
let imageProperties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any]
let image = CGImageSourceCreateImageAtIndex(source, 0, nil)
let options = [kCGImageSourceCreateThumbnailFromImageIfAbsent as String: true, kCGImageSourceThumbnailMaxPixelSize as String: 320] as [String: Any]
let thumb = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary)

上面Zoom in Demo中提到的分块增量加载,可以从imageProperties一个叫TIFF的sub dictionary找到相关的重要参数,JPEG的metadata没这个信息 :

1
2
3
4
5
6
7
8
9
10
11
let imageProperties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any]
"{TIFF}" = {
DateTime = "2017:04:01 22:50:24"; Make = Apple;
Model = "iPhone 7 Plus"; Orientation = 1;
ResolutionUnit = 2;
Software = "11.0";
TileLength = 512;
TileWidth = 512;
XResolution = 72;
YResolution = 72;
};

如何写HEIC图片文件,同样,把原来的jpg扩展名改成heic,注意用guard let做防御,一旦没有HEVC硬件编码器,destination 是nil,这是唯一能判断当前系统是否可以写HEIC的函数

1
2
3
4
5
6
7
8
9
// Writing a CGImage to a HEIF file
let url = URL(fileURLWithPath: "/tmp/output.heic")
guard let destination = CGImageDestinationCreateWithURL(url as CFURL,
AVFileType.heic as CFString, 1, nil)
else
{
fatalError("unable to create CGImageDestination")
}
CGImageDestinationAddImage(imageDestination, image, nil) CGImageDestinationFinalize(imageDestination)

HEIF与PhotoKit
Applying adjustments

  • Photos
  • Videos
  • Live Photos

Common workflows

  • Display
  • Backup
  • Share
照片编辑

第一个典型应用场景是读入一个图片、加旋转和滤镜,输出成通用的JPEG,对于读入图片是HEIF类型不用修改代码,PHContentEditingInput是透明的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func applyPhotoFilter(_ filterName: String, input: PHContentEditingInput, output: PHContentEditingOutput, completion: () -> ()) {
guard let inputImage = CIImage(contentsOf: input.fullSizeImageURL!) else {
fatalError("can't load input image")
}
let outputImage = inputImage .applyingOrientation(input.fullSizeImageOrientation) .applyingFilter(filterName, withInputParameters: nil)
// Write the edited image as a JPEG.
do {
try self.ciContext.writeJPEGRepresentation(of: outputImage,
to: output.renderedContentURL, colorSpace: inputImage.colorSpace!, options: [:])
} catch let error {
fatalError("can't apply filter to image: \(error)")
}
completion()
}

第二个典型场景是读入一个视频,无论输入是不是HEVC,编辑并输出成H.264编码的通用视频,透明的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Editing an HEVC video -- save as H.264
func applyVideoFilter(_ filterName: String, input: PHContentEditingInput, output: PHContentEditingOutput, completionHandler: @escaping () -> ()) {
guard let avAsset = input.audiovisualAsset else {
fatalError("can't get AV asset")
}
let composition = AVVideoComposition(asset: avAsset, applyingCIFiltersWithHandler: { request in
let img = request.sourceImage.applyingFilter(filterName, withInputParameters: nil) request.finish(with: img, context: nil)
})
// Export the video composition to the output URL.
guard let export = AVAssetExportSession(asset: avAsset, presetName: AVAssetExportPresetHighestQuality) else {
fatalError("can't set up AV export session")
}
export.outputFileType = AVFileType.mov
export.outputURL = output.renderedContentURL export.videoComposition = composition export.exportAsynchronously(completionHandler: completionHandler)
}

编辑Live Photo一样是格式透明的,不管输入是不是HEIF/HEVC,输出会是JPEG/H.264内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Editing a HEIF/HEVC live photo -- format handled automatically
func applyLivePhotoFilter(_ filterName: String, input: PHContentEditingInput, output: PHContentEditingOutput, completion: @escaping () -> ()) {
guard let livePhotoContext = PHLivePhotoEditingContext(livePhotoEditingInput: input)
else
{
fatalError("can't get live photo")
}
livePhotoContext.frameProcessor = { frame, _ in
return frame.image.applyingFilter(filterName, withInputParameters: nil)
}
livePhotoContext.saveLivePhoto(to: output) { success, error in
if success {
completion()
}
else {
print("can't output live photo") }
}
}

拍摄HEIF主题

AVCapturePhotoOutput的用法

HEIF拍摄的机型限制:iPhone7+ 和 10.5-inch iPad Pro+(NEW)

上图是AVCapturePhotoCaptureDelegate的处理流程,这个代理能很好支持拍照各个阶段,看上去扩展性可以,后来的迭代中也增加了didFinishRawCaptureFor支持RAW图片格式,也增加了didFinishProcessingLivePhotoMovie用来支持拍摄LivePhoto,但硬伤是iOS4之后一直用CoreMedia中的CMSampleBuffer,CMSampleBuffer可以用来存储各种媒体类型,同样适用于HEVC内容,但和HEIF格式的HEVC内容有本质区别,不支持分区存储增量解码,这样的内容会令解码器迷惑,所以有了新的照片拍摄结果“内存封装类AVCapturePhoto”,用来代替CMSampleBuffer,有如下特点:

  1. 比CMSampleBuffer更快,可以专门优化他的传输
  2. 100%不可修改,便于在module间共享
  3. 专有格式支持

AVCapturePhoto的格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
open class AVCapturePhoto : NSObject {
open var timestamp: CMTime { get }
open var isRawPhoto: Bool { get }
open var pixelBuffer: CVPixelBuffer? { get }
open var previewPixelBuffer: CVPixelBuffer? { get }
open var embeddedThumbnailPhotoFormat: [String : Any]? { get }
open var metadata: [String : Any] { get }
open var depthData: AVDepthData? { get }
open var resolvedSettings: AVCaptureResolvedPhotoSettings { get }
open var photoCount: Int { get }
open var bracketSettings: AVCaptureBracketedStillImageSettings? { get }
open var sequenceCount: Int { get }
open var lensStabilizationStatus: AVCaptureDevice.LensStabilizationStatus { get }
open func fileDataRepresentation() -> Data?
open func cgImageRepresentation() -> Unmanaged<CGImage>?
open func previewCGImageRepresentation() -> Unmanaged<CGImage>?
}

建议逐渐废弃之前AVCapturePhotoCaptureDelegate的老方法,未来会Deprecate

1
2
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto,
error: Error?)

用以往的方式存一个图片,意味着要大量操作thumbnail和metadata,会伴随很多图片Scale和压缩操作,性能上不是最优的。

新API的处理方式是让你充分定制容器属性,比如可以在AVCapturePhotoSettings指定codec、文件类型,metadata里指定地点,指定缩略图的分辨率,请求后delegate 返回的AVCapturePhoto已经被嵌入到HEIC容器,也包含了你要的那个缩略图,最后调用photo.fileDataRepresentation()的处理相比old way就简单多了,纯NSData到存储的字节copy,没有多余的压缩、缩放操作,尤其存HEIF图会充分释放性能优势

HEVC性能须知:

1.如果一边在录制HEVC视频一边在拍HEIC照片的话(这例子感觉有点极端啊,比如一个直播推H.265,一边在拍照。。WTF),HEVC硬件编码器会很忙,如果你拍的是4K 60fps视频,编码器会忙出翔,他只能优先处理对实时性有更高要求的视频拍摄,你的拍照Request会滞后;且为了保证你拍摄高清视频的帧率deadline,在你请求拍照时只能分出很少的时间片来处理你的请求,HEIF的高压缩率是有时间成本的,这时候需要空间换时间,Trade off掉20%的压缩率,换尽快callback你要的照片,如何解决这个极端场景的问题?可以在拍照里设置用JPEG,HEVC编码器就清净了。
2.还有个极端场景是长曝光,长曝光输出的不是Video,是序列帧图片,而且有每秒10帧的基线要求,对编码器处理效率要求很高的,HEIF格式勉强可以cover 10fps的序列帧(序列帧中图像应该是相互独立的,无帧间预测,他的10fps可以理解成每秒10个关键帧),但帧率要求更高的话就切回JPEG吧。

作者wolf6
有问题请 留言 或者私信我的 微博
满分是10分的话,这篇文章你给几分