Статьи‎ > ‎Фильтры‎ > ‎

Фильтры NullNull, Dump, NullIP, InfTee и др.

Несколько примеров

     Исследуя гороизонты  DirectShow приходим к изучению фильтров.  Эти компоненты применяются при считывании данных с диска (фильтры источников/Sources) и при преобразования их для отрисовки (трансформационные фильтры/Transform). Процесс отрисовки называется рендерингом (Render). Сами фильтры обычно имеют расширение .ax и содержатся в dll-файлах.

     Майкрософт с DirectShow поставляют 20 примеров фильтров (которые возможно найти в каталоге ...\DX90SDK\Samples\C++\DirectShow\Filters). Авторы пакета DSPack часть из них перевели в Delphi и дают возможность познакомиться с исходниками. Это будет хорошей иллюстрацией для нашего подробного рассмотрения.

NullNull

     Простейшую структуру фильтра можно увидеть на примере NullNull (DSPack\Demos\D6-D7\Filters\NullNull) Так как основную функциональность несут на себе базовые классы этот фильтр просто пропускает через себя данные ничего с ними не делая.

      Рассмотрим его реализацию. Базовый класс для такого фильтра есть TBCTransInPlaceFilter:

Рис. 1. Иерархия классов для фильтра CTransInPlaceFilter

     Для базового класса нам понадобится переопределить только два метода:  Transfom - в нашем случае он  виртуальный, предназначен лишь для возвращения значения типа HRESULT. Возвращаем (NOERROR) и делать в нем ничего не будем;  CheckInputType - проверяет, подходит ли переданный в параметре указатель на видеотип для входа - всегда отвечаем что подходит. Таким образом, реализация фильтра будет иметь следующий вид:

...
interface
...
type
TMyClass = class(TBCTransInPlaceFilter)
function Transform(Sample: IMediaSample): HRESULT; override;
function CheckInputType(mtin: PAMMediaType): HRESULT; override;
end;
...
implementation
...
function TMyClass.CheckInputType(mtin: PAMMediaType): HRESULT;
begin
result := S_OK;
end;
function TMyClass.Transform(Sample: IMediaSample): HRESULT;
begin
result := NOERROR;
end;
...

Далее регистрируем фильтр в системе.  Соответствующий CLSID в исходниках есть,  пишем в секции initialization:

initialization
TBCClassFactory.CreateFilter(TMyClass, 'Null-Null', CLSID_MyClass,
	CLSID_LegacyAmFilterCategory, MERIT_DO_NOT_USE, 2, @MyPins); 

Фнкция CreateFilter принимает следующие параметры:

constructor TBCClassFactory.CreateFilter(ComClass: TBCUnknownClass; Name: string; 
const ClassID: TGUID; const Category: TGUID;
Merit: LongWord; PinCount: Cardinal;
Pins: PRegFilterPins);
ComClass: TBCUnknownClass - класс нашего фильтра
Name: string; - имя фильтра
const ClassID: TGUID; - GUID
const Category: TGUID; - категория
Merit: LongWord; тот самый merit, о котором уже упоминалось
	(ссылку на него поставить здесь)
PinCount: Cardinal - количество контактов
Pins: PRegFilterPins - указатель на контакты, которые будут присутствовать
									в фильтре.
В нашем случае это @MyPins, смотрим, MyPins - константа, объявленная как
MyPins : array[0..1] of TRegFilterPins = (
(strName: 'Input'; bRendered: FALSE; bOutput: FALSE; bZero: FALSE; bMany: FALSE;
oFilter: nil; strConnectsToPin: nil; nMediaTypes: 1; lpMediaType: @MyPinTypes), 
(strName: 'Output'; bRendered: FALSE; bOutput: TRUE; bZero: FALSE; bMany: FALSE;
oFilter: nil; strConnectsToPin: nil; nMediaTypes: 1; lpMediaType: @MyPinTypes));

     Подробнее узнать о значениях элементов структуры TRegFilterPins можно в справке.

     После удачной компиляции проекта мы должны получить файл с именем NullNull.ax, регистрируем его из командной строки: regsvr32 NullNull.ax. Затем запускаем GraphEdit и строим граф. Например такой:

Рис. 2. Граф фильтров, включающий фильтр Null-Null

Dump

Следующий интересующий нас фильтр называется Dump (DSPack\Demos\D6-D7\Filters\Dump). Он  предназначен для записи на диск данных, поступивших от предыдущего фильтра. Dump хорошо подходит для проверки работы трансформационного фильтра. Это так же можно сделать дебагированием самих трансформационных фильтров.

Dump дожен иметь один входящий контакт и не иметь исходящих (приходящую ему информацию он будет записывать в указанный нами файл). Входящий контакт будет наследником интерфейса контакта, т.е. IPin, а еще точнее TBCRendererInputPin:

Рис. 3. Иерархия классов для контакта CRenderInputPin

Связка контакт необходима для того что бы фильтр функционировал как единое целое. Разрабатывая фильтры о контактах забывать нельзя. Наш фильтр является наследником  базового класса TBCBaseFilter и интерфейса IFileSinkFilter.  IFileSinkFilter разработан специально для фильтров записывающих медиапоток в файлы (он так же заменим на более поздний IFileSinkFilter2) и происходит от IUnknown. Ко всем известным уже методам  он объявляет свои: SetFileName и GetCurFile, о предназначении которых можно догадаться из названия. Нам предстоит их реализовать.

Начнем разбираться с классом TDump не забывая о том, что нам потребуются и некоторые методы класса TMyPin (это будет наш входящий контакт), но их разбор мы пока отложим.

Итак, смотрим файл ..Dump\Main.pas:

TDump = class (TBCBaseFilter,IFileSinkFilter) 
private
xxx: integer;
yyy: integer;
FPin: TMyPin;
FfileName: String;
protected
function SetFileName(pszFileName: PWideChar; pmt: PAMMediaType): HRESULT; stdcall;
function GetCurFile(out ppszFileName: PWideChar; pmt: PAMMediaType): HRESULT; stdcall;
public
function Stop: HRESULT; override;
function Run(tStart: TReferenceTime): HRESULT; override;
function GetPin(n: Integer): TBCBasePin; override;
constructor Create(Name: string; // Object description Unk : IUnKnown;
// IUnknown of delegating object Lock: TBCCritSec;
// Object who maintains lock const clsid: TGUID
// The clsid to be used to serialize this filter
); function GetPinCount: integer; override;
end;

Смотрим на приватные члены:

xxx и yyy используются для внутренних нужд и играют роль флагов.
FPin - входящий контакт
FfileName - имя файла, куда будет вестись запись.
В защищенной (protected) секции содержится объявление функций для упомянутого выше
интерфейса IFileSinkFilter: SetFileName и GetCurFile.
В общедоступной (public) секции содержится объявление функций:
Stop - остановка графа фильтров
Run - запуск графа фильтров
GetPin - выдача контакта
constructor Create(Name: string; // Object description Unk : IUnKnown; // IUnknown of delegating object Lock: TBCCritSec; // Object who maintains lock const clsid: TGUID // The clsid to be used to serialize this filter
); // - конструктор
GetPinCount - выдача количества контактов

Рассмотрим реализацию функций:

function TDump.GetPinCount: integer; 
begin
result := 1; // поскольку у нас есть только один контакт, то об этом и извещаем end;

     В конструкторе  вызываем конструктор базового класса:

constructor TDump.Create(Name: string; 	// Object description Unk : IUnKnown; // IUnknown of delegating object Lock: TBCCritSec; // Object who maintains lock const clsid: TGUID // The clsid to be used to serialize this filter 
);
begin
inherited create(Name,Unk,Lock,CLSID_DumpNew);
end;

    Если контакта нет, то создаем его. Далее возвращаем FPin.

function TDump.GetPin(n: Integer): TBCBasePin; 
var
hr: HRESULT;
begin
if (xxx = 0) thenbegin
xxx := 1;
FPin := TMyPin.Create('Null input pin', GetOwner, self, TBCCritSec.Create,
hr, 'Input', FfileName);//,PINDIR_INPUT); end;
result := FPin;
end;

     Реализация SetFileName и GetCuFile очень простая.  Остается Run, в которой вызывем функцию OpenFile входящего контакта, а затем метод Run предка:

function TDump.Run(tStart: TReferenceTime): HRESULT; 
begin
FPin.OpenFile;
result := inherited Run(tStart);
end;

И Stop, в котором вызываем метод CloseFile входного контакта и вызываем метод Stop предка:

function TDump.Stop: HRESULT; 
begin
FPin.CloseFile;
result := inherited Stop;
end;

     Коротко просмотрим  что происходит. В конструкторе вызываем конструктор предка, по требованию выдаем/создаем контакт, установленное имя и инициализационно-деинициализационные действия сообщаем контаку, на запуск и остановку вызываем соответствующие свойства контакта, а затем - метод предка. Все более-менее прозрачно, переходим к рассмотрению нашего контакта. Вот его объявление:

TMyPin = Class (TBCRenderedInputPin) //TBCBasePin private
FLast: TReferenceTime;
{$IFDEF DEBUG} FDump: TDump; {$ENDIF}
FFile: integer;
public mF:string;
constructor Create(ObjectName: string; pUnk: IUnKnown; Filter: TBCBaseFilter;
Lock: TBCCritSec; out hr: HRESULT; Name: WideString; mpFileN:string);
function CheckMediaType(mt: PAMMediaType): HRESULT; override;
function Receive(pSample: IMediaSample): HRESULT; override;
function EndOfStream: HRESULT; override;
function BreakConnect: HRESULT; override;
procedure OpenFile;
Procedure CloseFile;
end;

Как уже было сказано, он есть потомком класса TBCRenderedInputPin.

В приватной секции важен только член:

FFile: integer; - хэндл файла, в который будет вестись запись

Все остальные члены - просто глупость.

В public-секции есть один член:

mF: String; - задает имя файла

И следующие функции:

constructor Create(ObjectName: string; pUnk: IUnKnown; Filter: TBCBaseFilter; 
Lock: TBCCritSec; out hr: HRESULT; Name: WideString; mpFileN:string);
function CheckMediaType(mt: PAMMediaType): HRESULT; override;
function Receive(pSample: IMediaSample): HRESULT; override;
function EndOfStream: HRESULT; override;
function BreakConnect: HRESULT; override;
procedure OpenFile; Procedure CloseFile;
Procedure CloseFile;

Разберем их более подробно. Методы OpenFile и CloseFile служат для создания и закрытия файла, в который ведется запись. В конструкторе вызывается конструктор предка и открывается файл для записи. Метод BreakConnect, объявленный в CBaseInputPin, освобождаем контакт от соединения, его реализация вызывает одноименный метод предка. Функция CheckMediaType всегда возвращает S_OK, что значит, что контакт согласен со всем, что ему ни предложат. EndOfStream вызывается в случае, когда данные больше не ожидаются, реализация состоит в вызове метода предка. Реализация оставшегося метода - Receive выглядит следующим образом:

function TMyPin.Receive(pSample: IMediaSample): HRESULT; 
var
pbData: PBYTE;
tStart, tStop: TREFERENCETIME;
begin
pSample.GetTime(tStart,tStop);
pSample.GetPointer(pbData);
FileWrite(FFile, pbData^, pSample.GetActualDataLength);
result := S_OK;
end;

Этот метод вызывается, когда контакту приходят данные и он, недолго думая, просто пишет их в файл. Входной параметр имеет тип IMediaSample, информацию о котором смотрим в помощи и исходниках. Нам достаточно знать, что с помощью этого параметра можно получить входные данные и информацию о них.

В общем, все выглядит следующим образом: создается контакт, открывается файл для записи, при получении данных они пишутся в этот самый файл, а когда данные заканчиваются или получена команда об остановке, файл закрывается.

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

Теперь можно и проверить фильтр, но занимайтесь этим самостоятельно.

NullIP

Следующий рассматриваемый фильтр будет минимально полезным. Он будет перечислять поддерживаемые предыдущим и последующим фильтром в графе фильтров мультимедиа -форматы. Вещь почти бесполезная в жизни, но подходящая для тренировки. Еще одно нововведение, которое мы рассмотрим - страницы свойств фильтра.

Итак, переход к директории NUllIP и смотрим файл main.pas. Для начала разберемся с простыми деталями. Это - входящий и исходящий контакты. Вот их объявление:

TNullInPlaceInputPin = class(TBCTransInPlaceInputPin) 
public
constructor Create(ObjectName: string; TransInPlaceFilter: TBCTransInPlaceFilter;
out hr: HRESULT; Name: WideString);
function CheckMediaType(mt: PAMMediaType): HRESULT; override;
end;

TNullInPlaceOutputPin = class(TBCTransInPlaceOutputPin)
publicconstructor Create(ObjectName: string; TransInPlaceFilter:
	TBCTransInPlaceFilter; out hr: HRESULT; Name: WideString); 
function CheckMediaType(mt: PAMMediaType): HRESULT; override;
end;

Т.е. контакты - наследники соответственно TBCTransInPlaceInputPin и TBCTransInPlaceOutputPin:

Рис. 4. Иерархия классов для контакта CTransInPlaceInputPin

Рис. 5. Иерархия классов для контакта CtransInPlaceOutputPin

В их конструкторах всего лишь вызываются конструкторы предков. Рассмотрим функцию

CheckMediaType(mt: PAMMediaType). Этот метод используется для проверки, подходит ли соответтсвующему контакту указанный в параметре тип медиа. Для входящего контакта принцип алгоритма следующий: (ПЕРЕВЕСТИ)

// If we have been given a preferred media type from the property sheet

// then only accept a type that is exactly that.

// else if there is nothing downstream, then accept anything

// else if there is a downstream connection then first check to see if

// the subtype (and implicitly the major type) are different from the downstream

// connection and if they are different, fail them

// else ask the downstream input pin if the type (i.e. all details of it)

// are acceptable and take that as our answer.

Для исходящего контакта проверяем соответствие переданного видеотипа

PushSource

Модифицируем PushSource

InfTee

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

Итак, начнем. Нашей задачей будет написание (написание - сильно сказано, ведь мы будем постоянно оглядываться на исходники примера из SDK) некоего фильтра, имеющего один входной контакт и динамически изменяемое количество исходящих контактов. Все данные, которые попадают через входящий контакт в фильтр, будут пересланы по всем нижележащим путям далее вниз, для чего будет использовано соответствующее количество исходящих тредов, связанных с исходящими контактами.

Таким образом, в процессе установления связи между фильтрами, нам придется учесть тот факт, что, если меняется медиатип для одного соединения, то синхронно должны быть изменены и медиатипы для остальных соединений. Если же какое-то из соединений не сможет принять этот медиатип, то, в таком случае, фильтр (контакт) должен отвергнуть его.


Comments