【FFmpeg实战】音视频解封装格式

转载自原文地址:https://www.cnblogs.com/zuojie/p/16798273.html

一、什么是封装格式

封装格式也称为容器,用于打包音频、视频以及字幕等等,比如常见的容器有MP4、MOV、WMV、FLV、AVI、MKV等等。容器里面装的是音视频的压缩帧,但是不是所有类型的压缩帧都可以装入容器中,不同的容器对于压缩帧的格式是有要求的,有一些容器的兼容性要好一些,有一些容器的兼容性就会差一些。

我们平时看到的文件后缀名mp4或者mov是指文件格式,它的作用是让我们知道它是何种类型的文件,让操作系统知道打开文件时应该用哪个应用打开。正常来讲文件后缀名是和封装格式是有对应关系的,每个容器都有一个或多个文件后缀名。虽然说我们可以随意修改文件后缀名,但是封装格式属于文件的内部结构,而文件格式是文件外在表现,所以修改文件扩展名是无法修改容器原封装格式的,修改后播放器一般情况下也是可以播放的,因为播放器在播放时会打开文件判断是哪种容器。

二、使用 FFmpeg 实现解封装

现在对封装格式有了一个简单了解,接下来了解一下封装格式数据是如何被播放出来的,首先要对封装格式数据解封装,可以得到音频压缩数据和视频压缩数据,然后再对音频压缩数据和视频压缩数据分别进行解码,就得到了音频原始数据和视频原始数据,最后对音频原始数据进行处理送到扬声器,对视频数据进行处理送到屏幕,并且还要进行音视频同步处理。本文主要分享的是如何从封装格式数据中拿到音频原始数据和视频原始数据各自生成相对于的文件,音视频同步处理先不讨论。封装格式数据播放大致实现流程图如下:

img

下面开始使用FFmpeg的libavformat库(它是一个包含用于多媒体容器格式的解复用器和复用器的库)从MP4封装格式中解码出YUV数据(原始视频数据)和PCM数据(原始音频数据)。

1、创建解封装上下文打开流媒体文件

int avformat_open_input(AVFormatContext **ps, const char *url, ff_const59 AVInputFormat *fmt, AVDictionary **options);

参数说明:

  • ps:指向解封装上下文的指针,由avformat_alloc_context创建。如果传nullptr,函数avformat_open_input内部会帮我们创建解封装上下文(注意:函数调用失败时,会释放开发者手动创建的解封装上下文);
  • url:要打开的流的url,也就是要打开的流媒体文件(此处也可以传入设备名称和设备序号,我们在音视频录制时传入的就是设备序号);
  • fmt:如果非nulllptr将使用特定的输入格式,传nulllptr将自动检测输入格式;
  • options:包含解封装上下文和解封装器特有的参数的字典。 最后不要忘记使用函数avformat_close_input关闭解封装上下文,使用函数avformat_close_input就不需要再调用函数avformat_free_context了,其内部帮我们调用了函数avformat_free_context。

2、检索流信息

2.1、检索流信息

该函数可以读取一部分音视频数据并且获得一些相关的信息:

int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);

参数说明:

  • ic:需要读取信息的解封装上下文;
  • options:额外一些参数。

2.2、导出流信息到控制台

我们可以使用下面函数打印检索到的详细信息到控制台,包括音频流的采样率、通道数等,视频流包括视频的width、height、pixel format、码率、帧率等信息:

void av_dump_format(AVFormatContext *ic,
                    int index,
                    const char *url,
                    int is_output);

参数说明:

  • ic:需要打印分析的解封装上下文;
  • index:需要导出信息的流索引;
  • url:需要打印的输入或者输出流媒体文件 url。
  • is_output:是否输出,0 = 输入 / 1 = 输出;

在QT中直接调用这个方法,还不能打印出信息,通过看ffmpeg的官方例子里的信息可以了解到是要dump到stderr的控制台:

img

因此在Qt中还需要调用fflush(stderr)才能够将信息输出到控制台。fflush会强迫将缓冲区内容清空,就会立即输出所有在缓冲区中的内容。stderr是指标准错误输出设备,输出的文本内容一般是红色的,默认向屏幕输出内容。

打印的信息如下,这和我们在终端看到的信息是一样的:

img

看这个打印是不是很眼熟,跟我们在命令行中敲ffmpeg相关命令时打印的一样的

img

3、初始化音频解码器查找合适的音视流和视频流信息

读取多媒体文件音频流和视频流信息,函数av_find_best_stream是在FFmpeg新版本中添加的,老版本只可通过遍历的方式读取,我们可以通过stream->codecpar->codec_type判断流类型,可以取得同样的效果:

int av_find_best_stream(AVFormatContext *ic,
                        enum AVMediaType type,
                        int wanted_stream_nb,
                        int related_stream,
                        AVCodec **decoder_ret,
                        int flags);

参数说明:

  • ic:需要处理的流媒体文件,解封装上下文中包含流媒体文件信息;
  • type:要检索的流类型,比如音频流、视频流和字幕流等等;
  • wanted_stream_nb:请求的流序号,传 -1 自动选择;
  • related_stream:查找相关流,不查找传 -1;
  • decoder_ret:返回当前流对应的解码器。函数调用成功,并且参数 decoder_ret 不为 nullptr,将通过参数 decoder_ret 返回一个对应的解码器;
  • flags:目前没有定义;

AVMediaType流类型枚举:

enum AVMediaType {
    AVMEDIA_TYPE_UNKNOWN = -1,  ///< Usually treated as AVMEDIA_TYPE_DATA
    AVMEDIA_TYPE_VIDEO,
    AVMEDIA_TYPE_AUDIO,
    AVMEDIA_TYPE_DATA,          ///< Opaque data information usually continuous
    AVMEDIA_TYPE_SUBTITLE,
    AVMEDIA_TYPE_ATTACHMENT,    ///< Opaque data information usually sparse
    AVMEDIA_TYPE_NB
};

函数调用成功返回流序号,如果未找到请求类型的流返回AVERROR_STREAM_NOT_FOUND,如果找到了请求的流但是没有对应的解码器将返回AVERROR_DECODER_NOT_FOUND。

4、检验流

我们成功的查找到流后最好要检验一下流是否真的存在;

AVStream *stream = _fmtCtx->streams[*streamIdx];
if (!stream) {
    qDebug() << "stream is empty";
    return -1;
}

5、查找解码器

我们通过stream->codecpar->codec_id可以查找到对应的解码器:

AVCodec *avcodec_find_decoder(enum AVCodecID id);

参数说明:

  • codec:解码器;

最后需要使用函数avcodec_free_context释放解码上下文。

6、拷贝流参数到解码器

在FFmpeg旧版本中保存流信息参数是AVStream结构体中的codec字段。新版本中已经将AVStream结构体中的codec字段定义为废弃属性。因此无法像以前旧版本中直接通过参数codec获取流信息。当前版本保存流信息的参数是AVStream结构体中的codecpar字段,FFmpeg提供了函数avcodec_parameters_to_context将流信息拷贝到新的解码器中:

int avcodec_parameters_to_context(AVCodecContext *codec,
                                  const AVCodecParameters *par);

参数说明:

  • codec:解码器;
  • par:流中的参数,通过stream->codecpar获取;

7、打开解码器

int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);

参数说明:

  • avctx:需要初始化的解码上下文;
  • codec:解码器;
  • options:包含解封装上下文和解封装器特有的参数的字典。

8、从音视频流中读取压缩帧

我们可以通过pkt->stream_index判断读取到的压缩帧是音频压缩帧还是视频压缩帧等等,然后分别对音视频压缩数据进行解码:

int av_read_frame(AVFormatContext *s, AVPacket *pkt);

参数说明:

  • s:解封装上下文;
  • pkt:读取到的压缩帧数据;

在调用函数avcodec_send_packet之前我们需要创建一个AVPacket。在FFmpeg版本4.4中av_init_packet函数已经过期,实际上FFmpeg不建议我们把AVPacket放到栈空间了。建议使用函数av_packet_alloc来创建,av_packet_alloc创建的AVPacket是在堆空间的。下面写法不提倡:

// pkt 是在函数中定义,pkt 内存在栈空间,所以 pkt 内存不需要我们去申请和释放
AVPacket pkt;
// init 仅仅是初始化,并不会分配内存
av_init_packet(&pkt);
pkt.data = nullptr;
pkt.size = 0;

最后需要使用函数av_packet_free释放AVPacket,注意函数av_packet_unref仅仅是把AVPacket指向的一些额外内存释放掉,并不会释放AVPacket内存空间。

9、音视频解码

9.1发送压缩数据到解码器

首先使用函数avcodec_send_packet发送压缩数据到解码器:

int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);

9.2获取解码后的数据

然后使用函数avcodec_receive_frame从解码器中读取解码后的数据:

int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);

10、保存音视频输出参数

我们定义了两个结构体分别保存音频输出参数和视频输出参数:

// 音频输出参数
typedef struct {
    const char *filename; // 文件名
    int sampleRate; // 采样率
    AVSampleFormat sampleFmt; // 采样格式
    int chLayout; // 声道布局
} AudioDecodeSpec;
 
// 视频输出参数
typedef struct {
    const char *filename; // 文件名
    int width; // 宽
    int height; // 高
    AVPixelFormat pixFmt; // 像素格式
    int fps; // 帧率
} VideoDecodeSpec;

保存音频参数:

_aOut->sampleRate = _aDecodeCtx->sample_rate;
_aOut->sampleFmt = _aDecodeCtx->sample_fmt;
_aOut->chLayout = _aDecodeCtx->channel_layout;

保存视频参数:

_vOut->width = _vDecodeCtx->width;
_vOut->height = _vDecodeCtx->height;
_vOut->pixFmt = _vDecodeCtx->pix_fmt;
_vOut->fps = _vDecodeCtx->framerate.num / _vDecodeCtx->framerate.den;

通过上面方法获取到的帧率有可能是0,我们需要使用函数av_guess_frame_rate获取帧率:

AVRational framerate = av_guess_frame_rate(_fmtCtx, _fmtCtx->streams[_vStreamIdx], nullptr);
_vOut->fps = framerate.num / framerate.den;

10、音视频原始数据写入文件

10.1、音频原始数据写入文件

我们的最终目的是将音频原始数据写入到PCM文件并使用ffplay命令进行播放。因为播放器是不支持播放planar格式数据的,所以要求写入文件的数据为非planar格式。我们可以通过函数av_sample_fmt_is_planar来判断当前音频原始数据是否为planar格式,对于planar格式数据,我们要把每个声道中的音频样本交错写入文件。非planar格式直接写入文件即可:

img

// _sampleSize 每个音频样本的大小
_sampleSize = av_get_bytes_per_sample(_aOut->sampleFmt);
// _sampleFrameSize 每个音频样本帧的大小
_sampleFrameSize = _sampleSize * _aDecodeCtx->channels;
 
 
void Demuxer::writeAudioFrame(){
    // libfdk_aac解码器,解码出来的PCM格式:s16
    // aac解码器,解码出来的PCM格式:ftlp
    if(av_sample_fmt_is_planar(_aOut->sampleFmt)){// 是planar格式
        // 外层循环:每一个声道的样本数
        // si = sample index
        for (int si = 0; si < _frame->nb_samples; si++) {
           for (int ci = 0; ci < _aDecodeCtx->channels; ci++) {
               uint8_t *begin = (uint8_t *)(_frame->data[ci] + _sampleSize * si);
               _aOutFile.write((char *)begin, _sampleSize);
           }
       }
    }else{// 非planar格式
//        _aOutFile.write((char *)_frame->data[0],_frame->linesize[0]);
//        int v1 = _frame->linesize[0];
//        int v2 = _frame->nb_samples // 多少个样本
//                * av_get_bytes_per_sample(_aOut->sampleFmt) // 每个样本多大
//                * _aDecodeCtx->channels;// 声道
//        if(v1 != v2){
//            qDebug() <<"_frame->linesize[0]"<<v1 <<"_frame->nb_samples "<< v2;
//        }
        _aOutFile.write((char *)_frame->data[0],_frame->nb_samples * _sampleFrameSize);
    }
}

函数av_sample_fmt_is_planar内部会去sample_fmt_info表中查询当前采样格式是否为planar格式:

// 源码片段 ffmpeg-4.3.2/libavutil/samplefmt.c
/** this table gives more information about formats */
static const SampleFmtInfo sample_fmt_info[AV_SAMPLE_FMT_NB] = {
    [AV_SAMPLE_FMT_U8]   = { .name =   "u8", .bits =  8, .planar = 0, .altform = AV_SAMPLE_FMT_U8P  },
    [AV_SAMPLE_FMT_S16]  = { .name =  "s16", .bits = 16, .planar = 0, .altform = AV_SAMPLE_FMT_S16P },
    [AV_SAMPLE_FMT_S32]  = { .name =  "s32", .bits = 32, .planar = 0, .altform = AV_SAMPLE_FMT_S32P },
    [AV_SAMPLE_FMT_S64]  = { .name =  "s64", .bits = 64, .planar = 0, .altform = AV_SAMPLE_FMT_S64P },
    [AV_SAMPLE_FMT_FLT]  = { .name =  "flt", .bits = 32, .planar = 0, .altform = AV_SAMPLE_FMT_FLTP },
    [AV_SAMPLE_FMT_DBL]  = { .name =  "dbl", .bits = 64, .planar = 0, .altform = AV_SAMPLE_FMT_DBLP },
    [AV_SAMPLE_FMT_U8P]  = { .name =  "u8p", .bits =  8, .planar = 1, .altform = AV_SAMPLE_FMT_U8   },
    [AV_SAMPLE_FMT_S16P] = { .name = "s16p", .bits = 16, .planar = 1, .altform = AV_SAMPLE_FMT_S16  },
    [AV_SAMPLE_FMT_S32P] = { .name = "s32p", .bits = 32, .planar = 1, .altform = AV_SAMPLE_FMT_S32  },
    [AV_SAMPLE_FMT_S64P] = { .name = "s64p", .bits = 64, .planar = 1, .altform = AV_SAMPLE_FMT_S64  },
    [AV_SAMPLE_FMT_FLTP] = { .name = "fltp", .bits = 32, .planar = 1, .altform = AV_SAMPLE_FMT_FLT  },
    [AV_SAMPLE_FMT_DBLP] = { .name = "dblp", .bits = 64, .planar = 1, .altform = AV_SAMPLE_FMT_DBL  },
};

Planar格式

img

_frame->linesize[0]如果是视频表示的是0号平面它每一行的大小,而音频没有行的概念,是表示一个平面有多大。在音频中,planar格式每个声道的大小都是一样的,所有只有linesize[0]有值,linesize[1]是没有值的。

img

注意:

libfdk_aac解码器,解码出来的PCM格式:s16(非Planar格)

aac解码器,解码出来的PCM格式:ftlp(Planar格)

img

非Planar格式

img

在之前的AAC解码实战那里是按照的方式进行文件的写入:

_aOutFile.write((char *)_frame->data[0],_frame->linesize[0]);

但是这里如果使用linesize[0]来写入数据,会出现写入的文件大小比ffmpeg生成的文件大一些,相差了3072,由于是双声道的s16,所以一个样本帧是4个字节,有3072/4 = 768个样本帧。

img

可以通过下面的打印知道4096 -1024正好是3072

int v1 = _frame->linesize[0];
int v2 = _frame->nb_samples // 多少个样本
        * av_get_bytes_per_sample(_aOut->sampleFmt) // 每个样本多大
        * _aDecodeCtx->channels;// 声道
if(v1 != v2){
    qDebug() <<"_frame->linesize[0]"<<v1 <<"_frame->nb_samples "<< v2;
}

打印结果:

img

linesize是指缓冲区大小,而_frame->nb_samples是指frame里面解码出来有多少个音频样本,有可能frame中的样本数量并不足以填满缓冲区,所以它的大小不一定等于linesize[0],而是小于等于linesize[0]。所以inesize[0]还是有些不靠谱的.

img

10.2、视频原始数据写入文件

// 创建用于存放一帧解码图片的缓冲区
_imgSize = av_image_alloc(_imgBuf,_imgLinesizes,_vOut->width,_vOut->height,_vOut->pixFmt,1);
// _imgSize = av_image_get_buffer_size(_vOut->pixFmt,_vOut->width,_vOut->height,1);
 
 
void Demuxer::writeVideoFrame()
{
    // 拷贝frame的数据到_imgBuf缓冲区
    av_image_copy(_imgBuf,_imgLinesizes,(const uint8_t **)(_frame->data),_frame->linesize,
                  _vOut->pixFmt,_vOut->width,_vOut->height);
    // 将缓冲区的数据写入文件
    _vOutFile.write((char *)_imgBuf[0],_imgSize);
}

我们可以使用av_image_get_buffer_size方法来获取一张图片大小,但是这里的av_image_alloc方法的返回值就是一张图片大小,所以可以直接使用av_image_alloc方法的返回值。

11、关闭文件 & 释放资源

_aOutFile->close();
_vOutFile->close();
avcodec_free_context(&_aDecodeCtx);
avcodec_free_context(&_vDecodeCtx);
avformat_close_input(&_fmtCtx);
av_packet_free(&_pkt);
av_frame_free(&_frame);
av_freep(&_imgBuf[0]);

三、使用 FFmpeg 命令行解封装

$ ffmpeg -c:v h264 -c:a aac -i in.mp4 cmd_out.yuv -f f32le cmd_out.pcm

和使用FFmpeg命令行生成的PCM和YUV文件大小对比:

$ ls -al
-rw-r--r--   1 mac  staff          860790 Apr 20 12:38 in.mp4
-rw-r--r--   1 mac  staff         3459072 Apr 20 16:25 cmd_out.pcm
-rw-r--r--   1 mac  staff       125798400 Apr 20 16:25 cmd_out.yuv
-rw-r--r--   1 mac  staff         3459072 Apr 20 16:18 out.pcm
-rw-r--r--   1 mac  staff       125798400 Apr 20 16:18 out.yuv

四、总结

初始化解码器的流程(红框中)音频和视频都是一样的,仅仅AVMediaType不同,解码流程(绿框中)也是一样的。这部分代码音视频是可以共用的。整体流程参考下图:

img

   >>> 音视频开发 视频教程: https://ke.qq.com/course/3202131?flowToken=1031864 
   >>> 音视频开发学习资料、教学视频,免费分享有需要的可以自行添加学习交流群: 739729163  领取
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值