新手剛開始學習ffmpeg。
參考網上的ffmpeg資料和雷神的部落格,簡易做了個播放器,邊學邊做。
暫時未做音頻,是以播放時有沙沙聲。
視訊的播放速度也有問題,需要再調整,後續再處理速度和音頻的問題!
額,界面功能鍵也沒做,後續再說吧。
放效果圖:
該播放器是基于ffmpeg+SDL,可播放本地視訊和網絡URL位址的視訊,适合初學者學習。
視訊主要解封裝過程
FFmpeg的視訊解碼過程主要有以下幾個步驟:
- 初始化所有元件(所有的檔案格式及其對應的CODEC)
av_register_all()
- 打開檔案
avformat_open_input()
- 從檔案中提取流資訊
avformat_find_stream_info()
- 在多個資料流中找到視訊流 video stream(類型為
)MEDIA_TYPE_VIDEO
- 查找視訊流相對應的解碼器
avcodec_find_decoder
- 打開解碼器
avcodec_open2()
- 為解碼幀配置設定記憶體
av_frame_alloc()
- 從流中讀取讀取資料到Packet中
av_read_frame()
- 對視訊流的幀進行解碼,調用
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,将在後續的博文再更新修正視訊的播放速度問題!