探索 VideoToolbox 低延迟视频编码
Explore low-latency video encoding with VideoToolbox
2021年6月11日
一句话判断
如果你的视频会议或云游戏 App 的端到端延迟还在 200ms 以上,VideoToolbox 今年的低延迟编码改进能帮你砍掉至少一帧的延迟——在实时交互场景中,33ms 就是”能用”和”不能用”的区别。
这场 Session 讲了什么
Session 介绍了 VideoToolbox 框架在 iOS 15 和 macOS Monterey 中针对低延迟视频编码场景的改进。覆盖了 H.264、HEVC 和 ProRes 编码器的新参数配置、零延迟编码模式、以及 Apple Silicon 上的硬件加速细节。
低延迟编码的三个关键指标是:编码延迟(从原始帧到编码输出的时间)、缓冲延迟(编码器内部缓存的帧数)、和网络传输延迟。VideoToolbox 今年的改进主要集中在前两个。新增的 kVTCompressionPropertyKey_MaxFrameDelayCount 属性可以把编码器的缓冲帧数从默认的 3-5 帧降到 0——每一帧进来就立刻编码输出,不等待后续帧做 B-frame 参考。代价是压缩效率下降约 20-30%,但在实时场景中这个权衡是完全值得的。
值得深挖的点
Zero-delay 编码模式的配置要点
零延迟编码不是简单地设一个属性就完成了。你需要同时配置多个参数:MaxFrameDelayCount = 0(不缓冲帧)、AllowFrameReordering = false(不使用 B-frame)、MaxKeyFrameInterval = 0(不强制 GOP 大小,由编码器决定何时插入 I-frame)。还有一个容易被忽略的属性:RealTime = true——告诉编码器这是实时场景,编码器会在质量和速度之间偏向速度。Apple Silicon 的硬件编码器在 RealTime 模式下的编码延迟通常在 2-5ms,这个数字非常可观。
码率控制在低延迟场景下的特殊性
CBR(Constant Bitrate)在低延迟场景中比 VBR(Variable Bitrate)更合适,因为 CBR 的输出码率可预测,网络传输的抖动更小。但传统 CBR 的问题是画质波动大。VideoToolbox 新增了 kVTCompressionPropertyKey_ConstantBitRate 属性,配合 DataRateLimits 可以实现更精细的码率控制:设定一个目标码率和一个波动上限,编码器在这个范围内自适应调整。
代码片段
// 配置低延迟视频编码器
func setupLowLatencyEncoder(width: Int, height: Int) -> VTCompressionSession? {
var session: VTCompressionSession?
let status = VTCompressionSessionCreate(
allocator: nil,
width: Int32(width),
height: Int32(height),
codecType: kCMVideoCodecType_H264,
encoderSpecification: nil,
sourceImageBufferAttributes: nil,
compressedDataAllocator: nil,
outputCallback: encodingCallback,
outputCallbackRefCon: nil,
compressionSessionOut: &session
)
guard status == noErr, let encoder = session else { return nil }
// 关键:零延迟配置
var zeroFrameDelay: Int = 0
VTSessionSetProperty(encoder,
key: kVTCompressionPropertyKey_MaxFrameDelayCount,
value: NSNumber(value: zeroFrameDelay))
// 禁用 B-frame 重排序
var noReorder: Boolean = 0
VTSessionSetProperty(encoder,
key: kVTCompressionPropertyKey_AllowFrameReordering,
value: NSNumber(value: noReorder))
// 标记为实时场景
var realtime: Boolean = 1
VTSessionSetProperty(encoder,
key: kVTCompressionPropertyKey_RealTime,
value: NSNumber(value: realtime))
// 目标码率(2 Mbps)
let bitrate: Int = 2_000_000
VTSessionSetProperty(encoder,
key: kVTCompressionPropertyKey_AverageBitRate,
value: NSNumber(value: bitrate))
// 准备编码
VTCompressionSessionPrepareToEncodeFrames(encoder)
return encoder
}
// 编码单帧并立即输出
func encodeFrame(session: VTCompressionSession,
pixelBuffer: CVPixelBuffer,
timestamp: CMTime) {
// 每帧设置 presentation timestamp
// 在零延迟模式下,编码完成后回调立即触发
let status = VTCompressionSessionEncodeFrame(
session,
imageBuffer: pixelBuffer,
presentationTimeStamp: timestamp,
duration: .invalid,
frameProperties: nil,
sourceFrameRefCon: nil,
infoFlagsOut: nil
)
if status != noErr {
print("编码失败: \(status)")
}
}
// 编码回调——在零延迟模式下帧进来就立刻触发
let encodingCallback: VTCompressionOutputCallback = {
_, status, infoFlags, sampleBuffer in
guard status == noErr,
let buffer = sampleBuffer,
CMSampleBufferDataIsReady(buffer) else {
print("编码输出无效")
return
}
// 判断是否为关键帧
let attachments = CMSampleBufferGetSampleAttachmentsArray(buffer,
createIfNecessary: false)
let isKeyFrame = (attachments?.firstObject as? [CFString: Any])?
.filter { $0.key == kCMSampleAttachmentKey_DependsOnOthers }
.first?.value as? Bool ?? true == false
// 立即发送编码后的数据
let dataBuffer = CMSampleBufferGetDataBuffer(buffer)!
var length = CMBlockBufferGetDataLength(dataBuffer)
var data = Data(count: length)
data.withUnsafeMutableBytes { ptr in
CMBlockBufferCopyDataBytes(dataBuffer, atOffset: 0,
dataLength: length, destination: ptr.baseAddress!)
}
// 发送到网络或写入文件
NetworkSender.sendVideoFrame(data, isKeyFrame: !isKeyFrame)
}
最佳实践
- 低延迟编码器的分辨率和帧率选择要保守:720p@30fps 比尝试 1080p@60fps 更容易维持稳定延迟。在 Apple Silicon 上,1080p@30fps 的编码延迟约 3-5ms,60fps 时会翻倍。
- 使用
VTCompressionSessionEncodeFrame的异步模式时,确保你的回调不会阻塞。网络发送操作应该在回调中只做数据拷贝,实际发送放在另一个队列。 - 监控编码器的实际输出码率——如果持续低于目标码率,说明场景太简单编码器在偷懒;如果持续高于目标,网络可能会丢包导致画面撕裂。
还有什么值得关注
- ProRes 编码器在 macOS Monterey 上支持低延迟模式,适合专业视频制作中的实时预览场景。
- HEVC 的低延迟编码在 Apple Silicon 上的性能比 H.264 更好(同等画质下码率低 40%),如果你的目标平台支持 HEVC 解码,优先选用。
- VideoToolbox 的硬件编码器在 Intel Mac 和 Apple Silicon Mac 上的行为有细微差异——需要在两种平台上分别测试延迟指标。