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:
方法中实现。音频和视频的输出源分别是readerAudioOutput
和readerVideoOutput
。
2)用两个队列作为缓冲区,分别管理音频和视频解封装后的数据。
-
这两个队列分别是
_audioQueue
和_videoQueue
。 -
当外部向解封装器要数据而触发数据加载时,会把解封装后的数据先缓存到这两个队列中,缓冲的采样数不超过
KFMP4DemuxerQueueMaxCount
,以减少内存占用。 -
3)从音视频输出源读取数据。
-
核心逻辑在
-_loadNextSampleBuffer
方法中实现:从输出源readerAudioOutput
和readerVideoOutput
读取数据放入缓冲区队列_audioQueue
和_videoQueue
。 -
在外部调用
-copyNextAudioSampleBuffer
、-copyNextVideoSampleBuffer
时,触发读取数据。
4)从中断中恢复解封装。
- 在
-_resumeLastTime
方法中实现。
5)停止解封装。
- 在
-cancelReading
方法中实现。
6)解封装状态机管理。
- 在枚举
KFMP4DemuxerStatus
中定义了解封装器的各种状态,对于解封装器的状态机管理贯穿在解封装的整个过程中。
7)错误回调。
- 在
-callBackError:
方法向外回调错误。
8)清理封装器实例及数据缓冲区。
- 在
-deinit
方法中实现。
接下来来分析一下调用过程
初始化阶段
- KFVideoDemuxerViewController初始化
-
创建demuxerConfig:设置视频路径和解封装类型
-
创建KFMP4Demuxer实例:传入demuxerConfig并设置错误回调
启动阶段(点击"Start"按钮)
- 调用start()方法
-
检查asset是否存在
-
验证视频轨道信息
-
调用demuxer.startReading()方法
- KFMP4Demuxer的startReading()
-
在demuxerQueue队列中异步执行
-
首次调用时创建解封装器实例(setupDemuxReader)
- setupDemuxReader流程
-
检查asset有效性
-
创建AVAssetReader实例
-
获取媒体时间信息
-
处理视频轨道:
-
获取视频轨道、格式和尺寸信息
-
创建视频输出(AVAssetReaderTrackOutput)
-
添加视频输出到解封装器
-
处理音频轨道(如果需要)
-
启动AVAssetReader开始读取
- startReading完成回调
-
成功时调用fetchAndSaveDemuxedData()
-
失败时输出错误信息
数据处理阶段
- fetchAndSaveDemuxedData()
-
在全局队列异步执行
-
循环调用demuxer.hasVideoSampleBuffer()和copyNextVideoSampleBuffer()
-
对每个采样缓冲区调用saveSampleBuffer()
- 解封装数据读取流程
-
hasVideoSampleBuffer:检查是否还有视频数据可读
-
copyNextVideoSampleBuffer:
-
从视频队列获取采样缓冲区
-
如果队列为空,调用syncLoadNextSampleBuffer()同步加载
-
加载完成后调用asyncLoadNextSampleBuffer()异步准备下一批数据
- 加载采样数据(loadNextSampleBuffer)
-
检查解封装器状态,处理异常情况
-
从AVAssetReaderTrackOutput读取视频数据
-
将数据存入缓冲队列(videoQueue)
保存阶段
- 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 中设计的 KFAudioCapture
、KFAudioEncoder
的接口那样,有一个解封装后的数据回调接口。主要是因为解封装的速度是非常快的,不会成为一个音视频 pipeline 的瓶颈,而且考虑到解封装的资源可能会很大,所以一般不会一直不停地解出数据往外抛,这样下一个处理节点可能处理不过来这些数据。基于这些原因,解封装器的接口设计是让外部调用方主动找解封装器要数据来触发解封装操作,并且还要控制一定的缓存量防止内存占用过大。