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

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…...

突破智能驾舱边界,Imagination如何构建高安全GPU+AI融合计算架构

日前&#xff0c;“第十二届汽车电子创新大会暨汽车芯片产业生态发展论坛&#xff08;AEIF 2025&#xff09;”在上海顺利举办。大会围绕汽车前沿性、关键性和颠覆性技术突破&#xff0c;邀请行业众多专家学者&#xff0c;分享与探讨了汽车电子产业的技术热点与发展趋势。在5月…...

DeepSeek 如何实现 128K 上下文窗口?

DeepSeek 如何实现 128K 上下文窗口&#xff1f;长文本处理技术揭秘 系统化学习人工智能网站&#xff08;收藏&#xff09;&#xff1a;https://www.captainbed.cn/flu 文章目录 DeepSeek 如何实现 128K 上下文窗口&#xff1f;长文本处理技术揭秘摘要引言技术架构解析1. 动态…...

云计算简介:从“水电”到“数字引擎”的技术革命

云计算简介&#xff1a;从“水电”到“数字引擎”的技术革命 在当今数字化浪潮中&#xff0c;云计算早已从一个技术概念演变为支撑现代社会运转的核心基础设施。无论是你手机里的天气预报、电商购物的推荐系统&#xff0c;还是企业内部的ERP系统&#xff0c;背后都离不开云计算…...

计算圆周率 (python)

使用模特卡罗方法&#xff08;模拟法&#xff09;&#xff0c;模拟撒点100000次&#xff0c;计算圆周率π 输入格式: 一个整数&#xff0c;表示随机数种子 输出格式: 计算的π值&#xff0c;结果小数点后保留5位数字 输入样例: 在这里给出一组输入。例如&#xff1a; 10…...

Python 实现图片浏览和选择工具

实现将截图预览&#xff0c;并按照顺序加入一个pdf文件中&#xff0c;实现照片管理尤其对于喜欢看教程截图做笔记的网友们。 C:\pythoncode\new\python-image-pdf-processor.py 界面展示 &#x1f9f1; 一、核心结构概述 主类 ImageViewer(wx.Frame) 是主窗口类&#xff0c;…...

Python实现的在线词典学习工具

Python实现的在线词典学习工具 源码最初来自网络&#xff0c;根据实际情况进行了修改。 主要功能&#xff1a; 单词查询 通过Bing词典在线获取单词释义&#xff08;正则提取网页meta描述&#xff09;&#xff0c;支持回车键快速查询 内置网络请求重试和异常处理机制 在线网页…...

ES常识9:如何实现同义词映射(搜索)

在 Elasticsearch&#xff08;ES&#xff09;中实现同义词映射&#xff08;如“美丽”和“漂亮”&#xff09;&#xff0c;核心是通过 同义词过滤器&#xff08;Synonym Token Filter&#xff09; 在分词阶段将同义词扩展或替换为统一词项&#xff0c;从而让搜索时输入任意一个…...

BGP综合实验(2)

一、实验需求 1、实验拓扑图 2、实验需求 使用 PreVal 策略&#xff0c;让 R4 经 R2 到达 192.168.10.0/24 。 使用 AS_Path 策略&#xff0c;让 R4 经 R3 到达 192.168.11.0/24 。 配置 MED 策略&#xff0c;让 R4 经 R3 到达 192.168.12.0/24 。 使用 Local Preference 策…...

java实现poi-ooxml导出Excel的功能

文章目录 1. 添加poi-ooxml依赖2. Excel导出工具类3.核心逻辑说明4.扩展建议5.HSSF、XSSF、SXSSF 的核心原则和场景建议&#xff0c;帮助你在不同需求下快速决策&#xff1a; 以下是一个基于 Apache POI 实现的简单、通用的Java导出Excel工具类&#xff0c;代码逻辑清晰且注释详…...

代码随想录算法训练营 Day51 图论Ⅱ岛屿问题Ⅰ

图论 题目 99. 岛屿数量 使用 DFS 实现方法 判断岛屿方法 1. 遍历图&#xff0c;若遍历到了陆地 grid[i][j] 1 并且陆地没有被访问&#xff0c;在这个陆地的基础上进行 DFS 方法&#xff0c;或者是 BFS 方法 2. 对陆地进行 DFS 的时候时刻注意以访问的元素添加访问标记 //…...

【占融数科-注册/登录安全分析报告】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 暴力破解密码&#xff0c;造成用户信息泄露短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造成亏损无底洞…...

【CF】Day62——Codeforces Round 948 (Div. 2) CD (思维 + LCM + 枚举因数 | 思维 + 哈希)

C. Nikita and LCM 题目&#xff1a; 思路&#xff1a; 非常好的思维题&#xff0c;顺便复习了一下快速枚举因数和lcm的性质 我们先来看答案的上界&#xff0c;即全选&#xff0c;此时说明 lcm(a1,a2,a3,...) > a_max 其中 a_max 为 a 中最大的数&#xff0c;那么如果答案不…...

基于requests_html的python爬虫

前言&#xff1a;今天介绍一个相对性能更高的爬虫库requests_html&#xff0c;会不会感觉和requests有点联系&#xff1f;是的。为什么开始不直接介绍呢&#xff1f;因为我觉得requests是最基本入门的东西&#xff0c;并且在学习过程中也能学到很多东西。我的python老师在介绍这…...

循环神经网络:捕捉序列数据中的时间信息

目录 循环神经网络&#xff1a;捕捉序列数据中的时间信息 一、循环神经网络的基本概念 &#xff08;一&#xff09;RNN 的基本结构 &#xff08;二&#xff09;RNN 的工作原理 &#xff08;三&#xff09;RNN 的优势 &#xff08;四&#xff09;RNN 的局限性 二、循环神…...

第35周Zookkeeper+Dubbo 面试题精讲

面试题精讲 一、算法面试答题思路 理解思路的重要性:算法面试比基础面试更复杂,需先想清楚思路,与面试官沟通确认题目条件(如数据范围、是否包含负数/零等),这有助于理清解题思路并展示技术实力。变量命名清晰:算法中变量命名要明确含义和范围,避免使用模糊的变量名,…...

聊聊更新中断和更新事件那些事儿

最近在研究一些系统和设备的更新机制&#xff0c;发现更新中断和更新事件这两个概念很有意思&#xff0c;也容易让人混淆&#xff0c;今天就来和大家好好探讨一下。 一、更新事件 &#xff08;一&#xff09;定义与原理 更新事件&#xff0c;简单来说&#xff0c;是当出现某…...

STM32:按键模块 传感器模块 以及 相关C语言知识(详细讲解)

目录 按键 传感器模块 C语言知识 C语言数据类型 C语言宏定义 C语言typedef C语言结构体 C语言枚举 按键 常见的输入设备&#xff0c;按下导通&#xff0c;松手断开 按键抖动&#xff1a;由于按键内部使用的是机械式弹簧片来进行通断的&#xff0c;所以在按下和松手的瞬…...

C++23 std::mdspan:多维数组处理新利器

文章目录 引言C23简介std::mdspan的定义与特点定义特点 std::mdspan的优势零成本抽象的多维数据访问减少内存开销提高代码灵活性 std::mdspan的应用场景科学计算图形学 相关提案示例代码使用动态扩展使用静态和动态扩展 总结 引言 在C的发展历程中&#xff0c;每一个新版本都带…...

基于高德MCP2.0的智能旅游攻略系统设计与实现

前言&#xff1a;旅游规划的技术革命 在数字化旅游时代&#xff0c;MCP2.0&#xff08;Map-based Collaborative Planning&#xff09;系统代表着旅游攻略技术的最新演进。作为对1.0版本的全面升级&#xff0c;MCP2.0通过深度整合高德地图API和智能算法&#xff0c;实现了从静…...

【时时三省】(C语言基础)用函数实现模块化程序设计

山不在高&#xff0c;有仙则名。水不在深&#xff0c;有龙则灵。 ----CSDN 时时三省 为什么要用函数&#xff1f; 已经能够编写一些简单的C程序&#xff0c;但是如果程序的功能比较多&#xff0c;规模比较大&#xff0c;把所有的程序代码都写在一个主函数(main函数)中&#x…...

Flink流处理:实时计算URL访问量TopN(基于时间窗口)

目录 代码分析 背景知识拓展 代码调优 1. 性能优化 1.1 使用 KeyedStream 和 ProcessWindowFunction 替代 windowAll 1.2 使用 ReduceFunction 优化聚合 2. 功能扩展 2.1 支持动态窗口大小 2.2 支持多维度统计 2.3 支持持久化存储 3. 代码可读性 3.1 提取公共逻辑 …...

初识函数------了解函数的定义、函数的参数、函数的返回值、说明文档的书写、函数的嵌套使用、变量的作用域(全局变量与局部变量)

文章目录 一、什么是函数&#xff1f;二、函数定义与调用2.1 基本语法2.2 示例演示 三、函数参数详解3.1 位置参数3.2 默认参数3.3 可变参数3.4 关键字参数 四、返回值与文档说明4.1 返回多个值4.2 编写文档字符串 五、函数嵌套与作用域5.1 嵌套函数示例5.2 变量作用域5.3 glob…...

java collection集合特点知识点详解

在 Java 中&#xff0c;Collection 是所有集合类的根接口&#xff0c;它定义了一组对象的基本操作。Java 集合框架提供了丰富的实现类&#xff08;如List、Set、Queue&#xff09;&#xff0c;具有以下核心特点&#xff1a; 一、统一的接口设计 1. 核心接口层次 Collection …...

ngx_http_realip_module 模块概述

一、使用场景 日志记录 记录真实客户端 IP 而非反向代理的 IP&#xff0c;有助于流量分析和安全审计。访问控制 基于真实 IP 实现防火墙规则&#xff08;allow/deny&#xff09;或限流&#xff0c;而非误将上游 IP 视为客户端。GeoIP、WAF、限速等功能 模块化的上游真实 IP 支…...

自定义CString类与MFC CString类接口对比

接口对比表格 功能分类 你的 CString 接口 MFC CString 接口&#xff08;ANSI&#xff09; 一致性 差异说明 构造函数 CString() CString(const char*) CString(char) CString(const CString&) CString() CString(LPCSTR) CString(TCHAR) CString(const CString&…...

华为OD机试真题——考勤信息(2025A卷:100分)Java/python/JavaScript/C/C++/GO最佳实现

2025 A卷 100分 题型 本专栏内全部题目均提供Java、python、JavaScript、C、C++、GO六种语言的最佳实现方式; 并且每种语言均涵盖详细的问题分析、解题思路、代码实现、代码详解、3个测试用例以及综合分析; 本文收录于专栏:《2025华为OD真题目录+全流程解析+备考攻略+经验分…...

Go语言测试用例的执行与分析

在软件开发过程中&#xff0c;测试用例是确保代码质量的关键环节。Go语言作为一种现代的编程语言&#xff0c;它内置了强大的测试框架&#xff0c;可以帮助开发者轻松编写和执行测试用例。本文将介绍如何在 Go 语言中编写、执行测试用例&#xff0c;并对测试结果进行分析。 ## …...

vue3 vite 路由

如路由是这种格式 http://localhost:7058/admin/product/brand路由配置如下 import { createRouter, createWebHistory } from vue-router import HomeView from ../views/HomeView.vue import NProgress from nprogress; import nprogress/nprogress.css; import {errorRour…...

MyBatis:动态SQL

文章目录 动态SQLif标签trim标签where标签set标签foreach标签include标签和sql标签 Mybatis动态SQL的官方文档&#xff1a; https://mybatis.net.cn/dynamic-sql.html 动态SQL 动态SQL是 MyBatis的强大特性之一,如果是使用JDBC根据不同条件拼接sql很麻烦&#xff0c;例如拼接…...