iOS开发笔记iOS开发

iOS利用FFmpeg实现Video硬解码

2019-06-19  本文已影响0人  小东邪啊

需求

将编码的视频流解码为原始视频数据,编码视频流可以来自网络流或文件,解码后即可渲染到屏幕.


实现原理

正如我们所知,编码数据仅用于传输,无法直接渲染到屏幕上,所以这里利用FFmpeg解析文件中的编码的视频流,并将压缩视频数据(h264/h265)解码为指定格式(yuv,RGB)的视频原始数据,以渲染到屏幕上.

注意: 本例主要为解码,需要借助FFmpeg搭建模块,视频解析模块,渲染模块,这些模块在下面阅读前提皆有链接可直接访问.


阅读前提


代码地址 : Video Decoder

掘金地址 : Video Decoder

简书地址 : Video Decoder

博客地址 : Video Decoder


总体架构

简易流程

FFmpeg parse流程

FFmpeg decode流程

文件结构

image

快速使用

- (void)viewDidLoad {
    [super viewDidLoad];
    [self setupUI];
}

- (void)setupUI {
    self.previewView = [[XDXPreviewView alloc] initWithFrame:self.view.frame];
    [self.view addSubview:self.previewView];
    [self.view bringSubviewToFront:self.startBtn];
}
- (void)startDecodeByFFmpegWithIsH265Data:(BOOL)isH265 {
    NSString *path = [[NSBundle mainBundle] pathForResource:isH265 ? @"testh265" : @"testh264" ofType:@"MOV"];
    XDXAVParseHandler *parseHandler = [[XDXAVParseHandler alloc] initWithPath:path];
    XDXFFmpegVideoDecoder *decoder = [[XDXFFmpegVideoDecoder alloc] initWithFormatContext:[parseHandler getFormatContext] videoStreamIndex:[parseHandler getVideoStreamIndex]];
    decoder.delegate = self;
    [parseHandler startParseGetAVPackeWithCompletionHandler:^(BOOL isVideoFrame, BOOL isFinish, AVPacket packet) {
        if (isFinish) {
            [decoder stopDecoder];
            return;
        }
        
        if (isVideoFrame) {
            [decoder startDecodeVideoDataWithAVPacket:packet];
        }
    }];
}
-(void)getDecodeVideoDataByFFmpeg:(CMSampleBufferRef)sampleBuffer {
    CVPixelBufferRef pix = CMSampleBufferGetImageBuffer(sampleBuffer);
    [self.previewView displayPixelBuffer:pix];
}

具体实现

1. 初始化实例对象

因为本例中的视频数据源是文件,而format context上下文实在parse模块初始化的,所以这里仅仅需要将其传入解码器即可.

- (instancetype)initWithFormatContext:(AVFormatContext *)formatContext videoStreamIndex:(int)videoStreamIndex {
    if (self = [super init]) {
        m_formatContext     = formatContext;
        m_videoStreamIndex  = videoStreamIndex;
        
        m_isFindIDR = NO;
        m_base_time = 0;
        
        [self initDecoder];
    }
    return self;
}

2. 初始化解码器

- (void)initDecoder {
    // 获取视频流
    AVStream *videoStream = m_formatContext->streams[m_videoStreamIndex];
    // 创建解码器上下文对象
    m_videoCodecContext = [self createVideoEncderWithFormatContext:m_formatContext
                                                            stream:videoStream
                                                  videoStreamIndex:m_videoStreamIndex];
    if (!m_videoCodecContext) {
        log4cplus_error(kModuleName, "%s: create video codec failed",__func__);
        return;
    }
    
    // 创建视频帧
    m_videoFrame = av_frame_alloc();
    if (!m_videoFrame) {
        log4cplus_error(kModuleName, "%s: alloc video frame failed",__func__);
        avcodec_close(m_videoCodecContext);
    }
}
2.1. 创建解码器上下文对象
- (AVCodecContext *)createVideoEncderWithFormatContext:(AVFormatContext *)formatContext stream:(AVStream *)stream videoStreamIndex:(int)videoStreamIndex {
    AVCodecContext *codecContext = NULL;
    AVCodec *codec = NULL;
    
    // 指定解码器名称, 这里使用苹果VideoToolbox中的硬件解码器
    const char *codecName = av_hwdevice_get_type_name(AV_HWDEVICE_TYPE_VIDEOTOOLBOX);
    // 将解码器名称转为对应的枚举类型
    enum AVHWDeviceType type = av_hwdevice_find_type_by_name(codecName);
    if (type != AV_HWDEVICE_TYPE_VIDEOTOOLBOX) {
        log4cplus_error(kModuleName, "%s: Not find hardware codec.",__func__);
        return NULL;
    }
    
    // 根据解码器枚举类型找到解码器
    int ret = av_find_best_stream(formatContext, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0);
    if (ret < 0) {
        log4cplus_error(kModuleName, "av_find_best_stream faliture");
        return NULL;
    }
    
    // 为解码器上下文对象分配内存
    codecContext = avcodec_alloc_context3(codec);
    if (!codecContext){
        log4cplus_error(kModuleName, "avcodec_alloc_context3 faliture");
        return NULL;
    }
    
    // 将视频流中的参数填充到视频解码器中
    ret = avcodec_parameters_to_context(codecContext, formatContext->streams[videoStreamIndex]->codecpar);
    if (ret < 0){
        log4cplus_error(kModuleName, "avcodec_parameters_to_context faliture");
        return NULL;
    }
    
    // 创建硬件解码器上下文
    ret = InitHardwareDecoder(codecContext, type);
    if (ret < 0){
        log4cplus_error(kModuleName, "hw_decoder_init faliture");
        return NULL;
    }
    
    // 初始化解码器上下文对象
    ret = avcodec_open2(codecContext, codec, NULL);
    if (ret < 0) {
        log4cplus_error(kModuleName, "avcodec_open2 faliture");
        return NULL;
    }
    
    return codecContext;
}

#pragma mark - C Function
AVBufferRef *hw_device_ctx = NULL;
static int InitHardwareDecoder(AVCodecContext *ctx, const enum AVHWDeviceType type) {
    int err = av_hwdevice_ctx_create(&hw_device_ctx, type, NULL, NULL, 0);
    if (err < 0) {
        log4cplus_error("XDXParseParse", "Failed to create specified HW device.\n");
        return err;
    }
    ctx->hw_device_ctx = av_buffer_ref(hw_device_ctx);
    return err;
}

仅仅将解码器中具有相应字段的任何已分配字段par被释放并替换为par中相应字段的副本。不涉及解码器中没有par中对应项的字段。

int av_find_best_stream(AVFormatContext *ic,
                        enum FfmpegaVMediaType type,
                        int wanted_stream_nb,
                        int related_stream,
                        AVCodec **decoder_ret,
                        int flags);
2.2. 创建视频帧

AVFrame 作为解码后原始的音视频数据的容器.AVFrame通常被分配一次然后多次重复(例如,单个AVFrame以保持从解码器接收的帧)。在这种情况下,av_frame_unref()将释放框架所持有的任何引用,并在再次重用之前将其重置为其原始的清理状态。

    // Get video frame
    m_videoFrame = av_frame_alloc();
    if (!m_videoFrame) {
        log4cplus_error(kModuleName, "%s: alloc video frame failed",__func__);
        avcodec_close(m_videoCodecContext);
    }

3. 开始解码

首先找到编码数据流中第一个I帧, 然后调用avcodec_send_packet将压缩数据发送给解码器.最后利用循环接收avcodec_receive_frame解码后的视频数据.构造时间戳,并将解码后的数据填充到CVPixelBufferRef中并将其转为CMSampleBufferRef.

- (void)startDecodeVideoDataWithAVPacket:(AVPacket)packet {
    if (packet.flags == 1 && m_isFindIDR == NO) {
        m_isFindIDR = YES;
        m_base_time =  m_videoFrame->pts;
    }
    
    if (m_isFindIDR == YES) {
        [self startDecodeVideoDataWithAVPacket:packet
                             videoCodecContext:m_videoCodecContext
                                    videoFrame:m_videoFrame
                                      baseTime:m_base_time
                              videoStreamIndex:m_videoStreamIndex];
    }
}

- (void)startDecodeVideoDataWithAVPacket:(AVPacket)packet videoCodecContext:(AVCodecContext *)videoCodecContext videoFrame:(AVFrame *)videoFrame baseTime:(int64_t)baseTime videoStreamIndex:(int)videoStreamIndex {
    Float64 current_timestamp = [self getCurrentTimestamp];
    AVStream *videoStream = m_formatContext->streams[videoStreamIndex];
    int fps = DecodeGetAVStreamFPSTimeBase(videoStream);
    
    
    avcodec_send_packet(videoCodecContext, &packet);
    while (0 == avcodec_receive_frame(videoCodecContext, videoFrame))
    {
        CVPixelBufferRef pixelBuffer = (CVPixelBufferRef)videoFrame->data[3];
        CMTime presentationTimeStamp = kCMTimeInvalid;
        int64_t originPTS = videoFrame->pts;
        int64_t newPTS    = originPTS - baseTime;
        presentationTimeStamp = CMTimeMakeWithSeconds(current_timestamp + newPTS * av_q2d(videoStream->time_base) , fps);
        CMSampleBufferRef sampleBufferRef = [self convertCVImageBufferRefToCMSampleBufferRef:(CVPixelBufferRef)pixelBuffer
                                                                   withPresentationTimeStamp:presentationTimeStamp];
        
        if (sampleBufferRef) {
            if ([self.delegate respondsToSelector:@selector(getDecodeVideoDataByFFmpeg:)]) {
                [self.delegate getDecodeVideoDataByFFmpeg:sampleBufferRef];
            }
            
            CFRelease(sampleBufferRef);
        }
    }
}

4. 停止解码

释放相关资源

- (void)stopDecoder {
    [self freeAllResources];
}

- (void)freeAllResources {
    if (m_videoCodecContext) {
        avcodec_send_packet(m_videoCodecContext, NULL);
        avcodec_flush_buffers(m_videoCodecContext);
        
        if (m_videoCodecContext->hw_device_ctx) {
            av_buffer_unref(&m_videoCodecContext->hw_device_ctx);
            m_videoCodecContext->hw_device_ctx = NULL;
        }
        avcodec_close(m_videoCodecContext);
        m_videoCodecContext = NULL;
    }
    
    if (m_videoFrame) {
        av_free(m_videoFrame);
        m_videoFrame = NULL;
    }
}
上一篇下一篇

猜你喜欢

热点阅读