Практическое использование DirectShowNet

Практическое использование 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 по-русски или другими материалами, которых достаточно и в магазинах и в сети.

Остановимся на основных понятиях, используемых далее в статье:

  • DirectShow - это Windows API технология (базирующая на COM и потоках) и позволяющая работать с различными аудио и видеоустройствами (TV тюнерами, видео и веб-камерами, DVD-приводами, видео и аудио файлами...). DirectShow является расширяемой технологией и позволяет разработчикам создавать собственные компоненты обработки.

  • DirectShowNet - это библиотека (с открытым кодом, см. http://directshownet.sourceforge.net), позволяющая использовать методы DirectShow в .NET коде. Программист имеет возможность откомпилировать решение библиотеки и включить библиотеку в свой проект через References Solution.

  • Фильтры DirectShow - это Com объекты в которых производится какое либо действие над потоком (разделение, смешивание, копирование, генерация, анализ, отображение, изменение параметров...). Приложения создает фильтры и комбинирует их в требуемом для выполнения задачи порядке. Фильтры различают трех основных типов: источники, преобразователи и фильтры отображения (Rendered фильтры). Структура, элементами которой являются фильтры и связи между ними, называются графом фильтров. Примером графа фильтров может быть наш целевой граф статьи, представленный на Рис.1. Граф фильтров, который выполняет захват аудио или видео потоков принято называть графом захвата (Capture Graph).

  • GraphEdit - утилита, входящая в состав Windows SDK. С ее помощью можно соединять установленные и зарегистрированные в системе фильтры и просматривать графы фильтров, которые созданы в приложении. Фильтры в GraphEdit отображаются в виде прямоугольников ("кубиков") и имеют подписанные входы и выходы (Pins или пины). Пины с помощью мышки коммутируются в граф. Перед разработкой программного кода рекомендуется собрать целевой граф в GraphEdit и только после этого формировать программный код. Утилита бесплатна и ее можно без труда найти в сети (в частности в проекте "DirectShow по-русски ").

  • Интерфейсы - это ссылка на Com-объект. Получить интерфейс - значит получить ссылку и, таким образом, доступ к набору сгруппированных по какому либо признаку методов Com-объекта. Com-объект может поддерживать один или несколько интерфейсов.

  • Основные интерфейсы:

    • IBaseFilter - интерфейс управления фильтром. Включает основные методы для работы с большинством фильтров. Это методы получения и поиска пинов фильтра (EnumPins и FindPin), получения информации о фильтре (QueryFilterInfo, QueryVendorInfo, GetClassID, GetState, GetSyncSource), управления событиями фильтра (Pause, Run, Stop, SetSyncSource) и уведомления фильтра о присоединении к графу и отсоединении от него (JoinFilterGraph).

    • IGraphBuilder - интерфейс, с помощью которого можно программно строить граф. Основные методы этого интерфейса AddFilter, AddSourceFilter, Connect, ConnectDirect, Disconnect, Reconnect, RemoveFilter, Render, RenderFile. Эти методы позволяют добавлять и удалять фильтры, коммутировать пины (выходной пин -> входной пин) как непосредственно уже добавленных в граф фильтров (Connect, ConnectDirect), так и выполнять коммутацию с попыткой подбора и добавлением (при необходимости) промежуточных и выходных фильтров (Render, RenderFile).

    • ICaptureGraphBuilder2 - это интерфейс, специально предназначенный для упрощения построения графа захвата (Capture Graph). Он имеет дополнительные методы, среди которых SetOutputFileName (создание файла для записи из графа), AllocCapFile (установка размера файла), SetFiltergraph и GetFiltergraph (получение и установка графа), FindInterface (получение доступа к интерфейсам фильтров), ControlStream (установка временных параметров потоков), а также FindPin (поиск пина) и RenderStream (построение Capture Graph).

    • IMediaControl - интерфейс управления графом. Имеет методы управления событиями фильтра (Run, Pause, Stop), получение состояния графа и другие.

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


В начало

Параграф 2. Целевой граф и проект решения для его построения

Целевой граф показан на Рис.1. Он полностью отвечает поставленной в параграфе 1 задаче. Подобный граф может быть создан программно или в GraphEdit. Для удобства рассмотрения на рисунке цветами выделены некоторые этапы построения графа. На цифры соответствующего цвета будут даваться ссылки в тексте далее.

graph_002.gif

Рис.1. Целевой граф

Проект решения создается в MS Visual Studio 2005. Для построения данного графа создадим простой проект решения, поместив на форму два контрола Button, контрол Panel и один контрол PictureBox. Все свойства контролов, кроме названия кнопок оставим принятыми по умолчанию (Рис.2.).

graph_002.gif

Рис.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 тюнер вставлена антенна.

graph_003.gif

Рис.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();

graph_004.gif

Рис.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 сайты).

На главную страницу

К началу раздела и страницы

К началу подборки материала по C#


В начало


Сайт управляется системой uCoz