Наложение в предпросмотре сводится заранее; номер сборки в заголовке

Клип, не заполняющий кадр, стал уходить плееру отдельной дорожкой, и
весь показ пошёл через цепочку фильтров. Цепочка работает всё время
показа и считается процессором: на кадре 2688x1512 видео начало
отставать от звука.

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

Попутно, из разбора того же случая:

— заполнитель до первого наложения нарезается по длине файла. Одной
  записью mpv её обрезает («entry 0 uses 7082.687000 seconds, but file
  has only 15.021000»), дорожка оказывается короче монтажа и не
  совмещается с ним по времени;
— появление заготовки вызывает перезагрузку списка: строка списка при
  этом не меняется, и без признака готовности плеер так и оставался без
  дорожки наложения;
— в журнал пишется, сколько окон наложения и готовы ли файлы, а также
  собранная цепочка показа и неудача video-add. Без этого «картинки
  нет» нечем объяснить.

В заголовке окна теперь номер сборки: nlve 42 — фильм.nlve. Он растёт на
единицу при каждой сборке. По нему сразу видно, та ли это сборка, где
правка уже есть, — сегодня время ушло на то, что проверяли старым
двоичным файлом. Счётчик в build.number, в репозиторий не входит.

Проверено на монтаже с картинкой на 300-й секунде: в окне она лежит
поверх видео, цепочки показа нет.
This commit is contained in:
4aiman 2026-10-16 17:19:57 +03:00
commit 567c518a62
7 changed files with 192 additions and 16 deletions

4
.gitignore vendored
View file

@ -92,3 +92,7 @@ shot*.png
# Мусор компоновщика Lazarus.
link*.res
# Номер сборки и созданный из него файл: про эту машину, не про историю.
/build.number
/src/uBuildInfo.inc

View file

@ -731,12 +731,18 @@ Linux потом искала по нему `.dll`.
неоткуда. Заготовка с полями остаётся для случая, когда класть не на
что.
В предпросмотре накладываемая картинка тоже показывается заготовкой,
только без полей. Своих декодеров плееру хватает не на всё: настоящий
webp из сети дал «webp: invalid TIFF header in Exif data», и картинка
пропадала из окна, хотя в ролик попадала. У неподвижного кадра есть и
вторая беда — в списке кусков у него нет длины. Заготовка же обычное
видео нужной длины.
Предпросмотр идёт тем же путём: участок с наложением сводится заранее
и играется обычным файлом. Силами плеера рисуются только прозрачные
клипы и клипы с ключом — их участки бывают длинными, и пересобирать
минуты материала ради них дорого. Цепочка плеера работает всё время
показа и считается процессором: на кадре 2688x1512 видео начинает
отставать от звука, поэтому ради клипа, который просто не заполняет
кадр, её не включают.
Картинка, которую всё же рисует плеер, отдаётся ему заготовкой без
полей, а не самим файлом: своих декодеров ему хватает не на всё
(настоящий webp из сети дал «webp: invalid TIFF header in Exif data»),
да и у неподвижного кадра нет длины в списке кусков.
3. Границы кусков кладутся на кадровую сетку проекта: между кадрами
времени нет, а звук набирается с точностью до отсчёта, и доли кадра
разводят их в стороны.
@ -1104,6 +1110,18 @@ xdotool не видит ни одного окна и проверка «про
времени, второй щелчок по наезду тащит верхний клип вместо нижнего, а
картинка с уменьшением 50% выходит в ролик во весь кадр.
## Номер сборки
В заголовке окна стоит номер: `nlve 42 — фильм.nlve`. Он растёт на
единицу при каждой сборке редактора (`build.sh`), и по нему сразу видно,
та ли это сборка, где нужная правка уже есть. Без него проверяют старым
двоичным файлом, видят прежнее поведение, и время уходит на выяснение,
у кого что не работает.
Счётчик лежит в `build.number`, из него создаётся `src/uBuildInfo.inc`.
Оба файла в репозиторий не входят: номер про эту машину, а не про
историю проекта.
## Структура
| Файл | Что это |

View file

@ -72,8 +72,37 @@ run_lazbuild() {
return "$rc"
}
# Номер сборки. Растёт на единицу при каждой сборке редактора и попадает
# в заголовок окна.
#
# Зачем: по заголовку сразу видно, та ли это сборка, где правка уже есть.
# Без него человек проверяет старым двоичным файлом и видит прежнее
# поведение — так уже случалось, и время ушло на выяснение, у кого что
# не работает.
#
# Счётчик лежит рядом с исходниками, но в репозиторий не входит: он про
# эту машину, а не про историю проекта.
bump_version() {
local file="$HERE/build.number" n
n=0
[ -f "$file" ] && n=$(cat "$file" 2>/dev/null || echo 0)
case "$n" in
''|*[!0-9]*) n=0 ;;
esac
n=$((n + 1))
echo "$n" > "$file"
cat > "$HERE/src/uBuildInfo.inc" <<EOF
{ Создаётся build.sh при каждой сборке. В репозиторий не входит. }
const
BuildNumber = $n;
BuildStamp = '$(date '+%d.%m.%Y %H:%M')';
EOF
echo "сборка номер $n"
}
build_current() {
check_toolchain
bump_version
if run_lazbuild "Редактор под эту систему ($LCL_LINUX)" \
--ws="$LCL_LINUX" "$HERE/nlve.lpi"; then
echo "готово: $HERE/nlve"

View file

@ -57,6 +57,16 @@ type
Y: Integer;
Width: Integer;
Height: Integer;
{ Сколько секунд можно взять из этого файла.
Список кусков оверлея обязан покрывать монтаж от самого начала,
иначе плееру нечем совместить его по времени с основой. Промежуток
до первого показа заполняется тем же файлом, и просить у него
больше, чем в нём есть, нельзя: mpv обрезает такую запись
(«entry uses 7082.687000 seconds, but file has only 15.021000») —
дорожка оказывается короче монтажа, и оверлей не показывается
вовсе. Поэтому заполнитель нарезается на куски по этой длине. }
SrcLength: Double;
end;
TPreviewOverlays = array of TPreviewOverlay;
@ -1391,6 +1401,21 @@ end;
хочет увидеть под ним то, что на дорожке ниже, — иначе зачем уменьшать.
Если под клипом ничего нет, поля остаются полями: класть не на что. }
{ Кого плеер рисует поверх основы сам, своей цепочкой.
Только прозрачные клипы и клипы с ключом. Цепочка идёт через процессор
и работает всё время показа, а не только там, где лежит оверлей: на
2688x1512 видео начинает отставать от звука. Ради длинного
полупрозрачного слоя это оправдано — иначе пришлось бы пересобирать
минуты материала. Ради клипа, который просто не заполняет кадр, — нет:
его участок дешевле свести заранее, как для ролика, и тогда
предпросмотр играет обычный файл. }
function IsPlayerOverlay(AClip: TClip): Boolean;
begin
Result := (AClip <> nil) and (AClip.Kind in [ckVideo, ckImage]) and
(AClip.ColorKey.Enabled or (AClip.Opacity < 0.999));
end;
function IsOverlayClip(AProject: TProject; AClip: TClip): Boolean;
var
Item: TMediaItem;
@ -1471,7 +1496,11 @@ begin
for C := 0 to Track.Count - 1 do
begin
Clip := Track[C];
if not IsOverlayClip(AProject, Clip) then
{ Плееру отдаём только прозрачные и с ключом. Остальное, что не
заполняет кадр, сводится заранее вместе с тем, что под ним:
так предпросмотр совпадает с роликом и не упирается в
процессор. }
if not IsPlayerOverlay(Clip) then
Continue;
Item := AProject.FindMedia(Clip.MediaId);
if Item = nil then
@ -1526,6 +1555,7 @@ begin
Ov.TlEnd := RangeB[J];
Ov.FilePath := Item.EffectivePath;
Ov.SrcStart := Clip.SrcIn + (RangeA[J] - Clip.TlStart);
Ov.SrcLength := Item.Info.Duration;
Ov.Filter := OverlayFilter(Clip.Transform,
Item.Info.Width, Item.Info.Height,
AProject.Master.Width, AProject.Master.Height,
@ -1556,6 +1586,7 @@ begin
ImgReq.TlEnd := Ov.TlEnd;
Ov.FilePath := ImgReq.TargetPath;
Ov.SrcStart := 0;
Ov.SrcLength := ImgReq.Duration;
Ov.Filter := '';
if not FileExists(ImgReq.TargetPath) then
begin

View file

@ -33,6 +33,10 @@ uses
uIconData, uSubtitles, uTextMeasure, uTextForm, uExportForm,
uColorKeyForm;
{ Номер сборки и время. Файл создаётся build.sh при каждой сборке и в
репозиторий не входит. }
{$I uBuildInfo.inc}
const
{ Номера системных сообщений о перерисовке рамки окна. Берём числами, а
не из модуля Windows: тот переопределяет TRect и Point, и обычные
@ -130,6 +134,9 @@ type
FCheckMisplacedOnShow: Boolean;
FPreviewOverlayUrl: string;
FPreviewOverlayWindows: TOverlayWindows;
{ Лежат ли на диске все файлы наложения. Входит в сравнение списков:
появление заготовки обязано вызвать перезагрузку. }
FPreviewOverlayReady: Boolean;
{ Списки кусков звуковых дорожек для плеера. }
FPreviewAudioUrls: array of string;
{ Звуковые дорожки предпросмотра — их номера в проекте, а не сами
@ -1578,7 +1585,11 @@ begin
ProjectName := ExtractFileName(FProject.FileName);
if FProject.Modified then
ProjectName := ProjectName + ' *';
Caption := 'nlve — ' + ProjectName;
{ Номер сборки в заголовке: по нему сразу видно, та ли это сборка, где
правка уже есть. Без него проверяют старым двоичным файлом, видят
прежнее поведение, и время уходит на выяснение, у кого что не
работает. }
Caption := Format('nlve %d — %s', [BuildNumber, ProjectName]);
end;
procedure TMainForm.UpdateTimeLabel;
@ -2392,6 +2403,29 @@ var
Inc(N);
end;
{ Заполнить промежуток повторами файла, не прося у него больше, чем в
нём есть. Длина неизвестна — кладём одной записью, как было. }
procedure FillGap(const AOv: TPreviewOverlay; const AFrom, ATo: Double);
var
Left, Step: Double;
begin
Left := ATo - AFrom;
if AOv.SrcLength <= 0.001 then
begin
Push(AOv.FilePath, 0, Left);
Exit;
end;
{ Полсекунды запаса: длина файла у ffmpeg и у mpv расходится в
последнем знаке, и запись ровно во весь файл даёт то же самое
предупреждение об обрезке. }
Step := Max(0.5, AOv.SrcLength - 0.5);
while Left > 0.001 do
begin
Push(AOv.FilePath, 0, Min(Step, Left));
Left := Left - Step;
end;
end;
begin
FPreviewOverlayUrl := '';
FPreviewOverlayWindows := nil;
@ -2402,9 +2436,16 @@ begin
At := 0;
for I := 0 to High(AOverlays) do
begin
{ Промежуток до этого куска — заполнитель из того же файла. }
{ Промежуток до этого куска — заполнитель из того же файла, нарезанный
по его длине.
Одной записью нельзя: mpv обрезает запись, которая просит больше,
чем есть в файле («entry uses 7082.687000 seconds, but file has only
15.021000»), дорожка оверлея оказывается короче монтажа и не
совмещается с ним по времени — наложение просто не показывается.
Замерено на монтаже в два часа с картинкой на 7082-й секунде. }
if AOverlays[I].TlStart > At + 0.001 then
Push(AOverlays[I].FilePath, 0, AOverlays[I].TlStart - At);
FillGap(AOverlays[I], At, AOverlays[I].TlStart);
Push(AOverlays[I].FilePath, AOverlays[I].SrcStart,
AOverlays[I].TlEnd - AOverlays[I].TlStart);
At := AOverlays[I].TlEnd;
@ -2429,6 +2470,23 @@ begin
Exit;
end;
FPreviewOverlayUrl := BuildEdlUri(Segs);
{ Все ли файлы наложения уже есть на диске.
Картинка показывается заготовкой, а она делается не мгновенно. Пока
её нет, плееру нечего добавлять, и дорожки наложения он не заводит.
Когда заготовка появится, список кусков останется прежним — строка в
строку, — и перезагрузки не случится: предпросмотр сравнивает именно
строки. Поэтому готовность входит в сравнение наравне с ними.
Проверено на монтаже, где картинка так и не показывалась до первой
правки. }
FPreviewOverlayReady := True;
for I := 0 to N - 1 do
if not FileExists(Segs[I].FilePath) then
begin
FPreviewOverlayReady := False;
Break;
end;
end;
{ Делает ли идущая генерация то, что монтажу ещё нужно.
@ -2524,6 +2582,8 @@ begin
{ Наложение входит в сравнение наравне со звуком: сдвинули оверлей —
список кусков основы мог не измениться, а показывать надо иначе. }
AudioEdl := AudioEdl + '#' + FPreviewOverlayUrl;
if not FPreviewOverlayReady then
AudioEdl := AudioEdl + '@ждём';
for I := 0 to High(FPreviewOverlayWindows) do
AudioEdl := AudioEdl + Format('|%s@%d:%d:%s-%s',
[FPreviewOverlayWindows[I].Filter,
@ -2552,6 +2612,12 @@ begin
{ Внешний звук назначается до загрузки: он часть того, что открывается. }
FPlayer.SetAudioSources(FPreviewAudioUrls);
FPlayer.SetVideoOverlay(FPreviewOverlayUrl, FPreviewOverlayWindows);
{ Наложение — самая хрупкая часть предпросмотра: оно живёт отдельной
дорожкой, которой может не оказаться. Пишем в журнал, сколько окон
показа и готовы ли файлы, иначе «картинки нет» нечем объяснить. }
if FPreviewOverlayUrl <> '' then
Log(Format('Наложение: окон %d, файлы %s', [Length(FPreviewOverlayWindows),
BoolToStr(FPreviewOverlayReady, 'готовы', 'ещё делаются')]));
FPlayer.LoadUrl(Edl, Format('start=%s,pause=%s',
[SecondsToStr(Pos), BoolToStr(WasPaused, 'yes', 'no')]));
FLastLoadedEdl := Edl;

View file

@ -1093,6 +1093,10 @@ begin
if Graph = FLastGraph then
Exit;
FLastGraph := Graph;
{ В журнал пишется всегда, в том числе пустая: по ней видно, собралось
ли наложение. Пустая цепочка при заявленном наложении означает, что
дорожки для него у плеера нет. }
Note('Цепочка показа: ' + Graph);
mpv_set_property_string(FHandle, 'lavfi-complex', PAnsiChar(Graph));
end;
@ -1103,7 +1107,7 @@ end;
она заменила бы картинку. Её дело — быть входом цепочки. }
procedure TMpvPlayer.AttachOverlay;
var
Count, I: Integer;
Count, I, Code: Integer;
Kind, Ext, Id: string;
begin
if FHandle = nil then
@ -1113,7 +1117,12 @@ begin
FOverlayVid := '';
if (FOverlayUrl <> '') and (Length(FOverlayWindows) > 0) then
Command(['video-add', FOverlayUrl, 'auto']);
begin
Code := Command(['video-add', FOverlayUrl, 'auto']);
if Code < 0 then
Note(Format('Дорожку наложения добавить не удалось: video-add вернул %d',
[Code]));
end;
Count := StrToIntDef(GetPropString('track-list/count'), 0);
for I := 0 to Count - 1 do
@ -1133,6 +1142,13 @@ begin
else if FBaseVid = '' then
FBaseVid := Id;
end;
{ Пустая дорожка наложения при заявленном наложении — самый частый
способ остаться без картинки в окне. Пишем, что увидели. }
if (FOverlayUrl <> '') and (Length(FOverlayWindows) > 0) and
((FOverlayVid = '') or (FBaseVid = '')) then
Note(Format('Наложение не подключилось: дорожек %d, основа "%s", ' +
'наложение "%s"', [Count, FBaseVid, FOverlayVid]));
end;
{ Субтитры подключаются двумя путями, и оба нужны.

View file

@ -1134,17 +1134,29 @@ begin
Check(Pos('pad=', Segs[0].Layers[1].FitFilter) = 0,
'у верхнего слоя полей нет');
{ Предпросмотру картинка отдаётся готовым куском, а не самим файлом:
своих декодеров у плеера хватает не на всё, а у неподвижного кадра
нет длины. }
{ Предпросмотр идёт тем же путём, что и сборка: участок сводится
заранее. Плееру его не отдаём — цепочка наложения работает всё
время показа и на большом кадре уводит видео от звука. }
FlattenPreview(Proj, Segs, AudioSegs, Overlays, Missing, True);
Check(Length(Overlays) = 1, 'картинка ушла в накладываемые');
Check(Length(Overlays) = 0,
'непрозрачная картинка плееру не отдаётся');
Check((Length(Segs) = 1) and (Length(Segs[0].Layers) = 2),
'в предпросмотре тот же кусок со слоями, что и в сборке');
{ А вот прозрачная уходит плееру — и тоже готовым куском, а не самим
файлом: своих декодеров у него хватает не на всё, а у неподвижного
кадра нет длины. }
Clip.Opacity := 0.5;
FlattenPreview(Proj, Segs, AudioSegs, Overlays, Missing, True);
Check(Length(Overlays) = 1, 'прозрачная картинка ушла в накладываемые');
if Length(Overlays) = 1 then
begin
Check(Pos('.nlve-cache', Overlays[0].FilePath) > 0,
'показывается готовый кусок, а не сам файл');
Check(Pos('pad=', Overlays[0].Filter) = 0,
'полей в цепочке наложения нет');
Check(Overlays[0].SrcLength > 0,
'длина куска известна: заполнитель нарежут по ней');
PlainPath := '';
for I := 0 to High(Missing) do
if Missing[I].TargetPath = Overlays[0].FilePath then