Android音视频开发——FFmpeg入门编码流程

简介

FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。采用LGPL或GPL许可证。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的音频/视频编解码库libavcodec,为了保证高可移植性和编解码质量,libavcodec里很多code都是从头开发的。

FFmpeg基本概念

1 、容器

容器就是一种文件格式,视频文件本身是一个容器(container),里面包括了视频和音频,也可能有字幕等其他内容。常见的容器格式有以下几种:

MP4、MKV、WebM、AVI

下面的命令查看 FFmpeg 支持的容器。

ffmpeg -formats

2、 编码格式

视频和音频都需要经过编码才能保存成文件,因而就有了不同的编码格式(CODEC),对应着不同的压缩率

常用的视频编码格式:

H.264、H.265

常用的音频编码格式:

MP3、AAC

下面的命令可以查看 FFmpeg 支持的编码格式

ffmpeg -codecs

3 、流

流(Stream)是一种视频数据信息的传输方式,有5种流:音频,视频,字幕,附件,数据

4 、帧

帧(Frame)代表一幅静止的图像,分为I帧,P帧,B帧

5 、帧率

帧率也叫帧频率,帧率是视频文件中每一秒的帧数,肉眼想看到连续移动图像至少需要15帧

6 、码率

比特率,也叫码率、数据率,是一个确定整体视频/音频质量的参数,秒为单位处理的字节数,码率和视频质量成正比,在视频文件中中比特率用bps来表达设置帧率

FFmpeg组件

FFmpeg的组件包括libavcodec、libavutil、libavformat、libavfilter、libavdevice、libswscale和libswresample(这些都是可以应用与应用程序)

  1. libavutil是一个包含简化编程功能的库,包括随机数生成器、数学例程、核心多媒体使用程序等
  2. libavcodec是一个包含解码和编码器的音视频编解码器的库 libavformat是一个包含用于多媒体容器格式的demuxers和muxers的库
  3. libavdevice是一个包含输入和输出设备的库,用于抓取和呈现许多常见的多媒体输入/输出软件框架,包括Video4Linux、Video4Linux2、VFW和ALSA
  4. libavfilter是一个包含媒体过滤器的库
  5. libswscale是一个执行高度优化的音频重采样、rematrixing个实例格式转换操作的库
  6. libpostproc是一个用于后期效果处理的库

FFmpeg命令

三条最主要的命令:

  • ffmpeg:由命令行组成,用于音视频转码
  • ffplay:基于ffmpeg开源代码库libraries做的多媒体播放器
  • ffprobe:基于ffmpeg做的多媒体流分析器,可查看多媒体文件的信息

FFmpeg语法

ffmpeg 的命令行参数非常多,输入 ffmpeg -h 查看支持的参数,具体可以分成五个部分

ffmpeg {1} {2} -i {3} {4} {5}

具体解释如下:

  • 全局参数
  • 输入文件参数
  • 输入文件
  • 输出文件参数
  • 输出文件

为了便于查看,ffmpeg 命令可以写成多行

ffmpeg \
[全局参数] \
[输入文件参数] \
-i [输入文件] \
[输出文件参数] \
[输出文件]

FFmpeg解码流程

解码流程总览

解码流程分解

第一步:注册

使用FFmpeg对应的库,都需要进行注册,注册了这个才能正常使用编码器和解码器;

///第一步
av_register_all();

第二步:打开文件

打开文件,根据文件名信息获取对应的FFmpeg全局上下文

///第二步
AVFormatContext *pFormatCtx;    //文件上下文,描述了一个媒体文件或媒体流的构成和基本信息

pFormatCtx = avformat_alloc_context();  //分配指针

if (avformat_open_input(&pFormatCtx, file_path, NULL, NULL) != 0) { //打开文件,信息存储到文件上下文中,后续对针对文件上下文即可
    printf("无法打开文件");
    return -1;
}

第三步:探测流信息

一定要探测流信息,拿到流编码的编码格式,不探测流信息则器流编码器拿到的编码类型可能为空,后续进行数据转换的时候就无法知晓原始格式,导致错误;

///第三步
//探寻文件中是否存在信息流
if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
    printf("文件中没有发现信息流");
    return -1;
}

//探寻文件中是否存储视频流
int videoStream = -1;
for (i = 0; i < pFormatCtx->nb_streams; i++) {
    if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
        videoStream = i;
    }
}
//如果videoStream为-1 说明没有找到视频流
if (videoStream == -1) {
    printf("文件中未发现视频流");
    return -1;
}

//探寻文件中是否存在音频流
int audioStream = -1
for (i = 0; i < pFormatCtx->nb_streams; i++) {
    if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO) {
        audioStream = i;
    }
}
//如果audioStream 为-1 说明没有找到音频流
if (audioStream == -1) {
    printf("文件中未发现音频流");
    return -1;
}

第四步:查找对应的解码器

依据流的格式查找解码器,软解码还是硬解码是在此处决定的,但是特别注意是否支持硬件,需要自己查找本地的硬件解码器对应的标识,并查询其是否支持。普遍操作是,枚举支持文件后缀解码的所有解码器进行查找,查找到了就是可以硬解了;

注意:解码时查找解码器,编码时查找编码器,两者函数不同,不要弄错了,否则后续能打开但是数据是错的;

///第四步
AVCodecContext *pCodecCtx;      //描述编解码器上下文的数据结构,包含了众多编解码器需要的参数信息
AVCodec *pCodec;                //存储编解码器信息的结构体

//查找解码器
pCodecCtx = pFormatCtx->streams[videoStream]->codec;    //获取视频流中编码器上下文
pCodec = avcodec_find_decoder(pCodecCtx->codec_id);     //获取视频流的编码器信息

if (pCodec == NULL) {
    printf("未发现编码器");
    return -1;
}

第五步:打开解码器

打开获取到的解码器

///第五步
//打开解码器
if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
    printf("无法打开编码器");
    return -1;
}

第六步:申请缩放数据格式转换结构体

基本上解码的数据都是yuv系列格式,但是我们显示的数据是rgb等相关颜色空间的数据,所以此处转换结构体就是进行转换前导转换后的描述,给后续转换函数提供转码依据,是很关键并且非常常用的结构体;

///第六步
static struct SwsContext *img_convert_ctx;  //用于视频图像的转换

img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height,
            pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height,
            AV_PIX_FMT_BGR24, SWS_BICUBIC, NULL, NULL, NULL);

第七步:计算缩放颜色空间转换后缓存大小

///第七步
int numBytes;   //字节数
numBytes = avpicture_get_size(AV_PIX_FMT_BGR24, pCodecCtx->width,pCodecCtx->height);

第八步:申请缓存区,将AVFrama的data映射到单独的outBuffer上

申请一个缓存区outBuffer,fill到我们目标帧数据的data上,比如rgb数据,QAVFrame的data上存的是有指定格式的数据且存储有规则,而fill到outBuffer(自己申请的目标格式一帧缓存区),则是我们需要的数据格式存储顺序;

例如:解码转换后的数据为rgb888,实际直接使用data数据是错误的,但是用outBuffer就是对的,所以此处应该是FFmpeg的fill函数做了一些转换;

///第七步
AVFrame *pFrame, *pFrameRGB;    //存储音视频原始数据(即未被编码的数据)的结构体
pFrame = av_frame_alloc();
pFrameRGB = av_frame_alloc();

uint8_t *out_buffer;            //缓存
out_buffer = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));

avpicture_fill((AVPicture *) pFrameRGB, out_buffer, AV_PIX_FMT_BGR24,
            pCodecCtx->width, pCodecCtx->height);

第九步:循环解码

1、获取一帧packet

int y_size = pCodecCtx->width * pCodecCtx->height;
packet = (AVPacket *) malloc(sizeof(AVPacket)); //分配一个packet
av_new_packet(packet, y_size); //分配packet的数据

int ret, got_picture;
while(1) {
    if (av_read_frame(pFormatCtx, packet) < 0) {    //读取一帧packet数据包
        break; //这里认为视频读取完了
    }

    ......
}

2、解码获取原始数据

int y_size = pCodecCtx->width * pCodecCtx->height;
packet = (AVPacket *) malloc(sizeof(AVPacket)); //分配一个packet
av_new_packet(packet, y_size); //分配packet的数据

int ret, got_picture;
while(1) {
    if (av_read_frame(pFormatCtx, packet) < 0) {    //读取一帧packet数据包
        break; //这里认为视频读取完了
    }

    if (packet->stream_index == videoStream) {
        ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture,packet);    //解码packet包,原始数据存入pFrame中

        if (ret < 0) {
            printf("decode error.");
            return -1;
        }

        ......
    }
}

3、数据转换

int y_size = pCodecCtx->width * pCodecCtx->height;
packet = (AVPacket *) malloc(sizeof(AVPacket)); //分配一个packet
av_new_packet(packet, y_size); //分配packet的数据

int ret, got_picture;
while(1) {
    if (av_read_frame(pFormatCtx, packet) < 0) {    //读取一帧packet数据包
        break; //这里认为视频读取完了
    }

    if (packet->stream_index == videoStream) {
        ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture,packet);    //解码packet包,原始数据存入pFrame中

        if (ret < 0) {  //是否解析成功?
            printf("decode error.");
            return -1;
        }

        if (got_picture) {  //是否get一帧?
            //数据转换  
            sws_scale(img_convert_ctx,  
                    (uint8_t const * const *) pFrame->data,
                     pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data,
                     pFrameRGB->linesize);

            ......
        }
        ......
    }
}

4、自由操作

int y_size = pCodecCtx->width * pCodecCtx->height;
packet = (AVPacket *) malloc(sizeof(AVPacket)); //分配一个packet
av_new_packet(packet, y_size); //分配packet的数据

int ret, got_picture;
while(1) {
    if (av_read_frame(pFormatCtx, packet) < 0) {    //读取一帧packet数据包
        break; //这里认为视频读取完了
    }

    if (packet->stream_index == videoStream) {
        ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture,packet);    //解码packet包,原始数据存入pFrame中

        if (ret < 0) {  //是否解析成功?
            printf("decode error.");
            return -1;
        }

        if (got_picture) {  //是否get一帧?
            //数据转换  
            sws_scale(img_convert_ctx,  
                    (uint8_t const * const *) pFrame->data,
                     pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data,
                     pFrameRGB->linesize);

            //自由操作,SaveFrame是自定义函数
            SaveFrame(pFrameRGB, pCodecCtx->width,pCodecCtx->height,index++); //保存图片
            if (index > 50) return 0; //这里我们就保存50张图片
        }

        //释放QAVPacket
        av_free_packet(packet);
    }
}

5、释放QAVPacket

在进入循环解码前进行了av_new_packet,循环中未av_free_packet,造成内存溢出; 在进入循环解码前进行了av_new_packet,循环中进行av_free_pakcet,那么一次new对应无数次free,在编码器上是不符合前后一一对应规范的。 查看源代码,其实可以发现av_read_frame时,自动进行了av_new_packet(),那么其实对于packet,只需要进行一次av_packet_alloc()即可,解码完后av_free_packet。

//释放QAVPacket
 av_free_packet(packet);

第十步:释放a资源

全部解码完成后,按照申请顺序,进行对应资源的释放。

av_free(out_buffer);
av_free(pFrameRGB);

sws_freeContext(img_convert_ctx);

avcodec_close(pCodecCtx);   //关闭编码/解码器

avformat_close_input(&pFormatCtx);  //关闭文件全局上下文

小结,这以上就是有关音视频开发的FFmpeg的基础入门学习,主要介绍基本知识、组件、命令、语法以及简单的解码流程分析。对于FFmpeg的学习还有很多,大家可以参考《Android音视频开发入门精通版》这个由【网易音视频开发大佬整理】出的PDF文档,我看了里面内容很详细,100w字数以上+图文解析。所以这里推荐给各位想学习音视频的程序员。

音视频学习之路,是需要技术知识慢慢积累的。虽然技术需要很广很深,一步也不能吃成胖子,需要长时间学习加消化;冰冻三尺非一日之寒,加油鸭!

猜你喜欢

转载自blog.csdn.net/m0_71524094/article/details/126670731
今日推荐