iOS开发基础139-视频解码

要进行视频解码,我们同样可以使用VideoToolbox框架中的API来实现。以下示例会聚焦于解码H.264编码的视频流。解码过程大致分为几个步骤:创建解码会话、设置解码回调、输入编码后的数据,并在回调中接收解码后的图像。

下面是一个简化的视频解码器类实现,展示了如何设置一个解码会话并接收解码的视频帧。

VideoDecoder.h

// 引入必要的库
#import <Foundation/Foundation.h>
#import <VideoToolbox/VideoToolbox.h>

// 声明一个协议,用于处理解码后的视频帧。
@protocol VideoDecoderDelegate <NSObject>
- (void)videoDecoderDidDecodeFrame:(CVImageBufferRef)imageBuffer;
@end

// 定义VideoDecoder类,用于视频解码
@interface VideoDecoder : NSObject
@property (weak, nonatomic) id<VideoDecoderDelegate> delegate; // 委托对象,用于回调处理解码后的视频帧
- (void)decodeVideoData:(NSData *)videoData; // 解码视频数据的方法
@end

VideoDecoder.m

#import "VideoDecoder.h"

@interface VideoDecoder ()
@property (assign, nonatomic) VTDecompressionSessionRef decompressionSession; // 视频解码会话
@property (assign, nonatomic) CMVideoFormatDescriptionRef videoFormatDescription; // 视频格式描述符
@end

@implementation VideoDecoder

- (instancetype)init {
    if (self = [super init]) {
        _videoFormatDescription = NULL;  // 初始化时,将视频格式描述符设置为NULL
        _decompressionSession = NULL;    // 初始化时,将解码会话设置为NULL
    }
    return self;
}

- (void)createDecompressionSession {
    if (_decompressionSession != NULL) { 
        // 若解码会话已存在,先使其失效并释放资源
        VTDecompressionSessionInvalidate(_decompressionSession);
        CFRelease(_decompressionSession);
        _decompressionSession = NULL;
    }
    
    // 设置解码输出的图像缓存的格式
    NSDictionary *destinationImageBufferAttributes = @{
        (NSString *)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) // 使用YUV格式(420YpCbCr8BiPlanarFullRange)来表示解码后的图像
    };
    
    VTDecompressionOutputCallbackRecord callbackRecord; // 解码输出回调记录
    callbackRecord.decompressionOutputCallback = decompressionOutputCallback; // 设置解码输出的回调方法
    callbackRecord.decompressionOutputRefCon = (__bridge void *)(self); // 将当前对象传递给回调方法,以便在回调中可以访问当前对象的属性或方法

    OSStatus status = VTDecompressionSessionCreate(kCFAllocatorDefault, // 使用默认内存分配器
                                                   _videoFormatDescription, // 输入的视频格式描述
                                                   NULL, // 不使用解码规范
                                                   (__bridge CFDictionaryRef)(destinationImageBufferAttributes), // 解码输出图像缓存的设置
                                                   &callbackRecord, // 解码输出的回调记录
                                                   &_decompressionSession); // 创建的解码会话
    if (status != noErr) {
        NSLog(@"Error creating decompression session: %d", status); // 若创建失败,打印错误信息
    }
}

- (void)decodeVideoData:(NSData *)videoData timestamp:(CMTime)timestamp {
    // 要从`NSData`中的视频数据转换为`CMSampleBufferRef`,我们需要创建或获取一个有效的`CMSampleBufferRef`。这个过程相对复杂,因为它涉及到对输入数据的格式有一定的了解。下面是一个简化的示例,展示了如果你已经有了编码的视频帧(这里简化处理为H.264编码),如何构建一个基本的`CMSampleBufferRef`。这个过程涉及到生成或解析SPS(Sequence Parameter Set)和PPS(Picture Parameter Set),这两者用于生成一个视频格式描述来创建`CMSampleBuffer`。
    //   以下主要目的是为了演示过程,并不包含所有细节
    if (!videoData.length || !_decompressionSession) {
        NSLog(@"No video data or decompression session is null.");
        return;
    }
    
    // 这里假设videoData中已经包含了H.264帧数据。
    // 真实情况下,你可能需要从更复杂的数据结构中提取出H.264的NALU单元,并处理它们。
    
    // 1. 创建CMBlockBuffer
    CMBlockBufferRef blockBuffer = NULL;
    OSStatus status = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault,
                                                         (void *)videoData.bytes, // video data's pointer
                                                         videoData.length, // data length
                                                         kCFAllocatorNull,
                                                         NULL,
                                                         0,
                                                         videoData.length,
                                                         0,
                                                         &blockBuffer);
                                                         
    if (status != kCMBlockBufferNoErr) {
        NSLog(@"Error creating CMBlockBuffer: %d", status);
        return;
    }
    
    // 2. 创建CMSampleBuffer
    CMSampleBufferRef sampleBuffer = NULL;
    const size_t sampleSizeArray[] = {videoData.length}; // 样本大小数组
    status = CMSampleBufferCreateReady(kCFAllocatorDefault,
                                       blockBuffer, // 数据块
                                       _videoFormatDescription, // 前面通过SPS和PPS数据创建的视频格式描述
                                       1, // sample count
                                       0,
                                       NULL, // timing info array
                                       1, // sample size array entry count
                                       sampleSizeArray, // sample size array
                                       &sampleBuffer);
    
    // 不再需要blockBuffer,释放资源
    CFRelease(blockBuffer);
    
    if (status != kCMBlockBufferNoErr || !sampleBuffer) {
        NSLog(@"Error creating CMSampleBuffer: %d", status);
        return;
    }
    
    // 3. 解码CMSampleBuffer
    VTDecodeFrameFlags flags = 0;
    VTDecodeInfoFlags flagOut = 0;
    status = VTDecompressionSessionDecodeFrame(_decompressionSession,
                                               sampleBuffer,
                                               flags,
                                               NULL, // output callback reference
                                               &flagOut);
    
    if (status != noErr) {
        NSLog(@"Decode failed status: %d", status);
    }
    
    // 释放sampleBuffer资源
    CFRelease(sampleBuffer);
}


static void decompressionOutputCallback(void * CM_NULLABLE decompressionOutputRefCon,
                                         void * CM_NULLABLE sourceFrameRefCon,
                                         OSStatus status,
                                         VTDecodeInfoFlags infoFlags,
                                         CM_NULLABLE CVImageBufferRef imageBuffer,
                                         CMTime presentationTimeStamp,
                                         CMTime presentationDuration) {
    if (status != noErr) {
        NSLog(@"Error in decompression output callback: %d", status); // 若解码过程中有错误,打印错误信息
        return;
    }

    VideoDecoder *decoder = (__bridge VideoDecoder *)decompressionOutputRefCon; // 从传递到回调的引用中取回VideoDecoder对象
    if ([decoder.delegate respondsToSelector:@selector(videoDecoderDidDecodeFrame:)]) {
        [decoder.delegate videoDecoderDidDecodeFrame:imageBuffer]; // 将解码后的图像帧通过协议中的方法回调出去
    }
}

- (void)dealloc {
    if (_decompressionSession) {
        VTDecompressionSessionInvalidate(_decompressionSession); // 使解码会话失效
        CFRelease(_decompressionSession); // 释放解码会话资源
    }
    if (_videoFormatDescription) {
        CFRelease(_videoFormatDescription); // 释放视频格式描述符资源
    }
}
@end

使用方法

VideoDecoder *decoder = [[VideoDecoder alloc] init];
decoder.delegate = self; 

[decoder decodeVideoData:frameData];


- (void)videoDecoderDidDecodeFrame:(CVImageBufferRef)imageBuffer {
    // 处理解码图像缓冲区(显示、处理等)
}

说明

  1. 创建解码会话(VTDecompressionSessionCreate):在初始化解码器类时或者在第一次接收到视频数据时调用。此方法需要正确的CMVideoFormatDescriptionRef来描述输入视频流的格式,如H.264。对于网络流,这通常可以从流的SPS和PPS单元中解析得到。

  2. 解码视频数据(VTDecompressionSessionDecodeFrame):此函数用于输入编码的视频帧,并异步返回解码后的帧。解码的输出是通过设置的回调函数返回的。

  3. 回调函数:解码的帧通过设置的解码输出回调(decompressionOutputCallback)返回。回调提供CVImageBufferRef格式的解码视频帧,可以用于显示或进一步处理。

  4. 注意内存管理和线程安全:确保在不需要解码会话时使用VTDecompressionSessionInvalidate来销毁会话,并释放相关资源。解码操作是异步进行的,确保回调中的处理逻辑是线程安全的。

以上是一个基础的视频解码器实现,实际应用中可能需要根据特定需求进行适当的调整。例如,处理解码错误、支持不同的输入格式或优化解码性能等。

posted @ 2024-07-23 16:01  Mr.陳  阅读(3)  评论(0编辑  收藏  举报