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

Соединение фильтров

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

Соединение контактов

Фильтр соединяется своими контактами посредством интерфейса IPin. Исходящие контакты соединяются со входящими контактами. Каждое контактное соединение имеет медиатип, описываемое структурой AM_MEDIA_TYPE.

Приложения соединяют фильтры, вызывая методы на менеджере графа фильтров, никогда не вызывая непосредственно методов фильтров или их контактов. Приложение может прямо указать фильтры, которые нужно соединить, вызовами методов IFilterGraph::ConnectDirect или IGraphBuilder::Connect; или соединить фильтры косвенно, используя такой метод построителя графа, как IGraphBuilder::RenderFile.

Для успешного соединения оба фильтра должны присутствовать в графе фильтров. Приложение может добавить фильтр в граф фильтров вызовом метода IFilterGraph::AddFilter. Менеджер графа фильтров добавляет граф в фильтр, если все нормально. Когда фильтр добавлен, менеджер графа фильтров вызывает метод фильтра IBaseFilter::JoinFilterGrpah для его оповещения.

Общие контуры процесса соединения состоит в следующем:

  1. Менеджер графа фильтров вызывает метод исходящего контакта IPin::Connect, передавая указатель на входящий контакт.
  2. Если исходящий контакт принимает соединение, он вызывает метод входящего контакта IPin::ReceiveConnection.
  3. Если входящий контакт также может принять соединение, попытка соединения успешна и контакты соединяются.

Некоторые контакты можно отсоединять и пересоединять в то время, когда фильтр активен. Такой тип пересоединения называется динамическим. Здесь предлагается посмотреть раздел справки Dynamic Graph Building. Впрочем, большинство фильтров не поддерживают динамического пересоединения.

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

Когда вызываются методы контакта Connect или ReceiveConnection, контакт должен проверить, способен ли он поддерживать соединение. Детали этого вопроса зависят от конкретного фильтра. Наиболее общие проверки включают следующее:

  • Проверить, приемлем ли медиатип.
  • Договориться о распределителе памяти.
  • Запросить у другого контакта необходимые интерфейсы.

Договор о медиатипах

Когда менеджер графа фильтров вызывает метод IPin::Connect, есть несколько опций для спецификации медиатипа:

Законченный тип: Если медиатип специфицирован полностью, контакты пытаются соединитиься с этим типом. Если это не получается, попытка соединения считается неуспешной.

Частичный медиатип: Медиатип есть частичным, если старший тип, подтип или тип формата равны GUID_NULL. Значение GUID_NULL действует как "wildcard", означающее, что приемлемо любое значение. Контакты договариваются о типе, который содержит частичный медиатип.

Нет медиатипа: Если менеджер графа фильтров передает указатель на NULL, контакты могут согласиться на любой медиатип, подходящий для обоих контактов.

Если контакты соединились, соединение всегда имеет законченный медиатип. Предложения медиатипов, дающиеся менеджером графа фильтров, ограничивают возможные типы соединений.

В течении процесса договаривания исходящий контакт предлагает медиатип, вызывая метод входящего контакта IPin::ReceiveConnection. Входящий контакт может принять или отвергнуть предлагаемый медиатип. Процесс продолжается до тех пор, пока каждый входящий контакт не примет медиатип, либо исходящий контакт не прекратит перебор типов и соединение закончится неудачей.

Как исходящий контакт выбирает предлагаемые медиатипы, зависит от реализации. В базовых классах DirectShow исходящий контакт вызывает метод входящего контакта IPin::EnumMediaTypes. Этот метод возвращает перечислитель, который возвращает предпочитаемые медиатипы входящего контакта.

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

if (pmt->formattype == FORMAT_VideoInfo)
{
VIDEOINFOHEADER *pVIH = (VIDEOINFOHEADER*)pmt->pbFormat;
// Это неверно
}

Его нужно писать так:

if ((pmt->formattype == FORMAT_VideoInfo) && 
(pmt->cbFormat > sizeof(VIDEOINFOHEADER) &&
(pbFormat != NULL))
{
VIDEOINFOHEADER *pVIH = (VIDEOINFOHEADER*)pmt->pbFormat;
// Теперь можно ссылаться на pVIH
}

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

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

Наиболее общим транспортом есть транспорт локальной памяти, в котором медиаданные пребывают в главной памяти. Существует два особых транспорта локальной памяти - push модель и pull модель. В push-модели фильтр источника проталкивает данные в нижележащий фильтр, используя интерфейс IMemInputPin входящего контакта нижележащего фильтра. В pull-модели нижележащий фильтр запрашивает данные у фильтра источника, используя интерфейс IAsyncReader исходящего контакта фильтра источника. Для получения более подробной информации об этих двух моделях пересылки данных, см. Более пристально: передача данных в гафе фильтров.

В транспорте локальной памяти объект, ответственный за распределение буферов памяти, называется распределителем (alocator - алокатором). Распределитель поддерживает интерфейс IMemAllocator. Оба контакта совместно используют один распределитель. Каждый контакт может предоставить распределитель, но исходящий контакт выбирает тот из них, что будет использоваться.

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

В соединении посредством IMemInputPin работа с распределителя выглядит следующим образом:

  1. Опционально исходящий контакт вызывает метод IMemInputPin::GetAllocatorRequirements. Этот метод возвращает требования входящего контакта к буферу, такие, как выравнивание памяти. Вообще, исходящий контакт может учитывать требования входящего контакта.
  2. Опционально исходящий контакт вызывает метод IMemInputPin::GetAllocator. Этот метод затребывает распределитель входящего контакта. Входящий контакт возвращает либо его, либо код ошибки.
  3. Исходящий контакт выбирает распределитель. Он может использовать распределитель, предоставленный входящим контактом, либо создать его сам.
  4. Исходящий контакт вызывает метод IMemAllocator::SetProperties для установления свойств распределителя. Но, однако, распределитель может не удовлетворять требуемым свойствам. (Это может произойти, например, в том случае, когда распределитель предоставляет входящий контакт.) Распределитель возвращает свои свойства в исходящем параметре метода SetProperties.
  5. Исходящий контакт вызывает метод IMemInputPin::NotifyAllocator для извещения входящего контакта о выбранном распределителе.
  6. Входящий контакт должен вызвать метод IMemAllocator::GetProperties для проверки свойств распределителя.
  7. Исходящий контакт есть ответственным за принятие (commiting) и отмену (decommiting) распределителя. Это происходит при старте и остановке потока.

В соединении через IAsyncReader с распределителем нужно обращаться так:

  1. Входящий контакт вызывает метод исходящего контакта IAsyncReader::RequestAllocator. Входящий контакт указывает свои требования к буферу и, при желании, предоставляет распределитель.
  2. Исходящий контакт выбирает распределитель. Он может использовать либо распределитель, предоставленный входящим контактом, либо, если он не предоставлен, создать его самому.
  3. Исходящий контакт возвращает распределитель как исходящий параметр метода RequestAllocator. Входящий контакт должен проверить свойства распределителя.
  4. Входящий контакт является ответственным за принятие (commiting) и отмену (decommiting) распределителя.
  5. В любой момент в процессе договора о распределителе любой контакт может прервать соединение.
  6. Если исходящий контакт использует распределитель входящего контакта, он может использовать этот распределитель только для пересылки сэмплов входящему контакту. Фильтр-владелец не должен использовать распределитель для пересылки сэмплов другим контактам.

Предоставление пользовательского распределителя

Здесь описано, каким образом можно предоставить фильтру пользовательский распределитель памяти. Будет описано только соединение через IMemInputPin (push модель), но аналогичные шаги для IAsyncReader более простые.

Сначала нужно определить C++ класс для распределителя. Ваш распределитель может быть наследником одного из стандартных классов распределителей - CBaseAllocator или CMemAllocator, или вы можете создать полностью новый класс распределителя. Если вы создаете новый класс, то должны предоставить интерфейс IMemAllocator.

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

Рассмотрим вначале предоставление пользовательского распределителя для входящего контакта. Для этого нужно переписать метод входящего контакта CBaseInputPin::GetAllocator. Внутри этого метода нужно проверить переменную-член m_pAllocator. Если эта переменная есть не-NULL, это значит, что для этого соединения распределитель уже был выбран, так что метод GetAllocator должен вернуть указатель на этот распределитель. Если m_pAllocator равен NULL, это значит, что распределитель не был выбран, так что метод GetAllocator должен вернуть указатель на предпочитаемый распределитель для входящего контакта. В этом случае нужно создать экземпляр вашего пользовательского распределителя и вернуть указатель на его интерфейс IMemAllocator. Следующий код показывает, как нужно реализовывать метод GetAllocator:

STDMETHODIMP CMyInputPin::GetAllocator(IMemAllocator **ppAllocator)
{
CheckPointer(ppAllocator, E_POINTER);
if (m_pAllocator)
{
// Распределитель уже есть, так что его и возвращаем
*ppAllocator = m_pAllocator;
(*ppAllocator)->AddRef();
return S_OK;
}
// Распределителя еще нет, так что создаем свой собственный
HRESULT hr = S_OK;
CMyAllocator *pAlloc = new CMyAllocator(&hr);
if (!pAlloc)
{
return E_OUTOFMEMORY;
}
if (FAILED(hr))
{
delete pAlloc;
return hr;
}
// А теперь возвращаем указатель на интерфейс IMemAllocator
 // нашего распределителяreturn pAlloc->QueryInterface(IID_IMemAllocator, (void**)ppAllocator);
}

Когда вышележащий фильтр выбирает распределитель, он вызывает метод входящего контакта IMemInputPin::NotifyAllocator. Перепишите метод CBaseInputPin::NotifyAllocator для проверки свойств распределителя. В некоторых случаях входящий контакт может отказаться от распределителя, если это не его собственный распределитель, несмотря на то, что это может привести к ошибке все соединение контактов.

Теперь рассмотрим предоставление пользовательского распределителя памяти для исходящего контакта. Для этого нужно переписать метод CBaseOutputPin::InitAllocator для создания экземпляра вашего распределителя:

HRESULT MyOutputPin::InitAllocator(IMemAllocator **ppAlloc)
{
HRESULT hr = S_OK;
CMyAllocator *pAlloc = new CMyAllocator(&hr);
if (!pAlloc)
{
return E_OUTOFMEMORY;
}
if (FAILED(hr))
{
delete pAlloc;
return hr;
}
// Возвратим интерфейс IMemAllocator нашего распределителяreturn pAlloc->QueryInterface(IID_IMemAllocator, void**)ppAllocator);}
}

По умолчанию класс CBaseOutputPin затребывает сначала распределитель у входящего контакта. Если этот распределитель неподходящ, исходящий контакт создает свой собственный распределитель. Для навязывания соединения с использованием вашего собственного распределителя, перепишите метод CBaseOutputPin::DecideAllocator. Но нужно осознавать при этом, что это может не дать вашему исходящему контакту соединиться с определенными фильтрами, т.к. другие фильтры могут тоже требовать свои собственные распределители.

Пересоединение контактов

При соединении контактов фильтр может отсоединить или пересоединить один из своих прочих контактов так:

  1. Фильтр вызывает метод метод другого контакта фильтра IPin::QueryAccept и указывает новый медиатип.
  2. Если QueryAccept возвращает S_OK, фильтр вызывает метод IFilterGraph2::REconnectEx для пересоединения контактов.

Покажем два примера, когда фильтру может понадобиться пересоединение контактов:

  • Tee фильтр. Этот фильтр разделяет входящий поток на много исходящих без изменения данных в потоке. Tee фильтр может работать с разными медиатипами, но типы должны совпадать для всех контактов соединения. Таким образом, когда соединяется входящий контакт, фильтру может быть необходимо передоговориться со всеми существующими соединениями на исходящих контактах и наоборот. Предлагается посмотреть пример InfTee Filter.
  • Trans-in-place фильтры. В таком фильтре входящие данные модифицируются в исходном буфере вместо копирования данных в отдельный исходящий буфер. Такой фильтр должен использовать тот же распределитель памяти, что и обы его соединения - вышележащее и нижележащее. Сначала контакт для соединения (входящего или исходящего) договоривается о распределителе обычным образом. Но, когда происходит другое соединение, первый распределитель может не быть приемлемым. В этом случае второй контакт выбирает иной распределитель, и первый контакт пересоединяется, используя новый распределитель. Предлагается рассмотреть класс CTransInPlaceFilter.

В методе ReconnectEx менеджер графа фильтров асинхронно отсоединяет и пересоединяет контакты. Фильтр не должен пытаться пересоединиться без получения ответа S_OK на вызов QueryAccept. С другой стороны, контакт, который был отсоединен, вызывает ошибку графа. Фильтр также должен запрашивать пересоединение изнутри метода IPin::Connect на той же нити. Если метод Connect будет использоваться одной нитью, в то время, как на другой нити будет затребовано пересоединение, менеджер графа фильтров может запустить граф перед тем, как будет произведено соединение, вызвав тем самым ошибку графа.

Процесс соединения и класс CBasePin

Хотя этот пункт и лежит несколько в стороне, он будет, все же, рассмотрен для лучшего понимания процесса соединения контактов. Здесь мы опишем, как реализованы методы класса CBasePin для обеспечения процесса соединения.

Менеджер графа фильтров инициирует все соединения контактов. Он вызывает метод исходящего контакта IPin::Connect с указанием входящего контакта. Исходящий контакт совершает соединение, вызывая метод входящего контакта IPin::ReceiveConnection. Входящий контакт может принять или отказать соединению.

Менеджер графа фильтров может также указать медиатип для соединения. Если это так, то контакты попытаются соединиться, используя этот тип. Если же нет, то контакты должны будут договориться о типе. Менеджер графа фильтров может также указать частичный медиатип, который имеет значение GUID_NULL для каждого из главного типа, подтипа или форматного типа. В этом случае контакты попытаются проверить порцию указанных медиатипов; значение GUID_NULL действует как "wildcard".

Метод CBasePin::Connect начинает с проверки, что контакт может принять соединение. Например, поверяет, что он еще не соединен. Это передает остаток процесса соединения методу CBasePin::AgreeMediaType.

Если медиатип указан полностью, контакт вызывает метод CBasePin::AttemptConnection для попытки соединения. В противоположном случае он испытывает медиатипы в следующем порядке:

  1. Предпочитаемые типы входящего контакта.
  2. Предпочитаемые типы исходящего контакта.

Можно поменять этот порядок установкой флага CBasePin::m_bTryMyTypesFirst в TRUE.

В каждом случае контакт вызывает метод IPin::EnumMediaTypes для перечисления медиатипов. Этот метод возвращает объект перечисления, который передается в метод CBasePin::TryMediaTypes. Метод TryMediaTypes проверяет в цикле каждый медиатип посредством вызова AttemptConnection.

Внутри метода AttemptConnection исходящий контакт вызывает следующие методы:

  • Вызывается собственный метод CBasePin::CheckConnect для проверки, подходит ли входящий контакт.
  • Вызывается собственный метод CBasePin::CheckMediaType для проверки медиатипа.
  • Вызывается метод входящего контакта IPin::ReceiveConnection. Этот метод используется входящим контактом для определения, принимать ли соединение.
  • Вызывается собственный метод CBasePin::CompleteConnect для завершения соединения.

Нужно заметить следующее:

  • Метод CheckConnect - виртуальный. В базовом классе он проверяет направление контакта и совместимость. Исходящий контакт должен соединяться со входящим и наоборот. В производном классе контакта этот метод, обычно, переписывается для осуществления других проверок. Это может быть, например, запрос к другому контакту на предмет интерфейса, требуемого для соединения. Если в производном классе переписывается метод CheckConnect, то в нем должен быть вызван также метод базового класса CBasePin.
  • Метод CheckMediaType - чисто виртуальный; он должен быть реализован в производном классе.
  • Метод CompleteConnect - виртуальный, но в базовом классе не делает ничего. Производные классы могут переопределить этот метод для совершения любой дополнителной работы, необходимой для завершения соединения, такой, как, например, принятие распределителя памяти.

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

Метод входящего контакта ReceiveConnection вызывает методы входящего контакта CheckConnect, CheckMediaType и CompleteConnection. Если любые из них неудачны, попытка соединения также неудачна.

Следующая диаграмма показывает процесс соединения в классе CBasePin:

Рис. 10. Процесс соединения для класса CBasePin

Comments