Android 音视频之FFmpeg

FFmpeg介绍

FFmpeg是一套可以用来记录、处理数字音频、视频,并将其转换为流的开源框架,采用LPL或GPL许可证,提供了录制、转换以及流化音视频的完整解决方案。它的可移植性或者说跨平台特性非常强大。

  • 默认的编译会生成4个可执行文件和8个静态库
    • 可执行文件包括用于转码、推流、Dump媒体文件的ffmpeg、用于播放媒体文件的ffplay、用于获取媒体文件信息的ffprobe,以及作为简单流媒体服务器的ffserver
    • AVUtil:核心工具库,该模块是最基础的模块之一,下面的许多其他模块都会依赖该库做一些基本的音视频处理操作。
    • AVFormat:文件格式和协议库,该模块是最重要的模块之一,封装了Protocol层和Demuxer、Muxer层,使得协议和格式对于开发者来说是透明的。
    • AVCodec:编解码库,该模块也是最重要的模块之一,封装了Codec层,但是有一些Codec是具备自己的License的,FFmpeg是不会默认添加像libx264、FDK-AAC、lame等库的,但是FFmpeg就像一个平台一样,可以将其他的第三方的Codec以插件的方式添加进来,然后为开发者提供统一的接口。
    • AVFilter:音视频滤镜库,该模块提供了包括音频特效和视频特效的处理,在使用FFmpeg的API进行编解码的过程中,直接使用该模块为音视频数据做特效处理是非常方便同时也非常高效的一种方式。
    • AVDevice:输入输出设备库,比如,需要编译出播放声音或者视频的工具ffplay,就需要确保该模块是打开的,同时也需要libSDL的预先编译,因为该设备模块播放声音与播放视频使用的都是libSDL库。
    • SwrRessample:该模块可用于音频重采样,可以对数字音频进行声道数、数据格式、采样率等多种基本信息的转换。
    • SWScale:该模块是将图像进行格式转换的模块,比如,可以将YUV的数据转换为RGB的数据。
    • PostProc:该模块可用于进行后期处理,当我们使用AVFilter的时候需要打开该模块的开关,因为Filter中会使用到该模块的一些基础函数。
  • ffmpeg术语
    • 容器/文件(Conainer/File):即特定格式的多媒体文件,比如MP4、flv、mov等。
    • 媒体流(Stream):表示时间轴上的一段连续数据,如一段声音数据、一段视频数据或一段字幕数据,可以是压缩的,也可以是非压缩的,压缩的数据需要关联特定的编解码器。
    • 数据帧/数据包(Frame/Packet):通常,一个媒体流是由大量的数据帧组成的,对于压缩数据,帧对应着编解码器的最小处理单元,分属于不同媒体流的数据帧交错存储于容器之中。
    • 编解码器:编解码器是以帧为单位实现压缩数据和原始数据之间的相互转换的。
      AVFormatContext就是对容器或者说媒体文件层次的一个抽象,该文件中(或者说在这个容器里面)包含了多路流(音频流、视频流、字幕流等)对流的抽象就是AVStream;在每一路流中都会描述这路流的编码格式,对编解码格式以及编解码器的抽象就是AVCodecContextAVCodec;对于编码器或者解码器的输入输出部分,也就是压缩数据以及原始数据的抽象就是AVPacketAVFrame
  • ffmeg 使用步骤

    FFmpeg中最重要的几个模块都已经介绍完毕了,下面来具体看一个解码的实例,该实例实现的功能非常单一,就是把一个视频文件解码成为单独的音频PCM文件和视频YUV文件

  1. 首先,要使用FFmpeg就必须要引用它的头文件,以及在链接阶段使用它的静态库文件如果是在iOS下,那么可直接以下面这种方式引用头文件:

    #include "libavformat/avformat.h"
    #include "libswscale/swscale.h"
    #include "libswresample/swresample.h"
    #include "libavutil/pixdesc.h"
    如果是在Android的C++环境下,那么可直接以下面这种方式引用头文件:
    extern "C" {
    	 #include "3rdparty/ffmpeg/include/libavformat/avformat.h"
    	 #include "3rdparty/ffmpeg/include/libswscale/swscale.h" 
    	 #include "3rdparty/ffmpeg/include/libswresample/swresample.h"
    	 #include "3rdparty/ffmpeg/include/libavutil/pixdesc.h"
     }
    

    extern“C”的解释
    作为一种面向对象的语言,C++支持函数的重载,而面向过程的C语言是不支持函数重载的。同一个函数在C++中编译后与其在C中编译后,在符号表中的签名是不同的,假如对于同一个函数:void decode(float position, float duration)在C语言中编译出来的签名是_decoder,而在C++语言中,一般编译器的生成则类似于_decode_float_float。虽然在编译阶段是没有问题的,但是在链接阶段,如果不加extern“C”关键字的话,那么将会链接_decoder_float_float这个方法签名;而如果加了extern“C”关键字的话,那么寻找的方法签名就是_decoder。而FFmpeg就是C语言书写的,编译FFmpeg的时候所产生的方法签名都是C语言类型的签名,所以在C++中引用FFmpeg必须要加extern“C”关键字。

    可以看到,引用头文件的方式是不同的,因为每个平台配置的Header Search Path是不一样的,在iOS的IDE Xcode开发中,可以在工程文件的配置中修改Header Search Path;在Android的底层开发中,可以配置makefile文件中的内置变量LOCAL_C_INCLUDES来指定头文件的搜索路径,当然如果要在跨平台(Android平台和iOS平台)的模块(以C++语言编写)中引用FFmpeg的头文件,则需要编写一个platform_4_ffmpeg.h,并在其中根据各个平台预定义的宏去编译不同的引用方式,代码如下:

    #ifdef __ANDROID__
     extern "C" {
    	  #include "3rdparty/ffmpeg/include/libavformat/avformat.h" 
    	  #include "3rdparty/ffmpeg/include/libswscale/swscale.h" 
    	  #include "3rdparty/ffmpeg/include/libswresample/swresample.h" 
    	  #include "3rdparty/ffmpeg/include/libavutil/pixdesc.h" 
      }#elif defined(__APPLE__) // iOS或OS X 
      extern "C" { 
    	  #include "libavformat/avformat.h"
    	  #include "libswscale/swscale.h" 
    	  #include "libswresample/swresample.h" 
    	  #include "libavutil/pixdesc.h" 
      }#endif
    
  2. 注册协议、格式与编解码器
    使用FFmpeg的API,首先要调用FFmpeg的注册协议、格式与编解码器的方法,确保所有的格式与编解码器都被注册到了FFmpeg框架中,当然如果需要用到网络的操作,那么也应该将网络协议部分注册到FFmpeg框架,以便于后续再去查找对应的格式。代码如下:

    avformat_network_init();
    av_register_all();
    
  3. 打开媒体文件源,并设置超时回调
    注册了格式以及编解码器之后,接下来就应该打开对应的媒体文件了,当然该文件既可能是本地磁盘的文件,也可能是网络媒体资源的一个链接,如果是网络链接,则会涉及不同的协议,比如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);
    
  4. 寻找各个流,并且打开对应的解码器
    上一步中已打开了媒体文件,相当于打开了一根电线,这根电线里面其实还有一条红色的线和一条蓝色的线,这就和媒体文件中的流非常类似了,红色的线代表音频流,蓝色的线代表视频流。所以这一步我们就要寻找出各个流,然后找到流中对应的解码器,并且打开它。
    寻找音视频流:

    for(int i = 0; i < formatCtx->nb_streams; i++) {
    	AVStream* stream = formatCtx->streams[i];
    	if(AVMEDIA_TYPE_VIDEO == stream->codec->codec_type) {
    		// 视频流
    		videoStreamIndex = i;
    	} else if(AVMEDIA_TYPE_AUDIO == stream->codec->codec_type ){
    		// 音频流
    		audioStreamIndex = i;	
    	}
    }
    打开音频流解码器:
    AVCodecContext * audioCodecCtx = audioStream->codec;
    AVCodec *codec = avcodec_find_decoder(audioCodecCtx ->codec_id);
    if(!codec){
    	// 找不到对应的音频解码器
    }
    int openCodecErrCode = 0;
    if ((openCodecErrCode = avcodec_open2(codecCtx, codec, NULL)) < 0){
    	// 打开音频解码器失败
    }
    打开视频流解码器:
    AVCodecContext *videoCodecCtx = videoStream->codec;
    AVCodec *codec = avcodec_find_decoder(videoCodecCtx->codec_id);
    if(!codec) {
    	// 找不到对应的视频解码器
    }
    int openCodecErrCode = 0;
    if ((openCodecErrCode = avcodec_open2(codecCtx, codec, NULL)) < 0) {
    	// 打开视频解码器失败
    }
    
  5. 初始化解码后数据的结构体
    知道了音视频解码器的信息之后,下面需要分配出解码之后的数据所存放的内存空间,以及进行格式转换需要用到的对象。
    构建音频的格式转换对象以及音频解码后数据存放的对象:

    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();
    }
    构建视频的格式转换对象以及视频解码后数据存放的对象:
    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();
    }
    
  6. 读取流内容并且解码
    打开了解码器之后,就可以读取一部分流中的数据(压缩数据),然后将压缩数据作为解码器的输入,解码器将其解码为原始数据(裸数据),之后就可以将原始数据写入文件了:

    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数据。下面将其处理成我们所需要的格式并且进行写文件。
    音频裸数据的处理:

    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;
    }
    
  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;
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值