简介:使用ffmpeg和opencv处理视频,显示画面。
学习ffmpeg视频处理,看之前老的教程,编译一下出现一堆警告,类似下面这样的
warning: ‘void xxx(xxx*)’ is deprecated [-Wdeprecated-declarations]
所以按照4.4.4的官方example写了下4.4.4api的视频解码C++程序
ffmpeg版本: 4.4.4
opencv版本: 4.7.0
file: ffmpeg_opencv.cc
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#include <libavutil/log.h>
#include <libavutil/pixfmt.h>
#include <libswscale/swscale.h>
};
#include <chrono>
#include <opencv2/opencv.hpp>
int main(int argc, char **argv) {
if (argc < 2) {
printf("usage:\n\t %s filename\n", argv[0]);
return -1;
}
/* 打开文件,并记录文件格式信息 */
AVFormatContext *fmt_ctx = nullptr; // 用于存储视频文件封装格式中包含的信息
if (avformat_open_input(&fmt_ctx, argv[1], nullptr, nullptr) < 0) {
av_log(nullptr, AV_LOG_ERROR, "Cannot open input file\n");
return -1;
}
/* 在格式信息中检索流信息 */
if (avformat_find_stream_info(fmt_ctx, nullptr) < 0) {
av_log(nullptr, AV_LOG_ERROR, "Cannot find stream information\n");
return -1;
} // 流信息包含了视频流、音频流、字幕流等信息
printf("文件格式信息:\n");
av_dump_format(fmt_ctx, 0, argv[1], 0); // 打印文件格式信息
int total_frames = fmt_ctx->streams[0]->nb_frames;
printf("总帧数: %d\n", total_frames);
/* 根据格式信息选择其中的视频流 */
AVCodec *dec = nullptr; // 用于存储解码器
int video_stream_index = -1; // 用于存储视频流索引
video_stream_index =
av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, &dec,
0); // dec会被赋值为视频流对应的解码器
if (video_stream_index < 0 || !dec) { // 没有找到视频流或者解码器
av_log(nullptr, AV_LOG_ERROR,
"Cannot find a video stream in the input file\n");
return -1;
}
double fps = av_q2d(fmt_ctx->streams[video_stream_index]->avg_frame_rate);
int64_t wait_time_ms = static_cast<int64_t>(1000 / fps); // 获取延时时间,即帧间隔(ms)
/* 创建解码器上下文指针 */
AVCodecContext *dec_ctx = nullptr;
dec_ctx = avcodec_alloc_context3(dec); // 根据解码器创建解码器上下文指针
if (!dec_ctx) return AVERROR(ENOMEM); // 内存分配失败
avcodec_parameters_to_context(
dec_ctx,
fmt_ctx->streams[video_stream_index]
->codecpar); // 根据视频流编码信息填充解码器上下文
/* 初始化视频解码器 */
if (avcodec_open2(dec_ctx, dec, nullptr) < 0) {
av_log(nullptr, AV_LOG_ERROR, "Cannot open decoder\n");
return -1;
}
/* 创建packet、frame、frame_rgb和buffer用于存储解码前和解码后的数据 */
int ret = 0; // 用于存储函数返回值
AVPacket packet; // 用于存储一帧数据的packet
AVFrame *frame = nullptr; // 用于存储解码后的数据帧
AVFrame *frame_rgb = nullptr; // 用于存储转换为RGB格式后的数据帧
frame =
av_frame_alloc(); // 注意,av_frame_alloc不会为frame.data分配内存,
// 之后的avcodec_receive_frame会让frame.data指向解码后的数据
frame_rgb =
av_frame_alloc(); // 同理,av_frame_alloc不会为frame_rgb.data分配内存
// 之后的av_image_fill_arrays会让frame_rgb.data指向转换后的数据
if (!frame || !frame_rgb) {
perror("Could not allocate frame");
exit(1);
}
/* 转换格式设置 */
AVPixelFormat dst_pix_fmt =
AV_PIX_FMT_BGR24; // 将解码后的数据转换为RGB格式 AV_PIX_FMT_RGB24
// (对于opencv来说,转换为BGR格式)
uint8_t *buffer = nullptr; // 用于存储转换后的数据帧地址的指针
int num_bytes = av_image_get_buffer_size(
dst_pix_fmt, dec_ctx->width, dec_ctx->height,
1); // 计算转换后数据帧的大小,宽和高保持不变,1表示对齐字节数
if (num_bytes < 0) {
av_log(nullptr, AV_LOG_ERROR, "Could not get image buffer size\n");
return -1;
}
// 为buffer分配内存
buffer = static_cast<uint8_t *>(av_malloc(num_bytes * sizeof(uint8_t)));
/* av_image_fill_arrays将转换后数据帧的指针指向分配的内存,
* 同时linesize也会被赋值
* width和height应该与buffer分配时使用的宽高一致 */
av_image_fill_arrays(frame_rgb->data, frame_rgb->linesize, buffer,
dst_pix_fmt, dec_ctx->width, dec_ctx->height, 1);
struct SwsContext *sws_ctx = nullptr; // 创建转换上下文指针
sws_ctx = sws_getContext( // 根据解码器信息创建转换上下文
dec_ctx->width, dec_ctx->height, dec_ctx->pix_fmt, dec_ctx->width,
dec_ctx->height, dst_pix_fmt, SWS_BILINEAR, nullptr, nullptr, nullptr);
if (!sws_ctx) { // 如果转换上下文创建失败,打印转换格式信息,用于调试
av_log(nullptr, AV_LOG_ERROR,
"Impossible to create scale context for the conversion\n");
av_log(nullptr, AV_LOG_ERROR, "fmt:%s s:%dx%d -> fmt:%s s:%dx%d\n",
av_get_pix_fmt_name(dec_ctx->pix_fmt), dec_ctx->width,
dec_ctx->height, av_get_pix_fmt_name(dst_pix_fmt), dec_ctx->width,
dec_ctx->height);
return -1;
}
while (true) { // 从视频文件循环读取视频帧
std::chrono::system_clock::time_point start_time, end_time;
start_time = std::chrono::system_clock::now();
/* av_read_frame会将视频文件分解成frames,每次调用返回一个frame
* 读取数据时,如果读取到的是视频数据,那么packet将只包含一帧数据
* 如果读取到的是音频数据,那么可能包含整数倍数帧数据 */
if (ret = av_read_frame(fmt_ctx, &packet) <
0) // 读取数据,并将其存储在packet中,packet.data指向数据
break; // ret < 0, 文件读取完毕,跳出循环
if (packet.stream_index ==
video_stream_index) { // 如果packet.stream_index等于视频流索引,说明这个packet中包含的是视频数据
/* avcodec_send_packet将packet中的数据发送到解码器
* 注意:此时packet中可能包含多帧数据(例如:H.264编码中的NALU单元)
* 所以需要循环调用avcodec_receive_frame()函数读取所有数据 */
ret = avcodec_send_packet(dec_ctx, &packet);
if (ret < 0) { // 发送失败时,跳出循环
av_log(nullptr, AV_LOG_ERROR,
"Error while sending a packet to the decoder\n");
break;
}
while (ret >= 0) { // 当解码器中有多帧数据时,循环读取,
// 如果只有一帧数据,可以用if代替while
ret = avcodec_receive_frame(
dec_ctx,
frame); // 从解码器中读取一帧数据,存储在frame中,frame.data指向数据
if (ret == AVERROR(EAGAIN) ||
ret == AVERROR_EOF) { // 如果解码器中没有数据了,跳出循环
break;
} else if (ret < 0) { // 发生错误时,跳出循环
av_log(nullptr, AV_LOG_ERROR,
"Error while receiving a frame from the decoder\n");
goto end;
}
/* sws_scale根据sws_ctx转换上下文将数据帧转换为指定格式 */
sws_scale(sws_ctx, (const uint8_t *const *)frame->data, frame->linesize,
0, dec_ctx->height, frame_rgb->data, frame_rgb->linesize);
/*
*
* 至此我们已经得到了转换后的数据帧,可以对其进行处理
*
*/
/* 现在将转换后的数据帧通过opencv显示出来 */
cv::imshow("frame", cv::Mat(frame->height, frame->width, CV_8UC3,
frame_rgb->data[0]));
while (true) {
end_time = std::chrono::system_clock::now();
if (cv::waitKey(1) == 'q') { // 按下q键,跳出循环
av_frame_unref(frame);
av_packet_unref(&packet); // 别忘了释放packet、frame、buffer
goto end;
}
auto dura = (std::chrono::duration_cast<std::chrono::milliseconds>(
end_time - start_time))
.count(); // 计算时间差(毫秒)
if (dura >= wait_time_ms) {
// 达到延时时间,跳出循环
break;
}
}
av_frame_unref(frame); // 别忘了释放frame
}
}
av_packet_unref(&packet);
// memset(buffer, 0, num_bytes * sizeof(uint8_t)); // 清空buffer
}
end:
av_freep(&buffer);
sws_freeContext(sws_ctx);
avcodec_free_context(&dec_ctx);
avformat_close_input(&fmt_ctx);
av_frame_free(&frame);
av_frame_free(&frame_rgb);
if (ret < 0 && ret != AVERROR_EOF) {
// fprintf(stderr, "Error occurred: %s\n", av_err2str(ret));
exit(1);
}
exit(0);
}
// TODO: 重构代码,将代码分成多个函数
编译命令:
g++ ffmpeg_open.cc -o player -L/usr/ffmpeg/lib -lavcodec -lavutil -lavformat -lswscale -L/usr/local/lib -lopencv_highgui -lopencv_core
运行命令:
./player path/to/video.type
还有一个很奇怪的问题,这个程序在运行的时候会偶尔出现总线错误(bus error),但是当我在gdb中调试的时候怎么都不会出现总线错误,很困扰,有大佬知道为啥会出现这个总线错误吗???
找到错误了,所有声明指针的地方初始化为nullptr就好了。