Delphi. Определение разрешения видеофайла формата MP4 прямым парсингом без использования кодеков.

  Решение данной задачи потребовалось для автоматизированной пакетной обработки файлов формата MP4. Не указал сначала версии Delphi и Windows, исправляюсь. Обе семерки, Delphi 7 и Windows 7.
  Попытки решить задачу «в лоб», т.е. поиском в инете подходящего кода, у меня к положительному результату не привели. Наиболее адекватный код советовал грузить консольный декодек FFMPEG и у него запрашивать требуемое разрешение видеофайла. Это мне не понравилось — загрузи сторонний кодек, перенаправь ввод/вывод консоли, запроси требуемое, распарси вывод консоли для получения требуемого. Как то сложновато выглядит простое получение разрешения видеофайла MP4.
  Я был уверен что получить разрешение видеофайла MP4 можно гораздо быстрее и проще, надо только знать как. Пришлось полдня гуглить с промежуточными экспериментами и я нашел способ. Не знаю насколько он универсален, возможно на некоторых файлах MP4 работать не будет, у меня сработал на всех.

  Файлы MP4 используют контейнер «QuickTime», данные в котором организованы в древовидном порядке. Начало блока данных предваряет так называемый 8-байтный «Атом», представляющий собой 4 байта длины атома и 4 байта зарезервированного имени. Файл всегда начинается с атома «ftyp», прочитав его длину можно перейти на следующий и таким образом пройти по «Top Level» атомам файла. Что-бы вы имели представление, приведу примерный состав атомов видеофайла MP4:



  Из отрывочных сведений инета (в основном разбор парсинга MP4 файлов на GitHub) и собственных экспериментов и получился этот код. Выяснил, что найти разрешение MP4 изображения можно пройдя последовательно по атомам «moov» -> «trak» -> «tkhd», последний и содержит требуемое. Был написан наследник потока памяти, код получился очень простым:

type
  TMp4Stream = class(TMemoryStream)
  private
    function FindAtom(var aSize: DWORD): DWORD;
    function GetResolution(var aWidth, aHeight: WORD; aSize: DWORD): Boolean;
  public
    constructor Create(const aFileName: string);
    function ParceMP4(var aWidth, aHeight: WORD): Boolean;
  end;

Файл MP4 грузится в поток при его создании (потока) штатной процедурой LoadFromFile:

constructor TMp4Stream.Create(const aFileName: string);
begin
  inherited Create;
  LoadFromFile(aFileName);
end;

  Функция парсинга «ParceMP4» с помощью функции «FindAtom» сначала пробегает по «Top Level» атомам файла в поисках атома «moov». Как только он найден, она заходит в этот атом и продолжает искать атом «trak». Когда и этот атом найден, функция заходит в него и продолжает искать атом «tkhd». Если и этот атом найден, вызывается функция «GetResolution» которая и ищет разрешение видеофайла. Если находит, возвращает True и парсинг прерывается. Если не находит, парсинг продолжается до следующего атома «trak», как правило их не менее двух (так понимаю, видео и аудио, причем один на месте разрешения содержит нули).

function TMp4Stream.ParceMP4(var aWidth, aHeight: WORD): Boolean;
const
  moov = $766F6F6D; // top level container - atom 'moov' - movie header
  trak = $6B617274; // contained in moov - atom 'trak' - track header
  tkhd = $64686B74; // contained in trak - atom 'tkhd' - leaf atom
var
  iSize, iAtom, iPos: DWORD;
begin
  iPos := 0;
  aWidth := 0;
  aHeight := 0;
  Position := 0;
  Result := False;
  while Position < Size do begin
    iAtom := FindAtom(iSize);
    if (iAtom = moov) or (iAtom = trak) then iPos := iPos + SizeOf(Int64)
    else iPos := iPos + iSize;
    if iAtom = tkhd then begin
      Result := GetResolution(aWidth, aHeight, iSize);
      if Result then Break; // Resolution found - break
    end else Position := iPos;
  end;
end;

Функция «FindAtom» просто считывает данные (размер и заголовок) атома с преобразованием формата поля размера «BigEndian -> LittleEndian»:

function TMp4Stream.FindAtom(var aSize: DWORD): DWORD;
var
  SzBL: LongRec absolute aSize;
begin
  // BigEndian -> LittleEndian
  ReadBuffer(SzBL.Bytes[3], SizeOf(Byte));
  ReadBuffer(SzBL.Bytes[2], SizeOf(Byte));
  ReadBuffer(SzBL.Bytes[1], SizeOf(Byte));
  ReadBuffer(SzBL.Bytes[0], SizeOf(Byte));
  ReadBuffer(Result, SizeOf(Result));
end;

Функция «GetResolution» переходит в конец атома «tkhd», где и хранится разрешение трека (два последних двойных слова). Разрешение считывается также с пробразованием «BigEndian -> LittleEndian», проверяется на 0:

function TMp4Stream.GetResolution(var aWidth, aHeight: WORD; aSize: DWORD): Boolean;
var
  W: WordRec absolute aWidth;
  H: WordRec absolute aHeight;
begin
  // BigEndian -> LittleEndian
  Position := Position + (aSize - SizeOf(Int64) - SizeOf(Int64));
  ReadBuffer(W.Hi, SizeOf(Byte));
  ReadBuffer(W.Lo, SizeOf(Byte));
  Position := Position + SizeOf(WORD);
  ReadBuffer(H.Hi, SizeOf(Byte));
  ReadBuffer(H.Lo, SizeOf(Byte));
  Position := Position + SizeOf(WORD);
  Result := (aWidth <> 0) and (aHeight <> 0);
end;

Настроечный вывод в лог алгоритма поиска разрешения видеофайла MP4 выглядит так:

  --- атомы верхнего уровня ----------------
  Name-ftyp, Atom=0x70797466, Size-0x20
  Name-free, Atom=0x65657266, Size-0x8
  Name-mdat, Atom=0x7461646D, Size-0x4C9A33F
  Name-moov, Atom=0x766F6F6D, Size-0x43767
  --- атомы уровня 1 -----------------------
  Name-mvhd, Atom=0x6468766D, Size-0x6C
  Name-trak, Atom=0x6B617274, Size-0x26C69
  --- атомы уровня 2 -----------------------
  Name-tkhd, Atom=0x64686B74, Size-0x5C
  --- поиск окончен ------------------------
  Result, Width-1280, Height-720
  ------------------------------------------

Использование этого класса потока очень простое, к статье приложен простенький тестовый файл (с исходниками) для определения разрешения файлов MP4. Процедура нажатия кнопки для загрузки видеофайла MP4 и определения его разрешения:

procedure TForm1.Button1Click(Sender: TObject);
var
  iStream: TMp4Stream;
  iWidth, iHeight: WORD;
begin
  if OpenDialog1.Execute then begin
    iStream := TMp4Stream.Create(OpenDialog1.FileName);
    try
      if iStream.ParceMP4(iWidth, iHeight) then
        ShowMessageFmt('Width=%d, Height=%d', [iWidth, iHeight])
      else ShowMessage('Resolution not found');
    finally
      iStream.Free;
    end;
  end;
end;

P.S. Этот код работает правильно со стандартом файлов MP4, у которых поле размера атома составляет двойное слово, а значит размер атома не может превышать 2Гб. Читал что существует расширение стандарта в котором поле размера атома увеличено до 8 байт, с ним этот код работать не будет, да и файлов таких у меня нет.

Часть вторая. Доработка кода для поддержки больших файлов.

  Первый вариант поддерживает контейнер «QuickTime» версии 0, т.е видеофайлы MP4 размером до 4Гб. Эта версия устарела, и хотя до сих пор файлы в таком формате не редкость, выяснил, что встречаются видеофайлы MP4 созданые с учетом поддержки больших файлов, т.е. с поддержкой требований контейнера «QuickTime» версии 1. На таких файлах первоначальный код ошибался, поэтому был доработан.

type
  TMp4Stream = class(TMemoryStream)
  private
    function ReadSize64: Int64;
    function ConvertWord(aSize: Word): Word;
    function ConvertLong(aSize: DWord): DWord;
    function ReadAtom(var aSize: DWord): DWord;
    function GetResolution(var aWidth, aHeight: WORD; aSize: DWord): Boolean;
  public
    constructor Create(const aFileName: string);
    function ParceMP4(var aWidth, aHeight: Word): Boolean;
  end;

функции «Convert» это просто преобразование формата слова и двойного слова «BigEndian -> LittleEndian».

function TMp4Stream.ConvertWord(aSize: Word): Word;
var
  InBuff: WordRec absolute aSize;
  OutBuff: WordRec absolute Result;
begin
  // BigEndian -> LittleEndian
  OutBuff.Bytes[1] := InBuff.Bytes[0];
  OutBuff.Bytes[0] := InBuff.Bytes[1];
end;

function TMp4Stream.ConvertLong(aSize: DWord): DWord;
var
  InBuff: LongRec absolute aSize;
  OutBuff: LongRec absolute Result;
begin
  // BigEndian -> LittleEndian
  OutBuff.Lo := ConvertWord(InBuff.Hi);
  OutBuff.Hi := ConvertWord(InBuff.Lo);
end;

Как правильно заметил VGA название функции «FindAtom» не соответствовало выполняемой операции, поэтому оно было изменено на «ReadAtom».

function TMp4Stream.ReadAtom(var aSize: DWord): DWord;
var
  InBuff: DWord;
begin
  ReadBuffer(InBuff, SizeOf(InBuff)); // Size
  aSize := ConvertLong(InBuff);       // BigEndian -> LittleEndian
  ReadBuffer(Result, SizeOf(Result)); // Signature
end;

Добавилась функция «ReadSize64», читающая 8-байтный блок размера атома контейнера «QuickTime» версии 1.

function TMp4Stream.ReadSize64: Int64;
type
  Long64Rec = packed record
    Lo, Hi: DWord;
  end;
var
  InBuff: Long64Rec;
  OutBuff: Long64Rec absolute Result;
begin
  ReadBuffer(InBuff, SizeOf(InBuff));
  // BigEndian -> LittleEndian
  OutBuff.Lo := ConvertLong(InBuff.Hi);
  OutBuff.Hi := ConvertLong(InBuff.Lo);
end;

В функции «GetResolution» изменения больше косметические.

function TMp4Stream.GetResolution(var aWidth, aHeight: WORD; aSize: DWord): Boolean;
var
  Buff: Word;
begin
  Position := Position + (aSize - SizeOf(Int64) - SizeOf(Int64));
  ReadBuffer(Buff, SizeOf(Buff));
  aWidth := ConvertWord(Buff);
  Position := Position + SizeOf(Word);
  ReadBuffer(Buff, SizeOf(Buff));
  aHeight := ConvertWord(Buff);
  Position := Position + SizeOf(Word);
  Result := (aWidth <> 0) and (aHeight <> 0);
end;

  И наконец основная функция парсинга видеофайла MP4. После считывания размер атома проверяется на 0 и на 1. Это специально зарезервированные значения формата контейнера «QuickTime» версии 1, ведь даже минимальный размер пустого атома 8 байт. Если 0, то атом продолжается до конца файла и продолжать парсинг бесполезно. Если 1, то 8-байтное поле размера будет следовать сразу за заголовком.

function TMp4Stream.ParceMP4(var aWidth, aHeight: WORD): Boolean;
const
  moov = $766F6F6D; // top level container - atom 'moov' - movie header
  trak = $6B617274; // contained in moov - atom 'trak' - track header
  tkhd = $64686B74; // contained in trak - atom 'tkhd' - leaf atom
var
  iPos, lSize: Int64;
  iSize, iAtom: DWord;
begin
  iPos := 0;
  aWidth := 0;
  aHeight := 0;
  Position := 0;
  Result := False;
  while Position < Size do begin
    iAtom := ReadAtom(iSize);
    if iSize = 0 then Break; // atom extends to the end of the file - break
    if iSize = 1 then lSize := ReadSize64
    else lSize := iSize;
    if (iAtom = moov) or (iAtom = trak) then iPos := iPos + SizeOf(Int64)
    else iPos := iPos + lSize;
    if iAtom = tkhd then begin
      Result := GetResolution(aWidth, aHeight, lSize);
      if Result then Break; // Resolution found - break
    end else Position := iPos;
  end;
end;


Часть третья. Оптимизация.

  Решил сравнить разные программные решения функций изменения эндианности, как примененные мной, так и предложенные VGA. Вкратце напомню правила написания в Delphi функций на встроенном ассемблере. Правила использования регистра в операторе asm такие же, как и у внешней процедуры или функции. Оператор asm должен сохранять регистры EDI, ESI, ESP, EBP и EBX если он их изменяет, но может свободно изменять регистры EAX, ECX и EDX. При входе в оператор asm EBP указывает на текущий кадр стека, а ESP указывает на вершину стека. Если не указан спецификатор передачи параметров, подразумевается register, т.е. первые три параметра передаются через регистры EAX, EDX и ECX, const параметры напрямую, var через указатель (также через указатель передается параметр превышающий разрядность компилятора). Если параметров больше трех, остальные передаются через стек, результат функции возвращается через EAX (EAX+EDX).
  Таким образом спецификатор register можно не указывать, но его прямое указание в ассемблерных функциях считается хорошим тоном.
  Функции изменения эндианности убраны из методов класса. Это сделано потому, что в методы класса первым передается неявный указатель на класс Self, и при применении ассемблерных функций он мешает, да и не нужен он в этих функциях.
  Все функции запускались в цикле от 0 до MaxLong (0х7FFFFFFF, 2147483647), до запуска и после отработки цикла брались метки системного времени. Измерение времени цикла на каждой функции проводилось 6 раз, замеры усреднены.
1. Обмен байтами в слове. Тестировалось четыре варианта функции, расположены в порядке убывания времени выполнения.
1.A — моя функция, чистый паскаль. По коду сразу понятно что она делает:

function SwapEndian16(Value: Word): Word;
var
  InBuff: WordRec absolute Value;
  OutBuff: WordRec absolute Result;
begin
  OutBuff.Lo := InBuff.Hi;
  OutBuff.Hi := InBuff.Lo;
end;

1.B — предложена VGA, чистый паскаль, алгоритм сдвига и замены по маске. Без комментария алгоритм сразу не понять:

function SwapEndian16(Value: Word): Word;
begin
  Result := Value;
  Result := ((Result and $FF00) shr 8) or ((Result and $00FF) shl 8);
end;

1.C — legasy функция Swap Delphi, inline assembler, обмен байтами в 16-разрядном регистре:

function SwapEndian16(Value: Word): Word; register;
asm
  XCHG  AL, AH
end;

1.D — предложена VGA, inline assembler, переворот 16-байтного регистра влево, выдвинутые биты вдвигаются в регистр справа:

function SwapEndian16(Value: Word): Word; register;
asm
  ROL   AX, 8
end;

Результаты SwapEndian16:

  1.A - 17.750 ms
  1.B - 4.846 ms
  1.C - 3.427 ms
  1.D - 3.259 ms

Метод на паскале через сдвиг и замену по маске оказался быстрее прямого обмена регистрами, и ненамного уступает ассемблерным.

2. Обмен байтами в двойном слове. Тестировалось четыре варианта функции, расположены в порядке убывания времени выполнения.
2.A — моя функция, чистый паскаль, в комментарии не нуждается:

function SwapEndian32(Value: DWord): DWord;
var
  InBuff: LongRec absolute Value;
  OutBuff: LongRec absolute Result;
begin
  OutBuff.Bytes[0] := InBuff.Bytes[3];
  OutBuff.Bytes[1] := InBuff.Bytes[2];
  OutBuff.Bytes[2] := InBuff.Bytes[1];
  OutBuff.Bytes[3] := InBuff.Bytes[0];
end;

2.B — моя ранее примененная функция, в качестве SwapEndian16 тестировался наиболее быстрый ассемблерный вариант функции:

function SwapEndian32(Value: DWord): DWord;
var
  InBuff: LongRec absolute Value;
  OutBuff: LongRec absolute Result;
begin
  OutBuff.Lo := SwapEndian16(InBuff.Hi); // SwapEndian16 вариант 1.D
  OutBuff.Hi := SwapEndian16(InBuff.Lo);
end;

2.С — предложена VGA, чистый паскаль, алгоритм сдвига и замены по маске:

function SwapEndian32(Value: DWord): DWord;
begin
  Result := Value;
  Result := ((Result and $FFFF0000) shr 16) or ((Result and $0000FFFF) shl 16);
  Result := ((Result and $FF00FF00) shr 8) or ((Result and $00FF00FF) shl 8);
end;

2.D — предложена VGA, inline assembler, нативная команда i486 обмена байтами в двойном слове:

function SwapEndian32(Value: DWord): DWord; register;
asm
  BSWAP EAX
end;

Результаты SwapEndian32:

  2.A - 17.810 ms
  2.B - 17.808 ms
  2.C - 4.878 ms
  2.D - 3.276 ms

3. Обмен байтами в 64-разрядном числе. Тестировалось четыре варианта функции, расположены в порядке убывания времени выполнения.
3.A — предложена VGA, чистый паскаль, алгоритм сдвига и замены по маске:

function SwapEndian64(Value: Int64): Int64;
begin
  Result := Value;
  Result := ((Result and $FFFFFFFF00000000) shr 32) or ((Result and $00000000FFFFFFFF) shl 32);
  Result := ((Result and $FFFF0000FFFF0000) shr 16) or ((Result and $0000FFFF0000FFFF) shl 16);
  Result := ((Result and $FF00FF00FF00FF00) shr 8) or ((Result and $00FF00FF00FF00FF) shl 8);
end;

3.B — моя функция, чистый паскаль. По коду сразу понятно что она делает:

function SwapEndian64(Value: Int64): Int64;
type
  Long64Rec = packed record
    case Integer of
      0: (Lo, Hi: DWord);
      1: (Bytes: array [0..7] of Byte);
  end;
var
  InBuff: Long64Rec absolute Value;
  OutBuff: Long64Rec absolute Result;
begin
  OutBuff.Bytes[0] := InBuff.Bytes[7];
  OutBuff.Bytes[1] := InBuff.Bytes[6];
  OutBuff.Bytes[2] := InBuff.Bytes[5];
  OutBuff.Bytes[3] := InBuff.Bytes[4];
  OutBuff.Bytes[4] := InBuff.Bytes[3];
  OutBuff.Bytes[5] := InBuff.Bytes[2];
  OutBuff.Bytes[6] := InBuff.Bytes[1];
  OutBuff.Bytes[7] := InBuff.Bytes[0];
end;

3.C — моя ранее примененная функция, в качестве SwapEndian32 тестировался наиболее быстрый ассемблерный вариант функции:

function SwapEndian64(Value: Int64): Int64;
type
  Long64Rec = packed record
    case Integer of
      0: (Lo, Hi: DWord);
      1: (Bytes: array [0..7] of Byte);
  end;
var
  InBuff: Long64Rec absolute Value;
  OutBuff: Long64Rec absolute Result;
begin
  OutBuff.Lo := SwapEndian32(InBuff.Hi); // SwapEndian32 вариант 2.D
  OutBuff.Hi := SwapEndian32(InBuff.Lo);
end;

3.D — «чистый» ассемблерный вариант предыдущей функции:

function SwapEndian64(Value: Int64): Int64;
asm
  MOV   EDX, DWORD PTR [Value]
  BSWAP EDX
  MOV   EAX, DWORD PTR [Value+4]
  BSWAP EAX
end;

Результаты SwapEndian64:

  3.A - 27.469 ms
  3.B - 23.460 ms
  3.С - 10.541 ms
  3.D - 5.442 ms

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

Были отобраны по две самые быстрые функции, на ассемблере и на паскале, и добавлены в код с применением методов условной компиляции. Введен дефайн

{.$DEFINE ASM_ENDIAN} // BigEndian -> LittleEndian 

и две ветви условной компиляции

{$IFDEF ASM_ENDIAN}

function SwapEndian16(Value: Word): Word; register;
asm
  ROL   AX, 8
end;

function SwapEndian32(Value: DWord): DWord; register;
asm
  BSWAP EAX
end;

function SwapEndian64(Value: Int64): Int64; register;
asm
  MOV   EDX, DWORD PTR [Value]
  BSWAP EDX
  MOV   EAX, DWORD PTR [Value+4]
  BSWAP EAX
end;

{$ELSE}

function SwapEndian16(Value: Word): Word;
begin
  Result := Value;
  Result := ((Result and $FF00) shr 8) or ((Result and $00FF) shl 8);
end;

function SwapEndian32(Value: DWord): DWord;
begin
  Result := Value;
  Result := ((Result and $FFFF0000) shr 16) or ((Result and $0000FFFF) shl 16);
  Result := ((Result and $FF00FF00) shr 8) or ((Result and $00FF00FF) shl 8);
end;

function SwapEndian64(Value: Int64): Int64;
type
  Long64Rec = packed record
    case Integer of
      0: (Lo, Hi: DWord);
      1: (Bytes: array [0..7] of Byte);
  end;
var
  InBuff: Long64Rec absolute Value;
  OutBuff: Long64Rec absolute Result;
begin
  OutBuff.Bytes[0] := InBuff.Bytes[7];
  OutBuff.Bytes[1] := InBuff.Bytes[6];
  OutBuff.Bytes[2] := InBuff.Bytes[5];
  OutBuff.Bytes[3] := InBuff.Bytes[4];
  OutBuff.Bytes[4] := InBuff.Bytes[3];
  OutBuff.Bytes[5] := InBuff.Bytes[2];
  OutBuff.Bytes[6] := InBuff.Bytes[1];
  OutBuff.Bytes[7] := InBuff.Bytes[0];
end;

{$ENDIF}

Это позволяет легко переключаться между вариантами при необходимости.

  Также тестировался вариант этого-же кода, но являющегося наследником класса TFileStream вместо TMemoryStream. Для этого в коде меняется только содержание конструктора. Из-за того, что функции именения эндианности убраны из класса именился и код метода ReadSize64.

type
  TMp4Stream = class(TFileStream)
  private
    function ReadSize64: Int64;
    function ReadAtom(var aSize: DWord): DWord;
    function GetResolution(var aWidth, aHeight: WORD; aSize: DWord): Boolean;
  public
    constructor Create(const aFileName: string);
    function ParceMP4(var aWidth, aHeight: Word): Boolean;
  end;

Т.к. класс TFileStream является оболочкой файла его конструктор упростился:

constructor TMp4Stream.Create(const aFileName: string);
begin
  inherited Create(aFileName, fmOpenRead or fmShareExclusive);
end;

Функция ReadSize64 тоже упростилась иэ-эа применения внешней SwapEndian64:

function TMp4Stream.ReadSize64: Int64;
var
  InBuff: Int64;
begin
  ReadBuffer(InBuff, SizeOf(InBuff));
  Result := SwapEndian64(InBuff);
end;

  Тестирование времени исполнения классов, имеющих в качестве родительских классы TMemoryStream и TFileStream производилось на двух файлах из каталога C:\Windows\Performance\WinSAT\, это файлы Clip_480_5sec_6mbps_h264.mp4 и Clip_1080_5sec_10mbps_h264.mp4. Получение разрешения этих двух файлов запускалось в цикле на 5000 итераций. Результат:

    | TMemoryStream | TFileStream |
ASM |   30.119 ms   |  0.570 ms   | разница 52,8
PAS |   30.186 ms   |  0.585 ms   | разница 51,6

Наследник TFileStream оказался гораздо быстрее наследника TMemoryStream, влияние ассемблерных инструкций на код незначительно.

P.S. Оптимизация.

Из комментариев понятно, что оптимизацию до конца не довел. В качестве SwapEndian64 в варианте 3.С был рассмотрен вариант функции изменения эндианности 64-разрядного числа с ассемблерными подфункциями. Поэтому я не отнес ее к паскалевским и сделал неверный вывод, что самый быстрый алгоритм для 64-разрядного числа это прямой обмен байтами. А протестировать этот-же вариант с подфункциями на паскале я забыл, на что мне и указали, исправляюсь. Сразу приведу ветвь условной компиляции на паскале, ассемблерная не изменилась.

{$ELSE}

function SwapEndian16(Value: Word): Word;
begin
  Result := Value;
  Result := ((Result and $FF00) shr 8) or ((Result and $00FF) shl 8);
end;

function SwapEndian32(Value: DWord): DWord;
begin
  Result := Value;
  Result := ((Result and $FFFF0000) shr 16) or ((Result and $0000FFFF) shl 16);
  Result := ((Result and $FF00FF00) shr 8) or ((Result and $00FF00FF) shl 8);
end;

function SwapEndian64(Value: Int64): Int64;
type
  Long64Rec = packed record
    case Integer of
      0: (Lo, Hi: DWord);
      1: (Bytes: array [0..7] of Byte);
  end;
var
  InBuff: Long64Rec absolute Value;
  OutBuff: Long64Rec absolute Result;
begin
  OutBuff.Lo := SwapEndian32(InBuff.Hi);
  OutBuff.Hi := SwapEndian32(InBuff.Lo);
end;

{$ENDIF}

SwapEndian64 не отличается от примера 3.С но вызывает она паскалевский вариант подфункции SwapEndian32. В этом варианте время исполнения по той-же методике расчета составило 14.950 ms, что конечно же быстрее прямого обмена байтами в 64-разрядном числе.

Файлы в приложенном проекте заменил на доработанные.
  • ?
  • 31 октября 2019, 18:18
  • anakost
  • 1
Файлы в топике: Project.zip

Комментарии (20)

RSS свернуть / развернуть
LoadFromFile(aFileName);
Что-то мне подсказывает, что на запуск ffmpeg и полноценный парсинг им файла уйдет куда меньше времени, чем на загрузку мало-мальски приличного видеофайла в память целиком. Я уж не говорю о потреблении памяти.
Вместо абсолютно неуместного наследования от TMemoryStream следовало наследоваться от TObject и открывать файл через TFileStream. Это если тут вообще имеет смысл делать объект.
так понимаю, видео и аудио, причем один на месте разрешения содержит нули
Вообще-то, не мешало бы сперва выяснить какой вообще трек открыли, посмотрев на его тип.
// BigEndian -> LittleEndian
Вместо 5 вызовов ReadBuffer можно было 1 раз считать структуру и затем перевернуть в ней слово встроенной функцией, разворачивающейся в одну ассемблерную команду. Конечно, по сравнению с чтением всего файла это совершенно несущественная оптимизация, но она бы сделала код короче и понятнее.
Это же в полной мере относится к GetResolution, а вот FindAtom являет собой типичный пример названия, абсолютно не соответствующего выполняемой операции, что опять же усложняет понимание кода.
P.S. Этот код работает правильно со стандартом файлов MP4, у которых поле размера атома составляет двойное слово, а значит размер атома не может превышать 2Гб.
На самом деле он вылетит уже на файле размером примерно в 1.6ГБ, что в нынешних реалиях вызывает вопрос «а они вообще бывают такими мелкими?».

Кроме того, сведения в контейнере больше референсные и могут не соответствовать реальности. Иногда одни и те же данные записаны в десяти местах, противоречат друг другу или делают видео криво отображающимся в половине плееров. Так что ffmpeg даст более надежные сведения.
0
  • avatar
  • Vga
  • 31 октября 2019, 21:08
Вообще-то, не мешало бы сперва выяснить какой вообще трек открыли, посмотрев на его тип.
Я еще не понял, как определить тип трека и надо ли. Лишняя проверка.
Вместо 5 вызовов ReadBuffer можно было 1 раз считать структуру и затем перевернуть в ней слово встроенной функцией, разворачивающейся в одну ассемблерную команду.
Можно, до оптимизаций пока не добрался.
Так что ffmpeg даст более надежные сведения.
Возможно так, но геморойно.
0
Я еще не понял, как определить тип трека и надо ли. Лишняя проверка.
Проверка лишняя только тогда, когда не дает решить решаемую без нее задачу. Проверка на лицензию, например.
Тип трека, впрочем, надо искать в других атомах и эти поля в атоме tkhd действительно могут содержать только разрешение. Но судя по флагам — там может быть разрешение превью-картинки, например.
Возможно так, но геморойно.
Зато работает не только в самых простых случаях. Ты даже элементарнейше реализуемую поддержку больших файлов не сделал. А еще бывают сжатые метаданные, когда в moov единственный атом cmov. Да и парсить вывод не особо геморройно, а вот таскать с собой 100-метровый ффмпег может быть не очень приятно. Лучше посмотреть на библиотеки медиаинфо.
0
Ты даже элементарнейше реализуемую поддержку больших файлов не сделал.
Моя недоделка, мне просто повезло, и у всех используемых мной для тестирования MP4 атом «moov» оказывался перед атомом «mdat» (сами видео, аудио данные, субтитры если есть). Поэтому код находил разрешение и выходил, не заходя в «mdat».
Сегодня продолжил тестирование и на одном файле обломился, атом «mdat» оказался перед «moov». Лог файл:

  Name-ftyp, Atom-0x70797466, Size-0x18
  Name-free, Atom-0x65657266, Size-0x7E7C
  Name-mdat, Atom-0x7461646D, Size-0x1
  Name-dat , Atom-0x00746164, Size-0x16D
  Name-    , Atom-0x00030000, Size-0x3000003

Хорошо видно, что атом «mdat» имеет размер 1 байт, хотя минимальный размер пустого атома 8 байт. Из-за этого считывание сдвинулось на 1 байт, следующий атом стал называться «dat», а «m» сдвинулась в размер (0x6D). Ну и код полез черти-куда и ничего естественно не нашел.
Как лечить описано в описании "QuickTime container". Цитата:
The 4 bytes allotted for the atom size field limit the maximum size of an atom to 4 GB. Quicktime also has a provision to allow atoms with 64-bit atom size fields by setting the size field 1 and adding the 8-byte size field after the atom type:

bytes 0-3 always 0x00000001
bytes 4-7 atom type
bytes 8-15 atom size (including 16-byte size and type preamble)
bytes 16..n data
This is a logical exception since an atom always needs to be at least 8 bytes in length to account for the preamble. Therefore, if the size field is 1, load the 64-bit atom size from just after the atom type field.

If, on the other hand, the size field is 0, then the atom extends to the end of the file.
Т.е. если в поле размера атома стоит единица, значит 8-байтное поле размера будет следовать за заголовком. Если в поле размера атома стоит ноль, атом продолжается до конца файла.
Ничего этого я не учел, доделаю и выложу попозже.
0
Всегда приветствую конструктивную крититику, не дает закостенеть, спасибо VGA. Переработал первоначально представленный код с учетом выявленных ошибок и критики. Не стал вносить поправки в уже опубликованный код, продолжил статью второй частью.
0
Тестовый лог обновленного кода парсингом файла, на котором обломился в прошлый раз:

  --- атомы верхнего уровня ----------------
  Name-ftyp, Atom-70797466, Size-18
  Name-free, Atom-65657266, Size-7E7C
  Name-mdat, Atom-7461646D, Size-2A67CF7
  Name-uuid, Atom-64697575, Size-1147
  Name-moov, Atom-766F6F6D, Size-7E9B
  --- атомы уровня 1 -----------------------
  Name-mvhd, Atom-6468766D, Size-6C
  Name-trak, Atom-6B617274, Size-4700
  --- атомы уровня 2 -----------------------
  Name-tkhd, Atom-64686B74, Size-5C
  --- поиск окончен ------------------------
  Result, Width-1280, Height-720
  ------------------------------------------
0
Зачем ты по прежнему наследуешься от TMemoryStream и загружаешь файл целиком? Наследуйся от TObject и включи поле FData: TStream, значение для которого передается в конструктор (ну или TFileStream, создающийся в конструкторе).
Во-первых, грузить мало-мальски большой файл — долго. Во-вторых, жрет много памяти. И в-третьих — в 32-битных программах теоретический лимит на выделение памяти 2ГБ, а практический — около 1.6ГБ, на более крупном файле будет ошибка.

Как ни странно, встроенных функций конвертирования эндианности у дельфи нет. Только легаси-функция Swap, работающая с 16-битными значениями. Своим функции можно сделать двумя методами.
1) Воспользоваться ассемблерными инструкциями.
function SwapEndian32(Value: integer): integer; register;
asm
  bswap eax
end;

function SwapEndian16(Value: smallint): smallint; register;
asm
  rol   ax, 8
end;

2) Воспользоваться классическим методом:
X := ((X and $ffffffff00000000) shr 32) or ((X and $00000000ffffffff) shl 32);
X := ((X and $ffff0000ffff0000) shr 16) or ((X and $0000ffff0000ffff) shl 16);
X := ((X and $ff00ff00ff00ff00) shr 8) or ((X and $00ff00ff00ff00ff) shl 8);
0
  • avatar
  • Vga
  • 02 ноября 2019, 14:33
function SwapEndian64(Value: Int64): Int64; register;
asm
  mov eax, [ebp+$0c]
  mov edx, [ebp+$08]
  bswap eax
  bswap edx
end;
0
А, статье уже есть такое, и несколько правильней написанное.
0
Класс TMemoryStream применен на этапе изучения воэможности определения разрешения видеофайла формата MP4 прямым парсингом. Представленный код это только подтверждение такой возможности. Тонкости реализации и загрузка памяти на этом этапе меня не очень волновали. А так да, с точки зрения рационального использования памяти применение TFileStream лучше, еще лучше воспользоваться WinAPI с функциями CreateFile и CreateFileMapping.
Не думаю, что перевод функций конвертирования эндианности в ассемблерный вид даст какой-то значимый эффект. И даже если я выиграю тысячную долю секунды, зачем мне это? Выигрыш мизерный, читабельность гораздо хуже.
0
Ну-у, если говорить о читабельности, тот тут еще много к чему можно придраться. На этом фоне функции конвертирования эндианности — явно не проблема. Да и оно хоть и на ассемблере, а на редкость прямолинейно и понятно. Проблемы будут разве что если тащить этот код в десятую студию и собирать под какой-нить айфон.
еще лучше воспользоваться WinAPI с функциями CreateFile и CreateFileMapping.
TFileStream внутри — тот же CreateFile, а маппинг… Вряд ли он что-то даст кроме усложнения кода.
0
А ffprobe с выводом в json или xml — будет таки приятнее обрабатывать, нежели сырые боксы из mp4. Будет вам ещё с чем покопаться, когда зальют файл с 64-битными боксами.
0
Дополнил оптимизацией.
0
Код был написан для имеющихся в наличии файлов и работает быстро и безошибочно. Улыбнуло, ваш комментарий
Будет вам ещё с чем покопаться, когда зальют файл с 64-битными боксами.
напоминает анекдот о суровых таежных лесорубах и японской бензопиле.
0
Без комментария алгоритм сразу не понять:
Стандартные решения вроде этого должны опознаваться с первого взгляда. И я бы выкинул Result := Value, написав в следующей строке Result := ((Value and $FF00) shr 8) or ((Value and $00FF) shl 8). Выхлоп врядли изменится, но зачем захламлять код лишними операциями?
и ненамного уступает ассемблерным.
Примерно на 35% дольше. При частом использовании может быть существенно.
Вот и неожиданность, метод на паскале через сдвиг и замену по маске оказался медленнее прямого обмена регистрами для 64-разрядного аргумента.
Похоже, 64-битные сдвиги в 32-битном коде жутко медленны. Интересно, что там нагенерилось в ассемблере.
Были отобраны по две самые быстрые функции, на ассемблере и на паскале
А с чего это на паскале самая быстрая — с обменом по байтикам? Я думаю, перестановка двух свопнутых сдвигами двордов будет быстрее, причем в два раза. Еще чуть выиграть можно, подставив свопы прямо в тело функции, а не вызывая Swap32.
Получение разрешения этих двух файлов запускалось в цикле на 5000 итераций.
А вот здесь итерировать как раз не надо. Винда умная и пять тысяч раз читать файл не будет. А на практике уже ты не будешь по 5к раз подряд один файл читать. Возьми папку с кучей нормальных файлов, суммарным размером в несколько гигабайт и запроси у каждого по разу. Причем между тестами (и перед тестами, если ты эту папку собирал прямо перед тестом) TFileStream и TMemoryStream надо выбить буферизацию файлов из памяти — скажем, скопировать с диска на диск файлов на 2-3 объема установленной RAM.
0
  • avatar
  • Vga
  • 11 ноября 2019, 03:13
Интересно, что там нагенерилось в ассемблере.
Ухх, какая жесть. Чего стоит одно копирование данных из стека в EDX:EAX и затем
XOR EAX, EAX ; и зачем было грузить?
MOV EAX, EDX ; и зачем было очищать?
XOR EDX, EDX
0
И я бы выкинул Result := Value, написав в следующей строке Result := ((Value and $FF00) shr 8) or ((Value and $00FF) shl 8). Выхлоп врядли изменится, но зачем захламлять код лишними операциями?
При 32-битном аргументе это бесполезно. У функции не указан спецификатор передачи параметров, значит это register. При этом спецификаторе 32-битный аргумент передается в EAX, и 32-битный результат читается из EAX. Поэтому в функции
function Func(Value: DWord): DWord;
begin
Result := Value;
end;
при записи на ассемблере не нужно ни строчки кода. Другое дело если аргумент 64-битный, т.е. превышает разрядность компилятора. При этом в EAX будет не сам аргумент, а ссылка на него. Это наводит на мысль что функцию SwapEndian64 нужно записать по другому:
function SwapEndian64e(Value: Int64): Int64;
begin
Result := ((Value and $FFFFFFFF00000000) shr 32) or ((Value and $00000000FFFFFFFF) shl 32);
Result := ((Result and $FFFF0000FFFF0000) shr 16) or ((Result and $0000FFFF0000FFFF) shl 16);
Result := ((Result and $FF00FF00FF00FF00) shr 8) or ((Result and $00FF00FF00FF00FF) shl 8);
end;
Время исполнения действительно уменьшается и функция начинает обгонять метод прямого обмена байтами — 24.685 ms. Но все-же довольно медленно, наверное 64-битный аргумент обрабатывается по половине 32-битным компилятором. Посмотрев ассемблерный листинг это ясно видно.
Эффективнее сделать это самому, обработать 32-битные половинки отдельно и обменять. Дополнил статью.
Еще чуть выиграть можно, подставив свопы прямо в тело функции, а не вызывая Swap32.
Если-бы функции изменения эндианности были узким местом в алгоритме, можно было-бы пойти дальше и убрать вызовы функций, разместив их непосредственно в коде.
function SwapEndian64e(Value: Int64): Int64;
type
  Long64Rec = packed record
    case Integer of
      0: (Lo, Hi: DWord);
      1: (Bytes: array [0..7] of Byte);
  end;
var
  InBuff: Long64Rec absolute Value;
  OutBuff: Long64Rec absolute Result;
begin
  OutBuff.Lo := ((InBuff.Hi and $FFFF0000) shr 16) or ((InBuff.Hi and $0000FFFF) shl 16);
  OutBuff.Lo := ((OutBuff.Lo and $FF00FF00) shr 8) or ((OutBuff.Lo and $00FF00FF) shl 8);
  OutBuff.Hi := ((InBuff.Lo and $FFFF0000) shr 16) or ((InBuff.Lo and $0000FFFF) shl 16);
  OutBuff.Hi := ((OutBuff.Hi and $FF00FF00) shr 8) or ((OutBuff.Hi and $00FF00FF) shl 8);
end;

Время исполнения 12.765 ms. Но т.к. функции изменения эндианности узким местом не являются и их убыстрение сравнимо со статистической погрешностью, не думаю что стоит этим заниматься.
0
При 32-битном аргументе это бесполезно.
Это сокращает исходный код, выкидывая из него лишнюю семантическую единицу. При таком влиянии оптимальности свопа как в этом случае такое изменение оправдано даже если оно замедляет код.
Время исполнения 12.765 ms. Но т.к. функции изменения эндианности узким местом не являются и их убыстрение сравнимо со статистической погрешностью, не думаю что стоит этим заниматься.
Хотя они не являются узким местом кода, они являются единственным интересным местом статьи. Другим могло бы быть описание формата MP4, если бы оно, гм, было.
0
Другим могло бы быть описание формата MP4, если бы оно, гм, было.
Не-не-не, я пока не ошалел форматы ISO описывать. Могу подсказать неплохую шпаргалку от Simarron Systems.
0
Видел я эту шпаргалку, но на мои вопросы она не ответила. Пришлось ковырять какие-то сырки, более полные.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.