iOS音视频解封装分析

news2025/5/20 11:31:23

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

/// 解封装配置
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 的瓶颈,而且考虑到解封装的资源可能会很大,所以一般不会一直不停地解出数据往外抛,这样下一个处理节点可能处理不过来这些数据。基于这些原因,解封装器的接口设计是让外部调用方主动找解封装器要数据来触发解封装操作,并且还要控制一定的缓存量防止内存占用过大。

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

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

相关文章

突破智能驾舱边界,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. 动态…

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;支持回车键快速查询 内置网络请求重试和异常处理机制 在线网页…

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

代码随想录算法训练营 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老师在介绍这…

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 提取公共逻辑 …

华为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;并对测试结果进行分析。 ## …

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;例如拼接…

游戏引擎学习第280天:精简化的流式实体sim

回顾并为今天的内容做铺垫 今天的任务是让之前关于实体存储方式的改动真正运行起来。我们现在希望让实体系统变得更加真实和实用&#xff0c;能够支撑我们游戏实际所需的功能。这就要求我们对它进行更合理的实现和调试。 昨天我们基本让代码编译通过了&#xff0c;但实际上还…

王树森推荐系统公开课 排序03:预估分数融合

融合预估分数 p c l i c k ⋅ p l i k e p_{click} \cdot p_{like} pclick​⋅plike​ 有实际意义&#xff0c;等于在曝光中点赞的概率。 p c l i c k ⋅ p c o l l e c t p_{click} \cdot p_{collect} pclick​⋅pcollect​ 同理。 按多种排名做 ensemble sort。 某电商的融…

网络I/O学习-poll(三)

一、为什么要用Poll 由于select参数太多&#xff0c;较于复杂&#xff0c;调用起来较为麻烦&#xff1b;poll对其进行了优化 二、poll机制 poll也是一个系统调用&#xff0c;每次调用都会将所有客户端的fd拷贝到内核空间&#xff0c;然后进行轮询&#xff0c;判断IO是否就绪…