MFC中如何利用ffmpeg和SDL2.0多线程多窗口播放摄像头的视频
SDL
Simple Directmedia Layer
项目地址:https://gitcode.com/gh_mirrors/sd/SDL
免费下载资源
·
我前一篇文章,《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的设备支持列表,比如使用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、其他方面
- 可以通过Video的Stop方法来停止播放视频,可以通过Video的Pause方法来暂停播放视频。
- 以上的代码摘自项目中,但是每个部分介绍得比较清楚,可能需要调整一下,不过剩下的工作就比较简单了
GitHub 加速计划 / sd / SDL
8.87 K
1.67 K
下载
Simple Directmedia Layer
最近提交(Master分支:3 个月前 )
a57c5669 - 3 个月前
20a6193e - 3 个月前
更多推荐
已为社区贡献2条内容
所有评论(0)