...

воскресенье, 12 января 2014 г.

[Из песочницы] Бесшовное разбиение и склейка видео с помощью DirectShow

Один из наших отделов занимается ручным тестированием мультимедиа-компонентов для автомобилей. При этом постоянно ведется видеозапись всех производимых действий (нажатие кнопок, вставка дисков и т. п.) и реакции системы: одна из камер направлена на дисплей. Видео в данном случае является доказательством наблюдения ошибки, а также предоставляет разработчикам ценную информацию о том, какие действия производились и как быстро. Согласитесь, информация весьма важная для багрепортов, не так ли?

Специфика систем состоит в том, что ошибки могут возникнуть спонтанно и неожиданно, на любом этапе тесткейса, а то и вообще просто в режиме ожидания, когда запись видео не ведется. Интересующихся приглашаю под кат, где я опишу разработанное мной решение для бесшовного разбиения и склеивания видео. Благодаря нему запись ведется весь день, и видео сохраняется в удобные файлы небольшого размера, что позволяет нам отлавливать и документировать редчайшие ошибки, а заодно и радовать разработчиков десятками видео с невозможными реакциями системы.



Зачем разбивать видео?




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

Что нам предлагает DirectShow




Для реализации была выбрана технология DirectShow. Одно из требований также — на выходе должны получаться файлы Windows Media, а именно WMV3, так что было принято решение делать компрессию на лету, благо современные компьютеры это с легкостью позволяют. Основная идея такова: нам необходима возможность в произвольный момент времени переключить входящие потоки аудио и видео на другой файл, не потеряв при этом ни кадра. Так мы сможем вести запись в файлы продолжительностью, скажем, две минуты, а при необходимости бесшовно склеивать их.

Построим самый обычный граф фильтров для записи видео со звуком в формат Windows Media и с предпросмотром. Получится что-то вроде этого:


Как работает этот граф? Два входных фильтра для аудио и видео обслуживаются разными потоками, которые доставляют сэмплы (samples) на входные пины следующих фильтров. С помощью фильтра Smart Tee мы дублируем входящие видео-данные, одна копия отправляется на экран в Video Renderer, а вторая уходит в фильтр WM ASF Writer, который собственно и производит синхронизацию аудио и видео, их компрессию и запись в файл.


Решение «в лоб» с двумя фильтрами, которые можно было бы попеременно использовать в графе, изменяя имена выходных файлов, не работает.


У графа DirectShow есть одна особенность: до тех пор, пока он не будет остановлен, все его «выходные» фильтры держат файлы открытыми и не финализируют их. Кроме того, без остановки графа невозможно поменять имена выходных файлов или соединять/разъединять фильтры. Но остановка и запуск графа чреваты потерями нескольких кадров, а то и нескольких секунд! Ясно, что стандартными средствами не обойтись.


Самостоятельные графы




Одно из решений — сделать граф захвата аудио- и видео-данных (Capture Graph) независимым от графа записи (Record Graph), чтобы можно было останавливать последний для финализации файлов. Это возможно, например, с помощью GMFBridge от создателя DirectShow — Geraint Davies. Примерная схема работы всей системы выглядела бы так:


GMFBridge находится одновременно во всех трех графах, позволяя на лету переключать потоки сэмплов между первым и вторым Record Graph, не теряя ни одного сэмпла. В то время как один из графов записи коспрессирует наше видео, мы настраиваем второй граф (имя выходного файла), благо он вполне может быть остановлен, не влияя на остальные. В нужный момент мы запускаем второй граф, переключаем GMFBridge и останавливаем первый. Вуаля!


Но такое решение имеет очевидные недостатки. Во-первых, ресурсы. Необходимо иметь две копии графов записи, что отрицательно сказывается на общей производительности и использовании памяти. Кроме того, при наличии одновременно видео и аудио крайне сложно синхронизировать их — каждый граф имеет свой отсчет времени, к тому же сами сэмплы поставляются разными потоками. Все это приводило к спонтанным «зависаниям» самого GMFBridge в момент переключения графов, так что от этого решения было решено отказаться. Исходный код инструмента, конечно, открыт, и при желании можно было бы разобраться в причинах его нестабильной работы, но все же желание сэкономить ресурсы перевесило, и я решил подойти к задаче с другой стороны.


Пишем свой ASF Writer




С преферансом и куртизанками. Точно! Нам нужен такой WM ASF Writer, который умел бы по команде сам переключаться на другой файл без необходимости останавливать граф. Тогда мы сможем взять первый и самый простой граф, вставить туда наш кастомный фильтр вместо стандартного WM ASF Writer и радоваться жизни.

Создадим свой фильтр, добавив к стандартным методам еще один новый StreamToFile, который будет служить для переключения между файлами.

class CCustomASFWriter : public CBaseFilter
{
public:
STDMETHOD(StreamToFile)(BSTR szFileName);
}




По поводу примеров кода

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





Чтобы не потерять сэмплы в момент переключения, а также чтобы не блокировать потоки доставки сэмплов, добавим в наш фильтр многопоточную очередь для входящих данных. Я использовал реализацию наподобие вот этой, немного допилив ее для использования в режиме multiple producers — single consumer. Очередь я решил использовать одну и для видео, и для аудио, и вот почему. Все упирается в нашу новую функцию переключения файлов. Для этого важно помнить, что частота поставки видео-сэмплов, как правило, много выше таковой у аудио: например, 30 Гц видео и 2 Гц (по 500 мс на сэмпл) у аудио. Соответственно, переключение нужно производить сразу после доставки аудио-сэмпла. Соблюдая естественный порядок сэмплов в очереди, можно очень удобно делать именно так.

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



HRESULT STDMETHODCALLTYPE CCustomASFWriter::StreamToFile(BSTR szFileName)
{
wcscpy_s(m_szCurrentFile, szFileName);
{
CAutoLock lock(m_pLock);
m_bSwitchRequested = TRUE;
}
return S_OK;
}




Собственно сама компрессия и запись в файлы происходит с использованием Windows Media Format SDK, а именно интерфейса IWMWriter.

IWMWriter *pWriter = NULL;
WMCreateWriter(NULL, &pWriter);




Для этого в отдельном потоке крутится цикл обработки входящих сэмплов:

while (bRunning)
{
StreamSamplesToWriter(pWriter);
DWORD dwWaitResult = WaitForSingleObject(hEventStopStreaming, 33);
if (dwWaitResult == WAIT_OBJECT_0)
{
m_pPinVideo->StopQueuingNow();
m_pPinAudio->StopQueuingNow();
bRunning = FALSE;
}
}




Самое интересное и происходит в методе StreamSamplesToWriter. Здесь сэмплы отправляются в IWMWriter, а также происходит переключение файлов в правильный момент времени, если был дан сигнал к переключению с помощью метода StreamToFile.

STDMETHODIMP CCustomASFWriter::StreamSamplesToWriter(IWMWriter *pWriter)
{
BOOL bMustSwitch = FALSE;
void *pObject = NULL;

while (m_pSamplesQueue->Pop(pObject))
{
CQueuedSample *pSample = (CQueuedSample*)pObject;
DWORD inputNumber = pSample->MediaType == MEDIATYPE_Video ? m_pPinVideo->InputNumber : m_pPinAudio->InputNumber;

INSSBuffer *pBuffer = NULL;
pWriter->AllocateSample(pSample->DataSize, &pBuffer);
LPBYTE pbDestBuffer = NULL;
pBuffer->GetBuffer(&pbDestBuffer);

CopyMemory(pbDestBuffer, pSample->Data, pSample->DataSize);
pWriter->WriteSample(inputNumber, pSample->Start, pSample->IsDiscontinuity | pSample->IsSyncPoint, pBuffer);
pBuffer->Release();

if (inputNumber == m_pPinAudio->InputNumber)
{
{
CAutoLock lock(m_pLock);
bMustSwitch = m_bSwitchRequested;
if (m_bSwitchRequested)
m_bSwitchRequested = FALSE;
}
if (bMustSwitch)
{
pWriter->EndWriting();
pWriter->SetOutputFilename(m_szCurrentFile);
pWriter->BeginWriting();
}
}
delete pSample;
}
}




Итак, нам удалось добиться результата! Дергая метод StreamToFile в произвольные моменты времени, мы получаем новые файлы, причем не теряя ни единого кадра.

Склеиваем разрезанное




Ну что же, мы получили кучу файликов по две минуты. А что же делать, если нам нужно видео длиной 4 минуты, а самое интересное место наблюдается аккурат в момент переключения с одного файла на другой? Не беда — мы можем очень просто склеить эти файлы в один, причем сделать это без перекодирования! При этом склейка будет действительно бесшовной, так как при записи не было потеряно ни ни одного кадра.

Для этого используем IWMSyncReader и IWMWriterAdvanced.



IWMWriter *pWriter = NULL;
IWMWriterAdvanced *pWriterA = NULL;
WMCreateWriter(NULL, &pWriter);
pWriter->QueryInterface(IID_IWMWriterAdvanced, (void**)&pWriterA;
IWMSyncReader *pReader = NULL;
WMCreateSyncReader(NULL, 0, &pReader);

for (element = m_oMergeFileList.begin(); element < m_oMergeFileList.end(); element ++)
{
pReader->Open(element->FileName);
IWMProfile *pProfile = NULL;
pReader->QueryInterface(IID_IWMProfile, (void**)&pProfile);

// устанавливаем признак того, что мы не хотим декомпрессировать данные, а будем просто читать пакеты "как есть"
for (WORD i = 0; i < dwStreamCount; i++)
{
pProfile->GetStream(i, &pStream);
pStream->GetStreamNumber(&wStreamNumber);
pReader->SetReadStreamSamples(wStreamNumber, TRUE);
}

HRESULT hr = S_OK;
while (SUCCEEDED(hr))
{
hr = pReader->GetNextSample(0, &pSample, &cnsSampleTime, &cnsDuration, &dwFlags, &dwOutputNum, &wStreamNum);
pWriterA->WriteStreamSample(wStreamNum, qwSampleTimeToWrite, 0, cnsDuration, dwFlags, pSample);
}
}




В итоге очень быстро (миллисекунды!) получаем файл длиной 4 минуты, место склейки в котором обнаружить невозможно. Строго говоря, это не совсем так, и при редких и определенных условиях склейка все же не получается настолько идеальной (например, при очень низкой частоте кадров, настроенной на камере). Однако в нормальных условиях при просмотре это место действительно незаметно.

Хочу еще резать и клеить!




На этом я не остановился и решил пойти дальше. Была добавлена волшебная кнопка, которая позволяет мгновенно получить файл с видео за последнюю минуту (на самом деле, желаемая длительность настраивается). Функция оказалась очень востребована тестерами — увидел неожиданную ошибку, ткнул на кнопку, получил видео.

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



При нажатии на волшебную кнопку снова происходит переключение на новый файл, чтобы Файл 2 стал доступен. Но теперь нам нужно еще разрезать Файл 1, а потом склеить его с Файлом 2.


Здесь тоже ничего сложного. Про склеивание я уже писал, а разрезание производится аналогично: читаются сжатые пакеты без декомпрессии и пропускаются все ненужные, а начиная с некоторого момента времени все читаемые пакеты пишутся в файл с корректировкой временных меток, так чтобы первый пакет имел 00:00:00. Тут необходимо также правильно выбрать момент разрезания, чтобы первый пакет нового файла содержал опорный кадр (ключевой или I-кадр), а не предсказанный P-кадр (дельта-кадр). Опорные кадры могут размещаться в WMV-файлах даже раз в полминуты при небольших изменениях картинки, поэтому пришлось сконфигурировать использование форсированных опорных кадров. Я выбрал максимальную длительность между двумя опорными кадрами 1 с как компромисс между точностью позиционирования при нарезке и размером файла.



IWMVideoMediaProps *pVMProps = NULL;
pStreamConfig->QueryInterface(IID_IWMVideoMediaProps, (void**)&pVMProps);
pVMProps->SetMaxKeyFrameSpacing(10000000i64);




Ура! Волшебная кнопка работает!

Заключение




В качестве заключения хочу сказать, что разбирался во всех тонкостях DirectShow самостоятельно — MSDN, примеры кода в интернетах и метод проб и ошибок. Но именно это и сделало результат таким приятным — приложение активно используется для записи видео, причем одновременно с нескольких камер. При обнаружении ошибок тестеры радостно тыкают на волшебную кнопку и спустя 2 секунды получают готовые и синхронизированные видео-файлы с трех камер без необходимости какого-либо видеомонтажа вообще! А ни что так не радует разработчика, как благодарности от пользователей, не так ли?:)

This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers.


Комментариев нет:

Отправить комментарий