Статьи‎ > ‎

Руководство по созданию Transform-фильтра

Автор: Владимир Филиппов

Введение

Технология Microsoft®DirectShow®(DS) является частью DirectX®и предназначена для работы с потоковым мультимедиа — видео и звуком. Используя эту технологию, приложение может воспроизводить и захватывать видео и звук с различных источников, не вдаваясь в аппаратную сущность используемых устройств. Для этого используемые устройства должны иметь WDM-драйвера, позволяющие распознавать их и управлять ими через стандартные интерфейсы DirectShow.

Технология DirectShowоснована на модели компонентных объектов (Component Object Model, COM). Все объекты DSявляются СОМ-серверами.

DirectShow оперирует программными компонентами, называемыми фильтрами. Цепочка фильтров называется графом фильтров, а компонент, управляющий работой фильтров — менеджером графа фильтров (FilterGraphManager, FGM). На рис. 1 показана структура приложения, использующего DS.

Рис. 1. Взаимодействие приложения, DirectShow и драйверов.

Примечание: Кодеки, созданные для работы через интерфейсы AudioCompressionManager (ACM) и VideoCompressionManager (VCM) также поддерживаются, но они работают через «обёртки» (wrappers), наличие которых скрыто от приложения.

DS-фильтры можно разделить на три большие группы: фильтры-источники (sourcefilters), фильтры-преобразователи (transform-filters) и фильтры-визуализаторы (renderingfilters). Каждый фильтр предоставляет набор стандартных интерфейсов, с помощью которых осуществляется настройка этого фильтра или устройства, с которым он связан. В зависимости от типа фильтра интерфейсы могут отличаться. Также фильтр может предоставлять и нестандартные интерфейсы для реализации не заложенных вDSвозможностей или для защиты фильтра от несанкционированного использования.

Соединение фильтров осуществляется с помощью выводов (пинов, pins). Пин характеризуется направлением (входной или выходной) и типом носителя (mediatype). Фильтры могут иметь несколько входных и/или выходных пинов, но должны иметь хотя бы один пин. Тип носителя определяет формат данных, которые могут быть получены с этого пина или переданы на него. Существуют предопределённые (стандартные) типы носителей для звука и видео, хотя приложения могут зарегистрировать и использовать свой тип.

Для реализации своей задачи приложение должно создать экземпляр класса FGM, добавить в него требуемые фильтры и соединить их в цепочку, подключая выходной пин первого фильтра к входному пину второго и т.д. При соединении фильтров FGM проверяет совместимость типов носителей соединяемых выводов и в случае необходимости добавляет промежуточные фильтры, преобразующие тип носителя к требуемому. Такое подключение называется IntelligentConnectи используется в большинстве случаев, хотя и не всегда обеспечивает оптимальное соединение.

Данные между фильтрами передаются в виде сэмплов (sample) — объектов, содержащих помимо собственно буфера с данными, некоторые дополнительные свойства — время начала и окончания сэмпла, тип, признак ключевого кадра (для видео) и т.п.

Каждый сэмпл имеет штамп времени (timestamp), представляющий собой 64-битное целое число. Время измеряется в сотнях наносекунд. Для синхронизации всех фильтров в графе используются системные часы (referenceclock).

Для управления графом фильтров (запуск, останов) и информирования приложения о событиях, возникающих в графе, используются специальные интерфейсы —IMediaControlIMediaEventи другие.

Более подробная информация о технологии DirectShowи всех интерфейсах содержится в справочной системе DirectX 9 SDK.

Структура фильтра

Для создания фильтров необходимо установить пакет DirectX 9 SDK, а также MicrosoftVisualC++. После установки DXSDKв каталоге "Х:\DX90SDK\Samples\C++\DirectShow\Filters" содержатся примеры различных фильтров, в а каталоге "Х:\DX90SDK\Samples\C++\DirectShow\BaseClasses" — необходимые вспомогательные классы и функции, существенно упрощающие процесс написания и отладки фильтров.

Мы рассмотрим пример простейшего фильтра-преобразователя. Базовым классом для таких фильтров являются классы CTransInPlaceFilterили CTransformFilter. Отличие заключается в том, что CTransInPlaceFilterпредназначен для создания фильтров, входной и выходной типы носителей которого полностью совпадают. Этот фильтр использует общий буфер для входного и выходного пинов, чем достигается экономия ресурсов системы. А CTransformFilterпозволяет создавать фильтры, преобразующие тип носителя.

Рассмотрим пример фильтра, который не только не будет вносить никаких изменений в сэмплы, но и вообще никогда не будет включаться в цепочку фильтров. Как ни странно, даже в таком фильтре есть смысл. Утилита graphedt.exe(визуальный инструмент для сборки графа фильтров), входящая в DirectXSDK, позволяет отображать граф фильтров любого приложения, который зарегистрирован в таблице запущенных объектов (Running Object Table, ROT). Это очень удобно для отладочных целей — наглядно видны все фильтры и соединения, для этого достаточно в свою программу вставить регистрацию графа в таблице объектов. Но как быть, если надо «заглянуть» в чужой граф? Вот тут на помощь и приходит IntelligentConnect, упомянутый в начале статьи. Дело в том, что при регистрации фильтра указывается специальный параметр, который называется merit(«достоинство»). Этот целочисленный параметр определяет приоритет фильтра, когда FGM делает поиск промежуточных фильтров при сборке графа. Есть несколько предопределённых значений этого параметра. Если он равен MERIT_DO_NOT_USE или менее, то фильтр не участвует в процессе «умного соединения». Если используется больший merit, то FGMучтёт этот фильтр при сборке графа. Таким образом, если мы укажем большой merit, то велика вероятность, что FGM попытается использовать наш фильтр в первую очередь, при соединении двух пинов, имеющих разный тип носителя. При этом фильтр будет добавлен в граф и ему останется только зарегистрировать граф в таблицу объектов, после чего отклонить попытку соединения. FGM удалит фильтр из графа, но регистрация останется в силе и мы сможем с помощью graphedt.exeизучить содержимое этого графа.

Полный исходный код этого фильтра можно скачать здесь: fvAddToROT.zip. Текст имеет подробные комментарии. Основные моменты также будут разобраны ниже.

С точки зрения ОС, фильтр является обычной динамической библиотекой (DLL), импортирующий определенные функции. Таким образом, для создания фильтра вMicrosoftVisualC++ необходимо создать новый проект типа Win32 Dynamic-LinkLibrary и указать в файле *.defэтого проекта функции:

DllMain

DllGetClassObject

DllCanUnloadNow

DllRegisterServer

DllUnregisterServer

Функции DllGetClassObject и DllCanUnloadNow являются стандартными, их реализация не требуется. Достаточно включить файл windows.hи объявить эти функции в файле *.defвашего проекта.

Функции DllRegisterServerи DllUnregisterServerтребуют простейшей реализации. Здесь же показан код точки входа:

#include <streams.h>

STDAPI DllRegisterServer()

{

return AMovieDllRegisterServer2(TRUE);

}

STDAPI DllUnregisterServer()

{

return AMovieDllRegisterServer2(FALSE);

}

extern "C" BOOL WINAPI DllEntryPoint(HINSTANCE, ULONG, LPVOID);

BOOL APIENTRY DllMain(HANDLE hModule, DWORDdwReason, LPVOID lpReserved)

{

return DllEntryPoint((HINSTANCE)(hModule), dwReason, lpReserved);

}

Функция AMovieDllRegisterServer2() объявлена в файле dllsetup.h, но включать этот файл напрямую не нужно. Необходимо включить файл streams.h, находящийся в каталоге "Х:\DX90SDK\Samples\C++\DirectShow\BaseClasses". Этот каталог, также как и "Х:\DX90SDK\Include" нужно указать в настройках проекта, на вкладке "С/С++", в категории"Preprocessor", в поле "Additionalincludedirectories". Разумеется, вместо "Х:" необходимо указать букву диска, на котором установлен DirectXSDK. Также необходимо на вкладке"Link" указать дополнительные библиотеки. Отметим, что эти библиотеки несколько отличаются для отладочной (Debugbuild) и финальной (Releasebuild) версий фильтра. Подробнее об этом см. в примере фильтра.

Для того чтобы DLL стала фильтром, необходимо объявить глобальный массив g_Templates, содержащий объекты типа CFactoryTemplate. Каждый элемент массива соответствует одному фильтру или СОМ компоненту в библиотеке. Таким образом, в одной DLLможет содержаться любое количество фильтров (и вообще любых СОМ-серверов).

Регистрация и идентификация любого СОМ компонента происходит с помощью идентификатора класса (CLSID), который представляет собой глобальный уникальный идентификатор (GUID). Для генерации GUIDиспользуем утилиту GUIDGEN.EXE, входящую в MicrosoftDeveloperStudioи находящуюся в подкаталоге "Common\Tools"каталога установки MSDS.

В массиве g_Templates также указывается имя фильтра (строка в формате Unicode), адрес процедуры создания экземпляра фильтра и адрес структуры типа AMOVIESETUP_FILTER, которая, в свою очередь, содержит информацию о пинах фильтра величине параметра merit. Полностью регистрационная информация фильтра показана в следующем листинге:

staticconst WCHAR g_wszName[] = L"fv Add Graph To ROT Sample Filter";

const AMOVIESETUP_MEDIATYPE sudPinTypes =

{

&MEDIATYPE_Video,// Главныйтип (видео, аудиоит.п.)

&MEDIASUBTYPE_NULL// Подтип (формат видео, формат аудио и т.д.)

};

const AMOVIESETUP_PIN psudPins[] =

{

{

L"Input",// Имя пина (не используется)

FALSE,//

FALSE,// Выходной пин?

FALSE,// Фильтр может создавать пустой экземпляр

FALSE,// Фильтр может создавать более одного экземпляра

&CLSID_NULL,// Не используется

L"Output",// Не используется

1,// Кол-во поддерживаемых типов носителей для пина

&sudPinTypes },// Поддерживаемые типы носителей для пина

{ L"Output",// Имя пина (не используется)

FALSE,//

TRUE,// Выходной пин?

FALSE,// Фильтр может создавать пустой экземпляр

FALSE,// Фильтр может создавать более одного экземпляра

&CLSID_NULL,// Не используется

L"Input",// Не используется

1,// Кол-во поддерживаемых типов носителей для пина

&sudPinTypes// Поддерживаемые типы носителей для пина

}

};

const AMOVIESETUP_FILTER sudfvAddToRotFilter =

{

&CLSID_fvAddToRot,// Идентификатор класса (CLSID) фильтра

g_wszName,// Имя фильтра

0xA0000000,// Параметр merit для фильтра !!!

2,// Количество пинов

psudPins// Описание пинов

};

CFactoryTemplate g_Templates[] =

{

{ g_wszName// Имяфильтра

, &CLSID_fvAddToRot// Идентификаторкласса

, CfvAddToRot::CreateInstance// Статическийметод, создающийэкземпляркласса

, NULL// Адрес процедуры, вызываемой из точки входа в DLL

, &sudfvAddToRotFilter }// Информацияофильтре

};

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

Обратим внимание на значение параметра merit. Его можно указать любым, большим либо равным MERIT_PREFERRED (0х800000). Идентификатор CLSID_fvAddToRotобъявлен в отдельном файле fvuids.h:

// {2C5686DC-EAF6-4de2-97B4-651368BD9A7C}

DEFINE_GUID(CLSID_fvAddToRot,

0x2c5686dc, 0xeaf6, 0x4de2, 0x97, 0xb4, 0x65, 0x13, 0x68, 0xbd, 0x9a, 0x7c);

Теперь перейдём к объявлению класса фильтра и реализации его методов. Для этого объявим класс-наследник CTransformFilter и реализуем как минимум два виртуальных метода — CTransformFilter::CheckInputType() и CTransformFilter::Transform():

// CfvAddToRot prototype

class CfvAddToRot:public CTransInPlaceFilter

{

public:

// Созданиеэкземпляракласса

static CUnknown * WINAPI CreateInstance(LPUNKNOWN punk, HRESULT *phr);

// Проверка входного формата носителя — поддерживает ли фильтр данный формат

HRESULT CheckInputType(const CMediaType* mtIn);

// Этот метод вызывается при добавлении фильтра в граф фильтров

STDMETHODIMP JoinFilterGraph(IFilterGraph *pGraph, LPCWSTR pName);

private:

// Конструктор — в нём инициализируем внутренние переменные,

// считываем параметры из реестра и т.п.

CfvAddToRot(TCHAR *tszName, LPUNKNOWN punk, HRESULT *phr);

// Деструктор — в нём сохраняем параметры в реестре, особождаем

// выделенную память, какие-либо полученные интерфейсы и т.п.

~CfvAddToRot();

// В этом методе осуществляются изменения в поступающих сэмплах

HRESULT Transform(IMediaSample *pMediaSample);

// Добавлениеграфафильтровв ROT

HRESULT AddToRot(IUnknown *pUnkGraph, DWORD *pdwRegister);

};

Алгоритм работы нашего фильтра очень прост.

Благодаря высокому значению merit, фильтр будет добавляться в граф любой программы, которая при его сборке использует Intelligent Connect. При этом будет вызываться метод HRESULT JoinFilterGraph(IFilterGraph *pGraph, LPCWSTR pName), который мы также должны реализовать. В этом методе граф будет добавляться в ROT. Поскольку фильтр может быть добавлен в граф несколько раз, мы должны предусмотреть механизм, препятствующий многократному внесению одного и того же графа в ROT.

После добавления фильтра в граф FGM попытается соединить выходной пин какого-то фильтра к входному пину нашего фильтра. Мы должны отклонить любой входной формат, после чего FGM удалит фильтр из графа. На этом работа фильтра будет завершена.

Реализация четырёх основных методов и метода добавления графа в ROT показана ниже:

// CreateInstance

// Создаёмэкземплярфильтра

CUnknown * WINAPI CfvAddToRot::CreateInstance(LPUNKNOWN punk, HRESULT *phr)

{

CfvAddToRot *pNewObject = new CfvAddToRot(NAME("fv Add Graph To ROT Sample Filter"), punk, phr);

if (pNewObject == NULL) {

if (phr) *phr = E_OUTOFMEMORY;

}

return pNewObject;

}

// Проверка входного формата.

// Мы должны отклонить любой формат носителя

HRESULT CfvAddToRot::CheckInputType(const CMediaType* mtIn)

{

return E_INVALIDARG;

}

STDMETHODIMP CfvAddToRot::JoinFilterGraph(IFilterGraph *pGraph, LPCWSTR pName);

{

DWORD dwRot;

CAutoLock cAutoLock(&m_fvLock);

AddToRot(pGraph,&dwRot);

return CBaseFilter::JoinFilterGraph(pGraph,pName);

}

HRESULT CfvAddToRot::AddToRot(IUnknown *pUnkGraph, DWORD *pdwRegister)

{

// Создаём Event и проверяем его состояние. Если он сброшен , значит

// он создан первый раз и граф нужно добавить в ROT.

// Если Event установлен, значит граф уже добавлен и ничего делать не

// надо.

HANDLE g_hEvent=CreateEvent(NULL,TRUE,FALSE,"fvAddToRotEvent001");

if (WaitForSingleObject(g_hEvent,0)==WAIT_OBJECT_0) return S_OK;

SetEvent(g_hEvent);

// Добавляем граф в таблицу запущенных объектов

IMoniker * pMoniker;

IRunningObjectTable *pROT;

if (FAILED(GetRunningObjectTable(0, &pROT))) {

return E_FAIL;

}

WCHAR wsz[256];

wsprintfW(wsz, L"FilterGraph %08x pid %08x", (DWORD_PTR)pUnkGraph, GetCurrentProcessId());

HRESULT hr = CreateItemMoniker(L"!", wsz, &pMoniker);

if (SUCCEEDED(hr)) {

hr = pROT->Register(ROTFLAGS_REGISTRATIONKEEPSALIVE, pUnkGraph,

pMoniker, pdwRegister);

pMoniker->Release();

}

pROT->Release();

return hr;

}

// Этот метод вызывается при поступлении каждого сэмпла

// на вход фильтра. В данном случае никакие действия не нужны

HRESULT CfvAddToRot::Transform(IMediaSample *pMediaSample)

{

return NOERROR;

}

Заметим, что приведённый способ предотвращения повторного включения графа в ROT автоматически препятствует добавлению в ROT двух или более графов из одного приложения или даже из разных приложений, т.к. объект Event является глобальным. Поэтому для работы со вторым приложением необходимо завершить работу с первым, уничтожая тем самым созданныйEvent. Чтобы обойти это ограничение достаточно сделать, чтобы имя события зависело от графа, например, можно добавить к константной части имени ("fvAddToRotEvent001") строку, образованную конкатенацией строковых представлений идентификатора процесса и адреса графа.

Компиляция, установка и использование

Для компиляции и сборки фильтра, как уже говорилось выше, необходимо указать требуемые заголовочные файлы и библиотеки. В настройках проекта для отладочной версии указываются библиотеки strmbasd.lib и msvcrtd.lib, а для финальной (релизной) версии — strmbase.lib и msvcrt.lib. В приведённом примере обратите внимание на пути к этим файлам и если они не соответствуют вашим — исправьте их на правильные.

Регистрация фильтра проводится как обычно — с помощью стандартной утилиты regsvr32.exe. Не обязательно копировать фильтр в каталог "<WinDir>\System32", можно зарегистрировать его в том каталоге, в котором он находится после компиляции. Для этого, находясь в этом каталоге, в командной строке выполняем команду:

regsvr32 fvAddToRot.ax

Можно также воспользоваться прилагающимся в архиве с фильтром пакетным файлом install.bat (тогда фильтр будет скопирован в системный каталог). Если всё было сделано правильно, то появится сообщение об успешной регистрации. Теперь можно запустить любое приложение, использующее DS, например, FlyDS. При запуске не произойдёт ничего необычного, программа запустится и будет работать как обычно.

Далее, запускаем утилиту graphedt.exe из DirectX SDK и выбираем команду меню File->Connect to Remote Graph (илинажимаемклавишиCtrl+G). В появившемся списке должен присутствовать искомый граф:

Рис. 2. Подключение к удалённому графу.

Выбираем этот граф, нажимаем ОК и видим его содержимое:

Рис. 3. Фильтры в удалённом графе.

Разумеется, набор и подключение фильтров может отличаться от используемого устройства захвата. Таким же образом можно исследовать графы других программ, в том числе и своего собственного авторства, что упрощает отладку и поиск ошибочно подключённых фильтров.

Управлять удалённым графом не удастся, можно только просматривать свойства фильтров.

Разрегистрировать фильтр можно командой:

regsvr32 /u fvAddToRot.ax

Добавление страницы свойств

Чтобы создать страницу свойств фильтра, которую сможет «увидеть» и отобразить любое DS-приложение, необходимо сделать следующее:

<!--[if !supportLists]-->§<!--[endif]-->создать класс на основе CBasePropertyPage;

<!--[if !supportLists]-->§<!--[endif]-->добавить в проект файл ресурсов и создать в нём ресурс типа Dialog, в котором разместить все требуемые элементы управления;

<!--[if !supportLists]-->§<!--[endif]-->создать идентификатор класса (CLSID) и добавить его в массив g_Templates;

<!--[if !supportLists]-->§<!--[endif]-->добавить к описанию фильтра базовый класс ISpecifyPropertyPages и реализовать метод HRESULT GetPages(CAUUID *pPages).

Создадим для нашего фильтра страницу свойств, в которой можно будет изменять один параметр — активность фильтра, т.е. будет ли он добавлять граф в ROT или нет.

Массив g_Templates должен принять следующий вид:

CFactoryTemplate g_Templates[] =

{

{ g_wszName

, &CLSID_fvAddToRot

, CfvAddToRot::CreateInstance

, NULL

, &sudfvAddToRotFilter }

,

{ L"fv Add To ROT Property Page"

, &CLSID_fvAddToRotPropertyPage

, CAddToRotProperties::CreateInstance }

};

Так же как для фильтра, создадим идентификатор класса для страницы свойств (CLSID_fvAddToRotPropertyPage) и объявим его в файле fvuids.h. Объявим и реализуем метод GetPages() в классе фильтра:

STDMETHODIMP CfvAddToRot::GetPages(CAUUID *pPages)

{

CheckPointer(pPages,E_POINTER);

pPages->cElems = 1;

pPages->pElems = (GUID *)CoTaskMemAlloc(sizeof(GUID));

if (pPages->pElems == NULL) return E_OUTOFMEMORY;

*(pPages->pElems) = CLSID_fvAddToRotPropertyPage;

return NOERROR;

}

В конструкторе класса CAddToRotProperties указывается идентификатор ресурса Dialog, который будет добавлен на создаваемую страницу свойств. Кнопки OKCancel и Applyсоздавать не надо, они будут созданы автоматически. В свойствах ресурса Dialog необходимо установить свойство Style в Child, а Border в None.

Чтобы DS-приложение «увидело» страницу свойств, необходимо реализовать ещё один метод в классе фильтра — HRESULT NonDelegatingQueryInterface(REFIID riid, void **ppv):

STDMETHODIMP CfvAddToRot::NonDelegatingQueryInterface(REFIID riid, void **ppv)

{

CheckPointer(ppv,E_POINTER);

if (riid == IID_ISpecifyPropertyPages) {

return GetInterface((ISpecifyPropertyPages *) this, ppv);

} else {

return CTransformFilter::NonDelegatingQueryInterface(riid, ppv);

}

}

Если фильтр должен иметь несколько страниц свойств, то для каждой из них необходимо создать свой ресурс Dialog, свой класс, основанный на CBasePropertyPage и GUIDдля него. Затем их также необходимо добавить в метод NonDelegatingQueryInterface(), а метод GetPages() должен возвращать правильное число страниц свойств в поле pPages->cElemsи соответствующие GUIDв поле pPages->pElems.

Теперь рассмотрим механизм взаимодействия страницы свойств с самим фильтром, позволяющий запрашивать и изменять некоторые параметры фильтра. Для этого объявим ещё один класс (назовём его IfvAddToRot), в который поместим требуемые методы для запроса и установки параметров. Для этого класса также потребуется свойCLSID. Эти описания поместим в отдельный файл ifvAddToRot.h.

#ifndef __IfvAddToRot__

#define __IfvAddToRot__

#ifdef __cplusplus

extern "C" {

#endif

// {858EBD92-578B-439e-965A-99CA383F88FC}

DEFINE_GUID(IID_IfvAddToRot,

0x858ebd92, 0x578b, 0x439e, 0x96, 0x5a, 0x99, 0xca, 0x38, 0x3f, 0x88, 0xfc);

DECLARE_INTERFACE_(IfvAddToRot, IUnknown)

{

STDMETHOD(get_Active) (THIS_

int *bActive

) PURE;

STDMETHOD(put_Active) (THIS_

int bActive

) PURE;

};

#ifdef __cplusplus

}

#endif

#endif

Класс IfvAddToRot также добавим в список базовых классов для фильтра и сделаем реализации виртуальных методов:

STDMETHODIMP CfvAddToRot::get_Active(int *bActive)

{

CAutoLock cAutoLock(&m_fvLock);

CheckPointer(bActive,E_POINTER);

*bActive=bIsActive;

return NOERROR;

}

STDMETHODIMP CfvAddToRot::put_Active(int bActive)

{

CAutoLock cAutoLock(&m_fvLock);

bIsActive=bActive;

bParamChanged=TRUE;

return NOERROR;

}

Переменная bActive также объявлена в классе фильтра (в секции private) и перед добавлением графа в ROT проверяется её состояние, если она равна FALSE, то граф в ROT не добавляется.

Отдельно остановимся на операторе CAutoLock cAutoLock(&m_fvLock). Он необходим для предотвращения коллизий, связанных с обращением к одним и тем же переменным из разных нитей процесса. Этот оператор ожидает освобождения критической секции m_fvLock и когда она освобождается, устанавливает над ней владение. При попытке захватить владение из другой нити, она будет приостановлена вплоть до освобождения критической секции текущим владельцем. Т.к. переменная cAutoLock локальная для данного метода, то освобождение критической секции происходит автоматически при выходе из этого метода.

Чтобы у фильтра можно было запросить интерфейс IfvAddToRot, метод NonDelegatingQueryInterface() нужно дополнить следующим образом:

STDMETHODIMP CfvAddToRot::NonDelegatingQueryInterface(REFIID riid, void **ppv)

{

CheckPointer(ppv,E_POINTER);

if (riid == IID_ISpecifyPropertyPages) {

return GetInterface((ISpecifyPropertyPages *) this, ppv);

} else {

if (riid == IID_IfvAddToRot) {

return GetInterface((IfvAddToRot*) this, ppv);

} else {

return CTransformFilter::NonDelegatingQueryInterface(riid, ppv);

}

}

Вернёмся вновь в странице свойств. При подключении к фильтру вызывается метод OnConnect(), которому передаётся указатель на объект фильтра. Именно у этого объекта мы должны запросить интерфейс IfvAddToRot, через который будем получать (в методе OnActivate()) и изменять (в OnApplyChanges()) состояние активности. Освободить интерфейс нужно в методе OnDisconnect(). Изменение внутренней переменной bIsActive осуществляется в методе OnReceiveMessage(), который вызывается при поступлении сообщения окну свойств.

HRESULT CAddToRotProperties::OnConnect(IUnknown *pUnknown)

{

CheckPointer(pUnknown,E_POINTER);

HRESULT hr = pUnknown->QueryInterface(IID_IfvAddToRot, (void **)&m_pfvAddToRotCfg);

if (FAILED(hr)) return E_NOINTERFACE;

return NOERROR;

}

HRESULT CAddToRotProperties::OnDisconnect()

{

if (m_pfvAddToRotCfg) {

m_pfvAddToRotCfg->Release();

m_pfvAddToRotCfg = NULL;

}

return NOERROR;

}

HRESULT CAddToRotProperties::OnActivate()

{

if (m_pfvAddToRotCfg) m_pfvAddToRotCfg->get_Active(&bIsActive);

CheckDlgButton(m_hwnd, IDC_ISACTIVE, bIsActive);

return NOERROR;

}

HRESULT CAddToRotProperties::OnApplyChanges()

{

if (m_pfvAddToRotCfg) return m_pfvAddToRotCfg->put_Active(bIsActive);

return E_UNEXPECTED;

}

INT_PTR CAddToRotProperties::OnReceiveMessage(HWND hwnd,

UINT uMsg,

WPARAM wParam,

LPARAM lParam)

{

switch (uMsg) {

case WM_COMMAND: {

if (LOWORD(wParam) == IDC_ISACTIVE) {

// Пользователь щёлкнул по флажку на странице свойств

bIsActive=IsDlgButtonChecked(hwnd,IDC_ISACTIVE);

SetDirty();

return (LRESULT)1;

}

}

}

return CBasePropertyPage::OnReceiveMessage(hwnd,uMsg,wParam,lParam);

}

В приведённом фрагменте код метода OnReceiveMessage() сокращён. Подробнее см. прилагаемый пример фильтра.

Чтобы страница свойств отображалась на разных языках, в зависимости от языка ОС пользователя, достаточно сделать в файле ресурсов несколько одинаковых ресурсов Dialog, свой для каждого языка. Тогда при создании диалога будет автоматически загружен требуемый ресурс. Заголовок окна свойств, который задаётся в конструкторе CBasePropertyPage(), нужно поместить в таблицы строк (String Table), также разные для разных языков.

Заметим, что класс IfvAddToRot можно также использовать для простейшей защиты фильтра от использования в чужих приложениях. Допустим, вы разрабатываете DS-приложение и DS-фильтр, работающие в паре, и не хотите, чтобы этот фильтр использовался другими программами. Тогда в классе IfvAddToRot можно объявить метод, изменяющий некоторую внутреннюю переменную фильтра, который будет вызываться вашим приложением при создании экземпляра фильтра. Затем, во время запуска или работы, фильтр будет проверять состояние этой переменной, что позволит ему определить, используется ли он в «родном» приложении или нет.

Сохранение параметров фильтра

Для сохранения параметров фильтра можно добавить в конструктор и деструктор фильтра сохранение и считывание параметров в/из реестра. Тогда настройки фильтра, установленные на странице свойств, будут сохраняться и использоваться при следующем запуске приложения, использующего фильтр. Пример такого варианта сохранения приведён в рассматриваемом фильтре:

// Constructor for CfvAddToRot

CfvAddToRot::CfvAddToRot(TCHAR *tszName,LPUNKNOWN punk,HRESULT *phr):

CTransInPlaceFilter(tszName, punk, CLSID_fvAddToRot, phr),

bParamChanged(FALSE),

bIsActive(TRUE)

{

//Считываниепараметровизреестра

HKEY hKey;

if (RegOpenKeyEx(HKEY_CURRENT_USER, "Software\\FVG Programs\\fvAddToRotFilter", 0, KEY_READ, &hKey)==ERROR_SUCCESS) {

unsigned long cbData=4;

RegQueryValueEx(hKey,"bIsActive",0,NULL,(unsigned char*)&bIsActive,&cbData);

RegCloseKey(hKey);

bParamChanged=TRUE;

}

}

// Destructor for CfvAddToRot

CfvAddToRot::~CfvAddToRot()

{

//Запись параметров в реестр

HKEY hKey;

unsigned long dwDisposition;

if (bParamChanged) {

if (RegCreateKeyEx(HKEY_CURRENT_USER, "Software\\FVG Programs\\fvAddToRotFilter", 0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, &dwDisposition)==ERROR_SUCCESS) {

RegSetValueEx(hKey,"bIsActive",0,REG_DWORD,(unsigned char*)&bIsActive,4);

RegCloseKey(hKey);

}

}

}

Но у такого метода есть существенный недостаток — при использовании фильтра в разных программах не удастся сохранять параметры независимо. Кроме того, если программа позволяет использовать несколько пользовательских профилей (наборов настроек), то в разных профилях не удастся задать разные параметры (к таким программам относится, например, iuVCR).

Чтобы решить эту проблему, достаточно переложить сохранение настроек фильтра на само приложение, которое использует фильтр. Для этого многие приложения (в том числеiuVCR и graphedt.exe) используют интерфейс IPersistStream, который должен экспортировать фильтр. Приложение запрашивает этот интерфейс и через его методы передаёт указатель на объект IStream, в который фильтр сохраняет или из которого считывает свои параметры в виде бинарного потока. Приложение только лишь сохраняет содержимое этого потока. Где и как сохраняется поток — зависит от приложения, для фильтра это значения не имеет.

Вместо непосредственной реализации методов интерфейса IPersistStream, проще воспользоваться классомCPersistStream, с помощью которого можно упростить сохранение и считывание параметров из потока. Необходимо добавить класс CPersistStreamв список базовых классов фильтра и внести в метод NonDelegatingQueryInterface() изменения, чтобы при запросе интерфейса IID_IPersistStream возвращался указатель на фильтр, также как это было сделано для IfvAddToRot. Затем необходимо реализовать методы интерфейса СPersistStream. Здесь мы их рассматривать не будем, см. их реализацию в примере фильтра.

Отладка фильтра

Для отладки фильтров в DirectShowреализовано несколько функций и макросов, предназначенных для отображения различной информации. Для их использования достаточно подключить файл streams.h. В финальной версии (releasebuild) эти макросы не выполняют никаких действий, поэтому не обязательно их удалять из исходного кода после завершения отладки. Обратите внимание, что в отладочной версии следует использовать библиотеку "Х:\DX90SDK\Samples\C++\DirectShow\BaseClasses\Debug\strmbasd.lib", а в финальной — "Х:\DX90SDK\Samples\C++\DirectShow\BaseClasses\Release\strmbase.lib".

Простейший отладочный макрос — ASSERT(cond). Если его аргумент равен нулю (FALSE), то выводится сообщение (блокирующее работу нити, из которой был вызван макрос), в котором приводится имя исходного файла, номер строки и сам оператор ASSERT(). Этот макрос может только указать место ошибки в коде программы, но с его помощью не удастся вывести информацию о состоянии переменных.

Более широкие возможности отладки предоставляют следующие функции и макросы:

Имя

Описание

void DbgSetModuleLevel(

DWORD Types,

DWORD Level

);

Установка уровня отладки для одного или более типа сообщений.

Параметры

Types — комбинация одного или более типа сообщений;

Level — устанавливаемый уровень отладки для указанных типов сообщений.

BOOL DbgCheckModuleLevel(

DWORD Types,

DWORD Level

);

Проверяет, разрешён ли вывод сообщений для указанных типов сообщений и уровня отладки.

void DbgDumpObjectRegister(void);

Отображает информацию об активных объектах, являющихся потомками класса CBaseObject, которые были созданы в данном модуле и не были уничтожены.

Функция генерирует несколько сообщений типа LOG_MEMORY. При уровне отладки 1 выводится только число объектов. При уровне отладки 2 и выше выводится список объектов (имена объектов задаются в конструкторе CBaseObject).

void DbgLog((

DWORD Types,

DWORD Level,

const TCHAR *pFormat,

...

));

Вывод строки, если отладка разрешена для указанного типа сообщения и уровня отладки.

Параметры

Types — комбинация одного или более типа сообщений;

Level — уровень отладки для данного сообщения. Сообщение будет выведено только в том случае, если текущий уровень отладки для данного типа сообщения равен или выше указанному в этом параметре;

pFormat — отформатированный по правилам printf() текст сообщения;

... — дополнительные аргументы для отформатированного сообщения.

void DbgOutString(

LPCTSTR psz

);

Вывод строки, независимо от текущего уровня отладки.

void DisplayType(

LPTSTR label,

const AM_MEDIA_TYPE *pmtIn

);

Отображает информацию о типе носителя (MediaType).

Параметры

label— строка, выводящаяся вместе с информацией о типе носителя;

pmtIn— указатель на структуру AM_MEDIA_TYPE.

Функция генерирует несколько сообщений типа LOG_TRACE. При уровне отладки 2 или выше выводятся следующие поля структуры AM_MEDIA_TYPE: главный тип (majortype), подтип (subtype), тип формата (formattype) и данные из блока формата (formatblock). При уровне отладки 5 или выше выводится также дополнительная информация.

void DumpGraph(

IFilterGraph *pGraph,

DWORD dwLevel

);

Выводит информацию о графе фильтров. Параметр dwLevel определяет уровень отладки. Функция генерирует сообщения типа LOG_TRACE с указанным уровнем.

char* GuidNames[guid]

Глобальный массив строк, соответствующий идентификаторам (GUID), объявленным в uuids.h.

Если указан GUID, отсутствующий в uuids.h, то будет возвращена строка "Unknown GUID Name".

NAME(strLiteral)

В отладочной версии эквивалентен макросу TEXT(). В финальной версии возвращает (TCHAR*)NULL. Используется для задания имён объектов, произошедших от CBaseObject.

Каждое отладочное сообщение относится к одному или нескольким типам, причём для каждого из типов можно задать свой уровень отладки. Это позволяет легко манипулировать выводом определённых сообщений и игнорированием других, что помогает легче ориентироваться в отладочных сообщениях. Т.е., если текущий уровень отладки для используемого типа сообщения ниже, чем указан в функции вывода сообщения, то сообщение не выводится.

В DirectShowобъявлено несколько предопределённых типов сообщений (LOG_ERROR, LOG_LOCKING, LOG_MEMORY, LOG_TIMING, LOG_TRACE), а также пять пользовательских (с CUSTOM1 по CUSTOM5). Несмотря на свои названия, конкретное использование для каждого из типов можно выбирать самому.

Значения уровней отладки и расположение отладочных сообщений указываются в реестре. В разделе "HKLM\SOFTWARE\Debug" создаётся подраздел с именем, совпадающим с именем исполняемого файла (в рассматриваемом примере — "fvAddToRot.ax"), в котором находятся параметры, задающие уровни отладки по умолчанию. Текстовый параметр"LogToFile"определяет куда будут выводиться сообщения. Если он равен Console, то вывод будет осуществляться в консольное окно. Если параметр равен Deb, Debug, Debugger или пустой строке, то вывод будет происходить в окно отладчика. Чтобы выводить сообщения в файл, необходимо указать в этом параметре полный путь к файлу, например"C:\Temp\fvAddToRotLog.txt". Отладочные сообщения будут всегда добавляться в файл, без его удаления, поэтому не забывайте иногда удалять его вручную.

Класс CDisp также может быть использован для отладки, т.к. позволяет преобразовывать различные данные в текстовое представление. Этот класс имеет несколько конструкторов с разными параметрами. Класс может быть приведён к типу LPCTSTR. В таблице приведён список возможных параметров для конструкторов:

Конструктор

Описание

CDisp(

LONGLONG ll,

intFormat = CDISP_HEX

);

Переводит в текстовое представление 64-битное целое число ll, в зависимости от параметра Format. ЕслиFormatравенCDISP_HEX, то переводит в шестнадцатеричное представление, если CDISP_DEC, то в десятичное.

CDisp(

REFCLSID clsid

);

Отображает любой GUIDв виде строки.

CDisp(

CRefTime t

);

Отображает время.

CDisp(

IPin *pPin

);

Отображает информацию о пине, как {filter clsid}(pin name).

CDisp(

IUnknown *pUnk

);

Отображает информацию о фильтре или о пине,

Примеры использования класса CDisp:

// Отображение 64-битной переменной llValueв шестнадцатеричном виде:

DbgLog((LOG_TRACE, 3, TEXT("Value is %s"), (LPCTSTR)CDisp(llValue, CDISP_HEX)));

// Отображениелюбого GUID:

DbgLog((LOG_TRACE, 3, TEXT("Filter CLSID is %s"), (LPCTSTR)CDisp(CLSID_fvAddToRot)));

Для запуска отладочной версии фильтра на другом компьютере, необходимо перенести на него библиотеку msvcrtd.dll, которая входит в MicrosoftVisualStudioи которой может не оказаться на целевой системе.

Comments