Предпросмотр показывает кадрирование клипа

Клип со своим кадрированием играл в окне во весь кадр: цепочка фильтров
у mpv одна на весь список кусков, а включить её на одном куске и
выключить на следующем нечем — из фильтров ffmpeg по времени
переключается только наложение.

Обошлось без цепочки вовсе. У вывода mpv своя обрезка, и делает её
видеокарта: курсор зашёл в клип с кадрированием — выставили, вышел —
сняли. Ни перестройки цепочки, ни копирования кадров из памяти
видеокарты в обычную.

Рамку источника считает теперь один код на две стороны: по ней окно
«Кадрирование и поворот» рисует рамку, и её же плеер отдаёт как обрезку.
Проверка в tests/lfmcheck сверяет рамку с фильтром сборки: увеличение
2.77 даёт 970x546 посередине, и это ровно то, что вырезает
crop=2688:1512:2379:1338 после scale=7446:4188.

Проверено глазами на невидимом экране, на настоящем материале с дрона:
картинка в окне совпала с готовым роликом, на границе с обычным клипом
обрезка снимается и возвращается. Сверено со сборкой из прошлого
коммита — там на том же месте кадр без кадрирования.

Не покрыто и записано в BACKLOG: отражение (свойства для него нет),
растяжение и участки с наложением — там кадр уже сведён цепочкой.
This commit is contained in:
4aiman 2026-10-01 00:04:57 +03:00
commit 235cd00e9a
8 changed files with 329 additions and 70 deletions

View file

@ -76,46 +76,35 @@ ffmpeg проблемой не является: он запускается о
MP4/H.264 нельзя — контейнер и кодек альфу не хранят. Для этого нужен MP4/H.264 нельзя — контейнер и кодек альфу не хранят. Для этого нужен
отдельный вывод в VP9 или ProRes 4444. отдельный вывод в VP9 или ProRes 4444.
## Предпросмотр не показывает кадрирование обычного клипа ## Предпросмотр: чего он про кадрирование всё ещё не показывает
Кадрирование и поворот видны в окне только у клипов с ключом или Кадрирование обычного клипа окно теперь показывает — обрезкой на выводе
прозрачностью: их плеер кладёт поверх основы сам и применяет к ним плеера, её делает видеокарта даром. Остались три случая, где оно
цепочку фильтров. Обычный клип идёт списком кусков, а список фильтровать по-прежнему играется как есть, и для них точную картинку даёт окно
нечем — он играется как есть, во весь кадр. В готовом ролике при этом всё «Кадрирование и поворот»:
верно: такой кусок помечен на пересборку и обкладывается полями.
Проверено опытом: у клипа выключен только ключ, кадрирование оставлено — **Отражение.** У вывода mpv нет свойства для отражения, только для
окно показывает его во весь кадр. поворота. Понадобится — придётся вести такой клип через цепочку.
Почему нельзя просто добавить фильтр. Цепочка у mpv одна на весь список **Растяжение.** Кадр жмётся неравномерно, рамкой это не описать.
кусков, а кадрирование задаётся каждому клипу своё. Включить фильтр на
одном куске и выключить на следующем нечем: из фильтров ffmpeg по времени
переключаются только те, у кого есть поддержка временной шкалы, а у
`crop` и `scale` её нет — есть только у `overlay`. Значит время включения
выражается единственным способом: раздвоить поток, одну ветку обрезать и
растянуть, и наложить её на другую с `enable='between(t,...)'`. Ветка при
этом работает на каждом кадре фильма, а не только там, где клип виден.
Замер (DJI 2688x1512, HEVC 55 Мбит/с, 600 кадров, `--vo=null`): **Участок с наложением.** Там кадр уже сведён цепочкой, и обрезка на
выводе резала бы сведённое целиком, вместе с нижним слоем. Такой клип
нужно вести через ту же цепочку, что и само наложение.
Цена пути через цепочку известна, мерили (DJI 2688x1512, HEVC
55 Мбит/с, 600 кадров, `--vo=null`):
| | время | кадров в секунду | | | время | кадров в секунду |
|---|---|---| |---|---|---|
| без фильтра | 1.82 с | 330 | | без фильтра | 1.82 с | 330 |
| ветка «обрезать и растянуть» с `enable` | 2.81 с | 214 | | ветка «обрезать и растянуть» с `enable` | 2.81 с | 214 |
Нужно 60, то есть запас трёхкратный: путь через цепочку по деньгам Нужно 60, то есть запас трёхкратный: по деньгам проходит. Устроена она
проходит. Раньше здесь было написано, что для 2688x1512 это уже не так: раздвоить поток, одну ветку обрезать и растянуть, и наложить её на
пустяк, — написано по памяти о замере вписывания, а не об этом случае. другую с `enable='between(t,...)'`. Иначе никак — из фильтров ffmpeg по
времени переключается только `overlay`, у `crop` и `scale` поддержки
Но есть путь дешевле. mpv умеет обрезать и увеличивать на выводе, силами временной шкалы нет.
видеокарты: свойства `video-crop`, `video-zoom`, `video-pan-x/y`,
`video-rotate` есть в нашей версии 0.37. Они действуют на всё окно сразу,
но плеером управляем мы и знаем, где курсор: зашли в клип с кадрированием
— выставили, вышли — сняли. Ни цепочки, ни копирования кадров из памяти
видеокарты в обычную. Ограничения: одно преобразование на всё окно, так
что при одновременном наложении всё равно нужна цепочка; отражения таким
свойством не задать; переключение на границе клипа придёт с запозданием в
кадр-другой.
## Два видео рядом в одном кадре ## Два видео рядом в одном кадре

View file

@ -403,11 +403,22 @@ Ctrl+Shift+R, который схлопывает дыру под курсоро
Замер на материале 2688x1512 при 60 к/с: 327 кадров в секунду без фильтра, Замер на материале 2688x1512 при 60 к/с: 327 кадров в секунду без фильтра,
230 с настоящим масштабированием — запас четырёхкратный. 230 с настоящим масштабированием — запас четырёхкратный.
Чего предпросмотр не умеет: показать собственную настройку кадрирования **Кадрирование клипа окно показывает.** Цепочка фильтров у mpv одна на всю
файла. Цепочка фильтров у mpv одна на всю склейку, а заполнение с обрезкой, склейку, а кадрирование у каждого клипа своё, и включать его по времени в
поворот и смещение задаются для каждого файла отдельно — выбрать между ними цепочке нечем: из фильтров ffmpeg по времени переключается только
общий фильтр не может, поэтому он всегда вписывает целиком. Точную картинку наложение. Зато у вывода есть своя обрезка, и делает её видеокарта даром.
для такого файла показывает окно «Кадрирование и поворот». Курсор зашёл в клип со своим кадрированием — обрезка выставляется, вышел —
снимается. Ни лишней цепочки, ни копирования кадров из памяти видеокарты.
Рамку считает один и тот же код, что рисует её в окне «Кадрирование и
поворот», — иначе окно обещало бы одно, а предпросмотр показывал другое
(`TransformSourceRect` в `uTransform`, проверка в `tests/lfmcheck`).
Чего это не покрывает: отражение (свойства для него у вывода нет),
растяжение (там кадр жмётся неравномерно, рамкой не описать) и участки с
наложением — там кадр уже сведён цепочкой, и обрезка резала бы сведённое
целиком, вместе с нижним слоем. Для таких клипов точную картинку
по-прежнему показывает окно «Кадрирование и поворот».
## Цвета окна ## Цвета окна

View file

@ -167,6 +167,14 @@ function FlattenPreview(AProject: TProject;
Возвращает, сколько секунд осталось. } Возвращает, сколько секунд осталось. }
{ Клип, который виден в этот момент: с самой верхней включённой дорожки
нужного вида. Тот же отбор победителя, что и при уплощении.
Нужен предпросмотру: по нему решается, чьё кадрирование показывать
обрезкой на выводе плеера. }
function VisibleClipAtTime(AProject: TProject; AKind: TTrackKind;
const ATime: Double): TClip;
function ClipSegmentsToRange(var ASegments: TFlatSegmentArray; function ClipSegmentsToRange(var ASegments: TFlatSegmentArray;
const AFrom, ATo: Double): Double; const AFrom, ATo: Double): Double;
@ -341,6 +349,30 @@ begin
end; end;
end; end;
function VisibleClipAtTime(AProject: TProject; AKind: TTrackKind;
const ATime: Double): TClip;
var
Ti, C: Integer;
Track: TTrack;
begin
Result := nil;
if AProject = nil then
Exit;
for Ti := 0 to AProject.TrackCount - 1 do
begin
Track := AProject.Tracks[Ti];
if Track.Kind <> AKind then
Continue;
{ Выключенные дорожки пропускаются, а если где-то включено «соло» —
то и все несолирующие. }
if not AProject.TrackAudible(Track) then
Continue;
for C := 0 to Track.Count - 1 do
if Track[C].Covers(ATime) then
Exit(Track[C]);
end;
end;
function ClipSegmentsToRange(var ASegments: TFlatSegmentArray; function ClipSegmentsToRange(var ASegments: TFlatSegmentArray;
const AFrom, ATo: Double): Double; const AFrom, ATo: Double): Double;
var var

View file

@ -175,6 +175,8 @@ type
FPreviewSegments: TFlatSegmentArray; FPreviewSegments: TFlatSegmentArray;
FLastVolumeSent: Double; FLastVolumeSent: Double;
FLastVolumeKey: string; FLastVolumeKey: string;
{ Что сейчас показано обрезкой на выводе: лишний раз её не трогаем. }
FLastCropKey: string;
FGenerate: TGenerateThread; FGenerate: TGenerateThread;
FGenerateBusy: Boolean; FGenerateBusy: Boolean;
@ -235,6 +237,8 @@ type
FHintControl: TControl; FHintControl: TControl;
procedure ApplyVolumeAt(const ATime: Double); procedure ApplyVolumeAt(const ATime: Double);
{ Показывает кадрирование клипа, который виден сейчас. }
procedure ApplyCropAt(const ATime: Double);
{ Переписывает файл субтитров и отдаёт его плееру. } { Переписывает файл субтитров и отдаёт его плееру. }
procedure UpdateSubtitles; procedure UpdateSubtitles;
procedure UpdateRippleButton; procedure UpdateRippleButton;
@ -2404,6 +2408,15 @@ begin
FSeekPending := True; FSeekPending := True;
FSeekWaitTicks := 0; FSeekWaitTicks := 0;
{ Кадрирование выставляется заново после каждой загрузки.
Обрезка на выводе — свойство плеера, а не списка кусков: она
переживает загрузку и осталась бы от прошлого монтажа. А при открытии
проекта курсор не двигается вовсе, и без этого вызова первый клип
показывался бы без своего кадрирования до первого щелчка по монтажу. }
FLastCropKey := '';
ApplyCropAt(Pos);
SetStatus(Format('Предпросмотр: %d %s, %s с', SetStatus(Format('Предпросмотр: %d %s, %s с',
[Length(ASegments), Plural(Length(ASegments), 'кусок', 'куска', 'кусков'), [Length(ASegments), Plural(Length(ASegments), 'кусок', 'куска', 'кусков'),
SecondsToStr(TotalDuration(ASegments))])); SecondsToStr(TotalDuration(ASegments))]));
@ -3447,6 +3460,75 @@ begin
FPlayer.SetSubtitleFile(''); FPlayer.SetSubtitleFile('');
end; end;
{ Кадрирование клипа в предпросмотре.
Цепочка фильтров у mpv одна на весь список кусков, а кадрирование у
каждого клипа своё, и включать его по времени в цепочке нечем: из
фильтров ffmpeg по времени переключается только наложение. Зато у
вывода есть своя обрезка, и делает её видеокарта даром.
Раз она действует на весь вывод, переключать её обязаны мы: курсор
зашёл в клип со своим кадрированием — выставили, вышел — сняли.
Участки с наложением сюда не попадают: там кадр уже сведён цепочкой, и
обрезка на выводе резала бы сведённое целиком, вместе с нижним слоем.
Отражения обрезкой не выразить, растяжение — тоже: такие клипы
по-прежнему показываются как есть, точную картинку для них даёт окно
«Кадрирование и поворот». }
procedure TMainForm.ApplyCropAt(const ATime: Double);
var
I: Integer;
Clip: TClip;
Item: TMediaItem;
Sel: TRect;
Rotation: Integer;
Key: string;
begin
if (FPlayer = nil) or (not FPlayer.Initialized) or (FProject = nil) then
Exit;
Sel := Rect(0, 0, 0, 0);
Rotation := 0;
for I := 0 to High(FPreviewOverlayWindows) do
if (ATime >= FPreviewOverlayWindows[I].TlStart - 0.0005) and
(ATime <= FPreviewOverlayWindows[I].TlEnd + 0.0005) then
begin
FPlayer.SetOutputCrop(Sel, 0);
Exit;
end;
Clip := VisibleClipAtTime(FProject, tkVideo, ATime);
if (Clip <> nil) and (Clip.Kind = ckVideo) and
TransformFitsOutputCrop(Clip.Transform) then
begin
Item := FProject.FindMedia(Clip.MediaId);
if (Item <> nil) and Item.Info.Valid and
(Item.Info.Width > 0) and (Item.Info.Height > 0) then
begin
Rotation := Clip.Transform.Rotation;
if not TransformSourceRect(Clip.Transform,
Item.Info.Width, Item.Info.Height,
FProject.Master.Width, FProject.Master.Height, Sel) then
Sel := Rect(0, 0, 0, 0)
{ Рамка во весь кадр — это отсутствие обрезки. Ставить её значило бы
зря перестраивать вывод на каждом клипе. }
else if (Sel.Left <= 0) and (Sel.Top <= 0) and
(Sel.Right >= Item.Info.Width) and
(Sel.Bottom >= Item.Info.Height) then
Sel := Rect(0, 0, 0, 0);
end;
end;
Key := Format('%d,%d,%d,%d,%d',
[Sel.Left, Sel.Top, Sel.Right, Sel.Bottom, Rotation]);
if Key = FLastCropKey then
Exit;
FLastCropKey := Key;
FPlayer.SetOutputCrop(Sel, Rotation);
end;
procedure TMainForm.ApplyVolumeAt(const ATime: Double); procedure TMainForm.ApplyVolumeAt(const ATime: Double);
var var
I: Integer; I: Integer;
@ -3568,6 +3650,9 @@ end;
procedure TMainForm.TimelinePlayheadChange(Sender: TObject); procedure TMainForm.TimelinePlayheadChange(Sender: TObject);
begin begin
UpdateTimeLabel; UpdateTimeLabel;
{ Кадрирование показывается и на паузе: курсор переставили мышью —
картинка обязана смениться сразу, а не с началом воспроизведения. }
ApplyCropAt(FTimeline.Playhead);
{ Если курсор двинул сам плеер, перематывать его обратно нельзя — { Если курсор двинул сам плеер, перематывать его обратно нельзя —
получится бесконечная переброска. } получится бесконечная переброска. }
if FUpdatingFromPlayer then if FUpdatingFromPlayer then
@ -3622,6 +3707,7 @@ begin
Exit; Exit;
ApplyVolumeAt(FPlayer.TimePos); ApplyVolumeAt(FPlayer.TimePos);
ApplyCropAt(FPlayer.TimePos);
FUpdatingFromPlayer := True; FUpdatingFromPlayer := True;
try try

View file

@ -70,6 +70,10 @@ type
{ Номера подключённых звуковых дорожек, в порядке подключения. } { Номера подключённых звуковых дорожек, в порядке подключения. }
FAudioIds: array of string; FAudioIds: array of string;
FLastGraph: string; FLastGraph: string;
{ Что сейчас выставлено обрезкой на выводе. Хранится, чтобы не
переустанавливать свойство на каждом кадре. }
FLastCrop: string;
FLastRotate: string;
FSubtitlePath: string; FSubtitlePath: string;
{ Что произошло с выбором звуковой дорожки. Читается окном для { Что произошло с выбором звуковой дорожки. Читается окном для
журнала: без этого понять, откуда взялся звук, нечем. } журнала: без этого понять, откуда взялся звук, нечем. }
@ -147,6 +151,18 @@ type
громкость текущего куска и есть громкость всего вывода. } громкость текущего куска и есть громкость всего вывода. }
procedure SetVolume(const APercent: Double); procedure SetVolume(const APercent: Double);
{ Обрезка на выводе: показывать только этот кусок кадра исходника.
Делает её сама видеокарта, цепочки фильтров тут нет и кадры в
обычную память не копируются. Годится на то, чего цепочкой дорого:
показать собственное кадрирование клипа. Действует на весь вывод
сразу, поэтому включать и снимать её обязан тот, кто знает, где
сейчас курсор.
Пустой прямоугольник снимает обрезку. Поворот задаётся тем же
способом — отдельным свойством, а не фильтром. }
procedure SetOutputCrop(const ARect: TRect; ARotation: Integer);
{ Цепочка фильтров ffmpeg поверх всего, что играет. { Цепочка фильтров ffmpeg поверх всего, что играет.
Нужна ровно для одного: показать кадр проекта, а не кадр исходника. Нужна ровно для одного: показать кадр проекта, а не кадр исходника.
@ -727,6 +743,32 @@ begin
mpv_set_property(FHandle, 'volume', MPV_FORMAT_DOUBLE, @V); mpv_set_property(FHandle, 'volume', MPV_FORMAT_DOUBLE, @V);
end; end;
procedure TMpvPlayer.SetOutputCrop(const ARect: TRect; ARotation: Integer);
var
Value, Rot: string;
begin
if FHandle = nil then
Exit;
if (ARect.Right > ARect.Left) and (ARect.Bottom > ARect.Top) then
Value := Format('%dx%d+%d+%d',
[ARect.Right - ARect.Left, ARect.Bottom - ARect.Top,
ARect.Left, ARect.Top])
else
Value := '';
Rot := IntToStr(((ARotation mod 360) + 360) mod 360);
{ Лишний раз не трогаем: смена свойства перестраивает вывод, и на
каждом кадре это было бы заметно глазом. }
if (Value = FLastCrop) and (Rot = FLastRotate) then
Exit;
FLastCrop := Value;
FLastRotate := Rot;
mpv_set_property_string(FHandle, 'video-crop', PAnsiChar(Value));
mpv_set_property_string(FHandle, 'video-rotate', PAnsiChar(Rot));
end;
procedure TMpvPlayer.SetVideoFilter(const AFilter: string); procedure TMpvPlayer.SetVideoFilter(const AFilter: string);
var var
Value: string; Value: string;

View file

@ -429,45 +429,14 @@ end;
{ Рамка по настройке. Обратное к TransformFromSel: там же и объяснение, { Рамка по настройке. Обратное к TransformFromSel: там же и объяснение,
откуда берутся числа. } откуда берутся числа. }
procedure TPanCropForm.SelFromTransform; procedure TPanCropForm.SelFromTransform;
var
KFit, KFill, K: Double;
SW, SH, CW, CH, CX, CY: Double;
W, H: Double;
begin begin
UpdateRotatedSize; UpdateRotatedSize;
if (FRotW <= 0) or (FRotH <= 0) or (FDstWidth <= 0) or (FDstHeight <= 0) then if (FRotW <= 0) or (FRotH <= 0) or (FDstWidth <= 0) or (FDstHeight <= 0) then
Exit; Exit;
{ Считает uTransform: той же рамкой пользуется предпросмотр, отдавая её
KFit := Min(FDstWidth / FRotW, FDstHeight / FRotH); плееру как обрезку на выводе. Двух расчётов быть не должно — они
KFill := Max(FDstWidth / FRotW, FDstHeight / FRotH); разойдутся, и окно начнёт обещать не то, что покажет плеер. }
if FTransform.Fit = fmFill then TransformSourceRect(FTransform, FRotW, FRotH, FDstWidth, FDstHeight, FSel);
K := KFill
else
K := KFit;
if FTransform.Fit = fmStretch then
begin
{ Растяжение рамкой не описывается: берётся весь кадр. }
FSel := Rect(0, 0, FRotW, FRotH);
Exit;
end;
if FTransform.Zoom > 0 then
K := K * FTransform.Zoom;
if K <= 0 then
Exit;
SW := FRotW * K;
SH := FRotH * K;
CW := Min(SW, FDstWidth);
CH := Min(SH, FDstHeight);
CX := (SW - CW) / 2 + FTransform.OffsetX * FDstWidth;
CY := (SH - CH) / 2 + FTransform.OffsetY * FDstHeight;
CX := Max(0, Min(SW - CW, CX));
CY := Max(0, Min(SH - CH, CY));
W := CW / K;
H := CH / K;
FSel := Rect(Round(CX / K), Round(CY / K),
Round(CX / K + W), Round(CY / K + H));
end; end;
{ Настройка по рамке. { Настройка по рамке.

View file

@ -82,6 +82,27 @@ function OverlayFilter(const AT: TMediaTransform;
ASrcWidth, ASrcHeight, ADstWidth, ADstHeight: Integer; ASrcWidth, ASrcHeight, ADstWidth, ADstHeight: Integer;
out AX, AY, AWidth, AHeight: Integer): string; out AX, AY, AWidth, AHeight: Integer): string;
{ Какая часть исходника видна в кадре проекта — прямоугольник в точках
самого исходника.
Та же настройка, записанная с другой стороны: вместо «как вписать» —
«что останется». Окно кадрирования рисует по ней рамку, а плеер отдаёт
её mpv как обрезку на выводе, и оба обязаны считать одинаково — иначе
окно обещает одно, а предпросмотр показывает другое.
False — прямоугольником не описать: при растяжении берётся весь кадр и
сжимается неравномерно. }
function TransformSourceRect(const AT: TMediaTransform;
ASrcWidth, ASrcHeight, ADstWidth, ADstHeight: Integer;
out ARect: TRect): Boolean;
{ Можно ли показать это преобразование обрезкой на выводе плеера.
Обрезка умеет только выбрать кусок кадра. Поворот у mpv задаётся
отдельным свойством и потому разрешён, а отражение — нет: свойства для
него нет, и такой клип по-прежнему показывается как есть. }
function TransformFitsOutputCrop(const AT: TMediaTransform): Boolean;
{ Фильтр цветового ключа: этот цвет становится прозрачным. { Фильтр цветового ключа: этот цвет становится прозрачным.
Один на всех: и сборка, и предпросмотр должны выбивать одинаково, иначе Один на всех: и сборка, и предпросмотр должны выбивать одинаково, иначе
@ -153,6 +174,56 @@ begin
(ASrcWidth = ADstWidth) and (ASrcHeight = ADstHeight); (ASrcWidth = ADstWidth) and (ASrcHeight = ADstHeight);
end; end;
function TransformSourceRect(const AT: TMediaTransform;
ASrcWidth, ASrcHeight, ADstWidth, ADstHeight: Integer;
out ARect: TRect): Boolean;
var
KFit, KFill, K: Double;
SW, SH, CW, CH, CX, CY: Double;
begin
ARect := Rect(0, 0, ASrcWidth, ASrcHeight);
Result := False;
if (ASrcWidth <= 0) or (ASrcHeight <= 0) or
(ADstWidth <= 0) or (ADstHeight <= 0) then
Exit;
{ Растяжение рамкой не описывается: берётся весь кадр и жмётся
неравномерно. }
if AT.Fit = fmStretch then
Exit;
KFit := Min(ADstWidth / ASrcWidth, ADstHeight / ASrcHeight);
KFill := Max(ADstWidth / ASrcWidth, ADstHeight / ASrcHeight);
if AT.Fit = fmFill then
K := KFill
else
K := KFit;
if AT.Zoom > 0 then
K := K * AT.Zoom;
if K <= 0 then
Exit;
{ Кадр, растянутый до рабочего масштаба, и та его часть, что помещается
в кадр проекта. Смещение задаётся в долях кадра проекта и прижимается
к краям: выйти за пределы картинки нельзя. }
SW := ASrcWidth * K;
SH := ASrcHeight * K;
CW := Min(SW, ADstWidth);
CH := Min(SH, ADstHeight);
CX := (SW - CW) / 2 + AT.OffsetX * ADstWidth;
CY := (SH - CH) / 2 + AT.OffsetY * ADstHeight;
CX := Max(0, Min(SW - CW, CX));
CY := Max(0, Min(SH - CH, CY));
ARect := Rect(Round(CX / K), Round(CY / K),
Round((CX + CW) / K), Round((CY + CH) / K));
Result := True;
end;
function TransformFitsOutputCrop(const AT: TMediaTransform): Boolean;
begin
Result := (not AT.FlipH) and (not AT.FlipV) and (AT.Fit <> fmStretch);
end;
{ Дробное число через точку: у ffmpeg разделитель всегда точка, а в { Дробное число через точку: у ffmpeg разделитель всегда точка, а в
русской локали Format вставил бы запятую и развалил бы всю цепочку. } русской локали Format вставил бы запятую и развалил бы всю цепочку. }
function FilterNum(const AValue: Double): string; function FilterNum(const AValue: Double): string;

View file

@ -786,6 +786,64 @@ begin
Inc(ClickCount); Inc(ClickCount);
end; 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;
procedure CheckButtonClicks; procedure CheckButtonClicks;
var var
Btn: TButtonProbe; Btn: TButtonProbe;
@ -845,6 +903,7 @@ begin
CheckSplitAndPlayhead; CheckSplitAndPlayhead;
CheckSplitAllTracks; CheckSplitAllTracks;
CheckGapTolerance; CheckGapTolerance;
CheckSourceRect;
CheckButtonClicks; CheckButtonClicks;
WriteLn; WriteLn;