我前一篇文章,《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的设备支持列表,比如使用dshow,可以看dshow的参数
//使用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. 以上的代码摘自项目中,但是每个部分介绍得比较清楚,可能需要调整一下,不过剩下的工作就比较简单了
 
 

 

GitHub 加速计划 / sd / SDL
8.87 K
1.67 K
下载
Simple Directmedia Layer
最近提交(Master分支:3 个月前 )
a57c5669 - 3 个月前
20a6193e - 3 个月前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐