Создание мультимедийных программ

Автор: Валентин Вовк

IGraphBuilder

Программно граф фильтра строится с помощью интерфеса IGraphBuilder. Для этого нужно получить интерфейс самомого IGraphBuilder'а, интерфейсы всех составлящих его фильтров и интерфейсы контактов каждого фильтра. После этого нужно соединить фильтры посредством их контактов, и можно запускать граф. Как это сделать, мы посмотрим в следующих пунктах, сейчас только замечу, что стандартные DirectShow фильтры, поставляемые в составе DirectShow, описаны в соответствующем разделе справки.

Проигрывание видеофайла и Intelligent connect

Сейчас мы построим программным путем граф фильтров, описанный в Проигрывании видеофайла.

Для простого проигрывания нет необходимости вручную строить весь граф фильтров. Графу фильтров нужно подсунуть фильтр источника, а все остальное сделает IntelligentConnect.

Давайте проведем две лабораторных работы. В первой добьемся проигрывания какого-нибудь avi-файла "как-нибудь", а во второй попытаемся добиться большего контроля.

1. Создаем новый проект, в секцию "uses" добавляем модули ActiveX и DirectShow, бросаем на форму TMainMenu, в котором создаем пункт "Open...", который отвечает за выбор видеофайла через диалог выбора файлов, и TOpenDialog. Поскольку мы будем работать с КОМ-объектами, то давайте сразу, при создании формы, добавим инициализацию: CoInitialize(nil), а при разрушении - CoUnitialize. Далее, для приложения нам понадобятся три интерфейса: IGraphBuilder, IMediaControl и IMediaEvent.

IGraphBuilder - это интерфейс графа фильтров. IMediaControl используется для управления запуском графом. IMediaEvent - для контроля и управлением поведения графа.

Я буду приводить упрощенный, схематический код, без проверок успешности выполнения тех или иных операций, но он обязательно должен быть полным в реальных приложениях.

// Объявляем переменные:var
pGraphBuilder: IGraphBuilder = nil;
pMediaControl: IMediaControl = nil;
pMediaEvent : IMediaEvent = nil;
// Создаем граф фильтров:
CoCreateInstance(CLSID_FilterGraph, nil, CLSCTX_INPROC, IID_IGraphBuilder,
pGraphBuilder);
// Получаем интерфейсы IMediaControl и IMediaEvent:
pGraphBuilder.QueryInterface(IID_IMediaControl, pMediaControl);
pGraphBuilder.QueryInterface(IID_IMediaEvent, pMediaEvent);
// Теперь дадаим команду графу, используя IntelligentConnect, построиться:
pGraphBuilder.RenderFile(StringToOleStr(OpenDialog1.FileName), '');
// Теперь даем команду на воспроизведение файла:
pMediaControl.Run;
// И, наконец, ждем окончания воспроизведения:
pMediaEvent.WaitForCompletion(INFINITE, evCode);

Полный проект находится здесь.

Теперь попробуем отобразить видео не где-попало, а в указанном нами месте. Предварительные шаги - добавление в "uses", инициализация и деинициализация - те же. Только добавим еще к форме панельку (TPanel), на которой и будем отображать видео. Но нам понадобятся дополнительные интерфейсы: ICaptureGraphBuilder2 - используется для построения графа захвата, но нам он нужен будет для рендеринга файла так, как нам нужно; pSourceFile - фильтр файла-источника; и IVideoWindow - интерфейс окна для отображения в нем видео.

// Объявляем переменные:var
pGraphBuilder : IGraphBuilder = nil;
pCaptureGraphBuilder2: ICaptureGraphBuilder2 = nil;
pSourceFile : IBaseFilter = nil;
pMediaControl : IMediaControl = nil;
pMediaEvent : IMediaEvent = nil;
pVideoWindow : IVideoWindow = nil;
// Если в окне диалога выбора файлов что-то выбрано, то:// Создаем IGraphBuilder:
CoCreateInstance(CLSID_FilterGraph, nil, CLSCTX_INPROC, IID_IGraphBuilder,
  pGraphBuilder);
// Создаем ICaptureGraphBuilder2:
CoCreateInstance(CLSID_CaptureGraphBuilder2, nil, CLSCTX_INPROC,
  IID_ICaptureGraphBuilder2, pCaptureGraphBuilder2);
// Указываем нашему ядру рендеринга (лучше перевести "render engine" не могу)
// граф фильтров для использования:
pCaptureGraphBuilder2.SetFiltergraph(pGraphBuilder);
// Теперь добавляем к графу фильтров фильтр источника следующим образом:
pGraphBuilder.AddSourceFilter(StringToOleStr(OpenDialog1.FileName), '',
  pSourceFile);
// Получаем интерфесы IMediaControl и IMediaEvent:
pGraphBuilder.QueryInterface(IID_IMediaControl, pMediaControl);
pGraphBuilder.QueryInterface(IID_IMediaEvent, pMediaEvent);
// Рендерим нашей машиной раскраски (еще один вариант перевода):
pCaptureGraphBuilder2.RenderStream(nil, nil, pSourceFile as IBaseFilter, nil, nil);
// Все готово для получения видеоокна:
hr := pGraphBuilder.QueryInterface(IID_IVideoWindow, pVideoWindow);
// Установим теперь владельца окна, стиль и положение:
pVideoWindow.put_Owner(Panel1.Handle);
pVideoWindow.put_WindowStyle(WS_CHILD);
pVideoWindow.put_Left(0);
pVideoWindow.put_Width(Panel1.Width);
pVideoWindow.put_Top(0);
pVideoWindow.put_Height(Panel1.Height);
// Осталось запустить:
pMediaControl.Run;
pMediaEvent.WaitForCompletion(INFINITE, evCode);

Все, с этой задачей мы справились. Полный код проекта можно взять здесь.

Конвертирование WAV<->MP3

Следующие наши эксперименты будем проводить без использования Intelligent Connect, поскольку одна из наших целей - понимание системы работы DirectShow, поэтому будем пытаться побольше работы выполнять руками. Поскольку, как мы помним, у меня были проблемы с конвертированием MP3 в WAV, будем заниматься только конвертацией WAV-MP3.

Что для этого будет нужно? Во-первых, граф фильтров - ни один проект без него не обойдется, во вторых интерфейс WaveParser'а - его CLSID узнать просто: запускаем GraphEdit, находим этот фильтр, и смотрим в дереве его DisplayName - оттуда и вычисляем, что CLSID равен D51BD5A1-7548-11CF-A520-0080C77EF58A; в-третьих - LAME MPEG Layer III Audio Encoder - тем же способом получаем, что его CLSID равен B8D27088-DF5F-4B7C-98DC-0E91A1696286; для фильтра Dump CLSID = 36A5F770-FE4C-11CE-A8ED-00AA002FEAB5. Собственно, это все, что нужно для успешного построения проекта.

// Итак, объявляем константы и переменные:const
CLSID_WaveParser: TGUID = '{D51BD5A1-7548-11CF-A520-0080C77EF58A}';
CLSID_LameMPG3AE: TGUID = '{B8D27088-DF5F-4B7C-98DC-0E91A1696286}';
CLSID_Dump : TGUID = '{36A5F770-FE4C-11CE-A8ED-00AA002FEAB5}';
var
pGraphBuilder: IGraphBuilder = nil;
pSourceFile : IBaseFilter = nil;
pWaveParser : IBaseFilter = nil;
pLameEncoder : IBaseFilter = nil;
pDump : IBaseFilter = nil;
pMediaControl: IMediaControl = nil;
pMediaEvent : IMediaEvent = nil;

Дальше, давайте подумаем о том, как мы будем получать входящие и исходящие контакты фильтров. Для интерфейса IBaseFilter существует метод EnumPins, который заполняет соответствующую структуру, и позволянт перебрать все контакты и узнать кое-какую информацию о них. Нам нужно будет получать для каждого фильтра входящие и исходящие контакты, поэтому неплохо бы написать функцию, которая по переданному интерфейсу и типу контакта (входящий или исходящий) будет выдавать контакт. Я вовсе не такой умный, чтобы писать такие функции, я просто посмотрел в MSDN, и нашел там приблизительно следующий варинт:

function GetPin(pFilter: IBaseFilter; pinDir: PIN_DIRECTION): IPin;
var
bFound: Boolean;
pEnum : IEnumPins;
pPin : IPin;
PinDirThis: PIN_DIRECTION;
begin
Result := nil;
bFound := false;
pFilter.EnumPins(pEnum);
while (pEnum.Next(1, pPin, 0) = S_OK) dobegin
pPin.QueryDirection(PinDirThis);
if (pinDir = PinDirThis) thenbegin
bFound := true;
break;
end;
end;
pEnum.Reset;
if bFound then Result := pPin;
end;

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

Что же, создаем КОМ-объекты и добавляем их к графу фильтров:

CoCreateInstance(CLSID_FilterGraph, nil, CLSCTX_INPROC, IID_IGraphBuilder,
 pGraphBuilder);
pGraphBuilder.AddSourceFilter(StringToOleStr(OpenDialog1.FileName), '',
  pSourceFile);
CoCreateInstance(CLSID_WaveParser, nil, CLSCTX_INPROC, IID_IBaseFilter,
   pWaveParser);
pGraphBuilder.AddFilter(pWaveParser, 'Wave Parser');
CoCreateInstance(CLSID_LameMPG3AE, nil, CLSCTX_INPROC, IID_IBaseFilter,
   pLameEncoder);
pGraphBuilder.AddFilter(pLameEncoder, 'Lame Mpeg Layer III Audio Encoder');
CoCreateInstance(CLSID_Dump, nil, CLSCTX_INPROC, IID_IBaseFilter, pDump);
pGraphBuilder.AddFilter(pDump, 'Dump');

Теперь нужно соединить фильтры между собой, а для этого нужно получить интерфейсы исходящего контакта для одного фильтра и входящего для другого, а затем соединить их с помощью соответствующего метода графа фильтров. Будем полагать, что у нас уже объявлены и проинициализированы (= nil) следующий переменные:

pSourceOut : IPin; // исходящий контакт фильтра источника
pWaveParserIn : IPin; // входящий контакт фильтра 'Wave Parser'
pWaveParserOut : IPin; // исходящий контакт фильтра 'Wave Parser'
pLameEncoderIn : IPin; // входящий контакт фильтра 'Lame Encoder'
pLameEncoderOut: IPin; // исходящий контакт фильтра 'Lame Encoder'
pDumpIn : IPin; // входящий контакт фильтра 'Dump'// Получаем исходящий контакт фильтра источника:
pSourceOut := GetPin(pSourceFile, PINDIR_OUTPUT);
// Получаем входящий контакт фильтра 'Wave Parser':
pWaveParserIn := GetPin(pWaveParser, PINDIR_INPUT);
// Теперь соединяем фильтр источника и 'Wave Parser' посредством этих контактов:
pGraphBuilder.Connect(pSourceOut, pWaveParserIn);
// Функция Connect как раз и производит соединение. Будем продолжать в том же духе:
pWaveParserOut := GetPin(pWaveParser, PINDIR_OUTPUT);
pLameEncoderIn := GetPin(pLameEncoder, PINDIR_INPUT);
pGraphBuilder.Connect(pWaveParserOut, pLameEncoderIn);
pLameEncoderOut := GetPin(pLameEncoder, PINDIR_OUTPUT);
pDumpIn := GetPin(pDump, PINDIR_INPUT);
pGraphBuilder.Connect(pLameEncoderOut, pDumpIn);

Все, мы полностью собрали вручную граф фильтров. Это, все-таки, довольно утомительная и однообразная работа, но ничего, зато мы теперь знаем, как это происходит. Остается установить имя mp3-файла, в который будет произведена конвертация. Для этого используем наш Dump, который, как и положено наследнику IFileSinkFilter, поддерживает метод установки имени файла:

(pDump as IFileSinkFilter).SetFileName(StringToOleStr(SaveDialog1.FileName), nil);
// Осталось только получить интерфейсы IMediaControl и IMediaEvent
// и начать конвертацию:
pGraphBuilder.QueryInterface(IID_IMediaControl, pMediaControl);
pGraphBuilder.QueryInterface(IID_IMediaEvent, pMediaEvent);
// Запускаем:
pMediaControl.Run;
pMediaEvent.WaitForCompletion(INFINITE, evCode);

Замечу, что, поскольку граф у нас именно конкретный, то не всякий файл можно подавать на вход. Но во всяком случай, файл 'Вход в Windows.wav' подходит. Вот и проверьте, насколько mp3 компактнее несжатого wav'а.

Готовый проект можно взять здесь.

Comments