当前位置: 首页 > news >正文

苏州优秀网站设计做网站页面视频教学

苏州优秀网站设计,做网站页面视频教学,直接翻译网页的软件,网站建设 南通WebRTC视频 01 - 视频采集整体架构 WebRTC视频 02 - 视频采集类 VideoCaptureModule [WebRTC视频 03 - 视频采集类 VideoCaptureDS 上篇]#xff08;本文#xff09; WebRTC视频 04 - 视频采集类 VideoCaptureDS 中篇 WebRTC视频 05 - 视频采集类 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);// 判断我们是否要获取设备唯一IdUniqueIdif (deviceUniqueIdUTF8Length 0) {hr pBag-Read(LDevicePath, varName, 0);if (FAILED(hr)) {hr pBag-Read(LDescription, varName, 0);if (FAILED(hr)) {hr pBag-Read(LFriendlyName, 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使用的缺省值NULLproductUniqueIdUTF8Length使用的缺省值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 DirectShows 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 ComRefCountCaptureSinkFilter(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_refptrComRefCountCaptureInputPin 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::vectorAM_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 isnt 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_refptrIMemAllocator allocator_ RTC_GUARDED_BY(main_checker_);// 与当前pin连接的外部pinrtc::scoped_refptrIPin 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依据是什么大了浪费空间小了不够存。
http://www.hkea.cn/news/14340398/

相关文章:

  • 摄影师作品网站一个网站做无限关键词
  • 网站后台 清理缓存WordPress置顶不生效
  • 可以下载源程序的网站frontpage做网站教程
  • 做网站小程序做一网站要什么时候开始
  • 网站前端设计外包公司地图位置链接怎么做
  • 建站行业发展前景wordpress图片上传慢
  • 无忧主机建站的过程深圳做微商网站的公司
  • 网站建站推广上海市工程建设信息网
  • 免费网站平台推荐有那种网站么
  • 腾讯快速建站平台如何注册一个免费网站
  • 江苏建设标准网站徐州招聘网
  • 400网站建设推广个人外贸网站制作
  • 网站建设的标语2017网站seo如何做
  • 网站文章内容一键排版功能怎么做网站盈利
  • 成都网站seo诊断html做网站的设计
  • 找苏州网站建设wordpress商店如何添加商品
  • 网站运维平台建设原则旅游网站建设策划
  • 传媒公司做网站条件长宁长沙网站建设
  • 深圳网站建设ucreator合肥网站建设公司 推荐
  • 哪个小说网站版权做的好处网站建设所用系统
  • 做网站建设需要什么资质免费网址怎么申请注册
  • 单位网站及政务新媒体建设管理网页设计基础的教学目的
  • 内网网站建设所需硬件设备建设厅特种作业证件查询官网
  • 织梦网站后台管理系统做外贸那里发广告网站
  • 合肥网站建设 合肥网络推广wordpress 文章置顶不显示
  • 企业网站建设版本wordpress自动发货如何设置
  • 献县做网站广告设计公司需要资质吗
  • 黄石网站制作忘记网站后台用户名
  • 做网站盈利方式蛋糕烘焙wordpress主题
  • 网站开发价格明细内蒙古建设工程交易服务中心网站