三、解码视频帧

av_read_frame(fmt_ctx, packet)
这个函数的作用是读取一个视频包到 AVPacket 中
对于视频,数据包恰好包含一帧;对于音频,若每帧大小固定(如PCM或ADPCM),则包含整数个帧;若音频帧大小可变(如MPEG音频),则包含一帧。
这里有两段代码,区别在于是否循环调 avcodec_receive_frame。
上面的说明是 ffmpeg 对 av_read_frame 的注释,从这里看来,视频帧应该是不需要循环解码的,但是查找资料,都是需要循环解码的,考虑到 I 帧,P 帧,B帧之类的,即当前帧需依赖后续的帧来进行解码,所以可能后续输出多个帧。笔者尝试了几个视频,两个方法解码出的帧数并无差别,但查阅 ffmpeg ffmpeg/doc/examples/decode_video.c 后,里面也是循环接收帧的,因此还是建议用第二段代码。

 while (av_read_frame(fmt_ctx, packet) >= 0) {
        if (packet->stream_index == video_index) {
            ret = avcodec_send_packet(video_decode_ctx, packet);
            if (ret == 0) {
                ret = avcodec_receive_frame(video_decode_ctx, frame);
                if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
                    continue;
                } else if (ret < 0) {
                    return -1;
                }
            } else {
                return -1;
            }
        }
        av_packet_unref(packet); // 清除数据包
    }
 while (av_read_frame(fmt_ctx, packet) >= 0) {
        if (packet->stream_index == video_index) {
            ret = avcodec_send_packet(video_decode_ctx, packet);
            while ( ret >= 0) {
                ret = avcodec_receive_frame(video_decode_ctx, frame);
                if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
                    break;
                } else if (ret < 0) {
                    return -1;
                }
            }
        }
        av_packet_unref(packet);
    }

ffmpeg 官方 demo

/*
 * Copyright (c) 2001 Fabrice Bellard
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

/**
 * @file libavcodec video decoding API usage example
 * @example decode_video.c *
 *
 * Read from an MPEG1 video file, decode frames, and generate PGM images as
 * output.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <libavcodec/avcodec.h>

#define INBUF_SIZE 4096

static void pgm_save(unsigned char *buf, int wrap, int xsize, int ysize,
                     char *filename)
{
    FILE *f;
    int i;

    f = fopen(filename,"wb");
    fprintf(f, "P5\n%d %d\n%d\n", xsize, ysize, 255);
    for (i = 0; i < ysize; i++)
        fwrite(buf + i * wrap, 1, xsize, f);
    fclose(f);
}

static void decode(AVCodecContext *dec_ctx, AVFrame *frame, AVPacket *pkt,
                   const char *filename)
{
    char buf[1024];
    int ret;

    ret = avcodec_send_packet(dec_ctx, pkt);
    if (ret < 0) {
        fprintf(stderr, "Error sending a packet for decoding\n");
        exit(1);
    }

    while (ret >= 0) {
        ret = avcodec_receive_frame(dec_ctx, frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
            return;
        else if (ret < 0) {
            fprintf(stderr, "Error during decoding\n");
            exit(1);
        }

        printf("saving frame %3"PRId64"\n", dec_ctx->frame_num);
        fflush(stdout);

        /* the picture is allocated by the decoder. no need to
           free it */
        snprintf(buf, sizeof(buf), "%s-%"PRId64, filename, dec_ctx->frame_num);
        pgm_save(frame->data[0], frame->linesize[0],
                 frame->width, frame->height, buf);
    }
}

int main(int argc, char **argv)
{
    const char *filename, *outfilename;
    const AVCodec *codec;
    AVCodecParserContext *parser;
    AVCodecContext *c= NULL;
    FILE *f;
    AVFrame *frame;
    uint8_t inbuf[INBUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE];
    uint8_t *data;
    size_t   data_size;
    int ret;
    int eof;
    AVPacket *pkt;

    if (argc <= 2) {
        fprintf(stderr, "Usage: %s <input file> <output file>\n"
                "And check your input file is encoded by mpeg1video please.\n", argv[0]);
        exit(0);
    }
    filename    = argv[1];
    outfilename = argv[2];

    pkt = av_packet_alloc();
    if (!pkt)
        exit(1);

    /* set end of buffer to 0 (this ensures that no overreading happens for damaged MPEG streams) */
    memset(inbuf + INBUF_SIZE, 0, AV_INPUT_BUFFER_PADDING_SIZE);

    /* find the MPEG-1 video decoder */
    codec = avcodec_find_decoder(AV_CODEC_ID_MPEG1VIDEO);
    if (!codec) {
        fprintf(stderr, "Codec not found\n");
        exit(1);
    }

    parser = av_parser_init(codec->id);
    if (!parser) {
        fprintf(stderr, "parser not found\n");
        exit(1);
    }

    c = avcodec_alloc_context3(codec);
    if (!c) {
        fprintf(stderr, "Could not allocate video codec context\n");
        exit(1);
    }

    /* For some codecs, such as msmpeg4 and mpeg4, width and height
       MUST be initialized there because this information is not
       available in the bitstream. */

    /* open it */
    if (avcodec_open2(c, codec, NULL) < 0) {
        fprintf(stderr, "Could not open codec\n");
        exit(1);
    }

    f = fopen(filename, "rb");
    if (!f) {
        fprintf(stderr, "Could not open %s\n", filename);
        exit(1);
    }

    frame = av_frame_alloc();
    if (!frame) {
        fprintf(stderr, "Could not allocate video frame\n");
        exit(1);
    }

    do {
        /* read raw data from the input file */
        data_size = fread(inbuf, 1, INBUF_SIZE, f);
        if (ferror(f))
            break;
        eof = !data_size;

        /* use the parser to split the data into frames */
        data = inbuf;
        while (data_size > 0 || eof) {
            ret = av_parser_parse2(parser, c, &pkt->data, &pkt->size,
                                   data, data_size, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
            if (ret < 0) {
                fprintf(stderr, "Error while parsing\n");
                exit(1);
            }
            data      += ret;
            data_size -= ret;

            if (pkt->size)
                decode(c, frame, pkt, outfilename);
            else if (eof)
                break;
        }
    } while (!eof);

    /* flush the decoder */
    decode(c, frame, NULL, outfilename);

    fclose(f);

    av_parser_close(parser);
    avcodec_free_context(&c);
    av_frame_free(&frame);
    av_packet_free(&pkt);

    return 0;
}

视频解码完整示例,其中 save_frame_to_image 用于把视频帧保存成图片,以便查看解码效果,在后续文章中将详细介绍,目前可忽略。

#include <string>
extern "C" {        // 用C规则编译指定的代码
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavutil/avutil.h"
#include "libswscale/swscale.h"
}

using namespace std;
// 保存成图片,可以先忽略
bool save_frame_to_image(AVFrame* frame, const string& filename) {
    if (!frame) {
        return false;
    }

    // 格式转换
    AVFrame* rgb_frame = av_frame_alloc();
    if (!rgb_frame) {
        return false;
    }

    rgb_frame->format = AV_PIX_FMT_RGB24;
    rgb_frame->width = frame->width;
    rgb_frame->height = frame->height;

    int ret = av_frame_get_buffer(rgb_frame, 0);
    if (ret < 0) {
        av_frame_free(&rgb_frame);
        return false;
    }

    SwsContext* sws_ctx = sws_getContext(
        frame->width, frame->height, (AVPixelFormat)frame->format,
        rgb_frame->width, rgb_frame->height, AV_PIX_FMT_RGB24,
        SWS_BILINEAR, nullptr, nullptr, nullptr
        );
    if (!sws_ctx) {
        av_frame_free(&rgb_frame);
        return false;
    }

    ret = sws_scale(
        sws_ctx,
        frame->data, frame->linesize,
        0, frame->height,
        rgb_frame->data, rgb_frame->linesize
        );

    // 查找编码器
    const AVCodec* codec = avcodec_find_encoder(AV_CODEC_ID_PNG);
    if (!codec) {
        return false;
    }

    // 分配编码器上下文
    AVCodecContext* codec_ctx = avcodec_alloc_context3(codec);
    if (!codec_ctx) {
        return false;
    }

    // 设置编码器参数
    codec_ctx->width = frame->width;
    codec_ctx->height = frame->height;
    codec_ctx->pix_fmt = AV_PIX_FMT_RGB24;
    codec_ctx->time_base = {1, 25};

    // 打开编码器
    ret = avcodec_open2(codec_ctx, codec, nullptr);
    if (ret < 0) {
        avcodec_free_context(&codec_ctx);
        return false;
    }

    // 分配输出格式上下文
    AVFormatContext* fmt_ctx = nullptr;
    ret = avformat_alloc_output_context2(&fmt_ctx, nullptr, nullptr, filename.c_str());
    if (ret < 0) {
        avcodec_free_context(&codec_ctx);
        return false;
    }

    // 添加输出流
    AVStream* stream = avformat_new_stream(fmt_ctx, nullptr);
    if (!stream) {
        avcodec_free_context(&codec_ctx);
        avformat_free_context(fmt_ctx);
        return false;
    }
    stream->time_base = codec_ctx->time_base;
    avcodec_parameters_from_context(stream->codecpar, codec_ctx);

    // 打开输出文件
    if (!(fmt_ctx->oformat->flags & AVFMT_NOFILE)) {
        ret = avio_open(&fmt_ctx->pb, filename.c_str(), AVIO_FLAG_WRITE);
        if (ret < 0) {
            avcodec_free_context(&codec_ctx);
            avformat_free_context(fmt_ctx);
            return false;
        }
    }

    // 写入文件头
    ret = avformat_write_header(fmt_ctx, nullptr);
    if (ret < 0) {
        avcodec_free_context(&codec_ctx);
        if (!(fmt_ctx->oformat->flags & AVFMT_NOFILE)) {
            avio_closep(&fmt_ctx->pb);
        }
        avformat_free_context(fmt_ctx);
        return false;
    }

    AVPacket* packet = av_packet_alloc();
    if (!packet) {
        avcodec_free_context(&codec_ctx);
        if (!(fmt_ctx->oformat->flags & AVFMT_NOFILE)) {
            avio_closep(&fmt_ctx->pb);
        }
        avformat_free_context(fmt_ctx);
        return false;
    }
    rgb_frame->pts = 0;

    // 发送 frame 到编码器
    ret = avcodec_send_frame(codec_ctx, rgb_frame);
    if (ret < 0) {
        av_packet_free(&packet);
        avcodec_free_context(&codec_ctx);
        if (!(fmt_ctx->oformat->flags & AVFMT_NOFILE)) {
            avio_closep(&fmt_ctx->pb);
        }
        avformat_free_context(fmt_ctx);
        return false;
    }

    // 从编码器接收 packet 并写入文件
    while (ret >= 0) {
        ret = avcodec_receive_packet(codec_ctx, packet);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            break;
        } else if (ret < 0) {
            break;
        }

        av_packet_rescale_ts(packet, codec_ctx->time_base, stream->time_base);
        packet->stream_index = stream->index;

        // 写入 packet
        ret = av_interleaved_write_frame(fmt_ctx, packet);
        if (ret < 0) {
           // break;
        }

        av_packet_unref(packet);
    }

    // 写入文件尾
    av_write_trailer(fmt_ctx);

    // 释放资源
    sws_freeContext(sws_ctx);
    av_packet_free(&packet);
    avcodec_free_context(&codec_ctx);
    if (!(fmt_ctx->oformat->flags & AVFMT_NOFILE)) {
        avio_closep(&fmt_ctx->pb);
    }
    avformat_free_context(fmt_ctx);
    return true;
}

int main() {
    AVFormatContext *fmt_ctx = NULL;
    const char *filename = "/test.mp4";
    // 已不再需要
    // avformat_network_init();

    // 打开输入文件
    int ret = avformat_open_input(&fmt_ctx, filename, NULL, NULL);
    if (ret < 0) {
        return ret;
    }
    // 查找流信息
    ret = avformat_find_stream_info(fmt_ctx, NULL);
    if (ret < 0) {
        // 没有找到视频流信息
        return ret;
    }

    // 查找视频流
    int video_index = -1;
    video_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);

    if (video_index == -1) {
        return -1;
    }

    AVStream *src_video = fmt_ctx->streams[video_index];
    // 查找视频解码器
    enum AVCodecID video_codec_id = src_video->codecpar->codec_id;
    const AVCodec *video_codec = avcodec_find_decoder(video_codec_id);
    if (!video_codec) {
        return -1;
    }
    /*
     * AVCodec 存储解码器的属性
     * AVCodecContext 用于解码,编码操作
     */
    // 分配解码器的实例
    AVCodecContext *video_decode_ctx = avcodec_alloc_context3(video_codec);
    // 把视频流中的编解码参数复制给解码器的实例
    avcodec_parameters_to_context(video_decode_ctx, src_video->codecpar);
    // 打开解码器的实例
    ret = avcodec_open2(video_decode_ctx, NULL, NULL);
    if (ret < 0) {
        return ret;
    }
    /*
     *  函数名带 alloc 基本就是用于创建对象 理解成 new 即可
     */
    AVPacket* packet = av_packet_alloc();
    AVFrame*  frame  = av_frame_alloc();

    av_packet_unref(packet);
    int i = 0;
    /*
     * 对于视频,数据包恰好包含一帧;对于音频,若每帧大小固定(如PCM或ADPCM),
     * 则包含整数个帧;若音频帧大小可变(如MPEG音频),则包含一帧。
     */
    // 读取数据 这里是把视频包读到 packet , 函数名称则是历史遗留问题
    while (av_read_frame(fmt_ctx, packet) >= 0) { // 轮询数据包
        if (packet->stream_index == video_index) {
            // 把未解压的数据包发给解码器实例
            ret = avcodec_send_packet(video_decode_ctx, packet);
            // 从解码器实例获取还原后的数据帧
            while ( ret >= 0) {
                av_frame_unref(frame);
                ret = avcodec_receive_frame(video_decode_ctx, frame);
                if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
                    break;
                } else if (ret < 0) {
                    return -1;
                }
                i++;
                string filepath = "/images/" + to_string(i) + ".png";
                save_frame_to_image(frame, filepath);

            }
        }
			 av_packet_unref(packet);
    }
    // 关闭文件并释放上下文
    avformat_close_input(&fmt_ctx);
    return 0;
}

更多推荐