前言
ffmpeg播放rtsp網路流和攝像頭流。
使用ffmpeg播放區域網rtsp1080p海康攝像頭:延遲0.2s,存在馬賽克
使用ffmpeg播放網路rtsp檔案流:偶爾卡頓,延遲看不出
使用vlc軟體播放區域網rtsp1080p海康攝像頭:演示2s,不存在馬賽克
使用vlc軟體播放網路rtsp檔案流:不卡頓,延遲看不出
ffmpeg新增API的解碼執行流程。
新api解碼基本流程如下:
使用ffmpeg對應的庫,都需要進行註冊,可以註冊子項也可以註冊全部。
開啟檔案,根據檔名資訊獲取對應的ffmpeg全域性上下文。
一定要探測流資訊,拿到流編碼的編碼格式,不探測流資訊則其流編碼器拿到的編碼型別可能為空,後續進行資料轉換的時候就無法知曉原始格式,導致錯誤。
依據流的格式查詢解碼器,軟解碼還是硬解碼是在此處決定的,但是特別注意是否支援硬體,需要自己查詢本地的硬體解碼器對應的標識,並查詢其是否支援。普遍操作是,列舉支援檔案字尾解碼的所有解碼器進行查詢,查詢到了就是可以硬解了(此處,不做過多的討論,對應硬解碼後續會有文章進行進一步研究)。
(注意:解碼時查詢解碼器,編碼時查詢編碼器,兩者函式不同,不要弄錯了,否則後續能開啟但是資料是錯的)
開打解碼器的時候,播放的是rtsp流,需要設定一些引數,在ffmpeg中引數的設定是通過AVDictionary來設定的。
使用以上設定的引數,傳入並開啟獲取到的解碼器。
AVDictionary *pAVDictionary = 0
// 設定快取大小 1024000byte
av_dict_set(&pAVDictionary, "buffer_size", "1024000", 0);
// 設定超時時間 20s
av_dict_set(&pAVDictionary, "stimeout", "20000000", 0);
// 設定最大延時 3s
av_dict_set(&pAVDictionary, "max_delay", "30000000", 0);
// 設定開啟方式 tcp/udp
av_dict_set(&pAVDictionary, "rtsp_transport", "tcp", 0);
ret = avcodec_open2(pAVCodecContext, pAVCodec, &pAVDictionary);
if(ret)
{
LOG << "Failed to avcodec_open2(pAVCodecContext, pAVCodec, pAVDictionary)";
return;
}
此處特別注意,基本上解碼的資料都是yuv系列格式,但是我們顯示的資料是rgb等相關顏色空間的資料,所以此處轉換結構體就是進行轉換前到轉換後的描述,給後續轉換函式提供轉碼依據,是很關鍵並且非常常用的結構體。
申請一個快取區outBuffer,fill到我們目標幀資料的data上,比如rgb資料,QAVFrame的data上存是有指定格式的資料,且儲存有規則,而fill到outBuffer(自己申請的目標格式一幀快取區),則是我們需要的資料格式儲存順序。
舉個例子,解碼轉換後的資料為rgb888,實際直接用data資料是錯誤的,但是用outBuffer就是對的,所以此處應該是ffmpeg的fill函式做了一些轉換。
進入迴圈解碼:
拿取封裝的一個packet,判斷packet資料的型別進行送往解碼器解碼。
一個包可能存在多組資料,老的api獲取的是第一個,新的api分開後,可以迴圈獲取,直至獲取不到跳轉“步驟十二”。
拿到了原始資料自行處理。
不斷迴圈,直到拿取pakcet函式成功,但是無法got一幀資料,則代表檔案解碼已經完成。
幀率需要自己控制迴圈,此處只是迴圈拿取,可加延遲等。
此處要單獨列出是因為,其實很多網上和開發者的程式碼:
在進入迴圈解碼前進行了av_new_packet,迴圈中未av_free_packet,造成記憶體溢位;
在進入迴圈解碼前進行了av_new_packet,迴圈中進行av_free_pakcet,那麼一次new對應無數次free,在編碼器上是不符合前後一一對應規範的。
檢視原始碼,其實可以發現av_read_frame時,自動進行了av_new_packet(),那麼其實對於packet,只需要進行一次av_packet_alloc()即可,解碼完後av_free_packet。
執行完後,返回執行“步驟八:獲取一幀packet”,一次迴圈結束。
全部解碼完成後,安裝申請順序,進行對應資源的釋放。
關閉之前開啟的解碼/編碼器。
關閉檔案上下文後,要對之前申請的變數按照申請的順序,依次釋放。
ffmpeg開啟rtsp出現嚴重的馬賽克和部分卡頓,需要修改檔案udp.c的快取區大小,修改後需要重新編譯。
實測更改後的馬賽克會好一些,相比較軟體來說有一些差距的,這部分需要繼續優化。
編譯請參照《FFmpeg開發筆記(三):ffmpeg介紹、windows編譯以及開發環境搭建》
void FFmpegManager::testDecodeRtspSyncShow()
{
QString rtspUrl = "http://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear2/prog_index.m3u8";
// QString rtspUrl = "rtsp://admin:Admin123@192.168.1.65:554/h264/ch1/main/av_stream";
// SDL相關變數預先定義
SDL_Window *pSDLWindow = 0;
SDL_Renderer *pSDLRenderer = 0;
SDL_Surface *pSDLSurface = 0;
SDL_Texture *pSDLTexture = 0;
SDL_Event event;
qint64 startTime = 0; // 記錄播放開始
int currentFrame = 0; // 當前幀序號
double fps = 0; // 幀率
double interval = 0; // 幀間隔
// ffmpeg相關變數預先定義與分配
AVFormatContext *pAVFormatContext = 0; // ffmpeg的全域性上下文,所有ffmpeg操作都需要
AVStream *pAVStream = 0; // ffmpeg流資訊
AVCodecContext *pAVCodecContext = 0; // ffmpeg編碼上下文
AVCodec *pAVCodec = 0; // ffmpeg編碼器
AVPacket *pAVPacket = 0; // ffmpag單幀資料包
AVFrame *pAVFrame = 0; // ffmpeg單幀快取
AVFrame *pAVFrameRGB32 = 0; // ffmpeg單幀快取轉換顏色空間後的快取
struct SwsContext *pSwsContext = 0; // ffmpeg編碼資料格式轉換
AVDictionary *pAVDictionary = 0; // ffmpeg資料字典,用於配置一些編碼器屬性等
int ret = 0; // 函式執行結果
int videoIndex = -1; // 音訊流所在的序號
int numBytes = 0; // 解碼後的資料長度
uchar *outBuffer = 0; // 解碼後的資料存放快取區
pAVFormatContext = avformat_alloc_context(); // 分配
pAVPacket = av_packet_alloc(); // 分配
pAVFrame = av_frame_alloc(); // 分配
pAVFrameRGB32 = av_frame_alloc(); // 分配
if(!pAVFormatContext || !pAVPacket || !pAVFrame || !pAVFrameRGB32)
{
LOG << "Failed to alloc";
return;
}
// 步驟一:註冊所有容器和編解碼器(也可以只註冊一類,如註冊容器、註冊編碼器等)
av_register_all();
avformat_network_init();
// 步驟二:開啟檔案(ffmpeg成功則返回0)
LOG << "開啟:" << rtspUrl;
ret = avformat_open_input(&pAVFormatContext, rtspUrl.toUtf8().data(), 0, 0);
if(ret)
{
LOG << "Failed";
return;
}
// 步驟三:探測流媒體資訊
ret = avformat_find_stream_info(pAVFormatContext, 0);
if(ret < 0)
{
LOG << "Failed to avformat_find_stream_info(pAVFormatContext, 0)";
return;
}
// 步驟四:提取流資訊,提取視訊資訊
for(int index = 0; index < pAVFormatContext->nb_streams; index++)
{
pAVCodecContext = pAVFormatContext->streams[index]->codec;
pAVStream = pAVFormatContext->streams[index];
switch (pAVCodecContext->codec_type)
{
case AVMEDIA_TYPE_UNKNOWN:
LOG << "流序號:" << index << "型別為:" << "AVMEDIA_TYPE_UNKNOWN";
break;
case AVMEDIA_TYPE_VIDEO:
LOG << "流序號:" << index << "型別為:" << "AVMEDIA_TYPE_VIDEO";
videoIndex = index;
LOG;
break;
case AVMEDIA_TYPE_AUDIO:
LOG << "流序號:" << index << "型別為:" << "AVMEDIA_TYPE_AUDIO";
break;
case AVMEDIA_TYPE_DATA:
LOG << "流序號:" << index << "型別為:" << "AVMEDIA_TYPE_DATA";
break;
case AVMEDIA_TYPE_SUBTITLE:
LOG << "流序號:" << index << "型別為:" << "AVMEDIA_TYPE_SUBTITLE";
break;
case AVMEDIA_TYPE_ATTACHMENT:
LOG << "流序號:" << index << "型別為:" << "AVMEDIA_TYPE_ATTACHMENT";
break;
case AVMEDIA_TYPE_NB:
LOG << "流序號:" << index << "型別為:" << "AVMEDIA_TYPE_NB";
break;
default:
break;
}
// 已經找打視訊品流
if(videoIndex != -1)
{
break;
}
}
if(videoIndex == -1 || !pAVCodecContext)
{
LOG << "Failed to find video stream";
return;
}
// 步驟五:對找到的視訊流尋解碼器
pAVCodec = avcodec_find_decoder(pAVCodecContext->codec_id);
if(!pAVCodec)
{
LOG << "Fialed to avcodec_find_decoder(pAVCodecContext->codec_id):"
<< pAVCodecContext->codec_id;
return;
}
// 步驟六:開啟解碼器
// 設定快取大小 1024000byte
av_dict_set(&pAVDictionary, "buffer_size", "1024000", 0);
// 設定超時時間 20s
av_dict_set(&pAVDictionary, "stimeout", "20000000", 0);
// 設定最大延時 3s
av_dict_set(&pAVDictionary, "max_delay", "30000000", 0);
// 設定開啟方式 tcp/udp
av_dict_set(&pAVDictionary, "rtsp_transport", "tcp", 0);
ret = avcodec_open2(pAVCodecContext, pAVCodec, &pAVDictionary);
if(ret)
{
LOG << "Failed to avcodec_open2(pAVCodecContext, pAVCodec, pAVDictionary)";
return;
}
// 顯示視訊相關的引數資訊(編碼上下文)
LOG << "位元率:" << pAVCodecContext->bit_rate;
LOG << "寬高:" << pAVCodecContext->width << "x" << pAVCodecContext->height;
LOG << "格式:" << pAVCodecContext->pix_fmt; // AV_PIX_FMT_YUV420P 0
LOG << "幀率分母:" << pAVCodecContext->time_base.den;
LOG << "幀率分子:" << pAVCodecContext->time_base.num;
LOG << "幀率分母:" << pAVStream->avg_frame_rate.den;
LOG << "幀率分子:" << pAVStream->avg_frame_rate.num;
LOG << "總時長:" << pAVStream->duration / 10000.0 << "s";
LOG << "總幀數:" << pAVStream->nb_frames;
// 有總時長的時候計算幀率(較為準確)
// fps = pAVStream->nb_frames / (pAVStream->duration / 10000.0);
// interval = pAVStream->duration / 10.0 / pAVStream->nb_frames;
// 沒有總時長的時候,使用分子和分母計算
fps = pAVStream->avg_frame_rate.num * 1.0f / pAVStream->avg_frame_rate.den;
interval = 1 * 1000 / fps;
LOG << "平均幀率:" << fps;
LOG << "幀間隔:" << interval << "ms";
// 步驟七:對拿到的原始資料格式進行縮放轉換為指定的格式高寬大小
pSwsContext = sws_getContext(pAVCodecContext->width,
pAVCodecContext->height,
pAVCodecContext->pix_fmt,
pAVCodecContext->width,
pAVCodecContext->height,
AV_PIX_FMT_RGBA,
SWS_FAST_BILINEAR,
0,
0,
0);
numBytes = avpicture_get_size(AV_PIX_FMT_RGBA,
pAVCodecContext->width,
pAVCodecContext->height);
outBuffer = (uchar *)av_malloc(numBytes);
// pAVFrame32的data指標指向了outBuffer
avpicture_fill((AVPicture *)pAVFrameRGB32,
outBuffer,
AV_PIX_FMT_RGBA,
pAVCodecContext->width,
pAVCodecContext->height);
ret = SDL_Init(SDL_INIT_VIDEO);
if(ret)
{
LOG << "Failed";
return;
}
pSDLWindow = SDL_CreateWindow(rtspUrl.toUtf8().data(),
0,
0,
pAVCodecContext->width,
pAVCodecContext->height,
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
if(!pSDLWindow)
{
LOG << "Failed";
return;
}
pSDLRenderer = SDL_CreateRenderer(pSDLWindow, -1, 0);
if(!pSDLRenderer)
{
LOG << "Failed";
return;
}
startTime = QDateTime::currentDateTime().toMSecsSinceEpoch();
currentFrame = 0;
pSDLTexture = SDL_CreateTexture(pSDLRenderer,
// SDL_PIXELFORMAT_IYUV,
SDL_PIXELFORMAT_YV12,
SDL_TEXTUREACCESS_STREAMING,
pAVCodecContext->width,
pAVCodecContext->height);
if(!pSDLTexture)
{
LOG << "Failed";
return;
}
// 步驟八:讀取一幀資料的資料包
while(av_read_frame(pAVFormatContext, pAVPacket) >= 0)
{
if(pAVPacket->stream_index == videoIndex)
{
// 步驟八:對讀取的資料包進行解碼
ret = avcodec_send_packet(pAVCodecContext, pAVPacket);
if(ret)
{
LOG << "Failed to avcodec_send_packet(pAVCodecContext, pAVPacket) ,ret =" << ret;
break;
}
while(!avcodec_receive_frame(pAVCodecContext, pAVFrame))
{
sws_scale(pSwsContext,
(const uint8_t * const *)pAVFrame->data,
pAVFrame->linesize,
0,
pAVCodecContext->height,
pAVFrameRGB32->data,
pAVFrameRGB32->linesize);
// 格式為RGBA=8:8:8:8”
// rmask 應為 0xFF000000 但是顏色不對 改為 0x000000FF 對了
// gmask 0x00FF0000 0x0000FF00
// bmask 0x0000FF00 0x00FF0000
// amask 0x000000FF 0xFF000000
// 測試了ARGB,也是相反的,而QImage是可以正確載入的
// 暫時只能說這個地方標記下,可能有什麼設定不對什麼的
qDebug() << __FILE__ << __LINE__ << pSDLTexture;
SDL_UpdateYUVTexture(pSDLTexture,
NULL,
pAVFrame->data[0], pAVFrame->linesize[0],
pAVFrame->data[1], pAVFrame->linesize[1],
pAVFrame->data[2], pAVFrame->linesize[2]);
qDebug() << __FILE__ << __LINE__ << pSDLTexture;
SDL_RenderClear(pSDLRenderer);
// Texture複製到Renderer
SDL_Rect sdlRect;
sdlRect.x = 0;
sdlRect.y = 0;
sdlRect.w = pAVFrame->width;
sdlRect.h = pAVFrame->height;
qDebug() << __FILE__ << __LINE__ << SDL_RenderCopy(pSDLRenderer, pSDLTexture, 0, &sdlRect) << pSDLTexture;
// 更新Renderer顯示
SDL_RenderPresent(pSDLRenderer);
// 事件處理
SDL_PollEvent(&event);
}
// 下一幀
currentFrame++;
while(QDateTime::currentDateTime().toMSecsSinceEpoch() - startTime < currentFrame * interval)
{
SDL_Delay(1);
}
LOG << "current:" << currentFrame <<"," << time << (QDateTime::currentDateTime().toMSecsSinceEpoch() - startTime);
}
}
LOG << "釋放回收資源";
if(outBuffer)
{
av_free(outBuffer);
outBuffer = 0;
}
if(pSwsContext)
{
sws_freeContext(pSwsContext);
pSwsContext = 0;
LOG << "sws_freeContext(pSwsContext)";
}
if(pAVFrameRGB32)
{
av_frame_free(&pAVFrameRGB32);
pAVFrame = 0;
LOG << "av_frame_free(pAVFrameRGB888)";
}
if(pAVFrame)
{
av_frame_free(&pAVFrame);
pAVFrame = 0;
LOG << "av_frame_free(pAVFrame)";
}
if(pAVPacket)
{
av_free_packet(pAVPacket);
pAVPacket = 0;
LOG << "av_free_packet(pAVPacket)";
}
if(pAVCodecContext)
{
avcodec_close(pAVCodecContext);
pAVCodecContext = 0;
LOG << "avcodec_close(pAVCodecContext);";
}
if(pAVFormatContext)
{
avformat_close_input(&pAVFormatContext);
avformat_free_context(pAVFormatContext);
pAVFormatContext = 0;
LOG << "avformat_free_context(pAVFormatContext)";
}
// 步驟五:銷燬渲染器
SDL_DestroyRenderer(pSDLRenderer);
// 步驟六:銷燬視窗
SDL_DestroyWindow(pSDLWindow);
// 步驟七:退出SDL
SDL_Quit();
}
對應工程模板v1.5.0:增加播放rtsp使用SDL播放Demo