本文讲述如何在linux下,使用ffmpeg采集视频数据,并编码成h264文件。
打算分成3部分讲解:
- 需要具备的软硬件环境
- ffmpeg命令采集摄像头数据并编码成h264文件
- ffmpeg代码采集摄像头数据并编码成h264文件
需要具备的软硬件环境
需要具备的硬件环境,摄像头。我这里是一个usb 摄像头。
需要具备的软件环境,ffmpeg,并且启用了libx264编码的ffmpeg。假如是自己编译的ffmpeg,在configure 的时候,需要添加上参数“–enable-libx264”,我在这中间遇到了一些问题,老是找不到libx264编码器。后面做了2个操作,才完成的。
- 删除了原来老的ffmpeg(自己通过apt install安装的),及其相关的库(libavcodec等)。
- 自己安装了libx264-dev。然后configure 的时候加上了"–enable-libx264"参数,最后编译安装完成的。
ffmpeg命令采集摄像头数据并编码成h264文件
ffmpeg -f video4linux2 -i "/dev/video0" -vcodec libx264 -pix_fmt yuv420p mycamera.h264
使用上述命令,可以采集摄像头数据,并编码成h264文件。下面详细讲解一下这些参数。
-f video4linux2,表示采用video4linux2驱动程序。
-i “/dev/video0”,表示输入数据的地址是“/dev/video0”,这个设备地址,就是我们的摄像头设备地址。
-vcodec libx264,表示采用libx264编解码器。
-pix_fmt yuv420p,表示编码的数据采用yuv420p。
mycamera.h264,这个就是我们最终的编码好的h264数据。
然后使用命令播放一下,看看效果,没问题。
ffplay mycamera.h264
ffmpeg代码采集摄像头数据并编码成h264文件
分成几个部分,说明采集摄像头并编码成h264文件的过程。
- 打开设备
- 打开编码器
- 采集摄像头数据,为yuyv422格式
- 转换摄像头数据,从yuyv422转成yuv420p
- 编码yuv420p数据为h264数据,并写成文件
打开设备
// ctx
AVFormatContext *fmt_ctx = NULL;
AVDictionary *options = NULL;
char *devicename = "/dev/video0";
// register audio device
avdevice_register_all();
// get format
AVInputFormat *iformat = av_find_input_format("video4linux2");
av_dict_set(&options, "video_size", "640x480", 0);
av_dict_set(&options, "framerate", "30", 0);
av_dict_set(&options, "pixel_format", "yuyv422", 0);
// open device
avformat_open_input(&fmt_ctx, devicename, iformat, &options);
这个,我们做了3件事,来打开设备:
- 使用video4linux2这个驱动
- 指定了设备路径为/dev/video0
- 指定了一些参数,video_size, framerate, pixel_format
最后,使用avformat_open_input来打开设备。
打开编码器
AVCodec *codec = NULL;
codec = avcodec_find_encoder_by_name("libx264");
AVCodecContext **enc_ctx = NULL;
*enc_ctx = avcodec_alloc_context3(codec);
// 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 = 250;
(*enc_ctx)->keyint_min = 25; // option
//设置B帧数据
(*enc_ctx)->max_b_frames = 3; // option
(*enc_ctx)->has_b_frames = 1; // option
//参考帧的数量
(*enc_ctx)->refs = 3; // option
//设置输入YUV格式
(*enc_ctx)->pix_fmt = AV_PIX_FMT_YUV420P;
//设置码率
(*enc_ctx)->bit_rate = 600000; // 600kbps
//设置帧率
(*enc_ctx)->time_base = (AVRational){1, 30}; //帧与帧之间的间隔是time_base
(*enc_ctx)->framerate = (AVRational){30, 1}; //帧率,每秒 30帧
avcodec_open2((*enc_ctx), codec, NULL);
这里,也主要做了3件事:
- avcodec_find_encoder_by_name找到libx264这个编码器。
- 给编码器开辟了一个 编码器上下文,avcodec_alloc_context3,并对这个上下文设置了一些参数。这都是我们编码需要使用的参数。
- 使用avcodec_open2打开了编码器。
采集摄像头数据
AVPacket pkt;
while ( av_read_frame(fmt_ctx, &pkt) == 0) {
int i = 0;
av_log(NULL, AV_LOG_INFO, "packet size is %d(%p)\n", pkt.size,
pkt.data);
}
这个采集数据,只需要拿着第一步打开的设备上下文,传递给av_read_frame进行数据读取,就可以了。
转换摄像头数据,从yuyv422转成yuv420
void yuyv422ToYuv420p(AVFrame *frame, AVPacket *pkt) {
int i = 0;
int yuv422_length = V_WIDTH * V_HEIGHT * 2;
int y_index = 0;
// copy all y
for (i = 0; i < yuv422_length; i += 2) {
frame->data[0][y_index] = pkt->data[i];
y_index++;
}
// copy u and v
int line_start = 0;
int is_u = 1;
int u_index = 0;
int v_index = 0;
// copy u, v per line. skip a line once
for (i = 0; i < V_HEIGHT; i += 2) {
// line i offset
line_start = i * V_WIDTH * 2;
for (int j = line_start + 1; j < line_start + V_WIDTH * 2; j += 4) {
frame->data[1][u_index] = pkt->data[j];
u_index++;
frame->data[2][v_index] = pkt->data[j + 2];
v_index++;
}
}
}
我这里,摄像头读取的yuyv422存储在AVPacket中,然后将他转换成yuv420p,存放到AVFrame中。其中AVFrame的data[0]存放y数据,data[1]存放u数据,data[2]存放v数据。至于yuyv422和yuv420的存储方式,这里就不讲了,网上能搜索到的。
编码并保存为文件
static void encode(AVCodecContext *enc_ctx, AVFrame *frame, AVPacket *newpkt,
FILE *outfile) {
int ret = 0;
if (frame) {
printf("send frame to encoder, pts=%lld", frame->pts);
}
//送原始数据给编码器进行编码
ret = avcodec_send_frame(enc_ctx, frame);
if (ret < 0) {
printf("Error, Failed to send a frame for enconding!\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, Failed to encode!\n");
exit(1);
}
fwrite(newpkt->data, 1, newpkt->size, outfile);
fflush(outfile);
av_packet_unref(newpkt);
}
}
编码的话,主要通过2个步骤完成:
- avcodec_send_frame,将需要编码的AVFrame数据(就是上一步骤拿到的yuv420p的AVFrame数据),送给编码器。
- avcodec_receive_packet,将编码好的AVPacket数据,读取出来,并保存文件。
全部代码
#include <string.h>
#include "libavdevice/avdevice.h"
#include "libavformat/avformat.h"
#include "libavutil/avutil.h"
#define V_WIDTH 640
#define V_HEIGHT 480
//@brief
// return
static AVFormatContext *open_dev() {
int ret = 0;
char errors[1024] = {
0,
};
// ctx
AVFormatContext *fmt_ctx = NULL;
AVDictionary *options = NULL;
//摄像头的设备文件
char *devicename = "/dev/video0";
// register video device
avdevice_register_all();
// get format
AVInputFormat *iformat = av_find_input_format("video4linux2");
av_dict_set(&options, "video_size", "640x480", 0);
av_dict_set(&options, "framerate", "30", 0);
av_dict_set(&options, "pixel_format", "yuyv422", 0);
// open device
ret = avformat_open_input(&fmt_ctx, devicename, iformat, &options);
if (ret < 0) {
av_strerror(ret, errors, 1024);
fprintf(stderr, "Failed to open video device, [%d]%s\n", ret, errors);
return NULL;
}
return fmt_ctx;
}
static void open_encoder(int width, int height, AVCodecContext **enc_ctx) {
int ret = 0;
AVCodec *codec = NULL;
avcodec_register_all();
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 = 250;
(*enc_ctx)->keyint_min = 25; // option
//设置B帧数据
(*enc_ctx)->max_b_frames = 3; // option
(*enc_ctx)->has_b_frames = 1; // option
//参考帧的数量
(*enc_ctx)->refs = 3; // option
//设置输入YUV格式
(*enc_ctx)->pix_fmt = AV_PIX_FMT_YUV420P;
//设置码率
(*enc_ctx)->bit_rate = 600000; // 600kbps
//设置帧率
(*enc_ctx)->time_base = (AVRational){1, 30}; //帧与帧之间的间隔是time_base
(*enc_ctx)->framerate = (AVRational){30, 1}; //帧率,每秒 30帧
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", frame->pts);
}
//送原始数据给编码器进行编码
ret = avcodec_send_frame(enc_ctx, frame);
if (ret < 0) {
printf("Error, Failed to send a frame for enconding!\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, Failed to encode!\n");
exit(1);
}
fwrite(newpkt->data, 1, newpkt->size, outfile);
fflush(outfile);
av_packet_unref(newpkt);
}
}
void yuyv422ToYuv420p(AVFrame *frame, AVPacket *pkt) {
int i = 0;
int yuv422_length = V_WIDTH * V_HEIGHT * 2;
int y_index = 0;
// copy all y
for (i = 0; i < yuv422_length; i += 2) {
frame->data[0][y_index] = pkt->data[i];
y_index++;
}
// copy u and v
int line_start = 0;
int is_u = 1;
int u_index = 0;
int v_index = 0;
// copy u, v per line. skip a line once
for (i = 0; i < V_HEIGHT; i += 2) {
// line i offset
line_start = i * V_WIDTH * 2;
for (int j = line_start + 1; j < line_start + V_WIDTH * 2; j += 4) {
frame->data[1][u_index] = pkt->data[j];
u_index++;
frame->data[2][v_index] = pkt->data[j + 2];
v_index++;
}
}
}
void rec_video() {
int ret = 0;
int base = 0;
int count = 0;
// pakcet
AVPacket pkt;
AVFormatContext *fmt_ctx = NULL;
AVCodecContext *enc_ctx = NULL;
// set log level
av_log_set_level(AV_LOG_DEBUG);
// create file
char *yuvout = "./video.yuv";
char *out = "./video.h264";
FILE *yuvoutfile = fopen(yuvout, "wb+");
FILE *outfile = fopen(out, "wb+");
//打开设备
fmt_ctx = open_dev();
//打开编码器
open_encoder(V_WIDTH, V_HEIGHT, &enc_ctx);
//创建 AVFrame
AVFrame *frame = create_frame(V_WIDTH, V_HEIGHT);
//创建编码后输出的Packet
AVPacket *newpkt = av_packet_alloc();
if (!newpkt) {
printf("Error, Failed to alloc avpacket!\n");
goto __ERROR;
}
// read data from device
while (((ret = av_read_frame(fmt_ctx, &pkt)) == 0) && (count++ < 100)) {
int i = 0;
av_log(NULL, AV_LOG_INFO, "packet size is %d(%p)\n", pkt.size,
pkt.data);
// YUYVYUYVYUYVYUYV YUYV422
// YYYYYYYYUUVV YUV420
yuyv422ToYuv420p(frame, &pkt);
fwrite(frame->data[0], 1, 307200, yuvoutfile);
fwrite(frame->data[1], 1, 307200 / 4, yuvoutfile);
fwrite(frame->data[2], 1, 307200 / 4, yuvoutfile);
frame->pts = base++;
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);
}
// close device and release ctx
if (fmt_ctx) {
avformat_close_input(&fmt_ctx);
}
av_log(NULL, AV_LOG_DEBUG, "finish!\n");
return;
}
int main(int argc, char *argv[]) {
rec_video();
return 0;
}
全部代码,我这里命名为encode_video.c,然后使用命令
gcc -g -O0 encode_video.c -lavformat -lavutil -lavdevice -lavcodec -o encode_video
进行编译。然后运行./encode_video,录制视频,录制完的视频保存在video.h264中。最后播放一下,看看效果
ffplay video.h264
可以看到视频播放正常。