Предпросмотр показывает кадрирование клипа
Клип со своим кадрированием играл в окне во весь кадр: цепочка фильтров у 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 нельзя — контейнер и кодек альфу не хранят. Для этого нужен
|
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. Они действуют на всё окно сразу,
|
|
||||||
но плеером управляем мы и знаем, где курсор: зашли в клип с кадрированием
|
|
||||||
— выставили, вышли — сняли. Ни цепочки, ни копирования кадров из памяти
|
|
||||||
видеокарты в обычную. Ограничения: одно преобразование на всё окно, так
|
|
||||||
что при одновременном наложении всё равно нужна цепочка; отражения таким
|
|
||||||
свойством не задать; переключение на границе клипа придёт с запозданием в
|
|
||||||
кадр-другой.
|
|
||||||
|
|
||||||
## Два видео рядом в одном кадре
|
## Два видео рядом в одном кадре
|
||||||
|
|
||||||
|
|
|
||||||
21
README.md
21
README.md
|
|
@ -403,11 +403,22 @@ Ctrl+Shift+R, который схлопывает дыру под курсоро
|
||||||
Замер на материале 2688x1512 при 60 к/с: 327 кадров в секунду без фильтра,
|
Замер на материале 2688x1512 при 60 к/с: 327 кадров в секунду без фильтра,
|
||||||
230 с настоящим масштабированием — запас четырёхкратный.
|
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;
|
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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
{ Настройка по рамке.
|
{ Настройка по рамке.
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue