├── .editconfig ├── .gitignore ├── DSCamera ├── Camera │ ├── Camera.pri │ ├── CameraCommon.cpp │ ├── CameraCommon.h │ ├── CameraControl.cpp │ ├── CameraControl.h │ ├── CameraCore.cpp │ ├── CameraCore.h │ ├── CameraHotplug.cpp │ ├── CameraHotplug.h │ ├── CameraInfo.cpp │ ├── CameraInfo.h │ ├── CameraProbe.cpp │ ├── CameraProbe.h │ ├── CameraRegister.cpp │ ├── CameraRegister.h │ ├── CameraView.cpp │ ├── CameraView.h │ ├── ImageConverter.cpp │ ├── ImageConverter.h │ └── SampleGrabber.h ├── DSCamera.pro ├── Image │ ├── camera.ico │ ├── camera.png │ └── img.qrc ├── QML │ ├── Component │ │ └── ShadowRect.qml │ ├── Content │ │ ├── SettingArea.qml │ │ └── VideoArea.qml │ ├── main.qml │ └── qml.qrc └── main.cpp ├── LICENSE ├── QtUVCCamera.pro └── README.md /.editconfig: -------------------------------------------------------------------------------- 1 | #https://editorconfig.org/ 2 | root = true #所在目录是项目根目录,此目录及子目录下保存的文件都会生效 3 | 4 | [*] #匹配所有文件 5 | indent_style = tab #缩进风格 6 | tab_width = 4 #缩进宽度 7 | charset = utf-8 #文件编码 8 | end_of_line = crlf #行尾格式,win一般为CRLF,linux一般为LF 9 | insert_final_newline = true #文件尾添加换行符,以防警告 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | /bin_d 3 | 4 | .git 5 | .vs 6 | .vscode 7 | #.gitignore 8 | *.user 9 | .DS_Store 10 | -------------------------------------------------------------------------------- /DSCamera/Camera/Camera.pri: -------------------------------------------------------------------------------- 1 | HEADERS += \ 2 | $$PWD/CameraControl.h \ 3 | $$PWD/CameraCore.h \ 4 | $$PWD/CameraHotplug.h \ 5 | $$PWD/CameraInfo.h \ 6 | $$PWD/CameraProbe.h \ 7 | $$PWD/CameraRegister.h \ 8 | $$PWD/CameraCommon.h \ 9 | $$PWD/CameraView.h \ 10 | $$PWD/ImageConverter.h \ 11 | $$PWD/SampleGrabber.h 12 | 13 | SOURCES += \ 14 | $$PWD/CameraCommon.cpp \ 15 | $$PWD/CameraControl.cpp \ 16 | $$PWD/CameraCore.cpp \ 17 | $$PWD/CameraHotplug.cpp \ 18 | $$PWD/CameraInfo.cpp \ 19 | $$PWD/CameraProbe.cpp \ 20 | $$PWD/CameraRegister.cpp \ 21 | $$PWD/CameraView.cpp \ 22 | $$PWD/ImageConverter.cpp 23 | -------------------------------------------------------------------------------- /DSCamera/Camera/CameraCommon.cpp: -------------------------------------------------------------------------------- 1 | #include "CameraCommon.h" 2 | #include 3 | 4 | ULONG CameraCallback::AddRef() 5 | { 6 | return 1; 7 | } 8 | 9 | ULONG CameraCallback::Release() 10 | { 11 | return 1; 12 | } 13 | 14 | HRESULT CameraCallback::SampleCB(double time, IMediaSample *sample) 15 | { 16 | Q_UNUSED(time) 17 | Q_UNUSED(sample) 18 | return S_OK; 19 | } 20 | 21 | HRESULT CameraCallback::BufferCB(double time, BYTE *buffer, long len) 22 | { 23 | Q_UNUSED(time) 24 | if (!mRunning || !buffer || len <= 0) 25 | return S_OK; 26 | std::lock_guard guard(mMutex); 27 | Q_UNUSED(guard) 28 | // qDebug()<(this); 41 | return S_OK; 42 | } 43 | return E_NOINTERFACE; 44 | } 45 | 46 | void CameraCallback::start() 47 | { 48 | std::lock_guard guard(mMutex); 49 | Q_UNUSED(guard) 50 | mRunning = true; 51 | } 52 | 53 | void CameraCallback::stop() 54 | { 55 | mRunning = false; 56 | std::lock_guard guard(mMutex); 57 | Q_UNUSED(guard) 58 | } 59 | 60 | void CameraCallback::setCallback(const std::function &callback) 61 | { 62 | std::lock_guard guard(mMutex); 63 | Q_UNUSED(guard) 64 | mCallback = callback; 65 | } 66 | 67 | void CameraCallback::setSubtype(GUID subtype) 68 | { 69 | std::lock_guard guard(mMutex); 70 | Q_UNUSED(guard) 71 | mSubtype = subtype; 72 | mConverter = subtypeConverter(subtype); 73 | } 74 | 75 | void CameraCallback::setSize(int width, int height) 76 | { 77 | std::lock_guard guard(mMutex); 78 | Q_UNUSED(guard) 79 | mWidth = width; 80 | mHeight = height; 81 | } 82 | -------------------------------------------------------------------------------- /DSCamera/Camera/CameraCommon.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "SampleGrabber.h" 12 | #include "ImageConverter.h" 13 | 14 | #ifndef SAFE_RELEASE 15 | #define SAFE_RELEASE(ptr) { if (ptr) ptr->Release(); ptr = NULL; } 16 | #endif 17 | 18 | // 释放AM_MEDIA_TYPE 19 | inline void FreeMediaType(AM_MEDIA_TYPE &type) { 20 | if (type.cbFormat > 0) 21 | ::CoTaskMemFree(type.pbFormat); 22 | 23 | if (type.pUnk) 24 | type.pUnk->Release(); 25 | 26 | ::SecureZeroMemory(&type, sizeof(type)); 27 | } 28 | 29 | // 释放AM_MEDIA_TYPE* 30 | inline void DeleteMediaType(AM_MEDIA_TYPE *type) { 31 | if (!type) 32 | return; 33 | FreeMediaType(*type); 34 | ::CoTaskMemFree(type); 35 | } 36 | 37 | // 视频回调 38 | class CameraCallback : public ISampleGrabberCB 39 | { 40 | public: 41 | ULONG STDMETHODCALLTYPE AddRef() override; 42 | ULONG STDMETHODCALLTYPE Release() override; 43 | HRESULT STDMETHODCALLTYPE SampleCB(double time, IMediaSample *sample) override; 44 | HRESULT STDMETHODCALLTYPE BufferCB(double time, BYTE *buffer, long len) override; 45 | HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID *ppv) override; 46 | 47 | // 设置running状态,=true准备好接收图像,提前设置好参数再start 48 | void start(); 49 | // 设置running状态,=false时不处理图像 50 | void stop(); 51 | // 回调函数设置 52 | void setCallback(const std::function &callback); 53 | // 图像类型设置 54 | void setSubtype(GUID subtype); 55 | // 图像尺寸设置 56 | void setSize(int width, int height); 57 | 58 | private: 59 | // 给running加锁,防止正在接收图像时用到的参数被修改,强制同步 60 | std::mutex mMutex; 61 | // =false时不处理图像 62 | std::atomic mRunning{false}; 63 | // 处理好图像后通过回调传出 64 | std::function mCallback; 65 | // 图像数据类型 66 | GUID mSubtype{MEDIASUBTYPE_NULL}; 67 | // 数据类型对应的处理函数 68 | ImageConverter mConverter{convertEmpty}; 69 | // 图像尺寸 70 | int mWidth{0}; 71 | int mHeight{0}; 72 | }; 73 | -------------------------------------------------------------------------------- /DSCamera/Camera/CameraControl.cpp: -------------------------------------------------------------------------------- 1 | #include "CameraControl.h" 2 | #include 3 | #include 4 | 5 | CameraControl::CameraControl(QObject *parent) 6 | : QObject{parent} 7 | { 8 | // 视频预览回调 9 | auto preview_callback = [this](const QImage &img){ 10 | // static int i = 0; 11 | // qDebug()<<"grabber"<() << QUuid(0x65E8773DL, 0x8F56, 0x11D0, 0xA3, 0xB9, 0x00, 0xA0, 0xC9, 0x22, 0x31, 0x96)); 27 | // 插入设备刷新列表 28 | connect(&hotplug, &CameraHotplug::deviceAttached, this, [this](){ 29 | auto device_list = info.getDeviceList(); 30 | int index = getDeviceIndex(); 31 | if (index < 0 || index >= device_list.size()) { 32 | info.updateDeviceList(); 33 | if (info.getDeviceList().size() > 0) { 34 | selectDevice(0); 35 | } 36 | return; 37 | } 38 | CameraDevice device = device_list.at(index); 39 | // 更新设备列表 40 | info.updateDeviceList(); 41 | device_list = info.getDeviceList(); 42 | if (device_list.isEmpty()) 43 | return; 44 | int select = 0; 45 | for (int i = 0; i < device_list.size(); i++) 46 | { 47 | if (device.displayName.compare(device_list.at(i).displayName) == 0) { 48 | select = i; 49 | break; 50 | } 51 | } 52 | // TODO 检测之前是否拔出,不然每插一个都重新加载 53 | selectDevice(select); 54 | }, Qt::QueuedConnection); 55 | // 移除设备暂不处理 56 | // connect(&hotplug, &CameraHotplug::deviceDetached, this, [this](){}, Qt::QueuedConnection); 57 | 58 | // 初始化设备信息,默认选中一个设备 59 | info.updateDeviceList(); 60 | if (info.getDeviceList().size() > 0) { 61 | selectDevice(0); 62 | } 63 | } 64 | 65 | CameraControl::~CameraControl() 66 | { 67 | 68 | } 69 | 70 | CameraInfo *CameraControl::getInfo() 71 | { 72 | return &info; 73 | } 74 | 75 | CameraProbe *CameraControl::getProbe() 76 | { 77 | return &probe; 78 | } 79 | 80 | CameraHotplug *CameraControl::getHotplug() 81 | { 82 | return &hotplug; 83 | } 84 | 85 | int CameraControl::getState() const 86 | { 87 | return state; 88 | } 89 | 90 | void CameraControl::setState(int newState) 91 | { 92 | if (state != newState) { 93 | state = (CameraControl::CameraState)newState; 94 | emit stateChanged(state); 95 | } 96 | } 97 | 98 | int CameraControl::getDeviceIndex() const 99 | { 100 | return deviceIndex; 101 | } 102 | 103 | void CameraControl::setDeviceIndex(int index) 104 | { 105 | if (deviceIndex != index) { 106 | deviceIndex = index; 107 | emit deviceIndexChanged(); 108 | } 109 | } 110 | 111 | QSize CameraControl::getResolution() const 112 | { 113 | return QSize(core.getCurWidth(), core.getCurHeight()); 114 | } 115 | 116 | void CameraControl::attachView(CameraView *view) 117 | { 118 | if (!view) { 119 | return; 120 | } 121 | connect(this, &CameraControl::imageComing, view, &CameraView::updateImage, Qt::QueuedConnection); 122 | } 123 | 124 | bool CameraControl::selectDevice(int index) 125 | { 126 | auto device_list = info.getDeviceList(); 127 | if (index < 0 || index >= device_list.size()) 128 | return false; 129 | CameraDevice device = device_list.at(index); 130 | setDeviceIndex(index); 131 | probe.reset(); 132 | if (!core.openDevice(device)) { 133 | setState(Stopped); 134 | return false; 135 | } 136 | return play(); 137 | } 138 | 139 | bool CameraControl::setFormat(int width, int height) 140 | { 141 | return core.setFormat(width, height); 142 | } 143 | 144 | bool CameraControl::play() 145 | { 146 | if (!core.play()) { 147 | return false; 148 | } 149 | probe.reset(); 150 | setState(Playing); 151 | emit formatChanged(); 152 | return true; 153 | } 154 | 155 | bool CameraControl::pause() 156 | { 157 | if (!core.pause()) { 158 | return false; 159 | } 160 | setState(Paused); 161 | return true; 162 | } 163 | 164 | bool CameraControl::stop() 165 | { 166 | if (!core.stop()) { 167 | return false; 168 | } 169 | setState(Stopped); 170 | return true; 171 | } 172 | 173 | void CameraControl::popDeviceSetting(QQuickWindow *window) 174 | { 175 | if (getState() == Stopped) 176 | return; 177 | HWND winId = NULL; 178 | if (window) { 179 | winId = (HWND)window->winId(); 180 | } 181 | QMetaObject::invokeMethod(this, "deviceSetting", Qt::QueuedConnection, Q_ARG(HWND, winId)); 182 | } 183 | 184 | void CameraControl::popFormatSetting(QQuickWindow *window) 185 | { 186 | if (getState() == Stopped) 187 | return; 188 | HWND winId = NULL; 189 | if (window) { 190 | winId = (HWND)window->winId(); 191 | } 192 | QMetaObject::invokeMethod(this, "formatSetting", Qt::QueuedConnection, Q_ARG(HWND, winId)); 193 | } 194 | 195 | void CameraControl::deviceSetting(HWND winId) 196 | { 197 | core.deviceSetting(winId); 198 | } 199 | 200 | void CameraControl::formatSetting(HWND winId) 201 | { 202 | stop(); 203 | core.formatSetting(winId); 204 | selectDevice(getDeviceIndex()); 205 | } 206 | -------------------------------------------------------------------------------- /DSCamera/Camera/CameraControl.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include "CameraCore.h" 5 | #include "CameraInfo.h" 6 | #include "CameraProbe.h" 7 | #include "CameraHotplug.h" 8 | #include "CameraView.h" 9 | 10 | // 相机操作 11 | class CameraControl : public QObject 12 | { 13 | Q_OBJECT 14 | Q_PROPERTY(CameraInfo* info READ getInfo CONSTANT) 15 | Q_PROPERTY(CameraProbe* probe READ getProbe CONSTANT) 16 | Q_PROPERTY(CameraHotplug* hotplug READ getHotplug CONSTANT) 17 | Q_PROPERTY(int state READ getState NOTIFY stateChanged) 18 | Q_PROPERTY(int deviceIndex READ getDeviceIndex NOTIFY deviceIndexChanged) 19 | Q_PROPERTY(QSize resolution READ getResolution NOTIFY formatChanged) 20 | public: 21 | // 相机工作状态 22 | enum CameraState 23 | { 24 | // 默认停止状态 25 | Stopped, 26 | // 正在播放 27 | Playing, 28 | // 已暂停 29 | Paused 30 | }; 31 | Q_ENUM(CameraState) 32 | public: 33 | explicit CameraControl(QObject *parent = nullptr); 34 | ~CameraControl(); 35 | 36 | // 设备信息 37 | CameraInfo *getInfo(); 38 | // 拍图和录制 39 | CameraProbe *getProbe(); 40 | // 热插拔 41 | CameraHotplug *getHotplug(); 42 | 43 | // 相机工作状态 44 | int getState() const; 45 | void setState(int newState); 46 | 47 | // 当前设备选择 48 | int getDeviceIndex() const; 49 | void setDeviceIndex(int index); 50 | 51 | // 分辨率 52 | QSize getResolution() const; 53 | 54 | // 关联视频显示 55 | Q_INVOKABLE void attachView(CameraView *view); 56 | // 根据setting的设备列表下标打开某个设备,从0开始 57 | Q_INVOKABLE bool selectDevice(int index); 58 | // 设置分辨率等 59 | Q_INVOKABLE bool setFormat(int width, int height); 60 | // 播放 61 | Q_INVOKABLE bool play(); 62 | // 暂停 63 | Q_INVOKABLE bool pause(); 64 | // 停止 65 | Q_INVOKABLE bool stop(); 66 | // 设备设置,指定父窗口时模态显示 67 | Q_INVOKABLE void popDeviceSetting(QQuickWindow *window = nullptr); 68 | // 格式设置,指定父窗口时模态显示 69 | Q_INVOKABLE void popFormatSetting(QQuickWindow *window = nullptr); 70 | 71 | private: 72 | // 弹出directshow的设备设置,指定父窗口时模态显示 73 | Q_INVOKABLE void deviceSetting(HWND winId); 74 | // 弹出directshow的格式设置,指定父窗口时模态显示 75 | Q_INVOKABLE void formatSetting(HWND winId); 76 | 77 | signals: 78 | // 新的图像到来 79 | void imageComing(const QImage &img); 80 | // 播放/停止状态变化 81 | void stateChanged(int newState); 82 | // 设备选择 83 | void deviceIndexChanged(); 84 | // 格式设置 85 | void formatChanged(); 86 | 87 | private: 88 | // 底层接口操作 89 | CameraCore core; 90 | // 设备信息 91 | CameraInfo info; 92 | // 拍图和录制 93 | CameraProbe probe; 94 | // 热插拔检测 95 | CameraHotplug hotplug; 96 | // 当前播放状态 97 | CameraState state{Stopped}; 98 | // 设备选择序号 99 | int deviceIndex{-1}; 100 | }; 101 | -------------------------------------------------------------------------------- /DSCamera/Camera/CameraCore.cpp: -------------------------------------------------------------------------------- 1 | #include "CameraCore.h" 2 | #include 3 | #include 4 | 5 | CameraCore::CameraCore() 6 | { 7 | } 8 | 9 | CameraCore::~CameraCore() 10 | { 11 | stop(); 12 | releaseGraph(); 13 | } 14 | 15 | int CameraCore::getCurWidth() const 16 | { 17 | return mSetting.width; 18 | } 19 | 20 | int CameraCore::getCurHeight() const 21 | { 22 | return mSetting.height; 23 | } 24 | 25 | void CameraCore::setCallback(const std::function &previewCB, 26 | const std::function &stillCB) 27 | { 28 | mPreviewCallback.setCallback(previewCB); 29 | mStillCallback.setCallback(stillCB); 30 | } 31 | 32 | bool CameraCore::openDevice(const CameraDevice &device) 33 | { 34 | // 释放当前 35 | releaseGraph(); 36 | mDevice = device; 37 | 38 | HRESULT hr = S_FALSE; 39 | // 创建Filter Graph Manager. 40 | hr = ::CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER, IID_IGraphBuilder, reinterpret_cast(&mGraph)); 41 | if (FAILED(hr)) 42 | return false; 43 | 44 | // 创建Capture Graph Builder. 45 | hr = ::CoCreateInstance(CLSID_CaptureGraphBuilder2, NULL, CLSCTX_INPROC, IID_ICaptureGraphBuilder2, reinterpret_cast(&mBuilder)); 46 | if (FAILED(hr)) 47 | return false; 48 | mBuilder->SetFiltergraph(mGraph); 49 | 50 | // IMediaControl接口,用来控制流媒体在Filter Graph中的流动,例如流媒体的启动和停止; 51 | hr = mGraph->QueryInterface(IID_IMediaControl, reinterpret_cast(&mMediaControl)); 52 | if (FAILED(hr)) 53 | return false; 54 | 55 | // 创建用于预览的Sample Grabber Filter. 56 | hr = ::CoCreateInstance(CLSID_SampleGrabber, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter, reinterpret_cast(&mPreviewFilter)); 57 | if (FAILED(hr)) 58 | return false; 59 | 60 | // 获取ISampleGrabber接口,用于设置回调等相关信息 61 | hr = mPreviewFilter->QueryInterface(IID_ISampleGrabber, reinterpret_cast(&mPreviewGrabber)); 62 | if (FAILED(hr)) 63 | return false; 64 | 65 | // 创建用于抓拍的Sample Grabber Filter. 66 | hr = ::CoCreateInstance(CLSID_SampleGrabber, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter, reinterpret_cast(&mStillFilter)); 67 | if (FAILED(hr)) 68 | return false; 69 | 70 | // 获取ISampleGrabber接口,用于设置回调等相关信息 71 | hr = mStillFilter->QueryInterface(IID_ISampleGrabber, reinterpret_cast(&mStillGrabber)); 72 | if (FAILED(hr)) 73 | return false; 74 | 75 | if (!bindFilter(device.displayName)) 76 | return false; 77 | 78 | hr = mGraph->AddFilter(mSourceFilter, L"Source Filter"); 79 | if (FAILED(hr)) 80 | return false; 81 | hr = mGraph->AddFilter(mPreviewFilter, L"Preview Filter"); 82 | if (FAILED(hr)) 83 | return false; 84 | hr = mGraph->AddFilter(mStillFilter, L"Still Filter"); 85 | if (FAILED(hr)) 86 | return false; 87 | 88 | GUID sub_type = MEDIASUBTYPE_NULL; 89 | if (mSetting.valid) { 90 | sub_type = mSetting.type; 91 | } else { 92 | getType(sub_type); 93 | } 94 | if (!isValidSubtype(sub_type)) { 95 | sub_type = MEDIASUBTYPE_RGB32; 96 | } 97 | // 先设置一下后面才能生效 98 | AM_MEDIA_TYPE amt = {0}; 99 | amt.majortype = MEDIATYPE_Video; 100 | amt.subtype = sub_type; 101 | amt.formattype = FORMAT_VideoInfo; 102 | hr = mPreviewGrabber->SetMediaType(&amt); 103 | if (FAILED(hr)) 104 | return false; 105 | 106 | // 设置分辨率 107 | if (mSetting.valid) { 108 | setFormat(mSetting.width, mSetting.height, mSetting.avgTime, mSetting.type); 109 | mSetting.valid = false; 110 | } else { 111 | // 初始化格式 112 | setFormat(0, 0, 0, sub_type); 113 | } 114 | 115 | if (mState.recording) { 116 | IBaseFilter *mux = NULL; 117 | // 设置输出视频文件位置 118 | wchar_t path[MAX_PATH] = {0}; 119 | mState.recordPath.toWCharArray(path); 120 | hr = mBuilder->SetOutputFileName(&MEDIASUBTYPE_Avi, path, &mux, NULL); 121 | if (FAILED(hr)) 122 | return false; 123 | // RenderStream最后一个参数为空会弹出activemovie窗口显示预览视频 124 | hr = mBuilder->RenderStream(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, 125 | mSourceFilter, mPreviewFilter, mux); 126 | if (FAILED(hr)) 127 | return false; 128 | // 参考别人的代码,用完直接release 129 | mux->Release(); 130 | } else { 131 | // RenderStream最后一个参数为空会弹出activemovie窗口显示预览视频 132 | hr = mBuilder->RenderStream(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, 133 | mSourceFilter, NULL, mPreviewFilter); 134 | if (FAILED(hr)) 135 | return false; 136 | } 137 | 138 | mPreviewGrabber->SetOneShot(false); 139 | mPreviewGrabber->SetBufferSamples(false); 140 | // 回调函数 0-调用SampleCB 1-BufferCB 141 | hr = mPreviewGrabber->SetCallback(&mPreviewCallback, 1); 142 | if (FAILED(hr)) 143 | return false; 144 | // 设置格式 145 | // 其他的原生格式解析可以参考: 146 | // https://github.com/GoodRon/QtWebcam 147 | // 衍生版本:https://gitee.com/fsfzp888/UVCCapture 148 | // amt.subtype = MEDIASUBTYPE_MJPG; 149 | hr = mPreviewGrabber->SetMediaType(&amt); 150 | if (FAILED(hr)) 151 | return false; 152 | 153 | mStillGrabber->SetOneShot(false); 154 | mStillGrabber->SetBufferSamples(true); 155 | // 回调函数 0-调用SampleCB 1-BufferCB 156 | hr = mStillGrabber->SetCallback(&mStillCallback, 1); 157 | if (FAILED(hr)) 158 | return false; 159 | hr = mStillGrabber->SetMediaType(&amt); 160 | if (FAILED(hr)) 161 | return false; 162 | 163 | // still render可能失败,但是不return false 164 | hr = mBuilder->RenderStream(&PIN_CATEGORY_STILL, &MEDIATYPE_Video, 165 | mSourceFilter, NULL, mStillFilter); 166 | if (FAILED(hr)) { 167 | // 失败后,stillpin可能持续触发,比如yuy2转rgb时 168 | // 所以这里取消掉still的回调 169 | mStillGrabber->SetCallback(NULL, 1); 170 | } 171 | 172 | return true; 173 | } 174 | 175 | bool CameraCore::getType(GUID &type) 176 | { 177 | if (!mSourceFilter || !mBuilder) { 178 | return false; 179 | } 180 | 181 | HRESULT hr = S_FALSE; 182 | IAMStreamConfig *stream_config = NULL; 183 | hr = mBuilder->FindInterface(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Interleaved, mSourceFilter, 184 | IID_IAMStreamConfig, reinterpret_cast(&stream_config)); 185 | if (FAILED(hr) || !stream_config) { 186 | hr = mBuilder->FindInterface(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, mSourceFilter, 187 | IID_IAMStreamConfig, reinterpret_cast(&stream_config)); 188 | } 189 | if (FAILED(hr) || !stream_config) { 190 | return false; 191 | } 192 | 193 | AM_MEDIA_TYPE *pamt = NULL; 194 | hr = stream_config->GetFormat(&pamt); 195 | bool ret = false; 196 | // 设置分辨率,如果无效的话似乎会使用默认值 197 | if (SUCCEEDED(hr) && pamt && pamt->pbFormat) { 198 | type = pamt->subtype; 199 | ret = true; 200 | } 201 | DeleteMediaType(pamt); 202 | SAFE_RELEASE(stream_config); 203 | return ret; 204 | } 205 | 206 | bool CameraCore::setFormat(int width, int height, LONGLONG avgTime, GUID type) 207 | { 208 | qDebug()<<__FUNCTION__<<"call"<FindInterface(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Interleaved, mSourceFilter, 216 | IID_IAMStreamConfig, reinterpret_cast(&stream_config)); 217 | if (FAILED(hr) || !stream_config) { 218 | hr = mBuilder->FindInterface(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, mSourceFilter, 219 | IID_IAMStreamConfig, reinterpret_cast(&stream_config)); 220 | } 221 | if (FAILED(hr) || !stream_config) { 222 | return false; 223 | } 224 | int cap_count; 225 | int cap_size; 226 | VIDEO_STREAM_CONFIG_CAPS caps; 227 | hr = stream_config->GetNumberOfCapabilities(&cap_count, &cap_size); 228 | if (FAILED(hr) || sizeof(caps) != cap_size) 229 | { 230 | SAFE_RELEASE(stream_config); 231 | return false; 232 | } 233 | bool ret = false; 234 | for (int i = 0; i < cap_count && !ret; i++) 235 | { 236 | AM_MEDIA_TYPE *pamt = NULL; 237 | hr = stream_config->GetStreamCaps(i, &pamt, (BYTE*)(&caps)); 238 | if (FAILED(hr) || !pamt) { 239 | continue; 240 | } 241 | // 没设置类型时就用第一个类型 242 | if (type == MEDIASUBTYPE_NULL) { 243 | type = pamt->subtype; 244 | } 245 | if (pamt->formattype == FORMAT_VideoInfo && pamt->subtype == type) { 246 | VIDEOINFOHEADER *vih = reinterpret_cast(pamt->pbFormat); 247 | if (width > 0 && height > 0) { 248 | vih->bmiHeader.biWidth = width; 249 | vih->bmiHeader.biHeight = height; 250 | } else { 251 | width = vih->bmiHeader.biWidth; 252 | height = vih->bmiHeader.biHeight; 253 | } 254 | if (avgTime > 0) { 255 | // int fps = qRound(10000000.0 / pvi->AvgTimePerFrame); 256 | vih->AvgTimePerFrame = avgTime; 257 | } else { 258 | avgTime = vih->AvgTimePerFrame; 259 | } 260 | hr = stream_config->SetFormat(pamt); 261 | if (SUCCEEDED(hr)) { 262 | ret = true; 263 | qDebug()<<__FUNCTION__<<"result"<GetFormat(&pamt); 274 | // 设置分辨率,如果无效会使用默认值 275 | if (SUCCEEDED(hr) && pamt && pamt->pbFormat) { 276 | VIDEOINFOHEADER *vih = reinterpret_cast(pamt->pbFormat); 277 | if (width > 0 && height > 0) { 278 | vih->bmiHeader.biWidth = width; 279 | vih->bmiHeader.biHeight = height; 280 | } else { 281 | width = vih->bmiHeader.biWidth; 282 | height = vih->bmiHeader.biHeight; 283 | } 284 | if (avgTime > 0) { 285 | // int fps = qRound(10000000.0 / pvi->AvgTimePerFrame); 286 | vih->AvgTimePerFrame = avgTime; 287 | } else { 288 | avgTime = vih->AvgTimePerFrame; 289 | } 290 | // 这里可以判断原格式,不能设置 291 | hr = stream_config->SetFormat(pamt); 292 | if (SUCCEEDED(hr)) { 293 | ret = true; 294 | } 295 | qDebug()<<__FUNCTION__<<"other"<subtype; 296 | } 297 | DeleteMediaType(pamt); 298 | } 299 | SAFE_RELEASE(stream_config); 300 | if (ret) { 301 | mSetting.width = width; 302 | mSetting.height = height; 303 | } 304 | return ret; 305 | } 306 | 307 | bool CameraCore::play() 308 | { 309 | if (!mGraph || !mMediaControl || !mPreviewGrabber) 310 | return false; 311 | 312 | if (FAILED(mMediaControl->Run())) 313 | return false; 314 | 315 | AM_MEDIA_TYPE amt = {0}; 316 | HRESULT hr = mPreviewGrabber->GetConnectedMediaType(&amt); 317 | if (FAILED(hr)) 318 | return false; 319 | VIDEOINFOHEADER *vih = reinterpret_cast(amt.pbFormat); 320 | if (!vih) 321 | return false; 322 | 323 | int width = vih->bmiHeader.biWidth; 324 | int height = vih->bmiHeader.biHeight; 325 | LONGLONG avg_time = vih->AvgTimePerFrame; 326 | GUID sub_type = amt.subtype; 327 | 328 | mPreviewCallback.setSize(width, height); 329 | mPreviewCallback.setSubtype(sub_type); 330 | mPreviewCallback.start(); 331 | qDebug()<<__FUNCTION__<<"preview"<GetConnectedMediaType(&amt))) { 336 | // 可能 StillPin 的格式没有被设置成功 337 | VIDEOINFOHEADER *vih = reinterpret_cast(amt.pbFormat); 338 | if (vih) { 339 | mStillCallback.setSize(vih->bmiHeader.biWidth, vih->bmiHeader.biHeight); 340 | mStillCallback.setSubtype(amt.subtype); 341 | qDebug()<<__FUNCTION__<<"still"<bmiHeader.biWidth<bmiHeader.biHeight<Pause()); 359 | } 360 | return false; 361 | } 362 | 363 | bool CameraCore::stop() 364 | { 365 | if (mGraph && mMediaControl) { 366 | mPreviewCallback.stop(); 367 | mStillCallback.stop(); 368 | mState.running = false; 369 | return SUCCEEDED(mMediaControl->Stop()); 370 | } 371 | return false; 372 | } 373 | 374 | bool CameraCore::startRecord(const QString &savePath) 375 | { 376 | if (!mState.running) { 377 | return false; 378 | } 379 | // 暂时在core中调用 380 | // TODO 目前录制会卡顿,即便不解析图片数据 381 | stop(); 382 | mState.recording = true; 383 | mState.recordPath = savePath; 384 | // 打开时保持原来的size 385 | mSetting.valid = true; 386 | openDevice(mDevice); 387 | play(); 388 | return true; 389 | } 390 | 391 | bool CameraCore::stopRecord() 392 | { 393 | if (!mState.running) { 394 | return false; 395 | } 396 | mState.recording = false; 397 | mSetting.valid = true; 398 | openDevice(mDevice); 399 | play(); 400 | return true; 401 | } 402 | 403 | void CameraCore::releaseGraph() 404 | { 405 | mPreviewCallback.stop(); 406 | mStillCallback.stop(); 407 | if (mMediaControl) { 408 | mMediaControl->Stop(); 409 | } 410 | SAFE_RELEASE(mMediaControl); 411 | if (mGraph) { 412 | if (mSourceFilter) { 413 | freePin(mGraph, mSourceFilter); 414 | mGraph->RemoveFilter(mSourceFilter); 415 | } 416 | if (mPreviewFilter) { 417 | freePin(mGraph, mPreviewFilter); 418 | mGraph->RemoveFilter(mPreviewFilter); 419 | } 420 | if (mStillFilter) { 421 | freePin(mGraph, mStillFilter); 422 | mGraph->RemoveFilter(mStillFilter); 423 | } 424 | } 425 | SAFE_RELEASE(mSourceFilter); 426 | SAFE_RELEASE(mStillFilter); 427 | SAFE_RELEASE(mPreviewFilter); 428 | SAFE_RELEASE(mStillGrabber); 429 | SAFE_RELEASE(mPreviewGrabber); 430 | SAFE_RELEASE(mBuilder); 431 | SAFE_RELEASE(mGraph); 432 | } 433 | 434 | bool CameraCore::bindFilter(const QString &deviceName) 435 | { 436 | HRESULT hr = S_FALSE; 437 | 438 | // 调用 CoCreateInstance 以创建系统设备枚举器的实例 439 | ICreateDevEnum *devce_enum = NULL; 440 | hr = ::CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER, 441 | IID_ICreateDevEnum, reinterpret_cast(&devce_enum)); 442 | if (FAILED(hr)) { 443 | return false; 444 | } 445 | 446 | // 2.调用 ICreateDevEnum::CreateClassEnumerator,并将设备类别指定为 GUID 447 | IEnumMoniker *enum_moniker = NULL; 448 | hr = devce_enum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &enum_moniker, 0); 449 | if (FAILED(hr)) { 450 | SAFE_RELEASE(devce_enum); 451 | return false; 452 | } 453 | enum_moniker->Reset(); 454 | 455 | // CreateClassEnumerator 方法返回指向 IEnumMoniker 接口的指针 456 | // 若要枚举名字对象,请调用 IEnumMoniker::Next。 457 | IMoniker *moniker = NULL; 458 | IMalloc *malloc_interface = NULL; 459 | ::CoGetMalloc(1, reinterpret_cast(&malloc_interface)); 460 | while (enum_moniker->Next(1, &moniker, NULL) == S_OK) 461 | { 462 | BSTR name_str = NULL; 463 | hr = moniker->GetDisplayName(NULL, NULL, &name_str); 464 | if (FAILED(hr)) { 465 | SAFE_RELEASE(moniker); 466 | continue; 467 | } 468 | // "@device:pnp:\\\\?\\usb#vid_04ca&pid_7070&mi_00#6&16c57194&0&0000#{65e8773d-8f56-11d0-a3b9-00a0c9223196}\\global" 469 | QString display_name = QString::fromWCharArray(name_str); 470 | malloc_interface->Free(name_str); 471 | // qDebug()<<"displayname"<BindToObject(NULL, NULL, IID_IBaseFilter, 479 | reinterpret_cast(&mSourceFilter)); 480 | if (SUCCEEDED(hr)) { 481 | SAFE_RELEASE(moniker); 482 | break; 483 | } 484 | } 485 | SAFE_RELEASE(moniker); 486 | } 487 | 488 | SAFE_RELEASE(malloc_interface); 489 | SAFE_RELEASE(moniker); 490 | SAFE_RELEASE(enum_moniker); 491 | SAFE_RELEASE(devce_enum); 492 | return !!mSourceFilter; 493 | } 494 | 495 | void CameraCore::freePin(IGraphBuilder *inGraph, IBaseFilter *inFilter) const 496 | { 497 | if (!inGraph || !inFilter) 498 | return; 499 | 500 | IEnumPins *pin_enum = NULL; 501 | 502 | // 创建一个Pin的枚举器 503 | if (SUCCEEDED(inFilter->EnumPins(&pin_enum))) 504 | { 505 | pin_enum->Reset(); 506 | 507 | IPin *pin = NULL; 508 | ULONG fetched = 0; 509 | // 枚举该Filter上所有的Pin 510 | while (SUCCEEDED(pin_enum->Next(1, &pin, &fetched)) && fetched) 511 | { 512 | if (pin) 513 | { 514 | // 得到当前Pin连接对象的Pin指针 515 | IPin *connected_pin = NULL; 516 | pin->ConnectedTo(&connected_pin); 517 | if (connected_pin) 518 | { 519 | // 查询Pin信息(获取Pin的方向) 520 | PIN_INFO pin_info; 521 | if (SUCCEEDED(connected_pin->QueryPinInfo(&pin_info))) 522 | { 523 | pin_info.pFilter->Release(); 524 | if (pin_info.dir == PINDIR_INPUT) 525 | { 526 | // 如果连接对方是输入Pin(说明当前枚举得到的是输出Pin) 527 | // 则递归调用NukeDownstream函数,首先将下一级(乃至再下一级) 528 | // 的所有Filter删除 529 | freePin(inGraph, pin_info.pFilter); 530 | inGraph->Disconnect(connected_pin); 531 | inGraph->Disconnect(pin); 532 | inGraph->RemoveFilter(pin_info.pFilter); 533 | } 534 | } 535 | connected_pin->Release(); 536 | } 537 | pin->Release(); 538 | } 539 | } 540 | pin_enum->Release(); 541 | } 542 | } 543 | 544 | bool CameraCore::deviceSetting(HWND winId) 545 | { 546 | if (!mSourceFilter) 547 | return false; 548 | 549 | ISpecifyPropertyPages *prop_pages = NULL; 550 | if (S_OK == mSourceFilter->QueryInterface(IID_ISpecifyPropertyPages, reinterpret_cast(&prop_pages))) 551 | { 552 | CAUUID cauuid; 553 | prop_pages->GetPages(&cauuid); 554 | ::OleCreatePropertyFrame(winId, 30, 30, NULL, 1, 555 | reinterpret_cast(&mSourceFilter), 556 | cauuid.cElems, 557 | reinterpret_cast(cauuid.pElems), 558 | 0, 0, NULL); 559 | ::CoTaskMemFree(cauuid.pElems); 560 | prop_pages->Release(); 561 | return true; 562 | } 563 | return false; 564 | } 565 | 566 | bool CameraCore::formatSetting(HWND winId) 567 | { 568 | if (!mGraph || !mSourceFilter) 569 | return false;; 570 | 571 | HRESULT hr = S_FALSE; 572 | IAMStreamConfig *stream_config = NULL; 573 | hr = mBuilder->FindInterface(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Interleaved, mSourceFilter, 574 | IID_IAMStreamConfig, reinterpret_cast(&stream_config)); 575 | if (FAILED(hr) || !stream_config) { 576 | hr = mBuilder->FindInterface(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, mSourceFilter, 577 | IID_IAMStreamConfig, reinterpret_cast(&stream_config)); 578 | } 579 | if (FAILED(hr) || !stream_config) { 580 | return false; 581 | } 582 | 583 | ISpecifyPropertyPages *prop_pages = NULL; 584 | CAUUID cauuid; 585 | bool ret = false; 586 | if (S_OK == stream_config->QueryInterface(IID_ISpecifyPropertyPages, reinterpret_cast(&prop_pages))) 587 | { 588 | // 先停止,这一步放到了调用CameraCore::formatSetting前,重新打开放到了调用后 589 | // 这里不断开filter有的设备没法设置 590 | freePin(mGraph, mSourceFilter); 591 | 592 | prop_pages->GetPages(&cauuid); 593 | ::OleCreatePropertyFrame(winId, 30, 30, NULL, 1, 594 | reinterpret_cast(&stream_config), 595 | cauuid.cElems, 596 | reinterpret_cast(cauuid.pElems), 597 | 0, 0, NULL); 598 | ::CoTaskMemFree(cauuid.pElems); 599 | prop_pages->Release(); 600 | 601 | AM_MEDIA_TYPE *pamt = NULL; 602 | if (NOERROR == stream_config->GetFormat(&pamt)) 603 | { 604 | if (pamt->formattype == FORMAT_VideoInfo && pamt->majortype == MEDIATYPE_Video) 605 | { 606 | VIDEOINFOHEADER *vih = reinterpret_cast(pamt->pbFormat); 607 | int width = vih->bmiHeader.biWidth; 608 | int height = vih->bmiHeader.biHeight; 609 | LONGLONG avg_time = vih->AvgTimePerFrame; 610 | GUID sub_type = pamt->subtype; 611 | // 稍后会调用 openDevice 和 setFormat 函数 612 | if (!isValidSubtype(sub_type)) { 613 | sub_type = MEDIASUBTYPE_RGB32; 614 | } 615 | 616 | mSetting.width = width; 617 | mSetting.height = height; 618 | mSetting.avgTime = avg_time; 619 | mSetting.type = sub_type; 620 | mSetting.valid = true; 621 | qDebug()<<__FUNCTION__<Release(); 628 | return ret; 629 | } 630 | -------------------------------------------------------------------------------- /DSCamera/Camera/CameraCore.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "CameraCommon.h" 4 | #include "CameraInfo.h" 5 | 6 | // 封装DirectShow操作 7 | class CameraCore 8 | { 9 | public: 10 | explicit CameraCore(); 11 | ~CameraCore(); 12 | 13 | // 获取当前图像宽度 14 | int getCurWidth() const; 15 | // 获取当前图像高度 16 | int getCurHeight() const; 17 | 18 | // 设置回调 19 | void setCallback(const std::function &previewCB, 20 | const std::function &stillCB); 21 | 22 | // 打开设备,open成功后调用play 23 | bool openDevice(const CameraDevice &device); 24 | // 查询格式 25 | bool getType(GUID &type); 26 | // 设置分辨率、帧率等格式,编码目前固定 27 | bool setFormat(int width, int height, LONGLONG avgTime = 333333, GUID type = MEDIASUBTYPE_NULL); 28 | 29 | // 播放 30 | bool play(); 31 | // 暂停 32 | bool pause(); 33 | // 停止 34 | bool stop(); 35 | // 开始录制 36 | bool startRecord(const QString &savePath); 37 | // 结束录制 38 | bool stopRecord(); 39 | 40 | // 弹出directshow的设备设置,指定父窗口时模态显示 41 | bool deviceSetting(HWND winId); 42 | // 弹出directshow的格式设置,指定父窗口时模态显示 43 | bool formatSetting(HWND winId); 44 | 45 | private: 46 | void releaseGraph(); 47 | bool bindFilter(const QString &deviceName); 48 | // 删除与该Filter连接的下游的所有Filter 49 | void freePin(IGraphBuilder *inGraph, IBaseFilter *inFilter) const; 50 | 51 | private: 52 | // DirectShow 53 | ICaptureGraphBuilder2 *mBuilder{NULL}; 54 | IGraphBuilder *mGraph{NULL}; 55 | IMediaControl *mMediaControl{NULL}; 56 | 57 | // 视频流 58 | IBaseFilter *mSourceFilter{NULL}; 59 | ISampleGrabber *mPreviewGrabber{NULL}; 60 | IBaseFilter *mPreviewFilter{NULL}; 61 | CameraCallback mPreviewCallback; 62 | 63 | // still pin 拍照 64 | ISampleGrabber *mStillGrabber{NULL}; 65 | IBaseFilter *mStillFilter{NULL}; 66 | CameraCallback mStillCallback; 67 | 68 | // 当前open的设备信息 69 | CameraDevice mDevice; 70 | 71 | // selectDevice 时应用之前的设置 72 | struct { 73 | // valid=true保存了参数设置,打开设备时进行设置 74 | bool valid{false}; 75 | // 尺寸 76 | int width{0}; 77 | int height{0}; 78 | // 多久一帧,单位100ns纳秒,如果1秒30帧,就是0.0333333秒一帧 79 | // 换算成100ns单位就是0.0333333 * 1000 * 1000 * 10 = 333333 80 | LONGLONG avgTime = 333333; 81 | // 格式类型,JPG或其他 82 | GUID type; 83 | } mSetting; 84 | 85 | // 操作状态 86 | struct { 87 | // 开关 88 | bool running{false}; 89 | // 录制 90 | bool recording{false}; 91 | // 录制路径 92 | QString recordPath; 93 | } mState; 94 | }; 95 | -------------------------------------------------------------------------------- /DSCamera/Camera/CameraHotplug.cpp: -------------------------------------------------------------------------------- 1 | #include "CameraHotplug.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #pragma comment(lib, "user32.lib") 7 | 8 | // 内部结构,存储对应平台需要的数据 9 | class CameraHotplugPrivate 10 | { 11 | public: 12 | void deviceAttached(quint16 vid, quint16 pid) { 13 | QMetaObject::invokeMethod(ptr, "deviceAttached", Qt::QueuedConnection, 14 | Q_ARG(quint16, vid), 15 | Q_ARG(quint16, pid)); 16 | } 17 | void deviceDetached(quint16 vid, quint16 pid) { 18 | QMetaObject::invokeMethod(ptr, "deviceDetached", Qt::QueuedConnection, 19 | Q_ARG(quint16, vid), 20 | Q_ARG(quint16, pid)); 21 | } 22 | // 处理窗口消息 23 | static LRESULT CALLBACK windowMessageProcess(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam); 24 | // win32 窗口类名 25 | static QString windowClassName(); 26 | // 创建一个用于接收消息的窗口,注册消息回调 27 | bool createMessageWindow(const QVector &uuids); 28 | // 释放 29 | void destroyMessageWindow(); 30 | 31 | // 关联的对象 32 | CameraHotplug *ptr{nullptr}; 33 | // 关联的窗口 34 | HWND hwnd{nullptr}; 35 | // 设备通知句柄和 UUID/GUID 36 | QHash devNotifys; 37 | }; 38 | 39 | // 处理窗口消息 40 | LRESULT CALLBACK CameraHotplugPrivate::windowMessageProcess(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) 41 | { 42 | if (message == WM_DEVICECHANGE) { 43 | do { 44 | // 设备可用事件 45 | const bool is_add = wParam == DBT_DEVICEARRIVAL; 46 | // 设备移除事件 47 | const bool is_remove = wParam == DBT_DEVICEREMOVECOMPLETE; 48 | if (!is_add && !is_remove) 49 | break; 50 | // 过滤 device interface class 以外类型的消息 51 | DEV_BROADCAST_HDR *broadcast = reinterpret_cast(lParam); 52 | if (!broadcast || broadcast->dbch_devicetype != DBT_DEVTYP_DEVICEINTERFACE) 53 | break; 54 | // 获取 SetWindowLongPtrW 设置的对象 55 | CameraHotplugPrivate *data = reinterpret_cast(::GetWindowLongPtrW(hwnd, GWLP_USERDATA)); 56 | if (!data) 57 | break; 58 | // 过滤不监听的设备类型 59 | DEV_BROADCAST_DEVICEINTERFACE *device_interface = reinterpret_cast(broadcast); 60 | QUuid uid(device_interface->dbcc_classguid); 61 | if (!data->devNotifys.contains(uid)) 62 | break; 63 | QString device_name; 64 | if (device_interface->dbcc_name) { 65 | #ifdef UNICODE 66 | device_name = QString::fromWCharArray(device_interface->dbcc_name); 67 | #else 68 | device_name = QString(device_interface->dbcc_name); 69 | #endif 70 | } 71 | // 从设备描述中获取 vid 和 pid 72 | quint16 vid, pid; 73 | if (!CameraInfo::getVidPid(device_name, vid, pid)) { 74 | break; 75 | } 76 | if (is_add) { 77 | fprintf(stderr, "device attached: vid 0x%04x, pid 0x%04x.\n", vid, pid); 78 | data->deviceAttached(vid, pid); 79 | } else if (is_remove) { 80 | fprintf(stderr, "device detached: vid 0x%04x, pid 0x%04x.\n", vid, pid); 81 | data->deviceDetached(vid, pid); 82 | } 83 | } while(false); 84 | } 85 | 86 | return ::DefWindowProcW(hwnd, message, wParam, lParam); 87 | } 88 | 89 | // 窗口类名 90 | QString CameraHotplugPrivate::windowClassName() 91 | { 92 | return QLatin1String("Qt_CameraHotplug_Window_") + QString::number(quintptr(windowMessageProcess)); 93 | } 94 | 95 | // 创建一个用于接收消息的窗口,注册消息回调 96 | bool CameraHotplugPrivate::createMessageWindow(const QVector &uuids) 97 | { 98 | QString class_name = windowClassName(); 99 | HINSTANCE hi = ::GetModuleHandleW(nullptr); 100 | 101 | WNDCLASSW wc; 102 | memset(&wc, 0, sizeof(WNDCLASSW)); 103 | wc.lpfnWndProc = windowMessageProcess; 104 | wc.cbClsExtra = 0; 105 | wc.cbWndExtra = 0; 106 | wc.hInstance = hi; 107 | wc.lpszClassName = reinterpret_cast(class_name.utf16()); 108 | ::RegisterClassW(&wc); 109 | 110 | hwnd = ::CreateWindowW(wc.lpszClassName, // classname 111 | wc.lpszClassName, // window name 112 | 0, // style 113 | 0, // x 114 | 0, // y 115 | 0, // width 116 | 0, // height 117 | 0, // parent 118 | 0, // menu handle 119 | hi, // application 120 | 0); // windows creation data. 121 | if (!hwnd) { 122 | qDebug()<<"createMessageWindow error"<<(int)GetLastError(); 123 | } else { 124 | // 初始化 DEV_BROADCAST_DEVICEINTERFACE 数据结构 125 | DEV_BROADCAST_DEVICEINTERFACE_W filter_data; 126 | memset(&filter_data, 0, sizeof(DEV_BROADCAST_DEVICEINTERFACE_W)); 127 | filter_data.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE_W); 128 | filter_data.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE; 129 | for (auto &&uuid : uuids) 130 | { 131 | filter_data.dbcc_classguid = uuid; 132 | HDEVNOTIFY handle = ::RegisterDeviceNotificationW(hwnd, &filter_data, DEVICE_NOTIFY_WINDOW_HANDLE); 133 | if (handle) { 134 | devNotifys.insert(uuid, handle); 135 | } else { 136 | qDebug()<<"RegisterDeviceNotification error"<(windowClassName().utf16()), ::GetModuleHandleW(nullptr)); 158 | } 159 | 160 | 161 | CameraHotplug::CameraHotplug(QObject *parent) 162 | : QObject{parent} 163 | , dptr{new CameraHotplugPrivate} 164 | { 165 | dptr->ptr = this; 166 | } 167 | 168 | CameraHotplug::~CameraHotplug() 169 | { 170 | free(); 171 | } 172 | 173 | void CameraHotplug::init(const QVector &uuids) 174 | { 175 | const bool ret = dptr->createMessageWindow(uuids); 176 | if (!ret) { 177 | dptr->destroyMessageWindow(); 178 | } 179 | } 180 | 181 | void CameraHotplug::free() 182 | { 183 | dptr->destroyMessageWindow(); 184 | } 185 | -------------------------------------------------------------------------------- /DSCamera/Camera/CameraHotplug.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include "CameraInfo.h" 6 | class CameraHotplugPrivate; 7 | 8 | // 设备热插拔检测 9 | class CameraHotplug : public QObject 10 | { 11 | Q_OBJECT 12 | public: 13 | explicit CameraHotplug(QObject *parent = nullptr); 14 | ~CameraHotplug(); 15 | 16 | // RegisterDeviceNotification 注册对应的 GUID 消息通知 17 | // 暂未考虑重复注册和注册失败的处理 18 | void init(const QVector &uuids); 19 | 20 | // UnregisterDeviceNotification 21 | // 会在析构中自动调用一次 22 | void free(); 23 | 24 | signals: 25 | // 设备插入 26 | void deviceAttached(quint16 vid, quint16 pid); 27 | // 设备拔出 28 | void deviceDetached(quint16 vid, quint16 pid); 29 | 30 | private: 31 | QSharedPointer dptr; 32 | }; 33 | -------------------------------------------------------------------------------- /DSCamera/Camera/CameraInfo.cpp: -------------------------------------------------------------------------------- 1 | #include "CameraInfo.h" 2 | #include 3 | #include "Windows.h" 4 | #include "dshow.h" 5 | 6 | CameraInfo::CameraInfo(QObject *parent) 7 | : QObject{parent} 8 | { 9 | 10 | } 11 | 12 | QStringList CameraInfo::getDeviceNames() const 13 | { 14 | QStringList names; 15 | for (const CameraDevice &device : qAsConst(deviceList)) 16 | names << device.displayName; 17 | return names; 18 | } 19 | 20 | QStringList CameraInfo::getFriendlyNames() const 21 | { 22 | QStringList names; 23 | for (const CameraDevice &device : qAsConst(deviceList)) 24 | names << device.friendlyName; 25 | return names; 26 | } 27 | 28 | void CameraInfo::updateDeviceList() 29 | { 30 | deviceList = enumDeviceList(); 31 | emit deviceListChanged(); 32 | } 33 | 34 | QList CameraInfo::getDeviceList() const 35 | { 36 | return deviceList; 37 | } 38 | 39 | bool CameraInfo::getVidPid(const QString &name, quint16 &vid, quint16 &pid) 40 | { 41 | // 从设备描述中获取 vid 和 pid 42 | quint16 vid_temp = 0; 43 | quint16 pid_temp = 0; 44 | QString desc_temp = name.toUpper(); 45 | int offset = -1; 46 | #if defined(Q_OS_WIN32) 47 | offset = desc_temp.indexOf("VID_"); 48 | if (offset > 0 && offset + 8 <= desc_temp.size()) { 49 | vid_temp = desc_temp.mid(offset + 4, 4).toUShort(nullptr, 16); 50 | } else { 51 | return false; 52 | } 53 | offset = desc_temp.indexOf("PID_"); 54 | if (offset > 0 && offset + 8 <= desc_temp.size()) { 55 | pid_temp = desc_temp.mid(offset + 4, 4).toUShort(nullptr, 16); 56 | } else { 57 | return false; 58 | } 59 | #elif defined(Q_OS_MACOS) 60 | if (desc_temp.size() != 18) 61 | return false; 62 | offset = 10; 63 | vid_temp = desc_temp.mid(offset, 4).toUShort(nullptr, 16); 64 | offset = 14; 65 | pid_temp = desc_temp.mid(offset, 4).toUShort(nullptr, 16); 66 | #endif 67 | vid = vid_temp; 68 | pid = pid_temp; 69 | return true; 70 | } 71 | 72 | bool enumResolutions(ICaptureGraphBuilder2 *builder, IMoniker *moniker) 73 | { 74 | // 枚举该设备支持的格式和分辨率 75 | IBaseFilter *source_filter = NULL; 76 | HRESULT hr = moniker->BindToObject(NULL, NULL, IID_IBaseFilter, 77 | reinterpret_cast(&source_filter)); 78 | if (FAILED(hr) || !source_filter) 79 | return false; 80 | 81 | IAMStreamConfig *stream_config = NULL; 82 | hr = builder->FindInterface(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Interleaved, source_filter, 83 | IID_IAMStreamConfig, reinterpret_cast(&stream_config)); 84 | if (FAILED(hr) || !stream_config) { 85 | hr = builder->FindInterface(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, source_filter, 86 | IID_IAMStreamConfig, reinterpret_cast(&stream_config)); 87 | } 88 | if (FAILED(hr) || !stream_config) { 89 | SAFE_RELEASE(source_filter); 90 | return false; 91 | } 92 | // 枚举格式信息 93 | int cap_count; 94 | int cap_size; 95 | VIDEO_STREAM_CONFIG_CAPS caps; 96 | hr = stream_config->GetNumberOfCapabilities(&cap_count, &cap_size); 97 | if (FAILED(hr) || sizeof(caps) != cap_size) 98 | { 99 | SAFE_RELEASE(stream_config); 100 | SAFE_RELEASE(source_filter); 101 | return false; 102 | } 103 | bool ret = false; 104 | for (int i = 0; i < cap_count && !ret; i++) 105 | { 106 | AM_MEDIA_TYPE *pamt = NULL; 107 | hr = stream_config->GetStreamCaps(i, &pamt, (BYTE*)(&caps)); 108 | if (FAILED(hr) || !pamt) { 109 | continue; 110 | } 111 | if (pamt->formattype == FORMAT_VideoInfo) 112 | { 113 | VIDEOINFOHEADER *vih = reinterpret_cast(pamt->pbFormat); 114 | int width = vih->bmiHeader.biWidth; 115 | int height = vih->bmiHeader.biHeight; 116 | LONGLONG avg_time = vih->AvgTimePerFrame; 117 | // 分辨率和格式是混在一起的,需要自己分开 118 | // TODO 会有重复的,暂时先手动比较过滤 119 | qDebug() << pamt->subtype << width << height << avg_time; 120 | } 121 | DeleteMediaType(pamt); 122 | } 123 | { 124 | AM_MEDIA_TYPE *pamt = NULL; 125 | hr = stream_config->GetFormat(&pamt); 126 | // 默认值 127 | if (SUCCEEDED(hr) && pamt && pamt->pbFormat) { 128 | VIDEOINFOHEADER *vih = reinterpret_cast(pamt->pbFormat); 129 | int width = vih->bmiHeader.biWidth; 130 | int height = vih->bmiHeader.biHeight; 131 | LONGLONG avg_time = vih->AvgTimePerFrame; 132 | qDebug() << "default" << pamt->subtype << width << height << avg_time; 133 | } 134 | DeleteMediaType(pamt); 135 | } 136 | SAFE_RELEASE(stream_config); 137 | SAFE_RELEASE(source_filter); 138 | return true; 139 | } 140 | 141 | QList CameraInfo::enumDeviceList() const 142 | { 143 | // https://learn.microsoft.com/zh-cn/windows/win32/directshow/selecting-a-capture-device 144 | QList device_list; 145 | HRESULT hr = S_FALSE; 146 | 147 | // 1.调用 CoCreateInstance 以创建系统设备枚举器的实例。 148 | ICreateDevEnum *device_enum = NULL; 149 | hr = ::CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER, 150 | IID_ICreateDevEnum, reinterpret_cast(&device_enum)); 151 | if (FAILED(hr) || !device_enum) { 152 | return device_list; 153 | } 154 | 155 | // 2.调用 ICreateDevEnum::CreateClassEnumerator,并将设备类别指定为 GUID。 156 | IEnumMoniker *enum_moniker = NULL; 157 | hr = device_enum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &enum_moniker, 0); 158 | if (FAILED(hr) || !enum_moniker) { 159 | SAFE_RELEASE(device_enum); 160 | return device_list; 161 | } 162 | enum_moniker->Reset(); 163 | 164 | // 创建一个builder等下用来枚举格式信息 165 | ICaptureGraphBuilder2 *graph_builder = NULL; 166 | hr = ::CoCreateInstance(CLSID_CaptureGraphBuilder2 , NULL, CLSCTX_INPROC, 167 | IID_ICaptureGraphBuilder2, reinterpret_cast(&graph_builder)); 168 | if (FAILED(hr) || !graph_builder){ 169 | SAFE_RELEASE(device_enum); 170 | SAFE_RELEASE(enum_moniker); 171 | return device_list; 172 | } 173 | 174 | // 3.CreateClassEnumerator 方法返回指向 IEnumMoniker 接口的指针。 175 | // 若要枚举名字对象,请调用 IEnumMoniker::Next。 176 | IMoniker *moniker = NULL; 177 | IMalloc *malloc_interface = NULL; 178 | ::CoGetMalloc(1, reinterpret_cast(&malloc_interface)); 179 | qDebug()<<__FUNCTION__; 180 | int counter = 0; 181 | while (SUCCEEDED(enum_moniker->Next(1, &moniker, NULL)) && moniker) 182 | { 183 | counter++; 184 | qDebug()<<"device"<GetDisplayName(NULL, NULL, &name_str); 188 | if (FAILED(hr)) { 189 | SAFE_RELEASE(moniker); 190 | continue; 191 | } 192 | // 如"@device:pnp:\\\\?\\usb#vid_04ca&pid_7070&mi_00#6&16c57194&0&0000#{65e8773d-8f56-11d0-a3b9-00a0c9223196}\\global" 193 | device.displayName = QString::fromWCharArray(name_str); 194 | malloc_interface->Free(name_str); 195 | qDebug()<<"display name"<BindToStorage(NULL, NULL, IID_IPropertyBag, 199 | reinterpret_cast(&prop_bag)); 200 | if (FAILED(hr) || !prop_bag) { 201 | SAFE_RELEASE(moniker); 202 | continue; 203 | } 204 | 205 | VARIANT var; 206 | var.vt = VT_BSTR; 207 | hr = prop_bag->Read(L"FriendlyName", &var, NULL); 208 | if (FAILED(hr)) { 209 | SAFE_RELEASE(prop_bag); 210 | SAFE_RELEASE(moniker); 211 | continue; 212 | } 213 | // 如"Integrated Camera" 214 | device.friendlyName = QString::fromWCharArray(var.bstrVal); 215 | qDebug()<<"friendly name"<Read(L"DevicePath", &var, NULL); 219 | // if (FAILED(hr)) { 220 | // SAFE_RELEASE(prop_bag); 221 | // SAFE_RELEASE(moniker); 222 | // continue; 223 | // } 224 | // QString devicepath = QString::fromWCharArray(var.bstrVal); 225 | // qDebug()<<"devicepath"< 3 | #include "CameraCommon.h" 4 | 5 | // 设备信息 6 | struct CameraDevice 7 | { 8 | // 设备信息字符串,包含devicePath等信息 9 | QString displayName; 10 | // 用于ui显示的设备名 11 | QString friendlyName; 12 | }; 13 | 14 | // 设备信息 15 | class CameraInfo : public QObject 16 | { 17 | Q_OBJECT 18 | Q_PROPERTY(QStringList deviceNames READ getDeviceNames NOTIFY deviceListChanged) 19 | Q_PROPERTY(QStringList friendlyNames READ getFriendlyNames NOTIFY deviceListChanged) 20 | public: 21 | explicit CameraInfo(QObject *parent = nullptr); 22 | 23 | // 设备名列表-displayName 24 | QStringList getDeviceNames() const; 25 | // 设备名对应的显示名称-friendlyName 26 | QStringList getFriendlyNames() const; 27 | 28 | // 更新设备列表 29 | void updateDeviceList(); 30 | // 设备列表 31 | QList getDeviceList() const; 32 | // 获取设备名中的 vid pid 33 | static bool getVidPid(const QString &name, quint16 &vid, quint16 &pid); 34 | 35 | signals: 36 | void deviceListChanged(); 37 | 38 | private: 39 | // 枚举设备信息 40 | QList enumDeviceList() const; 41 | 42 | private: 43 | QList deviceList; 44 | }; 45 | -------------------------------------------------------------------------------- /DSCamera/Camera/CameraProbe.cpp: -------------------------------------------------------------------------------- 1 | #include "CameraProbe.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | CameraProbe::CameraProbe(QObject *parent) 11 | : QObject{parent} 12 | { 13 | // 拍图保存 14 | connect(this, &CameraProbe::captureFinished, this, [this](const QImage &img){ 15 | QString dir_path = getCacheDir(); 16 | QDir dir(dir_path); 17 | if (dir.exists() || dir.mkdir(dir_path)) { 18 | QString file_path = dir_path + QString("/%1_拍图.jpg").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hhmmss")); 19 | QFile::remove(file_path); 20 | img.save(file_path); 21 | } 22 | }); 23 | } 24 | 25 | bool CameraProbe::getRecording() const 26 | { 27 | return recording; 28 | } 29 | 30 | void CameraProbe::setRecording(bool record) 31 | { 32 | if (recording != record) { 33 | recording = record; 34 | emit recordingChanged(); 35 | } 36 | } 37 | 38 | void CameraProbe::capture() 39 | { 40 | saveNext = true; 41 | } 42 | 43 | void CameraProbe::startRecord() 44 | { 45 | if (!corePtr || getRecording()) 46 | return; 47 | QString dir_path = getCacheDir(); 48 | QDir dir(dir_path); 49 | if (dir.exists() || dir.mkdir(dir_path)) { 50 | QString file_path = dir_path + QString("/%1_录制.avi").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hhmmss")); 51 | QFile::remove(file_path); 52 | if (corePtr->startRecord(file_path)) { 53 | saveFirst = true; 54 | setRecording(true); 55 | } 56 | } 57 | } 58 | 59 | void CameraProbe::stopRecord() 60 | { 61 | if (!corePtr || !getRecording()) 62 | return; 63 | corePtr->stopRecord(); 64 | setRecording(false); 65 | if (!saveFirst) { 66 | QString dir_path = getCacheDir(); 67 | QDir dir(dir_path); 68 | if (dir.exists() || dir.mkdir(dir_path)) { 69 | QString file_path = dir_path + QString("/%1_录制.jpg").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hhmmss")); 70 | QFile::remove(file_path); 71 | recordFirst.save(file_path); 72 | } 73 | } 74 | } 75 | 76 | void CameraProbe::openCacheDir() 77 | { 78 | QString dir_path = getCacheDir(); 79 | QDir dir(dir_path); 80 | if (dir.exists()) { 81 | QDesktopServices::openUrl(QUrl::fromLocalFile(dir_path)); 82 | } 83 | } 84 | 85 | void CameraProbe::attachCore(CameraCore *core) 86 | { 87 | corePtr = core; 88 | } 89 | 90 | void CameraProbe::reset() 91 | { 92 | saveNext = false; 93 | saveFirst = false; 94 | stopRecord(); 95 | } 96 | 97 | void CameraProbe::previewUpdate(const QImage &img) 98 | { 99 | if (saveNext) { 100 | qDebug()<<"capture finished"< 3 | #include "CameraCommon.h" 4 | #include "CameraCore.h" 5 | 6 | // 数据探针 7 | class CameraProbe : public QObject 8 | { 9 | Q_OBJECT 10 | Q_PROPERTY(bool recording READ getRecording NOTIFY recordingChanged) 11 | public: 12 | explicit CameraProbe(QObject *parent = nullptr); 13 | 14 | // 录制状态 15 | bool getRecording() const; 16 | void setRecording(bool record); 17 | 18 | // 拍图 19 | Q_INVOKABLE void capture(); 20 | // 开始录制 21 | Q_INVOKABLE void startRecord(); 22 | // 结束录制 23 | Q_INVOKABLE void stopRecord(); 24 | // 打开保存图片文件夹 25 | Q_INVOKABLE void openCacheDir(); 26 | 27 | // 关联core进行操作 28 | void attachCore(CameraCore *core); 29 | // 重置状态 30 | void reset(); 31 | // 在preview数据线程回调 32 | void previewUpdate(const QImage &img); 33 | // 在still数据线程回调 34 | void stillUpdate(const QImage &img); 35 | // 获取缓存路径 36 | static QString getCacheDir(); 37 | 38 | signals: 39 | // 捕获到图片 40 | void captureFinished(const QImage &img); 41 | // 录制状态变化 42 | void recordingChanged(); 43 | 44 | private: 45 | // 底层操作 46 | CameraCore *corePtr{nullptr}; 47 | // 是否在录制 48 | bool recording{false}; 49 | // 录制时保存第一张 50 | std::atomic_bool saveFirst{false}; 51 | QImage recordFirst; 52 | // 下一帧保存 53 | std::atomic_bool saveNext{false}; 54 | }; 55 | -------------------------------------------------------------------------------- /DSCamera/Camera/CameraRegister.cpp: -------------------------------------------------------------------------------- 1 | #include "CameraRegister.h" 2 | #include 3 | #include 4 | #include 5 | #include "CameraInfo.h" 6 | #include "CameraProbe.h" 7 | #include "CameraHotplug.h" 8 | #include "CameraView.h" 9 | #include "CameraControl.h" 10 | 11 | // strmiids: DirectShow导出类标识符(CLSID)和接口标识符(IID) 12 | #pragma comment(lib, "strmiids.lib") 13 | // strmbase: DirectShow基类 14 | #pragma comment(lib, "strmbase.lib") 15 | // ole32: CoCreateInstance.CoInitialize 16 | #pragma comment(lib, "ole32.lib") 17 | // oleaut32: SysStringLen.VariantInit.VariantClear 18 | #pragma comment(lib, "oleaut32.lib") 19 | 20 | struct CameraGuard { 21 | CameraGuard() { 22 | ::CoInitialize(NULL); 23 | // ... 24 | } 25 | ~CameraGuard() { 26 | // ... 27 | ::CoUninitialize(); 28 | } 29 | }; 30 | 31 | CameraGuard guard; 32 | 33 | void Camera::registerType(QQmlApplicationEngine *engine){ 34 | qRegisterMetaType("HWND"); 35 | qmlRegisterType("Camera", 1, 0, "CameraInfo"); 36 | qmlRegisterType("Camera", 1, 0, "CameraProbe"); 37 | qmlRegisterType("Camera", 1, 0, "CameraHotplug"); 38 | qmlRegisterType("Camera", 1, 0, "CameraView"); 39 | qmlRegisterType("Camera", 1, 0, "CameraControl"); 40 | auto control = new CameraControl(qApp); 41 | engine->rootContext()->setContextProperty("cameraCtrl", control); 42 | } 43 | -------------------------------------------------------------------------------- /DSCamera/Camera/CameraRegister.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace Camera { 5 | 6 | // 注册类型以及初始化设置 7 | void registerType(QQmlApplicationEngine *engine); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /DSCamera/Camera/CameraView.cpp: -------------------------------------------------------------------------------- 1 | #include "CameraView.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | CameraView::CameraView(QQuickItem *parent) 14 | : QQuickItem(parent) 15 | { 16 | setFlag(ItemHasContents, true); 17 | 18 | connect(&fpsTimer, &QTimer::timeout, this, [this](){ 19 | setFps(fpsCount); 20 | fpsCount = 0; 21 | }); 22 | fpsTimer.start(1000); 23 | } 24 | 25 | bool CameraView::getIsEmpty() const 26 | { 27 | return viewImage.isNull(); 28 | } 29 | 30 | int CameraView::getFps() const 31 | { 32 | return fps; 33 | } 34 | 35 | void CameraView::setFps(int value) 36 | { 37 | if (fps != value) { 38 | fps = value; 39 | emit fpsChanged(); 40 | } 41 | } 42 | 43 | void CameraView::setHorFlipIschecked(bool check) 44 | { 45 | horFlipIschecked = check; 46 | update(); 47 | } 48 | 49 | void CameraView::setVerFlipIschecked(bool check) 50 | { 51 | verFlipIschecked = check; 52 | update(); 53 | } 54 | 55 | void CameraView::updateImage(const QImage &img) 56 | { 57 | if (viewImage.isNull() != img.isNull()) { 58 | emit isEmptyChanged(); 59 | } 60 | viewImage = img; 61 | fpsCount++; 62 | update(); 63 | } 64 | 65 | void CameraView::save() 66 | { 67 | 68 | } 69 | 70 | QSGNode *CameraView::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) 71 | { 72 | if (getIsEmpty()) 73 | return oldNode; 74 | 75 | QRect node_rect = boundingRect().toRect(); 76 | // 计算居中的位置 77 | const double image_ratio = viewImage.width() / (double)viewImage.height(); 78 | const double rect_ratio = node_rect.width() / (double)node_rect.height(); 79 | if (image_ratio > rect_ratio) { 80 | const int new_height = node_rect.width() / image_ratio; 81 | node_rect.setY(node_rect.y() + (node_rect.height() - new_height) / 2); 82 | node_rect.setHeight(new_height); 83 | } else { 84 | const int new_width = image_ratio * node_rect.height(); 85 | node_rect.setX(node_rect.x() + (node_rect.width() - new_width) / 2); 86 | node_rect.setWidth(new_width); 87 | } 88 | 89 | if (!node_rect.isValid()) 90 | return oldNode; 91 | // 图片缩放比例 92 | const double scale = node_rect.width() / (double)viewImage.width() * 100; 93 | 94 | QSGSimpleTextureNode *node = dynamic_cast(oldNode); 95 | if (!node) { 96 | node = new QSGSimpleTextureNode(); 97 | } 98 | QImage img = viewImage; 99 | // img = viewImage.mirrored(horFlipIschecked, verFlipIschecked); 100 | // 缩小时平滑缩放 101 | if (scale < 100) 102 | { 103 | img = img.scaled(node_rect.size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); 104 | } 105 | 106 | QSGTexture *m_texture = window()->createTextureFromImage(img); 107 | node->setTexture(m_texture); 108 | node->setOwnsTexture(true); 109 | node->setRect(node_rect); 110 | node->markDirty(QSGNode::DirtyGeometry); 111 | node->markDirty(QSGNode::DirtyMaterial); 112 | QSGSimpleTextureNode::TextureCoordinatesTransformMode trans_flag; 113 | if (horFlipIschecked) { 114 | trans_flag |= QSGSimpleTextureNode::MirrorHorizontally; 115 | } 116 | if (verFlipIschecked) { 117 | trans_flag |= QSGSimpleTextureNode::MirrorVertically; 118 | } 119 | node->setTextureCoordinatesTransform(trans_flag); 120 | 121 | return node; 122 | } 123 | -------------------------------------------------------------------------------- /DSCamera/Camera/CameraView.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | // 视频显示及鼠标交互 8 | class CameraView : public QQuickItem 9 | { 10 | Q_OBJECT 11 | Q_PROPERTY(bool isEmpty READ getIsEmpty NOTIFY isEmptyChanged) 12 | Q_PROPERTY(int fps READ getFps NOTIFY fpsChanged) 13 | public: 14 | explicit CameraView(QQuickItem *parent = nullptr); 15 | 16 | // 初始状态没有数据,会显示一个按钮 17 | bool getIsEmpty() const; 18 | 19 | // 帧率 20 | int getFps() const; 21 | void setFps(int value); 22 | 23 | // 横向翻转 24 | Q_INVOKABLE void setHorFlipIschecked(bool check); 25 | // 竖向翻转 26 | Q_INVOKABLE void setVerFlipIschecked(bool check); 27 | // 更新图像 28 | Q_INVOKABLE void updateImage(const QImage &img); 29 | // 保存图像 30 | Q_INVOKABLE void save(); 31 | 32 | protected: 33 | QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) override; 34 | 35 | signals: 36 | void isEmptyChanged(); 37 | void fpsChanged(); 38 | 39 | private: 40 | // 预览的图片 41 | QImage viewImage; 42 | 43 | // 横向翻转 44 | bool horFlipIschecked{false}; 45 | // 竖向翻转 46 | bool verFlipIschecked{false}; 47 | 48 | // 帧率 49 | int fps{0}; 50 | // 每秒接收帧数 51 | int fpsCount{0}; 52 | // 定时器统计每秒帧数 53 | QTimer fpsTimer; 54 | }; 55 | -------------------------------------------------------------------------------- /DSCamera/Camera/ImageConverter.cpp: -------------------------------------------------------------------------------- 1 | #include "ImageConverter.h" 2 | #include 3 | #include 4 | 5 | inline unsigned char rgbClip(int value) 6 | { 7 | return value < 0 ? 0 : (value > 255 ? 255 : value); 8 | } 9 | 10 | QImage convertRGB32(const unsigned char *data, long len, int width, int height) 11 | { 12 | if (width * height * 4 != len) { 13 | return convertEmpty(data, len, width, height); 14 | } 15 | // 参考 QCamera 代码,rgb 时 bmiHeader.biHeight < 0 为上到下,否则下到上需要翻转 16 | QImage img(data, width, height, QImage::Format_RGB32); 17 | if (height > 0) { 18 | img = img.mirrored(false, true); 19 | } else { 20 | img = img.copy(); 21 | } 22 | return img; 23 | } 24 | 25 | QImage convertMJPG(const unsigned char *data, long len, int width, int height) 26 | { 27 | QByteArray bytes = QByteArray::fromRawData(reinterpret_cast(data), len); 28 | QImage img; 29 | img.loadFromData(bytes, "JPG"); 30 | if (img.isNull()) { 31 | return convertEmpty(data, len, width, height); 32 | } 33 | if (img.format() != QImage::Format_RGB32) { 34 | img = img.convertToFormat(QImage::Format_RGB32); 35 | } else { 36 | img = img.copy(); 37 | } 38 | return img; 39 | } 40 | 41 | QImage convertYUY2(const unsigned char *data, long len, int width, int height) 42 | { 43 | if (width * height * 2 != len) { 44 | return convertEmpty(data, len, width, height); 45 | } 46 | QImage img(width, height, QImage::Format_RGB32); 47 | const unsigned char *p_yuv = data; 48 | int y1, u, y2, v; 49 | int r, g, b; 50 | for (int i = 0; i < height; i++) 51 | { 52 | unsigned char *p_rgb = img.scanLine(i); 53 | for (int j = 0; j < width; j += 2) 54 | { 55 | y1 = *p_yuv++ - 16; 56 | u = *p_yuv++ - 128; 57 | y2 = *p_yuv++ - 16; 58 | v = *p_yuv++ - 128; 59 | 60 | b = (298 * y1 + 516 * u + 128) >> 8; 61 | g = (298 * y1 - 100 * u - 208 * v + 128) >> 8; 62 | r = (298 * y1 + 409 * v + 128) >> 8; 63 | // RGB32/ARGB32 单个像素内存顺序为 BGRA-8888 64 | *p_rgb++ = rgbClip(b); 65 | *p_rgb++ = rgbClip(g); 66 | *p_rgb++ = rgbClip(r); 67 | *p_rgb++ = 0xFF; 68 | 69 | b = (298 * y2 + 516 * u + 128) >> 8; 70 | g = (298 * y2 - 100 * u - 208 * v + 128) >> 8; 71 | r = (298 * y2 + 409 * v + 128) >> 8; 72 | *p_rgb++ = rgbClip(b); 73 | *p_rgb++ = rgbClip(g); 74 | *p_rgb++ = rgbClip(r); 75 | *p_rgb++ = 0xFF; 76 | } 77 | } 78 | return img; 79 | } 80 | 81 | QImage convertEmpty(const unsigned char *data, long len, int width, int height) 82 | { 83 | Q_UNUSED(data) 84 | Q_UNUSED(len) 85 | Q_UNUSED(width) 86 | Q_UNUSED(height) 87 | return QImage(); 88 | } 89 | 90 | struct ConverterRow 91 | { 92 | GUID subtype; 93 | ImageConverter converter; 94 | }; 95 | 96 | static ConverterRow converterTable[] = { 97 | {MEDIASUBTYPE_RGB32, convertRGB32}, 98 | {MEDIASUBTYPE_MJPG, convertMJPG}, 99 | {MEDIASUBTYPE_YUY2, convertYUY2} 100 | }; 101 | 102 | bool isValidSubtype(GUID subtype) 103 | { 104 | for (auto &row : converterTable) 105 | { 106 | if (row.subtype == subtype) 107 | return true; 108 | } 109 | return false; 110 | } 111 | 112 | ImageConverter subtypeConverter(GUID subtype) 113 | { 114 | for (auto &row : converterTable) 115 | { 116 | if (row.subtype == subtype) 117 | return row.converter; 118 | } 119 | return convertEmpty; 120 | } 121 | -------------------------------------------------------------------------------- /DSCamera/Camera/ImageConverter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "SampleGrabber.h" 8 | 9 | // 图像格式转换 10 | typedef std::function ImageConverter; 11 | 12 | // MEDIASUBTYPE_RGB32 13 | QImage convertRGB32(const unsigned char *data, long len, int width, int height); 14 | 15 | // MEDIASUBTYPE_MJPG 16 | QImage convertMJPG(const unsigned char *data, long len, int width, int height); 17 | 18 | // MEDIASUBTYPE_YUY2 19 | QImage convertYUY2(const unsigned char *data, long len, int width, int height); 20 | 21 | // 不识别的转为空 22 | QImage convertEmpty(const unsigned char *data, long len, int width, int height); 23 | 24 | // 根据ID判断是否是支持的类型 25 | bool isValidSubtype(GUID subtype); 26 | 27 | // 根据GUID选择转换函数 28 | ImageConverter subtypeConverter(GUID subtype); 29 | -------------------------------------------------------------------------------- /DSCamera/Camera/SampleGrabber.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | // 部分接口被弃用,所以要单独写一份,旧的SDK是有这些id的,qedit.h 8 | 9 | interface ISampleGrabberCB : public IUnknown 10 | { 11 | virtual STDMETHODIMP SampleCB(double SampleTime, IMediaSample *pSample) = 0; 12 | virtual STDMETHODIMP BufferCB(double SampleTime, BYTE *pBuffer, long BufferLen) = 0; 13 | }; 14 | 15 | static const IID IID_ISampleGrabberCB = 16 | { 0x0579154A, 0x2B53, 0x4994,{ 0xB0, 0xD0, 0xE7, 0x73, 0x14, 0x8E, 0xFF, 0x85 } }; 17 | 18 | interface ISampleGrabber : public IUnknown 19 | { 20 | virtual HRESULT STDMETHODCALLTYPE SetOneShot(BOOL OneShot) = 0; 21 | virtual HRESULT STDMETHODCALLTYPE SetMediaType(const AM_MEDIA_TYPE *pType) = 0; 22 | virtual HRESULT STDMETHODCALLTYPE GetConnectedMediaType(AM_MEDIA_TYPE *pType) = 0; 23 | virtual HRESULT STDMETHODCALLTYPE SetBufferSamples(BOOL BufferThem) = 0; 24 | virtual HRESULT STDMETHODCALLTYPE GetCurrentBuffer(long *pBufferSize, long *pBuffer) = 0; 25 | virtual HRESULT STDMETHODCALLTYPE GetCurrentSample(IMediaSample **ppSample) = 0; 26 | virtual HRESULT STDMETHODCALLTYPE SetCallback(ISampleGrabberCB *pCallback, long WhichMethodToCallback) = 0; 27 | }; 28 | 29 | static const IID IID_ISampleGrabber = 30 | { 0x6B652FFF, 0x11FE, 0x4fce,{ 0x92, 0xAD, 0x02, 0x66, 0xB5, 0xD7, 0xC7, 0x8F } }; 31 | 32 | static const CLSID CLSID_SampleGrabber = 33 | { 0xC1F400A0, 0x3F08, 0x11d3,{ 0x9F, 0x0B, 0x00, 0x60, 0x08, 0x03, 0x9E, 0x37 } }; 34 | 35 | static const CLSID CLSID_NullRenderer = 36 | { 0xC1F400A4, 0x3F08, 0x11d3,{ 0x9F, 0x0B, 0x00, 0x60, 0x08, 0x03, 0x9E, 0x37 } }; 37 | 38 | static const CLSID CLSID_VideoEffects1Category = 39 | { 0xcc7bfb42, 0xf175, 0x11d1,{ 0xa3, 0x92, 0x0, 0xe0, 0x29, 0x1f, 0x39, 0x59 } }; 40 | 41 | static const CLSID CLSID_VideoEffects2Category = 42 | { 0xcc7bfb43, 0xf175, 0x11d1,{ 0xa3, 0x92, 0x0, 0xe0, 0x29, 0x1f, 0x39, 0x59 } }; 43 | 44 | static const CLSID CLSID_AudioEffects1Category = 45 | { 0xcc7bfb44, 0xf175, 0x11d1,{ 0xa3, 0x92, 0x0, 0xe0, 0x29, 0x1f, 0x39, 0x59 } }; 46 | 47 | static const CLSID CLSID_AudioEffects2Category = 48 | { 0xcc7bfb45, 0xf175, 0x11d1,{ 0xa3, 0x92, 0x0, 0xe0, 0x29, 0x1f, 0x39, 0x59 } }; 49 | -------------------------------------------------------------------------------- /DSCamera/DSCamera.pro: -------------------------------------------------------------------------------- 1 | QT += core 2 | QT += gui 3 | QT += widgets 4 | QT += quick 5 | QT += quickcontrols2 6 | # QT += core5compat 7 | 8 | CONFIG += c++11 9 | CONFIG += utf8_source 10 | 11 | RC_ICONS = Image/camera.ico 12 | DESTDIR = $$PWD/../bin 13 | 14 | INCLUDEPATH += $$PWD/Camera 15 | include($$PWD/Camera/Camera.pri) 16 | 17 | SOURCES += \ 18 | main.cpp 19 | 20 | RESOURCES += QML/qml.qrc \ 21 | Image/img.qrc 22 | 23 | # Default rules for deployment. 24 | qnx: target.path = /tmp/$${TARGET}/bin 25 | else: unix:!android: target.path = /opt/$${TARGET}/bin 26 | !isEmpty(target.path): INSTALLS += target 27 | -------------------------------------------------------------------------------- /DSCamera/Image/camera.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gongjianbo/QtUVCCamera/f27a09537049dc325bf02037586a75aeb4933987/DSCamera/Image/camera.ico -------------------------------------------------------------------------------- /DSCamera/Image/camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gongjianbo/QtUVCCamera/f27a09537049dc325bf02037586a75aeb4933987/DSCamera/Image/camera.png -------------------------------------------------------------------------------- /DSCamera/Image/img.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | camera.ico 4 | camera.png 5 | 6 | 7 | -------------------------------------------------------------------------------- /DSCamera/QML/Component/ShadowRect.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import QtGraphicalEffects 1.15 3 | // import Qt5Compat.GraphicalEffects 4 | 5 | // 带阴影的 Rectangle 6 | Item { 7 | id: control 8 | 9 | default property alias items: rect_area.children 10 | property alias color: rect_area.color 11 | property alias border: rect_area.border 12 | 13 | Rectangle { 14 | id: rect_area 15 | anchors.fill: parent 16 | clip: true 17 | } 18 | 19 | DropShadow { 20 | anchors.fill: rect_area 21 | horizontalOffset: 1 22 | verticalOffset: 1 23 | radius: 8 24 | samples: 16 25 | color: "#AAAAAA" 26 | source: rect_area 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /DSCamera/QML/Content/SettingArea.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import QtQuick.Window 2.15 3 | import QtQuick.Controls 2.15 4 | import QtQuick.Layouts 1.15 5 | import "../Component" 6 | 7 | // 左侧操作及设置面板 8 | ColumnLayout { 9 | id: control 10 | 11 | spacing: 12 12 | 13 | ComboBox { 14 | id: device_box 15 | height: 32 16 | Layout.fillWidth: true 17 | model: cameraCtrl.info.friendlyNames 18 | onActivated: { 19 | var ret = cameraCtrl.selectDevice(device_box.currentIndex) 20 | console.log("select", device_box.currentText, ret) 21 | } 22 | Connections { 23 | target: cameraCtrl 24 | // 如插入设备后顺序变化 25 | function onDeviceIndexChanged() { 26 | if (cameraCtrl.deviceIndex != device_box.currentIndex) { 27 | device_box.currentIndex = cameraCtrl.deviceIndex 28 | } 29 | } 30 | } 31 | } 32 | 33 | Button { 34 | text: "设备切换测试 " + (switch_timer.running ? "on" : "off") 35 | onClicked: { 36 | switch_timer.running = !switch_timer.running 37 | } 38 | Timer { 39 | id: switch_timer 40 | interval: 2000 41 | repeat: true 42 | running: false 43 | onTriggered: { 44 | var count = device_box.count 45 | var index = device_box.currentIndex + 1 46 | if (index >= count) { 47 | index = 0 48 | } 49 | 50 | device_box.currentIndex = index 51 | var ret = cameraCtrl.selectDevice(device_box.currentIndex) 52 | console.log("select", device_box.currentText, ret) 53 | } 54 | } 55 | } 56 | 57 | Button { 58 | text: "设置设备" 59 | onClicked: { 60 | cameraCtrl.popDeviceSetting(Window.window) 61 | } 62 | } 63 | 64 | Button { 65 | text: "设置格式" 66 | onClicked: { 67 | cameraCtrl.popFormatSetting(Window.window) 68 | } 69 | } 70 | 71 | Row { 72 | spacing: 12 73 | Button { 74 | text: "播放" 75 | onClicked: { 76 | cameraCtrl.play() 77 | } 78 | } 79 | Button { 80 | text: "暂停" 81 | onClicked: { 82 | cameraCtrl.pause() 83 | } 84 | } 85 | Button { 86 | text: "停止" 87 | onClicked: { 88 | cameraCtrl.stop() 89 | } 90 | } 91 | } 92 | 93 | Row { 94 | spacing: 12 95 | Button { 96 | text: "拍图" 97 | onClicked: { 98 | cameraCtrl.probe.capture() 99 | } 100 | } 101 | Button { 102 | text: cameraCtrl.probe.recording ? "结束" : "录制" 103 | onClicked: { 104 | // 录制的时候应该把其他操作禁用,不然会引发一系列问题 105 | if (cameraCtrl.probe.recording) { 106 | cameraCtrl.probe.stopRecord() 107 | } else { 108 | cameraCtrl.probe.startRecord() 109 | } 110 | } 111 | } 112 | } 113 | 114 | Button { 115 | text: "打开缓存文件夹" 116 | onClicked: { 117 | cameraCtrl.probe.openCacheDir() 118 | } 119 | } 120 | 121 | Item { 122 | Layout.fillWidth: true 123 | Layout.fillHeight: true 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /DSCamera/QML/Content/VideoArea.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import QtQuick.Controls 2.15 3 | import QtQuick.Layouts 1.15 4 | import Camera 1.0 5 | import "../Component" 6 | 7 | Item { 8 | id: control 9 | 10 | Item { 11 | anchors.fill: parent 12 | clip: true 13 | CameraView { 14 | id: camera_view 15 | anchors.fill: parent 16 | Component.onCompleted: { 17 | cameraCtrl.attachView(camera_view) 18 | } 19 | } 20 | } 21 | 22 | 23 | Row { 24 | spacing: 20 25 | ShadowRect { 26 | width: 160 27 | height: 30 28 | color: "#D7D7D7" 29 | Label { 30 | anchors.centerIn: parent 31 | text: "Size: " + cameraCtrl.resolution.width + "×" + cameraCtrl.resolution.height 32 | } 33 | } 34 | ShadowRect { 35 | width: 100 36 | height: 30 37 | color: "#D7D7D7" 38 | Label { 39 | anchors.centerIn: parent 40 | text: "FPS: " + camera_view.fps 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /DSCamera/QML/main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import QtQuick.Window 2.15 3 | import QtQuick.Controls 2.15 4 | import QtQuick.Layouts 1.15 5 | import "./Content" 6 | import "./Component" 7 | 8 | Window { 9 | width: 900 10 | height: 600 11 | visible: true 12 | title: qsTr("DirectShow Camera") 13 | color: "white" 14 | 15 | RowLayout { 16 | anchors.fill: parent 17 | anchors.margins: 12 18 | spacing: 12 19 | ShadowRect { 20 | Layout.fillHeight: true 21 | width: 240 22 | color: "#F0F0F0" 23 | SettingArea { 24 | anchors.fill: parent 25 | anchors.margins: 12 26 | } 27 | } 28 | ShadowRect { 29 | Layout.fillWidth: true 30 | Layout.fillHeight: true 31 | color: "#F0F0F0" 32 | VideoArea { 33 | anchors.fill: parent 34 | anchors.margins: 12 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /DSCamera/QML/qml.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | main.qml 4 | Content/SettingArea.qml 5 | Content/VideoArea.qml 6 | Component/ShadowRect.qml 7 | 8 | 9 | -------------------------------------------------------------------------------- /DSCamera/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "CameraRegister.h" 7 | 8 | int main(int argc, char *argv[]) 9 | { 10 | #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 11 | QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); 12 | // QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); 13 | #endif 14 | QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering); 15 | QQuickStyle::setStyle("Material"); 16 | 17 | QApplication app(argc, argv); 18 | 19 | QFont font; 20 | font.setFamily("SimSun"); 21 | font.setPixelSize(13); 22 | app.setFont(font); 23 | 24 | QQmlApplicationEngine engine; 25 | Camera::registerType(&engine); 26 | 27 | const QUrl url(QStringLiteral("qrc:/main.qml")); 28 | QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, 29 | &app, [url](QObject *obj, const QUrl &objUrl) { 30 | if (!obj && url == objUrl) 31 | QCoreApplication::exit(-1); 32 | }, Qt::QueuedConnection); 33 | engine.load(url); 34 | 35 | return app.exec(); 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 龚建波 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /QtUVCCamera.pro: -------------------------------------------------------------------------------- 1 | TEMPLATE = subdirs 2 | CONFIG += ordered 3 | 4 | SUBDIRS += DSCamera 5 | DSCamera.file = $$PWD/DSCamera/DSCamera.pro 6 | # DSCamera.depends += 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Qt UVC Camera 2 | 3 | - Qt 中实现摄像头的预览/拍图/录像等操作,因为 QCamera 的接口不完善且 BUG 多,故自己根据平台或第三方库进行实现。 4 | 5 | # 开发环境 6 | 7 | - Win10/Win11 + VS2019 + Qt5.15.2 32/64bit 8 | 9 | # 项目结构 10 | 11 | - DSCamera:Windows DirectShow 实现的 Camera 12 | 13 | # 注意事项 14 | 15 | - 目前只测试了 YUY2 和 MJPG 格式,其他格式没有设备来测试 16 | --------------------------------------------------------------------------------