Предпросмотр показывает кадрирование клипа
Клип со своим кадрированием играл в окне во весь кадр: цепочка фильтров у mpv одна на весь список кусков, а включить её на одном куске и выключить на следующем нечем — из фильтров ffmpeg по времени переключается только наложение. Обошлось без цепочки вовсе. У вывода mpv своя обрезка, и делает её видеокарта: курсор зашёл в клип с кадрированием — выставили, вышел — сняли. Ни перестройки цепочки, ни копирования кадров из памяти видеокарты в обычную. Рамку источника считает теперь один код на две стороны: по ней окно «Кадрирование и поворот» рисует рамку, и её же плеер отдаёт как обрезку. Проверка в tests/lfmcheck сверяет рамку с фильтром сборки: увеличение 2.77 даёт 970x546 посередине, и это ровно то, что вырезает crop=2688:1512:2379:1338 после scale=7446:4188. Проверено глазами на невидимом экране, на настоящем материале с дрона: картинка в окне совпала с готовым роликом, на границе с обычным клипом обрезка снимается и возвращается. Сверено со сборкой из прошлого коммита — там на том же месте кадр без кадрирования. Не покрыто и записано в BACKLOG: отражение (свойства для него нет), растяжение и участки с наложением — там кадр уже сведён цепочкой.
This commit is contained in:
parent
2160c33790
commit
235cd00e9a
8 changed files with 329 additions and 70 deletions
49
BACKLOG.md
49
BACKLOG.md
|
|
@ -76,46 +76,35 @@ ffmpeg проблемой не является: он запускается о
|
|||
MP4/H.264 нельзя — контейнер и кодек альфу не хранят. Для этого нужен
|
||||
отдельный вывод в 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 |
|
||||
| ветка «обрезать и растянуть» с `enable` | 2.81 с | 214 |
|
||||
|
||||
Нужно 60, то есть запас трёхкратный: путь через цепочку по деньгам
|
||||
проходит. Раньше здесь было написано, что для 2688x1512 это уже не
|
||||
пустяк, — написано по памяти о замере вписывания, а не об этом случае.
|
||||
|
||||
Но есть путь дешевле. mpv умеет обрезать и увеличивать на выводе, силами
|
||||
видеокарты: свойства `video-crop`, `video-zoom`, `video-pan-x/y`,
|
||||
`video-rotate` есть в нашей версии 0.37. Они действуют на всё окно сразу,
|
||||
но плеером управляем мы и знаем, где курсор: зашли в клип с кадрированием
|
||||
— выставили, вышли — сняли. Ни цепочки, ни копирования кадров из памяти
|
||||
видеокарты в обычную. Ограничения: одно преобразование на всё окно, так
|
||||
что при одновременном наложении всё равно нужна цепочка; отражения таким
|
||||
свойством не задать; переключение на границе клипа придёт с запозданием в
|
||||
кадр-другой.
|
||||
Нужно 60, то есть запас трёхкратный: по деньгам проходит. Устроена она
|
||||
так: раздвоить поток, одну ветку обрезать и растянуть, и наложить её на
|
||||
другую с `enable='between(t,...)'`. Иначе никак — из фильтров ffmpeg по
|
||||
времени переключается только `overlay`, у `crop` и `scale` поддержки
|
||||
временной шкалы нет.
|
||||
|
||||
## Два видео рядом в одном кадре
|
||||
|
||||
|
|
|
|||
21
README.md
21
README.md
|
|
@ -403,11 +403,22 @@ Ctrl+Shift+R, который схлопывает дыру под курсоро
|
|||
Замер на материале 2688x1512 при 60 к/с: 327 кадров в секунду без фильтра,
|
||||
230 с настоящим масштабированием — запас четырёхкратный.
|
||||
|
||||
Чего предпросмотр не умеет: показать собственную настройку кадрирования
|
||||
файла. Цепочка фильтров у mpv одна на всю склейку, а заполнение с обрезкой,
|
||||
поворот и смещение задаются для каждого файла отдельно — выбрать между ними
|
||||
общий фильтр не может, поэтому он всегда вписывает целиком. Точную картинку
|
||||
для такого файла показывает окно «Кадрирование и поворот».
|
||||
**Кадрирование клипа окно показывает.** Цепочка фильтров у mpv одна на всю
|
||||
склейку, а кадрирование у каждого клипа своё, и включать его по времени в
|
||||
цепочке нечем: из фильтров ffmpeg по времени переключается только
|
||||
наложение. Зато у вывода есть своя обрезка, и делает её видеокарта даром.
|
||||
Курсор зашёл в клип со своим кадрированием — обрезка выставляется, вышел —
|
||||
снимается. Ни лишней цепочки, ни копирования кадров из памяти видеокарты.
|
||||
|
||||
Рамку считает один и тот же код, что рисует её в окне «Кадрирование и
|
||||
поворот», — иначе окно обещало бы одно, а предпросмотр показывал другое
|
||||
(`TransformSourceRect` в `uTransform`, проверка в `tests/lfmcheck`).
|
||||
|
||||
Чего это не покрывает: отражение (свойства для него у вывода нет),
|
||||
растяжение (там кадр жмётся неравномерно, рамкой не описать) и участки с
|
||||
наложением — там кадр уже сведён цепочкой, и обрезка резала бы сведённое
|
||||
целиком, вместе с нижним слоем. Для таких клипов точную картинку
|
||||
по-прежнему показывает окно «Кадрирование и поворот».
|
||||
|
||||
## Цвета окна
|
||||
|
||||
|
|
|
|||
|
|
@ -167,6 +167,14 @@ function FlattenPreview(AProject: TProject;
|
|||
|
||||
Возвращает, сколько секунд осталось. }
|
||||
|
||||
{ Клип, который виден в этот момент: с самой верхней включённой дорожки
|
||||
нужного вида. Тот же отбор победителя, что и при уплощении.
|
||||
|
||||
Нужен предпросмотру: по нему решается, чьё кадрирование показывать
|
||||
обрезкой на выводе плеера. }
|
||||
function VisibleClipAtTime(AProject: TProject; AKind: TTrackKind;
|
||||
const ATime: Double): TClip;
|
||||
|
||||
function ClipSegmentsToRange(var ASegments: TFlatSegmentArray;
|
||||
const AFrom, ATo: Double): Double;
|
||||
|
||||
|
|
@ -341,6 +349,30 @@ begin
|
|||
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;
|
||||
const AFrom, ATo: Double): Double;
|
||||
var
|
||||
|
|
|
|||
|
|
@ -175,6 +175,8 @@ type
|
|||
FPreviewSegments: TFlatSegmentArray;
|
||||
FLastVolumeSent: Double;
|
||||
FLastVolumeKey: string;
|
||||
{ Что сейчас показано обрезкой на выводе: лишний раз её не трогаем. }
|
||||
FLastCropKey: string;
|
||||
|
||||
FGenerate: TGenerateThread;
|
||||
FGenerateBusy: Boolean;
|
||||
|
|
@ -235,6 +237,8 @@ type
|
|||
FHintControl: TControl;
|
||||
|
||||
procedure ApplyVolumeAt(const ATime: Double);
|
||||
{ Показывает кадрирование клипа, который виден сейчас. }
|
||||
procedure ApplyCropAt(const ATime: Double);
|
||||
{ Переписывает файл субтитров и отдаёт его плееру. }
|
||||
procedure UpdateSubtitles;
|
||||
procedure UpdateRippleButton;
|
||||
|
|
@ -2404,6 +2408,15 @@ begin
|
|||
FSeekPending := True;
|
||||
FSeekWaitTicks := 0;
|
||||
|
||||
{ Кадрирование выставляется заново после каждой загрузки.
|
||||
|
||||
Обрезка на выводе — свойство плеера, а не списка кусков: она
|
||||
переживает загрузку и осталась бы от прошлого монтажа. А при открытии
|
||||
проекта курсор не двигается вовсе, и без этого вызова первый клип
|
||||
показывался бы без своего кадрирования до первого щелчка по монтажу. }
|
||||
FLastCropKey := '';
|
||||
ApplyCropAt(Pos);
|
||||
|
||||
SetStatus(Format('Предпросмотр: %d %s, %s с',
|
||||
[Length(ASegments), Plural(Length(ASegments), 'кусок', 'куска', 'кусков'),
|
||||
SecondsToStr(TotalDuration(ASegments))]));
|
||||
|
|
@ -3447,6 +3460,75 @@ begin
|
|||
FPlayer.SetSubtitleFile('');
|
||||
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);
|
||||
var
|
||||
I: Integer;
|
||||
|
|
@ -3568,6 +3650,9 @@ end;
|
|||
procedure TMainForm.TimelinePlayheadChange(Sender: TObject);
|
||||
begin
|
||||
UpdateTimeLabel;
|
||||
{ Кадрирование показывается и на паузе: курсор переставили мышью —
|
||||
картинка обязана смениться сразу, а не с началом воспроизведения. }
|
||||
ApplyCropAt(FTimeline.Playhead);
|
||||
{ Если курсор двинул сам плеер, перематывать его обратно нельзя —
|
||||
получится бесконечная переброска. }
|
||||
if FUpdatingFromPlayer then
|
||||
|
|
@ -3622,6 +3707,7 @@ begin
|
|||
Exit;
|
||||
|
||||
ApplyVolumeAt(FPlayer.TimePos);
|
||||
ApplyCropAt(FPlayer.TimePos);
|
||||
|
||||
FUpdatingFromPlayer := True;
|
||||
try
|
||||
|
|
|
|||
|
|
@ -70,6 +70,10 @@ type
|
|||
{ Номера подключённых звуковых дорожек, в порядке подключения. }
|
||||
FAudioIds: array of string;
|
||||
FLastGraph: string;
|
||||
{ Что сейчас выставлено обрезкой на выводе. Хранится, чтобы не
|
||||
переустанавливать свойство на каждом кадре. }
|
||||
FLastCrop: string;
|
||||
FLastRotate: string;
|
||||
FSubtitlePath: string;
|
||||
{ Что произошло с выбором звуковой дорожки. Читается окном для
|
||||
журнала: без этого понять, откуда взялся звук, нечем. }
|
||||
|
|
@ -147,6 +151,18 @@ type
|
|||
громкость текущего куска и есть громкость всего вывода. }
|
||||
procedure SetVolume(const APercent: Double);
|
||||
|
||||
{ Обрезка на выводе: показывать только этот кусок кадра исходника.
|
||||
|
||||
Делает её сама видеокарта, цепочки фильтров тут нет и кадры в
|
||||
обычную память не копируются. Годится на то, чего цепочкой дорого:
|
||||
показать собственное кадрирование клипа. Действует на весь вывод
|
||||
сразу, поэтому включать и снимать её обязан тот, кто знает, где
|
||||
сейчас курсор.
|
||||
|
||||
Пустой прямоугольник снимает обрезку. Поворот задаётся тем же
|
||||
способом — отдельным свойством, а не фильтром. }
|
||||
procedure SetOutputCrop(const ARect: TRect; ARotation: Integer);
|
||||
|
||||
{ Цепочка фильтров ffmpeg поверх всего, что играет.
|
||||
|
||||
Нужна ровно для одного: показать кадр проекта, а не кадр исходника.
|
||||
|
|
@ -727,6 +743,32 @@ begin
|
|||
mpv_set_property(FHandle, 'volume', MPV_FORMAT_DOUBLE, @V);
|
||||
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);
|
||||
var
|
||||
Value: string;
|
||||
|
|
|
|||
|
|
@ -429,45 +429,14 @@ end;
|
|||
{ Рамка по настройке. Обратное к TransformFromSel: там же и объяснение,
|
||||
откуда берутся числа. }
|
||||
procedure TPanCropForm.SelFromTransform;
|
||||
var
|
||||
KFit, KFill, K: Double;
|
||||
SW, SH, CW, CH, CX, CY: Double;
|
||||
W, H: Double;
|
||||
begin
|
||||
UpdateRotatedSize;
|
||||
if (FRotW <= 0) or (FRotH <= 0) or (FDstWidth <= 0) or (FDstHeight <= 0) then
|
||||
Exit;
|
||||
|
||||
KFit := Min(FDstWidth / FRotW, FDstHeight / FRotH);
|
||||
KFill := Max(FDstWidth / FRotW, FDstHeight / FRotH);
|
||||
if FTransform.Fit = fmFill then
|
||||
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));
|
||||
{ Считает uTransform: той же рамкой пользуется предпросмотр, отдавая её
|
||||
плееру как обрезку на выводе. Двух расчётов быть не должно — они
|
||||
разойдутся, и окно начнёт обещать не то, что покажет плеер. }
|
||||
TransformSourceRect(FTransform, FRotW, FRotH, FDstWidth, FDstHeight, FSel);
|
||||
end;
|
||||
|
||||
{ Настройка по рамке.
|
||||
|
|
|
|||
|
|
@ -82,6 +82,27 @@ function OverlayFilter(const AT: TMediaTransform;
|
|||
ASrcWidth, ASrcHeight, ADstWidth, ADstHeight: Integer;
|
||||
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);
|
||||
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 разделитель всегда точка, а в
|
||||
русской локали Format вставил бы запятую и развалил бы всю цепочку. }
|
||||
function FilterNum(const AValue: Double): string;
|
||||
|
|
|
|||
|
|
@ -786,6 +786,64 @@ 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;
|
||||
|
||||
procedure CheckButtonClicks;
|
||||
var
|
||||
Btn: TButtonProbe;
|
||||
|
|
@ -845,6 +903,7 @@ begin
|
|||
CheckSplitAndPlayhead;
|
||||
CheckSplitAllTracks;
|
||||
CheckGapTolerance;
|
||||
CheckSourceRect;
|
||||
CheckButtonClicks;
|
||||
|
||||
WriteLn;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue