一、概述
實現了讀入本地yuv文件,通過libx265編碼為H265格式,并存儲到本地文件中
二、實現流程
準備文件
在build
路徑下準備yuv文件
在項目中添加文件參數,輸出為h265文件,使用libx265編碼
初始化解碼器
- 通過傳進來的
libx265
找到指定的編碼器
codec = avcodec_find_encoder_by_name(codec_name);
if (!codec) {fprintf(stderr, "Codec '%s' not found\n", codec_name);exit(1);
}
- 為編碼器分配上下文
codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {fprintf(stderr, "Could not allocate video codec context\n");exit(1);
}
- 綁定解碼器和解碼器上下文
ret = avcodec_open2(codec_ctx, codec, NULL);
if (ret < 0) {fprintf(stderr, "Could not open codec: %s\n", av_err2str(ret));exit(1);
}
- 設置編碼的視頻參數,如分辨率,幀率,時間基、比特率等
#define ENCODE_TIME_BASE 1000 // 設置時間基數,編碼需要根據時間來判斷碼率
#define ENCODE_FRAME_RATE 25 // 設置幀率
#define YUV_WIDTH 1280
#define YUV_HEIGH 720/* 設置分辨率*/
codec_ctx->width = YUV_WIDTH; // 根據實際去寫入
codec_ctx->height = YUV_HEIGH;
/* 設置time base */
codec_ctx->time_base = (AVRational){1, ENCODE_TIME_BASE}; // 1/1000
codec_ctx->framerate = (AVRational){ENCODE_FRAME_RATE, 1};
codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
- 也可以設置編碼的碼率,可以設置一個碼率的范圍
- 也可以設置編碼的緩存大小,用于在碼率波動的時候做緩存
codec_ctx->bit_rate = 3000000;codec_ctx->rc_max_rate = 3000000;
codec_ctx->rc_min_rate = 3000000;codec_ctx->rc_buffer_size = 2000000;
- 配置編碼器的高級參數,這里指的是而外的設置
- 比如編碼模式,GOP大小,B幀大小、I幀間隔、編碼
profile
級別、畫質等等 - preset參數是影響編碼速度的,比如ultrafast編碼最快,但是畫質最差
codec_ctx->gop_size = 25;
codec_ctx->max_b_frames = 0;
codec_ctx->keyint_min = 25;ret = av_opt_set(codec_ctx->priv_data, "preset", "medium", 0);
ret = av_opt_set(codec_ctx->priv_data, "profile", "main", 0);
ret = av_opt_set(codec_ctx->priv_data, "tune","film",0);
- 也可以通過av_opt_set的方法,將修改的內容直接發送到
libx265
編碼器內部進行修改
ret = av_opt_set(codec_ctx->priv_data, "x265-param", "--keyint=25, --bframes=2",0);
ret = av_opt_set(codec_ctx->priv_data, "x265-params", "--keyint=25:--frame-threads=4", 0);
ret = av_opt_set(codec_ctx->priv_data, "x265-params", "--keyint=25:--bframes=2", 0);
ret = av_opt_set(codec_ctx->priv_data, "x265-params", "--keyint=25:--frame-threads=4", 0);
- 比如我們設置了B幀為0,并且關閉了多線程,此時的延遲就為0了,觀察如下
- 可以發現,一幀數據傳入編碼器,立馬就編碼出一幀數據
- 初始之外,還可以通過設置零延遲的方法,當同時必須關閉多線程
ret = av_opt_set(codec_ctx->priv_data, "tune","zerolatency",0);
- 如果開啟多線程,就會存在編碼延遲,不過可以提高編碼速度
- 打印發現,傳入多幀數據后,才從編碼器中取出編碼后的幀
ret = av_opt_set(codec_ctx->priv_data, "x265-params", "--keyint=25:--frame-threads=4", 0);
- 也可以這樣設置多線程
codec_ctx->thread_count = 4; // 開了多線程后也會導致幀輸出延遲, 需要緩存thread_count幀后再編程。
codec_ctx->thread_type = FF_THREAD_FRAME; // 并 設置為FF_THREAD_FRAME
- 以下的設置,是將SPS、PPS、VPS放入擴展變量里面,即
codec->extradata
里面 - 此時就不會而外編碼一幀了,觀察打印結果
codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
啟動后,沒發現SPS、PPS、VPS
關閉后,前幾幀為SPS、PPS、VPS
更多的libx265的設置,可以使用對應的命令查詢
ffmpeg -h encoder=libx265 > libx265.log
編碼H265
- 打開輸入、輸出文件
// 打開輸入和輸出文件
infile = fopen(in_yuv_file, "rb");
if (!infile) {fprintf(stderr, "Could not open %s\n", in_yuv_file);exit(1);
}
outfile = fopen(out_h264_h265_file, "wb");
if (!outfile) {fprintf(stderr, "Could not open %s\n", out_h264_h265_file);exit(1);
}
- 分配對應的AVFrame和AVPacket的內存
// 分配pkt和frame
pkt = av_packet_alloc();
if (!pkt) {fprintf(stderr, "Could not allocate video frame\n");exit(1);
}
frame = av_frame_alloc();
if (!frame) {fprintf(stderr, "Could not allocate video frame\n");exit(1);
}
- 設置frame的參數,如分辨率、像素
// 為frame分配buffer
frame->format = codec_ctx->pix_fmt;
frame->width = codec_ctx->width;
frame->height = codec_ctx->height;ret = av_frame_get_buffer(frame, 0);
if (ret < 0) {fprintf(stderr, "Could not allocate the video frame data\n");exit(1);
}
- 通過分辨率和像素格式,計算出一幀數據對應的buffer大小,并且分配對應的緩沖區
int frame_bytes = av_image_get_buffer_size(frame->format, frame->width,frame->height, 1);
uint8_t *yuv_buf = (uint8_t *)malloc(frame_bytes);
- 循環編碼文件,直到讀到文件結束
av_frame_make_writable
確保幀可用,不開啟的話可能會寫入失敗
for (;;) {memset(yuv_buf, 0, frame_bytes);size_t read_bytes = fread(yuv_buf, 1, frame_bytes, infile);if(read_bytes <= 0) {printf("read file finish\n");break;}/* 確保該frame可寫, 如果編碼器內部保持了內存參考計數,則需要重新拷貝一個備份目的是新寫入的數據和編碼器保存的數據不能產生沖突*/ret = av_frame_make_writable(frame);if(ret != 0) {printf("av_frame_make_writable failed, ret = %d\n", ret);break;}int need_size = av_image_fill_arrays(frame->data, frame->linesize, yuv_buf,frame->format,frame->width, frame->height, 1);if(need_size != frame_bytes) {printf("av_image_fill_arrays failed, need_size:%d, frame_bytes:%d\n",need_size, frame_bytes);break;}pts += (ENCODE_TIME_BASE/ENCODE_FRAME_RATE);// 設置ptsframe->pts = pts; // 使用采樣率作為pts的單位,具體換算成秒 pts*1/采樣率begin_time = get_time();ret = encode(codec_ctx, frame, pkt, outfile);end_time = get_time();printf("encode time:%lldms\n", end_time - begin_time);if(ret < 0) {printf("encode failed\n");break;}
}
encode
函數如下,主要是送入一幀數據,然后循環讀取AVPacket
,直到輸出AVERROR_EOF
表示當前幀編碼完成
static int encode(AVCodecContext *enc_ctx, AVFrame *frame, AVPacket *pkt,FILE *outfile)
{int ret;/* send the frame to the encoder */if (frame)printf("send frame pts:%3"PRId64"\n", frame->pts);/* 通過查閱代碼,使用x265進行編碼時,具體緩存幀是在x265源碼進行,* 不會增加avframe對應buffer的reference*/ret = avcodec_send_frame(enc_ctx, frame);if (ret < 0){fprintf(stderr, "Error sending a frame for encoding\n");return -1;}while (ret >= 0){ret = avcodec_receive_packet(enc_ctx, pkt);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {return 0;}else if (ret < 0) {fprintf(stderr, "Error encoding audio frame\n");return -1;}printf("avcodec_receive_packet:pts:%3"PRId64" dts:%3"PRId64" (size:%5d)\n", pkt->pts, pkt->dts, pkt->size);fwrite(pkt->data, 1, pkt->size, outfile); print_h265_nal_unit_type(pkt->data, pkt->size); // 只針對H265}return 0;
}
print_h265_nal_unit_type
函數主要是通過H265的結構,找到startcode,然后解析NALU的頭部,用于輸出調試
void print_h265_nal_unit_type(uint8_t *data, size_t size)
{int i = 0;while (i+3 < size ) {if(data[i] == 0 && data[i+1]==0 && data[i+2] == 0 && data[i+3] == 1 ) {i += 4;printf("%02x nal_type:%d, pos:%d\n", data[i],(data[i]&0x7e)>>1, i);continue;}if(data[i] == 0 && data[i+1]==0 && data[i+2] == 1) {i += 3;printf("%02x nal_type:%d, pos:%d\n",data[i], (data[i]&0x7e)>>1, i);continue;}i++;}
}
沖刷編碼器
- 編碼結束后,編碼器內部可能還緩存了部分幀,此時需要傳入NULL,將編碼器中剩余的幀沖刷出來
/* 沖刷編碼器 */
encode(codec_ctx, NULL, pkt, outfile);
關閉文件
- 關閉文件、并且釋放相關內存,確保正確結束程序
// 關閉文件
fclose(infile);
fclose(outfile);// 釋放內存
if(yuv_buf) {free(yuv_buf);
}av_frame_free(&frame);
av_packet_free(&pkt);
avcodec_free_context(&codec_ctx);
完整代碼
/**
* @projectName 02-encode_h265
* @brief 視頻編碼,從本地讀取YUV數據進行H265編碼
* @author Liao Qingfu
* @date 2022-09-16
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#include <libavcodec/avcodec.h>
#include <libavutil/time.h>
#include <libavutil/opt.h>
#include <libavutil/imgutils.h>
#define ENCODE_TIME_BASE 1000 // 設置時間基數,編碼需要根據時間來判斷碼率
#define ENCODE_FRAME_RATE 25 // 設置幀率
#define YUV_WIDTH 1280
#define YUV_HEIGH 720void print_h265_nal_unit_type(uint8_t *data, size_t size);
int64_t get_time()
{return av_gettime_relative() / 1000; // 換算成毫秒
}
static int encode(AVCodecContext *enc_ctx, AVFrame *frame, AVPacket *pkt,FILE *outfile)
{int ret;/* send the frame to the encoder */if (frame)printf("send frame pts:%3"PRId64"\n", frame->pts);/* 通過查閱代碼,使用x264進行編碼時,具體緩存幀是在x264源碼進行,* 不會增加avframe對應buffer的reference*/ret = avcodec_send_frame(enc_ctx, frame);if (ret < 0){fprintf(stderr, "Error sending a frame for encoding\n");return -1;}while (ret >= 0){ret = avcodec_receive_packet(enc_ctx, pkt);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {return 0;}else if (ret < 0) {fprintf(stderr, "Error encoding audio frame\n");return -1;}printf("avcodec_receive_packet:pts:%3"PRId64" dts:%3"PRId64" (size:%5d)\n", pkt->pts, pkt->dts, pkt->size);fwrite(pkt->data, 1, pkt->size, outfile); print_h265_nal_unit_type(pkt->data, pkt->size); // 只針對H265}return 0;
}
/*** @brief 提取測試文件:ffmpeg -i test_1280x720.flv -t 5 -r 25 -pix_fmt yuv420p yuv420p_1280x720.yuv* 參數輸入: yuv420p_1280x720.yuv yuv420p_1280x720.h265 libx265* @param argc* @param argv* @return*/
int main(int argc, char **argv)
{char *in_yuv_file = NULL;char *out_h264_h265_file = NULL;FILE *infile = NULL;FILE *outfile = NULL;const char *codec_name = NULL;const AVCodec *codec = NULL;AVCodecContext *codec_ctx= NULL;AVFrame *frame = NULL;AVPacket *pkt = NULL;int ret = 0;if (argc < 4) {fprintf(stderr, "Usage: %s <input_file out_file codec_name >, argc:%d\n",argv[0], argc);return 0;}in_yuv_file = argv[1]; // 輸入YUV文件out_h264_h265_file = argv[2];codec_name = argv[3];/* 查找指定的編碼器 */codec = avcodec_find_encoder_by_name(codec_name);if (!codec) {fprintf(stderr, "Codec '%s' not found\n", codec_name);exit(1);}codec_ctx = avcodec_alloc_context3(codec);if (!codec_ctx) {fprintf(stderr, "Could not allocate video codec context\n");exit(1);}/* 設置分辨率*/codec_ctx->width = YUV_WIDTH; // 根據實際去寫入codec_ctx->height = YUV_HEIGH;/* 設置time base */codec_ctx->time_base = (AVRational){1, ENCODE_TIME_BASE}; // 1/1000codec_ctx->framerate = (AVRational){ENCODE_FRAME_RATE, 1};/* 設置I幀間隔* 如果frame->pict_type設置為AV_PICTURE_TYPE_I, 則忽略gop_size的設置,一直當做I幀進行編碼*/codec_ctx->gop_size = 25; // I幀間隔, H265單獨設置這里不起作用codec_ctx->keyint_min = 25;codec_ctx->max_b_frames = 0; // 如果不想包含B幀則設置為0codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;//if (codec->id == AV_CODEC_ID_H264) {// 相關的參數可以參考libx264.c的 AVOption options// ultrafast all encode time:2270ms// medium all encode time:5815ms// veryslow all encode time:19836msret = av_opt_set(codec_ctx->priv_data, "preset", "medium", 0);if(ret != 0) {printf("av_opt_set preset failed\n");}ret = av_opt_set(codec_ctx->priv_data, "profile", "main", 0); // 默認是highif(ret != 0) {printf("av_opt_set profile failed\n");}
// ret = av_opt_set(codec_ctx->priv_data, "tune","zerolatency",0); // 直播是才使用該設置
// ret = av_opt_set(codec_ctx->priv_data, "tune","film",0); // 畫質filmif(ret != 0) {printf("av_opt_set tune failed\n");}}else if (codec->id == AV_CODEC_ID_H265) {// 相關的參數可以參考libx265.c的 AVOption options// ultrafast all encode time:// medium all encode time:// veryslow all encode time:ret = av_opt_set(codec_ctx->priv_data, "preset", "medium", 0);if(ret != 0) {printf("av_opt_set preset failed\n");}ret = av_opt_set(codec_ctx->priv_data, "profile", "main", 0); // 默認是highif(ret != 0) {printf("av_opt_set profile failed\n");}ret = av_opt_set(codec_ctx->priv_data, "tune","zerolatency",0); // 直播是才使用該設置
// ret = av_opt_set(codec_ctx->priv_data, "tune","film",0); // 畫質filmif(ret != 0) {printf("av_opt_set tune failed\n");}// libx265/source\common\param.cpp
// ret = av_opt_set(codec_ctx->priv_data, "x265-param", "--keyint=25, --bframes=2",0);
// ret = av_opt_set(codec_ctx->priv_data, "x265-params", "--keyint=25:--frame-threads=4", 0);
// ret = av_opt_set(codec_ctx->priv_data, "x265-params", "--keyint=25:--bframes=2", 0);
// ret = av_opt_set(codec_ctx->priv_data, "x265-params", "--keyint=25:--frame-threads=4", 0);if(ret != 0) {printf("av_opt_set x265-param failed\n");return -1;}} else {printf("no support the codec :%s\n", codec_name);return -1;}/** 設置編碼器參數*//* 設置bitrate */codec_ctx->bit_rate = 3000000; //3000k
// codec_ctx->rc_max_rate = 3000000;
// codec_ctx->rc_min_rate = 3000000;
// codec_ctx->rc_buffer_size = 2000000;
// codec_ctx->thread_count = 4; // 開了多線程后也會導致幀輸出延遲, 需要緩存thread_count幀后再編程。
// codec_ctx->thread_type = FF_THREAD_FRAME; // 并 設置為FF_THREAD_FRAME/* 對于H264 AV_CODEC_FLAG_GLOBAL_HEADER 設置則只包含I幀,此時sps pps需要從codec_ctx->extradata讀取* 不設置則每個I幀都帶 sps pps sei*/codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; // 存本地文件時不要去設置/* 將codec_ctx和codec進行綁定 */ret = avcodec_open2(codec_ctx, codec, NULL);if (ret < 0) {fprintf(stderr, "Could not open codec: %s\n", av_err2str(ret));exit(1);}printf("thread_count: %d, thread_type:%d\n", codec_ctx->thread_count, codec_ctx->thread_type);// 打開輸入和輸出文件infile = fopen(in_yuv_file, "rb");if (!infile) {fprintf(stderr, "Could not open %s\n", in_yuv_file);exit(1);}outfile = fopen(out_h264_h265_file, "wb");if (!outfile) {fprintf(stderr, "Could not open %s\n", out_h264_h265_file);exit(1);}// 分配pkt和framepkt = av_packet_alloc();if (!pkt) {fprintf(stderr, "Could not allocate video frame\n");exit(1);}frame = av_frame_alloc();if (!frame) {fprintf(stderr, "Could not allocate video frame\n");exit(1);}// 為frame分配bufferframe->format = codec_ctx->pix_fmt;frame->width = codec_ctx->width;frame->height = codec_ctx->height;ret = av_frame_get_buffer(frame, 0);if (ret < 0) {fprintf(stderr, "Could not allocate the video frame data\n");exit(1);}// 計算出每一幀的數據 像素格式 * 寬 * 高// 1382400int frame_bytes = av_image_get_buffer_size(frame->format, frame->width,frame->height, 1);printf("frame_bytes %d\n", frame_bytes);uint8_t *yuv_buf = (uint8_t *)malloc(frame_bytes);if(!yuv_buf) {printf("yuv_buf malloc failed\n");return 1;}int64_t begin_time = get_time();int64_t end_time = begin_time;int64_t all_begin_time = get_time();int64_t all_end_time = all_begin_time;int64_t pts = 0;printf("start enode\n");for (;;) {memset(yuv_buf, 0, frame_bytes);size_t read_bytes = fread(yuv_buf, 1, frame_bytes, infile);if(read_bytes <= 0) {printf("read file finish\n");break;}/* 確保該frame可寫, 如果編碼器內部保持了內存參考計數,則需要重新拷貝一個備份目的是新寫入的數據和編碼器保存的數據不能產生沖突*/ret = av_frame_make_writable(frame);if(ret != 0) {printf("av_frame_make_writable failed, ret = %d\n", ret);break;}int need_size = av_image_fill_arrays(frame->data, frame->linesize, yuv_buf,frame->format,frame->width, frame->height, 1);if(need_size != frame_bytes) {printf("av_image_fill_arrays failed, need_size:%d, frame_bytes:%d\n",need_size, frame_bytes);break;}pts += (ENCODE_TIME_BASE/ENCODE_FRAME_RATE);// 設置ptsframe->pts = pts; // 使用采樣率作為pts的單位,具體換算成秒 pts*1/采樣率begin_time = get_time();ret = encode(codec_ctx, frame, pkt, outfile);end_time = get_time();printf("encode time:%lldms\n", end_time - begin_time);if(ret < 0) {printf("encode failed\n");break;}}/* 沖刷編碼器 */encode(codec_ctx, NULL, pkt, outfile);all_end_time = get_time();printf("all encode time:%lldms\n", all_end_time - all_begin_time);// 關閉文件fclose(infile);fclose(outfile);// 釋放內存if(yuv_buf) {free(yuv_buf);}av_frame_free(&frame);av_packet_free(&pkt);avcodec_free_context(&codec_ctx);printf("main finish, please enter Enter and exit\n");getchar();return 0;
}void print_h265_nal_unit_type(uint8_t *data, size_t size)
{int i = 0;while (i+3 < size ) {if(data[i] == 0 && data[i+1]==0 && data[i+2] == 0 && data[i+3] == 1 ) {i += 4;printf("%02x nal_type:%d, pos:%d\n", data[i],(data[i]&0x7e)>>1, i);continue;}if(data[i] == 0 && data[i+1]==0 && data[i+2] == 1) {i += 3;printf("%02x nal_type:%d, pos:%d\n",data[i], (data[i]&0x7e)>>1, i);continue;}i++;}
}
更多資料:https://github.com/0voice