新手刚开始学习ffmpeg。
参考网上的ffmpeg资料和雷神的博客,简易做了个播放器,边学边做。
暂时未做音频,所以播放时有沙沙声。
视频的播放速度也有问题,需要再调整,后续再处理速度和音频的问题!
额,界面功能键也没做,后续再说吧。
放效果图:
该播放器是基于ffmpeg+SDL,可播放本地视频和网络URL地址的视频,适合初学者学习。
FFmpeg的视频解码过程主要有以下几个步骤:
av_register_all()
avformat_open_input()
avformat_find_stream_info()
MEDIA_TYPE_VIDEO
)avcodec_find_decoder
avcodec_open2()
av_frame_alloc()
av_read_frame()
avcodec_decode_video2()
下面放核心代码
#include
#include
#include
#include
#include
#ifdef __MINGW32__
#undef main //防止SDL的MAIN问题
#endif
#include
int main(int argc, char *argv[]) {
AVFormatContext *pFormatCtx = NULL;
int i, videoStream;
AVCodecContext *pCodecCtx = NULL;
AVCodec *pCodec = NULL;
AVFrame *pFrame = NULL;
AVPacket packet;
int frameFinished;
AVDictionary *optionsDict = NULL;
struct SwsContext *sws_ctx = NULL;
SDL_Overlay *bmp = NULL;
SDL_Surface *screen_sdl = NULL;
SDL_Rect rect;
SDL_Event event;
if(argc < 2) {
fprintf(stderr, "Usage: please input \n");
exit(1);
}
//初始化所有组件,只有调用了该函数,才能使用复用器和编解码器
av_register_all();
if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
exit(1);
}
//打开一个文件并解析。可解析的内容包括:视频流、音频流、视频流参数、音频流参数、视频帧索引。
//该函数读取文件的头信息,并将其信息保存到AVFormatContext结构体中
if(avformat_open_input(&pFormatCtx, argv[1], NULL, NULL)!=0)
return -1; // Couldn't open file
//作用是为pFormatContext->streams填充上正确的音视频格式信息,通过av_dump_format函数输出
if(avformat_find_stream_info(pFormatCtx, NULL)<0)
return -1; // Couldn't find stream information
//将音视频数据格式通过av_log输出到指定的文件或者控制台,删除该函数的调用没有任何的影响
av_dump_format(pFormatCtx, 0, argv[1], 0);
//要解码视频,首先在AVFormatContext包含的多个流中找到CODEC,类型为AVMEDIA_TYPE_VIDEO
videoStream=-1;
for(i=0; inb_streams; i++)
if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) {
videoStream=i;
break;
}
if(videoStream==-1)
return -1; // 找不到就结束
// Get a pointer to the codec context for the video stream
pCodecCtx=pFormatCtx->streams[videoStream]->codec;
// 寻找解码器
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec==NULL) {
fprintf(stderr, "Unsupported codec!\n");
return -1; // 找不到Codec
}
// 调用avcodec_open2打开codec
if(avcodec_open2(pCodecCtx, pCodec, &optionsDict)<0)
return -1; // 无法打开codec
// 对 video frame进行分配空间
pFrame=av_frame_alloc();
//使用SDL做界面
screen_sdl = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 0, 0);
if(!screen_sdl ) {
fprintf(stderr, "SDL: could not set video mode - exiting\n");
exit(1);
}
// 把YUV数据放到screen
bmp = SDL_CreateYUVOverlay(pCodecCtx->width,
pCodecCtx->height,
SDL_YV12_OVERLAY,
screen_sdl );
sws_ctx =
sws_getContext
(
pCodecCtx->width,
pCodecCtx->height,
pCodecCtx->pix_fmt, //定义输入图像信息(寬、高、颜色空间(像素格式))
pCodecCtx->width,
pCodecCtx->height,
AV_PIX_FMT_YUV420P,//定义输出图像信息(寬、高、颜色空间(像素格式))
SWS_BILINEAR,//选择缩放算法(只有当输入输出图像大小不同时有效)
NULL,
NULL,
NULL
);
// 读取frames数据并且保存
i=0;
while(av_read_frame(pFormatCtx, &packet)>=0) {
if(packet.stream_index==videoStream) {
// Decode video frame
//作用是解码一帧视频数据。输入一个压缩编码的结构体AVPacket,输出一个解码后的结构体AVFrame
avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished,
&packet);
if(frameFinished) {
SDL_LockYUVOverlay(bmp);
//把图片转为YUV使用的格式
AVPicture pict;
pict.data[0] = bmp->pixels[0];
pict.data[1] = bmp->pixels[2];
pict.data[2] = bmp->pixels[1];
pict.linesize[0] = bmp->pitches[0];
pict.linesize[1] = bmp->pitches[2];
pict.linesize[2] = bmp->pitches[1];
sws_scale
(
sws_ctx,
(uint8_t const * const *)pFrame->data,
pFrame->linesize,
0,
pCodecCtx->height,
pict.data,
pict.linesize
);
SDL_UnlockYUVOverlay(bmp);
rect.x = 0;
rect.y = 0;
rect.w = pCodecCtx->width;
rect.h = pCodecCtx->height;
SDL_DisplayYUVOverlay(bmp, &rect);
}
}
//释放 packet
av_free_packet(&packet);
SDL_PollEvent(&event);
switch(event.type) {
case SDL_QUIT:
SDL_Quit();
exit(0);
break;
default:
break;
}
}
// 释放掉frame
av_free(pFrame);
//关闭打开的流和解码器
avcodec_close(pCodecCtx);
avformat_close_input(&pFormatCtx);
return 0;
}
使用方法:编译出来的文件+本地视频/视频URL地址
./a.out xxx.mp4
./a.out url地址
下面介绍各种函数:
av_read_frame()//读取帧数据
该函数用于读取具体的音/视频帧数据
int av_read_frame(AVFormatContext *s, AVPacket *pkt);
参数说明:
AVFormatContext *s // 文件格式上下文,输入的AVFormatContext
AVPacket *pkt // 这个值不能传NULL,必须是一个空间,输出的AVPacket
// 返回值:return 0 is OK, <0 on error or end of file
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)
就是初始化SDL
参数:
SDL_INIT_TIMER Initializes the timer subsystem.
SDL_INIT_AUDIO Initializes the audio subsystem.
SDL_INIT_VIDEO Initializes the video subsystem.
SDL_INIT_CDROM Initializes the cdrom subsystem.
SDL_INIT_JOYSTICK Initializes the joystick subsystem.
SDL_INIT_EVERYTHING Initialize all of the above.
SDL_INIT_NOPARACHUTE
Prevents SDL from catching fatal signals.
SDL_INIT_EVENTTHREAD
avformat_open_input
//打开一个文件并解析。可解析的内容包括:视频流、音频流、视频流参数、音频流参数、视频帧索引。
//该函数读取文件的头信息,并将其信息保存到AVFormatContext结构体中
pCodecCtx=pFormatCtx->streams[videoStream]->codec;
用一个指针指向视频流codec,为找解码器做准备
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
通过codec指针,avcodec_find_decoder为视频流找到解码器
avcodec_open2(pCodecCtx, pCodec, &optionsDict)
寻找解码器,找到就调用函数avcodec_open2打开,后面记得要关闭解码器
SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 0, 0);
SDL_Surface *SDL_SetVideoMode(int width, int height, int bpp, Uint32 flags);
前面两个参数为长宽
bpp:一般默认为0
flags:一般默认为0,其它参数如下。
SDL_SWSURFACE 在系统内存中创建视频表面
SDL_HWSURFACE 在视频内存中创建视频表面
SDL_ASYNCBLIT 允许使用显示表面的异步更新。这通常会减慢在单个CPU上的速度,但可能会提高SMP系统的速度。
SDL_ANYFORMAT 通常,如果所请求的每像素位(bpp)的视频表面不可用,SDL将模拟具有阴影表面的视频表面。通过SDL_ANYFORMAT可以防止这种情况发生,并导致SDL使用视频表面,无论其像素深度如何。
SDL_HWPALETTE 赋予SDL专用调色板访问权
SDL_DOUBLEBUF 启用硬件双缓冲;仅对SDL_HWSURFACE有效。调用SDL_Flip将翻转buf‐fer并更新屏幕。所有的绘图都将在目前没有显示的表面上进行。如果无法启用双缓冲,那么SDL_Flip将在整个屏幕上执行SDL_UpdateRect
SDL_FULLSCREEN SDL will attempt to use a fullscreen mode. If a hardware resolution change is not possible (for what‐
ever reason), the next higher resolution will be used and the display window centered on a black back‐
ground.
SDL_OPENGL Create an OpenGL rendering context. You should have previously set OpenGL video attributes with
SDL_GL_SetAttribute.
SDL_OPENGLBLIT Create an OpenGL rendering context, like above, but allow normal blitting operations. The screen (2D)
surface may have an alpha channel, and SDL_UpdateRects must be used for updating changes to the screen
surface.
SDL_RESIZABLE Create a resizable window. When the window is resized by the user a SDL_VIDEORESIZE event is generated
and SDL_SetVideoMode can be called again with the new size.
SDL_NOFRAME If possible, SDL_NOFRAME causes SDL to create a window with no title bar or frame decoration.
Fullscreen modes automatically have this flag set.
bmp = SDL_CreateYUVOverlay(pCodecCtx->width,
pCodecCtx->height,
SDL_YV12_OVERLAY, //选择Y\V\U模式
screen_sdl ); //这个就是绑定播放窗口
函数原型:
SDL_Overlay *SDL_CreateYUVOverlay(int width, int height, Uint32 format, SDL_Surface *display);
参数解析:width、height两参数指视频的分辨率大小,format有关图片的YUV三个参数,display绑定播放窗口
//SDL_CreateYUVOverlay - Create a YUV video overlay
//这函数就是把我们的YUV图像放在屏幕上
//CreateYUVOverlay的大小为视频分辨率,DisplayYUVOverlay则为播放窗口的大小
format参数具体项解析:
#define SDL_YV12_OVERLAY 0x32315659 /* Planar mode: Y + V + U */
#define SDL_IYUV_OVERLAY 0x56555949 /* Planar mode: Y + U + V */
#define SDL_YUY2_OVERLAY 0x32595559 /* Packed mode: Y0+U0+Y1+V0 */
#define SDL_UYVY_OVERLAY 0x59565955 /* Packed mode: U0+Y0+V0+Y1 */
#define SDL_YVYU_OVERLAY 0x55595659 /* Packed mode: Y0+V0+Y1+U0 */
avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);
ffmpeg中的avcodec_decode_video2()的作用是解码一帧视频数据。
输入一个压缩编码的结构体AVPacket,输出一个解码后的结构体AVFrame。
该函数的声明位于libavcodec\avcodec.h
FFmpeg里面的sws_scale库可以在一个函数里面同时实现:1.图像色彩空间转换;2.分辨率缩放;3.前后图像滤波处理。
函数struct SwsContext *sws_getContext(int srcW, int srcH, enum AVPixelFormat srcFormat,
int dstW, int dstH, enum AVPixelFormat dstFormat,
int flags,
SwsFilter *srcFilter, SwsFilter *dstFilter, const double *param);
函数目的:初始化sws_scale
参数int srcW, int srcH, enum AVPixelFormat srcFormat定义输入图像信息(寬、高、颜色空间(像素格式))
参数int dstW, int dstH, enum AVPixelFormat dstFormat定义输出图像信息寬、高、颜色空间(像素格式))。
参数int flags选择缩放算法(只有当输入输出图像大小不同时有效)
//后三个参数一般为NULL
参数SwsFilter *srcFilter, SwsFilter *dstFilter分别定义输入/输出图像滤波器信息,如果不做前后图像滤波,输入NULL
参数const double *param定义特定缩放算法需要的参数,默认为NULL
函数返回SwsContext结构体,定义了基本变换信息。
如果是对一个序列的所有帧做相同的处理,函数sws_getContext只需要调用一次就可以了。
sws_getContext(w, h, YV12, w, h, NV12, 0, NULL, NULL, NULL); // YV12->NV12 色彩空间转换
sws_getContext(w, h, YV12, w/2, h/2, YV12, 0, NULL, NULL, NULL); // YV12图像缩小到原图1/4
sws_getContext(w, h, YV12, 2w, 2h, YN12, 0, NULL, NULL, NULL); // YV12图像放大到原图4倍,并转换为NV12结构
int sws_scale(struct SwsContext *c,
const uint8_t *const srcSlice[], const int srcStride[],
int srcSliceY, int srcSliceH,
uint8_t *const dst[], const int dstStride[]);
函数目的:做转换
参数struct SwsContext *c,为上面sws_getContext函数返回值;
参数const uint8_t *const srcSlice[], const int srcStride[]定义输入图像信息(当前处理区域的每个通道数据指针,每个通道行字节数)
stride定义下一行的起始位置。stride和width不一定相同,这是因为:
1.由于数据帧存储的对齐,有可能会向每行后面增加一些填充字节这样 stride = width + N;
2.packet色彩空间下,每个像素几个通道数据混合在一起,例如RGB24,每个像素3字节连续存放,因此下一行的位置需要跳过3*width字节。
srcSlice和srcStride的维数相同,由srcFormat值来。
csp 维数 宽width 跨度stride 高
YUV420 3 w, w/2, w/2 s, s/2, s/2 h, h/2, h/2
YUYV 1 w, w/2, w/2 2s, 0, 0 h, h, h
NV12 2 w, w/2, w/2 s, s, 0 h, h/2
RGB24 1 w, w, w 3s, 0, 0 h, 0, 0
参数int srcSliceY, int srcSliceH,定义在输入图像上处理区域,srcSliceY是起始位置,srcSliceH是处理多少行。如果srcSliceY=0,srcSliceH=height,表示一次性处理完整个图像。
这种设置是为了多线程并行,例如可以创建两个线程,第一个线程处理 [0, h/2-1]行,第二个线程处理 [h/2, h-1]行。并行处理加快速度。
参数uint8_t *const dst[], const int dstStride[]定义输出图像信息(输出的每个通道数据指针,每个通道行字节数)
SDL_UnlockYUVOverlay(bmp);
SDL_DisplayYUVOverlay(bmp, &rect);
SDL_UnlockYUVOverlay:对YUV解锁,overlay展示之前必须先解锁
SDL_DisplayYUVOverlay:解码出一帧数据后就可通过调用此函数进行视频的显示
其实到这里就可以明白了,想要正常播放一个视频,就是将视频分解成一帧一帧的数据,然后再将每一帧显示出来,每一帧接连的播放合起来就是我们看到的视频。
SDL_PollEvent(&event);
SDL_PollEvent从事件队列里取出事件,判断类型,然后处理。
SDL_Event是一个结构体,其定义如下:
typedef union SDL_Event
{
Uint8 type; //事件类型
SDL_ActiveEvent active; //窗口焦点、输入焦点及鼠标焦点的失去和得到事件
SDL_KeyboardEvent key; //键盘事件,键盘按下和释放
SDL_MouseMotionEvent motion; //鼠标移动事件
SDL_MouseButtonEvent button; //鼠标按键事件
SDL_JoyAxisEvent jaxis; //手柄事件
SDL_JoyBallEvent jball; //手柄事件
SDL_JoyHatEvent jhat; //手柄事件
SDL_JoyButtonEvent jbutton; //手柄事件
SDL_ResizeEvent resize; //窗口大小变化事件
SDL_ExposeEvent expose; //窗口重绘事件
SDL_QuitEvent quit; //退出事件
SDL_UserEvent user; //用户自定义事件
SDL_SysWMEvent syswm; //平台相关的系统事件
} SDL_Event;
这个只是一个测试的demo,将在后续的博文再更新修正视频的播放速度问题!