iOS解码实现

import Foundation
import VideoToolboxclass KFVideoDecoderInputPacket {var sampleBuffer: CMSampleBuffer?
}class KFVideoDecoder {// MARK: - 常量private let kDecoderRetrySessionMaxCount = 5private let kDecoderDecodeFrameFailedMaxCount = 20// MARK: - 回调var pixelBufferOutputCallBack: ((CVPixelBuffer, CMTime) -> Void)?var errorCallBack: ((Error) -> Void)?// MARK: - 属性private var decoderSession: VTDecompressionSession? // 视频解码器实例private let decoderQueue: DispatchQueueprivate let semaphore: DispatchSemaphoreprivate var retrySessionCount: Int = 0 // 解码器重试次数private var decodeFrameFailedCount: Int = 0 // 解码失败次数private var gopList: [KFVideoDecoderInputPacket] = []private var inputCount: Int = 0 // 输入帧数var outputCount: Int = 0 // 输出帧数// MARK: - 生命周期init() {decoderQueue = DispatchQueue(label: "com.KeyFrameKit.videoDecoder", qos: .default)semaphore = DispatchSemaphore(value: 1)gopList = []}deinit {semaphore.wait()releaseDecompressionSession()clearCompressQueue()semaphore.signal()}// MARK: - 公共方法func decodeSampleBuffer(_ sampleBuffer: CMSampleBuffer) {guard CMSampleBufferIsValid(sampleBuffer),retrySessionCount < kDecoderRetrySessionMaxCount,decodeFrameFailedCount < kDecoderDecodeFrameFailedMaxCount else {return}// 为异步操作保留样本缓冲区let unmanagedSampleBuffer = Unmanaged.passRetained(sampleBuffer)decoderQueue.async { [weak self] inguard let self = self else {unmanagedSampleBuffer.release()return}self.semaphore.wait()// 1、如果还未创建解码器实例,则创建解码器var setupStatus = noErrif self.decoderSession == nil {if let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer) {setupStatus = self.setupDecompressionSession(videoDescription: formatDescription)self.retrySessionCount = (setupStatus == noErr) ? 0 : (self.retrySessionCount + 1)if setupStatus != noErr {self.releaseDecompressionSession()}}}if self.decoderSession == nil {unmanagedSampleBuffer.release()self.semaphore.signal()if self.retrySessionCount >= self.kDecoderRetrySessionMaxCount, let errorCallback = self.errorCallBack {DispatchQueue.main.async {let error = NSError(domain: String(describing: KFVideoDecoder.self), code: Int(setupStatus), userInfo: nil)errorCallback(error)}}return}// 2、对 sampleBuffer 进行解码var flags = VTDecodeFrameFlags._EnableAsynchronousDecompressionvar flagsOut = VTDecodeInfoFlags()var decodeStatus = VTDecompressionSessionDecodeFrame(self.decoderSession!,sampleBuffer: sampleBuffer,flags: flags,frameRefcon: nil,infoFlagsOut: &flagsOut)if decodeStatus == kVTInvalidSessionErr {// 解码当前帧失败,进行重建解码器重试self.releaseDecompressionSession()if let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer) {setupStatus = self.setupDecompressionSession(videoDescription: formatDescription)self.retrySessionCount = (setupStatus == noErr) ? 0 : (self.retrySessionCount + 1)if setupStatus == noErr {// 重建解码器成功后,要从当前 GOP 开始的 I 帧解码flags = ._DoNotOutputFramefor packet in self.gopList {if let packetBuffer = packet.sampleBuffer {_ = VTDecompressionSessionDecodeFrame(self.decoderSession!,sampleBuffer: packetBuffer,flags: flags,frameRefcon: nil,infoFlagsOut: &flagsOut)}}// 解码当前帧flags = ._EnableAsynchronousDecompressiondecodeStatus = VTDecompressionSessionDecodeFrame(self.decoderSession!,sampleBuffer: sampleBuffer,flags: flags,frameRefcon: nil,infoFlagsOut: &flagsOut)} else {// 重建解码器失败self.releaseDecompressionSession()}}} else if decodeStatus != noErr {print("KFVideoDecoder 解码错误: \(decodeStatus)")}// 统计解码入帧数self.inputCount += 1// 遇到新的 I 帧后,清空上一个 GOP 序列缓存if self.isKeyFrame(sampleBuffer: sampleBuffer) {self.clearCompressQueue()}// 存储当前帧到 GOP 列表let packet = KFVideoDecoderInputPacket()packet.sampleBuffer = unmanagedSampleBuffer.takeRetainedValue()self.gopList.append(packet)// 记录解码失败次数self.decodeFrameFailedCount = (decodeStatus == noErr) ? 0 : (self.decodeFrameFailedCount + 1)self.semaphore.signal()// 解码失败次数超过上限,报错if self.decodeFrameFailedCount >= self.kDecoderDecodeFrameFailedMaxCount, let errorCallback = self.errorCallBack {DispatchQueue.main.async {let error = NSError(domain: String(describing: KFVideoDecoder.self), code: Int(decodeStatus), userInfo: nil)errorCallback(error)}}}}func flush() {decoderQueue.async { [weak self] inguard let self = self else { return }self.semaphore.wait()self.flushInternal()self.semaphore.signal()}}func flush(completionHandler: @escaping () -> Void) {decoderQueue.async { [weak self] inguard let self = self else {completionHandler()return}self.semaphore.wait()self.flushInternal()self.semaphore.signal()completionHandler()}}// MARK: - 私有方法private func setupDecompressionSession(videoDescription: CMFormatDescription) -> OSStatus {if decoderSession != nil {return noErr}// 1、设置颜色格式let attrs: [String: Any] = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange]// 2、设置解码回调var outputCallbackRecord = VTDecompressionOutputCallbackRecord()outputCallbackRecord.decompressionOutputCallback = decompressionOutputCallbackoutputCallbackRecord.decompressionOutputRefCon = Unmanaged.passUnretained(self).toOpaque()// 3、创建解码器实例var session: VTDecompressionSession?let status = VTDecompressionSessionCreate(allocator: kCFAllocatorDefault,formatDescription: videoDescription,decoderSpecification: nil,imageBufferAttributes: attrs as CFDictionary,outputCallback: &outputCallbackRecord,decompressionSessionOut: &session)if status == noErr {decoderSession = session}return status}private func releaseDecompressionSession() {if let session = decoderSession {VTDecompressionSessionWaitForAsynchronousFrames(session)VTDecompressionSessionInvalidate(session)decoderSession = nil}}private func flushInternal() {if let session = decoderSession {VTDecompressionSessionFinishDelayedFrames(session)VTDecompressionSessionWaitForAsynchronousFrames(session)}}private func clearCompressQueue() {gopList.removeAll()}private func isKeyFrame(sampleBuffer: CMSampleBuffer) -> Bool {guard let attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: true) as? [[CFString: Any]],let attachment = attachments.first else {return false}return !attachment.keys.contains(kCMSampleAttachmentKey_NotSync)}
}// MARK: - 回调函数
private func decompressionOutputCallback(decompressionOutputRefCon: UnsafeMutableRawPointer?,sourceFrameRefCon: UnsafeMutableRawPointer?,status: OSStatus,infoFlags: VTDecodeInfoFlags,imageBuffer: CVImageBuffer?,presentationTimeStamp: CMTime,presentationDuration: CMTime)
{guard status == noErr else { return }if infoFlags.contains(.frameDropped) {print("KFVideoDecoder 丢弃帧")return}let decoderOptional = decompressionOutputRefCon.map { Unmanaged<KFVideoDecoder>.fromOpaque($0).takeUnretainedValue() }guard let decoder = decoderOptional,let pixelBuffer = imageBuffer else {return}if let callback = decoder.pixelBufferOutputCallBack {callback(pixelBuffer, presentationTimeStamp)decoder.outputCount += 1}
}
  • 初始化阶段

    • KFVideoDecoderViewControllerviewDidLoad 中初始化解封装器和解码器
    • 设置UI界面,添加"Start"按钮
  • 启动解封装

    • 用户点击"Start"按钮触发 start() 方法
    • 调用 demuxer.startReading,开始读取MP4文件
  • 解封装和解码

    • 解封装成功后,在 fetchAndDecodeDemuxedData 方法中:
      • 循环从 demuxer 获取视频和音频Sample Buffer
      • 将视频Sample Buffer传递给 decoder.decodeSampleBuffer 进行解码
      • 解码器内部使用VideoToolbox的 VTDecompressionSessionDecodeFrame 进行硬件解码
  • 解码回调处理

    • 解码成功后,通过回调函数 decompressionOutputCallback 传递解码后的像素缓冲区
    • 回调触发 pixelBufferOutputCallBack,由 KFVideoDecoderViewController 处理解码后的帧
    • 解码帧被保存到YUV文件中
  • 解码结束

    • demuxer.demuxerStatus.completed 时,表示解封装完成
    • 调用 decoder.flush() 确保所有帧都被处理
    • 将剩余YUV数据写入文件

六、关键技术点

  1. GOP管理
    视频解码需要关键帧作为解码起点。项目中通过gopList存储当前GOP的所有帧,当解码会话失效需要重建时,可以从GOP的I帧开始重新提交解码,避免画面丢失。
  2. 线程安全
    使用专用队列和信号量确保解码操作的线程安全:
    decoderQueue:确保所有解码操作在同一线程序列执行
    semaphore:确保关键资源操作的互斥访问
  3. 异步解码
    VideoToolbox的解码是异步进行的,通过回调函数机制返回结果:
    提交解码任务时设置_EnableAsynchronousDecompression标志开启异步
    解码完成后由VideoToolbox调用我们的回调函数
    回调函数中处理解码结果并传递给上层应用
  4. 错误处理和恢复机制
    解码器实现了健壮的错误处理和恢复机制:
    解码会话失效自动重建
    基于GOP的解码恢复策略
    失败次数限制和错误上报

在这里插入图片描述
需要注意的是,因为是把mp4文件解封装为h264,按照理论来说应该只用处理视频轨道,但是解封装内部的判断为,如果是一个mp4的音视频混合文件,那么视频和音频轨道需要都处理,不然解封装器的状态不能正确结束。

        while let reader = demuxReader, reader.status == .reading && (shouldContinueLoadingAudio || shouldContinueLoadingVideo) {loadCount += 1if loadCount > 100 {break  // 防止无限循环}// 加载音频数据if shouldContinueLoadingAudio {audioQueueSemaphore.wait()let audioCount = CMSimpleQueueGetCount(audioQueue)audioQueueSemaphore.signal()if audioCount < KFMP4DemuxerQueueMaxCount, let audioOutput = readerAudioOutput {// 从音频输出源读取音频数据if let next = audioOutput.copyNextSampleBuffer() {if CMSampleBufferGetDataBuffer(next) == nil {// 移除了CFRelease调用} else {// 将数据从音频输出源 readerAudioOutput 拷贝到缓冲队列 audioQueue 中lastAudioCopyNextTime = CMSampleBufferGetPresentationTimeStamp(next)audioQueueSemaphore.wait()let unmanagedSample = Unmanaged.passRetained(next)CMSimpleQueueEnqueue(audioQueue, element: unmanagedSample.toOpaque())let newAudioCount = CMSimpleQueueGetCount(audioQueue)audioQueueSemaphore.signal()}} else {audioEOF = reader.status == .reading || reader.status == .completedshouldContinueLoadingAudio = false}} else {shouldContinueLoadingAudio = false}}// 加载视频数据if shouldContinueLoadingVideo {videoQueueSemaphore.wait()let videoCount = CMSimpleQueueGetCount(videoQueue)videoQueueSemaphore.signal()if videoCount < KFMP4DemuxerQueueMaxCount, let videoOutput = readerVideoOutput {// 从视频输出源读取视频数据if let next = videoOutput.copyNextSampleBuffer() {if CMSampleBufferGetDataBuffer(next) == nil {// 移除了CFRelease调用} else {// 将数据从视频输出源 readerVideoOutput 拷贝到缓冲队列 videoQueue 中lastVideoCopyNextTime = CMSampleBufferGetDecodeTimeStamp(next)videoQueueSemaphore.wait()let unmanagedSample = Unmanaged.passRetained(next)CMSimpleQueueEnqueue(videoQueue, element: unmanagedSample.toOpaque())let newVideoCount = CMSimpleQueueGetCount(videoQueue)videoQueueSemaphore.signal()}} else {videoEOF = reader.status == .reading || reader.status == .completedshouldContinueLoadingVideo = false// 添加日志,记录视频EOF的设置print("视频EOF标记设置为: \(videoEOF), reader状态: \(reader.status.rawValue)")// 如果视频EOF且没有更多数据,设置demuxer状态为完成if videoEOF && !hasAudioTrack {print("视频处理完成,设置demuxer状态为completed")demuxerStatus = .completed}}} else {shouldContinueLoadingVideo = false}}}// 在函数末尾添加,检查解码是否完成if (audioEOF || !hasAudioTrack) && (videoEOF || !hasVideoTrack) {if demuxerStatus == .running {print("音频和视频均已处理完毕,设置解封装状态为completed")demuxerStatus = .completed}}

我们可以看到,当存在音频和视频轨道的时候,两个EOF必须都变为1,解封装器的状态才会进行改变。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/80867.shtml

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

相关文章

Redis应用--缓存

目录 一、什么是缓存 1.1 二八定律 二、使用Redis作为缓存 三、缓存的更新策略 3.1 定期更新 3.2 实时生成 四、缓存预热、缓存穿透、缓存雪崩和缓存击穿 4.1 缓存预热 4.2 缓存穿透 4.3 缓存雪崩 4.4 缓存击穿 一、什么是缓存 缓存(cache)是计算机的一个经典的概念…

Unity 喷烟喷气特效:喷快消失慢

快速喷气缓慢消失同时播放3*3序列帧动画。常用在火车烟囱特效、气体释放、摔倒、等效果中&#xff0c;可变种应用场景较多。 下列为Particle System参数&#xff1a; Color over Lifetime参数&#xff1a; UnityEditor.GradientWrapperJSON:{"gradient":{"ser…

关于收集 Android Telephony 网络信息的设计思考2

需求: 目标1: Android Telephony data(数据模块)侧收集多源(ServiceStateTracker/ImsService/其他)网络状态信息。目标2: 收集的数据需统一上报/存储到外部App的Provider。字段分散,不方便只在ServiceStateTracker中收集和插入。多触发点/多场景,需要统一插入。一、架构…

Elasticsearch 深入分析三种分页查询【Elasticsearch 深度分页】

前言&#xff1a; 在前面的 Elasticsearch 系列文章中&#xff0c;分享了 Elasticsearch 的各种查询&#xff0c;分页查询也分享过&#xff0c;本篇将再次对 Elasticsearch 分页查询进行专题分析&#xff0c;“深度分页” 这个名词对于我们来说是一个非常常见的业务场景&#…

CodeBuddy(腾讯云代码助手)最新功能——智能体 Craft 体验

文章目录 &#x1f4cb; 前言&#x1f3af; 关于智能体 Craft&#x1f3af; 智能体 Craft 体验&#x1f9e9; 安装方法&#x1f9e9; 如何使用&#x1f9e9; Craft 初体验&#x1f9e9; Craft 生成代码效果体验 &#x1f4dd;最后 &#x1f4cb; 前言 CodeBuddy&#xff08;腾讯…

05 接口自动化-框架封装思想建立之httprunner框架(中)

文章目录 一、httprunner常规的关键字详解httprunner测试用例的结构简化&#xff1a; 二、httpruner接口自动化项目架构三、环境变量四、代码项目框架如下图&#xff1a;api/wx_get_token.ymlapi/wx_get_tag.ymltestcases/wx_get_tag.ymltestsuites/wx_get_tag.yml.env 一、htt…

LabVIEW中EtherCAT从站拓扑离线创建及信息查询

该 VI 主要用于演示如何离线创建 EtherCAT 从站拓扑结构&#xff0c;并查询从站相关信息。EtherCAT&#xff08;以太网控制自动化技术&#xff09;是基于以太网的实时工业通信协议&#xff0c;凭借其高速、高效的特性在自动化领域广泛应用。与其他常见工业通讯协议相比&#xf…

java的synchronized 原理及功能

简介&#xff1a; Java中的synchronized关键字是一种同步机制&#xff0c;用于控制多个线程对共享资源的访问。 原理&#xff1a; 在Java锁有一个内部锁 Intrinsic Lock&#xff0c;也称为监视器锁或管程锁&#xff0c;每个Java对象都有一个关联的监视器锁&#xff0c;隐式锁…

HarmonyOS NEXT端云一体化工程目录结构

视频课程学习报名入口:HarmonyOS NEXT端云一体化开发 端云一体化开发工程由端开发工程(Application)和云开发工程(CloudProgram)两大核心模块构成。 1)端开发工程目录结构 端开发工程主要用于开发应用端侧的业务代码,通用云开发模板的端开发工程目录结构如下图所示: …

普通用户的服务器连接与模型部署相关记录

普通用户的服务器连接与模型部署相关记录 一、从登录到使用自己的conda 1.账号登陆&#xff1a; ssh xxx172.31.226.236 2.下载与安装conda&#xff1a; 下载conda&#xff1a; wget -c https://repo.anaconda.com/archive/Anaconda3-2023.03-1-Linux-x86_64.sh 安装con…

亲测有效!OGG 创建抽取进程报错 OGG-08241,如何解决?

前言 今天在测试 OGG 一个功能的时候&#xff0c;需要重新初始化 oggca&#xff0c;所以重装了一下 OGG。重建完之后重新添加抽取进程报错&#xff0c;一直无法添加成功&#xff1a; 经过一翻分析&#xff0c;找到了解决方案&#xff0c;本文记录一下解决过程。 问题描述 OG…

Docker构建 Dify 应用定时任务助手

概述 Dify 定时任务管理工具是一个基于 GitHub Actions 的自动化解决方案&#xff0c;用于实现 Dify Workflow 的定时执行和状态监控。无需再为缺乏定时任务支持而感到困扰&#xff0c;本工具可以帮助设置自动执行任务并获取实时通知&#xff0c;优化你的工作效率。 注意&…

ubuntu24.04+RTX5090D 显卡驱动安装

初步准备 Ubuntu默认内核太旧&#xff0c;用mainline工具安装新版&#xff1a; sudo add-apt-repository ppa:cappelikan/ppa sudo apt update && sudo apt full-upgrade sudo apt install -y mainline mainline list # 查看可用内核列表 mainline install 6.13 # 安装…

网络爬虫(Web Crawler)详解

网络爬虫(Web Crawler)详解 1. 基本概念与核心目标 定义: 网络爬虫是一种自动化的程序,通过HTTP协议访问网页,提取并存储数据(如文本、链接、图片),并根据策略递归访问新链接。核心目标: 数据采集:抓取特定网站或全网公开数据。索引构建:为搜索引擎提供页面内容(如…

大模型如何助力数学可视化?

大家好&#xff0c;我是 i 学习的老章 在数学学习和教学中&#xff0c;将抽象概念可视化对于理解至关重要。Manim 是一个强大的数学动画引擎&#xff0c;由著名数学科普视频作者 3Blue1Brown 开发并广为人知。 老章较早之前就介绍过 manim&#xff1a;B 站上爆红的数学视频&a…

Oracle基础知识(二)

目录 1.聚合函数 2.COUNT(1)&COUNT(*)&COUNT(字段)区别&#xff08;面试常问&#xff09; 3.分组聚合——group by 4.去重&#xff1a;DISTINCT 、GROUP BY 5.聚合函数的过滤HAVING 6.oracle中having与where的区别 (面试常问) 7.ROUND与TRUNC函数 8.ROLLUP上卷…

DTAS 3D多约束装配助力悬架公差分析尺寸链计算:麦弗逊/双叉臂/多连杆/H臂一网打尽

摘要&#xff1a;汽车四轮定位参数与悬架密切相关。汽车悬架对于车辆的行驶性能、安全性和舒适性至关重要。DTAS 3D提供了各类型悬架的公差仿真分析方法。 关键字&#xff1a;DTAS 3D、前后悬架、公差仿真分析、 运动耦合 一、悬架公差分析综述 悬架是车身&#xff08;或车架…

Serverless爬虫架构揭秘:动态IP、冷启动与成本优化

一、问题背景&#xff1a;旧技术的瓶颈 在传统爬虫架构中&#xff0c;我们通常部署任务在本地机器或虚拟机中&#xff0c;搭配定时器调度任务。虽然这种方式简单&#xff0c;但存在以下明显缺陷&#xff1a; 固定IP易被封禁&#xff1a;目标网站如拼多多会通过IP频率监控限制…

设备预测性维护的停机时间革命:中讯烛龙如何用AI重构工业设备管理范式

在工业4.0的智能化浪潮中&#xff0c;非计划停机每年吞噬企业3%-8%的产值。中讯烛龙预测性维护系统通过多模态感知矩阵分布式智能体的创新架构&#xff0c;实现设备健康管理的范式跃迁&#xff0c;帮助制造企业将停机时间压缩70%以上。本文将深度解析技术实现路径与行业级实践方…

Java面试攻略:从Spring Boot到微服务架构的深入探讨

Java面试攻略&#xff1a;从Spring Boot到微服务架构的深入探讨 场景设定 在一家知名互联网大厂的会议室里&#xff0c;资深面试官王老师正在对一位求职者谢飞机进行技术面试。谢飞机是一位幽默风趣的程序员&#xff0c;他的回答有时让人捧腹大笑。 第一轮&#xff1a;核心技…