视频采集所使用的代码和之前的相同,所以不再过多进行赘述
VideoCaptureConfig
设置分辨率1920x1080、帧率30fps、像素格式kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
AVCaptureSession
会话AVCaptureDeviceInput
AVCaptureVideoDataOutput
AVCaptureVideoPreviewLayer
AVCaptureVideoDataOutputSampleBufferDelegate
接收视频帧sampleBufferOutputCallBack
传递CMSampleBuffer
KFVideoEncoderConfig
对象//
// KFVideoEncoderConfig.swift
// VideoDemo
//
// Created by ricard.li on 2025/5/14.
//
import Foundation
import AVFoundation
import VideoToolbox
class KFVideoEncoderConfig {
/// 分辨率
var size: CGSize
/// 码率 (bps)
var bitrate: Int
/// 帧率 (fps)
var fps: Int
/// GOP 帧数 (关键帧间隔)
var gopSize: Int
/// 是否启用 B 帧
var openBFrame: Bool
/// 编码器类型
var codecType: CMVideoCodecType
/// 编码 profile
var profile: String
init() {
self.size = CGSize(width: 1080, height: 1920)
self.bitrate = 5000 * 1024
self.fps = 30
self.gopSize = self.fps * 5
self.openBFrame = true
var supportHEVC = false
if #available(iOS 11.0, *) {
// 注意 Swift 中直接调用 VTIsHardwareDecodeSupported
supportHEVC = VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC)
}
if supportHEVC {
self.codecType = kCMVideoCodecType_HEVC
self.profile = kVTProfileLevel_HEVC_Main_AutoLevel as String
} else {
self.codecType = kCMVideoCodecType_H264
self.profile = AVVideoProfileLevelH264HighAutoLevel
}
}
}
KFVideoEncoder
实例VTCompressionSession
编码会话kVTCompressionPropertyKey_RealTime
、kVTCompressionPropertyKey_ProfileLevel
等//
// KFVideoEncoder.swift
// VideoDemo
//
// Created by ricard.li on 2025/5/14.
//
import Foundation
import AVFoundation
import VideoToolbox
import UIKit
/// 视频编码器,基于 VideoToolbox,支持硬件编码 H264/H265
class KFVideoEncoder {
/// 编码会话
private var compressionSession: VTCompressionSession?
/// 编码配置
private(set) var config: KFVideoEncoderConfig
/// 编码专用队列,避免线程竞争
private let encoderQueue = DispatchQueue(label: "com.KeyFrameKit.videoEncoder")
/// 用于串行化的信号量
// private let semaphore = DispatchSemaphore(value: 1)
/// 是否需要刷新 session(比如进入后台后)
private var needRefreshSession = false
/// 重试创建 session 计数
private var retrySessionCount = 0
/// 编码失败的帧计数
private var encodeFrameFailedCount = 0
/// 编码成功后的 SampleBuffer 回调
var sampleBufferOutputCallBack: ((CMSampleBuffer) -> Void)?
/// 错误回调
var errorCallBack: ((Error) -> Void)?
/// 最大允许重试 session 创建次数
private let maxRetrySessionCount = 5
/// 最大允许编码失败帧数
private let maxEncodeFrameFailedCount = 20
/// 初始化
init(config: KFVideoEncoderConfig) {
self.config = config
NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
// semaphore.wait()
releaseCompressionSession()
// semaphore.signal()
}
/// 标记需要刷新 session
func refresh() {
needRefreshSession = true
}
/// 强制刷新编码器(不带完成回调)
func flush() {
encoderQueue.async { [weak self] in
guard let self = self else { return }
// self.semaphore.wait()
self.flushInternal()
// self.semaphore.signal()
}
}
/// 强制刷新编码器(带完成回调)
func flush(withCompleteHandler handler: @escaping () -> Void) {
encoderQueue.async { [weak self] in
guard let self = self else { return }
// self.semaphore.wait()
self.flushInternal()
// self.semaphore.signal()
handler()
}
}
/// 编码单帧视频
func encode(pixelBuffer: CVPixelBuffer, ptsTime: CMTime) {
guard retrySessionCount < maxRetrySessionCount, encodeFrameFailedCount < maxEncodeFrameFailedCount else { return }
encoderQueue.async { [weak self] in
guard let self = self else { return }
// self.semaphore.wait()
var setupStatus: OSStatus = noErr
/// 检查 session 是否需要重建
if self.compressionSession == nil || self.needRefreshSession {
self.releaseCompressionSession()
setupStatus = self.setupCompressionSession()
self.retrySessionCount = (setupStatus == noErr) ? 0 : (self.retrySessionCount + 1)
if setupStatus != noErr {
print("KFVideoEncoder setupCompressionSession error: \(setupStatus)")
self.releaseCompressionSession()
} else {
self.needRefreshSession = false
}
}
guard let session = self.compressionSession else {
// self.semaphore.signal()
if self.retrySessionCount >= self.maxRetrySessionCount {
DispatchQueue.main.async {
self.errorCallBack?(NSError(domain: "\(KFVideoEncoder.self)", code: Int(setupStatus), userInfo: nil))
}
}
return
}
var flags: VTEncodeInfoFlags = []
/// 编码当前帧
let encodeStatus = VTCompressionSessionEncodeFrame(session, imageBuffer: pixelBuffer, presentationTimeStamp: ptsTime, duration: CMTime(value: 1, timescale: CMTimeScale(self.config.fps)), frameProperties: nil, sourceFrameRefcon: nil, infoFlagsOut: &flags)
/// 检测 session 异常,尝试重建
if encodeStatus == kVTInvalidSessionErr {
self.releaseCompressionSession()
setupStatus = self.setupCompressionSession()
self.retrySessionCount = (setupStatus == noErr) ? 0 : (self.retrySessionCount + 1)
if setupStatus == noErr {
_ = VTCompressionSessionEncodeFrame(session, imageBuffer: pixelBuffer, presentationTimeStamp: ptsTime, duration: CMTime(value: 1, timescale: CMTimeScale(self.config.fps)), frameProperties: nil, sourceFrameRefcon: nil, infoFlagsOut: &flags)
} else {
self.releaseCompressionSession()
}
print("KFVideoEncoder kVTInvalidSessionErr")
}
/// 编码失败计数
if encodeStatus != noErr {
print("KFVideoEncoder VTCompressionSessionEncodeFrame error: \(encodeStatus)")
}
self.encodeFrameFailedCount = (encodeStatus == noErr) ? 0 : (self.encodeFrameFailedCount + 1)
// self.semaphore.signal()
/// 达到最大失败次数,触发错误回调
if self.encodeFrameFailedCount >= self.maxEncodeFrameFailedCount {
DispatchQueue.main.async {
self.errorCallBack?(NSError(domain: "\(KFVideoEncoder.self)", code: Int(encodeStatus), userInfo: nil))
}
}
}
}
/// 进入后台,标记 session 需要刷新
@objc private func didEnterBackground() {
needRefreshSession = true
}
/// 创建编码会话
private func setupCompressionSession() -> OSStatus {
var session: VTCompressionSession?
let status = VTCompressionSessionCreate(allocator: nil,
width: Int32(config.size.width),
height: Int32(config.size.height),
codecType: config.codecType,
encoderSpecification: nil,
imageBufferAttributes: nil,
compressedDataAllocator: nil,
outputCallback: { (outputCallbackRefCon, _, status, infoFlags, sampleBuffer) in
guard let sampleBuffer = sampleBuffer else {
if infoFlags.contains(.frameDropped) {
print("VideoToolboxEncoder kVTEncodeInfo_FrameDropped")
}
return
}
/// 将 sampleBuffer 通过回调抛出
let encoder = Unmanaged<KFVideoEncoder>.fromOpaque(outputCallbackRefCon!).takeUnretainedValue()
encoder.sampleBufferOutputCallBack?(sampleBuffer)
},
refcon: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()),
compressionSessionOut: &session)
if status != noErr {
return status
}
guard let compressionSession = session else { return status }
self.compressionSession = compressionSession
/// 设置基本属性
VTSessionSetProperty(compressionSession, key: kVTCompressionPropertyKey_RealTime, value: kCFBooleanTrue)
VTSessionSetProperty(compressionSession, key: kVTCompressionPropertyKey_ProfileLevel, value: config.profile as CFString)
VTSessionSetProperty(compressionSession, key: kVTCompressionPropertyKey_AllowFrameReordering, value: config.openBFrame as CFTypeRef)
/// 针对 H264,设置 CABAC
if config.codecType == kCMVideoCodecType_H264 {
VTSessionSetProperty(compressionSession, key: kVTCompressionPropertyKey_H264EntropyMode, value: kVTH264EntropyMode_CABAC)
}
/// 设置像素转换属性
let transferDict: [String: Any] = [kVTPixelTransferPropertyKey_ScalingMode as String: kVTScalingMode_Letterbox]
VTSessionSetProperty(compressionSession, key: kVTCompressionPropertyKey_PixelTransferProperties, value: transferDict as CFTypeRef)
/// 设置码率
VTSessionSetProperty(compressionSession, key: kVTCompressionPropertyKey_AverageBitRate, value: config.bitrate as CFTypeRef)
/// 针对 H264 且不支持 B 帧,限制数据速率
if !config.openBFrame && config.codecType == kCMVideoCodecType_H264 {
let limits = [config.bitrate * 3 / 16, 1] as [NSNumber]
VTSessionSetProperty(compressionSession, key: kVTCompressionPropertyKey_DataRateLimits, value: limits as CFArray)
}
/// 设置帧率、GOP
VTSessionSetProperty(compressionSession, key: kVTCompressionPropertyKey_ExpectedFrameRate, value: config.fps as CFTypeRef)
VTSessionSetProperty(compressionSession, key: kVTCompressionPropertyKey_MaxKeyFrameInterval, value: config.gopSize as CFTypeRef)
VTSessionSetProperty(compressionSession, key: kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration, value: (Double(config.gopSize) / Double(config.fps)) as CFTypeRef)
/// 准备编码
return VTCompressionSessionPrepareToEncodeFrames(compressionSession)
}
/// 释放编码会话
private func releaseCompressionSession() {
if let session = compressionSession {
VTCompressionSessionCompleteFrames(session, untilPresentationTimeStamp: .invalid)
VTCompressionSessionInvalidate(session)
self.compressionSession = nil
}
}
/// 内部刷新逻辑
private func flushInternal() {
if let session = compressionSession {
VTCompressionSessionCompleteFrames(session, untilPresentationTimeStamp: .invalid)
}
}
}
可以很容易的知道,在编码采集成功后,会有一个视频帧输出回调
会调用上面文件的encode方法,encode方法中,会对session回话进行配置,我们再看向session会话,如果编码成功的话,会通过闭包返回 sampleBuffer
CMSampleBuffer
数据CVPixelBuffer
和时间戳信息VTCompressionSessionEncodeFrame
提交帧进行编码CMFormatDescription
中获取H264的SPS、PPS或HEVC的VPS、SPS、PPSkCMSampleAttachmentKey_NotSync
是否存在)[extradata]|[length][NALU]|[length][NALU]|...
[startcode][NALU]|[startcode][NALU]|...
0x00000001
FileHandle
写入到.h264/.h265文件captureQueue
队列encoderQueue
队列