34

iOS利用FFmpeg实现Video硬解码

 4 years ago
source link: https://www.tuicool.com/articles/IV77zmj
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

实现原理

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

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

阅读前提

代码地址 : Video Decoder

掘金地址 : Video Decoder

简书地址 : Video Decoder

博客地址 : Video Decoder

总体架构

简易流程

FFmpeg parse流程

  • 创建format context: avformat_alloc_context

  • 打开文件流: avformat_open_input

  • 寻找流信息: avformat_find_stream_info

  • 获取音视频流的索引值: formatContext->streams[i]->codecpar->codec_type == (isVideoStream ? AVMEDIA_TYPE_VIDEO : AVMEDIA_TYPE_AUDIO)

  • 获取音视频流: m_formatContext->streams[m_audioStreamIndex]

  • 解析音视频数据帧: av_read_frame

  • 获取extra data: av_bitstream_filter_filter

FFmpeg decode流程

  • 确定解码器类型: enum AVHWDeviceType av_hwdevice_find_type_by_name(const char *name)

  • 创建视频流: int av_find_best_stream(AVFormatContext *ic,enum FfmpegaVMediaType type,int wanted_stream_nb,int related_stream,AVCodec **decoder_ret,int flags);

  • 初始化解码器: AVCodecContext *avcodec_alloc_context3(const AVCodec *codec)

  • 填充解码器上下文: int avcodec_parameters_to_context(AVCodecContext *codec, const AVCodecParameters *par);

  • 打开指定类型的设备: int av_hwdevice_ctx_create(AVBufferRef **device_ctx, enum AVHWDeviceType type, const char *device, AVDictionary *opts, int flags)

  • 初始化编码器上下文对象: int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options)

  • 初始化视频帧: AVFrame *av_frame_alloc(void)

  • 找到第一个I帧开始解码: packet.flags == 1

  • 将parse到的压缩数据送给解码器: int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt)

  • 接收解码后的数据: int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame)

  • 构造时间戳

  • 将解码后的数据存到 CVPixelBufferRef 并将其转为 CMSampleBufferRef ,解码完成

文件结构

IvEryqf.png!web

快速使用

  • 初始化preview

- (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 FunctionAVBufferRef *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;
}复制代码
  • av_find_best_stream : 在文件中找到最佳流信息.

    • ic: 媒体文件

    • type: video, audio, subtitles...

    • wanted_stream_nb: 用户请求的流编号,-1表示自动选择

    • related_stream: 试着找到一个相关的流,如果没有可填-1

    • decoder_ret: 非空返回解码器引用

    • flags: 保留字段

  • avcodec_parameters_to_context: 根据提供的解码器参数中的值填充解码器上下文

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

  • av_hwdevice_ctx_create: 打开指定类型的设备并为其创建AVHWDeviceContext。

  • avcodec_open2: 使用给定的AVCodec初始化AVCodecContext,在使用此函数之前,必须使用avcodec_alloc_context3()分配内存。

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);
        }
    }
}复制代码
  • avcodec_send_packet: 将压缩视频帧数据送给解码器

    • AVERROR(EAGAIN): 当前状态下不接受输入,用户必须通过 avcodec_receive_frame() 读取输出的buffer. (一旦所有输出读取完毕,packet应该被重新发送,调用不会失败)

    • AVERROR_EOF: 解码器已经被刷新,没有新的packet能发送给它.

    • AVERROR(EINVAL): 解码器没有被打开

    • AVERROR(ENOMEM): 将Packet添加到内部队列失败.

  • avcodec_receive_frame: 从解码器中获取解码后的数据

    • AVERROR(EAGAIN): 输出不可用, 用户必须尝试发送一个新的输入数据

    • AVERROR_EOF: 解码器被完全刷新,这儿没有更多的输出帧

    • AVERROR(EINVAL): 解码器没有被打开.

    • 其他负数: 解码错误.

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;
    }
}复制代码

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK