需求
將編碼的視頻流解碼為原始視頻數(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)
快速使用
- 初始化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)部隊列失敗.
- AVERROR(EAGAIN): 當(dāng)前狀態(tà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;
}
}