天天看点

Linux 基于ffplay的简易视频播放器(网络+本地)

新手刚开始学习ffmpeg。

参考网上的ffmpeg资料和雷神的博客,简易做了个播放器,边学边做。

暂时未做音频,所以播放时有沙沙声。

视频的播放速度也有问题,需要再调整,后续再处理速度和音频的问题!

额,界面功能键也没做,后续再说吧。

放效果图:

Linux 基于ffplay的简易视频播放器(网络+本地)

该播放器是基于ffmpeg+SDL,可播放本地视频和网络URL地址的视频,适合初学者学习。

视频主要解封装过程

FFmpeg的视频解码过程主要有以下几个步骤:

  1. 初始化所有组件(所有的文件格式及其对应的CODEC)

    av_register_all()

  2. 打开文件

    avformat_open_input()

  3. 从文件中提取流信息

    avformat_find_stream_info()

  4. 在多个数据流中找到视频流 video stream(类型为

    MEDIA_TYPE_VIDEO

  5. 查找视频流相对应的解码器

    avcodec_find_decoder

  6. 打开解码器

    avcodec_open2()

  7. 为解码帧分配内存

    av_frame_alloc()

  8. 从流中读取读取数据到Packet中

    av_read_frame()

  9. 对视频流的帧进行解码,调用

    avcodec_decode_video2()

下面放核心代码

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>

#include <SDL.h>
#include <SDL_thread.h>

#ifdef __MINGW32__
#undef main //防止SDL的MAIN问题
#endif

#include <stdio.h>

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 <file>\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; i<pFormatCtx->nb_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,将在后续的博文再更新修正视频的播放速度问题!