Затухание в полторы-две минуты собиралось минутами, и всё это время предпросмотр ждёт. Виноваты оказались не фильтры: на десяти секундах 2688x1512 цепочка со сведением стоит 13.5 с, а вовсе без фильтров — 12.7 с. Время уходит на разбор HEVC процессором. Ключ -hwaccel cuda перед входом убирает почти всё: настоящее затухание на семьдесят секунд собирается 18.3 с вместо 75.9 с, картинка бит в бит та же (одинаковый md5 потока, 4196 кадров там и там). Просится он только там, где кодировать будет та же карта — то есть выбран NVENC. На машине без неё ключ не появляется вовсе. Сверх того у подготовки вставок есть откат: не пошло — повторяем на процессоре, как это уже делает склейка с перекодированием. Сборка ролика пока по-старому: там кусок с наложением идёт в общий поток, и повторить его после отказа посреди работы нельзя — данные уже ушли в трубу. Это отдельная задача.
972 lines
37 KiB
ObjectPascal
972 lines
37 KiB
ObjectPascal
{
|
||
Проверка того, что свойства, записанные в файле формы, у наших
|
||
элементов действительно есть.
|
||
|
||
Зачем отдельно. Файл формы разбирается при запуске, а не при сборке:
|
||
опечатка в имени свойства или в значении перечисления даёт не ошибку
|
||
компиляции, а падение окна на старте. Здесь то же самое проверяется без
|
||
окна — по сведениям о типах.
|
||
}
|
||
program lfmcheck;
|
||
|
||
{$mode objfpc}{$H+}
|
||
|
||
uses
|
||
{ Interfaces нужен, чтобы связалась вся LCL и заработала загрузка
|
||
файлов форм. }
|
||
Interfaces, Forms, Controls, Classes, SysUtils, Math, TypInfo, uTheme,
|
||
uFfmpegCmd, uAppTypes,
|
||
uMediaProbe, uProject, uTransform, uTimelineCtrl,
|
||
uPanCropForm, uDepsForm, uMismatchForm, uProgressForm, uProjectInfoForm,
|
||
uRemuxForm;
|
||
|
||
var
|
||
Failed: Integer = 0;
|
||
|
||
procedure CheckProp(AClass: TClass; const AName: string);
|
||
begin
|
||
if GetPropInfo(AClass, AName) <> nil then
|
||
WriteLn(' ok ', AClass.ClassName, '.', AName)
|
||
else
|
||
begin
|
||
WriteLn(' ПЛОХО ', AClass.ClassName, '.', AName, ' — свойства нет');
|
||
Inc(Failed);
|
||
end;
|
||
end;
|
||
|
||
procedure CheckEnum(ATypeInfo: PTypeInfo; const AName: string);
|
||
begin
|
||
if GetEnumValue(ATypeInfo, AName) >= 0 then
|
||
WriteLn(' ok значение ', AName)
|
||
else
|
||
begin
|
||
WriteLn(' ПЛОХО значение ', AName, ' — такого нет');
|
||
Inc(Failed);
|
||
end;
|
||
end;
|
||
|
||
{ Создаёт окно и тут же убирает.
|
||
|
||
Смысл в самом создании: файл формы разбирается именно тогда. Элемент,
|
||
который есть в файле формы и которого нет в классе, или обработчик с
|
||
опечаткой в имени — всё это здесь и вскрывается, а иначе вскрылось бы у
|
||
человека при открытии окна.
|
||
|
||
Главное окно не проверяется: оно при создании читает настройки, строит
|
||
меню и заводит плеер — это уже не проверка формы, а запуск программы. }
|
||
procedure CheckForm(AClass: TFormClass);
|
||
var
|
||
F: TForm;
|
||
begin
|
||
try
|
||
F := AClass.Create(nil);
|
||
try
|
||
WriteLn(' ok ', AClass.ClassName, ' — форма загружена');
|
||
finally
|
||
F.Free;
|
||
end;
|
||
except
|
||
on E: Exception do
|
||
begin
|
||
WriteLn(' ПЛОХО ', AClass.ClassName, ': ', E.Message);
|
||
Inc(Failed);
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
procedure CheckTrue(const AName: string; AValue: Boolean);
|
||
begin
|
||
if AValue then
|
||
WriteLn(' ok ', AName)
|
||
else
|
||
begin
|
||
WriteLn(' ПЛОХО ', AName);
|
||
Inc(Failed);
|
||
end;
|
||
end;
|
||
|
||
{ Прилипание курсора воспроизведения к границам клипов.
|
||
|
||
Проверяется числами, а не мышью: расчёт открыт наружу именно ради
|
||
этого. Мышью пришлось бы поднимать окно, а окно в проверках — это уже
|
||
не проверка расчёта, а проверка окружения. }
|
||
procedure CheckPlayheadSnap;
|
||
var
|
||
Proj: TProject;
|
||
Info: TMediaInfo;
|
||
Media: TMediaItem;
|
||
Timeline: TTimelineControl;
|
||
begin
|
||
WriteLn;
|
||
WriteLn('Прилипание курсора к границам клипов');
|
||
WriteLn;
|
||
|
||
Info := Default(TMediaInfo);
|
||
Info.Valid := True;
|
||
Info.Kind := mkVideo;
|
||
Info.Duration := 600;
|
||
Info.HasVideo := True;
|
||
Info.VideoCodec := 'hevc';
|
||
Info.Width := 1920;
|
||
Info.Height := 1080;
|
||
Info.FpsNum := 30;
|
||
Info.FpsDen := 1;
|
||
Info.PixFmt := 'yuv420p';
|
||
|
||
Proj := TProject.Create;
|
||
Timeline := TTimelineControl.Create(nil);
|
||
try
|
||
Proj.Clear;
|
||
Proj.AddTrack('V1');
|
||
Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
|
||
{ Два куска встык: границы на 0, 1.5 и 3. }
|
||
Proj.AddClip(Proj.Tracks[0], Media, 0, 1.5, 0);
|
||
Proj.AddClip(Proj.Tracks[0], Media, 10, 1.5, 1.5);
|
||
|
||
Timeline.Project := Proj;
|
||
{ Сто точек в секунду: порог прилипания в восемь точек — это 0.08 с. }
|
||
Timeline.PixelsPerSecond := 100;
|
||
Timeline.PlayheadSnap := True;
|
||
|
||
CheckTrue('стык рядом — притянуло',
|
||
Abs(Timeline.SnapPlayhead(1.53) - 1.5) < 0.0001);
|
||
CheckTrue('с другой стороны — тоже',
|
||
Abs(Timeline.SnapPlayhead(1.47) - 1.5) < 0.0001);
|
||
CheckTrue('далеко от границ — не тронуло',
|
||
Abs(Timeline.SnapPlayhead(0.7) - 0.7) < 0.0001);
|
||
CheckTrue('начало монтажа притягивает',
|
||
Abs(Timeline.SnapPlayhead(0.05)) < 0.0001);
|
||
CheckTrue('конец последнего клипа притягивает',
|
||
Abs(Timeline.SnapPlayhead(2.96) - 3) < 0.0001);
|
||
|
||
{ На мелком масштабе порог в точках даёт больший запас по времени. }
|
||
Timeline.PixelsPerSecond := 10;
|
||
CheckTrue('масштаб мельче — притягивает дальше',
|
||
Abs(Timeline.SnapPlayhead(1.7) - 1.5) < 0.0001);
|
||
|
||
Timeline.PixelsPerSecond := 100;
|
||
Timeline.PlayheadSnap := False;
|
||
CheckTrue('выключено — не притягивает',
|
||
Abs(Timeline.SnapPlayhead(1.53) - 1.53) < 0.0001);
|
||
finally
|
||
Timeline.Free;
|
||
Proj.Free;
|
||
end;
|
||
end;
|
||
|
||
{ Рез не должен терять настройки клипа.
|
||
|
||
Половина клипа после реза — тот же клип, и цветовой ключ, кадрирование,
|
||
прозрачность, громкость и текст должны остаться при обеих половинах.
|
||
Однажды не остались: у хвоста пропадал ключ, клип переставал быть
|
||
наложением и закрывал собой всё, что под ним. }
|
||
procedure CheckSplitKeepsSettings;
|
||
var
|
||
Proj: TProject;
|
||
Info: TMediaInfo;
|
||
Media: TMediaItem;
|
||
Timeline: TTimelineControl;
|
||
Head, Tail, Cl: TClip;
|
||
Key: TColorKey;
|
||
Tr: TMediaTransform;
|
||
I: Integer;
|
||
begin
|
||
WriteLn;
|
||
WriteLn('Рез сохраняет настройки клипа');
|
||
WriteLn;
|
||
|
||
Info := Default(TMediaInfo);
|
||
Info.Valid := True;
|
||
Info.Kind := mkVideo;
|
||
Info.Duration := 600;
|
||
Info.HasVideo := True;
|
||
Info.VideoCodec := 'hevc';
|
||
Info.Width := 1920;
|
||
Info.Height := 1080;
|
||
Info.FpsNum := 30;
|
||
Info.FpsDen := 1;
|
||
Info.PixFmt := 'yuv420p';
|
||
|
||
Proj := TProject.Create;
|
||
Timeline := TTimelineControl.Create(nil);
|
||
try
|
||
Proj.Clear;
|
||
Proj.AddTrack('V1');
|
||
Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
|
||
Cl := Proj.AddClip(Proj.Tracks[0], Media, 0, 10, 0);
|
||
|
||
Key := DefaultColorKey;
|
||
Key.Enabled := True;
|
||
Key.Similarity := 22;
|
||
Cl.ColorKey := Key;
|
||
Cl.Opacity := 0.5;
|
||
Cl.Volume := 0.3;
|
||
Tr := Cl.Transform;
|
||
Tr.Zoom := 0.4;
|
||
Tr.OffsetX := 0.7;
|
||
Cl.Transform := Tr;
|
||
Cl.GroupId := 'g1';
|
||
|
||
Timeline.Project := Proj;
|
||
Timeline.PixelsPerSecond := 100;
|
||
Timeline.Playhead := 4;
|
||
Timeline.SplitAtPlayhead(False);
|
||
|
||
CheckTrue('клипов стало два', Proj.Tracks[0].Count = 2);
|
||
if Proj.Tracks[0].Count <> 2 then
|
||
Exit;
|
||
|
||
Head := nil;
|
||
Tail := nil;
|
||
for I := 0 to Proj.Tracks[0].Count - 1 do
|
||
if Proj.Tracks[0][I].TlStart < 0.001 then
|
||
Head := Proj.Tracks[0][I]
|
||
else
|
||
Tail := Proj.Tracks[0][I];
|
||
CheckTrue('обе половины нашлись', (Head <> nil) and (Tail <> nil));
|
||
if (Head = nil) or (Tail = nil) then
|
||
Exit;
|
||
|
||
CheckTrue('ключ остался у хвоста', Tail.ColorKey.Enabled);
|
||
CheckTrue('и с тем же цветом', Tail.ColorKey.Color = Key.Color);
|
||
CheckTrue('и с тем же допуском', Tail.ColorKey.Similarity = 22);
|
||
CheckTrue('прозрачность осталась', Abs(Tail.Opacity - 0.5) < 0.001);
|
||
CheckTrue('громкость осталась', Abs(Tail.Volume - 0.3) < 0.001);
|
||
CheckTrue('кадрирование осталось',
|
||
(Abs(Tail.Transform.Zoom - 0.4) < 0.001) and
|
||
(Abs(Tail.Transform.OffsetX - 0.7) < 0.001));
|
||
CheckTrue('ключ остался и у головы', Head.ColorKey.Enabled);
|
||
{ Связки разные, и обе не пустые: иначе удаление одной половины
|
||
уносило бы вторую. }
|
||
CheckTrue('связка у половин разная и не пустая',
|
||
(Head.GroupId <> Tail.GroupId) and (Head.GroupId <> '') and
|
||
(Tail.GroupId <> ''));
|
||
CheckTrue('появление у хвоста снято', Abs(Tail.FadeIn) < 0.001);
|
||
finally
|
||
Timeline.Free;
|
||
Proj.Free;
|
||
end;
|
||
end;
|
||
|
||
{ Схлопывание дыры без мыши.
|
||
|
||
Ctrl+Shift+R искал дыру только на дорожке под указателем мыши, а руки в
|
||
этот момент на клавиатуре. Со стороны выглядело как «горячая клавиша не
|
||
работает»: команда молча ничего не делала. }
|
||
procedure CheckRippleWithoutMouse;
|
||
var
|
||
Proj: TProject;
|
||
Info: TMediaInfo;
|
||
Media: TMediaItem;
|
||
Timeline: TTimelineControl;
|
||
begin
|
||
WriteLn;
|
||
WriteLn('Схлопывание дыры с клавиатуры');
|
||
WriteLn;
|
||
|
||
Info := Default(TMediaInfo);
|
||
Info.Valid := True;
|
||
Info.Kind := mkVideo;
|
||
Info.Duration := 600;
|
||
Info.HasVideo := True;
|
||
Info.VideoCodec := 'hevc';
|
||
Info.Width := 1920;
|
||
Info.Height := 1080;
|
||
Info.FpsNum := 30;
|
||
Info.FpsDen := 1;
|
||
Info.PixFmt := 'yuv420p';
|
||
|
||
Proj := TProject.Create;
|
||
Timeline := TTimelineControl.Create(nil);
|
||
try
|
||
Proj.Clear;
|
||
Proj.AddTrack('V1');
|
||
Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
|
||
Proj.Master := Info;
|
||
{ Дыра с 5 по 12 секунду. }
|
||
Proj.AddClip(Proj.Tracks[0], Media, 0, 5, 0);
|
||
Proj.AddClip(Proj.Tracks[0], Media, 20, 5, 12);
|
||
|
||
Timeline.Project := Proj;
|
||
Timeline.PixelsPerSecond := 100;
|
||
Timeline.ActiveTrack := 0;
|
||
Timeline.Playhead := 8;
|
||
|
||
{ Мышь не над дорожками: так и бывает, когда жмут сочетание клавиш. }
|
||
CheckTrue('дыра схлопнута без мыши', Timeline.RippleCloseGap);
|
||
CheckTrue('второй клип придвинут к первому',
|
||
Abs(Proj.Tracks[0][1].TlStart - 5) < 0.001);
|
||
{ Второй раз схлопывать нечего — и об этом надо сказать, а не молчать. }
|
||
CheckTrue('повторный вызов честно отвечает «нечего»',
|
||
not Timeline.RippleCloseGap);
|
||
finally
|
||
Timeline.Free;
|
||
Proj.Free;
|
||
end;
|
||
end;
|
||
|
||
{ Копирование клипа: модель должна переносить всё, кроме места.
|
||
|
||
Само сочетание клавиш проверяется руками, а вот перенос свойств —
|
||
числами: именно из-за него копия отбивки или текстового блока была бы
|
||
бесполезной. }
|
||
procedure CheckClipCopy;
|
||
var
|
||
Proj: TProject;
|
||
Info: TMediaInfo;
|
||
Media: TMediaItem;
|
||
Src, Dst: TClip;
|
||
Key: TColorKey;
|
||
begin
|
||
WriteLn;
|
||
WriteLn('Копия клипа');
|
||
WriteLn;
|
||
|
||
Info := Default(TMediaInfo);
|
||
Info.Valid := True;
|
||
Info.Kind := mkVideo;
|
||
Info.Duration := 600;
|
||
Info.HasVideo := True;
|
||
Info.VideoCodec := 'hevc';
|
||
Info.Width := 1920;
|
||
Info.Height := 1080;
|
||
Info.FpsNum := 30;
|
||
Info.FpsDen := 1;
|
||
Info.PixFmt := 'yuv420p';
|
||
|
||
Proj := TProject.Create;
|
||
try
|
||
Proj.Clear;
|
||
Proj.AddTrack('V1');
|
||
Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
|
||
Src := Proj.AddClip(Proj.Tracks[0], Media, 30, 7, 5);
|
||
Src.Kind := ckText;
|
||
Src.Text := 'Отбивка';
|
||
Src.Volume := 0.4;
|
||
Src.Opacity := 0.6;
|
||
Key := DefaultColorKey;
|
||
Key.Enabled := True;
|
||
Src.ColorKey := Key;
|
||
Src.FadeIn := 1.25;
|
||
Src.EdgeOut := edPrecise;
|
||
Src.GroupId := 'g7';
|
||
|
||
Dst := TClip.Create;
|
||
try
|
||
Dst.CopyAllFrom(Src);
|
||
CheckTrue('вид клипа перенесён', Dst.Kind = ckText);
|
||
CheckTrue('текст перенесён', Dst.Text = 'Отбивка');
|
||
CheckTrue('файл и кусок исходника перенесены',
|
||
(Dst.MediaId = Src.MediaId) and (Abs(Dst.SrcIn - 30) < 0.001) and
|
||
(Abs(Dst.Duration - 7) < 0.001));
|
||
CheckTrue('края перенесены', Dst.EdgeOut = edPrecise);
|
||
CheckTrue('громкость и прозрачность перенесены',
|
||
(Abs(Dst.Volume - 0.4) < 0.001) and (Abs(Dst.Opacity - 0.6) < 0.001));
|
||
CheckTrue('ключ перенесён', Dst.ColorKey.Enabled);
|
||
CheckTrue('затухание перенесено', Abs(Dst.FadeIn - 1.25) < 0.001);
|
||
{ Место и связка — дело того, кто вставляет. }
|
||
CheckTrue('место не перенесено', Abs(Dst.TlStart) < 0.001);
|
||
CheckTrue('связка не перенесена', Dst.GroupId = '');
|
||
finally
|
||
Dst.Free;
|
||
end;
|
||
finally
|
||
Proj.Free;
|
||
end;
|
||
end;
|
||
|
||
{ Общая заготовка: проект с одним файлом и заданной сеткой кадров. }
|
||
function TestInfo: TMediaInfo;
|
||
begin
|
||
Result := Default(TMediaInfo);
|
||
Result.Valid := True;
|
||
Result.Kind := mkVideo;
|
||
Result.Duration := 600;
|
||
Result.HasVideo := True;
|
||
Result.VideoCodec := 'hevc';
|
||
Result.Width := 1920;
|
||
Result.Height := 1080;
|
||
Result.FpsNum := 30;
|
||
Result.FpsDen := 1;
|
||
Result.PixFmt := 'yuv420p';
|
||
end;
|
||
|
||
{ Выделение областью.
|
||
|
||
Проверяется расчётом, а не мышью: сама протяжка проверена на невидимом
|
||
экране, а вот что попадает в рамку — числами, иначе первая же правка
|
||
захвата пройдёт незамеченной. }
|
||
procedure CheckRubberBand;
|
||
var
|
||
Proj: TProject;
|
||
Info: TMediaInfo;
|
||
Media: TMediaItem;
|
||
Timeline: TTimelineControl;
|
||
begin
|
||
WriteLn;
|
||
WriteLn('Выделение областью');
|
||
WriteLn;
|
||
|
||
Info := TestInfo;
|
||
Proj := TProject.Create;
|
||
Timeline := TTimelineControl.Create(nil);
|
||
try
|
||
Proj.Clear;
|
||
Proj.AddTrack('V1');
|
||
Proj.AddTrack('V2');
|
||
Proj.AddTrack('V3');
|
||
Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
|
||
Proj.Master := Info;
|
||
{ V1: клип 0..5 и клип 20..25. V2: длинный клип 0..30.
|
||
V3: клип 0..5 — дорожка вне рамки. }
|
||
Proj.AddClip(Proj.Tracks[0], Media, 0, 5, 0);
|
||
Proj.AddClip(Proj.Tracks[0], Media, 100, 5, 20);
|
||
Proj.AddClip(Proj.Tracks[1], Media, 0, 30, 0);
|
||
Proj.AddClip(Proj.Tracks[2], Media, 0, 5, 0);
|
||
|
||
Timeline.Project := Proj;
|
||
Timeline.PixelsPerSecond := 100;
|
||
|
||
{ Рамка по дорожкам 0 и 1, время 2..10. }
|
||
Timeline.SelectInRange(2, 10, 0, 1, False);
|
||
CheckTrue('задетый клип выделен', Proj.Tracks[0][0].Selected);
|
||
CheckTrue('клип за рамкой не выделен', not Proj.Tracks[0][1].Selected);
|
||
CheckTrue('длинный клип задет и выделен', Proj.Tracks[1][0].Selected);
|
||
CheckTrue('чужая дорожка не тронута', not Proj.Tracks[2][0].Selected);
|
||
|
||
{ Рамку тянут и справа налево — это та же рамка. }
|
||
Timeline.SelectInRange(25, 18, 0, 0, False);
|
||
CheckTrue('обратная протяжка выделила второй клип',
|
||
Proj.Tracks[0][1].Selected);
|
||
CheckTrue('прежнее выделение снято', not Proj.Tracks[0][0].Selected);
|
||
|
||
{ С Ctrl прежнее выделение остаётся. }
|
||
Timeline.SelectInRange(2, 4, 0, 0, True);
|
||
CheckTrue('добавление сохранило прежнее', Proj.Tracks[0][1].Selected);
|
||
CheckTrue('добавление выделило новое', Proj.Tracks[0][0].Selected);
|
||
|
||
{ Рамка, не задевшая ничего, просто снимает выделение. }
|
||
Timeline.SelectInRange(6, 8, 0, 0, False);
|
||
CheckTrue('пустая рамка ничего не выделила',
|
||
Proj.SelectedCount = 0);
|
||
finally
|
||
Timeline.Free;
|
||
Proj.Free;
|
||
end;
|
||
end;
|
||
|
||
{ Рез по курсору режет все дорожки.
|
||
|
||
Раньше резалась только дорожка под указателем мыши, и стоило указателю
|
||
оказаться над пустым местом, как клавиша S молча не делала ничего. }
|
||
procedure CheckSplitAllTracks;
|
||
var
|
||
Proj: TProject;
|
||
Info: TMediaInfo;
|
||
Media: TMediaItem;
|
||
Timeline: TTimelineControl;
|
||
begin
|
||
WriteLn;
|
||
WriteLn('Рез по курсору на всех дорожках');
|
||
WriteLn;
|
||
|
||
Info := TestInfo;
|
||
Proj := TProject.Create;
|
||
Timeline := TTimelineControl.Create(nil);
|
||
try
|
||
Proj.Clear;
|
||
Proj.AddTrack('V1');
|
||
Proj.AddTrack('V2');
|
||
Proj.AddTrack('A1');
|
||
Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
|
||
Proj.Master := Info;
|
||
{ Клипы 0..10 на первой и третьей дорожках, на второй — 20..30:
|
||
под курсором её нет, и резать там нечего. }
|
||
Proj.AddClip(Proj.Tracks[0], Media, 0, 10, 0);
|
||
Proj.AddClip(Proj.Tracks[1], Media, 0, 10, 20);
|
||
Proj.AddClip(Proj.Tracks[2], Media, 0, 10, 0);
|
||
|
||
Timeline.Project := Proj;
|
||
Timeline.PixelsPerSecond := 100;
|
||
Timeline.ActiveTrack := 1;
|
||
Timeline.Playhead := 4;
|
||
|
||
CheckTrue('разрезаны обе задетые дорожки',
|
||
Timeline.SplitAtPlayhead(False) = 2);
|
||
CheckTrue('на первой дорожке два клипа', Proj.Tracks[0].Count = 2);
|
||
CheckTrue('на звуковой дорожке два клипа', Proj.Tracks[2].Count = 2);
|
||
CheckTrue('нетронутая дорожка осталась с одним клипом',
|
||
Proj.Tracks[1].Count = 1);
|
||
|
||
{ Курсор ровно на стыке — резать нечего, и клипов нулевой длины
|
||
появиться не должно. }
|
||
CheckTrue('на стыке рез отвечает нулём',
|
||
Timeline.SplitAtPlayhead(False) = 0);
|
||
CheckTrue('лишних клипов не появилось',
|
||
(Proj.Tracks[0].Count = 2) and (Proj.Tracks[2].Count = 2));
|
||
finally
|
||
Timeline.Free;
|
||
Proj.Free;
|
||
end;
|
||
end;
|
||
|
||
{ Схлопывание дыры не требует попадать в тайминг.
|
||
|
||
Курсор может стоять не точно на краю дыры: во время воспроизведения он
|
||
останавливается там, где успел, а после перезагрузки предпросмотра —
|
||
где оказался плеер. Раньше дыра при этом не находилась. }
|
||
procedure CheckGapTolerance;
|
||
var
|
||
Proj: TProject;
|
||
Info: TMediaInfo;
|
||
Media: TMediaItem;
|
||
Timeline: TTimelineControl;
|
||
begin
|
||
WriteLn;
|
||
WriteLn('Схлопывание дыры с допуском');
|
||
WriteLn;
|
||
|
||
Info := TestInfo;
|
||
Proj := TProject.Create;
|
||
Timeline := TTimelineControl.Create(nil);
|
||
try
|
||
Proj.Clear;
|
||
Proj.AddTrack('V1');
|
||
Proj.AddTrack('A1');
|
||
Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
|
||
Proj.Master := Info;
|
||
{ Дыра на картинке с 5 по 12. У звука дыры нет вовсе. }
|
||
Proj.AddClip(Proj.Tracks[0], Media, 0, 5, 0);
|
||
Proj.AddClip(Proj.Tracks[0], Media, 20, 5, 12);
|
||
Proj.AddClip(Proj.Tracks[1], Media, 0, 17, 0);
|
||
|
||
Timeline.Project := Proj;
|
||
Timeline.PixelsPerSecond := 100;
|
||
{ Указатель и выбранная дорожка — звуковая, дыры на ней нет. }
|
||
Timeline.ActiveTrack := 1;
|
||
{ Курсор не в дыре, а на четыре сотых раньше её начала. }
|
||
Timeline.Playhead := 4.96;
|
||
|
||
CheckTrue('дыра найдена, хотя курсор чуть раньше края',
|
||
Timeline.RippleCloseGap);
|
||
CheckTrue('названа дорожка, где схлопнули',
|
||
Timeline.LastGapTrack = 'V1');
|
||
CheckTrue('клип придвинут вплотную',
|
||
Abs(Proj.Tracks[0][1].TlStart - 5) < 0.001);
|
||
|
||
{ Курсор далеко от дыры — ничего не двигаем. }
|
||
Proj.Tracks[0][1].TlStart := 12;
|
||
Timeline.Playhead := 2;
|
||
CheckTrue('далеко от дыры — не схлопываем',
|
||
not Timeline.RippleCloseGap);
|
||
finally
|
||
Timeline.Free;
|
||
Proj.Free;
|
||
end;
|
||
end;
|
||
|
||
{ Починка проекта: клипы на кадровую сетку.
|
||
|
||
Между кадрами времени нет, и монтаж не должен хранить доли кадра:
|
||
картинка выйдет целым числом кадров, звук наберётся до отсчёта, и
|
||
округление разведёт их. Правка двигает клип не больше чем на полкадра,
|
||
но сама сетка обязана получиться ровной. }
|
||
procedure CheckFrameGridRepair;
|
||
var
|
||
Proj: TProject;
|
||
Info: TMediaInfo;
|
||
Media: TMediaItem;
|
||
Step: Double;
|
||
I: Integer;
|
||
Clip: TClip;
|
||
OnGrid: Boolean;
|
||
begin
|
||
WriteLn;
|
||
WriteLn('Починка: клипы на кадровую сетку');
|
||
WriteLn;
|
||
|
||
Info := TestInfo;
|
||
{ 59.94 к/с — та самая сетка, на которой доли кадра особенно заметны. }
|
||
Info.FpsNum := 60000;
|
||
Info.FpsDen := 1001;
|
||
|
||
Proj := TProject.Create;
|
||
try
|
||
Proj.Clear;
|
||
Proj.AddTrack('V1');
|
||
Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
|
||
Proj.Master := Info;
|
||
Step := Info.FpsDen / Info.FpsNum;
|
||
|
||
{ Времена как в настоящем проекте: ни одно не попадает на кадр. }
|
||
Proj.AddClip(Proj.Tracks[0], Media, 48.558521, 2.457599, 0);
|
||
Proj.AddClip(Proj.Tracks[0], Media, 56.054198, 1.469934, 2.457599);
|
||
{ И клип короче кадра: его нельзя схлопнуть в ничто. }
|
||
Proj.AddClip(Proj.Tracks[0], Media, 62.710196, 0.004, 3.927533);
|
||
|
||
CheckTrue('все три клипа мимо сетки', Proj.OffGridClipCount = 3);
|
||
CheckTrue('починка подвинула три клипа', Proj.SnapClipsToFrameGrid = 3);
|
||
CheckTrue('мимо сетки не осталось', Proj.OffGridClipCount = 0);
|
||
|
||
OnGrid := True;
|
||
for I := 0 to Proj.Tracks[0].Count - 1 do
|
||
begin
|
||
Clip := Proj.Tracks[0][I];
|
||
if Abs(Clip.TlStart / Step - Round(Clip.TlStart / Step)) > 0.001 then
|
||
OnGrid := False;
|
||
if Abs(Clip.Duration / Step - Round(Clip.Duration / Step)) > 0.001 then
|
||
OnGrid := False;
|
||
if Abs(Clip.SrcIn / Step - Round(Clip.SrcIn / Step)) > 0.001 then
|
||
OnGrid := False;
|
||
end;
|
||
CheckTrue('начала, длины и точки входа — целые кадры', OnGrid);
|
||
|
||
CheckTrue('короткий клип остался длиной в кадр',
|
||
Abs(Proj.Tracks[0][2].Duration - Step) < 0.0001);
|
||
CheckTrue('клипы не разъехались больше чем на полкадра',
|
||
Abs(Proj.Tracks[0][0].TlStart - 0) < Step);
|
||
CheckTrue('повторная починка ничего не двигает',
|
||
Proj.SnapClipsToFrameGrid = 0);
|
||
finally
|
||
Proj.Free;
|
||
end;
|
||
end;
|
||
|
||
{ Рез и курсор: где режет S и куда после этого встаёт курсор.
|
||
|
||
Раньше курсор оставался там, где был, а рез мог попасть на долю кадра.
|
||
Теперь рез кладётся на кадровую сетку, а курсор встаёт ровно на место
|
||
реза — с ним и работают дальше. }
|
||
procedure CheckSplitAndPlayhead;
|
||
var
|
||
Proj: TProject;
|
||
Info: TMediaInfo;
|
||
Media: TMediaItem;
|
||
Timeline: TTimelineControl;
|
||
Step: Double;
|
||
I, N: Integer;
|
||
begin
|
||
WriteLn;
|
||
WriteLn('Рез, промежуток и курсор');
|
||
WriteLn;
|
||
|
||
Info := TestInfo;
|
||
Info.FpsNum := 60000;
|
||
Info.FpsDen := 1001;
|
||
Step := Info.FpsDen / Info.FpsNum;
|
||
|
||
Proj := TProject.Create;
|
||
Timeline := TTimelineControl.Create(nil);
|
||
try
|
||
Proj.Clear;
|
||
Proj.AddTrack('V1');
|
||
Proj.AddTrack('A1');
|
||
Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
|
||
Proj.Master := Info;
|
||
{ По два клипа на каждой дорожке, встык: 0..10 и 10..20. }
|
||
Proj.AddClip(Proj.Tracks[0], Media, 0, 10, 0);
|
||
Proj.AddClip(Proj.Tracks[0], Media, 20, 10, 10);
|
||
Proj.AddClip(Proj.Tracks[1], Media, 0, 10, 0);
|
||
Proj.AddClip(Proj.Tracks[1], Media, 20, 10, 10);
|
||
|
||
Timeline.Project := Proj;
|
||
Timeline.PixelsPerSecond := 100;
|
||
|
||
{ Рез по курсору: место кладётся на кадр, курсор встаёт на него. }
|
||
Timeline.Playhead := 3.3177;
|
||
CheckTrue('разрезаны обе дорожки', Timeline.SplitAtPlayhead(False) = 2);
|
||
CheckTrue('курсор встал на место реза',
|
||
Abs(Timeline.Playhead - Round(3.3177 / Step) * Step) < 0.0001);
|
||
CheckTrue('курсор на кадровой сетке',
|
||
Abs(Timeline.Playhead / Step - Round(Timeline.Playhead / Step)) < 0.001);
|
||
N := 0;
|
||
for I := 0 to Proj.Tracks[0].Count - 1 do
|
||
if Abs(Proj.Tracks[0][I].TlStart - Timeline.Playhead) < 0.0001 then
|
||
Inc(N);
|
||
CheckTrue('рез пришёлся ровно туда, где курсор', N = 1);
|
||
|
||
{ Промежуток по времени: рез по обоим краям, курсор — на начало. }
|
||
Timeline.SelectionMode := smRange;
|
||
Timeline.SetSelectionRange(12.13, 16.77);
|
||
CheckTrue('промежуток отмечен', Timeline.HasSelectionRange);
|
||
CheckTrue('разрезано по два клипа на дорожке',
|
||
Timeline.SplitAtPlayhead(False) = 4);
|
||
CheckTrue('курсор — на начале промежутка',
|
||
Abs(Timeline.Playhead - Timeline.RangeFrom) < 0.0001);
|
||
|
||
N := 0;
|
||
for I := 0 to Proj.Tracks[1].Count - 1 do
|
||
if (Abs(Proj.Tracks[1][I].TlStart - Timeline.RangeFrom) < 0.0001) or
|
||
(Abs(Proj.Tracks[1][I].TlStart - Timeline.RangeTo) < 0.0001) then
|
||
Inc(N);
|
||
CheckTrue('на звуковой дорожке резы по обоим краям', N = 2);
|
||
|
||
{ После реза выделена середина — то, что между краями: следующим
|
||
действием её обычно удаляют. }
|
||
N := 0;
|
||
for I := 0 to Proj.Tracks[0].Count - 1 do
|
||
if Proj.Tracks[0][I].Selected then
|
||
begin
|
||
Inc(N);
|
||
CheckTrue('выделенное лежит внутри промежутка',
|
||
(Proj.Tracks[0][I].TlStart >= Timeline.RangeFrom - 0.0001) and
|
||
(Proj.Tracks[0][I].TlEnd <= Timeline.RangeTo + 0.0001));
|
||
end;
|
||
CheckTrue('на дорожке картинки выделена одна середина', N = 1);
|
||
|
||
{ Смена режима снимает промежуток: резать по невидимой метке нельзя. }
|
||
Timeline.SelectionMode := smClips;
|
||
CheckTrue('смена режима сняла промежуток',
|
||
not Timeline.HasSelectionRange);
|
||
|
||
{ Удаление со схлопыванием: курсор встаёт на место стыка. }
|
||
Proj.Clear;
|
||
Proj.AddTrack('V1');
|
||
Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
|
||
Proj.Master := Info;
|
||
Proj.RippleEnabled := True;
|
||
Proj.AddClip(Proj.Tracks[0], Media, 0, 5, 0);
|
||
Proj.AddClip(Proj.Tracks[0], Media, 10, 5, 5);
|
||
Proj.AddClip(Proj.Tracks[0], Media, 20, 5, 10);
|
||
Timeline.Playhead := 0;
|
||
Proj.Tracks[0][1].Selected := True;
|
||
Timeline.DeleteSelected;
|
||
CheckTrue('после удаления клипов стало два', Proj.Tracks[0].Count = 2);
|
||
CheckTrue('курсор — на месте вырезанного куска',
|
||
Abs(Timeline.Playhead - 5) < 0.0001);
|
||
CheckTrue('хвост придвинут к стыку',
|
||
Abs(Proj.Tracks[0][1].TlStart - 5) < 0.0001);
|
||
|
||
{ Схлопывание дыры с клавиатуры: курсор тоже на стык. }
|
||
Proj.Clear;
|
||
Proj.AddTrack('V1');
|
||
Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
|
||
Proj.Master := Info;
|
||
Proj.AddClip(Proj.Tracks[0], Media, 0, 5, 0);
|
||
Proj.AddClip(Proj.Tracks[0], Media, 20, 5, 12);
|
||
Timeline.ActiveTrack := 0;
|
||
Timeline.Playhead := 8;
|
||
CheckTrue('дыра схлопнута', Timeline.RippleCloseGap);
|
||
CheckTrue('курсор — на стыке', Abs(Timeline.Playhead - 5) < 0.0001);
|
||
finally
|
||
Timeline.Free;
|
||
Proj.Free;
|
||
end;
|
||
end;
|
||
|
||
{ Кнопка нижней панели и быстрые щелчки.
|
||
|
||
Второй щелчок подряд оконная библиотека отдаёт не как щелчок, а как
|
||
двойной — и обработчик кнопки его не видел. Человек нажимал кнопку
|
||
дважды, а срабатывало один раз.
|
||
|
||
Проверяется двумя способами: у кнопки нет csDoubleClicks (тогда
|
||
двойных щелчков к ней вообще не приходит), и даже если двойной
|
||
придёт, он считается за щелчок. }
|
||
type
|
||
TButtonProbe = class(TThemedButton)
|
||
public
|
||
procedure Twice;
|
||
end;
|
||
|
||
procedure TButtonProbe.Twice;
|
||
begin
|
||
DblClick;
|
||
end;
|
||
|
||
var
|
||
ClickCount: Integer = 0;
|
||
|
||
type
|
||
TClickCounter = class
|
||
procedure Clicked(Sender: TObject);
|
||
end;
|
||
|
||
procedure TClickCounter.Clicked(Sender: TObject);
|
||
begin
|
||
Inc(ClickCount);
|
||
end;
|
||
|
||
{ Рамка источника и фильтр сборки должны говорить об одном.
|
||
|
||
Одна и та же настройка записана двумя способами: сборка растягивает
|
||
кадр и вырезает из него кусок, а окно кадрирования и предпросмотр
|
||
говорят, какая часть исходника видна. Разойдутся — окно начнёт обещать
|
||
не то, что покажет плеер, а плеер не то, что даст сборка. }
|
||
procedure CheckSourceRect;
|
||
var
|
||
T: TMediaTransform;
|
||
R: TRect;
|
||
Chain: string;
|
||
begin
|
||
WriteLn;
|
||
WriteLn('Рамка источника и фильтр сборки');
|
||
WriteLn;
|
||
|
||
T := DefaultTransform;
|
||
T.Zoom := 2.77;
|
||
|
||
CheckTrue('увеличение описывается рамкой',
|
||
TransformSourceRect(T, 2688, 1512, 2688, 1512, R));
|
||
{ Растянули в 2.77 раза — значит видна 1/2.77 часть кадра, посередине. }
|
||
CheckTrue('ширина рамки: ' + IntToStr(R.Right - R.Left),
|
||
Abs((R.Right - R.Left) - Round(2688 / 2.77)) <= 2);
|
||
CheckTrue('высота рамки: ' + IntToStr(R.Bottom - R.Top),
|
||
Abs((R.Bottom - R.Top) - Round(1512 / 2.77)) <= 2);
|
||
CheckTrue('рамка посередине по горизонтали',
|
||
Abs((R.Left + R.Right) - 2688) <= 3);
|
||
CheckTrue('рамка посередине по вертикали',
|
||
Abs((R.Top + R.Bottom) - 1512) <= 3);
|
||
|
||
{ Тот же расчёт с другой стороны: сборка растягивает до 7446x4188 и
|
||
вырезает 2688x1512 с отступом 2379:1338. Делим отступ на масштаб —
|
||
обязано выйти начало рамки. }
|
||
Chain := TransformFilter(T, 2688, 1512, 2688, 1512);
|
||
CheckTrue('сборка растягивает и вырезает: ' + Chain,
|
||
(Pos('scale=7446:4188', Chain) > 0) and
|
||
(Pos('crop=2688:1512:2379:1338', Chain) > 0));
|
||
CheckTrue('начало рамки совпадает с отступом обрезки',
|
||
Abs(R.Left - Round(2379 / 2.77)) <= 2);
|
||
|
||
{ Растяжение рамкой не описывается: там кадр жмётся неравномерно. }
|
||
T := DefaultTransform;
|
||
T.Fit := fmStretch;
|
||
CheckTrue('растяжение рамкой не описывается',
|
||
not TransformSourceRect(T, 2688, 1512, 1920, 1080, R));
|
||
|
||
{ Отражение обрезкой на выводе не показать — свойства для него нет. }
|
||
T := DefaultTransform;
|
||
T.FlipH := True;
|
||
CheckTrue('отражение обрезкой на выводе не показывается',
|
||
not TransformFitsOutputCrop(T));
|
||
T := DefaultTransform;
|
||
T.Rotation := 90;
|
||
CheckTrue('поворот показать можно: у него своё свойство',
|
||
TransformFitsOutputCrop(T));
|
||
end;
|
||
|
||
{ Сведение слоёв просит распаковку у видеокарты.
|
||
|
||
На длинном затухании это разница между тремя минутами и одной, а
|
||
ключ входной: ffmpeg принимает его только перед тем входом, к которому
|
||
он относится. Забыть его легко и незаметно — команда останется рабочей,
|
||
просто медленной. }
|
||
procedure CheckLayerHwDecode;
|
||
var
|
||
Layers: array[0..0] of TSegmentLayer;
|
||
A: TArgs;
|
||
I, Hw, Input: Integer;
|
||
begin
|
||
WriteLn;
|
||
WriteLn('Распаковка слоёв видеокартой');
|
||
WriteLn;
|
||
|
||
Layers[0] := Default(TSegmentLayer);
|
||
Layers[0].FilePath := '/несуществующий/видео.mp4';
|
||
Layers[0].SrcStart := 10;
|
||
Layers[0].FadeFrom := 1;
|
||
Layers[0].FadeTo := 0;
|
||
|
||
A := ArgsRenderLayers(Layers, '/выход.mkv', 70, 2688, 1512, 60000, 1001,
|
||
'yuv420p', 'hevc', False, 0, '', NotPiped, False);
|
||
Hw := -1;
|
||
for I := 0 to High(A) do
|
||
if A[I] = '-hwaccel' then
|
||
Hw := I;
|
||
CheckTrue('без просьбы ключа нет', Hw < 0);
|
||
|
||
A := ArgsRenderLayers(Layers, '/выход.mkv', 70, 2688, 1512, 60000, 1001,
|
||
'yuv420p', 'hevc', False, 0, '', NotPiped, True);
|
||
Hw := -1;
|
||
Input := -1;
|
||
for I := 0 to High(A) do
|
||
begin
|
||
if A[I] = '-hwaccel' then
|
||
Hw := I;
|
||
if (A[I] = '-i') and (Input < 0) then
|
||
Input := I;
|
||
end;
|
||
|
||
{ Просьба выполняется только там, где кодировать будет та же карта. На
|
||
машине без неё проверять нечего, и это не поломка. }
|
||
if Pos('nvenc', PreferredEncoder('hevc')) = 0 then
|
||
WriteLn(' -- видеокарты для кодирования нет, проверять нечего')
|
||
else
|
||
begin
|
||
CheckTrue('по просьбе ключ появляется', Hw >= 0);
|
||
CheckTrue('и стоит перед входом, а не после', (Hw >= 0) and (Hw < Input));
|
||
end;
|
||
end;
|
||
|
||
procedure CheckButtonClicks;
|
||
var
|
||
Btn: TButtonProbe;
|
||
Counter: TClickCounter;
|
||
begin
|
||
WriteLn;
|
||
WriteLn('Кнопки нижней панели');
|
||
WriteLn;
|
||
|
||
Counter := TClickCounter.Create;
|
||
Btn := TButtonProbe.Create(nil);
|
||
try
|
||
Btn.OnClick := @Counter.Clicked;
|
||
CheckTrue('двойные щелчки кнопке не приходят',
|
||
not (csDoubleClicks in Btn.ControlStyle));
|
||
ClickCount := 0;
|
||
Btn.Twice;
|
||
CheckTrue('двойной щелчок считается за щелчок', ClickCount = 1);
|
||
finally
|
||
Btn.Free;
|
||
Counter.Free;
|
||
end;
|
||
end;
|
||
|
||
begin
|
||
WriteLn('Свойства из файлов форм');
|
||
WriteLn;
|
||
|
||
CheckProp(TThemedButton, 'Icon');
|
||
CheckProp(TThemedButton, 'Down');
|
||
CheckProp(TThemedButton, 'Arrow');
|
||
CheckProp(TThemedButton, 'Caption');
|
||
CheckProp(TThemedButton, 'ShowHint');
|
||
|
||
CheckEnum(TypeInfo(TThemedIcon), 'tiSnap');
|
||
CheckEnum(TypeInfo(TThemedIcon), 'tiRipple');
|
||
CheckEnum(TypeInfo(TThemedIcon), 'tiLog');
|
||
CheckEnum(TypeInfo(TThemedIcon), 'tiNone');
|
||
|
||
WriteLn;
|
||
WriteLn('Загрузка файлов форм');
|
||
WriteLn;
|
||
Application.Initialize;
|
||
CheckForm(TPanCropForm);
|
||
CheckForm(TDepsForm);
|
||
CheckForm(TMismatchForm);
|
||
CheckForm(TProgressForm);
|
||
CheckForm(TProjectInfoForm);
|
||
CheckForm(TRemuxForm);
|
||
|
||
CheckPlayheadSnap;
|
||
CheckSplitKeepsSettings;
|
||
CheckRippleWithoutMouse;
|
||
CheckClipCopy;
|
||
CheckRubberBand;
|
||
CheckFrameGridRepair;
|
||
CheckSplitAndPlayhead;
|
||
CheckSplitAllTracks;
|
||
CheckGapTolerance;
|
||
CheckSourceRect;
|
||
CheckLayerHwDecode;
|
||
CheckButtonClicks;
|
||
|
||
WriteLn;
|
||
if Failed = 0 then
|
||
WriteLn('ВЕРДИКТ: все проверки пройдены')
|
||
else
|
||
begin
|
||
WriteLn('ВЕРДИКТ: не пройдено проверок: ', Failed);
|
||
Halt(1);
|
||
end;
|
||
end.
|