Специфика систем состоит в том, что ошибки могут возникнуть спонтанно и неожиданно, на любом этапе тесткейса, а то и вообще просто в режиме ожидания, когда запись видео не ведется. Интересующихся приглашаю под кат, где я опишу разработанное мной решение для бесшовного разбиения и склеивания видео. Благодаря нему запись ведется весь день, и видео сохраняется в удобные файлы небольшого размера, что позволяет нам отлавливать и документировать редчайшие ошибки, а заодно и радовать разработчиков десятками видео с невозможными реакциями системы.
Зачем разбивать видео?
В самом деле — зачем? Сохраняли бы себе восьмичасовые файлики, а потом в каком-нибудь видеоредакторе нарезали бы кусочки нужного видео, и дело с концом. На самом деле все так и происходило раньше, но тут много недостатков: копаться в длинных файлах неудобно, после вырезания так или иначе приходится перекомпрессировать видео с потерей времени и качества, да и место на дисках не резиновое все же. Кроме того, рано или поздно приходится останавливать запись для возможности работы с файлами, и вот тут-то по закону подлости всегда происходят самые вредные и неповторяющиеся ошибки. В общем, вердикт — надо резать на этапе записи!
Что нам предлагает 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 секунды получают готовые и синхронизированные видео-файлы с трех камер без необходимости какого-либо видеомонтажа вообще! А ни что так не радует разработчика, как благодарности от пользователей, не так ли?:)
ссылка на оригинал статьи http://habrahabr.ru/post/208788/
Добавить комментарий