【FFmpeg 3.x API应用一】视频解码

摘要

这篇文章介绍怎么实现视频解码,具体步骤为读取Sample.mkv视频文件,从中提取视频流,然后解码为YUV图像数据,把YUV数据存储为PGM灰度图像,或者存储为YUV420p RAW格式视频。

初始化FFmepg和FormatContext

使用FFmpeg API第一个操作就是执行初始化函数:av_register_all注册所有相关组件,然后使用avformat_open_input打开指定的媒体文件,并使用avformat_find_stream_info获取媒体流相关信息,把这些格式信息映射到AVFormatContext *mFormatCtx这个结构中。
使用函数av_dump_format可以从控制台输出媒体文件相关信息。

bool VideoDecoding::init(const char * file)
{
    av_register_all();

    if ((avformat_open_input(&mFormatCtx, file, 0, 0)) < 0) {
        printf("Failed to open input file\n");
    }

    if ((avformat_find_stream_info(mFormatCtx, 0)) < 0) {
        printf("Failed to retrieve input stream information\n");
    }

    av_dump_format(mFormatCtx, 0, file, 0);

    return false;
}

查询媒体流序号

多媒体文件一般都有一个视频流和多个音频流或者字幕流,每个媒体流都有序号Index。新版本的API使用av_find_best_stream函数查询相应的媒体流,第一个参数为初始化后的媒体格式Context,第二个参数即为媒体类型:
- AVMEDIA_TYPE_VIDEO:视频流
- AVMEDIA_TYPE_AUDIO:音频流
- AVMEDIA_TYPE_SUBTITLE:字幕流

后面几个参数是指定流特性的,如果从多个音频流中选择一个的话可以进行相关设置。此时只有一个视频流,所以参数设为-1即可返回默认的媒体流Index,得到这个Index后,接下来可以根据这个Index读取所需要的流。

bool VideoDecoding::findStreamIndex()
{
    // Find video stream in the file
    mVideoStreamIndex = av_find_best_stream(mFormatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
    if (mVideoStreamIndex < 0) {
        printf("Could not find stream in input file\n");
        return true;
    }

    return false;
}

配置编解码器CodecContext

  1. 首先使用avcodec_find_decoder函数根据流Index查找相应的解码器。
  2. 然后使用avcodec_alloc_context3函数根据解码器申请一个CodecContext。
  3. 接着根据流数据填充CodecContext各项信息。
  4. 最后完成CodecContext初始化操作。
// Initialize the AVCodecContext to use the given AVCodec.
bool VideoDecoding::initCodecContext()
{
    // Find a decoder with a matching codec ID
    AVCodec *dec = avcodec_find_decoder(mFormatCtx->streams[mVideoStreamIndex]->codecpar->codec_id);
    if (!dec) {
        printf("Failed to find codec!\n");
        return true;
    }

    // Allocate a codec context for the decoder
    if (!(mCodecCtx = avcodec_alloc_context3(dec))) {
        printf("Failed to allocate the codec context\n");
        return true;
    }

    // Fill the codec context based on the supplied codec parameters.
    if (avcodec_parameters_to_context(mCodecCtx, mFormatCtx->streams[mVideoStreamIndex]->codecpar) < 0) {
        printf("Failed to copy codec parameters to decoder context!\n");
        return true;
    }

    // Initialize the AVCodecContext to use the given Codec
    if (avcodec_open2(mCodecCtx, dec, NULL) < 0) {
        printf("Failed to open codec\n");
        return true;
    }

    return false;
}

读取视频数据进行解码

这里有两个概念:packet和frame。可以简单地理解为包packet为编码的数据结构,帧frame为解码后的数据结构。
使用av_read_frame函数从FormatContext中循环读取packet,每读到一个packet先根据流Index判断是否是需要的媒体流,如果是需要的视频流就进行下一步解码操作。
新版本的API里面编解码统一使用avcodec_send_packetavcodec_receive_frame这一对函数对媒体文件进行编解码操作,实现从packet到frame的相互转换(解码和编码)。此时是解码,从函数名字可以理解为向处理器发送一个packet,处理器实现自动解码,然后再从处理器接收一个解码后的frame。旧版本APIavcodec_decode_video2这一系列编解码函数已经弃用了。
这个步骤只进行视频解码,解码后的数据可以进行各种操作。

bool VideoDecoding::readFrameProc()
{
    AVPacket packet;
    AVFrame *frame = av_frame_alloc();
    int tmpW = mFormatCtx->streams[mVideoStreamIndex]->codecpar->width;
    int tmpH = mFormatCtx->streams[mVideoStreamIndex]->codecpar->height;
    char outFile[40] = { 0 };
    sprintf(outFile, "../assets/Sample_%dx%d_yuv420p.yuv", tmpW, tmpH);

    FILE *fd = fopen(outFile, "wb");

    while (int num = av_read_frame(mFormatCtx, &packet) >= 0) {
        // find a video stream
        if (packet.stream_index == mVideoStreamIndex) {
            decodeVideoFrame(&packet, frame, fd);
        }

        av_packet_unref(&packet);
    }

    fclose(fd);

    printf("Generate video files successfully!\nUse ffplay to play the yuv420p raw video.\n");
    printf("ffplay -f rawvideo -pixel_format yuv420p -video_size %dx%d %s.\n", tmpW, tmpH, outFile);

    return false;
}

bool VideoDecoding::decodeVideoFrame(AVPacket *pkt, AVFrame *frame, FILE *fd)
{
    avcodec_send_packet(mCodecCtx, pkt);
    int ret = avcodec_receive_frame(mCodecCtx, frame);
    if (!ret) {

        // 2种保存YUV数据的方式

        // 保存为未压缩的YUV视频文件
        saveYUV(frame, fd);

        // 保存为PGM灰度图像文件
        //savePGM(frame);

        printf("."); // program running state
        return false;
    }

    return true;
}

保存解码后的YUV数据

上一步进行了视频解码,要想验证是否真的解码成功就要保存YUV数据为可以查看的格式。可以把每一帧图像存为一副图像,也可以保存为YUV420p格式视频文件。

保存为YUV420p视频

YUV420视频格式如下图所示(引用自维基百科):
YUV420格式
YUV像素个数为4:1:1,Y分量个数为图像尺寸h*w,UV分量个数都是h*w/4
YUV420p中的字母p表示planar平面模式,即YUV分量按顺序排列存储,还有另外一个YUV420sp,表示UV分量是交错排列的。
解码后得到的frame->data结构是一个多维数组,此时data[0] data[1] data[2]分别为YUV分量的数据。

bool VideoDecoding::saveYUV(AVFrame *frame, FILE *fd)
{
    fwrite(frame->data[0], 1, mCodecCtx->width *mCodecCtx->height, fd);
    fwrite(frame->data[1], 1, mCodecCtx->width*mCodecCtx->height / 4, fd);
    fwrite(frame->data[2], 1, mCodecCtx->width*mCodecCtx->height / 4, fd);
    return false;
}

把每一个frame的未压缩YUV数据都写入到一个文件中就是YUV420p格式的原生视频数据了,可以直接使用FFmpeg中的ffplay命令进行播放,播放的参数为:ffplay -f rawvideo -pixel_format yuv420p -video_size 1280x534 file.yuv,注意指定其图像尺寸。

保存为PGM灰度图像

PGM(portable graymap format)图像格式是一种简单的未经压缩的灰度图像格式。用纯文本文件打开PGM文件可以看到,文件第一行以字符‘P5’作为标记,第二行为宽度和高度,第三行为灰度值最大值,接下来的内容为像素灰度数据。
PGM为灰度图像,所以这里只需把解码后的frame->data[0]所指向的Y分量数据保存到文件即可。

// pgm: Portable Gray Map
bool VideoDecoding::savePGM(AVFrame * frame)
{
    static int frameNum = 0;

    char pgmFile[30];
    sprintf(pgmFile, "../assets/frame%d.pgm", frameNum++);
    FILE *pFile = fopen(pgmFile, "wb");

    fprintf(pFile, "P5\n%d %d\n%d\n", frame->width, frame->height, 255);

    for (int i = 0; i < frame->height; i++) {
        // Y
        fwrite(frame->data[0] + i*frame->linesize[0], 1, mCodecCtx->width, pFile);
    }

    fclose(pFile);

    return false;
}

释放系统资源

最后不要忘记释放CodecContext和FormatContext资源,这里我们可以在析构函数里面进行释放。

VideoDecoding::~VideoDecoding()
{
    avcodec_free_context(&mCodecCtx);
    avformat_close_input(&mFormatCtx);
}

示例程序代码

上述示例的完整代码可以从Github下载: https://github.com/lmshao/FFmpeg-Basic

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章