|
|
Примитивный парсер .avi-файлаАвтор: Adrax
С надавних пор увлекли меня форматы медиафайлов. Интересно стало: как же внутри устроен тот же .avi, .vob или .mp3
И вот, странное дело: вроде бы, и форматы открытые, некоммерческие (почти все), и компонентов для работы с ними на современных языках программирования - хоть задницей ешь (для той же студии - и стандартный MediaPlayer, и Managed DirectX с кучей возможностей), и сорцов готовых в Сети немало. Только в сорцах тех нету совсем нормальных комментариев, а документации по форматам не то, что на русском - на английском днём с огнём не сыщешь! Вот и приходится по Сети рыскать, в чужом коде ковыряться (отдельное спасибо авторам VirtualDub - такая мощная прога, да ещё и Open Source!), MSDN взатяг курить - ради жалких крох информации. Но во время таких суматошных поисков какая-то ясность в голове наступает. И пока эта ясность не рассеялась - я пишу эту статью:)
Давайте-ка смастерим небольшой парсер .avi-файла - пусть он показывает нам содержимое его заголовков и визуализирует структуры данных
Начальные понятия о формате .avi вы можете почерпнуть из этой статьи. Лично я пользовался именно ей:) плюс распотрошил hex-редактором все имеющиеся у меня .avi-шники
В данный момент я экспериментирую, создавая парсеры на C# и Ассемблере. Перед вами самый первый рабочий прототип: он парсит только .avi-файлы "старого" формата и не справляется с вложенными LIST'ами, так что практической ценности не имеет. Тем не менее, он может оказаться полезным для тех, кто делает свои первые шаги в этой области - поэтому я приведу его полный исходный код в этой статье
GUI
Запускаем студию, командуем создать New Visual C# Project, уточняем - Windows Application. Кидаем на форму следующие компоненты: Menu, TreeView, OpenFileDialog
Форму я назвал "Форма" и указал её размер 392*88. В заголовке написал AVI_analyzer v.0.0.0.1 :)
TreeView я обозвал "Дерево" (а что? похож!) и свойство Visible установил в False
OpenFileDialog получил имя "Open", а его свойство Filter я установил "Файл AVI|*.avi|Все файлы|*.*"
Menu получило имя "Меню" и следующие пункты: Item1 (само меню, с текстом "Файл"), Item2 (с текстом "Открыть"; свойство Shortcut я установил CtrlO, чтобы этот пункт меню срабатывал по нажатию горячих клавиш Ctrl+O) и Item3 ("Выход", CtrlX)
Код
Чтобы не отвлекаться на мелочи, давайте сразу оформим обработчик мышиного клика по Item3 - по образцу из прошлой статьи:
private void Item3_Click(object sender, System.EventArgs e)
{
if (MessageBox.Show("Хотите выйти из программы?",
"Завершение",
MessageBoxButtons.YesNo)==DialogResult.Yes)
Application.Exit();
} |
Теперь поразмыслим - как же мы будем парсить .avi-файл? Для начала давайте вспомним его структуру. Типичный .avi-файл схематично можно представить так: 
Выше приведена структура RIFF "AVI "-чанка. В "старых" .avi-файлах такой чанк был единственным. В случае .avi 2-го поколения за вышеописанной структурой могут следовать вот такие чанки: 
Приведённый здесь пример парсера будет читать только самый первый RIFF "AVI "-чанк
Для добавления новых элементов (узлов) в TreeView, мы будем использовать самописный метод DobUzel(), принимающий строковые данные и создающий узел с таким текстом:
public void DobUzel(string text)
{
TreeNode node = Дерево.SelectedNode;
//получаем выделенный узел
if (node==null) {TreeNode nod = Дерево.Nodes.Add(text); Дерево.SelectedNode = nod;}
//если нет выделенного узла - мы в корне дерева,
//создаём новый узел и выделяем его
else {TreeNode nod = node.Nodes.Add(text); Дерево.SelectedNode = nod;}
//иначе добавляем подузел к выделенному узлу
//и выделяем подузел
} |
Ещё нам необходимо учесть, что данные в RIFF-файлах выровнены на границу слова (2-х байт), т.е. если мы получили нечётный размер чанка, необходимо добавить к нему 1, чтобы получилось чётное число - иначе следующую порцию данных мы считаем с неправильной позиции. Для выравнивания создадим такой метод:
public int Aligned(int a)
{
if ((a&1)!=0) a++;
return a;
} |
Для непосредственно парсинга .avi-файла я решил написать отдельный класс и незамысловато назвал его Parser (идея была подсмотрена у Giora Tamir). Объявляем класс так:
public class Parser
{
private byte[] восемьбайт = new byte[8];
private byte[] четыребайта = new byte[4];
FileStream stream;
} |
Эти два байтовых массива (восемьбайт и четыребайта) нам конкретно пригодятся. В переменной stream у нас будет поток, из которого мы будем читать данные (т.е. открытый файл). Теперь начнём заполнять наш класс методами, придавая ему работоспособность
Ксати, непередаваемое ощущение:). Вы фильм "Восставший из Ада" (Hellraiser) смотрели? Там были такие красивые кадры воскрешения: кости обрастают мясом, мясо покрывается кожей - и вот, восставший мертвец вернулся в мир живых...
При написании нового класса у меня возникает чувство, что я своими руками ввожу в этот мир новое живое существо. Класс потихоньку обрастает методами - как скелет мясом - и через некоторое время он уже держит на себе весь функционал программы. Наверное, ради этого чувства я и пишу программы... А, возможно, я просто извращенец...
Все методы класса будут иметь доступ к переменным класса - т.е. к массивам восемьбайт и четыребайта и к файловому потоку stream
Но сперва этот файловый поток надо будет создать. Поэтому первым методом класса Parser будет OpenFile(), получающий строку с именем файла и открывающий файд на чтение:
public FileStream OpenFile(string filename)
{
stream = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read);
return stream;
} |
Имя файла мы будем получать из диалога открытия файла
Следующие два метода будут заниматься побайтовым чтением из файла в байтовые массивы, а из байтовых массивов - в переменные. Придётся использовать указатели, и мы столкнёмся с сопротивлением со стороны компилятора, который не пожелает компилировать такой код. Что ж, придётся, во-первых, объявить эти методы небезопасными (unsafe) и разрешить компиляцию небезопасного кода (в окошке справа переключиться с Solution Explorer на Class View, щёлкнуть правой кнопкой мыши, в Properties найти Allow unsafe code blocks и выставить True). После этого мы сможем напрямую работать с памятью по указателям
Итак, эти методы:
public unsafe void ReadTwoInts(out int fourcc, out int size)
{
int прочитано = stream.Read(восемьбайт,0,8);
if (прочитано!=8)
{
MessageBox.Show("Ошибка чтения файла","Ошибка!",MessageBoxButtons.OK);
}
fixed (byte* bp = &восемьбайт[0])
{
fourcc = *((int*)bp);
size = *((int*)(bp + 4));
}
} |
public unsafe void ReadOneInt(out int fourcc)
{
int прочитано = stream.Read(четыребайта,0,4);
if (прочитано!=4)
{
MessageBox.Show("Ошибка чтения файла","Ошибка!",MessageBoxButtons.OK);
}
fixed (byte* bp = &четыребайта[0])
{
fourcc = *((int*)bp);
}
} |
Метод ReadTwoInts() придётся, видимо, прокомментировать. Этот метод будет читать из файлового потока 8 байт и возвращать их в двух переменных типа int. Используя стандартный механизм (т.е. указав тип возвращаемого результата), мы можем вернуть только одну переменную, поэтому поступим иначе - объявим метод как void, а возвращать переменные будем с помощью ключевого слова out
В теле метода мы читаем 8 байт из файлового потока в байтовый массив восемьбайт. Если 8 байт прочитать не удалось - выдаём ошибку. Если всё в порядке - создаём указатель на первый байт массива. Здесь необходимо использовать ключевое слово fixed. Дело в том, что CLR (среда исполнения .NET) в целях оптимизации может располагать элементы массива или поля структуры в произвольном порядке. Нам же необходимо, чтобы элементы массива располагались один за другим - потому и используем fixed
Ну а дальше всё просто: считываем FourCC (сигнатуру), передвигаем указатель на 4 байта и считываем поле размера
Метод ReadOneInt() отличается от ReadTwoInts() тем, что считывает не 8, а 4 байта
Мы получаем FourCC чанка в виде числа, но нам необходимо его строковое представление - для этой цели служит следующий метод:
public string FourCC(int fourcc)
{
char[] chars = new char[4];
chars[0] = (char)(fourcc & 0xFF);
chars[1] = (char)((fourcc >> 8) & 0xFF);
chars[2] = (char)((fourcc >> 16) & 0xFF);
chars[3] = (char)((fourcc >> 24) & 0xFF);
return new string(chars);
} |
Логика проста: создаём массив из 4-х char'ов. Далее побайтово делаем для FourCC AND с FFh и заносим в массив
Ещё нам понадобится метод для того, чтобы смещать позицию чтения внутри файла. Я написал для этого метод SkipData(), юзающий стандартный метод Seek(), принимающий два параметра: количество байт, на которое нужно сместиться, и стартовую позицию. Стартовую позицию задаём SeekOrigin.Current (т.е. текущая) и будем передавать методу SkipData() только количество байт:
public void SkipData(int сколько_байт)
{
stream.Seek(сколько_байт,SeekOrigin.Current);
} |
Метод для закрытия файла совсем простенький:
public void CloseFile()
{
stream.Close();
} |
Ну, и ещё один метод, проверяющий сигнатуры "RIFF" и "AVI ":
public int ReadRiff()
{
int fourcc, size, filetype;
ReadTwoInts(out fourcc, out size);
ReadOneInt(out filetype);
if ((FourCC(fourcc)!="RIFF")||(FourCC(filetype)!="AVI "))
{
MessageBox.Show("Не AVI","Неверный файл",MessageBoxButtons.OK);
CloseFile();
return 0;
}
return size;
} |
Его логика предельно проста. Объявляем три переменных типа int, считываем в них сигнатуру "RIFF", размер RIFF-чанка и сигнатуру "AVI ". Если не "RIFF", и не "AVI " - объявляем об ошибке, иначе - возвращаем размер RIFF-чанка
Итак, все самописные методы перед вами. Осталось лишь грамотно использовать их - и распарсить-таки этот проклятый .avi!
Вот он: обработчик мышиного клика по Item2:
private void Item2_Click(object sender, System.EventArgs e)
{
string name = "AVI_analyzer v.0.0.0.1";
int riffavisz,type,elfourcc,elsize;
Parser x = new Parser();
//создаём объект x - экземпляр класса Parser
Дерево.Nodes.Clear();
this.Text = name;
//очистили Дерево, дали Форме заголовок
if (Open.ShowDialog() == DialogResult.OK)
{
string filename = Open.FileName;
//срисовали имя файла из диалога открытия
FileInfo fi = new FileInfo(filename);
long filesize = fi.Length;
this.Text += " - " + fi.Name;
//добавили имя файла в заголовок окна
FileStream stream = x.OpenFile(filename);
//открываем файл для чтения
riffavisz = x.ReadRiff();
//получаем размер RIFF "AVI "-чанка
if (riffavisz==0)
{
x.CloseFile();
this.Text = name;
}
/*
если облажались - закрываем файл
и приводим заголовок к
изначальному виду
*/
else
{
this.Height = 685;
Дерево.Visible = true;
/*
а в этом я усмотрел красоту:)
окно резко растягивается в высоту,
и в его нижней части становится видимым
TreeView
*/
DobUzel("RIFF"); DobUzel("AVI ");
int bytesleft=riffavisz-12;
/*
в переменной bytesleft храним
количество байт, оставшееся
до конца RIFF "AVI "-чанка.
Будет для нас главным счётчиком:
стала равной нулю - заканчиваем
*/
int size,fourcc;
m1:
x.ReadTwoInts(out fourcc, out size);
//считали FourCC и поле размера
bytesleft-=8;
if (bytesleft<0) goto Out;
else
{
if (x.FourCC(fourcc)=="LIST")
//LIST-чанк
{
x.ReadOneInt(out type);
//считали сигнатуру
bytesleft-=4; size-=4;
DobUzel("LIST");
DobUzel(x.FourCC(type));
//добавили узлы
if (bytesleft<0) goto Out;
else
{
m2:
//парсим LIST
x.ReadTwoInts(out elfourcc, out elsize);
/*
считали сигнатуру и размер
вложенного в LIST чанка
*/
bytesleft-=8; size-=8;
DobUzel(x.FourCC(elfourcc));
//добавили узел
x.SkipData(Aligned(elsize));
//переместили позицию чтения
bytesleft-=Aligned(elsize); size-=Aligned(elsize);
/*
уменьшаем наш счётчик bytesleft на выровненный размер
прочитанного чанка;
на эту же величину уменьшаем переменную size,
в которую записан размер LIST'а.
Когда size станет равным 0 - LIST закончился
*/
if (size>0)
//заносим в Дерево элемент LIST'а
{
Дерево.SelectedNode = Дерево.SelectedNode.Parent;
goto m2;
}
else
//LIST закончился, поднимаемся по Дереву выше
{
Дерево.SelectedNode = Дерево.SelectedNode.Parent;
Дерево.SelectedNode = Дерево.SelectedNode.Parent;
Дерево.SelectedNode = Дерево.SelectedNode.Parent;
goto m1;
}
}
}
else
//не-LIST-чанк
{
DobUzel(x.FourCC(fourcc));
//добавили узел
x.SkipData(Aligned(size));
//переместили позицию чтения
bytesleft-=Aligned(size);
//уменьшили счётчик
if (bytesleft<0) goto Out;
else
{
Дерево.SelectedNode = Дерево.SelectedNode.Parent;
//поднялись на уровень выше
goto m1;
//повторно читаем 8 байт
}
}
}
}
Out:MessageBox.Show("Анализ RIFF AVI окончен");
}
}
|
Внушает? То-то же! А ведь это - самый примитивный вариант, который даже стандартный C:\Windows\clock.avi неправильно анализирует (потому что у того есть один нестандартный чанк после RIFF "AVI "-шного)  Окно программы после запуска
 Окно программы после открытия файла
Напомню, что лажового в этом коде: парсится только первый RIFF "AVI "-чанк, вложенные LIST'ы не парсятся. В принципе, доработать этот код не так уж и сложно. Дам подсказку: неплохо бы написать отдельный метод для парсинга LIST'ов с учётом вложенности. Думаю, в одной из будущих статей я выложу доработанный вариант, ну а пока - жду ваших писем, вопросов, предложений
С наилучшими пожеланиями, Adrax
|