FFmpeg 读取视频流并保存为BMP

13 篇文章 1 订阅
4 篇文章 0 订阅

简介

基本概念

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

  • 容器(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;
}
  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值