码流分析工具
- Elecard Stream Eye 最强大 也最贵的一个工具;
下载地址 - CodecVisa比Elecard 稍便宜;
- 雷神开发的工具 免费,只支持Windows;
H264码流分析工具下载地址
完整功能工具下载地址
x264编码器相关参数介绍
x264参数在ffmpeg中的映射关系
编码步骤
1. 打开编码器
avcodec_find_encoder_by_name("libx264") 获取编码器
根据codec获取编码器上下文, av,avcodec_alloc_context3;
-
设置编码器上下文的相关参数:
- SPS的profile = FF_PROFILE_H264_HIGH_444;表示支持最大等级
- SPS的level = 50; 相当于5.0,最大支持2560 x 1920分辨率 帧率30
- 设置分辨率 width height
- 设置采样格式,pix_fmt = AV_PIX_FMT_YUV420P;
- 设置码率 bit_rate = 600000B,根据level参数表,level5.0的最大码率支持4000kbps;
- 设置gop一组的帧数 gop_size,帧数越小I帧越多数据量就越大
- 设置帧率,framerate = (AVRational){25, 1} 表示1秒25帧, time_base = (AVRational){1, 25} 表示帧与帧之间的间隔
下面的参数设置是可选的:
- 设置gop中最小插入I帧的间隔 keyint_min,gop中也会自动根据变化大的帧数前插入I帧;
- 设置gop中最大B帧数,可以减少码流的大小,max_b_frames和has_b_frames, 一般设置不超过3帧;
- 设置参考帧的数量,refs, 一般是3~5;
打开编码器:av_codec_open2
static AVCodecContext* open_codec(int width, int height) {
AVCodecContext *ctx = NULL;
AVCodec *codec = NULL;
codec = avcodec_find_encoder_by_name("libx264");
// codec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!codec) {
printf("未找到libx264编码器\n");
return NULL;
}
ctx = avcodec_alloc_context3(codec);
ctx->profile = FF_PROFILE_H264_HIGH_444;
// 相当于5.0 分辨率最大支持2560 * 1920 最大帧率30 最大码率135000kbps
ctx->level = 50;
// 设置分辨率
ctx->width = width;
ctx->height = height;
//设置格式
ctx->pix_fmt = AV_PIX_FMT_YUV420P;
// AVRation 第一个是分子 第二个是分母, time_base表示是帧与帧之间的间隔,也就是1/25
ctx->time_base = (AVRational){1, 25};
// 设置帧率 1 秒 25帧
ctx->framerate = (AVRational){25,1};
// 设置码率为600kbps
ctx->bit_rate = 600000;
// 设置每组gop的帧数
ctx->gop_size = 50;
// 设置一组gop中最多的B帧数
ctx->max_b_frames = 3;
// 设置一组中的参考帧的数量
ctx->refs = 3;
int ret = avcodec_open2(ctx, codec, NULL);
if (ret < 0) {
printf("编码器打开失败:%s \n", av_err2str(ret));
avcodec_free_context(&ctx);
return NULL;
}
return ctx;
}
2. 准备编码数据AVFrame
- 跟音频编码差不多,alloc一个frame;
- 设置数据的分辨率、采样格式;
- 获取缓冲区,以32位对齐;av_frame_get_buffer
/**
* 创建编码输入缓冲区
*/
static AVFrame* create_frame(int width, int height) {
AVFrame *frame = NULL;
frame = av_frame_alloc();
if (!frame) {
return NULL;
}
frame->width = width;
frame->height = height;
frame->format = AV_PIX_FMT_YUV420P;
frame->pts = 0;
// 缓冲区以32位对齐
int ret = av_frame_get_buffer(frame, 32);
if (ret < 0) {
printf("初始化frame缓冲区失败:%s \n", av_err2str(ret));
if (frame) {
av_frame_free(&frame);
}
return NULL;
}
return frame;
}
3. 转换NV12到YUV420p进行存储
- 可以使用libyuv转,也可以自己写for循环转换;
- 转换步骤:
1、先将采集的数据的Y部分拷贝到frame,Y的大小 = 分辨率的宽 * 分辨率的高;
2、再通过遍历将U和V数据放到缓冲区中的第一组
3、确定遍历次数:因为420的比例中,U和V的数据各占Y的4分之一,所以遍历次数就是Y数据量的4分之一
4、U数据放入缓冲区的第二组,V数据放入缓冲区的第三组,再因为NV12的存储格式是YYYYYYYY UVUV,所以U和V数据是在Y数据之后的,而且UV数据的排列方式是线性的相互错开的,所以在获取UV的时候,也要错开; - 在存储的时候也要将缓冲区的1组和2组也要写入文件;
/**
将nv12的数据转换yuv420p,再进行存储
*/
static void convert_nv12_to_yuv420(AVFrame *frame, AVPacket *packet, FILE *file) {
// 手动转换nv12 到 YUV420p
// 1. 先将Y数据拷贝frame缓冲区的第一组
int y_count = 640 * 480;
memcpy(frame->data[0], packet->data, y_count);
// 2. 将UV数据分别拷贝到frame缓冲区的第二和第三组
for (int i = 0; i < y_count / 4; i ++) {
// 拷贝U数据
frame->data[1][i] = packet->data[y_count + i * 2];
// 拷贝V数据
frame->data[2][i] = packet->data[y_count + i * 2 + 1];
}
fwrite(frame->data[0], 1, y_count, file);
fwrite(frame->data[1], 1, y_count / 4, file);
fwrite(frame->data[2], 1, y_count / 4, file);
}
4. H264编码
- 编码过程跟音频编码差不多
- 调用av_send_frame,将数据送入编码器
- 调用av_receive_packet,,读取编码好的数据
- 需要注意的frame的pts每编码一次都要变化,进行+1操作,不然会造成花屏;
/**
开始编码
*/
static void encode(AVCodecContext *ctx, AVFrame *frame, AVPacket *packet, FILE *file) {
int ret = 0;
ret = avcodec_send_frame(ctx, frame);
while (ret == 0) {
ret = avcodec_receive_packet(ctx, packet);
if (ret == -EAGAIN || ret == AVERROR_EOF) {
return;
} else if (ret == 0) {
// 读取编码好的数据
fwrite(packet->data, 1, packet->size, file);
av_packet_unref(packet);
} else {
printf("获取视频编码错误: %s",av_err2str(ret));
}
}
}
5. 外层代码
void start_video_recorder(void) {
AVFormatContext *fmt_ctx = NULL;
fmt_ctx = open_device();
if (!fmt_ctx) {
goto __ERROR;
}
AVPacket *packet = NULL;
packet = av_packet_alloc();
if (!packet) {
goto __ERROR;
}
AVCodecContext *codec_ctx = open_codec(640, 480);
if (!codec_ctx) {
goto __ERROR;
}
AVFrame *codec_frame = create_frame(640, 480);
if (!codec_frame) {
goto __ERROR;
}
AVPacket *codec_packet = av_packet_alloc();
if (!codec_packet) {
goto __ERROR;
}
char *path = "/Users/cunw/Desktop/learning/音视频学习/音视频文件/video_recoder.yuv";
FILE *file = fopen(path, "wb+");
FILE *h264_file = fopen("/Users/cunw/Desktop/learning/音视频学习/音视频文件/video_encoder.h264", "wb+");
int rst = 0;
while (isRecording) {
rst = av_read_frame(fmt_ctx, packet);
if (rst == 0 && packet->size > 0) {
printf("packet size is %d\n", packet->size);
// 将nv12 转换成 yuv420
convert_nv12_to_yuv420(codec_frame,packet, file);
codec_frame->pts += 1;
// 开始编码
encode(codec_ctx, codec_frame, codec_packet, h264_file);
// 释放已读取的数据
av_packet_unref(packet);
} else if (rst == -EAGAIN) {
av_usleep(1);
}
}
encode(codec_ctx, NULL, codec_packet, h264_file);
__ERROR:
isRecording = 0;
fflush(file);
fclose(file);
fflush(h264_file);
fclose(h264_file);
avcodec_free_context(&codec_ctx);
av_frame_free(&codec_frame);
av_packet_free(&codec_packet);
av_packet_free(&packet);
avformat_close_input(&fmt_ctx);
printf("video encoder finished!\n");
}
void stop_video_recorder(void) {
isRecording = 0;
}