FFmpeg音视频解码原理与实战


***【在线视频教程】***

好文章,来自【福优学苑@音视频+流媒体】

FFmpeg解码音视频流程简介

音视频编码流程简介(使用 ffmpeg)

  • 第一步:注册组件(编码器、解码器…)

  • 第二步:初始化封装格式上下文

  • 第三步:打开输入文件

  • 第四步:创建输出码流(设置为视频流或音频流)

  • 第五步:查找视频/音频编码器

  • 第六步:打开视频/音频编码器

  • 第七步:写入文件头信息(有些文件头信息)->一般情况下都会有

  • 第八步:循环编码视频像素数据->视频压缩数据

    循环编码音频采样数据->音频压缩数据

  • 第九步:将编码后的视频/音频压缩数据写入文件中

  • 第十步:输入像素/采样数据读取完毕后回调函数

    作用:输出编码器中剩余AVPacket



音频解码流程

image.png

关键函数说明:

avcodec_find_decoder:根据指定的AVCodecID查找注册的解码器。

av_parser_init:初始化AVCodecParserContext。

avcodec_alloc_context3:为AVCodecContext分配内存。

avcodec_open2:打开解码器。

av_parser_parse2:解析获得一个Packet。

avcodec_send_packet:将AVPacket压缩数据给解码器。

avcodec_receive_frame:获取到解码后的AVFrame数据。

av_get_bytes_per_sample: 获取每个sample中的字节数。


关键数据结构说明:

AVCodecParser用于解析输入的数据流并把它分成一帧一帧的压缩编码数据。

比较形象的说法就是把长长的一段连续的数据“切割”成一段段的数据。


image.png


image.png


音频解码代码:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
//封装格式
#include "libavformat/avformat.h"
//解码
#include "libavcodec/avcodec.h"
//缩放
#include "libswscale/swscale.h"
#include "libswresample/swresample.h"
int main (void)
{
    //1.注册组件
    av_register_all();
    //封装格式上下文
    AVFormatContext *pFormatCtx = avformat_alloc_context();
    //2.打开输入音频文件
    if (avformat_open_input(&pFormatCtx, "test.mp3", NULL, NULL) != 0) {
        printf("%s", "打开输入音频文件失败");
        return;
    }
    //3.获取音频信息
    if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
        printf("%s", "获取音频信息失败");
        return;
    }
    //音频解码,需要找到对应的AVStream所在的pFormatCtx->streams的索引位置
    int audio_stream_idx = -1;
    int i = 0;
    for (; i < pFormatCtx->nb_streams; i++) {
        //根据类型判断是否是音频流
        if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO) {
            audio_stream_idx = i;
            break;
        }
    }
    //4.获取解码器
    //根据索引拿到对应的流,根据流拿到解码器上下文
    AVCodecContext *pCodeCtx = pFormatCtx->streams[audio_stream_idx]->codec;
    //再根据上下文拿到编解码id,通过该id拿到解码器
    AVCodec *pCodec = avcodec_find_decoder(pCodeCtx->codec_id);
    if (pCodec == NULL) {
        printf("%s", "无法解码");
        return;
    }
    //5.打开解码器
    if (avcodec_open2(pCodeCtx, pCodec, NULL) < 0) {
        printf("%s", "编码器无法打开");
        return;
    }
    //编码数据
    AVPacket *packet = av_malloc(sizeof(AVPacket));
    //解压缩数据
    AVFrame *frame = av_frame_alloc();
    
    //frame->16bit 44100 PCM 统一音频采样格式与采样率
    SwrContext *swrCtx = swr_alloc();
    //重采样设置选项-----------------------------------------------------------start
    //输入的采样格式
    enum AVSampleFormat in_sample_fmt = pCodeCtx->sample_fmt;
    //输出的采样格式 16bit PCM
    enum AVSampleFormat out_sample_fmt = AV_SAMPLE_FMT_S16;
    //输入的采样率
    int in_sample_rate = pCodeCtx->sample_rate;
    //输出的采样率
    int out_sample_rate = 44100;
    //输入的声道布局
    uint64_t in_ch_layout = pCodeCtx->channel_layout;
    //输出的声道布局
    uint64_t out_ch_layout = AV_CH_LAYOUT_MONO;
    swr_alloc_set_opts(swrCtx, out_ch_layout, out_sample_fmt, out_sample_rate, in_ch_layout, in_sample_fmt,
            in_sample_rate, 0, NULL);
    swr_init(swrCtx);
    //重采样设置选项-----------------------------------------------------------end
    //获取输出的声道个数
    int out_channel_nb = av_get_channel_layout_nb_channels(out_ch_layout);
    //存储pcm数据
    uint8_t *out_buffer = (uint8_t *) av_malloc(2 * 44100);
    FILE *fp_pcm = fopen("out.pcm", "wb");
    int ret, got_frame, framecount = 0;
    //6.一帧一帧读取压缩的音频数据AVPacket
    while (av_read_frame(pFormatCtx, packet) >= 0) {
        if (packet->stream_index == audio_stream_idx) {
            //解码AVPacket->AVFrame
            ret = avcodec_decode_audio4(pCodeCtx, frame, &got_frame, packet);
            if (ret < 0) {
                printf("%s", "解码完成");
            }
            //非0,正在解码
            if (got_frame) {
                printf("解码%d帧", framecount++);
                swr_convert(swrCtx, &out_buffer, 2 * 44100, frame->data, frame->nb_samples);
                //获取sample的size
                int out_buffer_size = av_samples_get_buffer_size(NULL, out_channel_nb, frame->nb_samples,
                        out_sample_fmt, 1);
                //写入文件进行测试
                fwrite(out_buffer, 1, out_buffer_size, fp_pcm);
            }
        }
        av_free_packet(packet);
    }
    fclose(fp_pcm);
    av_frame_free(&frame);
    av_free(out_buffer);
    swr_free(&swrCtx);
    avcodec_close(pCodeCtx);
    avformat_close_input(&pFormatCtx);
    return 0;
}





视频解码

在视频解码前,先了解以下几个基本的概念:


编解码器(CODEC):能够进行视频和音频压缩(CO)与解压缩(DEC),是视频编解码的核心部分。

容器/多媒体文件(Container/File):没有了解视频的编解码之前,总是错误的认为平常下载的电影的文件的后缀(avi,mkv,rmvb等)就是视频的编码方式。事实上,刚才提到的几种文件的后缀

并不是视频的编码方式,只是其封装的方式。一个视频文件通常有视频数据、音频数据以及字幕等,封装的格式决定这些数据在文件中是如何的存放的,封装在一起音频、视频等数据组成的多媒体文件,也可以叫做容器(其中包含了视音频数据)。所以,只看多媒体文件的后缀名是难以知道视音频的编码方式的。

流数据 Stream,例如视频流(Video Stream),音频流(Audio Stream)。流中的数据元素被称为帧Frame。

FFmpeg视频解码过程


通常来说,FFmpeg的视频解码过程有以下几个步骤:


  • 注册所支持的所有的文件(容器)格式及其对应的CODEC av_register_all()

  • 打开文件 avformat_open_input()

  • 从文件中提取流信息 avformat_find_stream_info()

  • 在多个数据流中找到视频流 video stream(类型为MEDIA_TYPE_VIDEO)

  • 查找video stream 相对应的解码器 avcodec_find_decoder

  • 打开解码器 avcodec_open2()

  • 为解码帧分配内存 av_frame_alloc()

  • 从流中读取读取数据到Packet中 av_read_frame()

  • 对video 帧进行解码,调用 avcodec_decode_video2()


解码过程的具体说明

1. 注册

av_register_all该函数注册支持的所有的文件格式(容器)及其对应的CODEC,只需要调用一次,故一般放在main函数中。也可以注册某个特定的容器格式,但通常来说不需要这么做。


2. 打开文件

avformat_open_input该函数读取文件的头信息,并将其信息保存到AVFormatContext结构体中。


3. 获取必要的CODEC参数

avformat_open_input通过解析多媒体文件或流的头信息及其他的辅助数据,能够获取到足够多的关于文件、流和CODEC的信息,并将这些信息填充到AVFormatContext结构体中。但任何一种多媒体格式(容器)提供的信息都是有限的,而且不同的多媒体制作软件对头信息的设置也不尽相同,在制作多媒体文件的时候难免会引入一些错误。也就是说,仅仅通过avformat_open_input并不能保证能够获取所需要的信息,所以一般要使用

avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options)

avformat_find_stream_info主要用来获取必要的CODEC参数,设置到ic->streams[i]->codec。


4. 打开解码器

经过上面的步骤,已经将文件格式信息读取到了AVFormatContext中,要打开流数据相应的CODEC需要经过下面几个步骤


找到视频流 video stream

一个多媒体文件包含有多个原始流,例如 movie.mkv这个多媒体文件可能包含下面的流数据

原始流 1 h.264 video

原始流 2 aac audio for Chinese

原始流 3 aac audio for English

原始流 4 Chinese Subtitle

原始流 5 English Subtitle


5. 读取数据帧并解码

已经有了相应的解码器,下面的工作就是将数据从流中读出,并解码为没有压缩的原始数据


AVPacket packet;

while (av_read_frame(pFormatCtx, &packet) >= 0)

{

        if (packet.stream_index == videoStream)

        {

            int frameFinished = 0;

            avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);

            if (frameFinished)

            {

                doSomething();

            }

        } 

    }


上述代码调用av_read_frame将数据从流中读取数据到packet中,并调用avcodec_decode_video2对读取的数据进行解码。


6. 关闭

需要关闭avformat_open_input打开的输入流,avcodec_open2打开的CODEC

    avcodec_close(pCodecCtxOrg);

    avformat_close_input(&pFormatCtx);  


视频解码代码

#include <stdio.h>
#include <stdlib.h>
//编码
#include "libavcodec/avcodec.h"
//封装格式处理
#include "libavformat/avformat.h"
//像素处理
#include "libswscale/swscale.h"
int main()
{
    //获取输入输出文件名
    const char *input = "test.mp4";
    const char *output = "test.yuv";
    //1.注册所有组件
    av_register_all();
    //封装格式上下文,统领全局的结构体,保存了视频文件封装格式的相关信息
    AVFormatContext *pFormatCtx = avformat_alloc_context();
    //2.打开输入视频文件
    if (avformat_open_input(&pFormatCtx, input, NULL, NULL) != 0)
    {
        printf("%s","无法打开输入视频文件");
        return;
    }
    //3.获取视频文件信息
    if (avformat_find_stream_info(pFormatCtx,NULL) < 0)
    {
        printf("%s","无法获取视频文件信息");
        return;
    }
    //获取视频流的索引位置
    //遍历所有类型的流(音频流、视频流、字幕流),找到视频流
    int v_stream_idx = -1;
    int i = 0;
    //number of streams
    for (; i < pFormatCtx->nb_streams; i++)
    {
        //流的类型
        if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
        {
            v_stream_idx = i;
            break;
        }
    }
    if (v_stream_idx == -1)
    {
        printf("%s","找不到视频流\n");
        return;
    }
    //只有知道视频的编码方式,才能够根据编码方式去找到解码器
    //获取视频流中的编解码上下文
    AVCodecContext *pCodecCtx = pFormatCtx->streams[v_stream_idx]->codec;
    //4.根据编解码上下文中的编码id查找对应的解码
    AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
    if (pCodec == NULL)
    {
        printf("%s","找不到解码器\n");
        return;
    }
    //5.打开解码器
    if (avcodec_open2(pCodecCtx,pCodec,NULL)<0)
    {
        printf("%s","解码器无法打开\n");
        return;
    }
    //输出视频信息
    printf("视频的文件格式:%s",pFormatCtx->iformat->name);
    printf("视频时长:%d", (pFormatCtx->duration)/1000000);
    printf("视频的宽高:%d,%d",pCodecCtx->width,pCodecCtx->height);
    printf("解码器的名称:%s",pCodec->name);
    //准备读取
    //AVPacket用于存储一帧一帧的压缩数据(H264)
    //缓冲区,开辟空间
    AVPacket *packet = (AVPacket*)av_malloc(sizeof(AVPacket));
    //AVFrame用于存储解码后的像素数据(YUV)
    //内存分配
    AVFrame *pFrame = av_frame_alloc();
    //YUV420
    AVFrame *pFrameYUV = av_frame_alloc();
    //只有指定了AVFrame的像素格式、画面大小才能真正分配内存
    //缓冲区分配内存
    uint8_t *out_buffer = (uint8_t *)av_malloc(avpicture_get_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height));
    //初始化缓冲区
    avpicture_fill((AVPicture *)pFrameYUV, out_buffer, AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height);
    //用于转码(缩放)的参数,转之前的宽高,转之后的宽高,格式等
    struct SwsContext *sws_ctx = sws_getContext(pCodecCtx->width,pCodecCtx->height,pCodecCtx->pix_fmt,
                                                pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P,
                                                SWS_BICUBIC, NULL, NULL, NULL);
    int got_picture, ret;
    FILE *fp_yuv = fopen(output, "wb+");
    int frame_count = 0;
    //6.一帧一帧的读取压缩数据
    while (av_read_frame(pFormatCtx, packet) >= 0)
    {
        //只要视频压缩数据(根据流的索引位置判断)
        if (packet->stream_index == v_stream_idx)
        {
            //7.解码一帧视频压缩数据,得到视频像素数据
            ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
            if (ret < 0)
            {
                printf("%s","解码错误");
                return;
            }
            //为0说明解码完成,非0正在解码
            if (got_picture)
            {
                //AVFrame转为像素格式YUV420,宽高
                //2 6输入、输出数据
                //3 7输入、输出画面一行的数据的大小 AVFrame 转换是一行一行转换的
                //4 输入数据第一列要转码的位置 从0开始
                //5 输入画面的高度
                sws_scale(sws_ctx, pFrame->data, pFrame->linesize, 0, pCodecCtx->height,
                          pFrameYUV->data, pFrameYUV->linesize);
                //输出到YUV文件
                //AVFrame像素帧写入文件
                //data解码后的图像像素数据(音频采样数据)
                //Y 亮度 UV 色度(压缩了) 人对亮度更加敏感
                //U V 个数是Y的1/4
                int y_size = pCodecCtx->width * pCodecCtx->height;
                fwrite(pFrameYUV->data[0], 1, y_size, fp_yuv);
                fwrite(pFrameYUV->data[1], 1, y_size / 4, fp_yuv);
                fwrite(pFrameYUV->data[2], 1, y_size / 4, fp_yuv);
                frame_count++;
                printf("解码第%d帧\n",frame_count);
            }
        }
        //释放资源
        av_free_packet(packet);
    }
    fclose(fp_yuv);
    av_frame_free(&pFrame);
    avcodec_close(pCodecCtx);
    avformat_free_context(pFormatCtx);
}




音视频解码完整代码:


#include <stdio.h>
#ifdef __cplusplus
extern "C"
{
#endif
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#ifdef __cplusplus
};
#endif
int openCodecContext(const AVFormatContext *pFormatCtx, int *pStreamIndex, enum AVMediaType type, AVCodecContext **ppCodecCtx)
{
    int streamIdx = -1;
    // 获取流下标
    for (int i = 0; i < pFormatCtx->nb_streams; i++)
    {
        if (pFormatCtx->streams[i]->codec->codec_type == type)
        {
            streamIdx = i;
            break;
        }
    }
    if (streamIdx == -1)
    {
        printf("find video stream failed!\n");
        exit(-2);
    }
    // 寻找解码器
    AVCodecContext  *pCodecCtx = pFormatCtx->streams[streamIdx]->codec;
    AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
    if (NULL == pCodec)
    {
        printf("avcode find decoder failed!\n");
        exit(-2);
    }
    //打开解码器
    if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0)
    {
        printf("avcode open failed!\n");
        exit(-2);
    }
    *ppCodecCtx        = pCodecCtx;
    *pStreamIndex    = streamIdx;
    return 0;
}
int main(void)
{
    AVFormatContext    *pInFormatCtx    = NULL;
    AVCodecContext  *pVideoCodecCtx    = NULL;
    AVCodecContext  *pAudioCodecCtx    = NULL;
    AVPacket *pPacket    = NULL;
    AVFrame *pFrame        = NULL;
    int ret;
    /* 支持本地文件和网络url */
    const char streamUrl[] = "./test.flv";
    /* 1. 注册 */
    av_register_all();
    pInFormatCtx = avformat_alloc_context();
    /* 2. 打开流 */
    if(avformat_open_input(&pInFormatCtx, streamUrl, NULL, NULL) != 0)
    {
        printf("Couldn't open input stream.\n");
        return -1;
    }
    /* 3. 获取流的信息 */
    if(avformat_find_stream_info(pInFormatCtx, NULL) < 0)
    {
        printf("Couldn't find stream information.\n");
        return -1;
    }
    int videoStreamIdx = -1;
    int audioStreamIdx = -1;
    /* 4. 寻找并打开解码器 */
    openCodecContext(pInFormatCtx, &videoStreamIdx, AVMEDIA_TYPE_VIDEO, &pVideoCodecCtx);
    openCodecContext(pInFormatCtx, &audioStreamIdx, AVMEDIA_TYPE_AUDIO, &pAudioCodecCtx);
    pPacket    = av_packet_alloc();
    pFrame    = av_frame_alloc();
    int cnt = 30;
    while (cnt--)
    {
        /* 5. 读流数据, 未解码的数据存放于pPacket */
        ret = av_read_frame(pInFormatCtx, pPacket);
        if (ret < 0)
        {
            printf("av_read_frame error\n");
            break;
        }
        /* 6. 解码, 解码后的数据存放于pFrame */
        /* 视频解码 */
        if (pPacket->stream_index == videoStreamIdx)
        {
            avcodec_decode_video2(pVideoCodecCtx, pFrame, &ret, pPacket);
            if (ret == 0)
            {
                printf("video decodec error!\n");
                continue;
            }
            printf("* * * * * * video * * * * * * * * *\n");
            printf("___height: [%d]\n", pFrame->height);
            printf("____width: [%d]\n", pFrame->width);
            printf("pict_type: [%d]\n", pFrame->pict_type);
            printf("___format: [%d]\n", pFrame->format);
            printf("* * * * * * * * * * * * * * * * * * *\n\n");
        }
        /* 音频解码 */
        if (pPacket->stream_index == audioStreamIdx)
        {
            avcodec_decode_audio4(pAudioCodecCtx, pFrame, &ret, pPacket);
            if (ret < 0)
            {
                printf("audio decodec error!\n");
                continue;
            }
            printf("* * * * * * audio * * * * * * * * * *\n");
            printf("____nb_samples: [%d]\n", pFrame->nb_samples);
            printf("__samples_rate: [%d]\n", pFrame->sample_rate);
            printf("channel_layout: [%lu]\n", pFrame->channel_layout);
            printf("________format: [%d]\n", pFrame->format);
            printf("* * * * * * * * * * * * * * * * * * *\n\n");
        }
        av_packet_unref(pPacket);
    }
    av_frame_free(&pFrame);
    av_packet_free(&pPacket);
    avcodec_close(pVideoCodecCtx);
    avcodec_close(pAudioCodecCtx);
    avformat_close_input(&pInFormatCtx);
    return 0;
}

好文章,来自【福优学苑@音视频+流媒体】
***【在线视频教程】***