Наезд двух клипов на одной дорожке — заказ на переход, то есть обычное дело. Нажатие же всегда попадало в верхний по списку, и нижний клип нельзя было ни выделить, ни потащить: чтобы до него добраться, приходилось сперва убирать верхний. Правило такое. Нажатие берёт тот из накрытых клипов, который уже выделен, иначе верхний. Щелчок без движения по уже выделенному переводит выделение на следующий, после последнего снова на верхний. Переключение сделано на отпускании намеренно: пока кнопку не отпустили, неизвестно, щелчок это или начало протяжки, а менять клип под рукой посреди протяжки нельзя. Благодаря этому протяжка тащит именно то, что перед ней выбрали щелчками. Строка состояния говорит, сколько клипов под указателем: перебор, о котором не сказали, — тайное знание. Отбор вынесен в TTrack.ClipsAt, числовая проверка — в lfmcheck. tests/pickcycle.sh щёлкает дважды и тащит: на прежней сборке уезжает верхний клип, а нижний едет за ним схлопыванием.
1227 lines
48 KiB
ObjectPascal
1227 lines
48 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 CheckMakeRoom;
|
||
var
|
||
Proj: TProject;
|
||
Info: TMediaInfo;
|
||
Media: TMediaItem;
|
||
Timeline: TTimelineControl;
|
||
Moved, I: Integer;
|
||
Gaps, Overlaps: Integer;
|
||
Step, At, Shift: Double;
|
||
Track: TTrack;
|
||
begin
|
||
WriteLn;
|
||
WriteLn('Раздвижка монтажа под вставку');
|
||
WriteLn;
|
||
|
||
Info := TestInfo;
|
||
Info.FpsNum := 60000;
|
||
Info.FpsDen := 1001;
|
||
|
||
Proj := TProject.Create;
|
||
Timeline := TTimelineControl.Create(nil);
|
||
try
|
||
Proj.Clear;
|
||
Proj.AddTrack('V1');
|
||
Proj.AddTrackOfKind('A1', tkAudio);
|
||
Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
|
||
Proj.Master := Info;
|
||
{ Три клипа встык на картинке и столько же на звуке: 0..10, 10..20,
|
||
20..30. }
|
||
for I := 0 to 2 do
|
||
begin
|
||
Proj.AddClip(Proj.Tracks[0], Media, 0, 10, I * 10);
|
||
Proj.AddClip(Proj.Tracks[1], Media, 0, 10, I * 10);
|
||
end;
|
||
Timeline.Project := Proj;
|
||
|
||
{ Вставляем пять секунд посреди второго клипа.
|
||
|
||
Ожидаемое считается по той же кадровой сетке, а не круглыми
|
||
числами: при 60000/1001 ни 15, ни 5 на кадр не попадают, и точка
|
||
реза с величиной сдвига садятся на ближайшие кадры. }
|
||
Step := Info.FpsDen / Info.FpsNum;
|
||
At := Round(15 / Step) * Step;
|
||
Shift := Round(5 / Step) * Step;
|
||
Moved := Timeline.MakeRoomAt(15, 5);
|
||
CheckTrue('что-то отодвинуто: ' + IntToStr(Moved), Moved >= 4);
|
||
CheckTrue('второй клип разошёлся надвое: клипов стало ' +
|
||
IntToStr(Proj.Tracks[0].Count), Proj.Tracks[0].Count = 4);
|
||
CheckTrue('звук разошёлся так же', Proj.Tracks[1].Count = 4);
|
||
|
||
{ Голова второго клипа осталась на месте, хвост уехал ровно на пять. }
|
||
CheckTrue('голова на месте: ' + SecondsToStr(Proj.Tracks[0][1].TlStart),
|
||
Abs(Proj.Tracks[0][1].TlStart - 10) < 0.001);
|
||
CheckTrue('хвост отъехал на длину вставки: ' +
|
||
SecondsToStr(Proj.Tracks[0][2].TlStart),
|
||
Abs(Proj.Tracks[0][2].TlStart - (At + Shift)) < 0.001);
|
||
CheckTrue('третий клип отъехал на столько же: ' +
|
||
SecondsToStr(Proj.Tracks[0][3].TlStart),
|
||
Abs(Proj.Tracks[0][3].TlStart - (20 + Shift)) < 0.001);
|
||
CheckTrue('звук уехал вместе с картинкой',
|
||
Abs(Proj.Tracks[1][2].TlStart - (At + Shift)) < 0.001);
|
||
CheckTrue('освобождено ровно столько, сколько просили: ' +
|
||
SecondsToStr(Proj.Tracks[0][2].TlStart - Proj.Tracks[0][1].TlEnd),
|
||
Abs((Proj.Tracks[0][2].TlStart - Proj.Tracks[0][1].TlEnd) - Shift)
|
||
< 0.001);
|
||
|
||
{ В освобождённом месте никого нет, и больше нигде не появилось ни
|
||
дыр, ни наездов. }
|
||
CheckTrue('место освобождено', Proj.Tracks[0].ClipAt(At + 1) = nil);
|
||
Gaps := 0;
|
||
Overlaps := 0;
|
||
for I := 0 to Proj.TrackCount - 1 do
|
||
begin
|
||
Track := Proj.Tracks[I];
|
||
for Moved := 0 to Track.Count - 2 do
|
||
if Track[Moved].TlEnd > Track[Moved + 1].TlStart + 0.001 then
|
||
Inc(Overlaps)
|
||
else if (Track[Moved].TlEnd < Track[Moved + 1].TlStart - 0.001) and
|
||
(Abs(Track[Moved].TlEnd - At) > 0.001) then
|
||
Inc(Gaps);
|
||
end;
|
||
CheckTrue('наездов не появилось', Overlaps = 0);
|
||
CheckTrue('лишних дыр не появилось', Gaps = 0);
|
||
|
||
{ Вставка в свободное место за концом монтажа ничего не двигает. }
|
||
Moved := Timeline.MakeRoomAt(100, 5);
|
||
CheckTrue('за концом монтажа двигать нечего', Moved = 0);
|
||
finally
|
||
Timeline.Free;
|
||
Proj.Free;
|
||
end;
|
||
end;
|
||
|
||
{ В месте наезда под указателем несколько клипов.
|
||
|
||
Наезд двух клипов на одной дорожке — заказ на переход, то есть обычное
|
||
дело. Пока отбор возвращал только первый по списку, нижний клип нельзя
|
||
было ни выделить, ни потащить. }
|
||
procedure CheckClipsAt;
|
||
var
|
||
Proj: TProject;
|
||
Info: TMediaInfo;
|
||
Media: TMediaItem;
|
||
A, B: TClip;
|
||
Got: TClipArray;
|
||
begin
|
||
WriteLn;
|
||
WriteLn('Клипы под одной точкой');
|
||
WriteLn;
|
||
|
||
Info := TestInfo;
|
||
Proj := TProject.Create;
|
||
try
|
||
Proj.Clear;
|
||
Proj.AddTrack('V1');
|
||
Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
|
||
Proj.Master := Info;
|
||
{ Второй наехал на первый на две секунды: 0..10 и 8..18. }
|
||
A := Proj.AddClip(Proj.Tracks[0], Media, 0, 10, 0);
|
||
B := Proj.AddClip(Proj.Tracks[0], Media, 0, 10, 8);
|
||
|
||
Got := Proj.Tracks[0].ClipsAt(9);
|
||
CheckTrue('в наезде под точкой двое: ' + IntToStr(Length(Got)),
|
||
Length(Got) = 2);
|
||
if Length(Got) = 2 then
|
||
begin
|
||
CheckTrue('первым идёт верхний по списку', Got[0] = A);
|
||
CheckTrue('вторым — тот, что под ним', Got[1] = B);
|
||
end;
|
||
|
||
Got := Proj.Tracks[0].ClipsAt(3);
|
||
CheckTrue('вне наезда клип один: ' + IntToStr(Length(Got)),
|
||
(Length(Got) = 1) and (Got[0] = A));
|
||
|
||
Got := Proj.Tracks[0].ClipsAt(100);
|
||
CheckTrue('за концом монтажа никого', Length(Got) = 0);
|
||
|
||
{ Прежний отбор остаётся прежним: он берёт верхний. }
|
||
CheckTrue('ClipAt по-прежнему даёт верхний',
|
||
Proj.Tracks[0].ClipAt(9) = A);
|
||
finally
|
||
Proj.Free;
|
||
end;
|
||
end;
|
||
|
||
{ Клип притягивается к границе и хвостом, а не только головой.
|
||
|
||
Подводят кусок к началу соседа слева: голова встаёт в свободное место,
|
||
а хвост при этом накрывает соседа. Наезд на одной дорожке — это заказ
|
||
на переход, то есть минуты сборки из-за промаха на полпикселя. }
|
||
procedure CheckSnapTail;
|
||
var
|
||
Proj: TProject;
|
||
Info: TMediaInfo;
|
||
Media: TMediaItem;
|
||
Timeline: TTimelineControl;
|
||
Mover: TClip;
|
||
Got: Double;
|
||
begin
|
||
WriteLn;
|
||
WriteLn('Прилипание хвостом');
|
||
WriteLn;
|
||
|
||
Info := TestInfo;
|
||
Info.FpsNum := 60000;
|
||
Info.FpsDen := 1001;
|
||
|
||
Proj := TProject.Create;
|
||
Timeline := TTimelineControl.Create(nil);
|
||
try
|
||
Proj.Clear;
|
||
Proj.AddTrack('V1');
|
||
Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
|
||
Proj.Master := Info;
|
||
{ Сосед занимает 20..30, двигаемый длиной 4 стоит пока в сторонке. }
|
||
Proj.AddClip(Proj.Tracks[0], Media, 0, 10, 20);
|
||
Mover := Proj.AddClip(Proj.Tracks[0], Media, 0, 4, 50);
|
||
Timeline.Project := Proj;
|
||
Timeline.PixelsPerSecond := 10; { допуск прилипания — 1.2 с }
|
||
|
||
{ Голова метит в 15.7, хвост при этом пришёлся бы на 19.7 — почти
|
||
встык с соседом. Правильный ответ: 16, тогда хвост ровно на 20. }
|
||
Got := Timeline.SnapTime(15.7, Mover, Mover.Duration);
|
||
CheckTrue('хвост сел на начало соседа: ' + SecondsToStr(Got),
|
||
Abs(Got - 16) < 0.001);
|
||
|
||
{ Голова у границы по-прежнему липнет головой: там ближе она. }
|
||
Got := Timeline.SnapTime(19.8, Mover, Mover.Duration);
|
||
CheckTrue('голова села на начало соседа: ' + SecondsToStr(Got),
|
||
Abs(Got - 20) < 0.001);
|
||
|
||
{ Без длины поведение прежнее: хвост не рассматривается. }
|
||
Got := Timeline.SnapTime(15.7, Mover);
|
||
CheckTrue('без длины притяжения хвостом нет: ' + SecondsToStr(Got),
|
||
Abs(Got - 15.7) < 0.001);
|
||
|
||
{ Хвост липнет и к концу соседа: клип встаёт вплотную справа. }
|
||
Got := Timeline.SnapTime(25.8, Mover, Mover.Duration);
|
||
CheckTrue('хвост сел на конец соседа: ' + SecondsToStr(Got),
|
||
Abs(Got - 26) < 0.001);
|
||
finally
|
||
Timeline.Free;
|
||
Proj.Free;
|
||
end;
|
||
end;
|
||
|
||
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 принимает его только перед тем входом, к которому
|
||
он относится. Забыть его легко и незаметно — команда останется рабочей,
|
||
просто медленной. }
|
||
{ Разбор строк о ходе работы ffmpeg.
|
||
|
||
Ход приходит блоками, и последняя строка блока — «progress=continue».
|
||
Места в ней нет: она сообщает только, что работа продолжается. Разбор
|
||
обязан оставить переданное число как было, а вызывающий — передавать
|
||
туда уже известное место, а не ноль. Иначе каждый блок заканчивается
|
||
обнулением доли, и показатель стоит на месте, дёргаясь вверх лишь
|
||
тогда, когда опрос попадёт между строкой со временем и этой. }
|
||
procedure CheckProgressParsing;
|
||
var
|
||
Seconds: Double;
|
||
AtEnd: Boolean;
|
||
begin
|
||
WriteLn;
|
||
WriteLn('Разбор хода работы ffmpeg');
|
||
WriteLn;
|
||
|
||
Seconds := 0;
|
||
AtEnd := False;
|
||
CheckTrue('время узнаётся', ParseProgressLine('out_time_us=2500000',
|
||
Seconds, AtEnd));
|
||
CheckTrue('и разбирается верно: ' + FloatToStrF(Seconds, ffFixed, 8, 3),
|
||
Abs(Seconds - 2.5) < 0.001);
|
||
|
||
CheckTrue('«продолжается» — тоже знакомая строка',
|
||
ParseProgressLine('progress=continue', Seconds, AtEnd));
|
||
CheckTrue('и она не трогает время: ' + FloatToStrF(Seconds, ffFixed, 8, 3),
|
||
Abs(Seconds - 2.5) < 0.001);
|
||
CheckTrue('и не объявляет конец', not AtEnd);
|
||
|
||
CheckTrue('конец узнаётся', ParseProgressLine('progress=end',
|
||
Seconds, AtEnd) and AtEnd);
|
||
CheckTrue('время и тут осталось прежним',
|
||
Abs(Seconds - 2.5) < 0.001);
|
||
|
||
CheckTrue('незнакомая строка не узнаётся',
|
||
not ParseProgressLine('bitrate= 12.3kbits/s', Seconds, AtEnd));
|
||
end;
|
||
|
||
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;
|
||
CheckMakeRoom;
|
||
CheckClipsAt;
|
||
CheckSnapTail;
|
||
CheckSplitAllTracks;
|
||
CheckGapTolerance;
|
||
CheckSourceRect;
|
||
CheckProgressParsing;
|
||
CheckLayerHwDecode;
|
||
CheckButtonClicks;
|
||
|
||
WriteLn;
|
||
if Failed = 0 then
|
||
WriteLn('ВЕРДИКТ: все проверки пройдены')
|
||
else
|
||
begin
|
||
WriteLn('ВЕРДИКТ: не пройдено проверок: ', Failed);
|
||
Halt(1);
|
||
end;
|
||
end.
|