MFC中如何利用ffmpeg和SDL2.0多執行緒多視窗播放攝像頭的視訊
阿新 • • 發佈:2019-02-17
我前一篇文章,《Window下用DirectShow查詢攝像頭(含解析度)和麥克風》,詳細介紹瞭如何查詢攝像頭和攝像頭支援的解析度資訊,查詢到攝像頭和麥克風之後做什麼呢?兩個目的,第一個目的是播放,第二個目的是編碼之後傳送伺服器流媒體資料,第三個目的就是存在本地硬碟上了,本文就是播放攝像頭採集的資料。
本人初次接觸音視訊相關的專案,研究了幾天,從網上斷斷續續的找到不少攝像頭播放的資料,但是都是簡單例子,本文解決了2個問題:
- 第一個問題是播放多個攝像頭的視訊
- 第二個問題是一個攝像頭播放兩個視訊(同樣的視訊流,畫面大小不一樣)
1、MFC中嵌入SDL2.0的播放視窗
用Visual Studio 2015 Community版本,建立一個MFC專案,新增一個Picture的控制元件即可,其實視訊都是一幀幀影象組成的,因此新增影象控制元件即可。CWnd* pWnd1 = this->GetDlgItem(IDC_PIC_1);//IDC_PIC_1就是影象控制元件的ID HWND handle1 = pWnd1->GetSafeHwnd(); //獲取影象控制元件的控制代碼 SDL_Window* screen = SDL_CreateWindowFrom(handle); //SDL建立視窗時,把控制代碼傳入即可
2、ffmpeg+SDL2.0的播放
1)播放類的定義
大量的文章都是基於SDL1.0的版本的,SDL2.0有較多的修改,寫程式碼時需要注意。 本文中,定義了兩個類,Video和Window- 一個Video繫結一個裝置和引數(如果要更改播放參數,需要從新生成一個物件)
- 一個Window繫結一個播放視窗,一個Video可以關聯多個Window這樣就可以實現一個攝像頭在多個不同大小的視窗播放
//播放視窗 class Window { public: void* handle; SDL_Window* screen; SDL_Renderer* sdlRenderer; SDL_Texture* sdlTexture; int width; int height; public: Window(int width, int height); ~Window(); int Init(void* handle,int width, int height); int Update(AVFrame* pFrameYUV); int Exit(); }; //視訊播放,一個攝像頭 class Video { public: int deviceIndex; //裝置序號 int videoIndex; //引數序號 AVFormatContext* pFormatCtx; //格式上下文 AVCodecContext* pCodecCtx; //編碼上下文 int width; int height; AVCodec* pCodec; //解碼器 SDL_Thread* thread; //執行緒 void* listHandle; //視窗控制代碼 void* mainHandle; //主視窗 Window* listWindow; Window* mainWindow; Video() { deviceIndex = 0; videoIndex = 0; pFormatCtx = NULL; pCodecCtx = NULL; pCodec = NULL; thread = NULL; listWindow = NULL; mainWindow = NULL; isStop = false; isPause = false; } ~Video() { } int Init(TDeviceInfo& device, TDeviceParam& param, int deviceIdx); int AddList(void* handle); int AddMain(void* handle); int Play(); int Stop(); int Pause(); int Exit(); private: bool isStop; bool isPause; };
2)初始化裝置
初始化裝置時,有幾個地方特別需要注意一下- 開啟攝像頭時,如果不指定攝像頭的解析度,預設的解析度是最高的,如果指定解析度,需要是該攝像頭支援的解析度列表中的,亂指定是不行的
- 用avformat_open_input開啟裝置時,如果裝置重名,需要指定重名的序號,比如兩個都叫“usb camera”,那麼需要指定開啟的是第一個還是第二個
- 設定解析度是video_size,格式是width*height,比如1024*768
//使用ffmpeg開啟裝置 int OpenVideoDevice(AVFormatContext* formatCtx, TDeviceInfo& device, TDeviceParam& param) { USES_CONVERSION; int width = param.width; int height = param.height; AVInputFormat* iformat = av_find_input_format("dshow"); char video_file[256]; char video_param[64]; char video_size[64]; char video_framerate[64]; snprintf(video_file, 256, "video=%s", W2A(device.FriendlyName) ); snprintf(video_param, 64, "%d", device.Index); snprintf(video_size, 64, "%d*%d", width, height); //snprintf(video_framerate, 64, "%.3f", framerate); printf("%s,%s,%s\n", video_file, video_param, video_size); AVDictionary* options = NULL; av_dict_set(&options, "video_device_number", video_param, 0); av_dict_set(&options, "video_size", video_size, 0); //av_dict_set(&options, "framerate", video_framerate, 0); if ( avformat_open_input(&formatCtx, video_file, iformat, &options) != 0) { printf("Couldn't open video device %s %d.\n", W2A(device.FriendlyName), device.Index); return -1; } } //查詢視訊流,其實只有一路,返回視訊流索引位置 int FindVideoStream(AVFormatContext * formatCtx) { if (avformat_find_stream_info( formatCtx, NULL)<0) { printf("Couldn't find stream information.\n"); return -1; } int videoindex = -1; for (int i = 0; i < formatCtx->nb_streams; i++) { AVCodecContext* codec = formatCtx->streams[i]->codec; printf("Find %d,%d,%d\n", codec->width, codec->height, codec->codec_type); if ( codec->codec_type == AVMEDIA_TYPE_VIDEO) { videoindex = i; break; } } return videoindex; } //根據視訊流的編碼方式開啟解碼器 int OpenCodeer(AVCodecContext * pCodecCtx, AVCodec** pCodec) { //查詢解碼器 *pCodec = avcodec_find_decoder(pCodecCtx->codec_id); if ( *pCodec == NULL) { printf("Codec not found.\n"); return -1; } //開啟解碼器 if (avcodec_open2(pCodecCtx, *pCodec, NULL)<0) { printf("Could not open codec.\n"); return -1; } return 0; } //初始化裝置TDeviceInfo和TDeviceParam在我上一篇文章中有定義 int Video::Init(TDeviceInfo & device, TDeviceParam & param, int deviceIdx){ this->Exit(); deviceIndex = deviceIdx; pFormatCtx = avformat_alloc_context(); //開啟給定引數攝像頭 if (OpenVideoDevice(pFormatCtx, device, param) != 0) { return -1; } //查詢視訊流 videoIndex = FindVideoStream(pFormatCtx); if (videoIndex == -1) { printf("Couldn't find a video stream.\n"); return -1; } //設定編碼上下文 pCodecCtx = pFormatCtx->streams[videoIndex]->codec; if (OpenCodeer(pCodecCtx, &pCodec) != 0) { return -1; } width = pCodecCtx->width; height = pCodecCtx->height; return 0; }
3)播放/迴圈獲取幀並轉換為YUV
int Video::Play() {
AVFrame *pFrame, *pFrameYUV;
unsigned char* out_buffer;
SwsContext* img_convert_ctx;
printf("code %d, width %d, height %d\n", pCodecCtx->codec_id, width, height);
//初始化各種資料
pFrame = av_frame_alloc(); //儲存解碼後AVFrame
pFrameYUV = av_frame_alloc(); //儲存轉換後AVFrame
out_buffer = (unsigned char *)av_malloc(
av_image_get_buffer_size(AV_PIX_FMT_YUV420P, width, height, 1));
av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize, out_buffer,
AV_PIX_FMT_YUV420P, width, height, 1);
AVPacket *packet = (AVPacket *)av_malloc(sizeof(AVPacket));
img_convert_ctx = sws_getContext(width, height, pCodecCtx->pix_fmt,
width, height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
printf("Could not initialize SDL - %s\n", SDL_GetError());
return -1;
}
int ret, got_picture;
while (!isStop) {
if (isPause) {
SDL_Delay(20);
continue;
}
bool getData = true;
while (1) {
if (av_read_frame(pFormatCtx, packet) < 0) {
getData = false;
break;
}
if (packet->stream_index == videoIndex) {
getData = true;
break;
}
}
if (!getData) {
break;
}
ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
if (ret < 0) {
av_free_packet(packet);
printf("Decode Error.\n");
break;
}
if (got_picture) {
//畫素格式轉換。pFrame轉換為pFrameYUV。
sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data, pFrame->linesize, 0, height,
pFrameYUV->data, pFrameYUV->linesize);
//這裡可以播放,也可以編碼流檔案轉發,也可以存在本地,都可以的
if (listWindow != NULL) {
listWindow->Update(pFrameYUV); //視窗1
}
if (mainWindow != NULL) {
mainWindow->Update(pFrameYUV); //視窗2
}
//延時20ms,50幀/秒
SDL_Delay(20);
}
av_free_packet(packet);
}
sws_freeContext(img_convert_ctx);
SDL_Quit();
av_free(out_buffer);
av_free(pFrameYUV);
return 0;
}
4)播放
//注意一下,每個視窗有screen、sdlRenderer和sdlTexture三個物件
int Window::init(void* handle,int width, int height){
this->handle = handle;
this->width = width;
this->height = height;
screen = SDL_CreateWindowFrom(handle);
/*如果是獨立開啟的視窗,可以用下面的方式建立窗體
SDL_Window *screen = SDL_CreateWindow("Simplest FFmpeg Read Camera",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
screen_w, screen_h,
SDL_WINDOW_OPENGL);
*/
sdlRenderer = SDL_CreateRenderer(screen, -1, 0);
sdlTexture = SDL_CreateTexture(sdlRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING,
width, height);
}
//更新幀呼叫如下程式碼
int Window::Update(AVFrame * pFrameYUV){
SDL_UpdateTexture(sdlTexture, NULL, pFrameYUV->data[0], pFrameYUV->linesize[0]);
SDL_RenderClear(sdlRenderer);
SDL_RenderCopy(sdlRenderer, sdlTexture, NULL, NULL);
SDL_RenderPresent(sdlRenderer);
return 0;
}
3、多攝像頭的多執行緒機制
一個攝像頭,啟動一個執行緒執行編解碼和播放,主執行緒不要負責處理編解碼和播放的事情,否則整個視窗就會僵死。在子執行緒中編解碼和播放,我看到有些平臺比如Android據說不能在子執行緒中繪製圖像,那麼只能在子執行緒中編解碼,在主執行緒中繪製圖片,但是我沒有仔細研究過,不得而知。//執行緒函式
int SDL_Play_Thread(void* param) {
Video* video = (Video*)param;
video->Play();
video->AddList(video->listHandle);
return 0;
}
//下面的程式碼中間省略了一些程式碼,可能無法正確編譯,可以簡單調整一下
//1、獲取裝置列表
HRESULT hrrst;
GUID guid = CLSID_VideoInputDeviceCategory;
std::vector<tdeviceinfo> videoDeviceVec;
hrrst = DsGetAudioVideoInputDevices(videoDeviceVec, guid);
//2.迴圈列表開啟裝置
for(int i = 0; i < videoDeviceVec.size(); i++){
video1.listHandle = handle;
//FirstChoose()是選擇引數函式,可以不用選擇,用第一個引數
video1.Init(videoDeviceVec[i], videoDeviceVec[i].FirstChoose(), i);
threadParam = (void*)&video1;
//用SDL_CreateThread來啟動一個執行緒
SDL_Thread *video_tid = SDL_CreateThread(SDL_Play_Thread, "sdl", threadParam);
}
</tdeviceinfo>
4、單攝像頭的多視窗機制
這裡相對簡單,就是上面的播放程式碼,視窗不為空則呼叫Update函式去更新視訊幀,當然是不是有更好的實現方式?比如在這裡暴露一個事件,按照事件註冊的方式去改造,外部呼叫時註冊事件進來,也是可以的。if (listWindow != NULL) {
listWindow->Update(pFrameYUV);
}
if (mainWindow != NULL) {
mainWindow->Update(pFrameYUV);
}
5、其他方面
- 可以通過Video的Stop方法來停止播放視訊,可以通過Video的Pause方法來暫停播放視訊。
- 以上的程式碼摘自專案中,但是每個部分介紹得比較清楚,可能需要調整一下,不過剩下的工作就比較簡單了