天天看點

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,将在後續的博文再更新修正視訊的播放速度問題!