写点什么

ffmpeg 完美实现解封装操作!

用户头像
txp
关注
发布于: 2021 年 04 月 11 日

一、前言

大家好,很长一段时间没有继续更新 ffmpeg 的相关技术文章了,最近更多的时间和精力主要集中在给自己不断灌入新的知识,所以接下来只要有时间就会疯狂输出所学习到的技术干货!

今天我们要分享的主要音视频里面的解封装过程详细解析;在讲解解封装之前,我们简单的来了解一下流媒体文件是如何被播放出来的,要实现播放,那这个过程到底要经历哪些技术处理呢?一般一个音视频流媒体文件播放实现流程图如下:


流媒体文件如何实现播放流程

从上面的流程图中,我们可以发现一个流媒体文件播放实现过程,看上去是不怎么复杂,但是其实里面有很多细小的技术点;今天暂时我们先来掌握解封装!

二、探索解封装的奥秘

1、什么是解封装呢?

在了解什么是解封装之前,不知道大家平时在自己的电脑里面播放视频文件的时候,有没有注意视频文件的后缀格式呢,比如下面几种文件格式:

常用的几种封装格式

上面的 mp4、flv、ts 等都是对音视频数据进行封装的一种封装格式,通俗的讲,就把很多东西合成一个东西,只是合成的这个东西,表现形式不一样而已,用更加的专业术语来讲,这里的合成就是复用器,我们可以用一张图来解释:

复用器

那么听了上面的解说,你自然而然的就会想到解复用器了,那么也就是解封装了,解封装的作用就跟上面的复用器起着相反的作用,就是把一个流媒体文件,拆解成音频数据和视频数据(专业的讲,一般被拆解成 H.264 编码的视频码流和 AAC 编码的音频码流),下面还是用一张图来解释:

解封装(解复用器)

三、利用 ffmpeg 接口实战解封装实现

经过上面的讲解,想必大家对解封装的概念已经非常清楚了;那么接下来呢,我们就可以利用 ffmpeg 里面的 libavformat 库(它是一个包含用于多媒体容器格式的解复用器和复用器的库,里面有很多可供我们开发人员进行实战操作的 api。)调用相关 api 来实现解封装的具体操作。

1、工欲善必先利其器:

在开始写代码实现之前呢,我们还要了解一下解封装的一个具体流程和相应的 api。我们先把解封装实现相应的 api 接口得介绍一下,不然很多朋友直接看代码实现不知道什么意思,而且也不知道这些接口说明去哪里找(这个曾经在交流的时候,还真有人不知道 api 接口里面传的参数是什么意思,其实吧,ffmpeg 官网手册 api 接口介绍里面有非常详细的介绍呢,或者 ffmpeg 源码里面也有 api 接口的详细说明使用!);当然如果有时间,我觉得非常有必要去研究一下 ffmpeg 的源码阅读,千万不要停留在只会调用 api 的层次,更多的是我们要了解背后深层次的东西;源码阅读,我目前在阅读 4.2.1 版本的 ffmpeg 源码:

ffmpeg 4.2.1 版本源码

好了,下面我们开始介绍解封装相关的接口和结构体说明;第一时间,大家可以去官网找到 ffmpeg 的 api 接口说明文档:

https://www.ffmpeg.org/documentation.html
复制代码




解封装常用的 api 如下:

  • avformat_alloc_context():负责申请一个 AVFormatContext 结构体的内存,并进行初始化,它的函数原型如下:


AVFormatContext* avformat_alloc_context (void) 

复制代码
  • avformat_free_context():释放 AVFormatContext 结构体里面的所有东西以及该结构体本身,函数原型如下:


void avformat_free_context(AVFormatContext *s) 

复制代码
  • avformat_open_input():从函数名称就知道是打开要输入的流媒体文件,函数原型如下:

int avformat_open_input ( AVFormatContext **  ps,const char *  url,AVInputFormat *  fmt,AVDictionary **  options )  
复制代码

参数说明:

  • ps:指向用户提供的 AVFormatContext 的指针(由 avformat_alloc_context 分配)。可能是指向 NULL 的指针,在这种情况下,此函数将分配 AVFormatContext 并将其写入 ps。请注意,用户提供的 AVFormatContext 将在失败时释放。

  • url:要打开的流的 url,也就是要打开的流媒体文件。

  • fmt:如果为非 NULL,则此参数强制使用特定的输入格式。否则,将自动检测格式。

  • options:包含 AVFormatContext 和 demuxer-private 选项的字典。返回时,此参数将被销毁并替换为包含未找到的选项的 dict。可能为 NULL。

注意:返回值为 0 的时候表示成功,失败的时候返回 AVERROR,跟 linux 里面的 api 接口机制类似。

  • avformat_close_input():关闭打开的输入 AVFormatContext,释放它及其所有内容,并将*s 设置为 NULL;关闭后就不需要再调用 avformat_free_context()进行释放了。它的函数原型如下:


void avformat_close_input (AVFormatContext **s) 

复制代码
  • avformat_find_stream_info():读取媒体文件的数据包以获取流信息,这对于没有标题的文件格式(例如 MPEG)很有用。在 MPEG-2 重复帧模式的情况下,此功能还可以计算实际帧率。该功能不会更改逻辑文件的位置。被检查的分组可以被缓冲以用于以后的处理。函数原型如下:

int avformat_find_stream_info ( AVFormatContext *  ic,AVDictionary **  options 
复制代码
  • 参数说明:

ic:传入的流媒体文件

options:如果为非 NULL,则是指向字典的 ic.nb_streams 长指针数组,其中第 i 个成员包含与第 i 个流相对应的编解码器选项。返回时,每本词典将填充未找到的选项。

注意:此函数不能保证打开所有编解码器,因此选项在返回时为非空是完全正常的行为。

  • av_read_frame():返回流的下一帧;此函数返回文件中存储的内容,并且不验证解码器是否存在有效的帧。它将文件中存储的内容拆分为多个帧,并为每个调用返回一个帧。它不会忽略有效帧之间的无效数据,从而为解码器提供可能的最大解码信息;如果 pkt-> buf 为 NULL,则该数据包在下一个 av_read_frame()或 avformat_close_input()之前一直有效。否则,数据包将无限期有效。在这两种情况下,当不再需要该数据包时,都必须使用 av_packet_unref 释放它。对于视频,数据包恰好包含一帧。对于音频,如果每个帧具有已知的固定大小(例如 PCM 或 ADPCM 数据),则它包含整数个帧。如果音频帧具有可变大小(例如 MPEG 音频),则它包含一帧。始终将 pkt-> pts,pkt-> dts 和 pkt-> duration 设置为以 AVStream.time_base 为单位的正确值(并猜测格式是否无法提供它们)。如果视频格式具有 B 帧,则 pkt-> pts 可以为 AV_NOPTS_VALUE,因此,如果不对有效载荷进行解压缩,则最好依靠 pkt-> dts。

函数原型如下:


int av_read_frame ( AVFormatContext *  s,AVPacket *  pkt )  

复制代码

注意:返回值为 0 时,表示成功,非 0 表示失败!

  • avformat_seek_file():寻求时间戳记(或者说定位文件位置);将进行搜索,以便可以成功呈现所有活动流的点将最接近 ts,并且在 min / max_ts 之内。活动流是所有具有 AVStream.discard <AVDISCARD_ALL 的流。如果标志包含 AVSEEK_FLAG_BYTE,则所有时间戳均以字节为单位,并且为文件位置(并非所有解复用器均支持)。如果标志包含 AVSEEK_FLAG_FRAME,则所有时间戳都在具有 stream_index 的流中的帧中(并非所有解复用器均支持)。否则,所有时间戳均以 stream_index 选择的流为单位,或者如果 stream_index 为-1,则以 AV_TIME_BASE 单位。如果标志包含 AVSEEK_FLAG_ANY,则将非关键帧视为关键帧(并非所有解复用器均支持此关键帧)。如果标志包含 AVSEEK_FLAG_BACKWARD,则将其忽略。

函数原型如下:


int avformat_seek_file ( AVFormatContext *  s,int  stream_index,int64_t  min_ts,int64_t  ts,int64_t  max_ts,int  flags 
复制代码
  • 参数说明:

s:媒体文件句柄

stream_index:流的索引,用作时基参考

min_ts:最小可接受时间戳

ts:目标时间戳

max_ts:最大可接受时间戳

flag:标志

注意:>=0 表示返回成功,否则都是失败;同时要注意这是仍在构建中的新 seek API 的一部分。因此,请不要使用此功能。它可能随时更改,不要期望与 ABI 兼容

2、解封装相关结构体介绍:

  • AVFormatContext:从上面的 api 介绍中,我们可以经常看到这个结构体,它的重要性不言而喻了,它存储了音视频封装格式含有的信息,这里我不做具体介绍,列了几个出来,感兴趣的朋友可以去 Avformat.h 中查看:

typedef struct AVFormatContext {    /**     * A class for logging and @ref avoptions. Set by avformat_alloc_context().     * Exports (de)muxer private options if they exist.     */    const AVClass *av_class;
    /**     * The input container format.     *     * Demuxing only, set by avformat_open_input().     */    ff_const59 struct AVInputFormat *iformat;
    /**     * The output container format.     *     * Muxing only, must be set by the caller before avformat_write_header().     */    ff_const59 struct AVOutputFormat *oformat;    /**     * Format private data. This is an AVOptions-enabled struct     * if and only if iformat/oformat.priv_class is not NULL.     *     * - muxing: set by avformat_write_header()     * - demuxing: set by avformat_open_input()     */    void *priv_data;
    /**     * I/O context.     *     * - demuxing: either set by the user before avformat_open_input() (then     *             the user must close it manually) or set by avformat_open_input().     * - muxing: set by the user before avformat_write_header(). The caller must     *           take care of closing / freeing the IO context.     *     * Do NOT set this field if AVFMT_NOFILE flag is set in     * iformat/oformat.flags. In such a case, the (de)muxer will handle     * I/O in some other way and this field will be NULL.     */    AVIOContext *pb;
    /* stream info */    /**     * Flags signalling stream properties. A combination of AVFMTCTX_*.     * Set by libavformat.     */    int ctx_flags;
    /**     * Number of elements in AVFormatContext.streams.     *     * Set by avformat_new_stream(), must not be modified by any other code.     */    unsigned int nb_streams;    /**     * A list of all streams in the file. New streams are created with     * avformat_new_stream().     *     * - demuxing: streams are created by libavformat in avformat_open_input().     *             If AVFMTCTX_NOHEADER is set in ctx_flags, then new streams may also     *             appear in av_read_frame().     * - muxing: streams are created by the user before avformat_write_header().     *     * Freed by libavformat in avformat_free_context().     */    AVStream **streams;#if FF_API_FORMAT_FILENAME    /**     * input or output filename     *     * - demuxing: set by avformat_open_input()     * - muxing: may be set by the caller before avformat_write_header()     *     * @deprecated Use url instead.     */    attribute_deprecated    char filename[1024];#endif
..............};
复制代码

大致简化为:

struct AVInputFormat *iformat:输入数据的封装格式
AVIOContext *pb:输入数据的缓存
unsigned int nb_streams:视音频流的个数
AVStream **streams:视音频流
char filename[1024]:文件名
int64_t duration:时长(单位:微秒us,转换为秒需要除以1000000)
int bit_rate:比特率(单位bps,转换为kbps需要除以1000)
AVDictionary *metadata:元数据

复制代码
  • AVStream:表示存储每一个音频和视频流的信息。它也是在头文件 AVformat.h 里面查看:

typedef struct AVStream {    int index;    /**< stream index in AVFormatContext */    /**     * Format-specific stream ID.     * decoding: set by libavformat     * encoding: set by the user, replaced by libavformat if left unset     */    int id;#if FF_API_LAVF_AVCTX    /**     * @deprecated use the codecpar struct instead     */    attribute_deprecated    AVCodecContext *codec;#endif    void *priv_data;
    /**     * This is the fundamental unit of time (in seconds) in terms     * of which frame timestamps are represented.     *     * decoding: set by libavformat     * encoding: May be set by the caller before avformat_write_header() to     *           provide a hint to the muxer about the desired timebase. In     *           avformat_write_header(), the muxer will overwrite this field     *           with the timebase that will actually be used for the timestamps     *           written into the file (which may or may not be related to the     *           user-provided one, depending on the format).     */    AVRational time_base;
................}
复制代码

大致简化为:

int index:标识该视频/音频流
AVCodecContext *codec:指向该视频/音频流的AVCodecContext(它们一一对应)
AVRational time_base:时基。通过该值可以把PTS,DTS转化为真正的时间,只有AVStream中的time_base是可用的。PTS*time_base=真正的时间
int64_t duration:该视频/音频流长度
AVDictionary *metadata:元数据信息
AVRational avg_frame_rate:帧率
AVPacket attached_pic:附带的图片。比如说一些MP3,AAC音频文件附带的专辑封面。

复制代码

3、代码实现框架:解封装流程

上面已经介绍了 api 和解封装结构体,剩下的就是我们该如何实现解封装的核心思想了,有了核心思想,我们就可以达到要实现的解封装效果了:具体流程图如下:

解封装实现流程图

四、解封装具体实现代码:

我这里开发环境是在 qt 下进行开发的,播放的是本地文件:



#include <stdio.h>#include <libavformat/avformat.h>
int main(int argc, char **argv){    //打开网络流。这里如果只需要读取本地媒体文件,不需要用到网络功能,可以不用加上这一句//    avformat_network_init();
    const char *default_filename = "believe.mp4";
    char *in_filename = NULL;
    if(argv[1] == NULL)    {        in_filename = default_filename;    }    else    {        in_filename = argv[1];    }    printf("in_filename = %s\n", in_filename);
    //AVFormatContext是描述一个媒体文件或媒体流的构成和基本信息的结构体    AVFormatContext *ifmt_ctx = NULL;           // 输入文件的demux
    int videoindex = -1;        // 视频索引    int audioindex = -1;        // 音频索引

    // 打开文件,主要是探测协议类型,如果是网络文件则创建网络链接    int ret = avformat_open_input(&ifmt_ctx, in_filename, NULL, NULL);    if (ret < 0)  //如果打开媒体文件失败,打印失败原因    {        char buf[1024] = { 0 };        av_strerror(ret, buf, sizeof(buf) - 1);        printf("open %s failed:%s\n", in_filename, buf);        goto failed;    }
    ret = avformat_find_stream_info(ifmt_ctx, NULL);    if (ret < 0)  //如果打开媒体文件失败,打印失败原因    {        char buf[1024] = { 0 };        av_strerror(ret, buf, sizeof(buf) - 1);        printf("avformat_find_stream_info %s failed:%s\n", in_filename, buf);        goto failed;    }
    //打开媒体文件成功    printf_s("\n==== av_dump_format in_filename:%s ===\n", in_filename);    av_dump_format(ifmt_ctx, 0, in_filename, 0);    printf_s("\n==== av_dump_format finish =======\n\n");    // url: 调用avformat_open_input读取到的媒体文件的路径/名字    printf("media name:%s\n", ifmt_ctx->url);    // nb_streams: nb_streams媒体流数量    printf("stream number:%d\n", ifmt_ctx->nb_streams);    // bit_rate: 媒体文件的码率,单位为bps    printf("media average ratio:%lldkbps\n",(int64_t)(ifmt_ctx->bit_rate/1024));    // 时间    int total_seconds, hour, minute, second;    // duration: 媒体文件时长,单位微妙    total_seconds = (ifmt_ctx->duration) / AV_TIME_BASE;  // 1000us = 1ms, 1000ms = 1秒    hour = total_seconds / 3600;    minute = (total_seconds % 3600) / 60;    second = (total_seconds % 60);    //通过上述运算,可以得到媒体文件的总时长    printf("total duration: %02d:%02d:%02d\n", hour, minute, second);    printf("\n");    /*     * 老版本通过遍历的方式读取媒体文件视频和音频的信息     * 新版本的FFmpeg新增加了函数av_find_best_stream,也可以取得同样的效果     */    for (uint32_t i = 0; i < ifmt_ctx->nb_streams; i++)    {        AVStream *in_stream = ifmt_ctx->streams[i];// 音频流、视频流、字幕流        //如果是音频流,则打印音频的信息        if (AVMEDIA_TYPE_AUDIO == in_stream->codecpar->codec_type)        {            printf("----- Audio info:\n");            // index: 每个流成分在ffmpeg解复用分析后都有唯一的index作为标识            printf("index:%d\n", in_stream->index);            // sample_rate: 音频编解码器的采样率,单位为Hz            printf("samplerate:%dHz\n", in_stream->codecpar->sample_rate);            // codecpar->format: 音频采样格式            if (AV_SAMPLE_FMT_FLTP == in_stream->codecpar->format)            {                printf("sampleformat:AV_SAMPLE_FMT_FLTP\n");            }            else if (AV_SAMPLE_FMT_S16P == in_stream->codecpar->format)            {                printf("sampleformat:AV_SAMPLE_FMT_S16P\n");            }            // channels: 音频信道数目            printf("channel number:%d\n", in_stream->codecpar->channels);            // codec_id: 音频压缩编码格式            if (AV_CODEC_ID_AAC == in_stream->codecpar->codec_id)            {                printf("audio codec:AAC\n");            }            else if (AV_CODEC_ID_MP3 == in_stream->codecpar->codec_id)            {                printf("audio codec:MP3\n");            }            else            {                printf("audio codec_id:%d\n", in_stream->codecpar->codec_id);            }            // 音频总时长,单位为秒。注意如果把单位放大为毫秒或者微妙,音频总时长跟视频总时长不一定相等的            if(in_stream->duration != AV_NOPTS_VALUE)            {                int duration_audio = (in_stream->duration) * av_q2d(in_stream->time_base);                //将音频总时长转换为时分秒的格式打印到控制台上                printf("audio duration: %02d:%02d:%02d\n",                       duration_audio / 3600, (duration_audio % 3600) / 60, (duration_audio % 60));            }            else            {                printf("audio duration unknown");            }
            printf("\n");
            audioindex = i; // 获取音频的索引        }        else if (AVMEDIA_TYPE_VIDEO == in_stream->codecpar->codec_type)  //如果是视频流,则打印视频的信息        {            printf("----- Video info:\n");            printf("index:%d\n", in_stream->index);            // avg_frame_rate: 视频帧率,单位为fps,表示每秒出现多少帧            printf("fps:%lffps\n", av_q2d(in_stream->avg_frame_rate));            if (AV_CODEC_ID_MPEG4 == in_stream->codecpar->codec_id) //视频压缩编码格式            {                printf("video codec:MPEG4\n");            }            else if (AV_CODEC_ID_H264 == in_stream->codecpar->codec_id) //视频压缩编码格式            {                printf("video codec:H264\n");            }            else            {                printf("video codec_id:%d\n", in_stream->codecpar->codec_id);            }            // 视频帧宽度和帧高度            printf("width:%d height:%d\n", in_stream->codecpar->width,                   in_stream->codecpar->height);            //视频总时长,单位为秒。注意如果把单位放大为毫秒或者微妙,音频总时长跟视频总时长不一定相等的            if(in_stream->duration != AV_NOPTS_VALUE)            {                int duration_video = (in_stream->duration) * av_q2d(in_stream->time_base);                printf("video duration: %02d:%02d:%02d\n",                       duration_video / 3600,                       (duration_video % 3600) / 60,                       (duration_video % 60)); //将视频总时长转换为时分秒的格式打印到控制台上            }            else            {                printf("video duration unknown");            }
            printf("\n");            videoindex = i;        }    }
    AVPacket *pkt = av_packet_alloc();
    int pkt_count = 0;    int print_max_count = 10;    printf("\n-----av_read_frame start\n");    while (1)    {        ret = av_read_frame(ifmt_ctx, pkt);        if (ret < 0)        {            printf("av_read_frame end\n");            break;        }
        if(pkt_count++ < print_max_count)        {            if (pkt->stream_index == audioindex)            {                printf("audio pts: %lld\n", pkt->pts);                printf("audio dts: %lld\n", pkt->dts);                printf("audio size: %d\n", pkt->size);                printf("audio pos: %lld\n", pkt->pos);                printf("audio duration: %lf\n\n",                       pkt->duration * av_q2d(ifmt_ctx->streams[audioindex]->time_base));            }            else if (pkt->stream_index == videoindex)            {                printf("video pts: %lld\n", pkt->pts);                printf("video dts: %lld\n", pkt->dts);                printf("video size: %d\n", pkt->size);                printf("video pos: %lld\n", pkt->pos);                printf("video duration: %lf\n\n",                       pkt->duration * av_q2d(ifmt_ctx->streams[videoindex]->time_base));            }            else            {                printf("unknown stream_index:\n", pkt->stream_index);            }        }
        av_packet_unref(pkt);    }
    if(pkt)        av_packet_free(&pkt);failed:    if(ifmt_ctx)        avformat_close_input(&ifmt_ctx);

    getchar(); //加上这一句,防止程序打印完信息马上退出    return 0;}

复制代码

最终运行效果如下:

运行结果

注意:不同封装格式的流媒体文件被解封装打印出来的信息是不同的,这点要注意!

五、总结:

今天的分享就到这里了,如果你也喜欢音视频开发,可以相互交流,一起进步;同时如果大家看到我没更新,不是在偷懒,一般是在给自己疯狂输入;好了,我们下期见!


用户头像

txp

关注

还未添加个人签名 2020.02.07 加入

音视频开发者

评论

发布
暂无评论
ffmpeg完美实现解封装操作!