[TOC]
開始前的BB
之前我們都是拿ffplay播放視頻晨继,做為一個專業(yè)的開發(fā)人員烟阐,會用就夠了么?
本章,我們就來進(jìn)行(莞式)(分離-解碼-顯示)一條龍蜒茄。
這章的這里就得簡單介紹一下SDL2了唉擂,
SDL 是一個跨平臺的媒體開發(fā)庫 用C寫的(pygame就是包裝的它),主要功能包括檀葛,圖像顯示玩祟、音頻播放、線程控制屿聋、事件處理空扎、定時器、字節(jié)序無關(guān)(大小端)
SDL2就是SDL1的升級版本润讥,變了很多API(沒有錯转锈,我解釋的就是這么通俗)
SDL2我們可以直接自己編譯一下 下載地址
選擇
下載源碼,解壓之后通過終端進(jìn)入,大概是這樣
然后我們就開始輸入命令編譯
./configure --disable-libsamplerate --disable-libudev --disable-dbus --disable-ime --disable-ibus --disable-fcitx
make -j8
make install
完事之后我們把include
這個目錄直接拷貝到我們項(xiàng)目的include/SDL2
里
在/usr/local/lib/
目錄找到libSDL2-2.0.0.dylib
,復(fù)制到librarys里
然后在Cmake文件中
把SDL2加進(jìn)來,就準(zhǔn)備開始愉快的玩耍了
在src中新建chapter_06/sdl_video.h
,擼碼開始
SDL2 播放解碼后的視頻
整體先瀏覽一下調(diào)用方法以及順序
/** 1.初始化SDL2 **/
void initSDL2();
/** 2.初始化FFmpeg **/
void preparDecodec(const char *url);
/** 3.解碼播放 **/
void decodecFrame();
/** 4.釋放資源 **/
void freeContext();
/** 3.1 繪制一幀數(shù)據(jù) 在 decodecFrame() 中調(diào)用 **/
void drawFrame(AVFrame *frame);
/** 播放視頻 (外部調(diào)用的總方法)**/
void playVideo(const char *url);
初始化SDL2
首先我們把SDL2初始化 新建方法initSDL2()
#define WINDOW_WIDTH 1080
#define WINDOW_HEIGHT 720
/** ########## SDL2 相關(guān) ############# **/
SDL_Window *window;
SDL_Renderer *render;
SDL_Texture *texture;
SDL_Rect rect;
/**
* 初始化SDL2
*/
void initSDL2() {
//初始化SDL2
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_TIMER)) {
cout << "[error] SDL Init error!" << endl;
return;
}
//創(chuàng)建Window
window = SDL_CreateWindow("LearnFFmpeg", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, WINDOW_WIDTH,
WINDOW_HEIGHT, SDL_WINDOW_OPENGL);
if (!window) {
cout << "[error] SDL CreateWindow error!" << endl;
return;
}
//創(chuàng)建Render
render = SDL_CreateRenderer(window, -1, 0);
//創(chuàng)建Texture
texture = SDL_CreateTexture(render, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, WINDOW_WIDTH, WINDOW_HEIGHT);
rect.x = 0;
rect.y = 0;
rect.w = WINDOW_WIDTH;
rect.h = WINDOW_HEIGHT;
}
FFmpeg 解復(fù)用+解碼
初始好窗口之后象对,我們來初始化ffmpeg相關(guān)的變量以及參數(shù)
/** ########### FFmpeg 相關(guān) ############# **/
AVFormatContext *formatContext;
AVCodecContext *codecContext;
AVCodec *codec;
AVPacket *packet;
AVFrame *frame;
int videoIndex = -1;
/** 初始化FFmpeg **/
void preparDecodec(const char *url) {
int retcode;
//初始化FormatContext
formatContext = avformat_alloc_context();
if (!formatContext) {
cout << "[error] alloc format context error!" << endl;
return;
}
//打開輸入流
retcode = avformat_open_input(&formatContext, url, nullptr, nullptr);
if (retcode != 0) {
cout << "[error] open input error!" << endl;
return;
}
//讀取媒體文件信息
retcode = avformat_find_stream_info(formatContext, NULL);
if (retcode != 0) {
cout << "[error] find stream error!" << endl;
return;
}
//分配codecContext
codecContext = avcodec_alloc_context3(NULL);
if (!codecContext) {
cout << "[error] alloc codec context error!" << endl;
return;
}
//尋找到視頻流的下標(biāo)
videoIndex = av_find_best_stream(formatContext, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
//將視頻流的的編解碼信息拷貝到codecContext中
retcode = avcodec_parameters_to_context(codecContext, formatContext->streams[videoIndex]->codecpar);
if (retcode != 0) {
cout << "[error] parameters to context error!" << endl;
return;
}
//查找解碼器
codec = avcodec_find_decoder(codecContext->codec_id);
if (codec == nullptr) {
cout << "[error] find decoder error!" << endl;
return;
}
//打開解碼器
retcode = avcodec_open2(codecContext, codec, nullptr);
if (retcode != 0) {
cout << "[error] open decodec error!" << endl;
return;
}
//初始化一個packet
packet = av_packet_alloc();
//初始化一個Frame
frame = av_frame_alloc();
}
初始化好之后就可以進(jìn)行解碼
/** 解碼數(shù)據(jù) **/
void decodecFrame() {
int sendcode = 0;
//讀取包
while (av_read_frame(formatContext, packet) == 0) {
if (packet->stream_index != videoIndex)continue;
//接受解碼后的幀數(shù)據(jù)
while (avcodec_receive_frame(codecContext, frame) == 0) {
//繪制圖像
drawFrame(frame);
}
//發(fā)送解碼前的包數(shù)據(jù)
sendcode = avcodec_send_packet(codecContext, packet);
//根據(jù)發(fā)送的返回值判斷狀態(tài)
if (sendcode == 0) {
cout << "[debug] " << "SUCCESS" << endl;
} else if (sendcode == AVERROR_EOF) {
cout << "[debug] " << "EOF" << endl;
} else if (sendcode == AVERROR(EAGAIN)) {
cout << "[debug] " << "EAGAIN" << endl;
} else {
cout << "[debug] " << av_err2str(AVERROR(sendcode)) << endl;
}
}
}
這邊我發(fā)現(xiàn)網(wǎng)上的教程都沒有說avcodec_send_packet
和avcodec_receive_frame
返回值是什么意思黑忱,這邊我來解釋一部分
0
讀取成功
AVERROR_EOF
已經(jīng)讀取到最后 流結(jié)束的標(biāo)志
AVERROR(EAGAIN)
當(dāng)前發(fā)送/接受隊(duì)里已滿/已空,需要調(diào)用對應(yīng)的recive
/send
接受到AVFrame
數(shù)據(jù)后調(diào)用drawFrame()
進(jìn)行繪制
SDL2顯示一幀畫面
/** 繪制一幀數(shù)據(jù) **/
void drawFrame(AVFrame *frame) {
if (frame == nullptr)return;
//上傳YUV到Texture
SDL_UpdateYUVTexture(texture, &rect,
frame->data[0], frame->linesize[0],
frame->data[1], frame->linesize[1],
frame->data[2], frame->linesize[2]
);
SDL_RenderClear(render);
SDL_RenderCopy(render, texture, NULL, &rect);
SDL_RenderPresent(render);
}
最后記得釋放資源
/** 釋放資源 **/
void freeContext() {
if (formatContext != nullptr) avformat_close_input(&formatContext);
if (codecContext != nullptr) avcodec_free_context(&codecContext);
if (packet != nullptr) av_packet_free(&packet);
if (frame != nullptr) av_frame_free(&frame);
}
整合步驟
我們來把這幾個方法組裝一下勒魔,方便外部調(diào)用
/** 播放視頻 **/
void playVideo(const char *url) {
initSDL2();
preparDecodec(url);
decodecFrame();
freeContext();
}
我們在main
方法中調(diào)用
const char *url = "../video/test_video.mp4";
playVideo(url);
喏甫煞,就顯示出來了
視頻自同步
是不是有些同學(xué)看的顯示的非常快冠绢,沒有錯抚吠,因?yàn)樗麤]有進(jìn)行同步的操作,我們可以來個簡單的同步操作
- 根據(jù)視頻的幀率進(jìn)行同步
我們都知道幀率是描述了視頻圖像連續(xù)出現(xiàn)在顯示器上的頻率,他的局限是有些幀之間的PTS差別較大/小的時候這種方式仍然會按照每個幀固定停留的時間進(jìn)行顯示弟胀,無法動態(tài)變化楷力,通過下面的公式計(jì)算出平均每幀顯示的時間(s)
s = 1/fps
所以我們可以新建一個變量double displayTimeUs = 0;
,decodecFrame()
可以改為
/** 解碼數(shù)據(jù) **/
void decodecFrame() {
int sendcode = 0;
//計(jì)算幀率
double frameRate = av_q2d(formatContext->streams[videoIndex]->avg_frame_rate);
//計(jì)算顯示的時間
displayTimeUs = 1*1000/frameRate;
//讀取包
while (av_read_frame(formatContext, packet) == 0) {
if (packet->stream_index != videoIndex)continue;
//接受解碼后的幀數(shù)據(jù)
while (avcodec_receive_frame(codecContext, frame) == 0) {
//繪制圖像
drawFrame(frame);
}
//發(fā)送解碼前的包數(shù)據(jù)
sendcode = avcodec_send_packet(codecContext, packet);
//根據(jù)發(fā)送的返回值判斷狀態(tài)
if (sendcode == 0) {
cout << "[debug] " << "SUCCESS" << endl;
} else if (sendcode == AVERROR_EOF) {
cout << "[debug] " << "EOF" << endl;
} else if (sendcode == AVERROR(EAGAIN)) {
cout << "[debug] " << "EAGAIN" << endl;
} else {
cout << "[debug] " << av_err2str(AVERROR(sendcode)) << endl;
}
}
}
在drawFrame()
中新增一行代碼SDL_Delay(displayTimeUs);
/** 繪制一幀數(shù)據(jù) **/
void drawFrame(AVFrame *frame) {
if (frame == nullptr)return;
//上傳YUV到Texture
SDL_UpdateYUVTexture(texture, &rect,
frame->data[0], frame->linesize[0],
frame->data[1], frame->linesize[1],
frame->data[2], frame->linesize[2]
);
SDL_RenderClear(render);
SDL_RenderCopy(render, texture, NULL, &rect);
SDL_RenderPresent(render);
SDL_Delay(displayTimeUs);
}
然后點(diǎn)擊啟動
然后就會發(fā)現(xiàn)播放起來已經(jīng)是正常了
未完持續(xù)。孵户。萧朝。