iOS视频编码详细步骤(视频编码器,基于 VideoToolbox,支持硬件编码 H264/H265)

article/2025/6/19 17:41:59

iOS视频编码详细步骤流程

1. 视频采集阶段

视频采集所使用的代码和之前的相同,所以不再过多进行赘述

  • 初始化配置
    • 通过VideoCaptureConfig设置分辨率1920x1080、帧率30fps、像素格式kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
    • 设置摄像头位置(默认前置)和镜像模式
  • 授权与初始化
    • 检查并请求相机权限
    • 创建AVCaptureSession会话
    • 配置摄像头输入源AVCaptureDeviceInput
    • 设置视频输出AVCaptureVideoDataOutput
    • 创建预览层AVCaptureVideoPreviewLayer
  • 数据回调
    • 实现AVCaptureVideoDataOutputSampleBufferDelegate接收视频帧
    • 通过sampleBufferOutputCallBack传递CMSampleBuffer

2. 视频编码准备

  • 编码参数配置
    • 创建KFVideoEncoderConfig对象
    • 设置分辨率1080x1920、码率5Mbps、帧率30fps、GOP帧数150帧
    • 检测设备支持情况,优先选择HEVC,不支持则降级为H264
    • 设置相应编码Profile(H264使用High Profile,HEVC使用Main Profile)
//
//  KFVideoEncoderConfig.swift
//  VideoDemo
//
//  Created by ricard.li on 2025/5/14.
//import Foundation
import AVFoundation
import VideoToolboxclass KFVideoEncoderConfig {/// 分辨率var size: CGSize/// 码率 (bps)var bitrate: Int/// 帧率 (fps)var fps: Int/// GOP 帧数 (关键帧间隔)var gopSize: Int/// 是否启用 B 帧var openBFrame: Bool/// 编码器类型var codecType: CMVideoCodecType/// 编码 profilevar profile: Stringinit() {self.size = CGSize(width: 1080, height: 1920)self.bitrate = 5000 * 1024self.fps = 30self.gopSize = self.fps * 5self.openBFrame = truevar supportHEVC = falseif #available(iOS 11.0, *) {// 注意 Swift 中直接调用 VTIsHardwareDecodeSupportedsupportHEVC = VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC)}if supportHEVC {self.codecType = kCMVideoCodecType_HEVCself.profile = kVTProfileLevel_HEVC_Main_AutoLevel as String} else {self.codecType = kCMVideoCodecType_H264self.profile = AVVideoProfileLevelH264HighAutoLevel}}
}
  • 编码器初始化
    • 创建KFVideoEncoder实例
    • 创建VTCompressionSession编码会话
    • 配置属性:kVTCompressionPropertyKey_RealTimekVTCompressionPropertyKey_ProfileLevel
    • 设置码率控制、GOP大小、帧率等参数
    • 配置编码回调函数
//
//  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 = configNotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)}deinit {NotificationCenter.default.removeObserver(self)
//        semaphore.wait()releaseCompressionSession()
//        semaphore.signal()}/// 标记需要刷新 sessionfunc refresh() {needRefreshSession = true}/// 强制刷新编码器(不带完成回调)func flush() {encoderQueue.async { [weak self] inguard let self = self else { return }
//            self.semaphore.wait()self.flushInternal()
//            self.semaphore.signal()}}/// 强制刷新编码器(带完成回调)func flush(withCompleteHandler handler: @escaping () -> Void) {encoderQueue.async { [weak self] inguard 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] inguard 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) inguard 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,设置 CABACif 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)}/// 设置帧率、GOPVTSessionSetProperty(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
在这里插入图片描述

3. 编码过程执行

  • 输入画面
    • 摄像头采集到CMSampleBuffer数据
    • 从中提取CVPixelBuffer和时间戳信息
  • 编码操作
    • 通过VTCompressionSessionEncodeFrame提交帧进行编码
    • 设置时间戳、帧持续时间等属性
    • 支持编码状态检查和异常处理
  • 应对中断
    • 应用进入后台时标记需刷新会话
    • 会话失效时进行重建
    • 最多重试5次,每次失败计数

4. 数据处理与存储

  • 参数集提取
    • CMFormatDescription中获取H264的SPS、PPS或HEVC的VPS、SPS、PPS
    • 检测关键帧(判断kCMSampleAttachmentKey_NotSync是否存在)
  • 格式转换
    • 原始数据为AVCC/HVCC格式:[extradata]|[length][NALU]|[length][NALU]|...
    • 转换为AnnexB格式:[startcode][NALU]|[startcode][NALU]|...
    • 添加起始码0x00000001
  • 数据写入
    • 关键帧时写入参数集(VPS、SPS、PPS)+ 帧数据
    • 普通帧只写入帧数据
    • 使用FileHandle写入到.h264/.h265文件

5. 并发与线程控制

  • 专用队列隔离
    • 采集使用captureQueue队列
    • 编码使用encoderQueue队列
    • 避免线程竞争和阻塞UI
  • 错误处理
    • 编码失败计数与阈值控制
    • 异常回调通知上层处理
    • 编码状态监控

6. 控制与交互

  • 用户界面控制
    • Start按钮:开始编码
    • Stop按钮:停止编码并刷新
    • Camera按钮:切换前后摄像头
    • 双击屏幕:快速切换摄像头

http://www.hkcw.cn/article/gsBpThtJgU.shtml

相关文章

FramePack本地部署教程:6GB显存即可生成高质量视频,彻底摆脱显存线性依赖!

FramePack 由ControlNet之父张吕敏团队研发&#xff0c;是一种用于逐步生成视频的下一帧&#xff08;下一帧部分&#xff09;预测神经网络结构。FramePack 将输入上下文压缩到固定长度&#xff0c;以便生成工作量与视频长度无关。即使在笔记本电脑 GPU 上&#xff0c;FramePack…

OpenCV与AI深度学习|16个含源码和数据集的计算机视觉实战项目(建议收藏!)

本文来源公众号“OpenCV与AI深度学习”&#xff0c;仅用于学术分享&#xff0c;侵权删&#xff0c;干货满满。 原文链接&#xff1a;分享&#xff5c;16个含源码和数据集的计算机视觉实战项目 本文将分享16个含源码和数据集的计算机视觉实战项目。具体包括&#xff1a; 1. 人…

04、Python爬虫——批量爬取douyin视频,下载到本地,半个小时内解决批量下载douyin视频

概要 针对批量爬取douyin视频分为两期进行讲解&#xff0c;本期&#xff08;第一期&#xff09;内容是讲解如何在上批量下载视频&#xff0c;如何快速的搭建环境&#xff0c;修改参数&#xff0c;让小伙伴们边看边学&#xff0c;半个小时内就可以轻松将douyin视频批量进行下载。…

opencv下载安装及VS配置(笔记)

1、opencv下载及安装 官网地址&#xff1a;https://opencv.org/&#xff0c;点击Releases进入下载界面&#xff1a; 根据自己的需要下载相应的版本&#xff0c;这里我下载的是opencv-4.10版本&#xff1a; 找到下载的exe文件&#xff1a; 双击安装&#xff0c;选择安装路径&…

[ComfyUI]腾讯混元视频:v2v视频驱动,最强开源视频模型,影视级画质与导演级运镜,本地16G可体验

前言 腾讯混元视频&#xff1a;v2v视频驱动&#xff0c;影视级画质与导演级运镜&#xff0c; HunyuanVideo简介 在之前文章中已经介绍过腾讯最新开源的当前最大参数的文生视频模型&#xff1a;HunyuanVideo。这是一款全新的开源视频生成具有130 亿参数大模型&#xff0c;具有…

一文就懂:基带、视频、中频、射频

在无线电领域&#xff0c;经常接触到基带、视频、中频、射频等概念。这些专业名词比较基础&#xff0c;大部分电子相关专业工程师对于这些概念都比较清楚&#xff0c;无需再往下看。因此本文的受众主要是非本专业相关人士但是又经常接触这些名词的同学&#xff0c;有些同学似懂…

离家出走的卡皮巴拉豆包回家了 两个月后胖了一斤多

6月3日凌晨2点,扬州市茱萸湾风景区之前出走的卡皮巴拉“豆包”走进园区诱捕笼,触动机关后自动门关闭,“豆包”顺利回家。据饲养员介绍,“豆包”出逃整整两个月,在外面不仅没有瘦,反而胖了一斤多,毛发圆润光滑。现在,“霸总”、“躲躲”与归来的“豆包”终于团圆,毛茸茸…

中国留学生在马来西亚自导自演“绑架案” 一人当庭认罪

5月5日,一名18岁的中国留学生在马来西亚遭绑架并被绑匪勒索虐待的画面曝光。这名男生在新加坡留学,4月30日因不明原因入境马来西亚后被一群男子绑架。5月2日,该男生在迪拜经商的父母收到绑匪发来的视频,显示男生被殴打和勒颈。绑匪向男生的父母勒索人民币350万元,并威胁如…

A股6月延续震荡格局?科技与消费仍是主线?十大券商策略来了 科技成长突围

近期,多家券商发布了对6月股市的看法。东吴证券认为,6月可能是新一轮“东升西落”交易的起点。美元周期是这一交易的关键因素。在全球流动性宽松、美元下行阶段,非美资产往往走强,中国市场也将受益。展望未来,弱美元仍是基准假设。基于特朗普政策持续扰动、美国政府债务压…

云南人的第一顿菌子还得是见手青 独特风味引追捧

云南人的第一顿菌子还得是见手青 独特风味引追捧!谈及蘑菇,或许并不陌生,但云南的菌子则另有一番天地。随着旱季结束迎来初雨,云南正式进入雨季,山间野生菌子带着泥土的芬芳,在各大山头肆意生长。每年六月至十一月,菌子们纷纷破土而出,成为云南人餐桌上的美味佳肴。这个…

一张截图骗走3.6万骗局揭秘 警惕"延迟到账"新型诈骗

随着微信等即时通讯工具的普及人际沟通变得更加便捷许多重要事务也开始通过微信处理然而这种便利性也被不法分子所利用近日南宁市江南区人民法院就审理了一起与微信社交相关的诈骗案件被告人陈某因出借个人银行卡用于电信网络诈骗活动被依法判刑基本案情2024年1月,南宁某海鲜批…

在日本花1万3千日元买了两个西瓜

在日本花1万3千日元买了两个西瓜。责任编辑:zx0002

谈判匆匆收场乌方还能找到哪些牌 战场内外寻新策

6月2日,俄乌在土耳其举行的第二轮直接谈判仅持续了大约一个小时。土耳其总统埃尔多安表示,谈判取得了“重大成果”,包括双方就进一步交换战俘和阵亡士兵遗体达成一致。俄罗斯方面称将交换25岁以下的战俘,至少各交换1000人,并单方面向乌方移交6000具阵亡士兵遗体,为此还在…

陶喆看到台下的歌迷在捡蝴蝶 让导演组把灯打开方便大家捡

陶喆演唱会唱到《蝴蝶》,满天飘起了蝴蝶的纸花,而且每一张纸花上都有蝴蝶的歌词简直不要太浪漫!唱完之后歌迷纷纷低头捡起了蝴蝶,DT也是让导演组把灯打开方便大家捡,还cue大家捡蝴蝶纸花的样子好可爱,陶喆你先别唱了,等我们捡完蝴蝶再说。责任编辑:zx0002

网红歌手段煜突发脑溢血去世 直播高压下的悲剧

网红歌手段煜突发脑溢血去世 直播高压下的悲剧!6月1日,网红歌手段煜因突发脑溢血抢救无效去世,年仅46岁。她的离世给粉丝和观众带来了极大的冲击。段煜本名段雪霞,曾经历过失败的婚姻,成为三个孩子的母亲。面对生活的压力,她没有放弃,而是选择了直播这条路,希望为孩子们…

台旅行团整团被卖至缅甸 5人生死未卜 警惕免费旅游陷阱

台旅行团整团被卖至缅甸 5人生死未卜 警惕免费旅游陷阱!台中8人因轻信“免费招待游泰之旅”,整团被卖到缅甸的诈骗组织。其中3名年龄较大的妇女因为不擅长使用电子产品,家属支付了约7万元人民币赎金后获释返台。剩下的5名年轻人则因具备使用电子设备的能力,被转卖至其他园区…

程潇在妈妈患癌后一瞬间长大 成为一个强大可靠的成年人

真正让程潇迅速长大,还是因为母亲犯乳腺癌。自从母亲生病后,程潇一夜之间就长大了,开始接很多的工作,把赚来的钱给妈妈租房看病。她还要供妹妹读书。她知道自己要成为家庭的顶梁柱了。看到程潇的要强,就知道她的辛苦。因为原生家庭没人可以为她兜底,她只能靠自己。哪个女…

半年不上班的心理变化 生活与精神的双重挑战

半年不上班的心理变化 生活与精神的双重挑战!如今,越来越多的人选择投身于自媒体行业,追求自由职业的生活方式。但也有一部分人在辞职后选择放松,既不积极寻找工作,也不愿意回归日常上班生活。长期不上班会带来哪些潜在的后果?长期不工作者又会有怎样的生活状态?长期不上…

贪心算法应用:超图匹配问题详解

贪心算法应用&#xff1a;超图匹配问题详解 贪心算法在超图匹配问题中有着广泛的应用。下面我将从基础概念到具体实现&#xff0c;全面详细地讲解超图匹配问题及其贪心算法解决方案。 一、超图匹配问题基础 1. 超图基本概念 **超图&#xff08;Hypergraph&#xff09;**是普…

贪心算法与材料切割问题详解

贪心算法与材料切割问题详解 材料切割问题&#xff08;Stock Cutting Problem&#xff09;是运筹学和算法设计中的经典优化问题&#xff0c;旨在通过最优的切割方案最大化材料利用率。本文将从数学建模、算法策略、Java实现到工业应用进行全面解析。 一、问题定义与数学模型 …