文章目录
- 前言
- 一、如何实现?
- 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、效果预览
四、性能测试
操作系统: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。但总的来说还算是一个可用方案,而且也为我们提供了一些思路,图像引擎可以用来做视频流合并。