实验 ffmpeg 采集 yuyv422数据 转 yuy420数据 后 编码 播放
#include <stdio.h>
#include <string.h>
#include "libavutil/avutil.h"
#include "libavdevice/avdevice.h"
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
#include "libswresample/swresample.h"
#define V_WIDTH 640
#define V_HEIGTH 480
/*打开编码器
参数1 分辨率宽
参数2 分辨率高
参数3 获取编码器上下文 enc_ctx,这是输出参数
注意:C语言只会以值拷贝的方式传递参数,这里我们需要传递一个空指针作为输出参数,
如果直接传递空指针,那么函数会以值拷贝的方式 生成一个临时的空指针变量,函数内部
为都是围绕临时变量工作,我们所传空指针没有被输出。所以这里需要传递二维指针!
*/
static void open_encoder(int width, int height, AVCodecContext **enc_ctx)
{
int ret = 0;
AVCodec *codec = NULL;
//通过名字 查找获取 libx264编码器
codec = avcodec_find_encoder_by_name("libx264");
if(!codec){
printf("Codec libx264 not found\n");
exit(1);
}
//创建编码器上下文
*enc_ctx = avcodec_alloc_context3(codec);
if(!enc_ctx){
printf("Could not allocate video codec context!\n");
exit(1);
}
//SPS/PPS
/*
SPS 序列参数集,作用于一串连续的视频图像,对帧组的参数设置。
PPS 图像参数集,作用于视频序列中的图像,对GOP一组视频帧的 每一个图像的设置
H264 Profile 对视频压缩特性的描述
H264 Level 对视频的描述(与 码率,分辨率,fps 成正比)
*/
(*enc_ctx)->profile = FF_PROFILE_H264_HIGH_444;
(*enc_ctx)->level = 50; //表示LEVEL是5.0 清晰度高
//设置分辫率
(*enc_ctx)->width = width; //640
(*enc_ctx)->height = height; //480
//GOP
/*
gop_size :GOP的大小,表示一个GOP组的最大帧数目,即两个I帧之间的最大帧数目
keyint_min :一个GOP组的最下帧数目,即最小25帧可以插入一个I帧
场景:如果一个GOP组比较大,但是中间有发生图像很明显变化的时候,当很明显的变化达到一定数值,会自动插入一个I帧
所以当丢帧时,能有多快恢复正常,不至于出现卡顿,此时 keyint_min参数就很重要。因为如果出现丢帧 GOP大小不变的情况下
等到下一个I帧出现的时间比较久,会出现明显卡顿,此时减少GOP大小,缩短I帧之间的间隔,快速恢复丢帧导致的卡顿问题。
*/
(*enc_ctx)->gop_size = 250;
(*enc_ctx)->keyint_min = 25; //可选
//设置B帧数据,为了减少码流大小
/* B帧压缩比大,只占I帧的1/4,所以为了减少码流大小,增加使用B帧
但是注意 B帧很占用CPU,而且很耗时,B帧越多,延迟性也会越大
max_b_frames 一个GOP组的B帧数量,一般设置B帧不超过3帧
has_b_frames
*/
(*enc_ctx)->max_b_frames = 3; //可选
(*enc_ctx)->has_b_frames = 1; //可选
//参考帧的数量 用于解码
/*
参考帧设置越多,处理越慢,但是图像还原性很好。解码后的数据与编码前原始数据越 差别越小
参考帧设置越小,处理越快,但是图像还原性的就差很多。解码后的数据与编码前原始数据越 差别越大
*/
(*enc_ctx)->refs = 3; //可选
//设置编码器输入数据YUV格式 libx264编码器要求的输入数据格式是 AV_PIX_FMT_YUV420P
(*enc_ctx)->pix_fmt = AV_PIX_FMT_YUV420P;
//设置码率 640*480 25帧 平均码率600Kbps
(*enc_ctx)->bit_rate = 600000; //600kbps
//设置帧率
/*
time_base :(AVRational){1, 25} 表示当 25帧/每秒,帧与帧之间的间隔时间。
framerate :帧率,25帧/每秒
*/
(*enc_ctx)->time_base = (AVRational){1, 25}; //帧与帧之间的间隔是time_base
(*enc_ctx)->framerate = (AVRational){25, 1}; //帧率,每秒 25 帧
//打开编码器
/*
参数1: 编码器上下文
参数2: 获取的编码器
参数3:其他选项 暂时不用
*/
ret = avcodec_open2((*enc_ctx), codec, NULL);
if(ret<0){
printf("Could not open codec: %s!\n", av_err2str(ret));
exit(1);
}
}
/* 申请 libx264编码器 视频输入数据空间
参数 :分辨率
*/
static AVFrame* create_frame(int width, int height){
int ret = 0;
//视频输入数据 空间初始化,它包含了 存放未编码的视频数据的空间
AVFrame *frame = NULL;
//视频输入数据 空间初始化,它包含了 存放未编码的视频数据的空间
//初始化 AVFrame 结构
frame = av_frame_alloc();
if(!frame){
printf("Error, No Memory!\n");
goto __ERROR;
}
//设置 AVFrame 结构 信息,用于指定存储数据空间的大小
/*
分辨率
数据格式,libx264 编码器要求输入数据格式必须是 AV_PIX_FMT_YUV420P
*/
frame->width = width;
frame->height = height;
frame->format = AV_PIX_FMT_YUV420P;
/* 为 AVFrame结构中用于存放未编码的视频数据的空间 分配空间
参数1: AVFrame
参数2: 对齐方式
*/
ret = av_frame_get_buffer(frame, 32); //按 32 位对齐
if(ret < 0){
printf("Error, Failed to alloc buffer for frame!\n");
goto __ERROR;
}
return frame;
__ERROR:
if(frame){
av_frame_free(&frame);
}
return NULL;
}
/* 将视频帧数据 送给 编码器进行编码 ,将编码后的数据 写到目标文件中
参数1 编码器上下文
参数2 libx264编码器的视频输入数据存储空间
参数3 libx264编码器的视频输出数据存储空间
参数4 写到目标文件
*/
static void encode(AVCodecContext *enc_ctx,
AVFrame *frame,
AVPacket *newpkt,
FILE *outfile){
int ret = 0;
if(!enc_ctx){
printf("Error, enc_ctx is NULL\n");
}
if(frame){
printf("send frame to encoder, pts=%lld", frame->pts);
}
//送原始数据(yuv420p)给编码器进行编码
/*
参数1:编码器上下文
参数2:需要编码的视频数据
*/
ret = avcodec_send_frame(enc_ctx, frame);
if(ret < 0) {
printf("Error, Failed to send a frame for enconding!\n");
exit(1);
}
//如果ret>=0说明数据送给编码器成功,此后 我们才能从编码器中去获取编码好的视频帧数据
while(ret >=0) {
//获取编码后的视频数据,如果成功,需要重复获取,直到失败为止
/*
我们向编码器送一帧视频帧数据的时候,编码器不一定就会马上返回一个AVPacket视频帧。
有可能是我们送了很多帧视频数据后,编码器才会返回一个编码好的AVPacket视频帧。也有
可能同时返回多个编码好的AVPacket视频帧。
参数1:编码器上下文
参数2:编码器编码后的视频帧数据
*/
ret = avcodec_receive_packet(enc_ctx, newpkt);
//如果编码器数据不足时会返回 EAGAIN,或者到数据尾时会返回 AVERROR_EOF
/*
ret > 0 表示本次获取成功,然后继续循环获取,因为有可能后面还有编码好的数据需要获取
ret == AVERROR(EAGAIN) : 表示当前编码没有任何问题,但是输入的视频频帧不够,所以当前没有packet输出,需要继续送入frame进行编码码
ret == AVERROR_EOF :内部缓冲区中数据全部编码完成,不再有编码后的数据包输出。编码到最后了 没有任何数据了
other ,ret < 0 && ret!= AVERROR(EAGAIN) && ret!=AVERROR_EOF :编码错误 直接退出
*/
if(ret == AVERROR(EAGAIN)){
// printf("data is not enough\n");
return;
}
if(ret == AVERROR_EOF){
// printf("encoding end\n");
return;
}else if( ret < 0){ // 编码错误直接退出
printf("Error, encoding video frame\n");
exit(1);
}
fwrite(newpkt->data, 1, newpkt->size, outfile);
av_packet_unref(newpkt);//减少newpkt引用计数
}
}
void rec_audio() {
int ret = 0;
int base = 0;
int i = 0;
int j = 0;
int k = 0;
char errors[1024] = {0, };
//视频数据上下文
AVFormatContext *fmt_ctx = NULL;
AVDictionary *options = NULL;
//编码器上下文
AVCodecContext *enc_ctx = NULL;
//pakcet 存储获取的视频数据
AVPacket pkt;
//视频设备节点
char *devicename = "/dev/video0";
//set log level
av_log_set_level(AV_LOG_DEBUG);
//register audio device 向ffmpeg注册设备
avdevice_register_all();
//设置采集方式,对于不同的平台,采集数据的方式不同 linux系统是
/*
返回值:输入格式
*/
AVInputFormat *iformat = av_find_input_format("v4l2");
/* 设置视频其他参数
参数1:视频参数 options
参数2:指定 -video_size 分辨率参数 ; -framerate 帧率
参数3:分辨率
参数4:暂时不关心
*/
av_dict_set(&options,"video_size","640x480",0);
av_dict_set(&options,"framerate","30",0);
av_dict_set(&options, "pixel_format", "yuyv422", 0);
//打开视频设备
/*
参数1 获得 视频数据上下文 AVFormatContext
参数2 网络地址/本地文件(设备名)
参数3 输入格式
参数4 其他参数 这里为NULL
*/
if((ret = avformat_open_input(&fmt_ctx, devicename, iformat, &options)) < 0 ){
av_strerror(ret, errors, 1024);
fprintf(stderr, "Failed to open video device, [%d]%s\n", ret, errors);
return;
}
/*打开编码器
参数1 分辨率宽
参数2 分辨率高
参数3 获取编码器上下文 enc_ctx,这是输出参数
注意:C语言只会以值拷贝的方式传递参数,这里我们需要传递一个空指针作为输出参数,
如果直接传递空指针,那么函数会以值拷贝的方式 生成一个临时的空指针变量,函数内部
为都是围绕临时变量工作,我们所传空指针没有被输出。所以这里需要传递二维指针!
*/
open_encoder(V_WIDTH, V_HEIGTH, &enc_ctx);
//创建 AVFrame,包含 libx264编码器的视频输入数据存储空间
//分配编码前保存AVFrame格式的视频数据空间
/*
参数 :分辨率
*/
AVFrame* frame = create_frame(V_WIDTH, V_HEIGTH);
//创建 AVPacket,包含 libx264编码器的视频输出数据存储空间
//分配 编码后保存AVPacket格式的视频数据的空间
AVPacket *newpkt = av_packet_alloc();
if(!newpkt){
printf("Error, Failed to alloc avpacket!\n");
goto __ERROR;
}
//创建输出的视频文件 将视频数据写到该文件
char *yuvout = "/home/mhr/Desktop/video/video_test/video.yuv";
char *out = "/home/mhr/Desktop/video/video_test/video.h264";
FILE *outfile = fopen(out, "wb+");
FILE *yuvoutfile = fopen(yuvout, "wb+");
/* read data from device 获取视频数据 到pkt
参数1: 视频数据上下文
参数2: 视频数据存放的目标地址
返回值:return 0 is OK
*/
while((ret = av_read_frame(fmt_ctx, &pkt)) == 0) {
av_log(NULL, AV_LOG_INFO,"packet size is %d(%p)\n", pkt.size, pkt.data);
//YUYVYUYV yuyv422
//YYYYYYYYUUVV YUV420 ,Y数据,数据包前307200个数据, 640*480=307200 每个像素点有一个Y分量(亮度)
/*
抽取 YUV 数据,
yuyv422总字节:2*640*480=614400
总像素640*480=307200 每个像素点有一个Y分量
data[0] 存放 YUV420格式 Y 数据
data[1] 存放 YUV420格式 U 数据
data[2] 存放 YUV420格式 V 数据
*/
//抽取Y分量
for(i=0; i < 307200; i++)
{
// Y : 0 2 4 6 ...
frame->data[0][i] = pkt.data[i*2];
}
//抽取U分量
for(i=0; i<V_HEIGTH; i++){
//丢弃偶数行的uv分量
if((i%2)!=0)continue;
//提取目标列U分量
for(j=0;j<(V_WIDTH/2);j++){
if((4*j+1)>(2*V_WIDTH))
break;
frame->data[1][k++]=pkt.data[i*2*V_WIDTH+4*j+1];
}
}
k = 0;
//抽取V分量
for(i=0;i<V_HEIGTH;i++){
//丢弃偶数行的uv分量
if((i%2)==0)
continue;
//提取目标列V分量
for(j=0;j<(V_WIDTH/2);j++){
if((4*j+3)>(2*V_WIDTH))
break;
frame->data[2][k++]=pkt.data[i*2*V_WIDTH+4*j+3];
}
}
//将抽取的 Y V U分量 按照YUV420格式写到 video.yuv文件
fwrite(frame->data[0], 1, 307200, yuvoutfile);
fwrite(frame->data[1], 1, 307200/4, yuvoutfile);
fwrite(frame->data[2], 1, 307200/4, yuvoutfile);
/* pts 暂时理解为每一帧的编号
对于 x264编码器来说,它要求我们送数据的时候 要求我们送进去的每一帧的pts都是连续的值,
这样它才会编码号我们送进去的数据。
pts这个值非常重要,必须要做处理。
*/
frame->pts = base++;
/*
参数1 编码器上下文
参数2 libx264编码器的视频输入数据存储空间
参数3 libx264编码器的视频输出数据存储空间
参数4 写到目标文件
*/
encode(enc_ctx, frame, newpkt, outfile);
av_packet_unref(&pkt); //release pkt
}
/*再次调用编码器,强制让编码器编码缓存中剩下的视频帧数据 并吐出
此时不送数据到编码器,让编码器对缓冲区中剩下的数据进行编码并吐出
*/
encode(enc_ctx, NULL, newpkt, outfile);
__ERROR:
if(yuvoutfile){
//close file
fclose(yuvoutfile);
}
//关闭设备 并 释放视频数据上下文
if(fmt_ctx) {
avformat_close_input(&fmt_ctx);
}
av_log(NULL, AV_LOG_DEBUG, "finish!\n");
return;
}
int main(int argc, char *argv[])
{
rec_audio();
return 0;
}
ffplay video.h264