FFmpeg 读取视频流并保存为BMP

时间:2022-12-13 23:21:49

简介

基本概念

在演示如何读取视频文件之前,应先了解几个关于视频流的概念:

  • 容器(Container): 视频文件本身就叫容器,容器的类型(比如AVI、MP4)决定了视频信息如何存储。
  • 流(Stream):每个容器可以包含若干个流。比如一个视频文件通常包含了一个视频流和一个音频流。
  • 帧(Frame):帧是流中数据的最小单位。每个流里面包含若干帧。
  • 编解码器(CODEC):流中的数据都是以编码器编码而成的,而不是直接存储原始数据。在处理每一帧时,需要用CODEC来解码才能得到原始数据。
  • 包(Packet):FFmpeg用包来描述从流中读到的数据,在实际处理时,将从流中不断读取数据到包,直到包中包含了一个整帧的内容再进行处理。

处理流程

FFmpeg读取视频流的一般流程为:

  1. 打开视频(音频)文件。
  2. 从流中读取数据到包。
  3. 如果包不是一个整帧,则执行2。如果包是一个整帧,则:
  4. 处理帧。
  5. 继续执行2,直到整个流处理完毕。

代码级别的一般流程为:

Created with Raphaël 2.1.0 注册所有的格式和解码器 打开视频文件 读取视频流信息并找到视频流 找到并打开与流对应的编解码器 创建并初始化解码后的帧 从流中读取帧数据到包 包是一个整帧 处理帧 yes no

示例

下面的程序读取一个视频流,将第一帧数据转储为BITMAP。(视频文件的路径由程序的启动参数获取。)

extern "C"
{
#include "libavcodec\avcodec.h"
#include "libavformat\avformat.h"
#include "libswscale\swscale.h"
#include "libavutil\imgutils.h"
}

#include <iostream>
#include <fstream>
#include <memory>
#include <Windows.h>

void SaveBitmap(uint8_t *data, int width, int height, int bpp)
{
BITMAPFILEHEADER bmpHeader = { 0 };
bmpHeader.bfType = ('M' << 8) | 'B';
bmpHeader.bfReserved1 = 0;
bmpHeader.bfReserved2 = 0;
bmpHeader.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);
bmpHeader.bfSize = bmpHeader.bfOffBits + width*height*bpp / 8;

BITMAPINFO bmpInfo = { 0 };
bmpInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmpInfo.bmiHeader.biWidth = width;
bmpInfo.bmiHeader.biHeight = -height; // 反转图片
bmpInfo.bmiHeader.biPlanes = 1;
bmpInfo.bmiHeader.biBitCount = bpp;
bmpInfo.bmiHeader.biCompression = 0;
bmpInfo.bmiHeader.biSizeImage = 0;
bmpInfo.bmiHeader.biXPelsPerMeter = 100;
bmpInfo.bmiHeader.biYPelsPerMeter = 100;
bmpInfo.bmiHeader.biClrUsed = 0;
bmpInfo.bmiHeader.biClrImportant = 0;

// 打开文件
std::ofstream fout("output.bmp", std::ofstream::out | std::ofstream::binary);
if (!fout)
{
return;
}
// 使用结束后关闭
std::shared_ptr<std::ofstream> foutCloser(&fout, [](std::ofstream *f){ f->close(); });

fout.write(reinterpret_cast<const char*>(&bmpHeader), sizeof(BITMAPFILEHEADER));
fout.write(reinterpret_cast<const char*>(&bmpInfo.bmiHeader), sizeof(BITMAPINFOHEADER));
fout.write(reinterpret_cast<const char*>(data), width * height * bpp / 8);
}

int main(int argc, char **argv)
{
// 注册所有的格式和解码器
av_register_all();

AVFormatContext *pFmtCtx = NULL;

// 打开视频文件,读取文件头信息到 AVFormatContext 结构体中
if (avformat_open_input(&pFmtCtx, argv[1], NULL, NULL) != 0)
{
return -1;
}
// 程序结束时关闭 AVFormatContext
std::shared_ptr<AVFormatContext*> fmtCtxCloser(&pFmtCtx, avformat_close_input);

// 读取流信息到 AVFormatContext->streams 中
// AVFormatContext->streams 是一个数组,数组大小是 AVFormatContext->nb_streams
if (avformat_find_stream_info(pFmtCtx, NULL) < 0)
{
return -1;
}

// 找到第一个视频流
int videoStream = -1;
for (decltype(pFmtCtx->nb_streams) i = 0; i < pFmtCtx->nb_streams; ++i)
{
if (pFmtCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
{
videoStream = i;
break;
}
}
if (videoStream == -1)
{
return -1;
}

// 获取解码器上下文
AVCodecParameters *pCodecParams = pFmtCtx->streams[videoStream]->codecpar;

// 获取解码器
AVCodec *pCodec = avcodec_find_decoder(pCodecParams->codec_id);
if (pCodec == NULL)
{
std::cerr << "Unsupported codec!" << std::endl;
return -1;
}

// 解码器上下文
AVCodecContext *pCodecCtx = avcodec_alloc_context3(NULL); // allocate
if (avcodec_parameters_to_context(pCodecCtx, pCodecParams) < 0) // initialize
{
return -1;
}
// 程序结束时关闭解码器
std::shared_ptr<AVCodecContext> codecCtxCloser(pCodecCtx, avcodec_close);

// 打开解码器
if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0)
{
return -1;
}

// 创建帧
AVFrame *pFrame = av_frame_alloc();
std::shared_ptr<AVFrame*> frameDeleter(&pFrame, av_frame_free);

// 创建转换后的帧
AVFrame *pFrameBGR = av_frame_alloc();
std::shared_ptr<AVFrame*> frameBGRDeleter(&pFrameBGR, av_frame_free);

// 开辟数据存储区
int numBytes = av_image_get_buffer_size(AV_PIX_FMT_BGR24, pCodecCtx->width, pCodecCtx->height, 1);
uint8_t *buffer = (uint8_t *)av_malloc(numBytes * sizeof(uint8_t));
// 程序结束时释放
std::shared_ptr<uint8_t> bufferDeleter(buffer, av_free);

// 填充 pFrameBGR 中的若干字段(data、linsize等)
av_image_fill_arrays(pFrameBGR->data, pFrameBGR->linesize, buffer, AV_PIX_FMT_BGR24,
pCodecCtx->width, pCodecCtx->height, 1);

// 获取图像处理上下文
SwsContext *pSwsCtx = sws_getContext(pCodecCtx->width, pCodecCtx->height,
pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_BGR24,
SWS_BILINEAR, NULL, NULL, NULL);
// 程序结束时释放
std::shared_ptr<SwsContext> swsCtxDeleter(pSwsCtx, sws_freeContext);

AVPacket packet;
int frameCount = 0;
const int saveFrameIndex = 1;
while (av_read_frame(pFmtCtx, &packet) >= 0) // 将 frame 读取到 packet
{
// 迭代结束后释放 av_read_frame 分配的 packet 内存
std::shared_ptr<AVPacket> packetDeleter(&packet, av_packet_unref);

if (packet.stream_index == videoStream) // 如果读到的是视频流
{
// 使用解码器 pCodecCtx 将 packet 解码
avcodec_send_packet(pCodecCtx, &packet);
// 返回 pCodecCtx 解码后的数据,注意只有在解码完整个 frame 时该函数才返回 0
if (avcodec_receive_frame(pCodecCtx, pFrame) == 0)
{
// 图像转换
sws_scale(pSwsCtx, pFrame->data,
pFrame->linesize, 0, pCodecCtx->height,
pFrameBGR->data, pFrameBGR->linesize);

// 将第 saveFrameIndex 帧保存为 bitmap 图片
if (++frameCount == saveFrameIndex)
{
SaveBitmap(pFrameBGR->data[0], pCodecCtx->width, pCodecCtx->height, 24);
break; // 结束处理
}
}
}
}

return 0;
}