Working with HEIF and HEVC

以下是 WWDC 2017 - Session 511 - Working with HEIF and HEVC 的学习笔记。

读取

PhotoKit 可以返回 HEVC 资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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
}
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
}

或者按需要直接返回二进制数据

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

播放

播放角度上看,并没有什么需要改变的地方,唯一需要注意的是不同设备解码能力的差异。软解在所有 iOS 11 / macOS 10.13 设备上是支持(也应该包括 tvOS 11),但是硬解需要 A9 芯片或者 6 代及更新的酷睿芯片才能够支持。硬解在 iOS 设备上被特别看中,毕竟移动设备的电量有限。

对于非实时的操作(如解码图片),你可以调用新加入 isDecodable 来判定是否能够在本机解码。

1
2
@available(iOS 11.0, *)
open var isDecodable: Bool { get }

而对于实时操作(如播放视频),你可以调用老的 isPlayable 来判定是否能够在本机播放。

如果你需要判定是否能够在本机使用硬件解码,可以使用下面这个新的 API 来判断。如判断 HEVC 支持,直接传入 kCMVideoCodecType_HEVC 即可。

1
2
@available(iOS 11.0, *)
public func VTIsHardwareDecodeSupported(_ codecType: CMVideoCodecType) -> Bool

录像

同样也是要考虑编码能力的差异。在 iOS 平台, 8 位的 HEVC 硬编码需要至少 A10 Fusion 芯片才能支持;而 10 位 HEVC 的话,估计要等 A11 芯片了。在 macOS 平台,8 位的 HEVC 硬编需要至少 酷睿 6 代,而 10 位需要酷睿 7 代(也就是今年所有的新款电脑);不过这些都是针对使用核显,而独立显卡的设备可就要具体问题具体分析了。而 8 位及 10 位 HEVC 的软编码则是全平台支持。

使用 HEVC 拍摄视频

Apple 给出的 Simple Code 是使用手机摄像头拍摄 4K 视频,并使用 AVCaptureMovieFileOutput 写入文件。唯一需要确定的就是本机是否包括 HEVC 的编解码器,并作出相应的选择。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)

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!)

如果希望兼容性至上,那么 H.264 是比较好的;但是如果考虑更小的文件尺寸,那么无疑请选择 HEVC 。

使用 HEVC 拍摄 Live Photo

同样,使用 HEVC 拍摄 Live Photo 时也是类似的操作,并且可以享受这些增强功能:

  • 视频防抖
  • 音乐回放
  • 30 FPS
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 来写入 HEVC 视频

同样,指定需要的视频编解码器即可。

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

输出

编解码能力前面已经叙述过了,此处不再重复。

使用 AVAssetExportSession 进行转码

当需要转码为 HEVC 格式时,使用新的预设即可。

1
2
3
AVAssetExportPresetHEVC1920x1080
AVAssetExportPresetHEVC3840x2160
AVAssetExportPresetHEVCHighestQuality

使用 AVAssetWriter 编码

AVAssetWriterInput 的输出设置指定编解码器为 HEVC:

1
settings = [AVVideoCodecKey: AVVideoCodecType.hevc]

当然,你也可以使用 AVOutputSettingsAssistant 来方便的为指定预设得到输出配置:

1
2
AVOutputSettingsPreset.hevc1920x1080
AVOutputSettingsPreset.hevc3840x2160

使用下面这个新增的方法,在输出设置中查询编码器支持的属性

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

Encoder ID 对与一个编码器来说是唯一标识符。
Properties 与 encoder ID 可以被用在输出配置中。

使用 VTCompressionSession 来编码采样数据

创建 HEVC 编码器与 H.264 编码器并没有显著区别,指定 kCMVideoCodecType_HEVC 即可。

1
2
3
4
5
6
7
8
9
10
let error = VTCompressionSessionCreate( 
kCFAllocatorDefault,
3840, 2160,
kCMVideoCodecType_HEVC,
encoderSpecification,
nil, nil, nil, nil, // using VTCompressionSessionEncodeFrameWithOutputHandler
&session);
if error == kVTCouldNotFindVideoEncoderErr {
// no HEVC encoder
}

对于 macOS 还有两个额外选项 kVTVideoEncoderSpecification_EnableHardwareAcceleratedVideoEncoderkVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder,可以在 encoderSpecification 中指定。

Bit depth 位深

8 bit 与 10 bit 的区别

当使用 HEVC 10-bit 进行编码时,请注意

  • 通过 kVTCompressionPropertyKey_ProfileLevel 设置 profile
1
2
// Check VTSessionCopySupportedPropertyDictionary() for support
kVTProfileLevel_HEVC_Main10_AutoLevel
  • CoreVideo pixel buffer format
1
kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange // 10-bit 4:2:0

上述两项要匹配使用。

HEVC Hierarchical Encoding 分层编码

视频编码入门-视频帧类型

  • 改进时间的可扩展性
  • 改进运动补偿
  • 文件注释 (MPEG-4 Part 15 - 8.4)

分层的视频编码

1.创建兼容的高帧率内容

2.设置基本层与捕获帧率

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

3.基本层必须被编码

4.编码或者丢弃其他层内容

使用 HEIF

HEIF 带来了深度

HEIF 文件扩展说明

HEIF 文件扩展说明

HEVC 文件加载,与加载一张 JPEG 图像仅有扩展名的不同。

1
2
3
4
5
6
7
// Read a heic 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)

Tiling Support in CGImageSource

1
2
3
4
5
6
7
8
9
10
11
12
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";
XResolution = 72;
YResolution = 72;
};

HEIF 文件写入,也是只扩展名的区别

1
2
3
4
5
6
7
8
9
10
// Writing a CGImage to a HEIC 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 照片,并存为 JPEG 格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Editing a HEIF photo -- save as JPEG
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
16
17
// 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)
}

编辑 HEIF/HEVC Live Photo,并自动处理文件格式

1
2
3
4
5
6
7
8
9
10
11
12
// 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") }
}
}

AVCapturePhotoOutput 的使用

AVCapturePhotoOutput 流程

全新的 AVCapturePhoto,用来代替 CMSampleBuffer 的不足

  • 比 CMSampleBuffer 更快
  • 绝对的不可变
  • 由专用的数据格式支持
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 新增的代理方法

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

AVCapturePhotoCaptureDelegate 废弃的代理方法

1
2
3
4
5
6
7
8
9
10
11
12
13
func photoOutput(_ output: AVCapturePhotoOutput, 
didFinishProcessingPhoto photoSampleBuffer: CMSampleBuffer?,
previewPhoto: CMSampleBuffer?,
resolvedSettings: AVCaptureResolvedPhotoSettings,
bracketSettings: AVCaptureBracketedStillImageSettings?,
error: Error?)

func photoOutput(_ output: AVCapturePhotoOutput,
didFinishProcessingRawPhoto rawSampleBuffer: CMSampleBuffer?,
previewPhoto: CMSampleBuffer?,
resolvedSettings: AVCaptureResolvedPhotoSettings,
bracketSettings: AVCaptureBracketedStillImageSettings?,
error: Error?)

iOS 10 与 iOS 11 AVCapturePhotoOutput 支持的格式对比

老方法与新方法流程对比

相关视频