分享
 
 
 

开发Transform filter

王朝vc·作者佚名  2006-01-09
窄屏简体版  字體: |||超大  

摘要:本篇文档主要讲述了利用Directshow开发传输filter 时应该注意的一些事情。

在开发自己的filter之前,看看DMO(DirectX Media Object)是否满足你的要求,因为DMO可以做许多和filter相同的工作,但是开发DMO比开发filter要简单多了。开发transform filter主要有下面的几个步骤,努力的遵循吧

第一步选择一个基类

下面的基类适合开发transform filter。

CTransformFilter就是为了transform filter而设计的基类,这个类中有分开的输入和输出buffers,这种类型的filter有时也称作copy-transform filter,当一个copy-transform filter接收到一个输入samples的时候,它就将sample写入到一块新的输出buffer中,然后将这个新的buffer传递给下一个filter。

CTransInPlaceFilter,这个类型的filter在原来的buffer里修改data,也叫trans-in-place filters.

当这种类型的filter接收到一个sample,它改变这个sample中的数据,然后将sample传递下去,这种类型的输入pin和输出pin总是按照某个媒体类型连接起来。

CVideoTransformFilter这个类型的filter仅仅是为了视频解码器设计的。从CTransFormFilter派生而来,但是这个filter可以根据下游的render自动的丢弃data。

CBaseFilter是个总基类,所有的filter都是从这个类派生出去的。如果上面的filter都不适合你,那么你只有自己从这个基类中派生了。

第二步声明自己的Filter 类

首先声明一个从基类派生的c++类

class CRleFilter : public CTransformFilter

{

/* Declarations will go here. */

};

每个filter类都需要连接的pin类。根据你的需要,你要派生和你的filter连接的pin类。

你还要给你的filter设置一个不能重复的CLSID,你可以利用Guidgen or Uuidgen来产生一个128位CLSID,切忌不要拷贝其它的filter的。有很多种方法来声明CLSID,下面的例子使用了DEFINE_GUID宏。

[RleFilt.h]

// {1915C5C7-02AA-415f-890F-76D94C85AAF1}

DEFINE_GUID(CLSID_RLEFilter,

0x1915c5c7, 0x2aa, 0x415f, 0x89, 0xf, 0x76, 0xd9, 0x4c, 0x85, 0xaa, 0xf1);

[RleFilt.cpp]

#include <initguid.h>

#include "RleFilt.h"

然后,给你的filter写一个构造函数

CRleFilter::CRleFilter()

: CTransformFilter(NAME("My RLE Encoder"), 0, CLSID_RLEFilter)

{

/* Initialize any private variables here. */

}

注意,构造函数中有个参数就是我前面定义的CLSID。

第三步 支持媒体类戏协议

当两个pin连接的时候,他们必须就某种媒体类型达成一致协议,否则连接失败,数据媒体类型描述了数据的格式,如果没有媒体类型,一个filter可能传递一种类型的数据,然后其它的filte却不能识别这种数据。

Pin连接的时候达成协议的机制主要通过IPin::ReceiveConnection方法来实现的。输出pin用某种媒体类型作参数调用输入pin上的这个方法,输入pin要么接受,要么拒绝。如果输入pin拒绝连接,那么输出pin更改一下媒体类型继续连接,直至所有的媒体类型都连接一遍,如果没有找到合适的媒体的类型,那么连接失败。

在输入pin也可以通过IPin::EnumMediaTypes方法来任意的枚举它所支持的媒体类型list。输出pin可以通过这个list也可以检查是否支持某种媒体类型。

CTransformFilter实现一个通用的框架。如下

1 输入pin没有首选的媒体类型,这个主要看上游的filter提议的媒体类型。对于视频数据,媒体类型包括图片的大小,和桢率,这个信息必须由上游的源filter或者parser filter提供。对于音频数据,设置的数据格式就小了许多,因此,要重载输入pin的CBasePin::GetMediaType

2 当上游的filter提议一个媒体类型进行连接的时候,输入pin就调用

CTransformFilter::CheckInputType方法,这个方法拒绝和接受媒体类型。

3 只有输入pin连接以后,输出pin才能够连接,这个是属于transform filter的一个特性。大多数情况下,filter在设置输出pin的type之前一定要设置好输入pin的类型

4当输出pin没有连接的时候,它向下游filter连接的时候,要枚举本filter支持的媒体类型,形成一个list,他通过调用CTransformFilter::GetMediaType方法来产生这个list,输出pin会就下游filter所支持的所有的媒体类型进行连接

5 为了检测输入pin是否支持某个特定的输出媒体类型,输出pin通过调用CTransformFilter::CheckTransform方法。

上面列出的三个CTransformFilter方法都是纯虚函数,因此你的filter必须实现这三个函数

当上游的filter连接的时候提议一个媒体类型,那么输入pin就会调用函数

virtual HRESULT CheckInputType(const CMediaType* mtIn) pure;

这个函数包含了一个CMediaType类型的对象指针,这个类型封装了一个AM_MEDIA_TYPE结构。在这个函数中,你要检查AM_MEDIA_TYPE结构的中相关的field,如果该结构中有任何fied不合法,就返回VFW_E_TYPE_NOT_ACCEPTED,如果所有的媒体类型都是正确的,返还S_OK

,例如,在RLE编码filter,输入类型必须是8位或者4位的没有压缩的RGB视频。没有必要支持其它的输入格式,例如16,24位,因为那样,filter还得进行转换。下面的例子假定filter只支持8位的视频,不支持4位的视频

HRESULT CRleFilter::CheckInputType(const CMediaType *mtIn)

{

if ((mtIn->majortype != MEDIATYPE_Video) ||

(mtIn->subtype != MEDIASUBTYPE_RGB8) ||

(mtIn->formattype != FORMAT_VideoInfo) ||

(mtIn->cbFormat < sizeof(VIDEOINFOHEADER)))

{

return VFW_E_TYPE_NOT_ACCEPTED;

}

VIDEOINFOHEADER *pVih =

reinterpret_cast<VIDEOINFOHEADER*>(mtIn->pbFormat);

if ((pVih->bmiHeader.biBitCount != 8) ||

(pVih->bmiHeader.biCompression != BI_RGB))

{

return VFW_E_TYPE_NOT_ACCEPTED;

}

// Check the palette table.

if (pVih->bmiHeader.biClrUsed > PALETTE_ENTRIES(pVih))

{

return VFW_E_TYPE_NOT_ACCEPTED;

}

DWORD cbPalette = pVih->bmiHeader.biClrUsed * sizeof(RGBQUAD);

if (mtIn->cbFormat < sizeof(VIDEOINFOHEADER) + cbPalette)

{

return VFW_E_TYPE_NOT_ACCEPTED;

}

// Everything is good.

return S_OK;

}

在这个例子中,函数首先检查major type and subtype,然后检查格式类型,为了确保block格式是一个VIDEOINFOHEADER结构,这个filter也要支持VIDEOINFOHEADER2,

如果格式类型是正确的,这个sample还得检查VIDEOINFOHEADER结构的biBitCount and biCompression members,

2 virtual HRESULT GetMediaType(int iPosition, CMediaType *pMediaType) PURE;

CTransformFilter::GetMediaType根据序号iPositiong返回一个fiter支持的输出类型。只有输入pin被连接上以后,这个方法才会被调用,因此,你可以利用上游filter支持的媒体类型来决定下游输出的媒体类型

下面的例子返回一个输出媒体类型,这个输出是根据输入类型修改的

HRESULT CRleFilter::GetMediaType(int iPosition, CMediaType *pMediaType)

{

ASSERT(m_pInput->IsConnected());

if (iPosition < 0)

{

return E_INVALIDARG;

}

if (iPosition == 0)

{

HRESULT hr = m_pInput->ConnectionMediaType(pMediaType);

if (FAILED(hr))

{

return hr;

}

FOURCCMap fccMap = FCC('MRLE');

pMediaType->subtype = static_cast<GUID>(fccMap);

pMediaType->SetVariableSize();

pMediaType->SetTemporalCompression(FALSE);

ASSERT(pMediaType->formattype == FORMAT_VideoInfo);

VIDEOINFOHEADER *pVih =

reinterpret_cast<VIDEOINFOHEADER*>(pMediaType->pbFormat);

pVih->bmiHeader.biCompression = BI_RLE8;

pVih->bmiHeader.biSizeImage = DIBSIZE(pVih->bmiHeader);

return S_OK;

}

// else

return VFW_S_NO_MORE_ITEMS;

}

这个例子函数中,调用了IPin::ConnectionMediaType从输入pin上得到输入的媒体类型。然后改变了媒体类型结构的几个filed,表示是压缩格式

1 It assigns a new subtype GUID, which is constructed from the FOURCC code 'MRLE', using the FOURCCMap class.

2 It calls the CMediaType::SetVariableSize method, which sets the bFixedSizeSamples flag to FALSE and the lSampleSize member to zero, indicating variable-sized samples.

3 It calls the CMediaType::SetTemporalCompression method with the value FALSE, indicating that every frame is a key frame. (This field is informational only, so you could safely ignore it.)

4 It sets the biCompression field to BI_RLE8.

5 It sets the biSizeImage field to the image size.

3 virtual HRESULT CheckTransform(const CMediaType* mtIn, const CMediaType* mtOut) PURE;

CTransformFilter::CheckTransform检查输出的媒体类型和输入的媒体类型是否匹配。当输入pin在输出pin连接之后才开始连接的时候,输出pin会调用这个函数来检查输出媒体类型是否和输入媒体类型是否匹配。

下面的例子演示了查询数据的格式是否为RLE8视频,图像的大小是否和输入的匹配,调色板的入口是否一致,如果图像大小不一致就要拒绝

HRESULT CRleFilter::CheckTransform(

const CMediaType *mtIn, const CMediaType *mtOut)

{

// Check the major type.

if (mtOut->majortype != MEDIATYPE_Video)

{

return VFW_E_TYPE_NOT_ACCEPTED;

}

// Check the subtype and format type.

FOURCCMap fccMap = FCC('MRLE');

if (mtOut->subtype != static_cast<GUID>(fccMap))

{

return VFW_E_TYPE_NOT_ACCEPTED;

}

if ((mtOut->formattype != FORMAT_VideoInfo) ||

(mtOut->cbFormat < sizeof(VIDEOINFOHEADER)))

{

return VFW_E_TYPE_NOT_ACCEPTED;

}

// Compare the bitmap information against the input type.

ASSERT(mtIn->formattype == FORMAT_VideoInfo);

BITMAPINFOHEADER *pBmiOut = HEADER(mtOut->pbFormat);

BITMAPINFOHEADER *pBmiIn = HEADER(mtIn->pbFormat);

if ((pBmiOut->biPlanes != 1) ||

(pBmiOut->biBitCount != 8) ||

(pBmiOut->biCompression != BI_RLE8) ||

(pBmiOut->biWidth != pBmiIn->biWidth) ||

(pBmiOut->biHeight != pBmiIn->biHeight))

{

return VFW_E_TYPE_NOT_ACCEPTED;

}

// Compare source and target rectangles.

RECT rcImg;

SetRect(&rcImg, 0, 0, pBmiIn->biWidth, pBmiIn->biHeight);

RECT *prcSrc = &((VIDEOINFOHEADER*)(mtIn->pbFormat))->rcSource;

RECT *prcTarget = &((VIDEOINFOHEADER*)(mtOut->pbFormat))->rcTarget;

if (!IsRectEmpty(prcSrc) && !EqualRect(prcSrc, &rcImg))

{

return VFW_E_INVALIDMEDIATYPE;

}

if (!IsRectEmpty(prcTarget) && !EqualRect(prcTarget, &rcImg))

{

return VFW_E_INVALIDMEDIATYPE;

}

// Check the palette table.

if (pBmiOut->biClrUsed != pBmiIn->biClrUsed)

{

return VFW_E_TYPE_NOT_ACCEPTED;

}

DWORD cbPalette = pBmiOut->biClrUsed * sizeof(RGBQUAD);

if (mtOut->cbFormat < sizeof(VIDEOINFOHEADER) + cbPalette)

{

return VFW_E_TYPE_NOT_ACCEPTED;

}

if (0 != memcmp(pBmiOut + 1, pBmiIn + 1, cbPalette))

{

return VFW_E_TYPE_NOT_ACCEPTED;

}

// Everything is good.

return S_OK;

}

第四步 设置Allocator属性

当连个pin就某个媒体类型达成一致协议的时候,他们就选择一个allocator,就allocator的属性进行设置,比如buffer大小,buffer的数量。

在CTransformFilter 类中,有两个allocator,一个用于上游的pin的连接,一个用于下游的pin的连接,上游的filter选择upstream allocator设置属性,无论上游的filter怎么设置这个upstream allocator,输入pin都会接受,如果你想改变这个中状况,你可以继承CBaseInputPin::NotifyAllocator函数

Transform filter的输出pin选择下游的allocator,步骤如下

1 如果下游的filter可以提供一个allocator,那么输出pin就使用这个allocator,否则,输出pin就创建一个新的allocator。

2 输出pin通过下游filter的输入pin上的IMemInputPin::GetAllocatorRequirements.方法来确定下游filter的allocator的要求。

3 输出pin调用transform filter上的CTransformFilter::DecideBufferSize函数,这个函数也是一个纯虚的函数,

virtual HRESULT DecideBufferSize( IMemAllocator * pAllocator,

ALLOCATOR_PROPERTIES *pprop) PURE;

这个函数有一个指向allocator的指针,和一个指向ALLOCATOR_PROPERTIES结构的指针,这个指针包含了对allocator的属性的设置,如果下游的filter对allocator没有设置属性,那么这个结构就是NULL。

4在DecideBufferSize方法中,派生类的函数通过调用IMemAllocator::SetProperties.函数来设置allocator的属性。

通常,派生类会根据输出的格式,下游filter得要求,自身得要求来设置allocator的属性,allocator属性的设置要符合下游filter的要求,否则的话,连接就可能被拒绝。

下面的例子中,

HRESULT CRleFilter::DecideBufferSize(

IMemAllocator *pAlloc, ALLOCATOR_PROPERTIES *pProp)

{

AM_MEDIA_TYPE mt;

HRESULT hr = m_pOutput->ConnectionMediaType(&mt);

if (FAILED(hr))

{

return hr;

}

ASSERT(mt.formattype == FORMAT_VideoInfo);

BITMAPINFOHEADER *pbmi = HEADER(mt.pbFormat);

pProp->cbBuffer = DIBSIZE(*pbmi) * 2;

if (pProp->cbAlign == 0)

{

pProp->cbAlign = 1;

}

if (pProp->cBuffers == 0)

{

pProp->cBuffers = 1;

}

// Release the format block.

FreeMediaType(mt);

// Set allocator properties.

ALLOCATOR_PROPERTIES Actual;

hr = pAlloc->SetProperties(pProp, &Actual);

if (FAILED(hr))

{

return hr;

}

// Even when it succeeds, check the actual result.

if (pProp->cbBuffer > Actual.cbBuffer)

{

return E_FAIL;

}

return S_OK;

}

即使SetProperties函数成功,你也要检查结果,以确保满足你的需要

缺省的情况下,所有的filter都采用CMemAllocator类类分配内存,这个类从客户进程的虚拟地址中分配内存,如果你的filter需要其它的内存,比如,DirectDraw表面,你可以派生一个通用的allocator,你可以从CBaseAllocator类派生一个新的类,根据不同的pin使用你的派生的新的allocator类,你需要继承不同的函数,

Input pin: CBaseInputPin::GetAllocator and CBaseInputPin::NotifyAllocator.

Output pin: CBaseOutputPin::DecideAllocator.

如果其它的filter拒绝使用你的custom allocator,你的filter和其它filter连接的时候就会失败,

第五步 传递媒体数据

上游filter通过调用filter上输入pin上的IMemInputPin::Receive方法,将sample传递到filter,filter调用CTransformFilter::Transform方法来处理数据,注意,这个方法也是一个纯虚的函数,你要是想用,你必须提供函数实现。

CTransformFilter::Transform有两个指针,一个指向输入sample,一直只想输出smaple,再调用这个方法之前,要将sample从输入sample拷贝到输出sample。

如果transform返回S_ok,filter就将sample传递到下游的filter。下面的代码演示了RLE encoder如何实现这个函数的,你可以参考一下,当然你的函数和这个是不一样的。要注意

HRESULT CRleFilter::Transform(IMediaSample *pSource, IMediaSample *pDest)

{

// Get pointers to the underlying buffers.

BYTE *pBufferIn, *pBufferOut;

hr = pSource->GetPointer(&pBufferIn);

if (FAILED(hr))

{

return hr;

}

hr = pDest->GetPointer(&pBufferOut);

if (FAILED(hr))

{

return hr;

}

// Process the data.

DWORD cbDest = EncodeFrame(pBufferIn, pBufferOut);

KASSERT((long)cbDest <= pDest->GetSize());

pDest->SetActualDataLength(cbDest);

pDest->SetSyncPoint(TRUE);

return S_OK;

}

需要注意的几个问题

1 时间戳,CTransformFilter在调用Transform方法之前就给输出sample打上了时间戳,它仅仅是从输入的stample上讲时间戳拷贝过来,不做任何的改动,如果你的filter需要改动时间戳,你可以调用输出sample上的IMediaSample::SetTime方法

2 数据格式的改变

上游的filter可以 动态的改变数据的格式,在改动数据的格式之前,它要调用你的输入pin上的IPin::QueryAccept方法,在filter上,这个方法的调用会引起CheckInputType,和CheckTransform的方法的调用。下游的filter也可以改变数据格式,机理和这个一样。

在你的filter中,需要做两件事情

1 )要确保QueryAccept返回正确

2 )如果你的filter不接受数据格式的改变,那么就在你的filter的Transform方法中调用IMediaSample::GetMediaType.方法,如果这个方法返回s_ok,那么你的filter就要适用数据的改变。

3线程,

在CTransformFilter中,filter在Receive方法同步的发送输出sample。Filter没有创建任何的线程来处理数据。

第六步支持COM特性

最后一步是支持com属性

添加com支持的步骤和前面一样,并且在前面也讲述的很清楚了,下面列出必须的几个要素

1 引用计数Reference Counting,接口查询QueryInterface

很简单,从基类派生即可

CMyFilter : public CBaseFilter, public IMyCustomInterface

{

public:

DECLARE_IUNKNOWN

STDMETHODIMP NonDelegatingQueryInterface(REFIID iid, void **ppv);

};

STDMETHODIMP CMyFilter::NonDelegatingQueryInterface(REFIID iid, void **ppv)

{

if (riid == IID_IMyCustomInterface) {

return GetInterface(static_cast<IMyCustomInterface*>(this), ppv);

}

return CBaseFilter::NonDelegatingQueryInterface(riid,ppv);

}

2对象的创建Object Creation

CUnknown * WINAPI CRleFilter::CreateInstance(LPUNKNOWN pUnk, HRESULT *pHr)

{

CRleFilter *pFilter = new CRleFilter();

if (pFilter== NULL)

{

*pHr = E_OUTOFMEMORY;

}

return pFilter;

}

模板数组

static WCHAR g_wszName[] = L"My RLE Encoder";

CFactoryTemplate g_Templates[] =

{

{

g_wszName,

&CLSID_RLEFilter,

CRleFilter::CreateInstance,

NULL,

NULL

}

};

int g_cTemplates = sizeof(g_Templates) / sizeof(g_Templates[0]);

3组件的注册

// Declare media type information.

FOURCCMap fccMap = FCC('MRLE');

REGPINTYPES sudInputTypes = { &MEDIATYPE_Video, &GUID_NULL };

REGPINTYPES sudOutputTypes = { &MEDIATYPE_Video, (GUID*)&fccMap };

// Declare pin information.

REGFILTERPINS sudPinReg[] = {

// Input pin.

{ 0, FALSE, // Rendered?

FALSE, // Output?

FALSE, // Zero?

FALSE, // Many?

0, 0,

1, &sudInputTypes // Media types.

},

// Output pin.

{ 0, FALSE, // Rendered?

TRUE, // Output?

FALSE, // Zero?

FALSE, // Many?

0, 0,

1, &sudOutputTypes // Media types.

}

};

// Declare filter information.

REGFILTER2 rf2FilterReg = {

1, // Version number.

MERIT_DO_NOT_USE, // Merit.

2, // Number of pins.

sudPinReg // Pointer to pin information.

};

STDAPI DllRegisterServer(void)

{

HRESULT hr = AMovieDllRegisterServer2(TRUE);

if (FAILED(hr))

{

return hr;

}

IFilterMapper2 *pFM2 = NULL;

hr = CoCreateInstance(CLSID_FilterMapper2, NULL, CLSCTX_INPROC_SERVER,

IID_IFilterMapper2, (void **)&pFM2);

if (SUCCEEDED(hr))

{

hr = pFM2->RegisterFilter(

CLSID_RLEFilter, // Filter CLSID.

g_wszName, // Filter name.

NULL, // Device moniker.

&CLSID_VideoCompressorCategory, // Video compressor category.

g_wszName, // Instance data.

&rf2FilterReg // Filter information.

);

pFM2->Release();

}

return hr;

}

STDAPI DllUnregisterServer()

{

HRESULT hr = AMovieDllRegisterServer2(FALSE);

if (FAILED(hr))

{

return hr;

}

IFilterMapper2 *pFM2 = NULL;

hr = CoCreateInstance(CLSID_FilterMapper2, NULL, CLSCTX_INPROC_SERVER,

IID_IFilterMapper2, (void **)&pFM2);

if (SUCCEEDED(hr))

{

hr = pFM2->UnregisterFilter(&CLSID_VideoCompressorCategory,

g_wszName, CLSID_RLEFilter);

pFM2->Release();

}

return hr;

}

有时候,你的filter并不总是通过DLL提供的,有时,你可能给一个特定的程序写了一个特定的filter,那么你就可以直接用你的filter,你可以直接用new方法,如下

#include "MyFilter.h" // Header file that declares the filter class.

// Compile and link MyFilter.cpp.

int main()

{

IBaseFilter *pFilter = 0;

{

// Scope to hide pF.

CMyFilter* pF = new MyFilter();

if (!pF)

{

printf("Could not create MyFilter.\n");

return 1;

}

pF->QueryInterface(IID_IBaseFilter,

reinterpret_cast<void**>(&pFilter));

}

/* Now use pFilter as normal. */

pFilter->Release(); // Deletes the filter.

return 0;

}

posted on 2005

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有