iOS音视频解封装分析

发布于:2025-05-18 ⋅ 阅读:(17) ⋅ 点赞:(0)

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

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

    }
}

然后是实现解封装控制器

import Foundation
import CoreMedia
import AVFoundation

// 解封装器状态枚举
enum KFMP4DemuxerStatus: Int {
    case unknown = 0
    case running = 1
    case failed = 2
    case completed = 3
    case cancelled = 4
}

// 错误码常量
private let KFMP4DemuxerBadFileError = 2000
private let KFMP4DemuxerAddVideoOutputError = 2001
private let KFMP4DemuxerAddAudioOutputError = 2002
private let KFMP4DemuxerQueueMaxCount = 3

class KFMP4Demuxer {
    // MARK: - 属性
    let config: KFDemuxerConfig
    var 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: DispatchQueue
    private let demuxerSemaphore: DispatchSemaphore
    private let audioQueueSemaphore: DispatchSemaphore
    private let videoQueueSemaphore: DispatchSemaphore
    
    // 数据队列
    private var audioQueue: CMSimpleQueue
    private var videoQueue: CMSimpleQueue
    
    // 时间戳
    private var lastAudioCopyNextTime = CMTime.zero  // 上一次拷贝的音频采样的时间戳
    private var lastVideoCopyNextTime = CMTime.zero  // 上一次拷贝的视频采样的时间戳
    
    // MARK: - 生命周期
    init(config: KFDemuxerConfig) {
        self.config = config
        self.demuxerSemaphore = DispatchSemaphore(value: 1)
        self.audioQueueSemaphore = DispatchSemaphore(value: 1)
        self.videoQueueSemaphore = DispatchSemaphore(value: 1)
        self.demuxerStatus = .unknown
        self.demuxerQueue = DispatchQueue(label: "com.KeyFrameKit.demuxerQueue", attributes: [])
        
        // 创建音频和视频缓冲队列
        var audioQueueRef: CMSimpleQueue? = nil
        var videoQueueRef: CMSimpleQueue? = nil
        CMSimpleQueueCreate(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 = self
        demuxerQueue.async {
            guard let self = weakSelf else { return }
            self.demuxerSemaphore.wait()
            
            // 在第一次开始读数据时,创建解封装器实例
            if self.demuxReader == nil {
                var error: Error? = nil
                self.setupDemuxReader(&error)
                self.audioEOF = !self.hasAudioTrack
                self.videoEOF = !self.hasVideoTrack
                self.demuxerStatus = error != nil ? .failed : .running
                self.demuxerSemaphore.signal()
                
                DispatchQueue.main.async {
                    completionHandler(error == nil, error)
                }
                return
            }
            
            self.demuxerSemaphore.signal()
        }
    }
    
    func cancelReading() {
        weak var weakSelf = self
        demuxerQueue.async {
            guard let self = weakSelf else { return }
            self.demuxerSemaphore.wait()
            
            // 取消读数据
            if let reader = self.demuxReader, reader.status == .reading {
                reader.cancelReading()
            }
            self.demuxerStatus = .cancelled
            
            self.demuxerSemaphore.signal()
        }
    }
    
    func hasAudioSampleBuffer() -> Bool {
        // 是否还有音频数据
        if hasAudioTrack && demuxerStatus == .running && !audioEOF {
            var audioCount: Int32 = 0
            audioQueueSemaphore.wait()
            if CMSimpleQueueGetCount(audioQueue) > 0 {
                audioCount = CMSimpleQueueGetCount(audioQueue)
            }
            audioQueueSemaphore.signal()
            
            return (audioCount == 0 && audioEOF) ? false : true
        }
        
        return false
    }
    
    func copyNextAudioSampleBuffer() -> CMSampleBuffer? {
        // 拷贝下一份音频采样
        var sampleBuffer: CMSampleBuffer? = nil
        while 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 = 0
            videoQueueSemaphore.wait()
            if CMSimpleQueueGetCount(videoQueue) > 0 {
                videoCount = CMSimpleQueueGetCount(videoQueue)
            }
            videoQueueSemaphore.signal()
            
            return (videoCount == 0 && videoEOF) ? false : true
        }
        
        return false
    }
    
    func copyNextVideoSampleBuffer() -> CMSampleBuffer? {
        // 拷贝下一份视频采样
        var sampleBuffer: CMSampleBuffer? = nil
        while 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 = readerError
            return
        }
        
        // 2、获取时间信息
        duration = asset.duration
        
        // 3、处理待解封装的资源中的视频
        if config.demuxerType.contains(.video) {
            // 取出视频轨道
            guard let videoTrack = asset.tracks(withMediaType: .video).first else {
                hasVideoTrack = false
                return
            }
            
            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! CMFormatDescription
            codecType = 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 = false
                return
            }
            
            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?.error
            return
        }
    }
    
    private func asyncLoadNextSampleBuffer() {
        // 异步加载下一份采样数据
        weak var weakSelf = self
        demuxerQueue.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 = .completed
                return
            case .failed:
                print("KFMP4Demuxer - 解封装失败: \(String(describing: reader.error))")
                if let nsError = reader.error as NSError?, nsError.code == AVError.operationInterrupted.rawValue {
                    print("KFMP4Demuxer - 操作被中断,尝试恢复")
                    // 如果当前解封装器的状态是被打断而失败,就尝试重新创建一下
                    var error: Error? = nil
                    setupDemuxReader(&error)
                    if error == nil {
                        print("KFMP4Demuxer - 恢复成功,重新启动解封装器")
                        // 同时做一下恢复处理
                        resumeLastTime()
                    } else {
                        print("KFMP4Demuxer - 恢复失败: \(String(describing: error))")
                    }
                }
                
                if reader.status == .failed {
                    // 如果状态依然是失败,就上报错误
                    print("KFMP4Demuxer - 解封装器状态仍为失败")
                    demuxerStatus = .failed
                    if let error = reader.error, let callback = errorCallBack {
                        print("KFMP4Demuxer - 调用错误回调: \(error)")
                        DispatchQueue.main.async {
                            callback(error)
                        }
                    }
                    return
                }
            case .cancelled:
                // 如果状态是取消,就直接 return
                print("KFMP4Demuxer - 解封装已取消")
                demuxerStatus = .cancelled
                return
            default:
                print("KFMP4Demuxer - 解封装器状态: \(reader.status.rawValue)")
                break
            }
        } else {
            print("KFMP4Demuxer - demuxReader为nil")
        }
        
        // 2、解封装器状态正常,加载下一份采样数据
        let audioNeedLoad = config.demuxerType.contains(.audio) && !audioEOF
        let videoNeedLoad = config.demuxerType.contains(.video) && !videoEOF
        var shouldContinueLoadingAudio = audioNeedLoad
        var shouldContinueLoadingVideo = videoNeedLoad
        
        print("KFMP4Demuxer - 需要加载: 音频=\(audioNeedLoad), 视频=\(videoNeedLoad)")
        
        var loadCount = 0
        while let reader = demuxReader, reader.status == .reading && (shouldContinueLoadingAudio || shouldContinueLoadingVideo) {
            loadCount += 1
            if 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 == .completed
                        shouldContinueLoadingAudio = false
                        print("KFMP4Demuxer - 音频数据读取结束,EOF=\(audioEOF)")
                    }
                } else {
                    shouldContinueLoadingAudio = false
                    if 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 == .completed
                        shouldContinueLoadingVideo = false
                        print("KFMP4Demuxer - 视频数据读取结束,EOF=\(videoEOF)")
                    }
                } else {
                    shouldContinueLoadingVideo = false
                    if 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 && !audioEOF
        let videoNeedLoad = lastVideoCopyNextTime.value > 0 && !videoEOF
        
        print("需要恢复音频: \(audioNeedLoad), 需要恢复视频: \(videoNeedLoad)")
        var shouldContinueLoadingAudio = audioNeedLoad
        var shouldContinueLoadingVideo = videoNeedLoad
        
        while 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 == .completed
                    print("音频恢复到达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 == .completed
                    print("视频恢复到达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 in
            guard 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 的瓶颈,而且考虑到解封装的资源可能会很大,所以一般不会一直不停地解出数据往外抛,这样下一个处理节点可能处理不过来这些数据。基于这些原因,解封装器的接口设计是让外部调用方主动找解封装器要数据来触发解封装操作,并且还要控制一定的缓存量防止内存占用过大。


网站公告

今日签到

点亮在社区的每一天
去签到