美文网首页Android开发Android技术进阶Android技术知识
Android音视频开发——FFmpeg入门编码流程

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

作者: 谁动了我的代码 | 来源:发表于2022-09-02 21:28 被阅读0次

    简介

    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字数以上+图文解析。所以这里推荐给各位想学习音视频的程序员。

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

    相关文章

      网友评论

        本文标题:Android音视频开发——FFmpeg入门编码流程

        本文链接:https://www.haomeiwen.com/subject/ewuynrtx.html