1.背景介紹
學習FFMPEG有段時間了,FFMPEG對通用的視頻編解碼做了統一接口處理的抽象,比如在解碼處理時,無須關心其具體的編解碼格式,僅需關心其pixfmt即可。
FFMPEG使用時需要關心下面這些核心的結構體。
AVFormatContext // 封視頻格裝上下文,是處理編封裝功能的結構體
AVCodecContext // 解碼器上下文,是編解碼功能的結構體,存儲了gop/definition/pixfmt/profile/bit_rate等參數
AVCodec // 存儲編解碼器信息的結構體
AVFrame // 存儲解碼後的視音頻數據,包括yuv/pcm/宏塊數據/運動矢量等信息
AVPacket // 存儲編碼後的數據
SwsContext // 格式轉換
AVStream // 存儲每一個視頻/音頻流信息的結構體
2.解碼
下面代碼完成如下功能:
- 視頻解碼,包括帶透明度的webm解碼;
- scaler縮放,輸出yuv420;
#include <iostream>
#include "math.h"
#include <sys/syscall.h>
#include <sys/types.h>
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#include "stdint.h"
#ifndef UINT64_C
#define UINT64_C(value)__CONCAT(value,ULL)
#endif
#ifdef __cplusplus
extern "C"
{
#endif
#include <libavutil/frame.h>
#include <libavutil/mem.h>
#include <libavcodec/avcodec.h>
#include <libavutil/log.h>
#include <libavutil/mathematics.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
#include <libavutil/time.h>
#ifdef __cplusplus
}
#endif
int ffmpeg_video_dec(char* path_in, char* path_yuv_out)
{
AVFormatContext* AFCtx_p; // 解封裝上下文,是解封裝功能的結構體
AVCodecContext* ACCtx_p; // 解碼器上下文,是編解碼功能的結構體,存儲了gop/definition/pixfmt/profile/bit_rate等參數
AVCodec* codec_p; // 存儲編解碼器信息的結構體
AVFrame* pFrame; // 存儲解碼後的視音頻數據,包括yuv/pcm/宏塊數據/運動矢量等信息
AVFrame* pFrameyuv;
AVPacket* packet;
struct SwsContext* img_convert_ctx;
AVStream* stream; // 存儲每一個視頻/音頻流信息的結構體
int videoindex = -1;
int y_size;
FILE* fp_yuv = fopen(path_yuv_out, "w+");
av_register_all(); // 初始化FFMPEG,註冊所有模塊,調用了這個才能正常使用編碼器和解碼器
// Allocate an AVFormatContext.
AFCtx_p = avformat_alloc_context(); // 創建AFCtx_p
avformat_network_init(); // 初始化網絡庫
// 打開stream文件並且read header,將文件信息存入解封裝上下文
int open_stream_ret = avformat_open_input(&AFCtx_p, path_in, NULL, NULL);
if (open_stream_ret != 0)
{
fprintf(stderr, "open failed[%d].\n", open_stream_ret);
return -1;
}
// 獲取視頻流信息
int find_stream_ret = avformat_find_stream_info(AFCtx_p, NULL);
if (find_stream_ret < 0)
{
fprintf(stderr, "stream find failed[%d].\n", find_stream_ret);
return -1;
}
// dump調試信息
av_dump_format(AFCtx_p, 0, path_in, 0);
// 獲取metadata字典
AVDictionaryEntry* entry = nullptr;
// webm透明通道標識
int alpha_flag = 0;
//打開視頻並且獲取了視頻流,設置視頻索引默認值
for (int i = 0; i < AFCtx_p->nb_streams; i++)
{
if (AFCtx_p->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
{
fprintf(stderr, "nb_streams[%d], videoindex[%d].\n", AFCtx_p->nb_streams, i);
videoindex = i;
}
stream = AFCtx_p->streams[i];
while ((entry = av_dict_get(AFCtx_p->streams[i]->metadata, "", entry, AV_DICT_IGNORE_SUFFIX))) {
if((!strcmp(entry->key, "ALPHA_MODE")) && (!strcmp(entry->value, "1"))) {
alpha_flag = 1;
fprintf(stderr, "webm alpha.\n");
} else {
alpha_flag = 0;
}
fprintf(stdout, "key: %s, value: %s\n", entry->key, entry->value);
}
fprintf(stderr, "stream[%d], num[%d], den[%d], frame_num[%ld].\n", i, stream->avg_frame_rate.num, stream->avg_frame_rate.den, stream->nb_frames);
}
// 如果沒有找到視頻索引,說明不是一個視頻文件
if (videoindex == -1)
{
fprintf(stderr, "not a video.\n");
return -1;
}
// 分配解碼器上下文空間
ACCtx_p = avcodec_alloc_context3(NULL);
// 從封裝上下文中獲取編解碼器上下文信息
int codec_get_ret = avcodec_parameters_to_context(ACCtx_p, AFCtx_p->streams[videoindex]->codecpar);
if (codec_get_ret < 0)
{
fprintf(stderr, "copy stream failed[%d].\n", codec_get_ret);
return -1;
}
// 查找解碼器
fprintf(stderr, "codec_id[%d].\n", ACCtx_p->codec_id);
fprintf(stderr, "timebase[%d/%d].\n", ACCtx_p->pkt_timebase.num, ACCtx_p->pkt_timebase.den);
if(1 == alpha_flag) {
if(AV_CODEC_ID_VP8 == ACCtx_p->codec_id) {
codec_p = avcodec_find_decoder_by_name("libvpx"); // vp8-alpha
} else if(AV_CODEC_ID_VP9 == ACCtx_p->codec_id) {
codec_p = avcodec_find_decoder_by_name("libvpx-vp9"); // vp9-alpha
} else {
codec_p = avcodec_find_decoder(ACCtx_p->codec_id); // 不帶alpha
}
} else {
// codec_p = avcodec_find_decoder_by_name("h264_cuvid");
codec_p = avcodec_find_decoder(ACCtx_p->codec_id); // 不帶alpha
}
if (!codec_p)
{
fprintf(stderr, "find decoder error.\n");
return -1;
}
// 打開解碼器
int open_codec_ret = avcodec_open2(ACCtx_p, codec_p, NULL);
if (open_codec_ret != 0)
{
fprintf(stderr, "open codec failed[%d].\n", open_codec_ret);
return -1;
}
// 分配AVPacket/AVFrame
packet = av_packet_alloc();
pFrame = av_frame_alloc();
pFrameyuv = av_frame_alloc();
// 獲取轉換後YUV數據的大小
int dst_width = ACCtx_p->width / 2 * 2;
int dst_height = ACCtx_p->height / 2 * 2;
int video_size = dst_width * dst_height;
uint8_t* buf = NULL;
// 裁剪圖像
fprintf(stderr, "src_width[%d], src_height[%d], dst_width[%d], dst_height[%d], pix_fmt[%d].\n",
ACCtx_p->width, ACCtx_p->height, dst_width, dst_height, ACCtx_p->pix_fmt);
// 根據格式分配scale操作相關的上下文SwsContext
if((AV_PIX_FMT_YUV420P == ACCtx_p->pix_fmt && alpha_flag == 1) || AV_PIX_FMT_RGBA == ACCtx_p->pix_fmt || AV_PIX_FMT_ARGB == ACCtx_p->pix_fmt || AV_PIX_FMT_YUVA444P12LE == ACCtx_p->pix_fmt || AV_PIX_FMT_PAL8 == ACCtx_p->pix_fmt) {
fprintf(stderr, "this is rgba/yuva data, pixfmt[%d].\n", ACCtx_p->pix_fmt);
// 這種格式libvpx解碼時會轉換爲yuva420
if((AV_PIX_FMT_YUV420P == ACCtx_p->pix_fmt && alpha_flag == 1)) {
img_convert_ctx = sws_getContext(ACCtx_p->width, ACCtx_p->height, AV_PIX_FMT_YUVA420P,
dst_width, dst_height, AV_PIX_FMT_YUVA420P, SWS_BICUBIC, NULL, NULL, NULL);
} else {
img_convert_ctx = sws_getContext(ACCtx_p->width, ACCtx_p->height, ACCtx_p->pix_fmt,
dst_width, dst_height, AV_PIX_FMT_YUVA420P, SWS_BICUBIC, NULL, NULL, NULL);
}
video_size = av_image_get_buffer_size(AV_PIX_FMT_YUVA420P, dst_width, dst_height, 1);
buf = (uint8_t*)av_malloc(video_size);
av_image_fill_arrays(pFrameyuv->data, pFrameyuv->linesize,
buf, AV_PIX_FMT_YUVA420P, dst_width, dst_height, 1);
} else {
fprintf(stderr, "this is rgb/yuv data, pixfmt[%d].\n", ACCtx_p->pix_fmt);
img_convert_ctx = sws_getContext(ACCtx_p->width, ACCtx_p->height, ACCtx_p->pix_fmt,
dst_width, dst_height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);
video_size = av_image_get_buffer_size(AV_PIX_FMT_YUV420P, dst_width, dst_height, 1);
buf = (uint8_t*)av_malloc(video_size);
av_image_fill_arrays(pFrameyuv->data, pFrameyuv->linesize,
buf, AV_PIX_FMT_YUV420P, dst_width, dst_height, 1);
}
if (!img_convert_ctx)
{
fprintf(stderr, "get swscale context failed.\n");
return -1;
}
// 循環讀取幀數據並轉換寫入
while (av_read_frame(AFCtx_p, packet) >= 0)
{
if (packet->stream_index == videoindex)
{
if (avcodec_send_packet(ACCtx_p, packet) != 0)
{
fprintf(stderr, "send video stream packet failed.\n");
return -1;
}
if (avcodec_receive_frame(ACCtx_p, pFrame) != 0)
{
fprintf(stderr, "receive video frame failed, status = %d.\n", avcodec_receive_frame(ACCtx_p, pFrame));
continue;
}
fprintf(stderr, "decoding frame %d.\n", ACCtx_p->frame_number);
sws_scale(img_convert_ctx, pFrame->data, pFrame->linesize,
0, ACCtx_p->height, pFrameyuv->data, pFrameyuv->linesize);
// 視頻編輯即對pFrameyuv->data進行編輯
y_size = dst_width * dst_height;
fwrite(pFrameyuv->data[0], 1, y_size, fp_yuv); //Y
fwrite(pFrameyuv->data[1], 1, y_size/4, fp_yuv); //U
fwrite(pFrameyuv->data[2], 1, y_size/4, fp_yuv); //V
if(AV_PIX_FMT_YUVA420P == ACCtx_p->pix_fmt || AV_PIX_FMT_RGBA == ACCtx_p->pix_fmt || AV_PIX_FMT_ARGB == ACCtx_p->pix_fmt || AV_PIX_FMT_YUVA444P12LE == ACCtx_p->pix_fmt || AV_PIX_FMT_PAL8 == ACCtx_p->pix_fmt) {
fwrite(pFrameyuv->data[3], 1, y_size, fp_yuv); //A
}
fflush(fp_yuv);
}
}
fclose(fp_yuv);
av_free(buf);
av_frame_free(&pFrame);
av_frame_free(&pFrameyuv);
av_packet_free(&packet);
sws_freeContext(img_convert_ctx);
avcodec_free_context(&ACCtx_p);
avformat_close_input(&AFCtx_p);
avformat_free_context(AFCtx_p);
return 1;
}
// g++ test_gpu_dec.cpp -L /usr/local/lib -lavcodec -lavformat -lavutil -lswscale -I /usr/local/include -o test_dec
// ./test_enc xxx.h264 xxx.yuv 10
int main(int argc, char** argv)
{
if(argc < 3)
fprintf(stderr, "input argc not enough.\n");
for(int i = 0; i < atoi(argv[3]); i++) {
fprintf(stderr, "i[%d].\n", i);
ffmpeg_video_dec(argv[1], argv[2]);
}
return 0;
}
3.編譯及運行
服務端使用g++編譯,生成可執行文件,運行時,指定輸入的h264文件、輸出的yuv文件以及運行次數,這裏運行10次,至此,解碼部分測試完成。
g++ test_gpu_dec.cpp -L /usr/local/lib -lavcodec -lavformat -lavutil -lswscale -I /usr/local/include -o test_dec
./test_enc xxx.h264 xxx.yuv 10 # 調用10次