1. 程式人生 > >MFC中如何利用ffmpeg和SDL2.0多執行緒多視窗播放攝像頭的視訊

MFC中如何利用ffmpeg和SDL2.0多執行緒多視窗播放攝像頭的視訊

    我前一篇文章,《Window下用DirectShow查詢攝像頭(含解析度)和麥克風》,詳細介紹瞭如何查詢攝像頭和攝像頭支援的解析度資訊,查詢到攝像頭和麥克風之後做什麼呢?兩個目的,第一個目的是播放,第二個目的是編碼之後傳送伺服器流媒體資料,第三個目的就是存在本地硬碟上了,本文就是播放攝像頭採集的資料。

    本人初次接觸音視訊相關的專案,研究了幾天,從網上斷斷續續的找到不少攝像頭播放的資料,但是都是簡單例子,本文解決了2個問題:

  1. 第一個問題是播放多個攝像頭的視訊
  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
  1. 一個Video繫結一個裝置和引數(如果要更改播放參數,需要從新生成一個物件)
  2. 一個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、其他方面

  1. 可以通過Video的Stop方法來停止播放視訊,可以通過Video的Pause方法來暫停播放視訊。
  2. 以上的程式碼摘自專案中,但是每個部分介紹得比較清楚,可能需要調整一下,不過剩下的工作就比較簡單了