FFmpeg的帧

之前FFmpeg频频出场,都是它的应用,但FFmpeg本身的结构或流程却还没有介绍过。就“能用即可”的角度,能把FFmpeg这个黑盒子用好,就已经是很好的成绩了。

但追求理解甚至想修改FFmpeg的你,应该会关心FFmpeg本身的结构与处理流程。

于是,小程准备用若干篇文章来介绍FFmpeg的结构与流程。在介绍过程中,小程尽量引用具体的数值,让你对结构有个直观的感知。为了拿到具体的数据,需要调试FFmpeg的代码,这部分的内容(包括gdb的使用)小程已经在前面的章节介绍过了。

本文介绍FFmpeg的帧的结构。

这里的帧并不是我们说的图像帧,它只是一个数据载体或一个结构体而已(可以是图像或音频数据)。

FFmpeg的“帧”涉及到两个结构,即AVPacket,以及AVFrame。

(一)AVPacket

AVPacket,是压缩数据的结构体,也就是解码前或编码后的数据的载体。

为了查看AVPacket结构体的变量的值,小程写了一段调用FFmpeg的代码:

#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"

void show_frame(const char* filepath) {
    av_register_all();
    av_log_set_level(AV_LOG_DEBUG);
    AVFormatContext* formatContext = avformat_alloc_context();
    int status = 0;
    int success = 0;
    int videostreamidx = -1;
    AVCodecContext* codecContext = NULL;
    status = avformat_open_input(&formatContext, filepath, NULL, NULL);
    if (status == 0) {
        status = avformat_find_stream_info(formatContext, NULL);
        if (status >= 0) {
            for (int i = 0; i < formatContext->nb_streams; i ++) {
                if (formatContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
                    videostreamidx = i;
                    break;
                }
            }
            if (videostreamidx > -1) {
                AVStream* avstream = formatContext->streams[videostreamidx];
                codecContext = avstream->codec;
                AVCodec* codec = avcodec_find_decoder(codecContext->codec_id);
                if (codec) {
                    status = avcodec_open2(codecContext, codec, NULL);
                    if (status == 0) {
                        success = 1;
                    }
                }
            }
        }
        else {
            av_log(NULL, AV_LOG_DEBUG, "avformat_find_stream_info error\n");
        }

        if (success) {
            av_dump_format(formatContext, 0, filepath, 0);
            int gotframe = 0;
            AVFrame* frame = av_frame_alloc();
            int decodelen = 0;
            int limitcount = 10;
            int pcindex = 0;
            while (pcindex < limitcount) {
                AVPacket packet;
                av_init_packet( &packet );
                status = av_read_frame(formatContext, &packet);
                if (status < 0) {
                    if (status == AVERROR_EOF) {
                        av_log(NULL, AV_LOG_DEBUG, "read end for file\n");
                    }
                    else {
                        av_log(NULL, AV_LOG_DEBUG, "av_read_frame error\n");
                    }
                    av_packet_unref(&packet);
                    break;  
                }
                else {
                    if (packet.stream_index == videostreamidx) {
                        decodelen = avcodec_decode_video2(codecContext, frame, &gotframe, &packet);
                        if (decodelen > 0 && gotframe) {
                            av_log(NULL, AV_LOG_DEBUG, "got one avframe, pcindex=%d\n", pcindex);
                        }   
                    }
                } 
                av_packet_unref(&packet);
                pcindex ++;
            }
            av_frame_free(&frame);
        }
        avformat_close_input(&formatContext);
    }
    avformat_free_context(formatContext);
}

int main(int argc, char *argv[])
{
    show_frame("moments.mp4");
    return 0;
}

从上面的代码可以看到,调用av_read_frame可以取得一个AVPacket,所以在调用这个函数的地方下个断点,看一下AVPacet长什么样子。

先说一下怎么编译上面这段代码,我使用的是mac电脑。

把上面的代码保存成show_frame.c文件,然后写一个makefile编译脚本(保存成makefile文件,与show_frame.c在同一目录),内容如下:

exe=showframe
srcs=show_frame.c 
$(exe):$(srcs)
    gcc -o $(exe) $(srcs) -Iffmpeg/include/ -Lffmpeg -lffmpeg -liconv -lz -g
clean:
    rm -f $(exe) *.o

整个项目的代码目录结构是这样的:
项目目录结构

注意,上图的show_avcodec.c要换成show_frame.c,小程沿用了另一个例子的截图,并且没有更改:)。

另外,FFmpeg是事先就编译了的,对于FFmpeg的编译,前文已说。

准备好环境后,就可以编译这段代码了:

make

编译代码后,使用gdb启动调试:

gdb showframe
b 38
r

单步调试时,可以看到,在没有调用av_read_frame前,AVPacket中的变量值:
没有调用av_read_frame的avpacket

调用av_read_frame后,AVPacket中的变量:
avpacket1

再一次av_read_frame后:
avpacket2

av_read_frame后,AVPacket也可能没有数据:
avpacket没有数据的情况

AVPacket是压缩数据,一个AVPacket,最多包含一帧视频数据,但可以包括多帧音频数据。AVPacket中的变量含义:

pts/dts,显示/解码时间戵,以packet所在的流的time_base为单位。
stream_index,所在流的索引。
data,avpacket拥有的数据。
size,avpacket的数据长度。
duration,avpacket的时长,同样以time_base为单位。

AVPacket结构,在libavcodec/avcodec.h中定义,你可以详细看下这个头文件的说明。

(二)AVFrame

AVFrame,是原始数据的结构体,也就是解码后或编码前的数据的载体。

可以简单理解为,AVFrame就是原始的音频或视频数据。有了它,可以做一些处理,比如音效或图效处理、特征提取、特征图绘制,等等。

为了看AVFrame的数据,使用调试AVPacket的代码即可,部分代码如截图:
调试AVFrame的代码片段

然后在avcodec_decode_video2的调用处下个断点,使用gdb进行单步调试。

可以看到,在解码前,avframe是这样的:
解码前avframe的内容

解码后,并且保证有解码到一帧数据时,avframe是这样的:
解码后avframe的内容

以下对AVFrame的一些变量作一些解释:

data,指针数组(最多8个指针),每个指针指向不同维度的byte数据。
对于视频来说,如果是planar的,则data[0]可能指向Y维度的数据,data[1]可能指向U维度的数据。
对于音频来说,如果声道是平面组织的(planar),则data[0]指向一个声道,data[1]指向另一个声道...;如果声音是打包形式的(packed,即左右不分开),则只有data[0]。

linesize,长度的数组。
对于视频,如果是planar数据,则linesize[i]是某个维度的一行的长度;如果是packet数据,则只有linesize[0],而且表示所有数据的长度。
对于音频,只有linesize[0]可用;如果是planar数据,则linesize[0]对应data[i]的长度(每个data[i]是一样的长度);如果是packed数据,则linesize[0]表示data[0]的长度。对于音频,没有“一行”的概念,linesize[0]表示的是整个长度。
对于视频,注意linesize[i]表示一行的长度时,可能比实际的数据的长度(宽)要大。

extern_data,对于视频,等同于data。对于音频,经常用于packed数据。
width/height,视频宽高。
nb_samples,一个声道的样本数。
format,视频的颜色空间,或音频的样本格式。
key_frame,是否为关键帧。
pict_type,视频帧的类型(ipb帧等)。
sample_aspect_ratio,宽高比例。
pts,表现时间戵。
pkt_pts,对应的AVPacket的pts。
quality,质量系数。
sample_rate,音频采样率。
channels,声道数。

AVFrame结构,在libavutil/frame.h中定义,你可以详细阅读里面的说明。

至此,FFmpeg的帧结构就介绍完毕了。

总结一下,本文介绍了FFmpeg的帧的结构(实际上是数据载体),包括AVFrame与AVPacket,并且通过调试查看了结构中变量的值的变化。


我思故我在

posted on 2019-06-18 16:49 广州小程 阅读() 评论() 编辑 收藏

版权声明:本文为freeself原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/freeself/p/11046127.html