Разработка конвертера видео из 264 в avi для видеорегистратора QCM-08DL

в 13:14, , рубрики: adpcm, avi, dvr, h264, winhex, звук, Работа с видео, реверс-инжиниринг

На самом деле, статья посвящена разработке программы для перепаковки видео DVR из одного контейнера в другой, если это можно назвать конвертацией. Хотя, я всю жизнь считал, что конвертер занимается преобразованием (перекодировкой) формата видео. Данная статья является второй частью моей прошлой публикации, где я в подробностях рассказал про осуществление доступа ко всем видеозаписям видеорегистратора. Но в самом начале публикации я ставил ещё одну задачу: изучить алгоритм, по которому работает штатная программа-перепаковщик 264-avi и создать такую же программу, которая выполняла бы те же операции, но уже не над одним, а над целой группой файлов, причём «одним нажатием».

Поясню ещё раз суть всех вещей простым языком.

Пользователь имеет видеорегистратор, например, популярной модели QCM-08DL. Ему нужна видеозапись за определённую дату и время. Он может её извлечь либо на флешку, либо через web-интерфейс видеорегистратора (DVR) на компьютер. Извлечённый файл с видеозаписью (расширение .264) откроется только в программе-плеере, которая прилагается с DVR. Плеер весьма неудобный. Его ещё можно открыть, в плеере VLC, поставив режим RAW H264 в настройках демультиплексирования (настройки для опытных пользователей). Но при этом нормальному воспроизведению мешают, видимо, блоки аудиопотоков, которые интерпретируются как видео, а звуковое сопровождение отсутствует. А для того, чтобы видео открыть в любом плеере, файл .264 нужно его предварительно преобразовать в какой-либо популярный формат, например, avi. Программа для преобразования также прилагается с DVR. Но она также очень неудобная. Когда речь идёт об одном или нескольких файлах, проблем нет. Однако когда ставится задача получить доступ ко всем видеозаписям на жёстком диске, а уж тем более их все преобразовать в популярный формат, штатный инструментарий практически не пригоден.

Разработка конвертера видео из 264 в avi для видеорегистратора QCM-08DL - 1

Задача о доступе ко всем файлам решена. Этому и была посвящена прошлая публикация. Приступим к решению второй задачи. Мне уже давали «дельные советы»: достаточно в имени файла переименовать расширение с «264» на «avi», и всё попрёт, мол, нечего заморачиваться. Но это самая распространённая ошибка любого рядового пользователя, который, как правило, не разбирается в соответствующих вопросах.

В прошлой публикации я уже писал кратко про структуру исходного файла .264. Напомню.

Основная информация с аудио и видео потоками берёт начало по смещению 65536 байт. Блоки видеопотока начинаются с 8-байтового заголовка «01dcH264» (встречается также «00dcH264»). Следующие за ним 4 байта описывают размер текущего блока видеопотока в байтах. Через 4 байта нулей (00 00 00 00) начинается сам блок видеопотока. Блоки аудиопотоков имеют заголовок «03wb» (хотя, по моим наблюдениям, первый символ заголовка в некоторых случаях был необязательно «0»). После – 12 байт информации, которую я пока не разгадал. А начиная с 17-ого байта – аудиопоток фиксированной длины 160 байт. Какие-либо метки в конце файла отсутствуют.

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

Каждый блок видеопотока представляет собой один кадр. Первый символ в заголовке блоков видеопотока необязательно «0». Его назначение я не разгадывал, ибо, как я выяснил, оно не является ключевым в решении поставленной задачи. Второй символ заголовка видеопотока может быть как «1», так и «0». Во втором случае содержание блока видеопотока представляет собой так называемый опорный кадр. А в первом случае содержание блока видеопотока представляет собой закодированный сжатый кадр, который зависит от опорного кадра. Размер содержимого опорного кадра значительно больше размера содержимого сжатого вспомогательного кадра. Период следования опорных кадров, скорее всего, зависит от настроек степени сжатия в DVR. Но в моём случае период следования составил 1 кадр/сек.

Разработка конвертера видео из 264 в avi для видеорегистратора QCM-08DL - 2

Штатная программа для перепаковки видео из контейнера «264» в контейнер «avi» давала разные результаты по поводу частоты кадров. В случае с видео, которые были записаны в режиме высокого разрешения (704*576) частота кадров составила 20 кадров/сек. А в случае с низким разрешением (352*288) – 25 кадров/сек. Эту информацию выдаёт утилита «MediaInfo» Она же выдаёт то, что размер видео при любом случае единый: 720*576, причём размер видеопотока (эта же утилита сообщает) — 704*576 или 352*288. Большинство плееров разворачиваются именно под размер видеопотока. Однако встречался мне плеер, который некорректно отображал полуэкранный режим при воспроизведении файла 352*288. Я хотел исправить этот незначительный недостаток штатного перепаковщика, заглянув в байты содержания видеопотока и вытащив оттуда информацию о размере кадра. Но на скорую руку этого мне сделать не удалось. Вышесказанное продемонстрировано на рисунке ниже.

Разработка конвертера видео из 264 в avi для видеорегистратора QCM-08DL - 3

Теперь по поводу частоты кадров. Как я выяснил, штатный перепаковщик не обращается ни к какому полю заголовка контейнера «264». Он судит о частоте кадров путём подсчёта соотношения количества блоков видео и аудиопотоков. И это значение при расчёте даже не округляется до целого значения, что видно из рисунка выше (обведено зелёным цветом). Как я выяснил, число блоков аудиопотока на единицу времени всегда и везде (в любом файле) фиксированное, а именно 25 блоков в секунду. Если исследовать файл видео с частотой 20 кадров/сек., то опорный кадр (блок) встречается через каждые 19 сжатых кадров, а для случая 25 кадров/сек. – через каждые 24 сжатых кадра.

Продолжим изучать структуру заголовка видеопотока. С первыми восьми байтами мы разобрались: это метка опорного или сжатого кадра плюс ключевое слово «H264». Следующие четыре байта, описывают, как я выяснил, не точный, а приблизительный размер содержимого видеопотока. А штатный перепаковщик перебрасывает полностью все байты этого содержимого, а затем получившийся размер записывает в соответствующие поля avi-контейнера. И это значение отличается от значения, указанного в соответствующем поле исходного .264 файла.

Двенадцать байт информации после заголовка блока аудиопотока я разгадал частично. Во всяком случае, ключевыми элементами являются 4 последних байта, после которых начинается аудиопоток. Это два 16-разрядных числа, которые описывают начальные параметры итерационной схемы декодирования из ADPCM в PCM. Декодирование увеличивает размер аудиопотока в 4 раза. Я ещё заранее при детальном исследовании файлов выяснил, что штатный перепаковщик декодирует аудио, но содержимое видео оставляет без изменений.

Не имея глубоких знаний, я долго пытался разгадывать, какой именно алгоритм декодирования применён в моём случае. Интуитивно уже догадывался, что применён метод сжатия ADPCM. Точнее, не интуитивно, а с грамотным подходом, опираясь на тот факт, что аудиопоток сжат ровно в 4 раза. А при открытии фрагмента в Adobe Audition как RAW в различных форматах (и сравнивая такой же фрагмент после перепаковки штатной программой), очень похожий (но не точный) по звучанию результат мне выдал ADPCM. Для разбора алгоритма сжатия мне помогла информация на сайте wiki.multimedia.cx/index.php/IMA_ADPCM. Здесь я узнал о двух начальных параметрах декодирования, а потом «методом тыка» догадался, что эти начальные параметры записаны в 4-х байтах перед началом аудиопотока. Опишу работу алгоритма и приведу грубую математическую интерпретацию (под спойлером).

Подробности алгоритма декодирования ADPCM
Имеется последовательность сэмплов $x_0, x_1, x_2, dots. $ Кроме того, как уже говорилось, имеются два начальных параметра $y_0$ и $s_0$. Требуется получить новую последовательность сэмплов $y_0, y_1, y_2dots.$. Как уже можно догадаться, первый по счёту выходной сэмпл уже известен: он совпадает с одним из начальных параметров $y_0$. Это есть нечто иное, как «начальное смещение». Стоит отметить, что входные (исходные) сэмплы закодированы четырьмя битами. Для знаковых типов под кодировку попадают целые числа от -8 до 7 включительно. Старший бит, по сути, отвечает за знак числа. Выходные PCM сэмплы, которые получаются после декодирования, имеют знаковый 16-битный стандартный формат.

Анализируя код алгоритма на Си, можно увидеть две таблицы. Они приведены ниже.

int ima_index_table[] =
{
	-1, -1, -1, -1, 2, 4, 6, 8
};
int ima_step_table[] =
{
		7,     8,     9,    10,    11,    12,    13,    14,
	   16,    17,    19,    21,    23,    25,    28,    31,
	   34,    37,    41,    45,    50,    55,    60,    66,
	   73,    80,    88,    97,   107,   118,   130,   143,
	  157,   173,   190,   209,   230,   253,   279,   307,
	  337,   371,   408,   449,   494,   544,   598,   658,
	  724,   796,   876,   963,  1060,  1166,  1282,  1411,
	 1552,  1707,  1878,  2066,  2272,  2499,  2749,  3024,
	 3327,  3660,  4026,  4428,  4871,  5358,  5894,  6484,
	 7132,  7845,  8630,  9493, 10442, 11487, 12635, 13899,
	15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794,
	32767
};

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

Объявляем необходимые, в том числе и вспомогательные переменные.

int current1;
int step;
int stepindex1;
int diff;
int current;
int stepindex;
int value; //Значение входного сэмпла;

Перед началом итерации нужно присвоить переменной current начальный параметр $y_0$, а переменной stepindex — $s_0$. Это делается за пределами рассматриваемого алгоритма, поэтому я не отражаю это кодом. Далее следуют преобразования, которые выполняются по кругу (в цикле).

        value = read(input_sample); //Псевдокод считывания входного сэмпла;
        current1 = current;
	stepindex1 = stepindex;
	step = ima_step_table[stepindex1];

	diff = step>>3;
	if(value & 1){
		diff += step >> 2;
	}
	if(value & 2){
		diff += step >> 1;
	}
	if(value & 4){
		diff += step;
	}

	if(value & 8){
		current1 -= diff;
		if(current1 < -32768){ //Лимит, если "зашкалит";
			current1 = -32768;
		}
	}else{
		current1 += diff;
		if(current1 > 32767){ //Лимит, если "зашкалит";
			current1 = 32767;
		}
	}
        //На этом этапе известно значение выходного сэмпла: это переменная current1;

	stepindex1 += ima_index_table[value & 7];
	if(stepindex1 < 0){ //Тоже "лимит";
		stepindex1 = 0;
	}
	if(stepindex1 > 88){ //Тоже "лимит";
		stepindex1 = 88;
	}

        output_sample = curent1; //Псевдокод вывода выходного сэмпла;
	current = current1;
	stepindex = stepindex1;

Во вспомогательную переменную step из массива ima_step_table записывается значение по индексу stepindex1. Для первой итерации это начальный параметр $s_0$, для дальнейших итераций это пересчитанный параметр $s_i$. Затем значение из этого массива делится на 8 (видимо, нацело) операцией битового сдвига вправо, и результатом этого деления инициализируется переменная diff. Затем происходит анализ трёх младших битов значения входного сэмпла и, в зависимости от их состояний, переменная diff может быть скорректирована тремя слагаемыми. Слагаемые представляют собой аналогичное целочисленное деление значения diff на 4 (>>2), на 2 (>>1) или diff без изменений (пусть это будет деление на 1 для обобщения). Затем анализируется старший (знаковый) бит значения входного сэмпла. В зависимости от его состояния к переменной current1 прибавляется или вычитается переменная diff, которая была сформирована перед этим. Это и будет значение выходного сэмпла. Для корректности значения ограничиваются сверху и снизу. Затем stepindex1 корректируется путём прибавления значения из массива ima_index_table по индексу значения входного сэмпла с обнулённым знаковым битом. Значения stepindex1 также подвергаются лимиту. В самом конце перед повторением этого алгоритма значениям current и stepindex присваиваются только что пересчитанные значения current1 и stepindex1, и алгоритм повторяется заново.

Можно попробовать разобраться, чтобы приблизительно понять, как формируется переменная diff. Пусть $f_i=f(s_i)$. Это значения переменной step на каждом i-ом шаге итерации, как значения функции (массива) аргумента $s_i$, где $i=0, 1, 2, dots$. Для удобства обозначим сишную переменную diff как $d$. Следуя логике рассуждений, описанных выше, имеем:

$d_i=frac{f_i}{8} + x^{(0)}_ifrac{f_i}{4}+x^{(1)}_ifrac{f_i}{2}+x^{(2)}_if_i,$

где $x_i^{(0)}, x_i^{(1)}, x_i^{(2)}$ — младшие 3 бита числа $x_i$. Приводя к общему знаменателю, преобразуем это выражение к более удобному виду:

$d_i=frac{f_i}8Bigg(1+2x_i^{(0)}+4x_i^{(1)}+8x_i^{(2)}Bigg)=$

$=frac{f_i}8Bigg(1+2Big(x_i^{(0)}+2x_i^{(1)}+4x_i^{(2)}Big)Bigg)=frac{f_i}{8}(2x_i+1).$

Последнее преобразование основано на том, что, в неком смысле, младшие три бита (0 или 1) числа $x_i$ с представленными коэффициентами есть нечто иное, как запись абсолютного значения этого числа, а старший бит числа $x_i$ будет соответствовать знаку всего выражения. Далее по формуле

$y_{i+1}=y_i+d_i$

вычисляется новое значение сэмпла на основе старого. Кроме того, вычисляется новое значение переменной $s$:

$s_{i+1}=s_i+t(|x_i|).$

Модуль в формуле указывает на то, что переменная $x_i$ попадает в функцию $t$ без учёта старшего знакового бита, что и отражено в коде. А функция $t$ — это значение массива ima_index_table с индексом, соответствующий аргументу.

В описании формул я пренебрёг операциями ограничения сверху и снизу. Итого, итерационная схема выглядит приблизительно так:

$y_0; s_0;  x_0, x_1, x_2,dots ;$

$d_i=frac{f(s_i)}{8}big(2x_i+1big) ;$

$y_{i+1}=y_i+d_i ;$

$s_{i+1}=s_i+t(|x_i|) ;$

$i=0, 1, 2, dots.$

Сильно глубоко в теорию кодирования/декодирования ADPCM я не вникал. Однако, табличные значения массива ima_step_table (из 89 штук), судя по отражению их на графике (см. рис. ниже), описывают вероятностное распределение сэмплов относительно нулевой линии. На практике обычно так: чем сэмпл ближе к нулевой линии, тем он чаще встречается. Следовательно, ADPCM основан на вероятностной модели, и далеко не любой исходный набор 16-битных сэмплов PCM может быть корректно преобразован в 4-битные сэмплы ADPCM. А вообще говоря, ADPCM — это PCM с переменным шагом квантования. Как раз, видимо, данный график отражает этот самый переменный шаг. Он выбран грамотно, на основе закона распределения аудиоданных на практике.

Разработка конвертера видео из 264 в avi для видеорегистратора QCM-08DL - 35

Теперь перейдём к описанию структуры avi контейнера. На самом деле, он представляет собой сложную иерархическую структуру.

Разработка конвертера видео из 264 в avi для видеорегистратора QCM-08DL - 36

Но, упростив задачу для частного случая, я представил структуру avi в линейном виде. Итого получилось так: файл avi состоит из большого заголовка, нулевых байтов пропуска (JUNK), области потоков аудио и видео (с их заголовками и размерами содержимого), и списка индексов. Последний служит, в частности, для прокрутки видео в плеере. Без этого списка прокрутка не будет работать (проверял). Он представляет собой только оглавление, где перечислены ключевые названия блоков потока (совпадающими с названиями в заголовках блока), соответствующие размеры содержимого и значения смещений (адреса) относительно начала области потоков.

Теперь можно перейти к разработке программы. Конкретное описание задачи следующее.

В корне раздела X: имеется каталог «DVR». В данном каталоге содержится множество непустых подкаталогов (и только подкаталогов) с именами, которые соответствуют неким датам. В каждом из таких подкаталогов имеется множество файлов с различными именами и расширением «264». Требуется в разделе Y: создать каталог «DVR», а в нём те же самые подкаталоги, как и в разделе X:. Каждый из таких подкаталогов заполнить файлами с теми же соответствующими именами, но с расширениями не «264», а «avi». Данные avi-файлы нужно получить из исходных 264-файлов путём их обработки, которая, так или иначе, повторяет алгоритм уже имеющийся программы. Обработка заключается в прямой перепаковке потоков видео, перепаковке с декодированием потоков аудио, форматировании файла avi. Программа должна запускаться из командной строки следующим образом: «264toavi.exe X: Y:», где «264toavi.exe» — имя программы, «X:» — исходный раздел, «Y:» — раздел назначения.

На самом деле, для упрощения задачи, можно было написать программу, которая занималась бы только преобразованием (перепаковкой) одного файла, сделав дня неё два аргумента: имя входного файла и имя выходного файла. А затем, чтобы реализовать именно групповую перепаковку, можно написать командный батник (bat), используя другие инструменты, например, Excel. Но мной была реализована полноценная программа, весьма громоздкая. Вряд ли исходный код заслужил бы внимания у читателей. Опишу структуру программного кода.

Программа написана на языке Си в среде разработки «Dev-C++» с элементами WinAPI. В программе реализованы три большие вспомогательные функции: функция формирования первоначального заголовка avi, функция декодирования сэмпла аудио и функция сканирования исходного файла «264» по словам. Словами я называю порцию из 4-х байт. Было замечено, что размеры заголовков и содержимого всех потоков кратны четырём байтам. Функция сканирования может возвращать пять значений: 0 – если это обычные 4 байта видеопотока для перепаковки, 1 – если это заголовок блока видеопотока опорного кадра, 2 – если это заголовок блока видеопотока сжатого кадра, 3 – если это заголовок блока аудиопотока, 4 – если это «испорченный» блок, который нужно игнорировать при перепаковке. Очень-очень редко, но такое встретилось. Испорченный блок (как я его назвал) представляет собой заголовок вида «H264», где «» — нулевой байт. Блоки такого вида штатный перепаковщик игнорирует. Разумеется, содержимое такого блока может оказаться вполне рабочим, но я подобные блоки игнорирую для максимального приближения своей программы к штатной.

В основной функции, кроме организации каталогов, происходит считывание входного файла функцией сканирования. В зависимости от того, что возвратила эта функция, происходят дальнейшие действия. Если это заголовки видеопотоков, то формируются в выходной avi файл соответствующие заголовки. Там они именуются по-другому: «00db» — это заголовок блока видеопотока опорного кадра, а «00dc» — для сжатого кадра. После операции перепаковки (переписывания слов) перед новым вновь встретившимся заголовком происходит расчёт размера перепакованного содержимого и запись этого значения в поле, которое следует сразу за заголовком только что обработанного потока. Если при сканировании встретился заголовок аудиопотока, то формируется в выходной avi файл имя заголовка «03wb» и тут же в цикле происходит декодирование аудиопотока из ADPCM в PCM одновременно с записью декодированного содержимого в avi файл. Одновременно со всем вышесказанным происходит фиксирование краткой информации (оглавления) во временный файл индексов «index». Функцию сканирования можно было не делать, а всё писать в основной функции. Но тогда программа бы получилась очень громоздкой и практически сложно читаемой.

В конце всей операции, когда кончился входной файл «264», прежде чем перейти на новый файл, программа грамотно завершает все операции. Сначала корректируются определённые поля в заголовке avi файла, значения которых зависят от размеров и количества прочитанных потоков, а затем к почти готовому файлу avi присоединяется содержимое временного файла «index», который затем удаляется. После этих операций выходной avi файл готов к воспроизведению.

Во время работы программы в командной строке происходит текстовая визуализация, которая отображает текущий каталог, файл, а также номер блока видеопотока, приходящийся на опорный кадр и соответствующий момент времени видео в минутах и секундах. А если входной файл имеет не произвольное имя, а исходное (содержащее номер канала, дату и время начала записи), то происходит более интерактивная визуализация, основанная на арифметике даты-времени.

При тестировании и отлаживании программы основные проблемы у меня возникали при работе с декодированием звука. Простая арифметика работала некорректно, если я при объявлении переменных в функции декодирования неграмотно расставлял типы. Из-за этого, некоторые блоки аудиопотоков были битыми, а на слух наблюдались щелчки. Несколько не совсем понятных полей заголовка исходного файла 264, которые я не смог разгадать, оказались нечувствительными к результату. В отличие от штатной программы моя программа не выкидывает из операции перепаковки последний незавершённый блок потока. Хотя, его отсутствие никакой практической роли не сыграет. Ещё штатная программа, в отличие от моей, оставляет за собой «мусор» в небольшом количестве (это содержимое последнего потока) в самом конце avi файла после индексов. При всём при этом видео воспроизводится практически одинаково. А перепаковку программа осуществляет за тот же промежуток времени, что и штатная программа.

В заключение я приведу иллюстрации, демонстрирующие структуру организации потоков в файле .264 (в шестнадцатеричном редакторе WinHex) на примере одного из файлов и вид программы RiffPad с открытым в нём перепакованным файлом avi. Данная программа мне очень упростила процесс изучения структуры avi файла. Она наглядно демонстрирует иерархическую структуру, показывает байтовое содержимое каждого члена структуры и даже умно интерпретирует содержимое заголовков в виде списка параметров. На картинке, в частности, продемонстрирован тот факт, что содержимое видеопотока переписывается без изменений.

Разработка конвертера видео из 264 в avi для видеорегистратора QCM-08DL - 37

Разработка конвертера видео из 264 в avi для видеорегистратора QCM-08DL - 38

Автор: R3EQ

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js