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

news2025/7/14 15:57:40

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 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_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 = 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
在这里插入图片描述

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.coloradmin.cn/o/2376148.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

浅析 Golang 内存管理

文章目录 浅析 Golang 内存管理栈&#xff08;Stack&#xff09;堆&#xff08;Heap&#xff09;堆 vs. 栈内存逃逸分析内存逃逸产生的原因避免内存逃逸的手段 内存泄露常见的内存泄露场景如何避免内存泄露&#xff1f;总结 浅析 Golang 内存管理 在 Golang 当中&#xff0c;堆…

C++ 并发编程(1)再学习,为什么子线程不调用join方法或者detach方法,程序会崩溃? 仿函数的线程启动问题?为什么线程参数默认传参方式是值拷贝?

本文的主要学习点&#xff0c;来自 这哥们的视频内容&#xff0c;感谢大神的无私奉献。你可以根据这哥们的视频内容学习&#xff0c;我这里只是将自己不明白的点&#xff0c;整理记录。 C 并发编程(1) 线程基础&#xff0c;为什么线程参数默认传参方式是值拷贝&#xff1f;_哔…

【Python 算法零基础 2.模拟 ④ 基于矩阵】

目录 基于矩阵 Ⅰ、 2120. 执行所有后缀指令 思路与算法 ① 初始化结果列表 ② 方向映射 ③ 遍历每个起始位置 ④ 记录结果 Ⅱ、1252. 奇数值单元格的数目 思路与算法 ① 初始化矩阵 ② 处理每个操作 ③ 统计奇数元素 Ⅲ、 832. 翻转图像 思路与算法 ① 水平翻转图像 ② 像素值…

【教程】Docker方式本地部署Overleaf

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhagn.cn] 如果本文帮助到了你&#xff0c;欢迎[点赞、收藏、关注]哦~ 目录 背景说明 下载仓库 初始化配置 修改监听IP和端口 自定义网站名称 修改数据存放位置 更换Docker源 更换Docker存储位置 启动Overleaf 创…

3337|3335. 字符串转换后的长度 I(||)

1.字符串转换后的长度 I 1.1题目 3335. 字符串转换后的长度 I - 力扣&#xff08;LeetCode&#xff09; 1.2解析 递推法解析 思路框架 我们可以通过定义状态变量来追踪每次转换后各字符的数量变化。具体地&#xff0c;定义状态函数 f(i,c) 表示经过 i 次转换后&#xff0…

PHP黑白胶卷底片图转彩图功能 V2025.05.15

关于底片转彩图 传统照片底片是摄影过程中生成的反色图像&#xff0c;为了欣赏照片&#xff0c;需要通过冲印过程将底片转化为正像。而随着数字技术的发展&#xff0c;我们现在可以使用数字工具不仅将底片转为正像&#xff0c;还可以添加色彩&#xff0c;重现照片原本的色彩效…

字符串检索算法:KMP和Trie树

目录 1.引言 2.KMP算法 3.Trie树 3.1.简介 3.2.Trie树的应用场景 3.3.复杂度分析 3.4.Trie 树的优缺点 3.5.示例 1.引言 字符串匹配&#xff0c;给定一个主串 S 和一个模式串 P&#xff0c;判断 P 是否是 S 的子串&#xff0c;即找到 P 在 S 中第一次出现的位置。暴力匹…

基于.Net开发的网络管理与监控工具

从零学习构建一个完整的系统 平常项目上线后&#xff0c;不仅意味着开发的完成&#xff0c;更意味着项目正式进入日常运维阶段。在这个阶段&#xff0c;网络的监控与管理也是至关重要的&#xff0c;这时候就需要一款网络管理工具&#xff0c;可以协助运维人员用于日常管理&…

Python并发编程:开启性能优化的大门(7/10)

1.引言 在当今数字化时代&#xff0c;Python 已成为编程领域中一颗璀璨的明星&#xff0c;占据着编程语言排行榜的榜首。无论是数据科学、人工智能&#xff0c;还是 Web 开发、自动化脚本编写&#xff0c;Python 都以其简洁的语法、丰富的库和强大的功能&#xff0c;赢得了广大…

易学探索助手-个人记录(十)

在现代 Web 应用中&#xff0c;用户体验的重要性不断上升。近期我完成了两个功能模块 —— 语音播报功能 与 用户信息修改表单&#xff0c;分别增强了界面交互与用户自管理能力。 一、语音播报功能&#xff08;SpeechSynthesis&#xff09; 功能特点 支持播放、暂停、继续、停…

学习51单片机01(安装开发环境)

新学期新相貌.......哈哈哈&#xff0c;我终于把贪吃蛇结束了&#xff0c;现在我们来学stc51单片机&#xff01; 要求&#xff1a;c语言的程度至少要到函数&#xff0c;指针尽量&#xff01;如果c语言不好的&#xff0c;可以回去看看我的c语言笔记。 1.开发环境的安装&#x…

SpringAI

机器学习&#xff1a; 定义&#xff1a;人工智能的子领域&#xff0c;通过数据驱动的方法让计算机学习规律&#xff0c;进行预测或决策。 核心方法&#xff1a; 监督学习&#xff08;如线性回归、SVM&#xff09;。 无监督学习&#xff08;如聚类、降维&#xff09;。 强化学…

lua 作为嵌入式设备的配置语言

从lua的脚本中获取数据 lua中栈的索引 3 | -1 2 | -2 1 | -3 可以在lua的解释器中加入自己自定的一些功能,其实没啥必要,就是为了可以练习下lua

ERP系统源码,小型工厂ERP系统源码,CRM+OA+进销存+财务

ERP系统源码&#xff0c;小型工厂ERP系统源码&#xff0c;ERP计划管理系统源码&#xff0c;CRMOA进销存财务 对于ERP来说&#xff0c;最为主要的作用就是能够强调企业的计划性&#xff0c;通过以业务订单以及客户的相关需求来作为企业计划的基础&#xff0c;并且还能够对企业现…

基于EFISH-SCB-RK3576/SAIL-RK3576的矿用本安型手持终端技术方案‌

&#xff08;国产化替代J1900的矿山智能化解决方案&#xff09; 一、硬件架构设计‌ ‌本安型结构设计‌ ‌防爆防护体系‌&#xff1a; 采用铸镁合金外壳复合防爆玻璃&#xff08;抗冲击能量>20J&#xff09;&#xff0c;通过GB 3836.1-2021 Ex ib I Mb认证 全密闭IP68接口…

配置文件介绍xml、json

#灵感# 常用xml&#xff0c; 但有点模棱两可&#xff0c;记录下AI助理给我总结的。 .xml XML&#xff08;eXtensible Markup Language&#xff0c;可扩展标记语言&#xff09;是一种用于存储和传输数据的标记语言。它与 HTML 类似&#xff0c;但有以下主要特点和用途&#xf…

【PostgreSQL数据分析实战:从数据清洗到可视化全流程】附录-D. 扩展插件列表(PostGIS/PostgREST等)

&#x1f449; 点击关注不迷路 &#x1f449; 点击关注不迷路 &#x1f449; 点击关注不迷路 文章大纲 附录D. PostgreSQL扩展插件速查表一、插件分类速查表二、核心插件详解三、安装与配置指南四、应用场景模板五、版本兼容性说明六、维护与优化建议七、官方资源与工具八、附录…

Qt笔记---》.pro中配置

文章目录 1、概要1.1、修改qt项目的中间文件输出路径和部署路径1.2、Qt 项目模块配置1.3、外部库文件引用配置 1、概要 1.1、修改qt项目的中间文件输出路径和部署路径 &#xff08;1&#xff09;、为解决 “ 输出文件 ” 和 “ 中间输出文件 ”全部在同一个文件夹下的问题&am…

【Liblib】基于LiblibAI自定义模型,总结一下Python开发步骤

一、前言 Liblib AI&#xff08;哩布哩布 AI&#xff09;是一个集成了先进人工智能技术和用户友好设计的 AI 图像创作绘画平台和模型分享社区。 强大的图像生成能力 &#xff1a;以 Stable Diffusion 技术为核心&#xff0c;提供文生图、图生图、图像后期处理等功能&#xff…

CCF第七届AIOps国际挑战赛季军分享(RAG)

分享CCF 第七届AIOps国际挑战赛的季军方案&#xff0c;从我们的比赛经历来看&#xff0c;并不会&#xff0c;相反&#xff0c;私域领域问答的优秀效果说明RAG真的很重要 历经4个月的时间&#xff0c;从初赛赛道第1&#xff0c;复赛赛道第2&#xff0c;到最后决赛获得季军&…