Наложение в предпросмотре сводится заранее; номер сборки в заголовке
Клип, не заполняющий кадр, стал уходить плееру отдельной дорожкой, и весь показ пошёл через цепочку фильтров. Цепочка работает всё время показа и считается процессором: на кадре 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:
parent
766f1ad196
commit
567c518a62
7 changed files with 192 additions and 16 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -92,3 +92,7 @@ shot*.png
|
|||
|
||||
# Мусор компоновщика Lazarus.
|
||||
link*.res
|
||||
|
||||
# Номер сборки и созданный из него файл: про эту машину, не про историю.
|
||||
/build.number
|
||||
/src/uBuildInfo.inc
|
||||
|
|
|
|||
30
README.md
30
README.md
|
|
@ -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`.
|
||||
Оба файла в репозиторий не входят: номер про эту машину, а не про
|
||||
историю проекта.
|
||||
|
||||
## Структура
|
||||
|
||||
| Файл | Что это |
|
||||
|
|
|
|||
29
build.sh
29
build.sh
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
{ Субтитры подключаются двумя путями, и оба нужны.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue