一、概述
熟悉了FFMPEG的命令行工具ffmpeg.exe之后,就开始了对这个库的函数的熟悉过程,按照零基础学习FFMPEG的学习轨迹,就应当实现一个“最简单的视频播放器”来熟悉FFMPEG的工作流程和一些常用的函数。实现播放器的文章请参考最简单的视频播放器,这里仅仅记录一下自己的心得。
二、播放器的解码流程
播放器的解码流程非常重要,有利于我们去读懂代码。流程图我就不自己画了,直接取自作者的文章里的图:
对这个流程图做一些解释:
1.av_register_all
注册所有的文件格式和编解码的库。具体的来说它初始化libavformat库,注册muxer,demuxer和一些协议。libavformat库提供了一个复用/解复用音视频、字幕的框架。它包括多媒体封装格式的多个复用器和解复用器,同样支持访问多媒体源的输入输出协议。
2.avformat_open_input
打开一个输入流并读取该流的头部信息。注意,这个时候解码器还没有打开;打开的流在后面解码工作完成后需要关闭。
3.av_find_stream_info
avformat_open_input只是检测了文件的头部,所以我们还需要av_find_stream_info来读取流的信息。这一步之后将为FormatContext->streams填充上正确的信息。此外在新版的FFMPEG中,该函数已经声明为要废弃的,使用avformat_find_stream_info来代替它。
4.avcodec_find_decoder
根据输入流的信息中的解码器ID,找到合适的解码器来进行解码工作。
5.avcodec_open
打开解码器。这个时候会对解码环境(AVCodecContext)做一些初始化的工作。
6.av_read_frame
读取下一帧数据。注意该数据帧还未解码。如果读取不到,说明已经读取完毕。当读取到后,还需要判断这一帧数据是否是视频类型的数据,如果是才进行解码。
7.avcodec_decode_video2
解码数据帧。
三、代码及注释
注意:这里仅仅对解码流程做了注释,而对于SDL部分并没有做注释。
#include <stdio.h>
//包含库
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "SDL/SDL.h"
};
int main(int argc, char* argv[])
{
//FFmpeg相关变量
AVFormatContext *pFormatCtx;//AVFormatContext主要存储视音频封装格式中包含的信息
int i;
int videoindex;//视频流所在序号
AVCodecContext *pCodecCtx;//AVCodecContext,存储该视频/音频流使用解码方式的相关数据
AVCodec *pCodec;//解码器
AVFrame *pFrame,*pFrameYUV;//解码后数据
AVPacket *packet;//解码前数据
struct SwsContext *img_convert_ctx;
//SDL
int screen_w,screen_h;//屏幕宽、高
SDL_Surface *screen;
SDL_VideoInfo *vi;
SDL_Overlay *bmp;
SDL_Rect rect;
FILE *fp_yuv;
int ret, got_picture;
char* filepath = "1.rmvb";
av_register_all();//初始化libformat库和一些别的工作
avformat_network_init();//初始化网络组件
pFormatCtx = avformat_alloc_context();
//打开视频文件然后读取头部信息到pFormatCtx
if(avformat_open_input(&pFormatCtx,filepath,NULL,NULL)!=0){
printf("Couldn't open input stream.\n");
return -1;
}
//获取流信息
if(avformat_find_stream_info(pFormatCtx,NULL)<0){
printf("Couldn't find stream information.\n");
return -1;
}
videoindex=-1;//视频流所处的流序号,因为媒体文件还可能包含音频流
//找到视频流的序号
for(i=0; i<pFormatCtx->nb_streams; i++)
if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO){
videoindex=i;
break;
}
if(videoindex==-1){
printf("Didn't find a video stream.\n");
return -1;
}
pCodecCtx=pFormatCtx->streams[videoindex]->codec;//获取解码环境
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);//获取解码器
if(pCodec==NULL){
printf("Codec not found.\n");
return -1;
}
//为使用解码器做些初始化工作
if(avcodec_open2(pCodecCtx, pCodec,NULL)<0){
printf("Could not open codec.\n");
return -1;
}
pFrame=av_frame_alloc();
pFrameYUV=av_frame_alloc();
//SDL Begin----------------------------
if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
printf( "Could not initialize SDL - %s\n", SDL_GetError());
return -1;
}
screen_w = pCodecCtx->width;
screen_h = pCodecCtx->height;
screen = SDL_SetVideoMode(screen_w, screen_h, 0,0);
if(!screen) {
printf("SDL: could not set video mode - exiting:%s\n",SDL_GetError());
return -1;
}
bmp = SDL_CreateYUVOverlay(pCodecCtx->width, pCodecCtx->height,SDL_YV12_OVERLAY, screen);
rect.x = 0;
rect.y = 0;
rect.w = screen_w;
rect.h = screen_h;
//SDL End------------------------
packet=(AVPacket *)av_malloc(sizeof(AVPacket));
img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);
//------------------------------
while(av_read_frame(pFormatCtx, packet)>=0){//读取下一帧数据
if(packet->stream_index==videoindex){//必须是视频流帧
//Decode
ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);//解码数据帧到pFrame
if(ret < 0){
printf("Decode Error.\n");
return -1;
}
if(got_picture){
SDL_LockYUVOverlay(bmp);
pFrameYUV->data[0]=bmp->pixels[0];
pFrameYUV->data[1]=bmp->pixels[2];
pFrameYUV->data[2]=bmp->pixels[1];
pFrameYUV->linesize[0]=bmp->pitches[0];
pFrameYUV->linesize[1]=bmp->pitches[2];
pFrameYUV->linesize[2]=bmp->pitches[1];
sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data, pFrame->linesize, 0,
pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize);
SDL_UnlockYUVOverlay(bmp);
SDL_DisplayYUVOverlay(bmp, &rect);
//Delay 40ms
SDL_Delay(40);
}
}
av_free_packet(packet);
}
//FIX: Flush Frames remained in Codec
while (1) {
ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
if (ret < 0)//出错
break;
if (!got_picture)//没有可解码的数据帧
break;
sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize);
SDL_LockYUVOverlay(bmp);
pFrameYUV->data[0]=bmp->pixels[0];
pFrameYUV->data[1]=bmp->pixels[2];
pFrameYUV->data[2]=bmp->pixels[1];
pFrameYUV->linesize[0]=bmp->pitches[0];
pFrameYUV->linesize[1]=bmp->pitches[2];
pFrameYUV->linesize[2]=bmp->pitches[1];
SDL_UnlockYUVOverlay(bmp);
SDL_DisplayYUVOverlay(bmp, &rect);
//Delay 40ms
SDL_Delay(40);
}
sws_freeContext(img_convert_ctx);
SDL_Quit();
av_free(pFrameYUV);//释放帧数据占用的内存
avcodec_close(pCodecCtx);//释放解码器环境
avformat_close_input(&pFormatCtx);//释放输入环境
return 0;
}
在解码流程的第6步中,需要判断读取帧是否是视频帧。这个可以通过判断这一帧的数据的流序号是否是视频流的序号即可。
packet->stream_index==videoindex
而视频流的序号可以通过该流的codec_type==AVMEDIA_TYPE_VIDEO来判断:
//找到视频流的序号
for(i=0; i<pFormatCtx->nb_streams; i++)
if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO){
videoindex=i;
break;
}
if(videoindex==-1){
printf("Didn't find a video stream.\n");
return -1;
}
下一步便是学习常用的数据结构:http://blog.csdn.net/gameloft9/article/details/45841935
如果想要了解SDL部分的内容,可以参考:关于《最简单的视频播放器》记录二