术语
容器/文件(Conainer/File):即特定格式的多媒体文件,比如 MP4、flv、mov等。
媒体流(Stream):表示时间轴上的一段连续数据,如一段声音数据、一段视频数据或一段字幕数据,可以是压缩的,也可以是非压缩的,压缩的数据需要关联特定的编解码器。
数据帧/数据包(Frame/Packet):通常,一个媒体流是由大量的数据帧组成的,对于压缩数据,帧对应着编解码器的最小处理单元,分属于不同媒体流的数据帧交错存储于容器之中。
编解码器:编解码器是以帧(针对视频,音频没有帧的概念,可以理解为一段音频数据)为单位实现压缩数据和原始数据之间的相互转换的。
解码过程
1.导入头文件。
#import
#import
#import
#import
#include
2.注册协议、格式、编解码器。
调用FFmpeg的注册协议、格式与编解码器的方法,确保所有的格式与编解码器都被注册到了FFmpeg框架中。
av_register_all(); //新库已经废弃了这个函数,不再需要注册
3.创建上下文,打开媒体文件源。
注册了格式以及编解码器之后,接下来就应该打开对应的媒体文件了,当然该文件既可能是本地磁盘的文件,也可能是网络媒体资源的一个链接,如果是网络链接,则会涉及不同的协议,比如RTMP、HTTP等协议的视频源。
AVFormatContext *formatCtx = avformat_alloc_context();
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 = avcodec_alloc_context3(NULL);
avcodec_parameters_to_context(audioCodecCtx, audioStream->codecpar);
AVCodec *codec = avcodec_find_decoder(audioCodecCtx ->codec_id);
if(!codec) {
// 找不到对应的音频解码器
}
int openCodecErrCode = 0;
if ((openCodecErrCode = avcodec_open2(codecCtx, codec, NULL)) < 0) {
// 打开音频解码器失败
}
打开视频流解码器:
AVCodecContext *videoCodecCtx = avcodec_alloc_context3(NULL);
avcodec_parameters_to_context(videoCodecCtx, videoStream->codecpar);
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 = av_frame_alloc();
}
构建视频的格式转换对象以及视频解码后数据存放的对象:
AVFrame *videoFrame = avcodec_alloc_frame();
//创建缓冲区
av_image_get_buffer_size(AV_PIX_FMT_YUV420P,
videoCodecCtx->width,
videoCodecCtx->height,
1);
//开辟一块内存空间
uint8_t *out_buffer = (uint8_t *)av_malloc(buffer_size);
//缓冲区数据填充
av_image_fill_arrays(avframe_yuv420p->data,
avframe_yuv420p->linesize,
out_buffer,
AV_PIX_FMT_YUV420P,
videoCodecCtx->width,
videoCodecCtx->height,
1);
//初始化视频数据存储对象
swsContext = sws_getContext(swsContext, videoCodecCtx->width,videoCodecCtx->height, videoCodecCtx->pix_fmt, videoCodecCtx->width, videoCodecCtx->height, PIX_FMT_YUV420P, SWS_FAST_BILINEAR,NULL, NULL, NULL);
6.读取流内容并且解码。
读取一部分流中的数据(压缩数据), 然后将压缩数据作为解码器的输入,解码器将其解码为原始数据(裸数 据),之后就可以将原始数据写入文件了:
AVPacket packet;
while(true) {
if(av_read_frame(formatContext, &packet)) { // End Of File
break;
}
int packetStreamIndex = packet.stream_index;
if(packetStreamIndex == videoStreamIndex) {
int send_result = avcodec_send_packet(videoCodecCtx, packet);
avcodec_receive_frame(videoCodecCtx, videoFrame);
if(send_result == 0) {
//处理视频数据,见【7.】
}
if(gotFrame) {
self->handleVideoFrame();
}
} else if(packetStreamIndex == audioStreamIndex) {
int send = avcodec_send_packet(audioCodecCtx, packet);
avcodec_receive_frame(audioCodecCtx, audioFrame);
if(send == 0) {
//处理音频数据,见【7.】
}
}
}
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;
}
接收到音频裸数据之后,就可以直接写文件了,比如写到文件 audio.pcm中。
视频裸数据的处理:
uint8_t* luma;
uint8_t* chromaB;
uint8_t* chromaR;
if(videoCodecCtx->pix_fmt == AV_PIX_FMT_YUV420P ||
videoCodecCtx->pix_fmt == 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);
}
接收到YUV数据之后也可以直接写入文件了,比如写到文件 video.yuv中。
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;
}