Ключ и прозрачность видно прямо в предпросмотре

Оверлей показывался как есть, со своим фоном: чтобы увидеть, что выйдет
после сборки, приходилось собирать. Теперь выбитый цвет и приглушение
считаются в той же цепочке плеера, на лету.

Строки фильтров стали общими со сборкой (ColorKeyFilter, OpacityFilter
в uTransform): окно и готовый ролик больше не могут разойтись по тому,
как записан цвет или порог.

Переключатель в меню «Вид», состояние переживает перезапуск. Замер на
2688x1512, 600 кадров: основа 2.07 с, с наложением 2.43 с, с ключом и
прозрачностью 2.66 с — вчетверо быстрее реального времени, но на слабой
машине это заметнее, поэтому выключить можно.

В бэклог записано окно настроек: переключателей, которые стоят машинного
времени, набралось много, и живут они кто где — кнопками внизу, в меню,
а выбор кодировщика вообще нигде.
This commit is contained in:
4aiman 2026-08-31 00:46:58 +03:00
commit ea51773bed
8 changed files with 165 additions and 18 deletions

View file

@ -76,6 +76,28 @@ ffmpeg проблемой не является: он запускается о
MP4/H.264 нельзя — контейнер и кодек альфу не хранят. Для этого нужен
отдельный вывод в VP9 или ProRes 4444.
## Окно настроек программы
Переключателей, которые стоят машинного времени, становится больше, а
живут они кто где: прилипание и схлопывание — кнопками внизу, ключ в
предпросмотре — в меню «Вид», выбор кодировщика вообще нигде (берётся
NVENC, если он есть). Это разрозненно и не даёт увидеть общую цену.
Нужно одно окно, где собрано всё, что можно выключить ради скорости:
- ключ и прозрачность в предпросмотре (сейчас в меню «Вид»);
- наложение в предпросмотре вообще — на слабой машине может не хватать и
его, тогда честнее показывать одну основу;
- кодировщик для пересборки: аппаратный или процессорный, и какой
именно;
- где держать кэш и когда его убирать;
- прилипание клипов и курсора (сейчас кнопками, но им место и здесь).
Почему это важно: разработка идёт на машине с быстрой видеокартой, и
всё, что здесь считается «недорогим», на другой может оказаться
неподъёмным. Пока цифры замеряны только на одной машине, у человека
должна быть возможность выключить любую из этих трат.
## Мелочи, до которых не дошли руки
**Значок файла программы под Windows.** Значок окна задаётся из

View file

@ -141,6 +141,18 @@ xdg-mime query default application/x-nlve-project
липнут при перетаскивании, курсор — при щелчке, и одно бывает нужно без
другого.
**Ключ и прозрачность в предпросмотре** (меню «Вид»). Включено — окно
показывает то же, что даст сборка: выбитый цвет и приглушение считаются
на лету. Выключено — оверлей виден целиком, со своим фоном: так понятно,
куда он встал и где его края.
Считается это по площади самого оверлея, а не всего кадра. Замер на
2688x1512 (600 кадров, десять секунд материала): основа 2.07 с, с
наложением 2.43 с, с ключом и прозрачностью 2.66 с. То есть наложение
стоит 0.36 с, ключ добавляет ещё 0.23 с — вчетверо быстрее реального
времени. На слабой машине это может быть заметнее, потому выключатель и
есть.
**Схлопывание.** Кнопка внизу показывает и режим, и состояние: утоплена —
автосхлопывание включено, и тогда при перетаскивании клипа всё, что
начинается не раньше него, едет следом. Стрелка рядом открывает выбор

View file

@ -61,8 +61,33 @@ mpv 0.37, видео 2688x1512 HEVC 87 Мбит/с, 600 кадров (10 с ма
и картинку, и звук.
- `./build.sh tests` — все проверки пройдены.
## Что осталось
## Продолжение: ключ прямо в предпросмотре
Ключ в предпросмотре не применяется — так решено сознательно. Стоит он
недорого (фильтр работает по небольшой площади), и при желании это
делается переключателем.
По просьбе сделано и это. Ключ и прозрачность считаются в той же цепочке
плеера, отдельным механизмом не обзаводились: строки фильтров теперь
общие со сборкой (`ColorKeyFilter`, `OpacityFilter` в `uTransform`), так
что окно и готовый ролик не могут разойтись по формату записи.
Переключатель — в меню «Вид», состояние переживает перезапуск. Замер на
тяжёлом материале 2688x1512, 600 кадров:
| Что | Время |
|---|---|
| только основа | 2.07 с |
| плюс наложение | 2.43 с |
| плюс ключ и прозрачность | 2.66 с |
Проверено снимками окна: с ключом сквозь пурпур видно нижнее видео,
без ключа оверлей виден плашкой целиком.
В бэклог записано окно настроек программы: переключателей, которые стоят
машинного времени, становится много, и живут они кто где.
## Западня с настройками
Тестовые запуски шли с подменённым HOME, и это не изолировало настройки:
в сессии задан `XDG_CONFIG_HOME`, и программа читала настоящий файл
пользователя (оттуда и бралась высота панели предпросмотра в 883 точки,
из-за которой монтаж не помещался в окно). Файл не пострадал — проверено
по содержимому, — но теперь тестовые скрипты задают `XDG_CONFIG_HOME`,
`XDG_CACHE_HOME` и `XDG_DATA_HOME` своими.

View file

@ -235,7 +235,7 @@ function ParseProgressLine(const ALine: string; var ASeconds: Double;
implementation
uses
StrUtils;
StrUtils, uTransform;
{ Общие параметры вывода в формате проекта. Объявлено заранее, потому что
используется выше по тексту, чем определено. }
@ -318,12 +318,8 @@ begin
масштабом. Цвет записывается как 0xRRGGBB — порядок обратный тому,
в каком он хранится у нас. }
if ALayers[I].KeyEnabled then
Fit := Fit + Format(',colorkey=0x%.2x%.2x%.2x:%s:%s',
[ALayers[I].KeyColor and $FF,
(ALayers[I].KeyColor shr 8) and $FF,
(ALayers[I].KeyColor shr 16) and $FF,
SecondsToStr(ALayers[I].KeySimilarity / 100),
SecondsToStr(ALayers[I].KeyBlend / 100)]);
Fit := Fit + ',' + ColorKeyFilter(ALayers[I].KeyColor,
ALayers[I].KeySimilarity, ALayers[I].KeyBlend);
Graph := Graph + Format('[%d:v]%s,format=yuva420p[v%d];', [I, Fit, I]);
end;

View file

@ -140,11 +140,17 @@ function FlattenProject(AProject: TProject;
под ними видно то, что лежит ниже, — а выдаются отдельным списком со
временем и местом в кадре.
AWithKey — применять ли к оверлею цветовой ключ и прозрачность прямо в
плеере. Тогда окно показывает то же, что даст сборка. Стоит это
недорого: фильтр работает по площади самого оверлея, а не всего кадра,
но на слабой машине лишним не будет, поэтому решает вызывающий.
Для сборки ролика это не применяется: там всё сводится по-настоящему. }
function FlattenPreview(AProject: TProject;
out AVideo, AAudio: TFlatSegmentArray;
out AOverlays: TPreviewOverlays;
out AMissing: TGenRequests): Boolean;
out AMissing: TGenRequests;
AWithKey: Boolean = False): Boolean;
{ Путь к сгенерированному клипу. Имя выводится из содержимого запроса,
поэтому одинаковые запросы дают один и тот же файл, и он делается
@ -1034,7 +1040,8 @@ end;
function FlattenPreview(AProject: TProject;
out AVideo, AAudio: TFlatSegmentArray;
out AOverlays: TPreviewOverlays;
out AMissing: TGenRequests): Boolean;
out AMissing: TGenRequests;
AWithKey: Boolean): Boolean;
var
Hidden: TFPList;
MissA: TGenRequests;
@ -1108,6 +1115,17 @@ begin
Item.Info.Width, Item.Info.Height,
AProject.Master.Width, AProject.Master.Height,
Ov.X, Ov.Y, Ov.Width, Ov.Height);
{ Порядок тот же, что при сборке: сперва вписали в кадр, потом
выбили цвет, потом приглушили. Иначе смягчение края разошлось бы
с тем, что даст готовый ролик. }
if AWithKey then
begin
if Clip.ColorKey.Enabled then
Ov.Filter := Ov.Filter + ',' + ColorKeyFilter(Clip.ColorKey.Color,
Clip.ColorKey.Similarity, Clip.ColorKey.Blend);
if OpacityFilter(Clip.Opacity) <> '' then
Ov.Filter := Ov.Filter + ',' + OpacityFilter(Clip.Opacity);
end;
SetLength(AOverlays, N + 1);
AOverlays[N] := Ov;
Inc(N);

View file

@ -129,6 +129,9 @@ type
первую. }
FErrorCount: Integer;
FLogButton: TThemedButton;
{ Пункт «Ключ и прозрачность в предпросмотре»: галку в нём приходится
держать за руку — она не следует за настройкой сама. }
FKeyPreviewItem: TMenuItem;
{ Показанная доля выполнения. Отдельно от настоящей, потому что растёт
она по своим правилам — см. AdvanceJobBar. }
FJobShown: Double;
@ -238,6 +241,7 @@ type
procedure AdvanceJobBar;
procedure LogButtonClick(Sender: TObject);
procedure MenuToggleLog(Sender: TObject);
procedure MenuTogglePreviewKey(Sender: TObject);
procedure MenuAbout(Sender: TObject);
procedure MenuAddText(Sender: TObject);
{ Открывает редактор текста для клипа. True — что-то изменилось. }
@ -583,8 +587,10 @@ procedure TMainForm.BuildMenu;
FMenu.Items.Add(Result);
end;
procedure AddItem(AParent: TMenuItem; const ACaption: string;
AHandler: TNotifyEvent; AShortCut: TShortCut = 0);
{ Возвращает созданный пункт: у пунктов с галкой её потом надо ставить
и снимать, а искать пункт по названию — способ ненадёжный. }
function AddItem(AParent: TMenuItem; const ACaption: string;
AHandler: TNotifyEvent; AShortCut: TShortCut = 0): TMenuItem;
var
Item: TMenuItem;
begin
@ -593,6 +599,7 @@ procedure TMainForm.BuildMenu;
Item.OnClick := AHandler;
Item.ShortCut := AShortCut;
AParent.Add(Item);
Result := Item;
end;
procedure AddSep(AParent: TMenuItem);
@ -675,6 +682,11 @@ begin
AddItem(MView, 'Отдалить', @MenuZoomOut, ShortCut(VK_SUBTRACT, [ssCtrl]));
AddSep(MView);
AddItem(MView, 'Журнал сообщений', @MenuToggleLog, ShortCut(VK_L, [ssCtrl]));
AddSep(MView);
FKeyPreviewItem := AddItem(MView, 'Ключ и прозрачность в предпросмотре',
@MenuTogglePreviewKey);
FKeyPreviewItem.AutoCheck := False;
FKeyPreviewItem.Checked := FSettings.PreviewKey;
MProject := AddTop('Проект');
AddItem(MProject, 'Сведения о проекте...', @MenuProjectInfo);
@ -1882,7 +1894,8 @@ begin
{ Уплощение для предпросмотра: клипы, которые кладутся поверх, идут
отдельно. Плеер положит их сам, ничего не собирая. }
if not FlattenPreview(FProject, Segments, AudioSegs, Overlays, Missing) then
if not FlattenPreview(FProject, Segments, AudioSegs, Overlays, Missing,
FSettings.PreviewKey) then
begin
FPlayer.Stop;
FLastLoadedEdl := '';
@ -3111,6 +3124,29 @@ end;
{ К началу самого раннего клипа по всем дорожкам. Если монтаж начинается
с пятой секунды, ноль — не то место, куда хотят попасть. }
{ Показывать ли в предпросмотре то, что даст сборка.
Выключено — оверлей виден целиком, со своим фоном: так понятно, куда он
встал и где его края. Включено — выбитый цвет и прозрачность считаются
на лету, и окно совпадает с готовым роликом.
Считается это не бесплатно, хотя и недорого: фильтр работает по площади
оверлея, а не всего кадра. Замерено на 2688x1512: наложение стоит 0.36 с
на десять секунд материала, ключ добавляет ещё 0.23 с. }
procedure TMainForm.MenuTogglePreviewKey(Sender: TObject);
begin
FSettings.PreviewKey := not FSettings.PreviewKey;
FKeyPreviewItem.Checked := FSettings.PreviewKey;
{ Список кусков не изменился, а показывать надо иначе — заставляем
пересобрать цепочку. }
FLastLoadedEdl := '';
SchedulePreview;
if FSettings.PreviewKey then
SetStatus('Предпросмотр показывает ключ и прозрачность.')
else
SetStatus('Предпросмотр показывает оверлей как есть.');
end;
procedure TMainForm.MenuToggleRipple(Sender: TObject);
begin
ToggleRipple;

View file

@ -37,6 +37,7 @@ type
FVideoPanelHeight: Integer;
FLogVisible: Boolean;
FPlayheadSnap: Boolean;
FPreviewKey: Boolean;
FDepsReminder: Boolean;
FRecentProjects: TStringList;
function FileName: string;
@ -70,6 +71,8 @@ type
property LogVisible: Boolean read FLogVisible write FLogVisible;
{ Прилипание курсора воспроизведения к границам клипов. }
property PlayheadSnap: Boolean read FPlayheadSnap write FPlayheadSnap;
{ Применять ли ключ и прозрачность прямо в предпросмотре. }
property PreviewKey: Boolean read FPreviewKey write FPreviewKey;
{ Напоминать ли о недостающих зависимостях при запуске. Гасится
кнопкой «Не напоминать» в самом напоминании. }
property DepsReminder: Boolean read FDepsReminder write FDepsReminder;
@ -103,6 +106,7 @@ begin
FVideoPanelHeight := DefaultVideoHeight;
FLogVisible := True;
FPlayheadSnap := True;
FPreviewKey := True;
FDepsReminder := True;
end;
@ -184,6 +188,7 @@ begin
FVideoPanelHeight := Obj.Get('video_panel_height', FVideoPanelHeight);
FLogVisible := Obj.Get('log_visible', FLogVisible);
FPlayheadSnap := Obj.Get('playhead_snap', FPlayheadSnap);
FPreviewKey := Obj.Get('preview_key', FPreviewKey);
FDepsReminder := Obj.Get('deps_reminder', FDepsReminder);
Arr := Obj.Find('recent_projects', jtArray) as TJSONArray;
@ -236,6 +241,7 @@ begin
Root.Add('video_panel_height', FVideoPanelHeight);
Root.Add('log_visible', FLogVisible);
Root.Add('playhead_snap', FPlayheadSnap);
Root.Add('preview_key', FPreviewKey);
Root.Add('deps_reminder', FDepsReminder);
Arr := TJSONArray.Create;

View file

@ -22,7 +22,7 @@ unit uTransform;
interface
uses
Classes, SysUtils;
Classes, SysUtils, uAppTypes;
type
{ Как соотносятся кадр исходника и кадр проекта. }
@ -82,6 +82,16 @@ function OverlayFilter(const AT: TMediaTransform;
ASrcWidth, ASrcHeight, ADstWidth, ADstHeight: Integer;
out AX, AY, AWidth, AHeight: Integer): string;
{ Фильтр цветового ключа: этот цвет становится прозрачным.
Один на всех: и сборка, и предпросмотр должны выбивать одинаково, иначе
окно обещает одно, а готовый ролик даёт другое. Цвет записывается как
0xRRGGBB — порядок обратный тому, в каком он хранится у нас. }
function ColorKeyFilter(AColor: TRgb; ASimilarity, ABlend: Integer): string;
{ Фильтр приглушения по альфа-каналу. Пусто, если приглушать нечего. }
function OpacityFilter(const AOpacity: Double): string;
function PreviewFitFilter(ADstWidth, ADstHeight: Integer): string;
function FitModeToStr(AMode: TFitMode): string;
@ -91,7 +101,7 @@ function FitModeCaption(AMode: TFitMode): string;
implementation
uses
Math, uAppTypes;
Math;
function DefaultTransform: TMediaTransform;
begin
@ -143,6 +153,28 @@ begin
(ASrcWidth = ADstWidth) and (ASrcHeight = ADstHeight);
end;
{ Дробное число через точку: у ffmpeg разделитель всегда точка, а в
русской локали Format вставил бы запятую и развалил бы всю цепочку. }
function FilterNum(const AValue: Double): string;
begin
Result := SecondsToStr(AValue);
end;
function ColorKeyFilter(AColor: TRgb; ASimilarity, ABlend: Integer): string;
begin
Result := Format('colorkey=0x%.2x%.2x%.2x:%s:%s',
[AColor and $FF, (AColor shr 8) and $FF, (AColor shr 16) and $FF,
FilterNum(ASimilarity / 100), FilterNum(ABlend / 100)]);
end;
function OpacityFilter(const AOpacity: Double): string;
begin
if AOpacity >= 0.999 then
Result := ''
else
Result := 'colorchannelmixer=aa=' + FilterNum(Max(0, AOpacity));
end;
function PreviewFitFilter(ADstWidth, ADstHeight: Integer): string;
var
W, H: string;