Использование DirectShow в Windows
Часть 3: Делаем собственные графы
DirectShow For
Media Playback In Windows
Part III: Customizing Graphs
By Chris Thompson
(aka Lightman)
13 September 2000
Введение
Продолжение серии учебников о DirectShow будет посвящено созданию собственных
графов фильтров. Есть целая куча причин, по которой вам может захотеться сделать
собственный граф. Например вам может понадобиться трансформирующий фильтр для
создания эквалайзера для аудио-вывода или вы захотите какой-нибудь цветовой
коррекции для видео-вывода. Иногда GraphBuilder просто не добавляет фильтр,
который нам нужен, поэтому приходится ручками вправлять граф до требуемой кондиции.
Ну и наконец это необходимо для изучения процессов, которые происходят при построении
графов: как в граф добавляются фильтры, как соединены 2 фильтра и т. д.
Больше интерфейсов
Оглашаю весь список интерфейсов, которые мы будем использовать. Если вам требуется
полноценное описание, обращайтесь к документации DirectShow.
- IEnumFilters - интерфейс, которым снабжает нас GraphBuilder и который позволит
перечислить все фильтры в графе
- IBaseFilter - интерфейс предоставляемый всеми фильтрами DirectShow
- IEnumPins - интерфейс предоставляемый фильтрами для перечисления всех их
контактов
- IPin - интерфейс предоставляемый всеми пинами в фильтре
Если мы хотим чего-то там менять в графе, нам необходимо знать все эти интерфейсы.
Вы увидите, что соединение двух фильтров происходит на уровне пинов, используя
интерфейс lPin. Это значит, что нам надо получить этот интерфейс из любых пинов,
принадлежащий любому фильтру в графе.
Есть несколько различных способов построить подходящий граф для различных хозяйственных
нужд. Первый способ - это автоматическая генерация графа с помощью IGraphBUilder::RenderFile(),
как мы уже с вами делали. Другой способ - выделение каждого необходимого нам
фильтра и по очереди их соединять. Это не самый простой способ, но это все же
возможность и хорошая учебная практика. Самый лучший способ (естесственно ИМХО)
- это дать GraphBuilder сделать столько работыЮ, сколько возможно, а потом вручную
изменять граф до достижения желаемого эффекта. Я называю это: полуавтоматически
созданными графами. Давайте разберем каждый способ по отдельности.
Автоматически построенные графы
Этим способом мы строили все графы до сих пор. Короткое напоминание:
IGraphBuilder* g_pGraphBuilder;
void BuildGraph(char* file)
{
WCHAR* wfile;
int length;
length = strlen(file)+1;
wfile = new WCHAR[length];
MultiByteToWideChar(CP_ACP, 0, file, -1, wfile, length);
g_pGraphBuilder->RenderFile(wfile, NULL);
}
Графы, построенные вручную
Так, предположим что мы использовали GraphEdit для построения графа для .WAV-файла,
поэтому мы уже лицезрели готовый граф и в точности знаем какие фильтры нам нужны
и как их соединять. Мы можем построить граф из ничего просто добавляя по одному
фильтру за раз. Вот вам схемка как дожен выглядет граф
<файловый источник>--<обработчик wav>--<вывод
звука>
Помните, что в COM у нас все интерфейсы обозначены 'IID_xxx', а специфические
реализации этих интерфейсов так: 'CLSID_xxx'. Нам потребуется узнать классовый
идентификатор (CLSID_xxx) для каждого фильтра, дабы вызывать функцию CoCreateInstance,
которая создаст фильтр. Большинсто обозначений обычных исходных и выводящих
фильтров перечислены в заголовочном файле uuids.h Там мы находим обозначения
CLSID_FileSource и CLSID_AudioRender (или CLSID_DSoundRender для вывода через
DirectSound). Теперь нам осталось найти CLSID для обработчика wav-данных, который
будет декодировать поток. Самый простой способ - поискать через регистр по строке
"Wave Parser" и использовать CLSID, который вы найдете. Есть и другие
способы, такие как использование IFilterMapper и IEnumRegFilters для вывода
списков зарегистрированных фильтров, но пока я не буду их разбирать.
Сначала давайте создадим фильтры, которые как мы знаем, нам понадобятся:
#define INITGUID
DEFINE_GUID(CLSID_WaveParser, 0xD51BD5A1, 0x7548, 0x11CF, 0xA5, 0x20, 0x00,
0x80, 0xC7, 0x7E, 0xF5, 0x8A);
IGraphBuilder* g_pGraphBuilder;
IBaseFilter* g_pSource;
IBaseFilter* g_pWaveParser;
IBaseFilter* g_pSoundRenderer;
void CreateFilters()
{
CoCreateInstance(CLSID_FileSource, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter,
(void**)&g_pSource);
CoCreateInstance(CLSID_WaveParser, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter,
(void**)&g_pWaveParser);
CoCreateInstance(CLSID_DSoundRender, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter,
(void**)&g_pSoundRenderer);
g_pGraphBuilder->AddFilter(g_pSource, NULL);
g_pGraphBuilder->AddFilter(g_pWaveParser, NULL);
g_pGraphBuilder->AddFliter(g_pSoundRender, NULL);
}
Заметьте, что я только что использовал макрос DEFINE_GUID для определения CLSID
(которые и есть GUID) для обработчика wav. GUID - это просто структура, поэтому
все, что нам надо - это создать ее и поместить туда правильные значения.
Теперь, когда у нас есть фильтры, следующее, чего нам захочется - это соединить
их. Соединения фильтров производятся на уровне контактов, поэтому нам надо найти
правильные контакты для соединения на каждом фильтре. Есть несколько способов
соединения пинов. IFilterGraph предоставляет метод ConnectDirect(), где мы можем
указать медиа-тип для использования при соединении и два пина, которые надо
соединить. Это все хорошо, когда вы знаете тип медиа, который будет использоваться
и вы уверены, что два фильтра счастливо сольются в экстазе. Так что более лучший
способо - это использование метода IGraphBuilder::Connect(), который пытается
соединить два контакта напрямую и если это не получится, он будет пытаться найти
комбинацию фильтров, которые войдут между двумя контактами для завершения соединения.
Это самый безопасный способ, поэтому его-то мы и будем использовать.
Мы используем интерфейс IEnumPins для перечисления всех пинов в фильтре, которые
хотим соединить. Так как фильтры могут иметь любое количество пинов, нам надо
выбрать правильные. Мы можем использовать наши знания, которые мы получили подглядев
на готовый фильтр в GraphEdit. Мы знаем, что фильтр источника и выводящий фильтр
имеют по одному контакту, поэтому с ними не будет никаких проблем. Мы так же
знаем, что обработчик wav имеет один входной контакт и один выход. Чтобы убедиться,
что мы выбрали именно тот контакт, который требовалось, нам всего лишь надо
проверить направленность контакта с помощью IPin::QueryPinInfo().
IGraphBuilder* g_pGraphBuilder;
IBaseFilter* g_pSource;
IBaseFilter* g_pWaveParser;
IBaseFilter* g_pSoundRenderer;
void ConnectFilters()
{
IEnumPins* EnumPins;
IPin* OutPin;
IPin* InPin;
ULONG fetched;
PIN_INFO pinfo;
g_pSource->EnumPins(&EnumPins);
EnumPins->Reset();
EnumPins->Next(1, &OutPin, &fetched); это именно то
EnumPins->Release();
g_pWaveParser->EnumPins(&EnumPins);
EnumPins->Reset();
EnumPins->Next(1, &InPin, &fetched);
InPin->QueryPinInfo(&pinfo);
pinfo.pFilter->Release();
if (pinfo.dir == PINDIR_OUTPUT)
{
InPin->Release();
EnumPins->Next(1, &InPin, &fetched);
}
g_pGraphBuilder->Connect(OutPin, InPin);
InPin->Release();
OutPin->Release();
EnumPins->Reset();
EnumPins->Next(1, &OutPin, &fetched);
OutPin->QueryPinInfo(&pinfo);
pinfo.pFilter->Release();
if (pinfo.dir == PINDIR_INPUT)
{
OutPin->Release();
EnumPins->Next(1, &OutPin, &fetched);
}
EnumPins->Release();
g_pSoundRenderer->EnumPins(&EnumPins);
EnumPins->Reset();
EnumPins->Next(1, &InPin, &fetched); - автоматически правильно
EnumPins->Release();
g_pGraphBuilder->Connect(OutPin, InPin);
InPin->Release();
OutPin->Release();
}
Ну вот, теперь мы должны иметь полнофункциональный граф фильтров, такой же,
как созданный с помощью IGraphBuilder::RenderFile(). Выглядит так, что мы потратили
кучу времени зря, когда могли автоматически сгенерировать граф, поэтому дружно
забьем на этот метод в дальнейшем. Однако как иллюстрация одной из возможностей
это сойдет. Ну а теперь давайте посмотрим на более практичный способ контролировать
граф, позволив GraphBuilder'у выполнить всю грязную работу.
Полуавтоматически построенные графы
Когда я говорю "полуавтоматически построенные графы", это может означать
несколько вещей. У нас может быть граф фильтров, который был сгенерирован автоматически,
но мы вставили или изменили несколько фильтров. Так же, мы можем создать фильтр
источника, добавить его к графу (соединить с какими-то трансформирующими фильтрами,
если надо), а затем дать графопостроителю автоматически закончить работу. Наконец,
мы мжем добавить несколько специфических фильтров, которые мы хотим использовать,
в граф и пусть графопостроитесь сам строит весь граф, уситывая те фильтры, которые
мы добавили. Сейчас я быстро пробегусь по каждому из способов.
Скажем, мы все еще пытаемся проиграть .wav-файл. Но теперь мы хотим пропустить
данные через трансформирующий фильтр, чтобы наложить эффект. Мы будем использовать
фильтр по имени Gargle, так как это пример фильра, поставляемый с DShow SDK.
Учтите, что фильтры-примеры должны быть собраны и зарегистрированы прежде чем
они могут быть использованы, поэтому убедитесь, что вы скомпилировали пример
Gargle и зарегистрировали компонент с помощью 'regsvr32', прежде чем использовать
его. Наш граф фильтров будет выглядеть вот так:
<фильтр источника>--<обработчик wav>--<эффект
бульканья>--<вывод звука>
Мы позволим графопостроителю создать граф по умолчанию (без эффекта). Затем
мы перечислим фильтры и выявим выводящий фильтр. Мы знаем, что несжатый звук
передается выводящему фильтру из фильтра, к которому он непосредственно подключен
и то, что входной контакт фильтра Gargle так же принимает несжатые данные. Поэтому
мы можем спокойно втиснуть эффект прямо перед выводящим фильтром. Для трансформирующих
фильтров - это обычная практика. Вы можете вставить эффект в граф для файлов
mp3 или AIFF. Вы всегда можете быть уверены, что данные, которые поступают на
вход выводящего фильтра, всегда можно пропустить через трансформирующий фильтр.
Мы можем найти выводящий фильтр как проведя поиск по имени, так и проверяя
количества выходных контактов. Единственный фильтр во всем графе, не имеющий
таковых окажется выводящим. Чтобы наш код мог работать с разными рендерами (помните,
что даже в поставке DirectShow есть два разных фильтра, выводящих звук), мы
не будем искать их по имени. Поэтому мы будем проверять количество выходных
пинов. Когда мы найдем выводящий фильтр, мы соединим пины фильтра Gargle.
Вот код, который делает это:
IGraphBuilder* g_pGraphBuilder;
void BuildGraph()
{
IEnumFilters* EnumFilters;
IBaseFilter* Renderer;
IBaseFilter* Gargle;
IEnumPins* EnumPins;
bool FoundRenderer = false;
IPin* InPin;
IPin* OutPin;
IPin* GargleIn;
IPin* GargeOut;
ULONG fetched;
PIN_INFO pinfo;
int numoutputpins = 0;
g_pGraphBuilder->RenderFile(L"blah.wav"); который нам необходим
g_pGraphBuilder->EnumFilters(&EnumFilters);
EnumFilters->Reset();
while (FoundRenderer == false)
{
EnumFilters->Next(1, &Renderer, &fetched);
Renderer->EnumPins(&EnumPins);
EnumPins->Reset();
numoutputpins = 0;
while (EnumPins->Next(1, &InPin, &fetched) == S_OK)
{
InPin->QueryPinInfo(&pinfo);
pinfo.pFilter->Release();
InPin->Release();
if (pinfo.dir == PINDIR_OUTPUT)
{
numoutputpins++;
break;
}
}
EnumPins->Release();
if (numoutputpins == 0)
FoundRenderer = true;
else
Renderer->Release();
}
EnumFilters->Release();
Filter->EnumPins(&EnumPins);
EnumPins->Reset();
EnumPins->Next(1, &InPin, &fetched);
EnumPins->Release();
Pin->ConnectedTo(&OutPin);
g_pGraphBuilder->Disconnect(InPin);
g_pGraphBuilder->Disconnect(OutPin);
CoCreateInstance(CLSID_Gargle, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter,
(void**)&Gargle);
g_pGraphBuilder->AddFilter(Gargle, NULL);
Gargle->EnumPins(&EnumPins);
EnumPins->Reset();
EnumPins->Next(1, &GargleIn, &fetched);
GargleIn->QueryPinInfo(&pinfo);
pinfo.pFilter->Release();
if (pinfo.dir = PINDIR_OUTPUT)
{
GargleOut = GargleIn;
EnumPins->Next(1, &GargleIn, &fetched);
}
else
EnumPins->Next(1, &GargleOut, &fetched);
g_pGraphBuilder->Connect(OutPin, GargleIn);
g_pGraphBuilder->Connect(GargleOut, InPin);
}
Гм. Это было довольно долго и беспорядочною. По крайней мере мы получили граф
тем способом, которым хотели и это не зависит от типа декодера и рендера, которые
были использованы. Обратите внимание, что для того, чтобы получить выходной
контакт фильтра, который был подключен к рендеру, мы просто вызываем метода
входного пина рендера IPin::ConnectedTo().
Второй способ создания полуавтоматических графов используем некоторые вещи,
которые мы знаем о том, как графопостроитель создает графы. Мы знаем, что пока
GraphBuilder пытается создать граф, он пытается подключить текущий фильтр, с
которым он работает к каждому фильтру, который уже есть, прежде чем добавлять
новый. Это значит, что мы можем создать фильтр Gargle, добавить его в граф и
вызывать IGraphBuilder::RenderFile(), и графопостроитель автоматически прилепит
его к первому месту, куда он подойдет. Вот как мы это сделаем:
IGraphBuilder* g_pGraphBuilder; // already created
void Buildgraph()
{
IBaseFilter* Gargle;
CoCreateInstance(CLSID_Gargle, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter,
(void**)&Gargle);
g_pGraphBuilder->AddFilter(Gargle, NULL);
g_pGraphBuilder->RenderFile(L"blah.wav");
}
Вот и все. Вы можете проделать такую же штуку в GraphEdit и убедиться, что она
работает. Некоторые фильтры по одной или другой причине не будут автоматически
выставляться. Лучший способ убедиться, что фильтры очутились именно там, где
вы хотели - это добавить сперва их в граф, затем перечислить их контакты и проверить
подсоединены ли они. Если нет, то используйте процесс ручной вставки перед рендером,
как мы уже делали.
И наконец последний полуавтоматический способ создания графа - это создать
первый фильтр (или фильтры) и уже потом сгенерировать продолжение. Давайте предположим,
что на этот раз мы собрались проиграть .wav файл с сайта. Вместо обычного исходного
фильтра 'File Source (Async)', нам понадобится фильтр источника 'File Source
(URL)'. Нам всего лишь надо создать только этот фильтр, а потом мы вызовем IGraphBuilder::Render().
Впрочем нам не обязательно надо бросать ручное построение на фильтрах-источниках.
В любое время, когда у нас есть недостроенный граф (фильтр источника подсоединенный
к любому числу трансформирующих фильтров), мы вызываем метод Render() неподсоединенного
выходного пина последнего фильтра в цепочке и графопостроитель будет пытаться
закончить граф за нас. Вот так это происходит:
#define INITGUID
DEFINE_GUID(CLSID_FileSourceURL,
0xE436EBB6, 0x524F, 0x11CE, 0x9F, 0x53, 0x00, 0x20, 0xAF, 0x0B, 0xA7, 0x70);
IGraphBuilder* g_pGraphBuilder; //
void BuildGraph()
{
IBaseFilter* Source;
IFileSourceFilter* FileSource;
IEnumPins* EnumPins;
IPin* Pin;
CoCreateInstance(CLSID_FileSourceURL, NULL, CLSCTX_INPROC_SERVER,
IID_IBaseFilter, (void**)&Source);
g_pGraphBuilder->AddFilter(Source, NULL);
Source->QueryInterface(IID_IFileSourceFilter, (void**)&Source);
FileSource->Load("http://www.asite.com/blah.wav"); //
FileSource->Release();
Source->EnumPins(&EnumPins);
EnumPins->Reset();
EnumPins->Next(1, &Pin, &fetched);
EnumPins->Release();
g_pGraphBuilder->Render(Pin); //
Pin->Release();
Source->Release();
}
Заключение
Используя эту технику, вы теперь должны уметь строить любой граф фильтров,
который вам понадобится. В будущих частях мы будем использовать IDDrawExclModeVideo
чтобы заставить DirectShow проигрывать видео в окне, принадлежащем нашему приложению,
а так же посмотрим на потоковое мультимедиа, используя DirectShow. Потоковое
мультимедиа очень полезно для игр так как оно передает в поток данные графики
и звука, так же как поток данных из файла, пропуская его через граф фильтров.
Иногда мы будем так же рассматривать создание собственных фильтров, так что
вы сможете использовать собственные трансформации или использовать собственные
форматы медиа данных.
|
 |
 |
|