基本步骤
-
打开编码器
在打开编码器时,要设置一些参数,例如具体使用的哪一个编码器,编H264时要使用libx264,编H265时要使用libx265。
还需要设置GOP,码流大小、分辨率宽和高也需要设置。 -
转换NV12到YUV420P
NV12是mac下采集视频的格式,NV12是一个包裹的格式,分两层,第一层是Y,第二层是UVUV进行交替存储,yuv420P是编码器libx264要求的格式。转换时可以采用ffmpeg自身的swscale转换,还可以用google开发的libyuv,还可以自己手工写一个进行转换。
-
准备编码数据AVFrame
因为从设备上获取的数据是AVPacket,是一个输出的数据,不能作为编码器的输入,所以要将AVPacket中的数据导入到AVFrame中,因为AVFrame是输入数据。
-
H264编码
前面的过程都处理完后,最后通过一个循环,就可以将数据进行编码了,拿到编码后的AVPacket存储进文件中,就可以通过播放器进行播放了。
示例代码
//
// video_encode.c
// FFmpegDemo
//
// Created by qqy on 2021/7/20.
//
#include "video_encode.h"
#include "time.h"
#define V_WIDTH 640
#define V_HETGHT 480
static int rec_status = 0;
void set_status(int status){
rec_status = status;
}
static AVFormatContext *open_dev(void){
int ret = 0;
char errors[1024] = {0, };
//ctx
AVFormatContext *fmt_ctx = NULL;
AVDictionary *options = NULL;
//[[video device]:[audio device]]
//0: 机器的摄像头
//1: 桌面
char *devicename = "0:";
//register audio device
avdevice_register_all();
//get format
const AVInputFormat *iformat = av_find_input_format("avfoundation");
av_dict_set(&options, "video_size", "640x480", 0);
av_dict_set(&options, "framerate", "30", 0);
av_dict_set(&options, "pixel_format", "nv12", 0);
//open device
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 NULL;
}
av_dump_format(fmt_ctx, 0, devicename, 0);
return fmt_ctx;
}
/**
* @brief
* @param[in] width
* param[in] height
* param[out] enc_ctx
*/
static void open_encoder(int width, int height, AVCodecContext **enc_ctx){
int ret = 0;
const AVCodec *codec = NULL;
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
(*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
(*enc_ctx)->gop_size = 30; //设置很小时,I帧就会很多,码流就会很大,设置很小时,I帧就会很少,一旦丢包就会出现异常,等到下一个I帧时才会修正。
(*enc_ctx)->keyint_min = 25;//最小I帧间隔,也就是I帧最小25帧就可以插入I帧 可选项,不设置也可以
//设置B帧数量
(*enc_ctx)->max_b_frames = 3; //可选项,不设置也可以
(*enc_ctx)->has_b_frames = 1; // 可选项,不设置也可以
//参考帧数量
(*enc_ctx)->refs = 3;//设置参考帧数量 可选项,不设置也可以
//设置输入YUV格式 设置像素格式
(*enc_ctx)->pix_fmt = AV_PIX_FMT_YUV420P;
//设置码率
(*enc_ctx)->bit_rate = 1000000; // 1000kbps
//设置帧率
(*enc_ctx)->time_base = (AVRational){1, 25}; //帧与帧之间的间隔是time_base
(*enc_ctx)->framerate = (AVRational){25, 1}; //帧率,每秒25帧
ret = avcodec_open2((*enc_ctx), codec, NULL);
if(ret < 0){
printf("Could not open codec:%s!\n", av_err2str(ret));
exit(1);
}
}
static AVFrame *create_frame(int width, int height){
int ret = 0;
AVFrame *frame = NULL;
frame = av_frame_alloc();
if(!frame){
printf("Error, No Memory!\n");
goto __ERROR;
}
//设置参数
frame->width = width;
frame->height = height;
frame->format = AV_PIX_FMT_YUV420P;
//alloc inner memory
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;
}
static void encode(AVCodecContext *enc_ctx, AVFrame *frame, AVPacket *newpkt, FILE *outfile){
int ret = 0;
if(frame){
printf("send frame to encoder pts=%lld\n", frame->pts);
}
//送原始数据给编码器进行编码
ret = avcodec_send_frame(enc_ctx, frame);
if(ret < 0){
printf("Error, Failed to send a frame for encoding\n");
exit(1);
}
//从编码器获取编码好的数据
while(ret >= 0){
ret = avcodec_receive_packet(enc_ctx, newpkt); //编码器并不是一帧一帧的输出,可能是送入好几个都不输出,然后送入一个出来好几个,所以要用循环进行获取
//如果编码器数据不足时,会返回EAGAIN 或者到数据尾部时会返回AVERROR_EOF
if(ret == AVERROR(EAGAIN)||ret == AVERROR_EOF){
return;
}else if(ret < 0){
printf("Error, Failled to encode!\n");
exit(1);
}
fwrite(newpkt->data, 1, newpkt->size, outfile);
av_packet_unref(newpkt);//减少packet的引用计数
}
}
void rec_video(void){
int ret = 0;
//packet
AVPacket pkt;
AVFormatContext *fmt_ctx = NULL;
AVCodecContext *enc_ctx = NULL;
//set log level
av_log_set_level(AV_LOG_DEBUG);
//start record
rec_status = 1000;
//create file
char *yuvout = "/Users/qqy/workspace/VedioFiles/video.yuv";
char *out = "/Users/qqy/workspace/VedioFiles/video.h264";
FILE *yuvoutfile = fopen(yuvout, "wb+");
FILE *outfile = fopen(out, "wb+");
//打开设备
fmt_ctx = open_dev();
if(!fmt_ctx){
printf("Error, Failed to open device!\n");
goto __ERROR;
}
//打开编码器
open_encoder(V_WIDTH, V_HETGHT, &enc_ctx);
//创建 AVFrame
AVFrame *frame = create_frame(V_WIDTH, V_HETGHT);
//创建编码输出packet
AVPacket *newpkt = av_packet_alloc();
if(!newpkt){
printf("Error, Failed to alloc avpacket!\n");
goto __ERROR;
}
int base = 0;
//read data from device
// while((ret = av_read_frame(fmt_ctx, &pkt)) == 0 && (rec_status > 0)) {
while ((ret = av_read_frame(fmt_ctx, &pkt)) == 0 || ret == -35) {
if(!rec_status){
break;
}
if(ret == -35){
av_usleep(100);
continue;
}
av_log(NULL, AV_LOG_INFO, "packet size is %d(%p)\n", pkt.size, pkt.data);
// (宽 x 高) x(yuv420=1.5 / yuv422=2 / yuv444=3)
// fwrite(pkt.data, 1, 460800, yuvoutfile); //直接将采集的数据格式NV12存储下来
//此时直接从设备获取到的数据格式为 YYYYYYYYUVUV NV12的数据格式
//目标要转成的YUV420P数据格式为 YYYYYYYYUUVV YUV420的数据格式
memcpy(frame->data[0], pkt.data, V_WIDTH*V_HETGHT); //640x480=307200 Y数据
//307200之后是UV数据
for(int i = 0; i < V_WIDTH*V_HETGHT/4; i++){
frame->data[1][i] = pkt.data[V_WIDTH*V_HETGHT+i*2];
frame->data[2][i] = pkt.data[V_WIDTH*V_HETGHT+i*2+1];
}//虽然已经实现了NV12转为YUV420,但是效率不算太高,可以尝试使用汇编指令优化来进行优化。这里暂时理解就可以,后面可以使用libyuv实现转换
fwrite(frame->data[0], 1, V_WIDTH*V_HETGHT, yuvoutfile);
fwrite(frame->data[1], 1, V_WIDTH*V_HETGHT/4, yuvoutfile);
fwrite(frame->data[2], 1, V_WIDTH*V_HETGHT/4, yuvoutfile);
printf("pkt, pkt.size=%d!\n", pkt.size);
fflush(yuvoutfile);
frame->pts = base++;
encode(enc_ctx, frame, newpkt, outfile);
av_packet_unref(&pkt); //release pkt
rec_status--;
}
encode(enc_ctx, NULL, newpkt, outfile); //这一步如果不做,可能送进去最后一帧时,编码器没有将全部数据输出,这个时候就有可能发生丢帧的现象
__ERROR:
if(yuvoutfile){
fclose(yuvoutfile);
}
if(fmt_ctx){
avformat_close_input(&fmt_ctx);
}
av_log(NULL, AV_LOG_DEBUG, "finish!\n");
return;
}