文章目录

  • 前言
  • 一、如何实现?
  • 1、创建HBitmap
  • 2、写入HBitmap
  • 3、获取HBitmap数据
  • 二、封装对象
  • 1、接口设计
  • 2、完整代码
  • 三、使用示例
  • 1、输入流中合并
  • 2、单独线程合并
  • 3、效果预览
  • 四、性能测试
  • 总结



前言

以前用ffmpeg的滤镜实现过多路视频流合并,后来想到其实只要是图形处理库应该都能实现图像的合并,于是尝试了一下用使用gdi来实现视频流的合并,实际发现效果还可以,可以支持rgb格式的图像数据的合并,对于1080p的数据合并2路流的耗时也在可接受范围内。本文将介绍gdi如何实现视频流的合并,以及封装成对象使用。


一、如何实现?

1、创建HBitmap

合并视频流首先需要一个图像缓存用于保存合并的图片,以及能够使用gdi进行操作,所以必然是需要一个HBitmap对象的。

//获取桌面hdc,用于创建设备兼容hdc
auto srcHdc = GetDC(GetDesktopWindow());
//创建设备兼容的hdc
auto hdc = CreateCompatibleDC(srcHdc);
//设置bitmap参数
BITMAPINFO bmi;
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = _outputWidth;
bmi.bmiHeader.biHeight = -_outputHeight;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
bmi.bmiHeader.biSizeImage = 0;
bmi.bmiHeader.biXPelsPerMeter = 0;
bmi.bmiHeader.biYPelsPerMeter = 0;
bmi.bmiHeader.biClrUsed = 0;
bmi.bmiHeader.biClrImportant = 0;
//创建bitmap
auto hBitmap = CreateDIBSection(_hdc, &bmi, DIB_RGB_COLORS, NULL, NULL, 0);
//将bitmap设备hdc的位图缓存
auto oldBitmap = (HBITMAP)SelectObject(_hdc, _hBitmap);

2、写入HBitmap

对于得到的输入流图像数据我们可以直接调用StretchDIBits将图像数据写入指定的位置

//输入流的图像信息
BITMAPINFO bmi;
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = j->bufferFrame.Width;
bmi.bmiHeader.biHeight = -j->bufferFrame.Height;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount =  32 ;
bmi.bmiHeader.biCompression = BI_RGB;
bmi.bmiHeader.biSizeImage = 0;
bmi.bmiHeader.biXPelsPerMeter = 0;
bmi.bmiHeader.biYPelsPerMeter = 0;
bmi.bmiHeader.biClrUsed = 0;
bmi.bmiHeader.biClrImportant = 0;
//写入到bitmap
StretchDIBits(_hdc, dstX, dstY, dstWidth, dstHeight, srcX, srcY, srcWidth, srcHeight, j->bufferFrame.Data, &bmi, 0, SRCCOPY);

3、获取HBitmap数据

合并图像后需要拿到内部的byte数据,我们可以通过GetObject获取到位图的信息里面就包括了byte数据指针。

BITMAP bmp;
//获取位图对象信息
GetObject(hBitmap, sizeof(BITMAP), &bmp);
//图像数据
int Data = (unsigned char*)bmp.bmBits;
//图像数据长度
int DataLength = bmp.bmWidthBytes * bmp.bmHeight * bmp.bmPlanes;
//一行数据长度
int Stride = bmp.bmWidthBytes;

二、封装对象

1、接口设计

设计一个视频流合并工具,接口如下:

#pragma once
#include"PixelFormat.h"
#include<map>
#include<shared_mutex>
#include<Windows.h>
/************************************************************************
* @Project:  	GdiVideoMerger
* @Decription:  Gdi多路视频合并工具
* 支持多路视频视频的剪切与合并
* 只支持bgra32和bgr24格式数据
* 出构造析构外,所有方法线程安全。
* @Verision:  	v1.0.0.0
* @Author:  	Xin Nie
* @Create:  	2022/6/6 22:46:00
* @LastUpdate:  2022/6/6 22:46:00
************************************************************************
* Copyright @ 2022. All rights reserved.
************************************************************************/
namespace AC {
	class  GdiVideoMerger 
	{
	public:
		/// <summary>
        /// 区域值的模式
        /// </summary>
		enum RectMode {
			/// <summary>
			/// 按实际计算
			/// </summary>
			TrueValue,
			/// <summary>
			/// 按比例计算
			/// </summary>
			RatioValue
		};
		/// <summary>
		/// 构造方法
		/// </summary>
		GdiVideoMerger();
		/// <summary>
        /// 析构方法
        /// </summary>
		~GdiVideoMerger();
		/// <summary>
		/// 设置输入流的区域
		/// 未设置的流默认为全屏
		/// 线程安全,可以与其他方法同时调用
		/// </summary>
		/// <param name="streamId">输入流id,由用户设置</param>
		/// <param name="dstX">在输出流中的x坐标</param>
		/// <param name="dstY">在输出流中的y坐标</param>
		/// <param name="dstWidth">在输出流中的宽</param>
		/// <param name="dstHeight">在输出流中的高</param>
		/// <param name="dstZIndex">在输出流中的层次</param>
		/// <param name="dstRectMode">区域(x,y,width,height)值的模式,按比例还是实际值</param>
		void SetStreamRect(int streamId,double dstX, double dstY, double dstWidth, double dstHeight, int dstZIndex, RectMode dstRectMode = RatioValue);
		/// <summary>
		/// 设置输入流的区域
		/// 未设置的流默认为全屏
	    /// 线程安全,可以与其他方法同时调用
		/// </summary>
		/// <param name="streamId">输入流id,由用户设置</param>
		/// <param name="srcX">输入流裁剪x坐标</param>
		/// <param name="srcY">输入流裁剪y坐标</param>
		/// <param name="srcWidth">输入流裁剪的宽</param>
		/// <param name="srcHeight">输入流裁剪的高</param>
		/// <param name="dstX">在输出流中的x坐标</param>
		/// <param name="dstY">在输出流中的y坐标</param>
		/// <param name="dstWidth">在输出流中的宽</param>
		/// <param name="dstHeight">在输出流中的高</param>
		/// <param name="dstZIndex">在输出流中的层次</param>
		/// <param name="dstRectMode">区域(x,y,width,height)值的模式,按比例还是实际值</param>
		void SetStreamRect(int streamId, double srcX, double srcY, double srcWidth, double srcHeight, double dstX, double dstY, double dstWidth, double dstHeight, int dstZIndex, RectMode dstRectMode = RatioValue);
		/// <summary>
		/// 写入输入流
        /// 线程安全,可以与其他方法同时调用
		/// </summary>
		/// <param name="streamId">输入流id,由用户设置</param>
		/// <param name="frame">流数据帧,PixelFormat只支持PIXELFORMAT_RGB32和PIXELFORMAT_RGB24</param>
		void Write(int streamId, VideoFrame*frame);
		/// <summary>
		/// 获取一帧合并流
		/// 可以在单独线程按照一定帧率调用此方法
		/// 也可以在某条输入流中调用此方法
		/// 调用后会锁住当前数据,使用完成后调用UnlockFrame解锁。
		/// 线程安全,可以与其他方法同时调用
		/// </summary>
		/// <returns>合并流帧</returns>
		VideoFrame* LockFrame();
		/// <summary>
		/// 解锁合并流帧
		/// 必须与LockFrame成对出现。
		/// 线程安全,可以与其他方法同时调用
		/// </summary>
		void UnlockFrame();
		/// <summary>
		/// 设置输出大小
		/// 线程安全,可以与其他方法同时调用
		/// </summary>
		/// <param name="width"></param>
		/// <param name="height"></param>
		void SetOutputSize(int width, int height);
		/// <summary>
		/// 设置输出格式
		/// 线程安全,可以与其他方法同时调用
		/// </summary>
		/// <param name="format">像素格式,只支持PIXELFORMAT_RGB32和PIXELFORMAT_RGB24</param>
		void SetOutputFormat(PixelFormat format);
	};
}

其他的必要的对象:

#pragma once
#include<stdint.h>
namespace AC {
	typedef enum
	{
		PIXELFORMAT_UNKNOWN = -1,
		PIXELFORMAT_BGR24 = 0,
		PIXELFORMAT_BGRA32,		
	}PixelFormat;
	class VideoFrame {
	public:
		AC::PixelFormat Format;
		uint8_t* Data;
		int DataLength;
		int Stride;
		int Width;
		int Height;
		int64_t Timestamp;
	};
}

2、完整代码

包含了完整代码和示例的vs项目。gdi合并多路视频流只能输入输出bgra32或bgr24,请根据需要下载。


三、使用示例

1、输入流中合并

int main(int argc, char** argv) {
	//创建窗口
	Win32Window window(L"多路流合并预览窗口");
	bool exitFlag = false;
	//流合并对象
	AC::GdiVideoMerger gm;
	//设置每条输入流参数
	gm.SetStreamRect(0, 0, 0, 0.25, 0.25, 2);
	gm.SetStreamRect(1, 360, 0, 960, 540, 1, AC::GdiVideoMerger::TrueValue);
	//设置输出流大小
	gm.SetOutputSize(1920, 1080);
	//设置输出流格式
	gm.SetOutputFormat(AC::PixelFormat::PIXELFORMAT_BGRA32);
	//启动截屏线程
	//输入流1
	std::thread t1([&]() {
		DesktopGrabber dg;
		while (!exitFlag)
		{
			//截屏
			dg.Grab();
			//填充数据
			AC::VideoFrame frame;
			frame.Data = dg.Data();
			frame.DataLength = dg.DataLength();
			frame.Format = AC::PixelFormat::PIXELFORMAT_BGRA32;
			frame.Stride = dg.Stride();
			frame.Width = dg.Width();
			frame.Height = dg.Height();
			//写入流
			gm.Write(0, &frame);
			//读取合并流,放在此输入流线程中合并,相当于以这条流帧率为基准进行合并。也可以放在独立的线程中,自定义帧率。
			auto t = clock();
			//读取当前合并帧
			auto oFrame = gm.LockFrame();
			printf("merge one frame cost time:%dms\n", clock() - t);
			//显示合并帧
			PresentBgra(oFrame->Format == AC::PIXELFORMAT_BGRA32 ? 32 : 24, window.Hwnd(), oFrame->Data, oFrame->Width, oFrame->Height, 0, 0, 640, 360);
			gm.UnlockFrame();
			Sleep(40);
		}
		});
	//输入流2
	std::thread t2([&]() {
		DesktopGrabber dg(24);
		while (!exitFlag)
		{
			//截屏
			dg.Grab();
			//填充数据
			AC::VideoFrame frame;
			frame.Data = dg.Data();
			frame.DataLength = dg.DataLength();
			frame.Format = AC::PixelFormat::PIXELFORMAT_BGR24;
			frame.Stride = dg.Stride();
			frame.Width = dg.Width();
			frame.Height = dg.Height();
			//写入流
			gm.Write(1, &frame);
			//动态修改参数-向右拉伸
			width += 0.01;
			gm.SetStreamRect(0, 0, 0, width, 0.25, 1);
			if (width > 1)
				width = 0.1;
			Sleep(30);
		}
		});
	//消息循环等待窗口关闭
	Win32Window::Exec();
	exitFlag = true;
	t1.join();
	t2.join();
	return 0;
}

2、单独线程合并

在单独线程中合并可以自定义帧率。下面示例只是简单的固定值调用Sleep,实际情况一般需要计算延时动态值调用Sleep以保持帧率稳定。

int main(int argc, char** argv) {
	//创建窗口
	Win32Window window(L"多路流合并预览窗口");
	bool exitFlag = false;
	//流合并对象
	AC::GdiVideoMerger gm;
	//设置每条输入流参数
	gm.SetStreamRect(0, 0, 0, 0.25, 0.25, 2);
	gm.SetStreamRect(1, 360, 0, 960, 540, 1, AC::GdiVideoMerger::TrueValue);
	//设置输出流大小
	gm.SetOutputSize(1920, 1080);
	//设置输出流格式
	gm.SetOutputFormat(AC::PixelFormat::PIXELFORMAT_BGRA32);
	//启动截屏线程
    //输入流1	
	std::thread t1([&]() {
		DesktopGrabber dg;
		while (!exitFlag)
		{
			//截屏
			dg.Grab();
			//填充数据
			AC::VideoFrame frame;
			frame.Data = dg.Data();
			frame.DataLength = dg.DataLength();
			frame.Format = AC::PixelFormat::PIXELFORMAT_BGRA32;
			frame.Stride = dg.Stride();
			frame.Width = dg.Width();
			frame.Height = dg.Height();
			//写入流
			gm.Write(0, &frame);			
			Sleep(40);
		}
		});
	//输入流2
	std::thread t2([&]() {
		DesktopGrabber dg(24);
		while (!exitFlag)
		{
			//截屏
			dg.Grab();
			//填充数据
			AC::VideoFrame frame;
			frame.Data = dg.Data();
			frame.DataLength = dg.DataLength();
			frame.Format = AC::PixelFormat::PIXELFORMAT_BGR24;
			frame.Stride = dg.Stride();
			frame.Width = dg.Width();
			frame.Height = dg.Height();
			//写入流
			gm.Write(1, &frame);
			//动态修改参数-向右拉伸
			width += 0.01;
			gm.SetStreamRect(0, 0, 0, width, 0.25, 1);
			if (width > 1)
				width = 0.1;
			Sleep(30);
		}
		});
	//单独线程合并
	AC::PixelFormat foramt = AC::PixelFormat::PIXELFORMAT_BGRA32;
	std::thread t7([&]() {
		DesktopGrabber dg;
		while (!exitFlag)
		{
			auto t = clock();
			//读取当前合并帧
			auto oFrame = gm.LockFrame();
			printf("merge one frame cost time:%dms\n", clock() - t);
			//显示合并帧
			PresentBgra(oFrame->Format == AC::PIXELFORMAT_BGRA32 ? 32 : 24, window.Hwnd(), oFrame->Data, oFrame->Width, oFrame->Height, 0, 0, 640, 360);
			gm.UnlockFrame();
			//按照一定的帧率合并
			Sleep(33);
		};
		});
	//消息循环等待窗口关闭
	Win32Window::Exec();
	exitFlag = true;
	t1.join();
	t2.join();
	t7.join();
	return 0;
}

3、效果预览

ios 视频流双流合并输出 视频合流实现_ios 视频流双流合并输出


四、性能测试

操作系统:win11
cpu:i7 8750
测试方法:30秒内取5次数据计算均值

输入流

输出流

合并一帧耗时

2路1080p

1080p

24.8ms

3路1080p

1080p

38.2ms

4路1080p

1080p

41.8ms


总结

以上就是今天要讲的内容,gdi是可以实现多路视频流合并的,对于1080p两路流的合并耗时是可以接受的, 但是仅支持bgra32和bgr24。但总的来说还算是一个可用方案,而且也为我们提供了一些思路,图像引擎可以用来做视频流合并。