- PVSM.RU - https://www.pvsm.ru -

Видеозапись с помощью Directshow.NET

Добрый день, уважаемые хабрапользователи. Некоторое время назад мне пришлось работать над несложным windows-приложением, в котором требовалось производить аудио- и видеозапись с различных устройств. В частности, захват аудио нужно было производить с шести каналов карты MAudio, а захват hd видео — с двух карт захвата AverMedia, сигнал на которые приходил с видеокамер по компонентному входу. Также нужно было делать скриншоты с документ-камеры, подключенной по USB-интерфейсу. Приложение было решено писать на C#, а видеозапись производить при помощи библиотеки DirectShow.NET.

По мотивам решения данной задачи возникла идея написать статью и поделиться опытом относительно видеозахвата. Может быть, кому-нибудь данная информация будет полезна. Кому интересно — прошу под кат.

Вместо предисловия.

Хотя для выполнения подобных задач сейчас все больше используется MediaFoundation [1], эта платформа, на мой взгляд, пока еще недостаточно распространена, даже с учетом того, что в новых версиях Windows, начиная с 8й, Microsoft постепенно отказыватся от использования и поддержки DirectShow. Существуют и различные библиотеки компьютерного зрения, поддерживающие возможность видеозаписи, такие как OpenCV или AForge, но при простом видеозахвате их мощный функционал обработки не очень требуется, да и внутри подобные библиотеки зачастую могут использовать DirectShow.

В интернете есть довольно много статей и материалов про то, что такое DirectShow [2] и как он работает, да и на Хабре проскакивала информация в этой статье [3], поэтому я постараюсь не щеголять терминами, которых могу не знать и сам, а рассмотрю все с практической стороны — каким образом человек, ранее не знакомый с Directshow, сможет написать свое приложение видеозаписи на C#, с чего ему начинать и куда двигаться, а также расскажу о проблемах, с которыми пришлось столкнуться.

Для примера (см. код на GitHub [4]) к этой статье я буду использовать простенькую usb-карту захвата EasyCap:

Видеозапись с помощью Directshow.NET

1. С чего начать. Требования, инструменты и информация

Инструменты, которые понадобятся, это:

1) K-Lite Codec Pack Mega [5] и инструмент GraphStudio — для быстрого прототипирования графа видеозахвата.

Видеозапись с помощью Directshow.NET

2) GraphEditPlus [6] — коммерческий аналог GraphStudio, который позволяет генерировать код. спроектированного графа на языках C++ и C#. Доступна 30-дневная триальная версия, ограничением которой является то, что сгенерированный код нельзя скопировать в буфер обмена.
3) Среда для разработки на С# — в моем случае это будет Visual Studio 12.
4) Библиотека DirectShow.net [7].
5) Библиотека WindowsMediaLib [8].

К сожалению, в интернете не получилось найти цельного и структурированного руководства по тому, как делать приложение видеозаписи, но некоторые страницы оказали поистине неоценимую помощь, это, в первую очередь:

1) Небольшая страничка [9], информация с которой стала катализатором всего процесса. Также там можно найти понятные описания классов и интерфейсов DirectShow.net. Очень полезная страница и огромная благодарность её автору.
2) Открытый исходный код, подобный этому [10], который помог разобраться с кроссбарами и прочими вопросами.
3) MSDN, в котором есть целый раздел [11], посвященный программированию DirectShow.

2. Фильтры, создание графа видеозаписи и визуальный редактор

Графы DirectShow строятся из фильтров, которые соединяются друг с другом входными и выходными пинами.
Более подробно об этом — здесь [12].

Для простейшего случая в GraphStudio можно построить граф, например, для встроенной видеокамеры следующим образом:

Видеозапись с помощью Directshow.NET

Но для нашей задачи требуется несколько фильтров, и граф (с учетом записи в WMV-формат) в результате должен выглядеть так:

Видеозапись с помощью Directshow.NET

На этом графе присутствуют фильтры:

SMI Grabber Device (группа фильтров — WDM Streaming Capture Devices) — фильтр, представляющий собой устройство захвата, именно с него мы получаем видео (а также аудио) потоки. Но в данном случае записывается не аудиопоток, поступающий c устройства захвата, а поток с микрофона (Фильтр «Микрофон...» из группы Audio Capture Sources).

SM BDA Crossbar Filter — фильтр кроссбара для устройства захвата, именно его настройка определяет коммутацию входного сигнала, поступает ли он со входа SVideo или с композитного входа.

Видеозапись с помощью Directshow.NET

Smart Tee — разветвитель потока, имеющий два выхода, поток с выхода Capture идет на запись в файл, а
поток с выхода Preview идет в окно предпросмотра через фильтр AVI Decompressor. Надо заметить, что цепочка
AVI Decompressor -> Video Renderer создается автоматически при выборе опции Preview -> Render Pin.
(в качестве отступления замечу, что бывают разные типы фильтров renderer, и одним из наиболее продвинутых является Enhanced Video Renderer, но в данном примере используется обычный фильтр по умолчанию)

WM ASF Writer — фильтр, обеспечивающий самый простой вариант записи видеофайла необходимого качества в формат WMV. При этом возможно менять качество записи, в том числе и на пользовательское.

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

3. Библиотека DirectShow.net и перевод графа в код

3.1. Генерация кода в GraphEditPlus

Следующий шаг — перевод получившегося графа в код. В этом деле неоценимую помощь оказывает инструмент GraphEditPlus. В целом этот редактор графов более удобен, чем GraphStudio из комплекта K-Lite, но самой главной его фичей является возможность генерации кода по построенному графу:

Видеозапись с помощью Directshow.NET

Видеозапись с помощью Directshow.NET

К сожалению, данный инструмент не может кастомизировать код настройки определенных фильтров, таких как Crossbar или WM ASF Writer, но в качестве первого шага он неоценим.

3.2. Приложение видеозаписи

Повторюсь, код простого приложения, написанного специально для этой статьи можно посмотреть и скачать здесь [4]. Заранее прошу прощения за его неоптимальность и нарушение SOLID, так как это всего лишь тестовый пример.

В данном приложении основные операции над графом (создание, уничтожение, поиск пинов, crossbar routing, запуск, остановка и прочие) определены в абстрактном классе VideoCapturebase [13], а классы-наследники, такие как VideoCapturePreview [14], VideoCaptureAsfRecord [15] или VideoCaptureScreenshots [16] реализуют абстрактный метод построения графа из фильтров BuildGraph(), добавляя новые фильтры в цепочку. Класс ControlVideoCommon [17] содержит операции создания окна и привязки к нему графа, операцию остановки и уничтожения графа, а также несколько других утилитарных операций.

3.3. Не всегда очевидные моменты

3.3.1. Добавление устройств

Если существует несколько устройств одного типа (несколько одинаковых карт захвата, например),
то у них будут одинаковые guid, но разные параметры DisplayName. В этом случае необходимо найти все устройства при помощи следующего кода:

private readonly List<DsDevice> _captures = new List<DsDevice>();
//...
//search for devices 
foreach (var device in DsDevice.GetDevicesOfCat(FilterCategory.VideoInputDevice))
{
	if (device.Name.ToLower().Contains(deviceNamePart.ToLower()) 
	{
		_captures.Add(device);
	}
}
//full device paths that differ
var devicePath1 = _captures[0].DevicePath;
var devicePath2 = _captures[1].DevicePath;
//...

Далее при создании графов используются уже пути devicePath1и devicePath2, полученные данным методом

3.3.2. Сrossbar routing

Устройство видеозахвата может иметь или не иметь кроссбар для использования разных типов видеовходов (например, AverMedia и EasyCap из данного примера — имеют, а встроенная веб-камера или карта захвата BlackMagic — нет). Следовательно, необходимо, чтобы связывание с кроссбаром производилось автоматически.

Для этого в базовом классе определеяется метод
FixCrossbarRouting(ref IBaseFilter captureFilter, PhysicalConnectorType? physicalConnectorType), который производит поиск и подключение кроссбара (при его наличии) с переключением на необходимый тип входа:

/// <summary>
/// Configure crossbar inputs and connect crossbar to input device
/// </summary>
/// <param name="captureFilter">Filter to find and connect crossbar for</param>
/// <param name="physicalConnectorType">crossbar connector type to be used</param>
/// <returns></returns>
protected int FixCrossbarRouting(ref IBaseFilter captureFilter, PhysicalConnectorType? physicalConnectorType)
{
    object obj = null;
    //fixing crossbar routing
    int hr = CaptureGraphBuilder.FindInterface(FindDirection.UpstreamOnly, null, captureFilter,
                                        typeof(DirectShowLib.IAMCrossbar).GUID, out obj);
    if (hr == 0 && obj != null)
    {
        //found something, check if it is a crossbar
        var crossbar = obj as IAMCrossbar;
        if (crossbar == null)
            throw new Exception("Crossbar object has not been created");

        int numOutPin;
        int numInPin;
        crossbar.get_PinCounts(out numOutPin, out numInPin);

        //for all output pins
        for (int iOut = 0; iOut < numOutPin; iOut++)
        {
            int pinIndexRelatedOut;
            PhysicalConnectorType physicalConnectorTypeOut;
            crossbar.get_CrossbarPinInfo(false, iOut, out pinIndexRelatedOut, out physicalConnectorTypeOut);

            //for all input pins
            for (int iIn = 0; iIn < numInPin; iIn++)
            {
                // check if we can make a connection between the input pin -> output pin
                hr = crossbar.CanRoute(iOut, iIn);
                if (hr == 0)
                {
                    //it is possible, get input pin info
                    int pinIndexRelatedIn;
                    PhysicalConnectorType physicalConnectorTypeIn;
                    crossbar.get_CrossbarPinInfo(true, iIn, out pinIndexRelatedIn, out physicalConnectorTypeIn);

                    //bool indication if current input oin can be connected to output pin
                    bool canRoute = physicalConnectorTypeIn == physicalConnectorType;

                    //get video from composite channel (CVBS)
                    //should output pin be connected to current input pin
                    if (canRoute)
                    {
                        //connect input pin to output pin
                        hr = crossbar.Route(iOut, iIn);
                        if (hr != 0) throw new Exception("Output and input pins cannot be connected");
                    }
                } //if(hr==0)
            } //for(iIn...)
        } //for(iOut...)
    } //if(hr==0 && obj!=null)
    return hr;
}
3.3.3. Освобождение ресурсов

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

if (Graph == null) return;
IEnumFilters ef;
var f = new IBaseFilter[1];
int hr = Graph.EnumFilters(out ef);
if (hr == 0)
{
    while (0 == ef.Next(1, f, IntPtr.Zero))
    {
        Graph.RemoveFilter(f[0]);
        ef.Reset();
    }
}
Graph = null;
3.3.4. Конфигурация потока (частота кадров, разрешение, и т.д.)

Устройства видеозахвата могут выдавать разные конфигурации видеопотока, между которыми можно переключаться. Например, hd-камера может выдавать как картинку 640 на 480 с частотой 60 кадров в секунду, так и картинку hd-качества с частотой кадров 30 кадров в секунду. Для частоты кадров существуют даже дробные цифры вроде 29.97 кадров в секунду. Для настройки подобных параметров нужно создать объект streamConfigObject при помощи метода FindInterface интерфейса CaptureGraphBuilder2, привести его к интерфейсу IAMStreamConfig, вызвать метод GetFormat, чтобы получить объект типа AMMEdiaType, получить заголовок:

var infoHeader = (VideoInfoHeader)Marshal.PtrToStructure(mediaType.formatPtr, typeof(VideoInfoHeader));

и в дальнейшем производить операции над его параметрами
AvgTimePerFrame,
BmiHeader.Width,
BmiHeader.Height
и другими.

В коде это можно посмотреть в методах ConfigureResolution и ConfigureFramerate класса VideoCaptureAsfRecord.

3.3.5. Скриншоты

Для того, чтобы можно было делать скриншоты с видеопотока, необходимо унаследовать класс, в котором строится граф (VideoCaptureScreenshots) от ISampleGrabberCB, и переопределить два метода — BufferCB и SampleCB.
SampleCB может быть пустым, а в BufferCB производим копирование полученного массива:

if ((pBuffer != IntPtr.Zero) && (bufferLen > 1000) && (bufferLen <= _savedArray.Length))
{
    Marshal.Copy(pBuffer, _savedArray, 0, bufferLen);
}

а также вызов обработчика:

_invokerControl.BeginInvoke(new CaptureDone(OnCaptureDone))

, в котором следует вызвать метод
SetCallback SamlpleGrabber'a

_iSampleGrabber.SetCallback(null, 0);

В методе же BuildGraph при включении фильтра SampleGrabber в цепочку следует произвести его настройку, а донастройку произвести после добавления остальных фильтров (магия, но иначе не работает). В тестовом примере за это отвечают методы ConfigureSampleGrabberInitial() и ConfigureSampleGrabberFinal(). При начальной настройке определяется AMMEdiaType, и при окончательной — установка VideoInfoHeader и вызов двух методов ISampleGrabber: SetBufferSamples(false) и SetOneShot(false).
Первый необходим для того, чтобы отключить буферизацию проходящих черезх фильтр сэмплов, а второй — для того, чтобы обратный вызов функции снимка экрана можно было дергать несколько раз.

3.3.6. Формат wmv, файлы .prx и WindowsMediaLib

Для того, чтобы обеспечить приемлемое качество записи, необходимо переопределить настройки записи wmv-файла.
Проще всего это сделать при помощи создания пользовательского файла профиля с расширением .prx и переопределения в нем параметров, отвечающих за качество потока. Пример данного файла в коде — good.prx [18]

Для считывания файлов профиля и создания по ним профиля в методе ConfigProfileFromFile(IBaseFilter asfWriter, string filename) был использован класс WMLib из проекта Team MediaPortal, распространяющегося под лицензией GPL. После создания профиль применяется к ASF Writer посредством метода ConfigureFilterUsingProfile(wmProfile) интерфейса IConfigAsfWriter.

Вместо послесловия или Большая Проблема, с которой пришлось столкнуться

Mpeg4Layer3, Кодеки, AVIMux и синхронизация аудио и видео

В самом начале разработки приложения была идея записывать видео в формате Mpeg4, а звук в формате Layer3, сводя все это при помощи AVI MUX в единый файл, как на следующем графе:

Видеозапись с помощью Directshow.NET

где на месте фильтра XVid Codec мог находиться любой фильтр из видеокомпрессоров в Mpeg-4. Были попытки использовать как xvid, так и ffdshow, и некоторые другие фильтры, однако, после нескольких попыток заставить граф записывать видео, стало понятно, что не все так просто, как кажется на первый взгляд. Возникала проблема обрыва записи спустя какое-то время после ее начала. Причина здесь, видимо, кроется в том, что при сведении видео и аудио в контейнере AVI MUX не производится автоматическая синхронизация видео и аудиодорожки, и даже с подстройкой правильной частоты граф мог остановиться в случайный момент, при этом запись обрывалась, а при воспроизведении можно было заметить, что аудио и видео выходили из синхронизации.

К сожалению, не смогу рассказать о решении данной проблемы, так как справиться с ней пришлось радикальным способом — переводом на запись в формат wmv при помощи ASF Writer.

Если эту статью читает кто-то, кто с данной проблемой сталкивался и знаком — буду рад совету.

Большое спасибо за внимание и интерес, надеюсь, данная статья не была смертельно скучной, и также надеюсь, что кому-нибудь данный материал может принести практическую пользу.

Автор: NeoNN

Источник [19]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/programmirovanie/30849

Ссылки в тексте:

[1] MediaFoundation: http://en.wikipedia.org/wiki/Media_Foundation

[2] DirectShow: http://en.wikipedia.org/wiki/DirectShow

[3] этой статье: http://habrahabr.ru/post/111609/

[4] см. код на GitHub: https://github.com/KonstantinFinagin/TestVideoCapture

[5] K-Lite Codec Pack Mega: http://fileforum.betanews.com/search?search=KLite-Mega-Codec-Pack

[6] GraphEditPlus: http://www.infognition.com/GraphEditPlus/

[7] DirectShow.net: http://directshownet.sourceforge.net/

[8] WindowsMediaLib: http://windowsmedianet.sourceforge.net/

[9] Небольшая страничка: http://wladm.narod.ru/C_Sharp/directshow.html

[10] этому: https://sources.team-mediaportal.com/svn/public/tags/Deprecated/MediaPortal%200.2.1.0/Core/DShowNET/Helper/CrossBar.cs

[11] раздел: http://msdn.microsoft.com/en-us/library/windows/desktop/dd375454(v=vs.85).aspx

[12] здесь: http://en.wikipedia.org/wiki/DirectShow#Architecture

[13] VideoCapturebase: https://github.com/KonstantinFinagin/TestVideoCapture/blob/master/TestVideoCapture/Capture/VideoCaptureBase.cs

[14] VideoCapturePreview: https://github.com/KonstantinFinagin/TestVideoCapture/blob/master/TestVideoCapture/Capture/VideoCapturePreview.cs

[15] VideoCaptureAsfRecord: https://github.com/KonstantinFinagin/TestVideoCapture/blob/master/TestVideoCapture/Capture/VideoCaptureAsfRecord.cs

[16] VideoCaptureScreenshots: https://github.com/KonstantinFinagin/TestVideoCapture/blob/master/TestVideoCapture/Capture/VideoCaptureScreenshots.cs

[17] ControlVideoCommon: https://github.com/KonstantinFinagin/TestVideoCapture/blob/master/TestVideoCapture/ControlVideoCommon.cs

[18] good.prx: https://github.com/KonstantinFinagin/TestVideoCapture/blob/master/TestVideoCapture/Qualities/good.prx

[19] Источник: http://habrahabr.ru/post/174867/