Бесшовное разбиение и склейка видео с помощью 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 секунды получают готовые и синхронизированные видео-файлы с трех камер без необходимости какого-либо видеомонтажа вообще! А ни что так не радует разработчика, как благодарности от пользователей, не так ли?:)

ссылка на оригинал статьи http://habrahabr.ru/post/208788/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *