音视频开发 - FFmpeg

FFmpeg是一套可以用来记录、处理数字音频、视频,并将其转换为流的开源框架,采用LPL或GPL许可证,提供了录制、转换以及流化音视频的完整解决方案

1、FFmpeg编译选项详解

FFmpeg和大部分的GNU软件的编译方式类似,都是通过configure脚本来实现编译

前定制的,这种方式允许用户在编译前对软件进行裁剪,同时通过对最终运行到的

系统以及目标平台的配置来决定对某些模块设定合适的配置。

标准选项:GNU软件配置项目,例如安装路径、--prefix=…等。

编译、链接选项:默认配置是生成静态库而不是生成动态库,例如--disable-static、--enable-shared等。

可执行程序控制选项:决定是否生成FFmpeg、ffplay、ffprobe和ffserver等。

模块控制选项:裁剪编译模块,包括整个库的裁剪,例如--disable-avdevice;
一组模块的筛选,例如--disable-decoders;单个模块的裁剪,例如--disable-demuxer。

能力展示选项[图片上传中...(屏幕快照 2019-09-05 上午9.21.53.png-7df104-1567646545418-0)]
:列出当前源代码支持的各种能力集,例如--list-decoders、--list-encoders。

其他:允许开发者深度定制,如交叉编译环境配置、自定义编译

音视频开发 - FFmpeg_第1张图片
3-1

默认的编译会生成4个可执行文件和8个静态库。

可执行文件:

1、ffmpeg:转码、推流、媒体文件
2、ffplay:播放媒体文件,需要libSDL的预先编译
3、ffprobe: 获取文件详细信息
4、ffserver:作为简单流媒体服务器

8个静态库:

AVUtil:基础的模块之一,基本的音视频处理操作。

AVFormat:文件格式(avi、MP4、flv、mov等)和协议库,封装了Protocol层和Demuxer、Muxer层。

AVCodec:编解码库,例如MPEG-4。可以将其他的第三方的Codec以插件的方式添加进来,libx264、FDK-AAC、lame等库。

AVFilter:音视频滤镜库,包括音频特效和视频特效的处理。

AVDevice:输入输出设备库,比如,需要编译出播放声音或者视频的工具ffplay,需要该类。

SwrRessample:该模块可用于音频重采样,可以对数字音频进行声道数、数据格式、采样率等多种基本信息的转换。

SWScale:该模块是将图像进行格式转换的模块,比如,可以将YUV的数据转换为RGB的数据。

PostProc:该模块可用于进行后期处理,当我们使用AVFilter的时候需要打开该模块的开关,因为Filter中会使用到该模块的一些基础函数。

2、FFmpeg安装(安装命令行工具)

  • ruby -e "$(curl -fsSkL raw.github.com/mxcl/homebrew/go)" 安装homebrew
  • brew install ffmpeg 安装ffmpeg

常用的命令行工具:

ffmpeg是进行媒体文件转码的命令行工具,ffprobe是用于查看媒体文件头信息的工具,ffplay则是用于播放媒体文件的工具。

1、ffprobe 查看流媒体格式

  • 首先用ffprobe查看一个音频的文件:
    ffprobe ~/Desktop/output.mp3
    • Duration: 00:00:09.44, bitrate: 77 kb/s
      表明该音频文件的时长是09秒44毫秒,开始播放时间是0,整个媒体文件的比特率是77Kbit/s

    • Stream #0:0: Audio: aac (LC), 44100 Hz, mono, fltp, 77 kb/s
      第一个流是音频流,编码格式是aac格式,采样率是44.1kHz,声道是mono,采样表示格式是fltp),这路流的比特率是77Kbit/s。

    • ...有的视频中是有多路音频文件的,比如国语或者是粤语、英语等

  • 然后再使用ffprobe查看一个视频的文件:
    ffprobe ~/Desktop/aroey.mp4

    • Metadata:
      major_brand : isom
      minor_version : 512
      compatible_brands: isomiso2avc1mp41
      encoder : Lavf58.20.100
      这行信息表明了该文件的Metadata信息,比如encoder是Lavf55.12.100,其中Lavf代表的是FFmpeg输出的文件,后面的编号代表了FFmpeg的版本代号,接下来的一行信息如下:

    • Duration: 00:00:09.94, start: 0.000000, bitrate: 5166 kb/s
      Duration是0分09秒94毫秒,开始播放的时间是从0ms开始播放的,整个文件的比特率是5166Kbit/s

    • Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1080x1920, 5100 kb/s, 29.97 fps, 29.97 tbr, 30k tbn, 59.94 tbc (default)
      这行信息表示第一个stream是视频流,编码方式是H264的格式(封装格式是AVC1),每一帧的数据表示是YUV420P的格式,分辨率是1080x1920,这路流的比特率是5100Kbit/s,帧率是每秒钟29.97帧(fps是29.97)

  • 其他音视频文件信息

2、ffplay

ffplay是以FFmpeg框架为基础,外加渲染音视频的库libSDL来构建的媒体文件播放器。

  • ffplay 32037.mp3
    播放音频

  • ffplay 32037.mp4
    播放视频,业内的ijkplayer播放器就是基于ffplay开发的

  • 其他播放相关的信息

3、ffmpeg

ffmpeg强大的媒体文件转换工具。它可以转换任何格式的媒体文件,并且还可以用自己的AudioFilter以及VideoFilter进行处理和编辑。

  • 通用参数
    -f fmt:指定格式(音频或者视频格式)。
    -i filename:指定输入文件名,在Linux下当然也能指定:0.0(屏幕录制)或摄像头。
    -y:覆盖已有文件。
    -t duration:指定时长。
    -fs limit_size:设置文件大小的上限。
    -ss time_off:从指定的时间(单位为秒)开始,也支持[-]hh:mm:ss[.xxx]的格式。
    -re:代表按照帧率发送,尤其在作为推流工具的时候一定要加入该参数,否则ffmpeg会按照最高速率向流媒体服务器不停地发送数据。
    -map:指定输出文件的流映射关系。例如:“-map 1:0-map 1:1”要求将第二个输入文件的第一个流和第二个流写入输出文件。如果没有-map选项,则ffmpeg采用默认的映射关系。

  • 视频参数
    -b:指定比特率(bit/s),ffmpeg是自动使用VBR的,若指定了该参数则使用平均比特率。
    -bitexact:使用标准比特率。
    -vb:指定视频比特率(bits/s)。
    -r rate:帧速率(fps)。
    -s size:指定分辨率(320×240)。
    -aspect aspect:设置视频长宽比(4:3,16:9或1.3333,1.7777)。
    -croptop size:设置顶部切除尺寸(in pixels)。
    -cropbottom size:设置底部切除尺寸(in pixels)。
    -cropleft size:设置左切除尺寸(in pixels)。
    -cropright size:设置右切除尺寸(in pixels)。
    -padtop size:设置顶部补齐尺寸(in pixels)。
    -padbottom size:底补齐(in pixels)。
    -padleft size:左补齐(in pixels)。
    -padright size:右补齐(in pixels)。
    -padcolor color:补齐带颜色(000000-FFFFFF)。
    -vn:取消视频的输出。
    -vcodec codec:强制使用codec编解码方式('copy'代表不进行重新编码)。

  • 音频参数
    -ab:设置比特率(单位为bit/s,老版的单位可能是Kbit/s),对于MP3格式,若要听到较高品质的声音则建议设置为160Kbit/s(单声道则设置为80Kbit/s)以上。
    -aq quality:设置音频质量(指定编码)。
    -ar rate:设置音频采样率(单位为Hz)。
    -ac channels:设置声道数,1就是单声道,2就是立体声。
    -an:取消音频轨。
    -acodec codec:指定音频编码('copy'代表不做音频转码,直接复制)。
    -vol volume:设置录制音量大小(默认为256)<百分比>。

简单使用
转化格式: ffmpeg -i 目标地址.webm 最终.mp4
分离视频: ffmpeg -i 目标地址.mp4 -vcodec copy -an 最终.mp4
分离音频: ffmpeg -i 目标地址.mp4 -acodec copy -vn 最终.aac

3、FFmpeg交叉编译生成IOS/Android端的代码API

  • 下载编译FFmpeg所需要的脚本文件gas-preprocessor.pl
    • 下载地址: https://github.com/libav/gas-preprocessor

    • 复制gas-preprocessor.pl到/usr/sbin下,(这个应该是复制到/usr/local/bin)

    • 修改文件权限:chmod 777 /usr/local/bin/gas-preprocessor.pl

  • 下载脚本FFmpeg脚本
    • 地址: https://github.com/kewlbear/FFmpeg-iOS-build-script
    • 解压,找到文件 build-ffmpeg.sh
    • 执行服本文件:./build-ffmpeg.sh

介绍几个术语:

容器/文件(Conainer/File):即特定格式的多媒体文件,比如MP4、flv、mov等。

媒体流(Stream):表示时间轴上的一段连续数据,如一段声音数据、一段视频数据或一段字幕数据,可以是压缩的,也可以是非压缩的,压缩的数据需要关联特定的编解码器。

数据帧/数据包(Frame/Packet):通常,一个媒体流是由大量的数据帧组成的,对于压缩数据,帧对应着编解码器的最小处理单元,分属于不同媒体流的数据帧交错存储于容器之中。

编解码器:编解码器是以帧为单位实现压缩数据和原始数据之间的相互转换的。

其中AVFormatContext就是对容器或者说媒体文件层次的一个抽象,该文件中(或者说在这个容器里面)包含了多路流(音频流、视频流、字幕流等),对流的抽象就是AVStream;在每一路流中都会描述这路流的编码格式,对编解码格式以及编解码器的抽象就是AVCodecContext与AVCodec;对于编码器或者解码器的输入输出部分,也就是压缩数据以及原始数据的抽象就是AVPacket与AVFrame;除了编解码之外,对于音视频的处理肯定是针对于原始数据的处理,也就是针对于AVFrame的处理,使用的就是AVFilter。

案例1:将视频文件解码成单独的PCM文件和YUV文件
//1.注册协议、格式与编解码器
    //注册网络协议部分
    avformat_network_init();
    
    //注册所有的编解码器、协议、格式
    av_register_all();
    
    
    
    //2.打开媒体文件源,并设置超时回调(本地磁盘文件也可能是网络媒体资源链接,会涉及RTMP/HTTP等协议的视频源,设置超时时间)
    AVFormatContext *formatCtx = avformat_alloc_context();
    AVIOInterruptCB int_cb = {interrupt_callback,(__bridge void *)(self)};
    formatCtx->interrupt_callback = int_cb;
    avformat_open_input(formatCtx, path, NULL, NULL);
    avformat_find_stream_info(formatCtx, NULL);
    
    
    //3.寻找各个流,并且打开对应的解码器
    for (int i = 0; inb_streams; i++) {
        AVStream *stream = formatCtx->streams[i];
        if (AVMEDIA_TYPE_VIDEO == stream->codec->codec_type) {
            //视频流
            videoStreamIndex = i;
            videoCodecCtx = stream->codec;
        }else if(AVMEDIA_TYPE_AUDIO == stream->codec->codec_type){
            //音频流
            audioStreamIndex = i;
            audioCodecCtx = stream->codec;
        }
    }
    
    //打开音频解码器
    AVCodec *codec = avcodec_find_decoder(audioCodecCtx ->codec_id);
    if(!codec){
        // 找不到对应的音频解码器
        
    }
    int openCodecErrCode = 0;
    if ((openCodecErrCode = avcodec_open2(codecCtx, codec, NULL)) < 0){
        // 打开音频解码器失败
        
    }
    
    //打开视频流解码器
    AVCodec *codec1 = avcodec_find_decoder(videoCodecCtx->codec_id);
    if(!codec) {
        // 找不到对应的视频解码器
        
    }
    int openCodecErrCode1 = 0;
    if ((openCodecErrCode1 = avcodec_open2(codecCtx, codec1, NULL)) < 0) {
        // 打开视频解码器失败
    }
    
    
    //4.初始化解码后数据的结构体
    //4.1需要分配出解码之后的数据所存放的内存空间
    SwrContext *swrContext = NULL;
    if(audioCodecCtx->sample_fmt != AV_SAMPLE_FMT_S16) {
        // 如果不是我们需要的数据格式
        swrContext = swr_alloc_set_opts(NULL,outputChannel, AV_SAMPLE_FMT_S16, outSampleRate,in_ch_layout, in_sample_fmt, in_sample_rate, 0, NULL);
        if(!swrContext || swr_init(swrContext)) {
            if(swrContext) {
                swr_free(&swrContext);
                
            }
            
        }
        audioFrame = avcodec_alloc_frame();
    }
    //4.2以及进行格式转换所需要用到的对象
    AVPicture picture;
    bool pictureValid = avpicture_alloc(&picture,PIX_FMT_YUV420P,videoCodecCtx->width,videoCodecCtx->height) == 0;
    if (!pictureValid){
        // 分配失败
        return false;
        
    }
    swsContext = sws_getCachedContext(swsContext,
                                      videoCodecCtx->width,
                                      videoCodecCtx->height,
                                      videoCodecCtx->pix_fmt,
                                      videoCodecCtx->width,
                                      videoCodecCtx->height,
                                      PIX_FMT_YUV420P,
                                      SWS_FAST_BILINEAR,
                                      NULL, NULL, NULL);
    videoFrame = avcodec_alloc_frame();
    
    
    //5.读取流内容并且解码
    //解码器之后,就可以读取一部分流中的数据(压缩数据),然后将压缩数据作为解码器的输入,解码器将其解码为原始数据(裸数据),之后就可以将原始数据写入文件了
    AVPacket packet;
    int gotFrame = 0;
    while(true) {
        if(av_read_frame(formatContext, &packet)) {
            // End Of File
            break;
            
        }
        int packetStreamIndex = packet.stream_index;
        if(packetStreamIndex == videoStreamIndex) {
            int len = avcodec_decode_video2(videoCodecCtx, videoFrame,&gotFrame, &packet);
            if(len < 0) {
                break;
                
            }
            if(gotFrame) {
                self->handleVideoFrame();
                
            }
            
        } else if(packetStreamIndex == audioStreamIndex) {
            int len = avcodec_decode_audio4(audioCodecCtx, audioFrame,&gotFrame, &packet);
            if(len < 0) {
                break;
            }
            if(gotFrame) {
                self->handleVideoFrame();
                
            }
            
        }
    }
    
    
    //7、处理解码后的裸数据
    //裸数据,音频就是PCM数据,视频就是YUV数据。下面将其处理成我们所需要的格式并且进行写文件
    //7.1 音频裸数据处理
    //将音频裸数据直接写入文件,比如写入到文件audio.pcm中
    void* audioData;
    int numFrames;
    if(swrContext) {
        int bufSize = av_samples_get_buffer_size(NULL, channels,(int)(audioFrame->nb_samples * channels),AV_SAMPLE_FMT_S16, 1);
        if (!_swrBuffer || _swrBufferSize < bufSize) {
            swrBufferSize = bufSize;
            swrBuffer = realloc(_swrBuffer, _swrBufferSize);
            
        }
        Byte *outbuf[2] = { _swrBuffer, 0 };
        numFrames = swr_convert(_swrContext, outbuf,(int)(audioFrame->nb_samples * channels),(const uint8_t **)_audioFrame->data,audioFrame->nb_samples);
        audioData = swrBuffer;
        
    } else {
        audioData = audioFrame->data[0];
        numFrames = audioFrame->nb_samples;
        
    }
    
    //7.2视频的裸数据的处理
    //接收到YUV数据,写入文件video.yuv中
    uint8_t * luma;
    uint8_t *chromaB;
    uint8_t *chromaR;
    if (videoCodecCtx->pix_fmt == AV_PIX_FMT_YUV420P || videoCodecCtx->pix_fmt == av_pix+AV_PIX_FMT_YUVJ420P) {
        luma = copyFrameData(videoFrame->data[0],
                             videoFrame->linesize[0],
                             videoCodecCtx->width,
                             videoCodecCtx->height);
        
        chromaB = copyFrameData(videoFrame->data[1],
                                videoFrame->linesize[1],
                                videoCodecCtx->width / 2,
                                videoCodecCtx->height / 2);
        chromaR = copyFrameData(videoFrame->data[2],
                                videoFrame->linesize[2],
                                videoCodecCtx->width / 2,
                                videoCodecCtx->height / 2);
        
    }else{
        sws_scale(_swsContext,
                  (const uint8_t **)videoFrame->data,
                  videoFrame->linesize,
                  0,
                  videoCodecCtx->height,
                  picture.data,
                  picture.linesize);
        luma = copyFrameData(picture.data[0],
                             picture.linesize[0],
                             videoCodecCtx->width,
                             videoCodecCtx->height);
        chromaB = copyFrameData(picture.data[1],
                                picture.linesize[1],
                                videoCodecCtx->width / 2,
                                videoCodecCtx->height / 2);
        chromaR = copyFrameData(picture.data[2],
                                picture.linesize[2],
                                videoCodecCtx->width / 2,
                                videoCodecCtx->height / 2);
        
    }
    
    //8、关闭所有的资源
    //退出的时候,要将用到的FFmpeg框架中的资源,包括FFmpeg框架对外的连接资源等全都释放掉
    //清空所有的音频资源
    if (swrBuffer) {
        free(swrBuffer);
        swrBuffer = NULL;
        swrBufferSize = 0;
    }
    if (swrContext) {
        swr_free(&swrContext);
        swrContext = NULL;
    }
    if (audioFrame) {
        av_free(audioFrame);
        audioFrame = NULL;
    }
    if (audioCodecCtx) {
        avcodec_close(audioCodecCtx);
        audioCodecCtx = NULL;
    }
    //清空所有的视频资源
    if (swsContext) {
        sws_freeContext(swsContext);
        swsContext = NULL;
    }
    if (pictureValid) {
        avpicture_free(&picture);
        pictureValid = false;
    }
    if (videoFrame) {
        av_free(videoFrame);
        videoFrame = NULL;
    }
    if (videoCodecCtx) {
        avcodec_close(videoCodecCtx);
        videoCodecCtx = NULL;
    }
    
    //关闭连接资源
    if (formatCtx) {
        avformat_close_input(&formatCtx);
        formatCtx = NULL;
    }
  • 注册协议、格式与编解码器
  • 打开媒体文件源,并设置超时回调(本地磁盘文件也可能是网络媒体资源链接,会涉及RTMP/HTTP等协议的视频源,设置超时时间)
  • 寻找各个流(音频视频流),并且打开对应的解码器(音频视频解码器)
  • 初始化解码后数据的结构体
    • 需要分配出解码之后的数据所存放的内存空间
    • 进行格式转换所需要用到的对象
  • 读取流内容并且解码。解码器之后,就可以读取一部分流中的数据(压缩数据),然后将压缩数据作为解码器的输入,解码器将其解码为原始数据(裸数据),之后就可以将原始数据写入文件了
  • 处理解码后的裸数据。裸数据,音频就是PCM数据,视频就是YUV数据。下面将其处理成我们所需要的格式并且进行写文件
    • 音频裸数据处理,将音频裸数据直接写入文件,比如写入到文件audio.pcm中
    • 视频的裸数据的处理,接收到YUV数据,写入文件video.yuv中
  • 关闭所有的资源
    • 退出的时候,要将用到的FFmpeg框架中的资源,包括FFmpeg框架对外的连接资源等全都释放掉

FFmpeg主要的文件libavformat、libavcodec

libavformat和libavcodec

音视频开发 - FFmpeg_第2张图片
libavformate主要架构

AVFormatContext是API层直接接触到的结构体,它会进行格式的封装与解封装,它的数据部分

由底层提供,底层使用了AVIOContext,这个AVIOContext实际上就是为普通的I/O增加了一层

Buffer缓冲区,再往底层就是URLContext,也就是到达了协议层,协议层的具体实现有很多,包

括rtmp、http、hls、file等,这就是libavformat的内部封装了。

音视频开发 - FFmpeg_第3张图片
libavcodec主要架构
 这一层我们能接触到的最顶层的结构体就是AVCodecContext,该结构体包含的就是与实际的

编解码有关的部分。首先,AVCodecContext是包含在一个AVStream里面的,即描述了这路流的

编码格式是什么,其中存放了具体的编码格式信息,根据Codec的信息可以打开编码器或者解码

器,然后利用该编码器或者解码器进行AVPacket与AVFrame之间的转换(实际上就是解码或者

编码的过程),这是FFmpeg中最重要的一部分。

FFmpegAPI

1、通用API

  • av_register_all分析
    内部实现会先调用avcodec_register_all来注册所有config.h里面开放的编解码器,然后会注册所有的Muxer和Demuxer(也就是封装格式),最后注册所有的Protocol(即协议层的东西)。这样一来,在configure过程中开启(enable)或者关闭(disable)的选项就就作用到了运行时,该函数的源码分析涉及的源码文件包括:url.c、allformats.c、mux.c、format.c等文件

  • av_find_codec分析
    寻找解码器,一部分是寻找编码器

  • avcodec_open2分析
    打开编解码器(Codec)的函数

  • avcodec_close分析
    close就是一个逆过程,找到对应的实现文件中的close函数指针所指向的函数,然后该函数会调用对应第三方库的API来关闭掉对应的编码库

2、FFmpeg解码API

  • avformat_open_input分析
    avformat_open_input会根据所提供的文件路径判断文件的格式,其实就是通过这一步来决定使用的到底是哪一个Demuxer

  • avformat_find_stream_info分析
    直播场景下的拉流客户端中“秒开首屏”,就是与该函数分析的代码实现息息相关的

  • avcodec_decode分析
    一部分是解码视频,一部分是解码音频

  • avformat_close_input分析
    首先会调用对应的Demuxer中的生命周期read_close方法,然后释放掉AVFormatContext,最后关闭文件或者远程网络连接。

3、FFmpeg编码API

  • avformat_alloc_output_context2分析
    调用方法avformat_alloc_context来分配一个AVFormatContext结构体,当然最关键的还是根据上一步注册的Muxer和Demuxer部分(也就是封装格式部分)去找到对应的格式

你可能感兴趣的:(音视频开发 - FFmpeg)