WebRTC视频 03 - 视频采集类 VideoCaptureDS 上篇
WebRTC视频 01 - 视频采集整体架构
WebRTC视频 02 - 视频采集类 VideoCaptureModule
[WebRTC视频 03 - 视频采集类 VideoCaptureDS 上篇](本文)
WebRTC视频 04 - 视频采集类 VideoCaptureDS 中篇
WebRTC视频 05 - 视频采集类 VideoCaptureDS 下篇
一、前言:
前面两篇文章我们介绍了WebRtc的视频采集架构,并且,分析了所有关键类之间如何相互协调,一直分析到操作VideoCaptureDS这个类为止。心中有了框架,接下来我们分析具体的点,就是VideoCaptureDS再往下如何操作硬件的。
二、流程图:
其实主要干了几件事:
- 连接CaptureFilter的输出Pin到SinkFilter的输入Pin,这样数据就源源不断从输出Pin到输入Pin了。
- CaptureFilter是由DirectShow提供的,可以通过CaptureFilter来控制DirectShow完成视频采集,而SinkFilter是webrtc自己构造的。
- 入口函数还记得吗?是VideoCaptureDS::Init()。
三、COM编程方法介绍:
CreateClassEnumerator:
CreateClassEnumerator
是DirectShow API中的一个函数,它用于创建一个枚举器对象,该对象可用于枚举系统中注册的所有DirectShow滤波器的类标识符(CLSID)。
该函数的原型通常是:
HRESULT CreateClassEnumerator(REFCLSID clsidDeviceClass,IEnumMoniker **ppEnumMoniker,DWORD dwFlags
);
clsidDeviceClass
: 指定要枚举的设备类别的 CLSID。传入NULL
时,将枚举所有的设备类别。ppEnumMoniker
: 指向IEnumMoniker
接口指针的指针。枚举器将通过该指针返回。dwFlags
: 可选的标志,用于指定枚举器的行为。
IEnumMoniker:
IEnumMoniker
接口是COM编程中的一个接口,用于枚举 IMoniker
接口的集合。IEnumMoniker
接口中的 Next
方法用于检索指定数量的Moniker对象。
下面是 IEnumMoniker
接口的 Next
方法的一般原型:
HRESULT Next(ULONG celt,IMoniker **rgelt,ULONG *pceltFetched
);
celt
: 指定要检索的Moniker对象数量。rgelt
: 用于输出枚举的Moniker对象的指针数组。pceltFetched
: 指向一个ULONG
变量的指针,用于返回实际成功检索的Moniker对象数量。
Next
方法会尝试从枚举器的当前位置检索指定数量的Moniker对象,并将它们填充到提供的 rgelt
数组中。成功获取的Moniker对象数量将通过 pceltFetched
参数返回。如果成功检索了指定数量的Moniker对象,则返回 S_OK
,否则返回 S_FALSE
。
BindToStorage:
IMoniker::BindToStorage
是一个用于将 Moniker 绑定到存储对象的方法。在 COM 编程中,Moniker 是用于标识和定位对象的抽象机制,而 BindToStorage
允许将 Moniker 解析为存储对象,从而可以访问该对象的数据。
具体来说,IMoniker::BindToStorage
方法的作用是将 Moniker 绑定到存储器,并返回一个指向该存储器对象的接口指针,以便可以访问存储器中所包含的数据。这个方法通常用于从 Moniker 获取实际对象的数据或属性。
下面是 IMoniker::BindToStorage
方法的一般原型:
HRESULT BindToStorage(IBindCtx *pbc,IMoniker *pmkToLeft,REFIID riid,void **ppvObj
);
pbc
: 指向绑定上下文对象的指针,用于控制绑定操作的一些方面。pmkToLeft
: 在某些情况下可能用到,表示左侧的 Moniker 对象。riid
: 指定所请求接口的 IID(接口标识符)。ppvObj
: 用于返回存储器对象的接口指针的指针。
通过调用 IMoniker::BindToStorage
方法,可以通过 Moniker 定位并访问存储器对象中的数据。这在 COM 编程中特别有用,特别是在处理对象链接和嵌入(OLE)等场景中。
IPropertyBag:
IPropertyBag
是 COM 编程中的一个接口,用于提供一种机制,允许通过属性名称来检索和设置属性值。它通常用于在 COM 对象之间传递属性信息,并提供一种灵活的方式来访问和操作属性。
下面是 IPropertyBag
接口的一般原型:
interface IPropertyBag : IUnknown
{virtual HRESULT Read(LPCOLESTR pszPropName, VARIANT *pVar, IErrorLog *pErrorLog) = 0;virtual HRESULT Write(LPCOLESTR pszPropName, VARIANT *pVar) = 0;
};
Read
: 通过属性名称读取属性值,并将其存储在传入的VARIANT
结构中。如果属性不存在或读取失败,可以使用IErrorLog
接口来记录错误信息。Write
: 根据属性名称设置属性值,传入要设置的属性值的VARIANT
结构。
通过 IPropertyBag
接口,可以实现一种通用的属性存储和检索机制,使得 COM 对象之间可以方便地传递和共享属性信息。这种机制在许多场景下非常有用,特别是在配置对象、持久化对象属性、或者在不同组件之间传递配置信息等方面。
BindToObject:
BindToObject
是 COM 编程中常用的一个方法,通常用于将 Moniker 绑定到对象,从而获取对象的接口指针。这个方法通常由 IMoniker
接口提供,可以用于实现对象的定位和访问。
下面是 IMoniker::BindToObject
方法的一般原型:
HRESULT BindToObject(IBindCtx *pbc,IMoniker *pmkToLeft,REFIID riidResult,void **ppvResult
);
pbc
: 指向绑定上下文对象的指针,用于控制绑定操作的一些方面。pmkToLeft
: 在某些情况下可能用到,表示左侧的 Moniker 对象。riidResult
: 请求的接口的 IID(接口标识符)。ppvResult
: 用于返回绑定到的对象的接口指针的指针。
通过调用 IMoniker::BindToObject
方法,可以将 Moniker 解析为一个对象,并获取该对象的接口指针。这个方法在 COM 编程中常用于实现对象的定位和访问,特别是在处理对象链接、远程过程调用(RPC)和其他需要动态定位对象的场景中。
三、CaptureFilter:
1、作用:
CaptureFilter就是控制DirectShow完成视频采集的。
2、获取CaptureFilter:
代码入口:
int32_t VideoCaptureDS::Init(const char* deviceUniqueIdUTF8) {// ...// 构造CaptureFilter_captureFilter = _dsInfo.GetDeviceFilter(deviceUniqueIdUTF8);if (!_captureFilter) {RTC_LOG(LS_INFO) << "Failed to create capture filter.";return -1;}// ...
}
可以看出,是通过DeviceInfoDS
的对象 _dsInfo
来获取CaptureFilter
的。
看看如何获取CaptureFilter的:
// 获取CaptureFilter走这儿,其中productUniqueIdUTF8和productUniqueIdUTF8Length都传入的0
IBaseFilter* DeviceInfoDS::GetDeviceFilter(const char* deviceUniqueIdUTF8,char* productUniqueIdUTF8,uint32_t productUniqueIdUTF8Length) {const int32_t deviceUniqueIdUTF8Length = (int32_t)strlen((char*)deviceUniqueIdUTF8); // UTF8 is also NULL terminatedif (deviceUniqueIdUTF8Length > kVideoCaptureUniqueNameLength) {RTC_LOG(LS_INFO) << "Device name too long";return NULL;}// enumerate all video capture devicesRELEASE_AND_CLEAR(_dsMonikerDevEnum);// CreateClassEnumerator 是获取视频采集设备的枚举器到_dsMonikerDevEnumHRESULT hr = _dsDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory,&_dsMonikerDevEnum, 0);if (hr != NOERROR) {RTC_LOG(LS_INFO) << "Failed to enumerate CLSID_SystemDeviceEnum, error 0x"<< rtc::ToHex(hr) << ". No webcam exist?";return 0;}// reset之后就可以从0开始遍历了_dsMonikerDevEnum->Reset();ULONG cFetched;IMoniker* pM;IBaseFilter* captureFilter = NULL;bool deviceFound = false;// 使用Moniker遍历所有视频采集设备while (S_OK == _dsMonikerDevEnum->Next(1, &pM, &cFetched) && !deviceFound) {IPropertyBag* pBag;// 获取对象的Bag接口,通过这个Bag接口后续获取属性hr = pM->BindToStorage(0, 0, IID_IPropertyBag, (void**)&pBag);if (S_OK == hr) {// Find the description or friendly name.// 先找设备唯一标识,找不到就去找设备描述,再找不到就去找设备名VARIANT varName;VariantInit(&varName);// 判断我们是否要获取设备唯一Id(UniqueId)if (deviceUniqueIdUTF8Length > 0) {hr = pBag->Read(L"DevicePath", &varName, 0);if (FAILED(hr)) {hr = pBag->Read(L"Description", &varName, 0);if (FAILED(hr)) {hr = pBag->Read(L"FriendlyName", &varName, 0);}}if (SUCCEEDED(hr)) {// 将设备路径进行 UTF-8 编码转换char tempDevicePathUTF8[256];// 临时存储 UTF-8 编码的设备路径tempDevicePathUTF8[0] = 0;// 将获取的devicePath保存到tempDevicePathUTF8当中WideCharToMultiByte(CP_UTF8, 0, varName.bstrVal, -1,tempDevicePathUTF8, sizeof(tempDevicePathUTF8),NULL, NULL);// 比较下是否为我们想要找的deviceif (strncmp(tempDevicePathUTF8, (const char*)deviceUniqueIdUTF8,deviceUniqueIdUTF8Length) == 0) {// We have found the requested device// 找到了请求的设备deviceFound = true;// 获取CaptureFilter接口到captureFilterhr =pM->BindToObject(0, 0, IID_IBaseFilter, (void**)&captureFilter);ifFAILED(hr) {RTC_LOG(LS_ERROR) << "Failed to bind to the selected ""capture device "<< hr;}// 如果产品唯一标识存在且长度大于 0,获取设备名称,我们调用的时候传入的Null和0,这儿不会执行if (productUniqueIdUTF8 &&productUniqueIdUTF8Length > 0) // Get the device name{GetProductId(deviceUniqueIdUTF8, productUniqueIdUTF8,productUniqueIdUTF8Length);}}}}VariantClear(&varName);pBag->Release();}pM->Release();}return captureFilter;
}
- 我们找到第一个就直接退出了,不会找出所有设备;
- 我们调用的时候productUniqueIdUTF8使用的缺省值NULL,productUniqueIdUTF8Length使用的缺省值0,因此不会执行GetProductId;
至此,我们VideoCaptureDS就持有了CaptureFilter了。
3、添加CaptureFilter到FilterGraph:
我们所有的Filter都必须添加到FilterGraph,这样,FilterGraph才能控制我们完成一些业务逻辑。
int32_t VideoCaptureDS::Init(const char* deviceUniqueIdUTF8) {// 省略部分代码...// 构造CaptureFilter_captureFilter = _dsInfo.GetDeviceFilter(deviceUniqueIdUTF8);if (!_captureFilter) {RTC_LOG(LS_INFO) << "Failed to create capture filter.";return -1;}// Get the interface for DirectShow's GraphBuilder// 创建FilterGraph,并返回IGraphBuilder接口HRESULT hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER,IID_IGraphBuilder, (void**)&_graphBuilder);if (FAILED(hr)) {RTC_LOG(LS_INFO) << "Failed to create graph builder.";return -1;}// 获取IMediaControl接口,用于控制数据的流转hr = _graphBuilder->QueryInterface(IID_IMediaControl, (void**)&_mediaControl);if (FAILED(hr)) {RTC_LOG(LS_INFO) << "Failed to create media control builder.";return -1;}// 将前面构造好的CaptureFilter添加到FilterGraph当中hr = _graphBuilder->AddFilter(_captureFilter, CAPTURE_FILTER_NAME);if (FAILED(hr)) {RTC_LOG(LS_INFO) << "Failed to add the capture device to the graph.";return -1;}// 省略部分代码...
}
4、获取输出Pin:
前面我们枚举整个终端的视频采集设备,找到了我们请求的设备,并返回了CaptureFilter。我们CaptureFilter也有很多Pin,因此,如法炮制,继续枚举CaptureFilter的Pin,找到我们想要的输出Pin。
入口函数:
int32_t VideoCaptureDS::Init(const char* deviceUniqueIdUTF8) {// 省略部分代码...// 获取CaptureFilter的输出Pin_outputCapturePin = GetOutputPin(_captureFilter, PIN_CATEGORY_CAPTURE);if (!_outputCapturePin) {RTC_LOG(LS_INFO) << "Failed to get output capture pin";return -1;}
}
注入我们要的输出Pin类型是PIN_CATEGORY_CAPTURE
/*** 获取输出引脚* @param filter 表示是哪个Filter的引脚* @param Category 表示引脚的种类*/
IPin* GetOutputPin(IBaseFilter* filter, REFGUID Category) {HRESULT hr;IPin* pin = NULL;IEnumPins* pPinEnum = NULL;// 获得枚举pin的接口到pPinEnum中filter->EnumPins(&pPinEnum);if (pPinEnum == NULL) {return NULL;}// get first unconnected pinhr = pPinEnum->Reset(); // set to first pin 让从0开始枚举// 遍历每个pinwhile (S_OK == pPinEnum->Next(1, &pin, NULL)) {// 获取这个pin的方向PIN_DIRECTION pPinDir;pin->QueryDirection(&pPinDir);if (PINDIR_OUTPUT == pPinDir) // This is an output pin{// 判断pin的类型,是否为我们想要的// GUID_NULL表示任意类型if (Category == GUID_NULL || PinMatchesCategory(pin, Category)) {pPinEnum->Release();return pin;}}pin->Release();pin = NULL;}pPinEnum->Release();return NULL;
}
其实逻辑也很简单了,就是遍历这个CaptureFilter的所有Pin,判断下是不是输出pin,如果是,再判断下pin类型是否为我们想要的,都符合就找到了。
那么,如何判断pin类型是否为我们想要的呢?
/*** 判断pin类型是否匹配*/
BOOL PinMatchesCategory(IPin* pPin, REFGUID Category) {BOOL bFound = FALSE;// 获取IKsPropertySet接口到pKs当中IKsPropertySet* pKs = NULL;HRESULT hr = pPin->QueryInterface(IID_PPV_ARGS(&pKs));if (SUCCEEDED(hr)) {GUID PinCategory;DWORD cbReturned;// 从AMPROPSETID_Pin这个属性集中,获取AMPROPERTY_PIN_CATEGORY属性,// 将属性数据存放于PinCategory当中,实际返回的数据大小存于cbReturned中hr = pKs->Get(AMPROPSETID_Pin, AMPROPERTY_PIN_CATEGORY, NULL, 0,&PinCategory, sizeof(GUID), &cbReturned);// 判断返回的数据和我们要存储的数据大小是否一致,一致表示找到了目标pinif (SUCCEEDED(hr) && (cbReturned == sizeof(GUID))) {bFound = (PinCategory == Category);}pKs->Release();}return bFound;
}
发现它是去获取我们请求的AMPROPERTY_PIN_CATEGORY这种类型的Pin的属性是否和我们请求的一直,一直就认为类型一致。
四、SinkFilter:
前面已经创建好了输入数据的Filter,也就是CaptureFilter,并将它加入到了FilterGraph当中,同时找到了合适的输出Pin,准备输出数据,我们现在就创建一个输出Filter,也就是SinkFilter,接收CaptureFilter采集的数据。
注意:之前讲的CaptureFilter是由DirectShow提供的,而SinkFilter是webrtc自己构造的;
入口函数:
int32_t VideoCaptureDS::Init(const char* deviceUniqueIdUTF8) {// Create the sink filte used for receiving Captured frames.// 开始构造CaptureSinkFiltersink_filter_ = new ComRefCount<CaptureSinkFilter>(this);// 将CaptureSinkFilter加入到GraphicBuilder当中hr = _graphBuilder->AddFilter(sink_filter_, SINK_FILTER_NAME);if (FAILED(hr)) {RTC_LOG(LS_INFO) << "Failed to add the send filter to the graph.";return -1;}// 获取SinkFilter的输入pin_inputSendPin = GetInputPin(sink_filter_);if (!_inputSendPin) {RTC_LOG(LS_INFO) << "Failed to get input send pin";return -1;}return 0;
}
发现我们是创建了一个CaptureSinkFilter对象,并让GraphicBuilder将自己管理起来,最后获取SinkFilter的输入Pin,既然SinkFilter是自己构建的,我们看看它的类长什么样:
1、CaptureSinkFilter:
class CaptureSinkFilter : public IBaseFilter {public:CaptureSinkFilter(VideoCaptureImpl* capture_observer);HRESULT SetRequestedCapability(const VideoCaptureCapability& capability);// Called on the capture thread.// Filter采集到数据之后,通过这个方法传给上层void ProcessCapturedFrame(unsigned char* buffer,size_t length,const VideoCaptureCapability& frame_info);void NotifyEvent(long code, LONG_PTR param1, LONG_PTR param2);bool IsStopped() const;// IUnknownSTDMETHOD(QueryInterface)(REFIID riid, void** ppv) override;// IPersistSTDMETHOD(GetClassID)(CLSID* clsid) override;// IMediaFilter.STDMETHOD(GetState)(DWORD msecs, FILTER_STATE* state) override;STDMETHOD(SetSyncSource)(IReferenceClock* clock) override;STDMETHOD(GetSyncSource)(IReferenceClock** clock) override;STDMETHOD(Pause)() override;STDMETHOD(Run)(REFERENCE_TIME start) override;STDMETHOD(Stop)() override;// IBaseFilterSTDMETHOD(EnumPins)(IEnumPins** pins) override; // 遍历所有引脚STDMETHOD(FindPin)(LPCWSTR id, IPin** pin) override;STDMETHOD(QueryFilterInfo)(FILTER_INFO* info) override;STDMETHOD(JoinFilterGraph)(IFilterGraph* graph, LPCWSTR name) override;STDMETHOD(QueryVendorInfo)(LPWSTR* vendor_info) override;protected:virtual ~CaptureSinkFilter();private:SequenceChecker main_checker_;const rtc::scoped_refptr<ComRefCount<CaptureInputPin>> input_pin_;VideoCaptureImpl* const capture_observer_;FILTER_INFO info_ RTC_GUARDED_BY(main_checker_) = {};// Set/cleared in JoinFilterGraph. The filter must be stopped (no capture)// at that time, so no lock is required. While the state is not stopped,// the sink will be used from the capture thread.IMediaEventSink* sink_ = nullptr;FILTER_STATE state_ RTC_GUARDED_BY(main_checker_) = State_Stopped;
};
- capture_observer_: 是一个观察者,sinkFilter获取到数据之后,通过这个observer传给上层;
- state_: sinkFilter的状态,初始为stoped,运行之后就是started;
- input_pin_:sinkFilter的输入pin,真正获取数据的地方;
- ProcessCapturedFrame // Filter采集到数据之后,通过这个方法传给上层;
- EnumPins // 遍历所有引脚
2、输入Pin:
class CaptureInputPin : public IMemInputPin, public IPin {public:CaptureInputPin(CaptureSinkFilter* filter);HRESULT SetRequestedCapability(const VideoCaptureCapability& capability);// Notifications from the filter.void OnFilterActivated();void OnFilterDeactivated();protected:virtual ~CaptureInputPin();private:CaptureSinkFilter* Filter() const;HRESULT AttemptConnection(IPin* receive_pin, const AM_MEDIA_TYPE* media_type);std::vector<AM_MEDIA_TYPE*> DetermineCandidateFormats(IPin* receive_pin,const AM_MEDIA_TYPE* media_type);void ClearAllocator(bool decommit);HRESULT CheckDirection(IPin* pin) const;// IUnknownSTDMETHOD(QueryInterface)(REFIID riid, void** ppv) override;// clang-format off// clang isn't sure what to do with the longer STDMETHOD() function// declarations.// IPin// 用于连接某个pinSTDMETHOD(Connect)(IPin* receive_pin,const AM_MEDIA_TYPE* media_type) override;// 当与某个pin连接成功之后,回调这个方法,查看能否与某个pin进行连接STDMETHOD(ReceiveConnection)(IPin* connector,const AM_MEDIA_TYPE* media_type) override;STDMETHOD(Disconnect)() override;STDMETHOD(ConnectedTo)(IPin** pin) override;STDMETHOD(ConnectionMediaType)(AM_MEDIA_TYPE* media_type) override;STDMETHOD(QueryPinInfo)(PIN_INFO* info) override;STDMETHOD(QueryDirection)(PIN_DIRECTION* pin_dir) override;STDMETHOD(QueryId)(LPWSTR* id) override;STDMETHOD(QueryAccept)(const AM_MEDIA_TYPE* media_type) override;STDMETHOD(EnumMediaTypes)(IEnumMediaTypes** types) override;STDMETHOD(QueryInternalConnections)(IPin** pins, ULONG* count) override;STDMETHOD(EndOfStream)() override;STDMETHOD(BeginFlush)() override;STDMETHOD(EndFlush)() override;STDMETHOD(NewSegment)(REFERENCE_TIME start, REFERENCE_TIME stop,double rate) override;// IMemInputPin// 分配一个内存分配器(因为有些Filter是虚拟的,必须靠这个来 IMemInputPin 这些方法管理内存STDMETHOD(GetAllocator)(IMemAllocator** allocator) override;STDMETHOD(NotifyAllocator)(IMemAllocator* allocator, BOOL read_only) override;STDMETHOD(GetAllocatorRequirements)(ALLOCATOR_PROPERTIES* props) override;// 获取当前引脚的数据(比如CaptureSinkFilter调用这个接口获取)STDMETHOD(Receive)(IMediaSample* sample) override;STDMETHOD(ReceiveMultiple)(IMediaSample** samples, long count,long* processed) override;STDMETHOD(ReceiveCanBlock)() override;// clang-format onSequenceChecker main_checker_;SequenceChecker capture_checker_;// 用户请求的能力VideoCaptureCapability requested_capability_ RTC_GUARDED_BY(main_checker_);// Accessed on the main thread when Filter()->IsStopped() (capture thread not// running), otherwise accessed on the capture thread.// 最终最接近用户请求能力的真实能力VideoCaptureCapability resulting_capability_;DWORD capture_thread_id_ = 0;// 内存分配器rtc::scoped_refptr<IMemAllocator> allocator_ RTC_GUARDED_BY(main_checker_);// 与当前pin连接的外部pinrtc::scoped_refptr<IPin> receive_pin_ RTC_GUARDED_BY(main_checker_);std::atomic_bool flushing_{false};std::atomic_bool runtime_error_{false};// Holds a referenceless pointer to the owning filter, the name and// direction of the pin. The filter pointer can be considered const.// pin信息PIN_INFO info_ = {};// 每个pin都有自己支持的媒体类型,不支持的会拒绝掉AM_MEDIA_TYPE media_type_ RTC_GUARDED_BY(main_checker_) = {};
};
我基本都写注释了,但是,还有几点需要注意:
-
IMemInputPin: 是与内存相关的,因为有些引脚是物理引脚,有些引脚是虚拟的,比如CaptureSinkFilter就是虚拟的Filter,虚拟的就会涉及到内存的申请释放,IMemInputPin就是定义这些方法的;
-
IPin就是实际的引脚;
-
当调用CaptureInputPin的Receive获取CaptureInputPin的数据之后,就可以交给CaptureSinkFilter,再通过其ProcessCapturedFrame 传给capture_observer_;
至于InputPin的枚举获取和之前CaptureFilter的OutputPin逻辑一样,不再赘述。
五、连接Filter:
Filter连接是一个比较复杂的流程,打算单独写一篇介绍,读者可以先思考几个问题:
- 每个Filter都有自己支持的能力,那么这俩Filter要连起来,分别应该选择哪个能力?
- 两个Filter之间要传递数据怎么传递?存储数据的Buffer由哪个Filter管理?
- 需要创建多大的Buffer,依据是什么?大了浪费空间,小了不够存。
相关文章:

WebRTC视频 03 - 视频采集类 VideoCaptureDS 上篇
WebRTC视频 01 - 视频采集整体架构 WebRTC视频 02 - 视频采集类 VideoCaptureModule [WebRTC视频 03 - 视频采集类 VideoCaptureDS 上篇](本文) WebRTC视频 04 - 视频采集类 VideoCaptureDS 中篇 WebRTC视频 05 - 视频采集类 VideoCaptureDS 下篇 一、前…...
python os.path.basename(获取路径中的文件名部分) 详解
os.path.basename 是 Python 的 os 模块中的一个函数,用于获取路径中的文件名部分。它会去掉路径中的目录部分,只返回最后的文件名或目录名。 以下是 os.path.basename 的详细解释和使用示例: 语法 os.path.basename(path) 参数 path&…...

《FreeRTOS任务基础知识以及任务创建相关函数》
目录 1.FreeRTOS多任务系统与传统单片机单任务系统的区别 2.FreeRTOS中的任务(Task)介绍 2.1 任务特性 2.2 FreeRTOS中的任务状态 2.3 FreeRTOS中的任务优先级 2.4 在任务函数中退出 2.5 任务控制块和任务堆栈 2.5.1 任务控制块 2.5.2 任务堆栈…...

036集——查询CAD图元属性字段信息:窗体显示(CAD—C#二次开发入门)
提取CAD图元所有属性字段,通过窗体显示,效果如下:(curve改为entity) 代码如下: public void 属性查询() {List<Curve> ents Z.db.SelectEntities<Curve>();if (ents is null ||ents.Cou…...

Swift从0开始学习 函数和闭包 day2
一、函数 1.1函数定义 使用 func 来声明一个函数,使用名字和参数来调用函数。使用 -> 来指定函数返回值的类型。 示例:拼接字符串 //有参数和返回值的函数 func append1(name : String, description : String) -> String {return "\(name)…...
内网、公网(外网)划分
内网、公网(外网)划分 声明! 学习视频来自B站up主 泷羽sec 有兴趣的师傅可以关注一下,如涉及侵权马上删除文章 笔记只是方便各位师傅的学习和探讨,文章所提到的网站以及内容,只做学习交流,其…...

【linux】centos7 换阿里云源
相关文章 【linux】CentOS 的软件源(Repository)学习-CSDN博客 查看yum配置文件 yum的配置文件通常位于/etc/yum.repos.d/目录下。你可以使用以下命令查看这些文件: ls /etc/yum.repos.d/ # 或者 ll /etc/yum.repos.d/备份当前的yum配置文…...

用OMS进行 OceanBase 租户间数据迁移的测评
基本概念 OceanBase迁移服务(,简称OMS),可以让用户在同构或异构 RDBMS 与OceanBase 数据库之间进行数据交互,支持数据的在线迁移,以及实时增量同步的复制功能。 OMS 提供了可视化的集中管控平台ÿ…...

【因果分析方法】MATLAB计算Liang-Kleeman信息流
【因果分析方法】MATLAB计算Liang-Kleeman信息流 1 Liang-Kleeman信息流2 MATLAB代码2.1 函数代码2.2 案例参考Liang-Kleeman 信息流(Liang-Kleeman Information Flow)是由 Liang 和 Kleeman 提出的基于信息论的因果分析方法。该方法用于量化变量之间的因果关系,通过计算信息…...

【Java基础知识系列】之Java类的初始化顺序
前言 类的初始化顺序 简单场景 代码示例 public class Person {private String name initName();private String initName() {System.out.println("【父类】初始化实例变量name");return "【父类】史蒂夫";}private int age;private static int staticVa…...

Swift 宏(Macro)入门趣谈(二)
概述 苹果在去年 WWDC 23 中就为 Swift 语言新增了“其利断金”的重要小伙伴 Swift 宏(Swift Macro)。为此,苹果特地用 2 段视频(入门和进阶)颇为隆重的介绍了它。 那么到底 Swift 宏是什么?有什么用&…...

vue elementui el-dropdown-item设置@click无效的解决方案
如图,直接在el-dropdown-item上面设置click,相应的method并没有被触发,查找资料发现需要在它的上级 el-dropdown 处使用 command 方法触发。 【template】 <el-dropdown placement"bottom-end" command"handleCommand&quo…...
如何用re从第1排第2个位置中找到两个数字返回(0,1)
以下是使用 Python 的re模块从第1班第2个位置这样的字符串中提取出数字并返回类似(0, 1)这种形式的示例代码,假设数字都是一位数的情况(如果是多位数可以按照后续介绍稍作调整): import redef extract_numbers(text):numbers re.…...
vue中的keep-alive是什么,有哪些使用场景,使用了什么原理,缓存后如何更新数据
<keep-alive> 是 Vue.js 提供的一个内置组件,用于缓存动态组件,避免频繁的销毁和重建。这在某些场景下可以显著提升性能,特别是在组件频繁切换的情况下。以下是对 keep-alive 的详细讲解,包括它的定义、使用场景、原理分析、…...

LeetCode105.从前序与中序遍历构造二叉树
题目要求 给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。 提示: 1 < preorder.length < 3000inorder.length preorder.length-3000 < pr…...

LeetCode654.最大二叉树
LeetCode刷题记录 文章目录 📜题目描述💡解题思路⌨C代码 📜题目描述 给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建: 创建一个根节点,其值为 nums 中的最大值。 递归地在最大值 左边 的 子…...
C# 字段和属性
在 C# 中,字段和属性是定义类和结构体数据的两种方式。 字段用于直接存储数据,而属性提供了对字段的封装和访问控制。 1. 字段(Fields) 定义 字段是类或结构体中用于存储数据的变量。字段可以是任何数据类型,包括基…...

【leetcode】N皇后 回溯法c++
目录 51.N皇后 52.N皇后II 51.N皇后 51. N 皇后 - 力扣(LeetCode) 按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。 n 皇后问题 研究的是如何将 n 个皇后放置在 nn 的棋盘上,并且使皇后彼此之间…...

Ubuntu 系统端口查询与管理详细分析
目录 前言1. 查询端口占用情况2. 释放占用的端口3. 修改应用程序的端口 前言 Window的端口被占用,类似的知识点:重装mysql时3306端口被占用解决方法 事情起因是宝塔的CPU负载过大,重启服务进程之后还是爆,后续发现是端口被占用&…...
Unity中使用StartCoroutine协程和Lerp方法,使GameObject缓慢移动
移动方法(传入需要移动的instance和目标位置) public Transform targetPosition; //目标位置 Vector3 target targetPosition.position;private IEnumerator MoveTowardsTarget(GameObject instance, Vector3 target){// 缓慢移动到目标的方法Vector3 …...
synchronized 学习
学习源: https://www.bilibili.com/video/BV1aJ411V763?spm_id_from333.788.videopod.episodes&vd_source32e1c41a9370911ab06d12fbc36c4ebc 1.应用场景 不超卖,也要考虑性能问题(场景) 2.常见面试问题: sync出…...
golang循环变量捕获问题
在 Go 语言中,当在循环中启动协程(goroutine)时,如果在协程闭包中直接引用循环变量,可能会遇到一个常见的陷阱 - 循环变量捕获问题。让我详细解释一下: 问题背景 看这个代码片段: fo…...
C++:std::is_convertible
C++标志库中提供is_convertible,可以测试一种类型是否可以转换为另一只类型: template <class From, class To> struct is_convertible; 使用举例: #include <iostream> #include <string>using namespace std;struct A { }; struct B : A { };int main…...

【JavaEE】-- HTTP
1. HTTP是什么? HTTP(全称为"超文本传输协议")是一种应用非常广泛的应用层协议,HTTP是基于TCP协议的一种应用层协议。 应用层协议:是计算机网络协议栈中最高层的协议,它定义了运行在不同主机上…...
在HarmonyOS ArkTS ArkUI-X 5.0及以上版本中,手势开发全攻略:
在 HarmonyOS 应用开发中,手势交互是连接用户与设备的核心纽带。ArkTS 框架提供了丰富的手势处理能力,既支持点击、长按、拖拽等基础单一手势的精细控制,也能通过多种绑定策略解决父子组件的手势竞争问题。本文将结合官方开发文档,…...
前端倒计时误差!
提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...

定时器任务——若依源码分析
分析util包下面的工具类schedule utils: ScheduleUtils 是若依中用于与 Quartz 框架交互的工具类,封装了定时任务的 创建、更新、暂停、删除等核心逻辑。 createScheduleJob createScheduleJob 用于将任务注册到 Quartz,先构建任务的 JobD…...

高危文件识别的常用算法:原理、应用与企业场景
高危文件识别的常用算法:原理、应用与企业场景 高危文件识别旨在检测可能导致安全威胁的文件,如包含恶意代码、敏感数据或欺诈内容的文档,在企业协同办公环境中(如Teams、Google Workspace)尤为重要。结合大模型技术&…...

让AI看见世界:MCP协议与服务器的工作原理
让AI看见世界:MCP协议与服务器的工作原理 MCP(Model Context Protocol)是一种创新的通信协议,旨在让大型语言模型能够安全、高效地与外部资源进行交互。在AI技术快速发展的今天,MCP正成为连接AI与现实世界的重要桥梁。…...
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别 直接训练提示词嵌入向量的核心区别 您提到的代码: prompt_embedding = initial_embedding.clone().requires_grad_(True) optimizer = torch.optim.Adam([prompt_embedding...