iOS利用FFmpeg實現(xiàn)Video硬解碼

需求

將編碼的視頻流解碼為原始視頻數(shù)據(jù),編碼視頻流可以來自網(wǎng)絡(luò)流或文件,解碼后即可渲染到屏幕.


實現(xiàn)原理

正如我們所知,編碼數(shù)據(jù)僅用于傳輸,無法直接渲染到屏幕上,所以這里利用FFmpeg解析文件中的編碼的視頻流,并將壓縮視頻數(shù)據(jù)(h264/h265)解碼為指定格式(yuv,RGB)的視頻原始數(shù)據(jù),以渲染到屏幕上.

注意: 本例主要為解碼,需要借助FFmpeg搭建模塊,視頻解析模塊,渲染模塊,這些模塊在下面閱讀前提皆有鏈接可直接訪問.


閱讀前提


代碼地址 : Video Decoder

掘金地址 : Video Decoder

簡書地址 : Video Decoder

博客地址 : Video Decoder


總體架構(gòu)

簡易流程

FFmpeg parse流程

  • 創(chuàng)建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]
  • 解析音視頻數(shù)據(jù)幀: av_read_frame
  • 獲取extra data: av_bitstream_filter_filter

FFmpeg decode流程

  • 確定解碼器類型: enum AVHWDeviceType av_hwdevice_find_type_by_name(const char *name)
  • 創(chuàng)建視頻流: 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);
  • 打開指定類型的設(shè)備: 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到的壓縮數(shù)據(jù)送給解碼器: int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt)
  • 接收解碼后的數(shù)據(jù): int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame)
  • 構(gòu)造時間戳
  • 將解碼后的數(shù)據(jù)存到CVPixelBufferRef并將其轉(zhuǎn)為CMSampleBufferRef,解碼完成

文件結(jié)構(gòu)

image

快速使用

  • 初始化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];
}
  • 解析并解碼文件中視頻數(shù)據(jù)
- (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];
        }
    }];
}
  • 將解碼后數(shù)據(jù)渲染到屏幕上
-(void)getDecodeVideoDataByFFmpeg:(CMSampleBufferRef)sampleBuffer {
    CVPixelBufferRef pix = CMSampleBufferGetImageBuffer(sampleBuffer);
    [self.previewView displayPixelBuffer:pix];
}

具體實現(xiàn)

1. 初始化實例對象

因為本例中的視頻數(shù)據(jù)源是文件,而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];
    // 創(chuàng)建解碼器上下文對象
    m_videoCodecContext = [self createVideoEncderWithFormatContext:m_formatContext
                                                            stream:videoStream
                                                  videoStreamIndex:m_videoStreamIndex];
    if (!m_videoCodecContext) {
        log4cplus_error(kModuleName, "%s: create video codec failed",__func__);
        return;
    }
    
    // 創(chuàng)建視頻幀
    m_videoFrame = av_frame_alloc();
    if (!m_videoFrame) {
        log4cplus_error(kModuleName, "%s: alloc video frame failed",__func__);
        avcodec_close(m_videoCodecContext);
    }
}
2.1. 創(chuàng)建解碼器上下文對象
- (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);
    // 將解碼器名稱轉(zhuǎn)為對應(yīng)的枚舉類型
    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;
    }
    
    // 根據(jù)解碼器枚舉類型找到解碼器
    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;
    }
    
    // 為解碼器上下文對象分配內(nèi)存
    codecContext = avcodec_alloc_context3(codec);
    if (!codecContext){
        log4cplus_error(kModuleName, "avcodec_alloc_context3 faliture");
        return NULL;
    }
    
    // 將視頻流中的參數(shù)填充到視頻解碼器中
    ret = avcodec_parameters_to_context(codecContext, formatContext->streams[videoStreamIndex]->codecpar);
    if (ret < 0){
        log4cplus_error(kModuleName, "avcodec_parameters_to_context faliture");
        return NULL;
    }
    
    // 創(chuàng)建硬件解碼器上下文
    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;
}
  • av_find_best_stream : 在文件中找到最佳流信息.

    • ic: 媒體文件
    • type: video, audio, subtitles...
    • wanted_stream_nb: 用戶請求的流編號,-1表示自動選擇
    • related_stream: 試著找到一個相關(guān)的流,如果沒有可填-1
    • decoder_ret: 非空返回解碼器引用
    • flags: 保留字段
  • avcodec_parameters_to_context: 根據(jù)提供的解碼器參數(shù)中的值填充解碼器上下文

僅僅將解碼器中具有相應(yīng)字段的任何已分配字段par被釋放并替換為par中相應(yīng)字段的副本对竣。不涉及解碼器中沒有par中對應(yīng)項的字段党涕。

  • av_hwdevice_ctx_create: 打開指定類型的設(shè)備并為其創(chuàng)建AVHWDeviceContext码泛。
  • avcodec_open2: 使用給定的AVCodec初始化AVCodecContext,在使用此函數(shù)之前,必須使用avcodec_alloc_context3()分配內(nèi)存。
int av_find_best_stream(AVFormatContext *ic,
                        enum FfmpegaVMediaType type,
                        int wanted_stream_nb,
                        int related_stream,
                        AVCodec **decoder_ret,
                        int flags);
2.2. 創(chuàng)建視頻幀

AVFrame 作為解碼后原始的音視頻數(shù)據(jù)的容器.AVFrame通常被分配一次然后多次重復(fù)(例如摄咆,單個AVFrame以保持從解碼器接收的幀)袁波。在這種情況下,av_frame_unref()將釋放框架所持有的任何引用披泪,并在再次重用之前將其重置為其原始的清理狀態(tài)纤子。

    // 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. 開始解碼

首先找到編碼數(shù)據(jù)流中第一個I幀, 然后調(diào)用avcodec_send_packet將壓縮數(shù)據(jù)發(fā)送給解碼器.最后利用循環(huán)接收avcodec_receive_frame解碼后的視頻數(shù)據(jù).構(gòu)造時間戳,并將解碼后的數(shù)據(jù)填充到CVPixelBufferRef中并將其轉(zhuǎn)為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: 將壓縮視頻幀數(shù)據(jù)送給解碼器

    • AVERROR(EAGAIN): 當(dāng)前狀態(tài)下不接受輸入,用戶必須通過avcodec_receive_frame()讀取輸出的buffer. (一旦所有輸出讀取完畢,packet應(yīng)該被重新發(fā)送,調(diào)用不會失敗)
    • AVERROR_EOF: 解碼器已經(jīng)被刷新,沒有新的packet能發(fā)送給它.
    • AVERROR(EINVAL): 解碼器沒有被打開
    • AVERROR(ENOMEM): 將Packet添加到內(nèi)部隊列失敗.
  • avcodec_receive_frame: 從解碼器中獲取解碼后的數(shù)據(jù)

    • AVERROR(EAGAIN): 輸出不可用, 用戶必須嘗試發(fā)送一個新的輸入數(shù)據(jù)
    • AVERROR_EOF: 解碼器被完全刷新,這兒沒有更多的輸出幀
    • AVERROR(EINVAL): 解碼器沒有被打開.
    • 其他負數(shù): 解碼錯誤.

4. 停止解碼

釋放相關(guān)資源

- (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;
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子控硼,更是在濱河造成了極大的恐慌泽论,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件卡乾,死亡現(xiàn)場離奇詭異翼悴,居然都是意外死亡,警方通過查閱死者的電腦和手機幔妨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進店門鹦赎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人误堡,你說我怎么就攤上這事古话。” “怎么了锁施?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵陪踩,是天一觀的道長。 經(jīng)常有香客問我悉抵,道長膊毁,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任基跑,我火速辦了婚禮婚温,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘媳否。我一直安慰自己栅螟,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布篱竭。 她就那樣靜靜地躺著力图,像睡著了一般。 火紅的嫁衣襯著肌膚如雪掺逼。 梳的紋絲不亂的頭發(fā)上吃媒,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天,我揣著相機與錄音吕喘,去河邊找鬼赘那。 笑死,一個胖子當(dāng)著我的面吹牛氯质,可吹牛的內(nèi)容都是我干的募舟。 我是一名探鬼主播,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼闻察,長吁一口氣:“原來是場噩夢啊……” “哼拱礁!你這毒婦竟也來了琢锋?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤呢灶,失蹤者是張志新(化名)和其女友劉穎吴超,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鸯乃,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡鲸阻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了飒责。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡仆潮,死狀恐怖宏蛉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情性置,我是刑警寧澤拾并,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站鹏浅,受9級特大地震影響嗅义,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜隐砸,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一之碗、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧季希,春花似錦褪那、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至峰尝,卻和暖如春偏窝,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背武学。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工祭往, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人火窒。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓链沼,卻偏偏與公主長得像,于是被迫代替她去往敵國和親沛鸵。 傳聞我的和親對象是個殘疾皇子括勺,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,828評論 2 345

推薦閱讀更多精彩內(nèi)容