当前位置: 首页 > news >正文

iOS音视频解封装分析

首先是进行解封装的简单的配置

/// 解封装配置
class KFDemuxerConfig {// 媒体资源var asset: AVAsset?// 解封装类型,指定是音频、视频或两者都需要var demuxerType: KFMediaType = .avinit() {}
}

然后是实现解封装控制器

import Foundation
import CoreMedia
import AVFoundation// 解封装器状态枚举
enum KFMP4DemuxerStatus: Int {case unknown = 0case running = 1case failed = 2case completed = 3case cancelled = 4
}// 错误码常量
private let KFMP4DemuxerBadFileError = 2000
private let KFMP4DemuxerAddVideoOutputError = 2001
private let KFMP4DemuxerAddAudioOutputError = 2002
private let KFMP4DemuxerQueueMaxCount = 3class KFMP4Demuxer {// MARK: - 属性let config: KFDemuxerConfigvar errorCallBack: ((Error) -> Void)?// 媒体信息属性private(set) var hasAudioTrack = false  // 是否包含音频数据private(set) var hasVideoTrack = false  // 是否包含视频数据private(set) var videoSize = CGSize.zero  // 视频大小private(set) var duration = CMTime.zero  // 媒体时长private(set) var codecType = CMVideoCodecType(0)  // 编码类型private(set) var demuxerStatus = KFMP4DemuxerStatus.unknown  // 解封装器状态private(set) var audioEOF = false  // 是否音频结束private(set) var videoEOF = false  // 是否视频结束private(set) var preferredTransform = CGAffineTransform.identity  // 图像的变换信息// 解封装相关private var demuxReader: AVAssetReader?  // 解封装器实例private var readerAudioOutput: AVAssetReaderTrackOutput?  // Demuxer 的音频输出private var readerVideoOutput: AVAssetReaderTrackOutput?  // Demuxer 的视频输出// 队列和同步private let demuxerQueue: DispatchQueueprivate let demuxerSemaphore: DispatchSemaphoreprivate let audioQueueSemaphore: DispatchSemaphoreprivate let videoQueueSemaphore: DispatchSemaphore// 数据队列private var audioQueue: CMSimpleQueueprivate var videoQueue: CMSimpleQueue// 时间戳private var lastAudioCopyNextTime = CMTime.zero  // 上一次拷贝的音频采样的时间戳private var lastVideoCopyNextTime = CMTime.zero  // 上一次拷贝的视频采样的时间戳// MARK: - 生命周期init(config: KFDemuxerConfig) {self.config = configself.demuxerSemaphore = DispatchSemaphore(value: 1)self.audioQueueSemaphore = DispatchSemaphore(value: 1)self.videoQueueSemaphore = DispatchSemaphore(value: 1)self.demuxerStatus = .unknownself.demuxerQueue = DispatchQueue(label: "com.KeyFrameKit.demuxerQueue", attributes: [])// 创建音频和视频缓冲队列var audioQueueRef: CMSimpleQueue? = nilvar videoQueueRef: CMSimpleQueue? = nilCMSimpleQueueCreate(allocator: kCFAllocatorDefault, capacity: Int32(KFMP4DemuxerQueueMaxCount), queueOut: &audioQueueRef)CMSimpleQueueCreate(allocator: kCFAllocatorDefault, capacity: Int32(KFMP4DemuxerQueueMaxCount), queueOut: &videoQueueRef)self.audioQueue = audioQueueRef!self.videoQueue = videoQueueRef!}deinit {// 清理状态机if demuxerStatus == .running {demuxerStatus = .cancelled}// 清理解封装器实例demuxerSemaphore.wait()if let reader = demuxReader, reader.status == .reading {reader.cancelReading()}demuxerSemaphore.signal()// 清理音频数据队列audioQueueSemaphore.wait()while CMSimpleQueueGetCount(audioQueue) > 0 {if let item = CMSimpleQueueDequeue(audioQueue) {// 释放队列中的对象Unmanaged<CMSampleBuffer>.fromOpaque(item).release()}}audioQueueSemaphore.signal()// 清理视频数据队列videoQueueSemaphore.wait()while CMSimpleQueueGetCount(videoQueue) > 0 {if let item = CMSimpleQueueDequeue(videoQueue) {// 释放队列中的对象Unmanaged<CMSampleBuffer>.fromOpaque(item).release()}}videoQueueSemaphore.signal()}// MARK: - 公共方法func startReading(completionHandler: @escaping (Bool, Error?) -> Void) {weak var weakSelf = selfdemuxerQueue.async {guard let self = weakSelf else { return }self.demuxerSemaphore.wait()// 在第一次开始读数据时,创建解封装器实例if self.demuxReader == nil {var error: Error? = nilself.setupDemuxReader(&error)self.audioEOF = !self.hasAudioTrackself.videoEOF = !self.hasVideoTrackself.demuxerStatus = error != nil ? .failed : .runningself.demuxerSemaphore.signal()DispatchQueue.main.async {completionHandler(error == nil, error)}return}self.demuxerSemaphore.signal()}}func cancelReading() {weak var weakSelf = selfdemuxerQueue.async {guard let self = weakSelf else { return }self.demuxerSemaphore.wait()// 取消读数据if let reader = self.demuxReader, reader.status == .reading {reader.cancelReading()}self.demuxerStatus = .cancelledself.demuxerSemaphore.signal()}}func hasAudioSampleBuffer() -> Bool {// 是否还有音频数据if hasAudioTrack && demuxerStatus == .running && !audioEOF {var audioCount: Int32 = 0audioQueueSemaphore.wait()if CMSimpleQueueGetCount(audioQueue) > 0 {audioCount = CMSimpleQueueGetCount(audioQueue)}audioQueueSemaphore.signal()return (audioCount == 0 && audioEOF) ? false : true}return false}func copyNextAudioSampleBuffer() -> CMSampleBuffer? {// 拷贝下一份音频采样var sampleBuffer: CMSampleBuffer? = nilwhile sampleBuffer == nil && demuxerStatus == .running && !audioEOF {// 先从缓冲队列取数据audioQueueSemaphore.wait()if CMSimpleQueueGetCount(audioQueue) > 0 {if let item = CMSimpleQueueDequeue(audioQueue) {sampleBuffer = Unmanaged<CMSampleBuffer>.fromOpaque(item).takeRetainedValue()}}audioQueueSemaphore.signal()// 缓冲队列没有数据,就同步加载一下试试if sampleBuffer == nil && demuxerStatus == .running {syncLoadNextSampleBuffer()}}// 异步加载一下,先缓冲到数据队列中,等下次取asyncLoadNextSampleBuffer()return sampleBuffer}func hasVideoSampleBuffer() -> Bool {// 是否还有视频数据if hasVideoTrack && demuxerStatus == .running && !videoEOF {var videoCount: Int32 = 0videoQueueSemaphore.wait()if CMSimpleQueueGetCount(videoQueue) > 0 {videoCount = CMSimpleQueueGetCount(videoQueue)}videoQueueSemaphore.signal()return (videoCount == 0 && videoEOF) ? false : true}return false}func copyNextVideoSampleBuffer() -> CMSampleBuffer? {// 拷贝下一份视频采样var sampleBuffer: CMSampleBuffer? = nilwhile sampleBuffer == nil && demuxerStatus == .running && !videoEOF {// 先从缓冲队列取数据videoQueueSemaphore.wait()if CMSimpleQueueGetCount(videoQueue) > 0 {if let item = CMSimpleQueueDequeue(videoQueue) {sampleBuffer = Unmanaged<CMSampleBuffer>.fromOpaque(item).takeRetainedValue()}}videoQueueSemaphore.signal()// 缓冲队列没有数据,就同步加载一下试试if sampleBuffer == nil && demuxerStatus == .running {syncLoadNextSampleBuffer()}}// 异步加载一下,先缓冲到数据队列中,等下次取asyncLoadNextSampleBuffer()return sampleBuffer}// MARK: - 私有方法private func setupDemuxReader(_ error: inout Error?) {guard let asset = config.asset else {error = NSError(domain: String(describing: type(of: self)), code: 40003, userInfo: nil)return}// 1、创建解封装器实例// 使用 AVAssetReader 作为解封装器。解封装的目标是 config 中的 AVAsset 资源do {demuxReader = try AVAssetReader(asset: asset)} catch let readerError {error = readerErrorreturn}// 2、获取时间信息duration = asset.duration// 3、处理待解封装的资源中的视频if config.demuxerType.contains(.video) {// 取出视频轨道guard let videoTrack = asset.tracks(withMediaType: .video).first else {hasVideoTrack = falsereturn}hasVideoTrack = true// 获取图像变换信息preferredTransform = videoTrack.preferredTransform// 获取图像大小。要应用上图像变换信息videoSize = CGSizeApplyAffineTransform(videoTrack.naturalSize, videoTrack.preferredTransform)videoSize = CGSize(width: abs(videoSize.width), height: abs(videoSize.height))// 获取编码格式guard let formatDesc = videoTrack.formatDescriptions.first else { return }let formatDescription = formatDesc as! CMFormatDescriptioncodecType = CMFormatDescriptionGetMediaSubType(formatDescription)// 基于轨道创建视频输出readerVideoOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: nil)readerVideoOutput?.alwaysCopiesSampleData = false // 避免总是做数据拷贝,影响性能// 给解封装器绑定视频输出guard let videoOutput = readerVideoOutput, let reader = demuxReader, reader.canAdd(videoOutput) else {error = demuxReader?.error ?? NSError(domain: String(describing: type(of: self)), code: KFMP4DemuxerAddVideoOutputError, userInfo: nil)return}reader.add(videoOutput)}// 4、处理待解封装的资源中的音频if config.demuxerType.contains(.audio) {// 取出音频轨道guard let audioTrack = asset.tracks(withMediaType: .audio).first else {hasAudioTrack = falsereturn}hasAudioTrack = true// 基于轨道创建音频输出readerAudioOutput = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: nil)readerAudioOutput?.alwaysCopiesSampleData = false // 避免总是做数据拷贝,影响性能// 给解封装器绑定音频输出guard let audioOutput = readerAudioOutput, let reader = demuxReader, reader.canAdd(audioOutput) else {error = demuxReader?.error ?? NSError(domain: String(describing: type(of: self)), code: KFMP4DemuxerAddAudioOutputError, userInfo: nil)return}reader.add(audioOutput)}// 5、音频和视频数据都没有,就报错if !hasVideoTrack && !hasAudioTrack {error = NSError(domain: String(describing: type(of: self)), code: KFMP4DemuxerBadFileError, userInfo: nil)return}// 6、启动解封装guard let reader = demuxReader, reader.startReading() else {error = demuxReader?.errorreturn}}private func asyncLoadNextSampleBuffer() {// 异步加载下一份采样数据weak var weakSelf = selfdemuxerQueue.async {guard let self = weakSelf else { return }self.demuxerSemaphore.wait()self.loadNextSampleBuffer()self.demuxerSemaphore.signal()}}private func syncLoadNextSampleBuffer() {// 同步加载下一份采样数据demuxerSemaphore.wait()loadNextSampleBuffer()demuxerSemaphore.signal()}/// 把解封装的数据加载到缓冲队列中private func loadNextSampleBuffer() {guard demuxerStatus == .running else { print("KFMP4Demuxer - loadNextSampleBuffer: 当前状态非运行中,状态=\(demuxerStatus)")return }// 1、根据解封装器的状态,处理异常情况if let reader = demuxReader {switch reader.status {case .completed:print("KFMP4Demuxer - 解封装已完成")demuxerStatus = .completedreturncase .failed:print("KFMP4Demuxer - 解封装失败: \(String(describing: reader.error))")if let nsError = reader.error as NSError?, nsError.code == AVError.operationInterrupted.rawValue {print("KFMP4Demuxer - 操作被中断,尝试恢复")// 如果当前解封装器的状态是被打断而失败,就尝试重新创建一下var error: Error? = nilsetupDemuxReader(&error)if error == nil {print("KFMP4Demuxer - 恢复成功,重新启动解封装器")// 同时做一下恢复处理resumeLastTime()} else {print("KFMP4Demuxer - 恢复失败: \(String(describing: error))")}}if reader.status == .failed {// 如果状态依然是失败,就上报错误print("KFMP4Demuxer - 解封装器状态仍为失败")demuxerStatus = .failedif let error = reader.error, let callback = errorCallBack {print("KFMP4Demuxer - 调用错误回调: \(error)")DispatchQueue.main.async {callback(error)}}return}case .cancelled:// 如果状态是取消,就直接 returnprint("KFMP4Demuxer - 解封装已取消")demuxerStatus = .cancelledreturndefault:print("KFMP4Demuxer - 解封装器状态: \(reader.status.rawValue)")break}} else {print("KFMP4Demuxer - demuxReader为nil")}// 2、解封装器状态正常,加载下一份采样数据let audioNeedLoad = config.demuxerType.contains(.audio) && !audioEOFlet videoNeedLoad = config.demuxerType.contains(.video) && !videoEOFvar shouldContinueLoadingAudio = audioNeedLoadvar shouldContinueLoadingVideo = videoNeedLoadprint("KFMP4Demuxer - 需要加载: 音频=\(audioNeedLoad), 视频=\(videoNeedLoad)")var loadCount = 0while let reader = demuxReader, reader.status == .reading && (shouldContinueLoadingAudio || shouldContinueLoadingVideo) {loadCount += 1if loadCount > 100 {print("KFMP4Demuxer - 加载循环次数过多,退出循环")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调用print("KFMP4Demuxer - 音频帧没有数据缓冲区")} else {// 将数据从音频输出源 readerAudioOutput 拷贝到缓冲队列 audioQueue 中lastAudioCopyNextTime = CMSampleBufferGetPresentationTimeStamp(next)audioQueueSemaphore.wait()let unmanagedSample = Unmanaged.passRetained(next)CMSimpleQueueEnqueue(audioQueue, element: unmanagedSample.toOpaque())let newAudioCount = CMSimpleQueueGetCount(audioQueue)audioQueueSemaphore.signal()print("KFMP4Demuxer - 加载音频帧,时间戳: \(CMTimeGetSeconds(lastAudioCopyNextTime))秒,队列中帧数: \(newAudioCount)")}} else {audioEOF = reader.status == .reading || reader.status == .completedshouldContinueLoadingAudio = falseprint("KFMP4Demuxer - 音频数据读取结束,EOF=\(audioEOF)")}} else {shouldContinueLoadingAudio = falseif audioCount >= KFMP4DemuxerQueueMaxCount {print("KFMP4Demuxer - 音频队列已满: \(audioCount)")} else {print("KFMP4Demuxer - 音频输出源不可用")}}}// 加载视频数据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调用print("KFMP4Demuxer - 视频帧没有数据缓冲区")} else {// 将数据从视频输出源 readerVideoOutput 拷贝到缓冲队列 videoQueue 中lastVideoCopyNextTime = CMSampleBufferGetDecodeTimeStamp(next)videoQueueSemaphore.wait()let unmanagedSample = Unmanaged.passRetained(next)CMSimpleQueueEnqueue(videoQueue, element: unmanagedSample.toOpaque())let newVideoCount = CMSimpleQueueGetCount(videoQueue)videoQueueSemaphore.signal()print("KFMP4Demuxer - 加载视频帧,时间戳: \(CMTimeGetSeconds(lastVideoCopyNextTime))秒,队列中帧数: \(newVideoCount)")}} else {videoEOF = reader.status == .reading || reader.status == .completedshouldContinueLoadingVideo = falseprint("KFMP4Demuxer - 视频数据读取结束,EOF=\(videoEOF)")}} else {shouldContinueLoadingVideo = falseif videoCount >= KFMP4DemuxerQueueMaxCount {print("KFMP4Demuxer - 视频队列已满: \(videoCount)")} else {print("KFMP4Demuxer - 视频输出源不可用")}}}}print("KFMP4Demuxer - 加载完成,加载循环次数: \(loadCount)")}private func resumeLastTime() {// 对于异常中断后的处理,需要根据记录的时间戳 lastAudioCopyNextTime/lastVideoCopyNextTime 做恢复操作print("开始恢复解封装,上次音频时间: \(CMTimeGetSeconds(lastAudioCopyNextTime))秒, 上次视频时间: \(CMTimeGetSeconds(lastVideoCopyNextTime))秒")let audioNeedLoad = lastAudioCopyNextTime.value > 0 && !audioEOFlet videoNeedLoad = lastVideoCopyNextTime.value > 0 && !videoEOFprint("需要恢复音频: \(audioNeedLoad), 需要恢复视频: \(videoNeedLoad)")var shouldContinueLoadingAudio = audioNeedLoadvar shouldContinueLoadingVideo = videoNeedLoadwhile let reader = demuxReader, reader.status == .reading && (shouldContinueLoadingAudio || shouldContinueLoadingVideo) {if shouldContinueLoadingAudio, let audioOutput = readerAudioOutput {// 从音频输出源读取音频数据if let next = audioOutput.copyNextSampleBuffer() {if CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(next)) <= CMTimeGetSeconds(lastAudioCopyNextTime) || CMSampleBufferGetDataBuffer(next) == nil {// 从输出源取出的数据时间戳小于上次标记的时间,则表示这份采样数据已经处理过了// 移除了CFRelease调用print("跳过已处理的音频帧,时间戳: \(CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(next)))秒")} else {print("找到恢复点后的音频帧,时间戳: \(CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(next)))秒")audioQueueSemaphore.wait()let unmanagedSample = Unmanaged.passRetained(next)CMSimpleQueueEnqueue(audioQueue, element: unmanagedSample.toOpaque())audioQueueSemaphore.signal()shouldContinueLoadingAudio = false}} else {audioEOF = reader.status == .reading || reader.status == .completedprint("音频恢复到达EOF: \(audioEOF)")shouldContinueLoadingAudio = false}}if shouldContinueLoadingVideo, let videoOutput = readerVideoOutput {// 从视频输出源读取视频数据if let next = videoOutput.copyNextSampleBuffer() {if CMTimeGetSeconds(CMSampleBufferGetDecodeTimeStamp(next)) <= CMTimeGetSeconds(lastVideoCopyNextTime) || CMSampleBufferGetDataBuffer(next) == nil {// 从输出源取出的数据时间戳小于上次标记的时间,则表示这份采样数据已经处理过了// 移除了CFRelease调用print("跳过已处理的视频帧,时间戳: \(CMTimeGetSeconds(CMSampleBufferGetDecodeTimeStamp(next)))秒")} else {print("找到恢复点后的视频帧,时间戳: \(CMTimeGetSeconds(CMSampleBufferGetDecodeTimeStamp(next)))秒")videoQueueSemaphore.wait()let unmanagedSample = Unmanaged.passRetained(next)CMSimpleQueueEnqueue(videoQueue, element: unmanagedSample.toOpaque())videoQueueSemaphore.signal()shouldContinueLoadingVideo = false}} else {videoEOF = reader.status == .reading || reader.status == .completedprint("视频恢复到达EOF: \(videoEOF)")shouldContinueLoadingVideo = false}}}print("恢复过程完成")}
} 

上面是 KFMP4Demuxer 的实现,从代码上可以看到主要有这几个部分:

1)创建解封装器实例及对应的音频和视频数据输出源。第一次调用 -startReading: 时会创建解封装器实例,另外在 -_loadNextSampleBuffer 时如果发现当前解封装器的状态是被打断而失败时,会尝试重新创建解封装器实例。

  • -_setupDemuxReader: 方法中实现。音频和视频的输出源分别是 readerAudioOutputreaderVideoOutput

2)用两个队列作为缓冲区,分别管理音频和视频解封装后的数据。

  • 这两个队列分别是 _audioQueue_videoQueue

  • 当外部向解封装器要数据而触发数据加载时,会把解封装后的数据先缓存到这两个队列中,缓冲的采样数不超过 KFMP4DemuxerQueueMaxCount,以减少内存占用。

  • 3)从音视频输出源读取数据。

  • 核心逻辑在 -_loadNextSampleBuffer 方法中实现:从输出源 readerAudioOutputreaderVideoOutput 读取数据放入缓冲区队列 _audioQueue_videoQueue

  • 在外部调用 -copyNextAudioSampleBuffer-copyNextVideoSampleBuffer 时,触发读取数据。

4)从中断中恢复解封装。

  • -_resumeLastTime 方法中实现。

5)停止解封装。

  • -cancelReading 方法中实现。

6)解封装状态机管理。

  • 在枚举 KFMP4DemuxerStatus 中定义了解封装器的各种状态,对于解封装器的状态机管理贯穿在解封装的整个过程中。

7)错误回调。

  • -callBackError: 方法向外回调错误。

8)清理封装器实例及数据缓冲区。

  • -deinit 方法中实现。

接下来来分析一下调用过程

初始化阶段

  1. KFVideoDemuxerViewController初始化
  • 创建demuxerConfig:设置视频路径和解封装类型

  • 创建KFMP4Demuxer实例:传入demuxerConfig并设置错误回调

启动阶段(点击"Start"按钮)

  1. 调用start()方法
  • 检查asset是否存在

  • 验证视频轨道信息

  • 调用demuxer.startReading()方法

  1. KFMP4Demuxer的startReading()
  • 在demuxerQueue队列中异步执行

  • 首次调用时创建解封装器实例(setupDemuxReader)

  1. setupDemuxReader流程
  • 检查asset有效性

  • 创建AVAssetReader实例

  • 获取媒体时间信息

  • 处理视频轨道:

  • 获取视频轨道、格式和尺寸信息

  • 创建视频输出(AVAssetReaderTrackOutput)

  • 添加视频输出到解封装器

  • 处理音频轨道(如果需要)

  • 启动AVAssetReader开始读取

  1. startReading完成回调
  • 成功时调用fetchAndSaveDemuxedData()

  • 失败时输出错误信息

数据处理阶段

  1. fetchAndSaveDemuxedData()
  • 在全局队列异步执行

  • 循环调用demuxer.hasVideoSampleBuffer()和copyNextVideoSampleBuffer()

  • 对每个采样缓冲区调用saveSampleBuffer()

  1. 解封装数据读取流程
  • hasVideoSampleBuffer:检查是否还有视频数据可读

  • copyNextVideoSampleBuffer:

  • 从视频队列获取采样缓冲区

  • 如果队列为空,调用syncLoadNextSampleBuffer()同步加载

  • 加载完成后调用asyncLoadNextSampleBuffer()异步准备下一批数据

  1. 加载采样数据(loadNextSampleBuffer)
  • 检查解封装器状态,处理异常情况

  • 从AVAssetReaderTrackOutput读取视频数据

  • 将数据存入缓冲队列(videoQueue)

保存阶段

  1. saveSampleBuffer()处理视频帧
  • 调用isKeyFrame()判断是否为关键帧

  • 关键帧时通过getPacketExtraData()获取编码参数(SPS/PPS/VPS)

  • 将AVCC格式(长度前缀)转换为Annex-B格式(0x00000001分隔符)

  • 写入文件(fileHandle)

整个过程是一个异步的数据流:从MP4文件解封装→读取视频帧→转换格式→写入文件。主要瓶颈和关键点在于解封装过程和数据格式转换。

最后是关于异常中断的验证

    // MARK: - 测试恢复功能@objc private func testResumeFunction() {print("====== 开始测试resumeLastTime功能 ======")// 收集测试前信息var framesBeforeInterruption: [CMTime] = []var framesAfterResume: [CMTime] = []demuxer.startReading { [weak self] success, error inguard success, let self = self else { print("解封装器启动失败")return }// 收集中断前的5帧print("开始收集中断前帧")for _ in 0..<5 {if let sample = self.demuxer.copyNextVideoSampleBuffer() {let time = CMSampleBufferGetPresentationTimeStamp(sample)framesBeforeInterruption.append(time)print("中断前帧,时间戳: \(CMTimeGetSeconds(time))秒")} else {print("无法获取中断前帧")}}// 模拟中断print("模拟解封装中断...")self.simulateInterruption()// 等待恢复机制生效,增加等待时间print("等待恢复机制生效...")DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {print("开始尝试恢复后读取")// 先检查状态if self.demuxer.hasVideoSampleBuffer() {print("恢复后还有视频数据可读")} else {print("警告:恢复后没有视频数据可读")}// 强制触发一次loadNextSampleBuffer,通过读取帧来触发恢复机制print("强制触发恢复机制")_ = self.demuxer.copyNextVideoSampleBuffer()// 增加等待时间,确保恢复完成DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {// 收集恢复后的帧print("收集恢复后的帧")for i in 0..<10 { // 增加尝试帧数if let sample = self.demuxer.copyNextVideoSampleBuffer() {let time = CMSampleBufferGetPresentationTimeStamp(sample)framesAfterResume.append(time)print("恢复后帧\(i+1),时间戳: \(CMTimeGetSeconds(time))秒")} else {print("无法获取恢复后帧\(i+1)")}}// 验证恢复效果self.validateResume(beforeFrames: framesBeforeInterruption, afterFrames: framesAfterResume)}}}}

在这里插入图片描述

为什么 KFMP4Demuxer 不像前面的 Demo 中设计的 KFAudioCaptureKFAudioEncoder 的接口那样,有一个解封装后的数据回调接口。主要是因为解封装的速度是非常快的,不会成为一个音视频 pipeline 的瓶颈,而且考虑到解封装的资源可能会很大,所以一般不会一直不停地解出数据往外抛,这样下一个处理节点可能处理不过来这些数据。基于这些原因,解封装器的接口设计是让外部调用方主动找解封装器要数据来触发解封装操作,并且还要控制一定的缓存量防止内存占用过大。

相关文章:

iOS音视频解封装分析

首先是进行解封装的简单的配置 /// 解封装配置 class KFDemuxerConfig {// 媒体资源var asset: AVAsset?// 解封装类型&#xff0c;指定是音频、视频或两者都需要var demuxerType: KFMediaType .avinit() {} }然后是实现解封装控制器 import Foundation import CoreMedia i…...

探究电阻分压的带负载能力

我们经常使用两个电阻去分压来获得特定的电压,那么我是两个大阻值电阻分压获得的电压驱动能力强,还是小阻值电阻分压得到的电压驱动能力强呢? 一、电压相同时,电流的大小 下面是两个阻值分压得到的仿真图 电路分析: VCC都是5V,探针1和探针2测到的电压都是1.67V; 根据…...

14、Python时间表示:Unix时间戳、毫秒微秒精度与time模块实战

适合人群&#xff1a;零基础自学者 | 编程小白快速入门 阅读时长&#xff1a;约5分钟 文章目录 一、问题&#xff1a;计算机中的时间的表示、Unix时间点&#xff1f;1、例子1&#xff1a;计算机的“生日”&#xff1a;Unix时间点2、答案&#xff1a;&#xff08;1&#xff09;U…...

PCL 绘制二次曲面

文章目录 一、简介二、实现代码三、实现效果一、简介 这里基于二次曲面的公式: z = a 0 + a 1 x + a 2 y + a...

消息队列与Kafka基础:从概念到集群部署

目录 一、消息队列 1.什么是消息队列 2.消息队列的特征 3.为什么需要消息队列 二、Kafka基础与入门 1.Kafka基本概念 2.Kafka相关术语 3.Kafka拓扑架构 4.Topic与partition 5.Producer生产机制 6.Consumer消费机制 三、Zookeeper概念介绍 1.zookeeper概述 2.zooke…...

计算机指令分类和具体的表示的方式

1.关于计算机的指令系统 下面的这个就是我们的一个简单的计算机里面涉及到的指令&#xff1a; m就是我们的存储器里面的地址&#xff0c;可以理解为memory这个意思&#xff0c;r可以理解为rom这样的单词的首字母&#xff0c;帮助我们去进行这个相关的指令的记忆&#xff0c;不…...

pcie phy-电气层-gen1/2(TX)

S IP物理层讲解 在synopsys IP中对于phy层的内容分离的比较多&#xff1a; cxpl中&#xff1a; u_cx_phy_logical&#xff1a;包含ts序列的解析&#xff08;smlh&#xff09;&#xff1b; pipe层协议的转换&#xff08;rmlh,xmlh)&#xff1b;pipe转dllp包&#xff08;rplh&…...

Baklib加速企业AI数据智理转型

Baklib智理AI数据资产 在AI技术深度渗透业务场景的背景下&#xff0c;Baklib通过构建企业级知识中台架构&#xff0c;重塑了数据资产的治理范式。该平台采用智能分类引擎与语义分析模型&#xff0c;将分散在邮件、文档、数据库中的非结构化数据转化为标准化的知识单元&#xf…...

深度学习驱动下的目标检测技术:原理、算法与应用创新

一、引言​ 1.1 研究背景与意义​ 目标检测作为计算机视觉领域的核心任务之一&#xff0c;旨在识别图像或视频中感兴趣目标的类别&#xff0c;并确定其在图像中的位置&#xff0c;通常以边界框&#xff08;Bounding Box&#xff09;的形式表示 。其在现实生活中有着极为广泛且…...

window 显示驱动开发-使用有保证的协定 DMA 缓冲区模型

Windows Vista 的显示驱动程序模型保证呈现设备的 DMA 缓冲区和修补程序位置列表的大小。 修补程序位置列表包含 DMA 缓冲区中命令引用的资源的物理内存地址。 在有保证的协定模式下&#xff0c;用户模式显示驱动程序知道 DMA 缓冲区和修补程序位置列表的确切大小&#xff0c;…...

《指针与整数相加减的深入解析》

&#x1f680;个人主页&#xff1a;BabyZZの秘密日记 &#x1f4d6;收入专栏&#xff1a;C语言 &#x1f30d;文章目入 一、指针与整数相加的原理二、指针与整数相减的原理三、使用场景&#xff08;一&#xff09;数组操作&#xff08;二&#xff09;内存遍历 四、注意事项&…...

C++_STL_map与set

1. 关联式容器 在初阶阶段&#xff0c;我们已经接触过STL中的部分容器&#xff0c;比如&#xff1a;vector、list、deque、 forward_list(C11)等&#xff0c;这些容器统称为序列式容器&#xff0c;因为其底层为线性序列的数据结构&#xff0c;里面 存储的是元素本身。那什么是…...

1949-2022年各省农作物播种面积数据(22个指标)

1949-2022年各省农作物播种面积数据&#xff08;22个指标&#xff09; 1、时间&#xff1a;1949-2022年 2、来源&#xff1a;各省年鉴、国家统计局、农业部、农业年鉴 3、范围&#xff1a;31省 4、指标&#xff1a;年度标识、省份编码、省份名称、农作物总播种面积、粮食作…...

汽车二自由度系统模型以及电动助力转向系统模型

汽车二自由度系统模型与电动助力转向系统&#xff08;EPS&#xff09;的详细建模方案&#xff0c;包含理论推导、MATLAB/Simulink实现代码及参数说明&#xff1a; 一、二自由度汽车模型 1. 模型描述 包含以下两个自由度&#xff1a; 横向运动&#xff08;侧向加速度&#xf…...

【学习笔记】计算机操作系统(四)—— 存储器管理

第四章 存储器管理 文章目录 第四章 存储器管理4.1 存储器的层次结构4.1.1 多层结构的存储器系统4.1.2 主存储器与寄存器4.1.3 高速缓存和磁盘缓存 4.2 程序的装入和链接4.2.1 程序的装入4.2.2 程序的链接 4.3 连续分配存储管理方式4.3.1 单一连续分配4.3.2 固定分区分配4.3.3 …...

51单片机的lcd12864驱动程序

#include <reg51.h> #include <intrins.h>#define uchar...

(03)数字化转型之库存管理:从进库到出库的数字化运营

在当今竞争激烈的商业环境中&#xff0c;高效的库存管理已成为企业降低成本、提高运营效率的关键。本文将系统性地介绍库存管理的全流程&#xff0c;包括进库、出库、移库、盘点等核心环节&#xff0c;帮助企业构建科学合理的库存管理体系。 一、进库管理&#xff1a;从计划到执…...

windows编程中加载DLL的两种典型方式的比较

文章目录 DLL定义头文件定义CPP实现DLL的调用代码直接使用通过LoadLibrary调用导入表的依赖LoadLibrary使用DLL库中的类DLL中定义工厂函数调用时的代码补充:为什么LoadLibrary不能直接导出类在windows的编程中,使用DLL是一个非常常见的操作。一般来说,有两种集成DLL的方式:…...

存储器上如何存储1和0

在计算机存储器中&#xff0c;数据最终以**二进制形式&#xff08;0和1&#xff09;**存储&#xff0c;这是由硬件特性和电子电路的物理特性决定的。以下是具体存储方式的详细解析&#xff1a; 一、存储的物理基础&#xff1a;半导体电路与电平信号 计算机存储器&#xff08;…...

【笔记】记一次PyCharm的问题反馈

#工作记录 最近更新至 PyCharm 社区版的最新版本后&#xff0c;我遇到了多个影响使用体验的问题。令人感到不便的是&#xff0c;一些在旧版本中非常便捷的功能&#xff0c;在新版本中却变得操作复杂、不够直观。过去&#xff0c;我一直通过 PyCharm 内置的故障报告与反馈机制反…...

logrotate按文件大小进行日志切割

✅ 编写logrotate文件&#xff0c;进行自定义切割方式 adminip-127-0-0-1:/data/test$ cat /etc/logrotate.d/test /data/test/test.log {size 1024M #文件达到1G就切割rotate 100 #保留100个文件compressdelaycompressmissingoknotifemptycopytruncate #这个情况服务不用…...

基于大模型的脑出血智能诊疗与康复技术方案

目录 一、术前阶段1.1 数据采集与预处理系统伪代码实现流程图1.2 特征提取与选择模块伪代码实现流程图1.3 大模型风险评估系统伪代码实现流程图二、术中阶段2.1 智能手术规划系统伪代码实现流程图2.2 麻醉智能监控系统伪代码实现流程图三、术后阶段3.1 并发症预测系统伪代码片段…...

P21-RNN-心脏病预测

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 一、RNN 循环神经网络&#xff08;Recurrent Neural Network&#xff0c;简称 RNN&#xff09;是一类以序列数据为输入&#xff0c;在序列的演进方向进行递归…...

懒汉式单例模式的线程安全实现

懒汉式单例模式的线程安全实现 懒汉式单例模式的核心特点是延迟实例化(在第一次使用时创建对象),但其基础实现存在线程安全问题。以下是不同线程安全实现方式的详细说明和对比: 1. 非线程安全的基础懒汉式 public class UnsafeLazySingleton {private static UnsafeLazyS…...

Java 常用的Arrays函数

文章目录 ArrayssorttoStringbinarySearchequalsfill 数组拷贝copyOfcopyOfRangearraycopy 二维数组定义遍历deepToString空指针异常 Arrays sort int[] array new int[]{1,20,3}; Arrays.sort(array);// 1 3 20toString 帮助数组转为字符串 int[] array new int[]{1,2,3…...

FEKO许可证与版本兼容性问题

随着电磁仿真技术的不断进步&#xff0c;FEKO软件不断更新迭代&#xff0c;为用户提供更强大的功能和更优秀的性能。然而&#xff0c;在升级过程中&#xff0c;FEKO许可证与版本兼容性问题往往成为用户关注的焦点。本文将为您详细解读FEKO许可证与版本兼容性问题&#xff0c;帮…...

HarmonyOs开发之——— ArkWeb 实战指南

HarmonyOs开发之——— ArkWeb 实战指南 谢谢关注!! 前言:上一篇文章主要介绍HarmonyOs开发之———合理使用动画与转场:CSDN 博客链接 一、ArkWeb 组件基础与生命周期管理 1.1 Web 组件核心能力概述 ArkWeb 的Web组件支持加载本地或在线网页,提供完整的生命周期回调体…...

冰箱磁力贴认证标准16CFR1262

在亚马逊平台&#xff0c;冰箱磁力贴这类可能被儿童接触到的产品&#xff0c;有着严格的规范哦。必须得遵守 16 CFR 1262 标准&#xff0c;还得有符合该标准的测试报告和 GCC 证书&#xff0c;不然产品就可能被禁止销售或者面临召回&#xff0c;那可就损失大啦&#xff01; ​ …...

Java中的锁机制全解析:从synchronized到分布式锁

在多线程编程中&#xff0c;锁是保证线程安全的核心工具。本文将详解Java中常见的锁机制及其实际应用场景&#xff0c;帮助开发者选择最合适的锁方案。 一、内置锁&#xff1a;synchronized 原理 通过JVM内置的监视器锁&#xff08;Monitor&#xff09;实现&#xff0c;可修…...

OptiStruct实例:3D实体转子分析

上一节介绍了1D转子的临界转速分析。在1D转子模型中&#xff0c;转子是以集中质量单元的形式建模的。此种建模方法不可避免地会带来一些简化和局部特征的缺失。接下来介绍OptiStruct3D实体转子的建模及临界转速分析实例。 3D实体转子建立详细的转子网格模型&#xff0c;然后将…...

简单记录坐标变换

以三维空间坐标系为例 rTt代表机械手末端相对robot root坐标系的变换关系 rTt dot p_in_tool 可以把tool坐标系下表示的某点转到root坐标系表示 其中rTt表示tool相对于root坐标系的平移和旋转 以二维图像坐标系为例说明 1坐标系定为图片坐标系左上角&#xff0c;横平竖直的…...

自定义快捷键软件:AutoHotkey 高效的快捷键执行脚本软件

AutoHotkey 是一种适用于 Windows 的免费开源脚本语言&#xff0c;它允许用户轻松创建从小型到复杂的脚本&#xff0c;用于各种任务&#xff0c;例如&#xff1a;表单填充、自动点击、宏等。 定义鼠标和键盘的热键&#xff0c;重新映射按键或按钮&#xff0c;并进行类似自动更…...

【Android构建系统】了解Soong构建系统

背景介绍 在Android7.0之前&#xff0c;Android使用GNU Make描述和执行build规则。Android7.0引入了Soong构建系统&#xff0c;弥补Make构建系统在Android层面变慢、容易出错、无法扩展且难以测试等缺点。 Soong利用Kati GNU Make克隆工具和Ninja构建系统组件来加速Android的…...

显性知识的主要特征

有4个主要特征&#xff1a; 客观存在性静态存在性可共享性认知元能性...

STM32F407VET6实战:CRC校验

CRC校验在数据传输快&#xff0c;且量大的时候使用。下面是STM32F407VET6HAL库使用CRC校验的思路。 步骤实现&#xff1a; CubeMX配置 c // 在CubeMX中启用CRC模块 // AHB总线时钟自动启用 HAL库代码 c // 初始化&#xff08;main函数中&#xff09; CRC_HandleTypeDef …...

LeetCode 746 使用最小花费爬楼梯

当然可以&#xff01;LeetCode 746 是一道经典的动态规划入门题&#xff0c;我来用 C 为你详细解释。 题目描述 给定一个整数数组 cost&#xff0c;其中每个元素 cost[i] 表示从第 i 个台阶向上爬需要支付的费用。一旦支付费用&#xff0c;你可以选择向上爬 1 步 或 2 步。 你…...

隧道结构安全在线监测系统解决方案

一、方案背景 隧道是地下隐蔽工程&#xff0c;会受到潜在、无法预知的地质因素影响。随着我国公路交通建设的发展&#xff0c;隧道占新建公路里程的比例越来越大。隧道属于线状工程&#xff0c;有的规模较大&#xff0c;可长达几公里或数十公里&#xff0c;往往穿越许多不同环境…...

牛客网NC22000:数字反转之-三位数

牛客网NC22000:数字反转之-三位数 &#x1f50d; 题目描述 时间限制&#xff1a;C/C/Rust/Pascal 1秒&#xff0c;其他语言2秒 空间限制&#xff1a;C/C/Rust/Pascal 32M&#xff0c;其他语言64M &#x1f4dd; 输入输出说明 输入描述: 输入一个3位整数n (100 ≤ n ≤ 999)…...

等离子模块【杀菌消毒】

图片来源于网络&#xff0c;与任何公司或实验室无关。 洗衣机中的等离子模块&#xff0c;又叫等离子杀菌模块或等离子发生器&#xff0c;是一种利用等离子体技术进行杀菌消毒、除异味、净化空气的部件。 输出正高压&#xff1a;3.0KV~4.0KV 输出负高压&#xff1a;-3.…...

LlamaIndex 第九篇 Indexing索引

索引概述 数据加载完成后&#xff0c;您将获得一个文档对象(Document)列表&#xff08;或节点(Node)列表&#xff09;。接下来需要为这些对象构建索引(Index)&#xff0c;以便开始执行查询。 索引&#xff08;Index&#xff09; 是一种数据结构&#xff0c;能够让我们快速检索…...

PCIe Switch 问题点

系列文章目录 文章目录 系列文章目录完善PCIe Retimer Overview Document OutlineSwitch 维度BroadComMicroChipAsmedia 祥硕Cyan其他 完善 Functional block diagram&#xff0c;功能框图Key Features and Benefits&#xff0c;主要功能和优点Fabric 链路Multi-root PCIe Re…...

Linux》Ubuntu》安装Harbor 私有仓库

Harbor 下载Harbor地址 # 下载测试镜像 docker pull hello-world# 给镜像重新打标签 docker tag hello-world serverip/library/hello-world:latest# 登录进行上传 docker login serverip docker push serverip/library/hello-world:latest...

2025 Adobe Acrobat DC安装教程

Adobe Acrobat DC是由Adobe公司开发的一款PDF编辑软件&#xff0c;具有将各种文件扫描至PDF、转换PDF文档&#xff1b;编辑PDF、将PDF转换为Word、Excel、打印PDF&#xff1b;创建富媒体PDF文件等功能。 一.软件下载 点此下载 https://pan.xunlei.com/s/VOQDq6Tk1KUFmyCw9M1E…...

第八节第三部分:认识枚举、枚举的作用和应用场景

认识枚举 枚举的概述 枚举的特点 枚举的应用场景 代码&#xff1a; 代码一&#xff1a;认识枚举 A&#xff08;枚举&#xff09; package com.d6_enum;public enum A {//注意&#xff1a;枚举类的第一行必须罗列的是枚举对象的名字X,Y,Z;private String name;public String…...

WEB安全--Java安全--shiro721反序列化漏洞

一、前言 既然我把shiro721和shiro550分开写&#xff0c;就说明两者是有区别的 不过两者的概念和作用也是大相径庭的&#xff0c;这里就不再赘述 可以参考上一篇文章&#xff1a; WEB安全--Java安全--shiro550反序列化漏洞-CSDN博客 二、shiro721 2.1、原理 区别于shiro5…...

[Lc] 5.16 One question a day周总结

感受&#xff1a; 一个数据结构 表示不了&#xff0c;那就再用一个数据结构来帮助标识 逻辑清晰的分析出过程 就一定能写出来~ dp 逆构 依照上述 3 个条件&#xff0c;筛选字符串即可 历程 最开始一眼dp&#xff0c;后来发现要return string&#xff0c;看数据也不是很大&…...

【数据机构】2. 线性表之“链表”

- 第 97 篇 - Date: 2025 - 05 - 16 Author: 郑龙浩/仟墨 【数据结构 2】 续上一篇 线性表之“顺序表” 文章目录 3 链表(用指针来首位相连)① 基本介绍② 分类 与 变量命名1 &#xff09;分类&#xff1a;2 &#xff09;大体介绍不同结构&#xff1a; ③ “单链表” 的实现:*…...

《数字藏品APP开发:解锁高效用户身份认证与KYC流程》

开发一款数字藏品APP&#xff0c;要面对诸多复杂且关键的环节&#xff0c;其中&#xff0c;实现高效的用户身份认证与KYC&#xff08;了解你的客户&#xff09;流程&#xff0c;无疑是重中之重。这不仅关乎用户资产安全与平台合规运营&#xff0c;更是构建用户信任、保障平台可…...

问题 | 国内外软件定义卫星最新进展研究

软件定义卫星 **一、国内进展****二、国际进展****三、未来发展方向****总结** 软件定义卫星&#xff08;Software-Defined Satellite, SDS&#xff09;作为航天领域的重要技术革新方向&#xff0c;近年来在全球范围内发展迅速。其核心是通过开放式架构和动态软件配置实现卫星功…...

安全生产调度管理系统的核心功能模块

安全生产调度管理系统是运用现代信息技术构建的智能化管理平台&#xff0c;旨在实现生产安全风险的全面管控和应急资源的优化调度。该系统通过整合物联网、大数据、人工智能等前沿技术&#xff0c;建立起覆盖风险监测、预警预测、指挥调度、决策支持的全链条安全管理体系。 一…...