Сборка вставки не начинается заново от каждой правки
Затухание в семьдесят секунд не собиралось никогда: сборка сведённого куска шла полторы минуты, и любая правка в это время сбрасывала её на ноль. В журнале это видно как «Расстановка изменилась» и тот же самый запуск ffmpeg по кругу. Причина — в вопросе, который задавался. «Нужна ли ещё эта работа» проверялось по списку недостающего, а недостающее у нас — это то, чего нет на диске. У куска, который делается прямо сейчас, файл уже есть: ffmpeg создаёт его в первую же секунду. Кусок в список не попадал, ответ был «не нужна», работа отменялась. Теперь уплощение отдельно отдаёт список нужного — независимо от того, лежит файл на диске или ещё пишется. Заодно закрыта та же дыра со вставками тишины: проверка их вообще не видела, потому что звуковые дорожки считаются отдельным проходом. Проверено обеими сторонами (tests/genrestart.sh, на настоящем материале, затухание 70 с): пять резов в стороне от затухания — один запуск и ни одной отмены, было шесть запусков и десять отмен. Удалили всё — работа прекращается за две секунды, как и прежде.
This commit is contained in:
parent
235cd00e9a
commit
d2a49343c3
4 changed files with 229 additions and 7 deletions
25
README.md
25
README.md
|
|
@ -840,6 +840,16 @@ Wine с новой библиотекой не грузилось ничего.
|
||||||
привычный RGB. Записанный по привычке код даёт цвет с переставленными
|
привычный RGB. Записанный по привычке код даёт цвет с переставленными
|
||||||
красным и синим.
|
красным и синим.
|
||||||
|
|
||||||
|
**Список недостающего — не список нужного.** Недостающее у нас — это то,
|
||||||
|
чего нет на диске. Но у куска, который делается прямо сейчас, файл уже
|
||||||
|
есть: ffmpeg создаёт его в первую же секунду и пишет до конца. Проверка
|
||||||
|
«нужна ли ещё эта работа» спрашивала по списку недостающего — и отвечала
|
||||||
|
«не нужна» про то, что делается. Работа отменялась в разгаре и начиналась
|
||||||
|
заново. На коротком затухании это проходило незаметно, на затухании в
|
||||||
|
семьдесят секунд сборка не заканчивалась никогда: любая правка сбрасывала
|
||||||
|
её. Теперь уплощение отдельно отдаёт список нужного, независимо от того,
|
||||||
|
лежит файл на диске или ещё пишется (`tests/genrestart.sh`).
|
||||||
|
|
||||||
**MousePos у колеса мыши — не экранный.** По описанию `DoMouseWheel`
|
**MousePos у колеса мыши — не экранный.** По описанию `DoMouseWheel`
|
||||||
получает точку в координатах экрана, но под gtk2 в ней уже клиентские:
|
получает точку в координатах экрана, но под gtk2 в ней уже клиентские:
|
||||||
`ScreenToClient` переводит их второй раз, и точка уезжает влево ровно на
|
`ScreenToClient` переводит их второй раз, и точка уезжает влево ровно на
|
||||||
|
|
@ -935,6 +945,21 @@ Wine с новой библиотекой не грузилось ничего.
|
||||||
уход выглядит как накапливающийся рассинхрон, которого нет. На это уже
|
уход выглядит как накапливающийся рассинхрон, которого нет. На это уже
|
||||||
попались один раз.
|
попались один раз.
|
||||||
|
|
||||||
|
Ещё три проверки гоняют саму программу на невидимом экране (`Xvfb :99`).
|
||||||
|
Рабочий экран они не занимают:
|
||||||
|
|
||||||
|
```
|
||||||
|
./tests/buttonclicks.sh <окно> <x> <y> каждый щелчок по кнопке считается
|
||||||
|
./tests/zoomanchor.py <окно> масштаб берётся от точки под мышкой
|
||||||
|
./tests/genrestart.sh [<путь к nlve>] сборка вставки не сбрасывается зря
|
||||||
|
```
|
||||||
|
|
||||||
|
Последней нужен проект с одним клипом и долгим — секунд на семьдесят —
|
||||||
|
затуханием: такой кусок сводится около полутора минут, и всё это время
|
||||||
|
человек продолжает работать. Путь к проекту задаётся переменной
|
||||||
|
`NLVE_FADE_PROJECT`; в репозиторий он не входит, потому что ссылается на
|
||||||
|
съёмку, которой здесь нет.
|
||||||
|
|
||||||
## Структура
|
## Структура
|
||||||
|
|
||||||
| Файл | Что это |
|
| Файл | Что это |
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,8 @@ function FlattenStream(AProject: TProject; AKind: TTrackKind;
|
||||||
списком кусков, а дальше они складываются его же средствами. }
|
списком кусков, а дальше они складываются его же средствами. }
|
||||||
function FlattenAudioTrack(AProject: TProject; ATrack: TTrack;
|
function FlattenAudioTrack(AProject: TProject; ATrack: TTrack;
|
||||||
const ATotal: Double;
|
const ATotal: Double;
|
||||||
out ASegments: TFlatSegmentArray; out AMissing: TGenRequests): Boolean;
|
out ASegments: TFlatSegmentArray; out AMissing: TGenRequests;
|
||||||
|
AWanted: TStrings = nil): Boolean;
|
||||||
|
|
||||||
{ Длина слышимого материала одного вида дорожек. }
|
{ Длина слышимого материала одного вида дорожек. }
|
||||||
function AudibleEnd(AProject: TProject; AKind: TTrackKind): Double;
|
function AudibleEnd(AProject: TProject; AKind: TTrackKind): Double;
|
||||||
|
|
@ -196,9 +197,14 @@ function GeneratedPath(const ACacheDir: string; const AReq: TGenRequest): string
|
||||||
|
|
||||||
Для сборки ролика это не применяется: там наложение считается заново с
|
Для сборки ролика это не применяется: там наложение считается заново с
|
||||||
параметрами вывода, и промежуточный файл был бы лишней потерей
|
параметрами вывода, и промежуточный файл был бы лишней потерей
|
||||||
качества. }
|
качества.
|
||||||
|
|
||||||
|
AWanted — куда сложить пути всех сведённых кусков, которые этой
|
||||||
|
расстановке нужны, независимо от того, лежат они уже на диске или ещё
|
||||||
|
делаются. По списку недостающего этого не узнать: файл появляется в
|
||||||
|
первую же секунду работы ffmpeg и пишется до конца. }
|
||||||
procedure ResolveLayers(AProject: TProject; var ASegments: TFlatSegmentArray;
|
procedure ResolveLayers(AProject: TProject; var ASegments: TFlatSegmentArray;
|
||||||
var AMissing: TGenRequests);
|
var AMissing: TGenRequests; AWanted: TStrings = nil);
|
||||||
|
|
||||||
{ Заготовка чёрной вставки самой ходовой длины.
|
{ Заготовка чёрной вставки самой ходовой длины.
|
||||||
|
|
||||||
|
|
@ -416,7 +422,7 @@ begin
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure ResolveLayers(AProject: TProject; var ASegments: TFlatSegmentArray;
|
procedure ResolveLayers(AProject: TProject; var ASegments: TFlatSegmentArray;
|
||||||
var AMissing: TGenRequests);
|
var AMissing: TGenRequests; AWanted: TStrings);
|
||||||
var
|
var
|
||||||
I: Integer;
|
I: Integer;
|
||||||
Req: TGenRequest;
|
Req: TGenRequest;
|
||||||
|
|
@ -443,6 +449,16 @@ begin
|
||||||
Req.TlEnd := At + ASegments[I].Duration;
|
Req.TlEnd := At + ASegments[I].Duration;
|
||||||
At := Req.TlEnd;
|
At := Req.TlEnd;
|
||||||
|
|
||||||
|
{ Список нужного — это не то же самое, что список недостающего.
|
||||||
|
|
||||||
|
Недостающее — чего нет на диске. Но пока кусок делается, файл уже
|
||||||
|
есть: ffmpeg создаёт его сразу и пишет в него до самого конца.
|
||||||
|
Спрашивать «нужен ли ещё тот кусок, что сейчас делается» по списку
|
||||||
|
недостающего бессмысленно — его там не будет, и работа отменится в
|
||||||
|
разгаре. }
|
||||||
|
if AWanted <> nil then
|
||||||
|
AWanted.Add(Req.TargetPath);
|
||||||
|
|
||||||
if not FileExists(Req.TargetPath) then
|
if not FileExists(Req.TargetPath) then
|
||||||
begin
|
begin
|
||||||
SetLength(AMissing, Length(AMissing) + 1);
|
SetLength(AMissing, Length(AMissing) + 1);
|
||||||
|
|
@ -1115,7 +1131,8 @@ end;
|
||||||
|
|
||||||
function FlattenAudioTrack(AProject: TProject; ATrack: TTrack;
|
function FlattenAudioTrack(AProject: TProject; ATrack: TTrack;
|
||||||
const ATotal: Double;
|
const ATotal: Double;
|
||||||
out ASegments: TFlatSegmentArray; out AMissing: TGenRequests): Boolean;
|
out ASegments: TFlatSegmentArray; out AMissing: TGenRequests;
|
||||||
|
AWanted: TStrings): Boolean;
|
||||||
var
|
var
|
||||||
C, N: Integer;
|
C, N: Integer;
|
||||||
At: Double;
|
At: Double;
|
||||||
|
|
@ -1160,6 +1177,11 @@ var
|
||||||
Req.Duration := BucketFor(Take);
|
Req.Duration := BucketFor(Take);
|
||||||
Req.TargetPath := GeneratedPath(AProject.CacheDir, Req);
|
Req.TargetPath := GeneratedPath(AProject.CacheDir, Req);
|
||||||
Seg.FilePath := Req.TargetPath;
|
Seg.FilePath := Req.TargetPath;
|
||||||
|
{ Нужное и недостающее — разные списки: см. пояснение у
|
||||||
|
ResolveLayers. Кусок, который делается прямо сейчас, на диске уже
|
||||||
|
есть, и в недостающее он не попадёт. }
|
||||||
|
if AWanted <> nil then
|
||||||
|
AWanted.Add(Req.TargetPath);
|
||||||
{ Один и тот же файл не заказываем дважды: длинная тишина набирается
|
{ Один и тот же файл не заказываем дважды: длинная тишина набирается
|
||||||
повторами, и без этой проверки минутный кусок делался бы столько
|
повторами, и без этой проверки минутный кусок делался бы столько
|
||||||
раз, сколько его повторов. }
|
раз, сколько его повторов. }
|
||||||
|
|
|
||||||
|
|
@ -2327,11 +2327,29 @@ end;
|
||||||
одна из готовящихся вставок в новом списке не нужна, работу можно
|
одна из готовящихся вставок в новом списке не нужна, работу можно
|
||||||
прекращать. Ошибиться тут не страшно: отменённое, если оно всё же
|
прекращать. Ошибиться тут не страшно: отменённое, если оно всё же
|
||||||
понадобится, закажут заново на следующей пересборке. }
|
понадобится, закажут заново на следующей пересборке. }
|
||||||
|
{ Нужна ли ещё та работа, что идёт сейчас.
|
||||||
|
|
||||||
|
Спрашивать по одному только списку недостающего нельзя, и это стоило
|
||||||
|
повисшей сборки. Недостающее — то, чего нет на диске; а у куска,
|
||||||
|
который делается прямо сейчас, файл уже есть: ffmpeg создаёт его в
|
||||||
|
первую же секунду и пишет до конца. Поэтому сведённый кусок в списке
|
||||||
|
недостающего не появлялся, проверка отвечала «не нужен», работа
|
||||||
|
отменялась в разгаре — и начиналась заново с нуля.
|
||||||
|
|
||||||
|
На коротком затухании это проходило незаметно: кусок успевал
|
||||||
|
посчитаться между правками. На затухании в семьдесят секунд, которое
|
||||||
|
считается полторы минуты, любая следующая правка сбрасывала работу, и
|
||||||
|
сборка не заканчивалась никогда.
|
||||||
|
|
||||||
|
Поэтому спрашиваем иначе: нужен ли расстановке сам этот путь — неважно,
|
||||||
|
лежит он уже на диске или ещё пишется. }
|
||||||
function TMainForm.GenerationStillNeeded: Boolean;
|
function TMainForm.GenerationStillNeeded: Boolean;
|
||||||
var
|
var
|
||||||
Segs, Auds: TFlatSegmentArray;
|
Segs, Auds, TrackSegs: TFlatSegmentArray;
|
||||||
Ovl: TPreviewOverlays;
|
Ovl: TPreviewOverlays;
|
||||||
Missing: TGenRequests;
|
Missing, TrackMissing: TGenRequests;
|
||||||
|
Wanted: TStringList;
|
||||||
|
TotalLen: Double;
|
||||||
I, J: Integer;
|
I, J: Integer;
|
||||||
begin
|
begin
|
||||||
Result := False;
|
Result := False;
|
||||||
|
|
@ -2341,6 +2359,33 @@ begin
|
||||||
FSettings.PreviewKey) then
|
FSettings.PreviewKey) then
|
||||||
Exit;
|
Exit;
|
||||||
|
|
||||||
|
Wanted := TStringList.Create;
|
||||||
|
try
|
||||||
|
ResolveLayers(FProject, Segs, Missing, Wanted);
|
||||||
|
|
||||||
|
{ Звуковые дорожки считаются тем же путём, что и при пересборке:
|
||||||
|
вставки тишины под дыры у каждой дорожки свои, и они тоже могут
|
||||||
|
делаться прямо сейчас. }
|
||||||
|
TotalLen := Max(AudibleEnd(FProject, tkVideo), AudibleEnd(FProject, tkAudio));
|
||||||
|
for I := 0 to FProject.TrackCount - 1 do
|
||||||
|
begin
|
||||||
|
if FProject.Tracks[I].Kind <> tkAudio then
|
||||||
|
Continue;
|
||||||
|
if not FProject.TrackAudible(FProject.Tracks[I]) then
|
||||||
|
Continue;
|
||||||
|
if FProject.Tracks[I].Count = 0 then
|
||||||
|
Continue;
|
||||||
|
FlattenAudioTrack(FProject, FProject.Tracks[I], TotalLen,
|
||||||
|
TrackSegs, TrackMissing, Wanted);
|
||||||
|
end;
|
||||||
|
|
||||||
|
for J := 0 to FGenerate.TargetCount - 1 do
|
||||||
|
if Wanted.IndexOf(FGenerate.TargetPath(J)) >= 0 then
|
||||||
|
Exit(True);
|
||||||
|
finally
|
||||||
|
Wanted.Free;
|
||||||
|
end;
|
||||||
|
|
||||||
for I := 0 to High(Missing) do
|
for I := 0 to High(Missing) do
|
||||||
for J := 0 to FGenerate.TargetCount - 1 do
|
for J := 0 to FGenerate.TargetCount - 1 do
|
||||||
if Missing[I].TargetPath = FGenerate.TargetPath(J) then
|
if Missing[I].TargetPath = FGenerate.TargetPath(J) then
|
||||||
|
|
|
||||||
130
tests/genrestart.sh
Executable file
130
tests/genrestart.sh
Executable file
|
|
@ -0,0 +1,130 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Сборка сведённого куска не должна начинаться заново от каждой правки.
|
||||||
|
#
|
||||||
|
# Затухание, наплыв, прозрачность — всё это предпросмотр не умеет играть
|
||||||
|
# как есть: такой кусок заранее сводится в отдельный файл. На затухании в
|
||||||
|
# семьдесят секунд файл считается около полутора минут, и всё это время
|
||||||
|
# человек продолжает работать.
|
||||||
|
#
|
||||||
|
# Проверяется два обратных случая:
|
||||||
|
# 1. Режем монтаж в стороне от затухания. Сам кусок от этого не
|
||||||
|
# меняется — значит сборка обязана продолжаться, а не начинаться
|
||||||
|
# заново.
|
||||||
|
# 2. Удаляем всё. Кусок стал не нужен — сборка обязана прекратиться.
|
||||||
|
#
|
||||||
|
# Раньше первый случай отменял работу: «нужен ли ещё этот кусок»
|
||||||
|
# спрашивалось по списку недостающего, а у куска, который делается прямо
|
||||||
|
# сейчас, файл уже есть — ffmpeg создаёт его сразу. Кусок в список не
|
||||||
|
# попадал, работа отменялась в разгаре и начиналась с нуля.
|
||||||
|
#
|
||||||
|
# Запуск: tests/genrestart.sh [<путь к nlve>]
|
||||||
|
# Проект берётся из test/поток/длинный-фейд.nlve (см. README, раздел
|
||||||
|
# проверок): один клип с затуханием в семьдесят секунд.
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
BIN=${1:-$PWD/nlve}
|
||||||
|
DISP=${NLVE_DISPLAY:-:99}
|
||||||
|
# Проект для проверки в репозиторий не входит: он ссылается на съёмку,
|
||||||
|
# которой в нём нет. Годится любой с одним клипом и долгим затуханием —
|
||||||
|
# путь можно задать переменной.
|
||||||
|
PROJ=${NLVE_FADE_PROJECT:-$PWD/test/поток/длинный-фейд.nlve}
|
||||||
|
LOG=$(dirname "$BIN")/nlve.log
|
||||||
|
|
||||||
|
[ -f "$PROJ" ] || { echo "нет проекта для проверки: $PROJ"; exit 2; }
|
||||||
|
|
||||||
|
start() {
|
||||||
|
pkill -x nlve 2>/dev/null || true
|
||||||
|
# Закрытая программа не уносит с собой свой ffmpeg: дочерний процесс
|
||||||
|
# остаётся и продолжает писать в кэш. Убираем только его — по пути
|
||||||
|
# этого самого проекта, чтобы не задеть чужую работу.
|
||||||
|
pkill -f "длинный-фейд.nlve-cache" 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
rm -rf "$PWD/test/поток/длинный-фейд.nlve-cache" "$PWD/test/поток/профиль-$1"
|
||||||
|
rm -f "$PROJ.autosave"
|
||||||
|
SP=$PWD/test/поток/профиль-$1
|
||||||
|
mkdir -p "$SP"
|
||||||
|
: > "$LOG"
|
||||||
|
XDG_CONFIG_HOME=$SP XDG_CACHE_HOME=$SP XDG_DATA_HOME=$SP DISPLAY=$DISP \
|
||||||
|
"$BIN" "$PROJ" >/dev/null 2>&1 &
|
||||||
|
sleep 16
|
||||||
|
WIN=$(DISPLAY=$DISP xdotool search --name "длинный-фейд" | tail -1)
|
||||||
|
DISPLAY=$DISP xdotool windowmove "$WIN" 0 0 windowsize "$WIN" 1500 860
|
||||||
|
sleep 2
|
||||||
|
}
|
||||||
|
|
||||||
|
runs() { grep -c filter_complex "$LOG" || true; }
|
||||||
|
stops() { grep -c 'Расстановка изменилась' "$LOG" || true; }
|
||||||
|
|
||||||
|
BAD=0
|
||||||
|
|
||||||
|
echo "1. Правки в стороне от затухания"
|
||||||
|
start правки
|
||||||
|
echo " сборка началась: запусков $(runs)"
|
||||||
|
X=650
|
||||||
|
I=0
|
||||||
|
while [ $I -lt 5 ]; do
|
||||||
|
DISPLAY=$DISP xdotool mousemove $X 640 click 1
|
||||||
|
sleep 0.6
|
||||||
|
DISPLAY=$DISP xdotool key s
|
||||||
|
sleep 3
|
||||||
|
X=$((X + 40))
|
||||||
|
I=$((I + 1))
|
||||||
|
done
|
||||||
|
sleep 5
|
||||||
|
N=$(runs)
|
||||||
|
S=$(stops)
|
||||||
|
echo " после пяти резов: запусков $N, отмен $S"
|
||||||
|
case "$(DISPLAY=$DISP xdotool getwindowname "$WIN")" in
|
||||||
|
*\*) ;;
|
||||||
|
*) echo " ПЛОХО: монтаж не изменился, правки не дошли — проверка ничего не значит"
|
||||||
|
BAD=$((BAD + 1)) ;;
|
||||||
|
esac
|
||||||
|
if [ "$N" = "1" ] && [ "$S" = "0" ]; then
|
||||||
|
echo " ok: работа не сбрасывалась"
|
||||||
|
else
|
||||||
|
echo " ПЛОХО: сборка начиналась заново"
|
||||||
|
BAD=$((BAD + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "2. Кусок стал не нужен"
|
||||||
|
start отмена
|
||||||
|
echo " сборка началась: запусков $(runs)"
|
||||||
|
DISPLAY=$DISP xdotool mousemove 700 600 click 1
|
||||||
|
sleep 0.5
|
||||||
|
DISPLAY=$DISP xdotool key ctrl+a
|
||||||
|
sleep 0.8
|
||||||
|
DISPLAY=$DISP xdotool key Delete
|
||||||
|
|
||||||
|
# Ждём, пока работа прекратится, а не спим наугад: ffmpeg получает сигнал
|
||||||
|
# и ещё дописывает то, что у него в руках. Именно -x у pgrep: иначе под
|
||||||
|
# раздачу попадёт и сама эта оболочка, у которой слово ffmpeg есть в
|
||||||
|
# командной строке.
|
||||||
|
ALIVE=1
|
||||||
|
WAITED=0
|
||||||
|
while [ "$WAITED" -lt 30 ]; do
|
||||||
|
sleep 1
|
||||||
|
WAITED=$((WAITED + 1))
|
||||||
|
ALIVE=$(pgrep -a -x ffmpeg | grep -c "длинный-фейд.nlve-cache" || true)
|
||||||
|
[ "$ALIVE" = "0" ] && break
|
||||||
|
done
|
||||||
|
S=$(stops)
|
||||||
|
echo " отмен $S, живых ffmpeg $ALIVE, ждали $WAITED с"
|
||||||
|
if [ "$S" -ge 1 ] && [ "$ALIVE" = "0" ]; then
|
||||||
|
echo " ok: ненужная работа прекращена"
|
||||||
|
else
|
||||||
|
echo " ПЛОХО: работа продолжается впустую"
|
||||||
|
BAD=$((BAD + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
pkill -x nlve 2>/dev/null || true
|
||||||
|
pkill -f "длинный-фейд.nlve-cache" 2>/dev/null || true
|
||||||
|
rm -f "$PROJ.autosave"
|
||||||
|
|
||||||
|
echo
|
||||||
|
if [ "$BAD" = "0" ]; then
|
||||||
|
echo "ВЕРДИКТ: сборка вставок не сбрасывается зря и не идёт впустую"
|
||||||
|
else
|
||||||
|
echo "ВЕРДИКТ: не пройдено случаев: $BAD"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Loading…
Add table
Add a link
Reference in a new issue