Практическое использование DirectShowNet для работы с TV тюнерамиАннотация: Эта статья появилась после того, как автору,
в силу срочной необходимости, пришлось долго и мучительно создавать
и заставлять правильно функционировать граф, показанный ниже на Рис.1.
Программа писалась на "не родном" языке (Delphi) и пришлось не один
раз обращаться на форумы. Как ни странно, на дельфийских форумах было
менее всего помощи. Основную помощь оказали как раз коллеги
- те, кто работает на C++. Автор выражает персональную
благодарность Vlafy - модератору форума
Тюнеры - Программирование за его ответы,
которые помогли разобраться в материале и легли в основу ряда решений,
и его участие в редактировании статьи.
Граф все-таки заработал как надо. Попытка сохранить наработанное на будущее
- "в лоб" переписать созданное на родной язык, не удалась. Работа
с DirectShowNet оказалась изобилующей множеством особенностей, которых
нет в DirectShow для других языков, нет ни в SDK, ни в примерах,
поставляемых с DirectShowNet. Именно этим особенностям и посвящена
эта статья.
Параграф 1. Постановка задачи, основные понятияТребуется создать программу, которая захватывает изображение с TV тюнера (например, Aver Media или Pinnacle). Изображение должно записываться в файл в формате avi вместе со звуком, отображаться на форме программы, а отдельные кадры видео потока передаваться в компонент (например, Picture Box) для отображеня. Приложение должно иметь возможность устанавливать требуемое разрешения видео потока (например, 720*576). Решение задачи предполагает, что читатель имеет представление о DirectShow, как составной части DirectX, и умеет работать с GraphEdit. Для тех, кто хочет знать основы, необходимые для работы с TV тюнерами, предлагается познакомиться с проектом DirectShow по-русски или другими материалами, которых достаточно и в магазинах и в сети. Остановимся на основных понятиях, используемых далее в статье:
Отметим, что ряд действий может быть выполнено на уровне пинов, для чего предназначен интерфейс управления контактом IPin. О других интерфейсах мы будем говорить в процессе построения графа. Параграф 2. Целевой граф и проект решения для его построенияЦелевой граф показан на Рис.1. Он полностью отвечает поставленной в параграфе 1 задаче. Подобный граф может быть создан программно или в GraphEdit. Для удобства рассмотрения на рисунке цветами выделены некоторые этапы построения графа. На цифры соответствующего цвета будут даваться ссылки в тексте далее.
Рис.1. Целевой граф Проект решения создается в MS Visual Studio 2005. Для построения данного графа создадим простой проект решения, поместив на форму два контрола Button, контрол Panel и один контрол PictureBox. Все свойства контролов, кроме названия кнопок оставим принятыми по умолчанию (Рис.2.).
Рис.2. Проект решения Скачанную библиотеку DirectShowlib-xxxx.dll поместим в корень папки проекта решения(или в любое другое место) и добавим на нее ссылку в References проекта решения(Рис.2.). Более наш проект решения ничем не отличается от обычного Windows Application решения. Параграф 3. Построение графа3.1. Создание графа и получение его интерфейсовФормирование графа будем выполнять в коде обработчика нажатия кнопки 1. Первым делом получаем ссылки на Com-объект графа и получаем доступ к методам построения и управления графом. Далее, связываем интерфейсы графа захвата(ICaptureGraphBuilder2) и построителя графа (IGraphBuilder). Как результат - оба интерфейса являются интерфейсами одного Com-объекта нашего строящегося графа. IMediaControl также является интерфейсом этого объекта как результат используемого метода его получения. Использование объекта DsROTEntry позволяет зарегистрировать граф в Running Object Table (ROT) - глобально доступной таблицы просмотра. Это позволяет использовать GraphEdit для просмотра графа на всех этапах его построения, вызвав в меню пункт "Connect to remote Graph". В функции StopAll() будем уничтожать все созданные объекты и ссылки на них. namespace tvCapture { public partial class Form1 : Form { private IGraphBuilder iGraphBuilder = null; private ICaptureGraphBuilder2 iCaptureGraphBuilder2 = null; private IMediaControl iMediaControl = null; int hr = 0; #if DEBUG DsROTEntry my_graph_to_root = null; #endif .............. private void button1_Click(object sender, EventArgs e) { if (iGraphBuilder != null) return; iGraphBuilder = (IGraphBuilder)new FilterGraph(); iCaptureGraphBuilder = (ICaptureGraphBuilder2)new CaptureGraphBuilder2(); iMediaControl = (IMediaControl)iGraphBuilder; hr = iCaptureGraphBuilder.SetFiltergraph(this.iGraphBuilder); if (hr != 0){StopAll();return;} #if DEBUG my_graph_to_root = new DsROTEntry(iGraphBuilder); #endif ..... 3.2. Добавление фильтров для захвата изображения и звукаПриступим к добавлению в граф фильтров. На первом этапе добавим непосредственно фильтр TV тюнера, аудио фильтр, фильтр для коммутации потоков и фильтр захвата (Рис.1. многоугольник 1). Большее число фильтров добавлять нельзя, так как рендеринг при большем числе фильтров приведет к тому, что коммутация контактов может оказаться не той, что нам требуется (в частности, для аудио потока, о чем речь пойдет ниже). Добавление фильтров выполняется по одной схеме: поиск фильтров относящихся к одной категории (FilterCategory или Guid), выбор фильтра по имени, создание фильтра (получение ссылки на Com-объект) и получение его интерфейса как базового фильтра (IBaseFilter), добавление фильтра в граф с использованием интерфейса IGraphBuilder. Отметим, что это не единственный, но наиболее понятный, способ добавления фильтров в граф. В приведенном коде можно добавить и выборку для других тюнеров, а имена фильтров в системе можно посмотреть при пошаговом выполнении цикла foreach, убрав в нем оператор break. В области определения переменных добавим: private string vsDeviceName = string.Empty; private IBaseFilter TunerF = null; private IBaseFilter AudioF = null; private IBaseFilter CrossBarF = null; private IBaseFilter CaptureF = null; int viI = 0; В обработчике нажатия кнопки 1 продолжим код: viI = -1; //Добавляеем фильтр TV тюнера //Поиск фильтра foreach (DsDevice dsDev in DsDevice.GetDevicesOfCat(FilterCategory.AMKSTVTuner)) { if (dsDev.Name.ToUpper().Contains("PINNACLE") || dsDev.Name.ToUpper().Contains("AVER")) { vsDeviceName = dsDev.Name; viI++; break; } } if (viI == -1) { StopAll(); return; } //Создание фильтра и получение его интерфейса TunerF = CreateFilter(FilterCategory.AMKSTVTuner, vsDeviceName); if (TunerF == null) { StopAll(); return; } //Добавление фильтра в граф hr = iGraphBuilder.AddFilter(TunerF, vsDeviceName/*"TV TUNER"*/); if (hr != 0) { StopAll(); return; } //Добавление фильтра захвата в граф //Поиск фильтра viI = -1; foreach (DsDevice dsDev in DsDevice.GetDevicesOfCat(FilterCategory.VideoInputDevice)) { if (dsDev.Name.ToUpper().Contains("PINNACLE") || dsDev.Name.ToUpper().Contains("AVER")) { vsDeviceName = dsDev.Name; viI++; break; } } if (viI == -1) { StopAll(); return; } //Создание фильтра и получение его интерфейса CaptureF = CreateFilter(FilterCategory.VideoInputDevice, vsDeviceName); //Добавление фильтра в граф hr = iGraphBuilder.AddFilter(CaptureF, vsDeviceName); if (hr != 0) { StopAll(); return; } //Добавление аудио фильтра в граф //Поиск фильтра viI = -1; foreach (DsDevice dsDev in DsDevice.GetDevicesOfCat(FilterCategory.AMKSTVAudio)) { if (dsDev.Name.ToUpper().Contains("PINNACLE") || dsDev.Name.ToUpper().Contains("AVER")) { vsDeviceName = dsDev.Name; viI++; break; } } if (viI == -1) { StopAll(); return; } //Создание фильтра и получение его интерфейса AudioF = CreateFilter(FilterCategory.AMKSTVAudio, vsDeviceName); //Добавление фильтра в граф hr = iGraphBuilder.AddFilter(AudioF, vsDeviceName); if (hr != 0) { StopAll(); return; } //Добавление фильтра коммутацмм аудио и видео потоков в граф //Поиск фильтра viI = -1; foreach (DsDevice dsDev in DsDevice.GetDevicesOfCat(FilterCategory.AMKSCrossbar)) { if (dsDev.Name.ToUpper().Contains("PINNACLE") || dsDev.Name.ToUpper().Contains("AVER")) { vsDeviceName = dsDev.Name; viI++; break; } } if (viI == -1) { StopAll(); return; } //Создание фильтра и получение его интерфейса CrossBarF = CreateFilter(FilterCategory.AMKSCrossbar, vsDeviceName); //Добавление фильтра в граф hr = iGraphBuilder.AddFilter(CrossBarF, vsDeviceName); if (hr != 0) { StopAll(); return 3.3. Коммутации фильтровДля коммутации фильтров можно использовать метод RenderStream ICaptureGraphBuilder2, методы Render и Connect интерфейса IGraphBuilder или метод Connect интерфейса IPin. В приведенном коде использованы методы Render и Connect интерфейса IGraphBuilder. Причем, для коммутации видео потока использован метод Render, а для аудио потока - Connect. Несомнено, что метод Render эффективнее метода Connect, но при попытке его применения для аудио потока будут не только коммутированы пины уже присутствующих в графе фильтров, но будет добавлен фильтр рендеринга звука (Default Direct Sound Device), что, на данном этапе создания графа, нам не подходит. Для сокращения объема кода, поиск входных и выходных пинов оформлен функцией ipinFindPin. Причем поиск по имени предполагает и наличие русского имени в названии пина. Вы, возможно обратили внимание на то, что на Рис.1. фильтр захвата имеет нечитаемые названия некоторых пинов. Непонятно, для чего разработчики национальных ОС для фильтров сделали перевод названий некоторых пинов (Capture - Запись, Preview - Просмотр..), но, поскольку GraphEdit отказывается понимать по русски, то пришлось в данной функции читать в пошаговом режиме имена фильтров (sPinName), и далее, включать прочитанные имена в вызов функции. Поскольку подписи не зависят от разработчиков драйверов, они "прошиты" в самой ОС, то предусмотрена возможность использовать два и более названия для имени пина (если потребуется). Отметим, что для поиска пинов часто наиболее подходящим является метод FindPin интерфейса ICaptureGraphBuilder2, который выглядит следующим образом: FindPin(object pSource, PinDirection pindir, DsGuid PinCategory, DsGuid MediaType, bool fUnconnected, int ZeroBasedIndex, out IPin ppPin); Однако, у нас в графе присутствуют фильтры, для которых есть несколько пин (входных или выходных) одной категории (MediaType). В этом случае потребуется указывать ZeroBasedIndex, что не всегда удобно. В силу этого, функция поиска пинов базируется на методе FindPin интерфейса IGraphBuilder: #region ipinFindPin // pinname1,pinname3 - English pinname2,pinname4 russian or English private IPin ipinFindPin(ref IBaseFilter filter, PinDirection pindir, string pinname1, string pinname2, string pinname3, string pinname4) { IPin pin = null; IEnumPins enumPins; IPin[] ipPins = new IPin[2]; PinInfo pininfo; int pcfetched = 0; PinDirection pindirs; filter.EnumPins(out enumPins); enumPins.Reset(); while (enumPins.Next(1, ipPins, (IntPtr)pcfetched) == 0) { string sPinName; ipPins[0].QueryId(out sPinName); ipPins[0].QueryDirection(out pindirs); ipPins[0].QueryPinInfo(out pininfo); if (pindir == pindirs && (sPinName.Contains(pinname1) || (pininfo.name.Contains(pinname1) || pininfo.name.Contains(pinname2)))) { if (pinname3 != string.Empty) { if (sPinName.Contains(pinname3) || (pininfo.name.Contains(pinname3) || pininfo.name.Contains(pinname4))) { pin = ipPins[0]; break; } } else { pin = ipPins[0]; break; } } }//while return pin; } #endregion Добавим в области определения переменных: private IPin OutPin = null; private IPin InPin = null; Как отмечено выше, для коммутации пинов выбран метод Connect интерфейса IGraphBuilder, который требует явного указания входного и выходного пина. Connect(IPin ppinOut, IPin ppinIn); Часто, более правильным будет использование метода ConnectDirect, который помимо явного указания входного и выходного пина при коммутации проверяет и медео тип пинов, но поскольку медео тип уже был определен при поиске, то метод Connect, в нашем случае, эквивалентен методу ConnectDirect: Код коммутации пинов будет следующим: //Видео поток OutPin = ipinFindPin(ref TunerF, PinDirection.Output, "Video", "Видео", "", ""); hr = iGraphBuilder.Render(OutPin); if (hr != 0) { StopAll(); return; } //Аудио поток Тюнер - аудио фильтр OutPin = ipinFindPin(ref TunerF, PinDirection.Output, "Audio", "Аудио", "", ""); InPin = ipinFindPin(ref AudioF, PinDirection.Input, "Audio", "Аудио", "In", ""); hr = iGraphBuilder.Connect(OutPin, InPin); if (hr != 0) { StopAll(); return; } //Audio поток аудио фильтр коммутатор OutPin = ipinFindPin(ref AudioF, PinDirection.Output, "Audio", "Аудио", "Out", "Выход"); InPin = ipinFindPin(ref CrossBarF, PinDirection.Input, "Audio", "Аудио","Tuner", "Тюнер"); hr = iGraphBuilder.Connect(OutPin, InPin); if (hr != 0) { StopAll(); return; } //Audio поток коммутатор - фильтр захвата OutPin = ipinFindPin(ref CrossBarF, PinDirection.Output, "Audio", "Аудио","Decoder", "Декодер"); InPin = ipinFindPin(ref CaptureF, PinDirection.Input, "Audio", "Аудио","Analog", "Аналог"); hr = iGraphBuilder.Connect(OutPin, InPin); if (hr != 0) { StopAll(); return; } 3.4. Необходимость применения SmartTee фильтраМы поставили задачу вывода, записи изображения и захвата отдельных кадров в формате 720*576. Формат по умолчанию 320*240. Фильтр захвата имеет пины Capture и Preview, тоесть одиночный видео поток на входе делится на два потока. Что бы сохранить общую производительность на пине Capture, на пин Preview, при необходимости, начинает выдаваться меньшее число фрэймов. Это может привести к полному пропаданию информации на выходе Preview. Поэтому, в данном решении мы добавляем SmartTee фильтр, задача которого разделить видео поток 720*568 на два, один для отображения и захвата кадра, второй на запись. Добавим код для включения в граф SmartTee фильтра (Рис.1. прямоугольник 2): IBaseFilter SmartTeeF = null; SmartTeeF = (IBaseFilter)new SmartTee(); hr = iGraphBuilder.AddFilter(SmartTeeF, "Smart Tee"); if (hr != 0) { if (SmartTeeF != null) { Marshal.ReleaseComObject(SmartTeeF); SmartTeeF = null; } StopAll(); return; } OutPin = ipinFindPin(ref CaptureF, PinDirection.Output, "Capture", "Запись", "", ""); InPin = ipinFindPin(ref SmartTeeF, PinDirection.Input, "Input", "Input", "", ""); hr = iGraphBuilder.Connect(OutPin, InPin); if (hr != 0) { if(SmartTeeF != null) { Marshal.ReleaseComObject(SmartTeeF); SmartTeeF = null; } StopAll(); return; } 3.5. SampleGrabber. Включение в граф и конфигурированиеДля захвата одиночных снимков используется фильтр SampleGrabber ("хапуга сымплов" - ну и название). Его использование DirectShowNet не является таким же тривиальным, как например в Delphi при использовании DsPack. И, хотя в приложении к DirectShowNet дан пример использования SampleGrabber, но выбор того, что надо включить в свое приложение, а что нет - не так уж и тривиален. Приложение должно наследовать интерфейс ISampleGrabberCB. Для работы с SampleGrabber потребуется добавить функции интерфейса, функцию-делегата, несколько переменных и массив для хранения битов изображения. Прежде всего делаем класс Form1 нашего приложения наследующим ISampleGrabberCB и включаем в него необходимые переменные, массив и функции: namespace tvCapture { public partial class Form1 : Form,ISampleGrabberCB { ....... private ISampleGrabber isampleGrabber = null; private IBaseFilter SampleGrabberF = null; private bool fCaptured = true; private byte[] savedArray; private int viBufferedSize; private delegate void CaptureDone(); private VideoInfoHeader videoInfoHeader; ....... Следующим этапом - является добавление в приложение двух функций интерфейса: SampleCB и BufferCB. Функция SampleCB может быть пустой. Задача функция BufferCB - извлечение данных в битовый массив. int ISampleGrabberCB.SampleCB(double SampleTime, IMediaSample pSample) { return 0; } int ISampleGrabberCB.BufferCB(double SampleTime, IntPtr pBuffer, int BufferLen) { if (fCaptured || (savedArray == null)) { return 1; } fCaptured = true; bufferedSize = BufferLen; if ((pBuffer != IntPtr.Zero) && (BufferLen > 1000) && (BufferLen <= savedArray.Length)) { Marshal.Copy(pBuffer, savedArray, 0, BufferLen); } else { return 1; } this.BeginInvoke(new CaptureDone(this.OnCaptureDone)); return 0; } Требуется также функция обратного вызова, которая по завершении извлечения картинки из буфера преобразует ее и поместит в PictureBox. void OnCaptureDone() { int hr=0; if (isampleGrabber == null) return; try { hr = isampleGrabber.SetCallback(null, 0); int w = videoInfoHeader.BmiHeader.Width; int h = videoInfoHeader.BmiHeader.Height; if (((w & 0x03) != 0) || (w < 32) || (w > 4096) || (h < 32) || (h > 4096)) return; int stride = w * 3; GCHandle handle = GCHandle.Alloc(savedArray, GCHandleType.Pinned); int scan0 = (int)handle.AddrOfPinnedObject(); scan0 += (h - 1) * stride; Bitmap b = new Bitmap(w, h, -stride, PixelFormat.Format24bppRgb, (IntPtr)scan0); handle.Free(); savedArray = null; Image oldImage = pictureBox1.Image; pictureBox1.Image = b; if (oldImage != null) oldImage.Dispose(); } catch (Exception ee) { MessageBox.Show(this, ee.Message, "DirectShow.NET", MessageBoxButtons.OK, MessageBoxIcon.Stop); } } Естественно, необходим толчок к захвату снимка. Его оформим в обработчике нажатия кнопки 2. О создании videoInfoHeader мы будем вести речь ниже, при обсуждении вопроса о конфигурировании видео потока. private void button2_Click(object sender, EventArgs e) { if (isampleGrabber == null) return; if (savedArray == null) { int size = videoInfoHeader.BmiHeader.ImageSize; if ((size < 1000) || (size > 16000000)) return; savedArray = new byte[size + 64000]; } Image old = pictureBox1.Image; pictureBox1.Image = null; if (old != null) old.Dispose(); fCaptured = false; hr = isampleGrabber.SetCallback(this, 1); } И последнее - требуется создать сам SampleGrabber и провести его предварительную настройку. Создаем SampleGrabber, продолжая код в обработчике нажатия кнопки 1. isampleGrabber = (ISampleGrabber) new SampleGrabber(); SampleGrabberF = (IBaseFilter)isampleGrabber; hr = iGraphBuilder.AddFilter(SampleGrabberF, "Sample Grabber"); if (hr != 0) { StopAll(); return; } //Вызывается функция предварительного конфигурирования ConfigureSampleGrabber1(ref isampleGrabber); if (hr < 0) { StopAll(); return; } //Коммутируем выход Preview SmartTee фильтра на вход SamleGrabber OutPin = ipinFindPin(ref SmartTeeF, PinDirection.Output, "Preview", "Просмотр", "", ""); InPin = ipinFindPin(ref SampleGrabberF, PinDirection.Input, "Input", "Input", "", ""); hr = iGraphBuilder.Connect(OutPin, InPin); if (hr != 0) { StopAll(); return; } Код предварительного конфигурирования вынесем в отдельную функцию: private int ConfigureSampleGrabber1(ref ISampleGrabber isampleGrabber) { int hr; AMMediaType mediatype = new AMMediaType(); mediatype.majorType = MediaType.Video; mediatype.subType = MediaSubType.RGB24; mediatype.formatType = FormatType.VideoInfo; hr = isampleGrabber.SetMediaType(mediatype); DsUtils.FreeAMMediaType(mediatype); mediatype = null; return hr; } На этом работу с SampleGrabber можно считать почти законченной, но нажимать кнопку 2 мы пока не будем (до создания videoInfoHeader). Кроме того, еще не полностью проведено конфигурирование и самого SampleGrabber. 3.6. VideoVindow. Просмотр изображенияНа данном этапе у нас видео поток доведен до SampleGrabber (Рис.1. прямоугольник 3.) и можно организовать его вывод на панель приложения. Воспользуемся методом Render интерфейса IGraphBuilder: OutPin = ipinFindPin(ref SampleGrabberF, PinDirection.Output, "Output", "Output", "", ""); hr = iGraphBuilder.Render(OutPin); if (hr != 0) { StopAll(); return; } После выполнения данного кода формирование видео потока для просмотра будет закончено(Рис.1. прямоугольник 4.). Осталось указать то, где будем показывать телепрограмму - создать видео окно и связать его с нашим графом. Определяем интерфейс видео окна: public partial class Form1 : Form,ISampleGrabberCB { ........ private IVideoWindow iVideoWindow; ........ Указываем графу, что он должен выполнять рендеринг на панели приложения (иначе вывод будет в отдельном окне): iVideoWindow = (IVideoWindow)iGraphBuilder; if (iVideoWindow != null) { hr = iVideoWindow.put_Owner(panel1.Handle); if(hr == 0) { hr = iVideoWindow.put_WindowStyle(WindowStyle.Child | WindowStyle.ClipChildren); Rectangle rc = panel1.ClientRectangle; iVideoWindow.SetWindowPosition(0, 0, rc.Right, rc.Bottom); iVideoWindow.put_Visible(OABool.True); } } if (hr != 0) { StopAll(); return; } 3.7. Запись изображенияПрежде всего определим переменную для имени файла для записи и фильтр мультиплексора (для смешивания изображения и звука) и фильтр FileSinkWriter (для записи потока в файл): public partial class Form1 : Form,ISampleGrabberCB { ........ private string sFileName=string.Empty; private IBaseFilter MultiplexerF; private IFileSinkFilter FileSinkWriter; ........ Добавляем фильтры мультиплексор и фильтр для записи (имеет интерфейс IFileSinkFilter): sFileName = @"C:\1.avi"; hr = iCaptureGraphBuilder2.SetOutputFileName(MediaSubType.Avi, sFileName, out MultiplexerF, out FileSinkWriter); if (hr != 0){StopAll();return;} Результат - Рис.1. прямоугольник 5. Осталось коммутировать SmartTee на мультиплексор: OutPin = ipinFindPin(ref SmartTeeF, PinDirection.Output, "Capture", "Запись", "", ""); InPin = ipinFindPin(ref MultiplexerF, PinDirection.Input, "Input", "Input", "", ""); hr = iGraphBuilder.Connect(OutPin, InPin); if (hr != 0){StopAll();return;} 3.8. Запись и вывод звукаЕще раз посмотрим Рис.1. Звуковой поток у нас доведен до Audio выхода фильтра захвата. Его требуется разделить на два потока - записи в файл и вывода на динамики компьютера. Нам опять понадобится фильтр для разделения потоков. Таким фильтром является InfTee фильтр. Добавим его в граф. public partial class Form1 : Form,ISampleGrabberCB { ........ private IBaseFilter iInfTeeF = null; private InfTee TeeF = null; ........ Код для добавления и коммутации InfTee фильтра может быть таким: TeeF = new InfTee(); iInfTeeF = (IBaseFilter)TeeF; hr = iGraphBuilder.AddFilter((IBaseFilter)TeeF, "InfTee"); if (hr != 0) { StopAll(); return; } OutPin = ipinFindPin(ref CaptureF, PinDirection.Output, "Audio", "Audio", "", ""); InPin = ipinFindPin(ref iInfTeeF, PinDirection.Input, "Input", "Input", "", ""); hr = iGraphBuilder.Connect(OutPin, InPin); if (hr != 0) { StopAll(); return; } OutPin = ipinFindPin(ref iInfTeeF, PinDirection.Output, "Output", "Output", "", ""); hr = iGraphBuilder.Render(OutPin); if (hr != 0) { StopAll(); return; } При коммутации пинов мы использовали метод Render интерфейса IGraphBuilder. В отличии от случая описанного выше, в данном случае есть куда направлять звуковой поток - на мультиплексор и, поэтому добавление Default Audio Render не произойдет ( Рис.1. прямоугольник 6.). Однако теперь нам придется самостоятельно его добавлять и коммутировать. Это выполним уже знакомыми нам способами (правда опять с маленькими особенностями, которые показывают еще один вариант поиска фильтров): object source = null; Guid iid = typeof(IBaseFilter).GUID; foreach (DsDevice device in DsDevice.GetDevicesOfCat(FilterCategory.AudioRendererCategory)) { if (device.Name.Contains("Default")) { device.Mon.BindToObject(null, null, ref iid, out source); break; } } AudioRenderF = (IBaseFilter)source; hr = iGraphBuilder.AddFilter(AudioRenderF, "AudioF Render"); if (hr != 0) { StopAll(); return; } OutPin = ipinFindPin(ref iInfTeeF, PinDirection.Output, "Output", "Output", "2", "2"); InPin = ipinFindPin(ref AudioRenderF, PinDirection.Input, "Input", "Input", "", ""); hr = iGraphBuilder.Connect(OutPin, InPin); if (hr != 0) { StopAll(); return; } Результат - Рис.1. прямоугольник 7 и полностью построенный граф. Параграф 4. Настройка и внутренняя коммутация потоковКак отмечено выше - на данном этапе граф построен полностью. Но всей целевой функциональности он выполнять не будет. Скорее всего он будет нем, а изображение 320*240. Кроме того, для захвата изображения снимков экрана нам необходим VideoInfoHeader. Для того, что бы заставить граф работать так как надо, добавим в код несколько функций, предназначение которых ясно из их названий: fSetTVTunerParamAndRouteCrossBar(); iSetVideoFormat(15, 720, 568); ConfigureSampleGrabber2(ref isampleGrabber); //Стартовать граф iMediaControl.Run(); Рассмотрим эти функции. Первая из них предназначена для установки параметров TV тюнера и коммутации фильтра GrossBar. Настройка заключается в установке параметров TV тюнера, показанных на Рис.3(слева). Отметим, что большинство параметров по умолчанию соответствуют поставленной цели, однако код страны вводить необходимо всегда. Кроме того "Тип входа" должен быть кабель - несмотря на то, что в TV тюнер вставлена антенна.
Рис.3. Параметры TV тюнера и фильтра коммутации Задача настройки фильтра коммутации заключается в сопоставлении входа и выхода для аудио и видео потоков. Как видно из Рис.3(справа) 3й входной контакт должен быть коммутирован на 1й выходной, а 0й входной на 0й выходной. Это так называемый ройтинг. Эта задача также выполняется в функции fSetTVTunerParamAndRouteCrossBar. Приведем один из возможных ее кодов: private bool fSetTVTunerParamAndRouteCrossBar() { object obj = null; hr = iCaptureGraphBuilder2.FindInterface(null, null, TunerF, typeof(IAMTVTuner).GUID, out obj); if (hr >= 0) { IAMTVTuner TVTuner = (IAMTVTuner)obj; TVTuner.put_Mode(AMTunerModeType.TV); TVTuner.put_InputType(0, TunerInputType.Cable); TVTuner.put_CountryCode(7); //TVTuner.put_TuningSpace(0); hr = iCaptureGraphBuilder2.FindInterface(null, null, CaptureF, typeof(DirectShowLib.IAMCrossbar).GUID, out obj); if (hr >= 0) { IAMCrossbar crossbar = (IAMCrossbar)obj; int numOutPin, numInPin; int nOutputAudioLink, nInputAudioLink, nOutputVideoLink, nInputVideoLink; nOutputAudioLink = nInputAudioLink = nOutputVideoLink = nInputVideoLink = -1; crossbar.get_PinCounts(out numOutPin,out numInPin); int pIdxRel; PhysicalConnectorType pct; for (int i = 0; i < numInPin; i++) { crossbar.get_CrossbarPinInfo(true, i, out pIdxRel, out pct); if (pct == PhysicalConnectorType.Audio_Tuner) nInputAudioLink = i; if (pct == PhysicalConnectorType.Video_Tuner) nInputVideoLink = i; if (nInputAudioLink != -1 && nInputVideoLink != -1) break; } for (int i = 0; i < numOutPin; i++) { crossbar.get_CrossbarPinInfo(false, i, out pIdxRel, out pct); if (pct == PhysicalConnectorType.Audio_AudioDecoder) nOutputAudioLink = i; if (pct == PhysicalConnectorType.Video_VideoDecoder) nOutputVideoLink = i; if (nOutputAudioLink != -1 && nOutputVideoLink != 1) break; } try { if (crossbar.Route(nOutputAudioLink, nInputAudioLink) >= 0 && crossbar.Route(nOutputVideoLink, nInputVideoLink) >= 0) { obj = null; return true; } else { obj = null; return false; } } catch { obj = null; return false; } } } else { obj = null; return false; } obj = null; return true; } Следующая функция iSetVideoFormat. Ее основная задача установить формат видео потока равным 720*576. Это вольный перевод написанной мной на Delphi функции, посему не исключено, что в ней не все так, как надо, но по крайней мере она работает. private int iSetVideoFormat(int viF, int viWidth, int viHeight) { int hr; int r = 0; object comObj = null; hr = iCaptureGraphBuilder2.FindInterface(PinCategory.Capture, MediaType.Video, CaptureF, typeof(IAMStreamConfig).GUID, out comObj); if (hr != 0) { return hr; } IAMStreamConfig iamsc = comObj as IAMStreamConfig; int CapCount, CapSize; iamsc.GetNumberOfCapabilities(out CapCount, out CapSize); AMMediaType mediatype = new AMMediaType(); iamsc.GetFormat(out mediatype); VideoInfoHeader videoInfoHeader = (VideoInfoHeader)Marshal.PtrToStructure(mediatype.formatPtr, typeof(VideoInfoHeader)); videoInfoHeader.BmiHeader.Width = viWidth; videoInfoHeader.BmiHeader.Height = viHeight; videoInfoHeader.BmiHeader.BitCount = 24; videoInfoHeader.AvgTimePerFrame = 10000000 / viF; r = videoInfoHeader.BmiHeader.ImageSize; videoInfoHeader.BmiHeader.ImageSize = viWidth * viHeight * 3; DsRect rt =new DsRect(0,0,viWidth,viHeight); videoInfoHeader.SrcRect = rt; videoInfoHeader.TargetRect = rt; Marshal.StructureToPtr(videoInfoHeader,mediatype.formatPtr, false); mediatype.sampleSize = viWidth * viHeight * 3; hr = iamsc.SetFormat(mediatype); return hr; } И последняя функция - заключительная настройка SapleGrabber. Она должна быть именно здесь, и только после настройки параметров видно потока. Кроме того в ней получаем давно обещанную ссылку на VideoInfoHeader, которая необходима для получения картинки с SapleGrabber. private int ConfigureSampleGrabber2(ref ISampleGrabber isampleGrabber) { int hr; AMMediaType mediatype = new AMMediaType(); hr = isampleGrabber.GetConnectedMediaType(mediatype); if (hr < 0) { return 1; } if ((mediatype.formatType != FormatType.VideoInfo) || (mediatype.formatPtr == IntPtr.Zero)) { return 1; } videoInfoHeader = (VideoInfoHeader)Marshal.PtrToStructure(mediatype.formatPtr, typeof(VideoInfoHeader)); Marshal.FreeCoTaskMem(mediatype.formatPtr); mediatype.formatPtr = IntPtr.Zero; hr = isampleGrabber.SetBufferSamples(false); if (hr == 0) hr = isampleGrabber.SetOneShot(false); if (hr == 0) hr = isampleGrabber.SetCallback(null, 0); if (hr < 0) { return 1; } return 0; } Осталось запустить граф, и насладиться проделанной работой (Рис.4.): iMediaControl.Run();
Рис.4. Программа в работе При выходе из обработчика следует удалить все объекты и ссылки на интерфейсы, которые более не нужны, а при закрытии формы и те, которые используются, в том числе предусмотреть отсоединение от Rot нашего графа (иначе при закрытии программы Вы будите получать ошибку). #if DEBUG if (my_graph_to_root != null) { my_graph_to_root.Dispose(); } #endif P.S. В статье остались неописанные методы. По просьбам: #region IBaseFilter CreateFilter(Guid category, string friendlyname) private IBaseFilter CreateFilter(Guid category, string friendlyname) { object source = null; Guid iid = typeof(IBaseFilter).GUID; foreach (DsDevice device in DsDevice.GetDevicesOfCat(category)) { if (device.Name.CompareTo(friendlyname) == 0) { device.Mon.BindToObject(null, null, ref iid, out source); break; } } return (IBaseFilter)source; } #endregion #region private void StopAll() { if (iGraphBuilder != null) { Marshal.ReleaseComObject(iGraphBuilder); iGraphBuilder = null; } if (iCaptureGraphBuilder2 != null) { Marshal.ReleaseComObject(iCaptureGraphBuilder2); iCaptureGraphBuilder2 = null; } if (iMediaControl != null) { Marshal.ReleaseComObject(iMediaControl); iMediaControl = null; } if (TunerF != null) { Marshal.ReleaseComObject(TunerF); TunerF = null; } if (InPin != null) { Marshal.ReleaseComObject(InPin); InPin = null; } if (OutPin != null) { Marshal.ReleaseComObject(OutPin); OutPin = null; } if (SampleGrabberF != null) { Marshal.ReleaseComObject(SampleGrabberF); SampleGrabberF = null; } if (iVideoWindow != null) { Marshal.ReleaseComObject(iVideoWindow); iVideoWindow = null; } if (iMultiplexerF != null) { Marshal.ReleaseComObject(iMultiplexerF); iMultiplexerF = null; } if (iInfTeeF != null) { Marshal.ReleaseComObject(iInfTeeF); iInfTeeF = null; } if (SmartTeeF != null) { Marshal.ReleaseComObject(SmartTeeF); SmartTeeF = null; } if (AudioRenderF != null) { Marshal.ReleaseComObject(AudioRenderF); AudioRenderF = null; } } #endregion Молчанов Владислав 12.09.2007г. Еcли Вы пришли с поискового сервера - посетите мою главную страничкуНа главной странице Вы найдете программы комплекса Veles - программы для автолюбителей, программы из раздела графика - программы для работы с фото, сделанными цифровым фотоаппаратом, программу Bricks - игрушку для детей и взрослых, программу записную книжку, программу TellMe - говорящий Русско-Английский разговорник - программу для тех, кто собирается погостить за бугром или повысить свои знания в английском, теоретический материал по программированию в среде Borland C++ Builder, C# (Windows приложения и ASP.Net Web сайты). |