diff --git a/README.md b/README.md index 92e26a0..e462982 100644 --- a/README.md +++ b/README.md @@ -840,6 +840,16 @@ Wine с новой библиотекой не грузилось ничего. привычный RGB. Записанный по привычке код даёт цвет с переставленными красным и синим. +**Список недостающего — не список нужного.** Недостающее у нас — это то, +чего нет на диске. Но у куска, который делается прямо сейчас, файл уже +есть: ffmpeg создаёт его в первую же секунду и пишет до конца. Проверка +«нужна ли ещё эта работа» спрашивала по списку недостающего — и отвечала +«не нужна» про то, что делается. Работа отменялась в разгаре и начиналась +заново. На коротком затухании это проходило незаметно, на затухании в +семьдесят секунд сборка не заканчивалась никогда: любая правка сбрасывала +её. Теперь уплощение отдельно отдаёт список нужного, независимо от того, +лежит файл на диске или ещё пишется (`tests/genrestart.sh`). + **MousePos у колеса мыши — не экранный.** По описанию `DoMouseWheel` получает точку в координатах экрана, но под gtk2 в ней уже клиентские: `ScreenToClient` переводит их второй раз, и точка уезжает влево ровно на @@ -935,6 +945,21 @@ Wine с новой библиотекой не грузилось ничего. уход выглядит как накапливающийся рассинхрон, которого нет. На это уже попались один раз. +Ещё три проверки гоняют саму программу на невидимом экране (`Xvfb :99`). +Рабочий экран они не занимают: + +``` +./tests/buttonclicks.sh <окно> каждый щелчок по кнопке считается +./tests/zoomanchor.py <окно> масштаб берётся от точки под мышкой +./tests/genrestart.sh [<путь к nlve>] сборка вставки не сбрасывается зря +``` + +Последней нужен проект с одним клипом и долгим — секунд на семьдесят — +затуханием: такой кусок сводится около полутора минут, и всё это время +человек продолжает работать. Путь к проекту задаётся переменной +`NLVE_FADE_PROJECT`; в репозиторий он не входит, потому что ссылается на +съёмку, которой здесь нет. + ## Структура | Файл | Что это | diff --git a/src/uFlatten.pas b/src/uFlatten.pas index aa58bc5..7c85c48 100644 --- a/src/uFlatten.pas +++ b/src/uFlatten.pas @@ -119,7 +119,8 @@ function FlattenStream(AProject: TProject; AKind: TTrackKind; списком кусков, а дальше они складываются его же средствами. } function FlattenAudioTrack(AProject: TProject; ATrack: TTrack; 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; @@ -196,9 +197,14 @@ function GeneratedPath(const ACacheDir: string; const AReq: TGenRequest): string Для сборки ролика это не применяется: там наложение считается заново с параметрами вывода, и промежуточный файл был бы лишней потерей - качества. } + качества. + + AWanted — куда сложить пути всех сведённых кусков, которые этой + расстановке нужны, независимо от того, лежат они уже на диске или ещё + делаются. По списку недостающего этого не узнать: файл появляется в + первую же секунду работы ffmpeg и пишется до конца. } procedure ResolveLayers(AProject: TProject; var ASegments: TFlatSegmentArray; - var AMissing: TGenRequests); + var AMissing: TGenRequests; AWanted: TStrings = nil); { Заготовка чёрной вставки самой ходовой длины. @@ -416,7 +422,7 @@ begin end; procedure ResolveLayers(AProject: TProject; var ASegments: TFlatSegmentArray; - var AMissing: TGenRequests); + var AMissing: TGenRequests; AWanted: TStrings); var I: Integer; Req: TGenRequest; @@ -443,6 +449,16 @@ begin Req.TlEnd := At + ASegments[I].Duration; At := Req.TlEnd; + { Список нужного — это не то же самое, что список недостающего. + + Недостающее — чего нет на диске. Но пока кусок делается, файл уже + есть: ffmpeg создаёт его сразу и пишет в него до самого конца. + Спрашивать «нужен ли ещё тот кусок, что сейчас делается» по списку + недостающего бессмысленно — его там не будет, и работа отменится в + разгаре. } + if AWanted <> nil then + AWanted.Add(Req.TargetPath); + if not FileExists(Req.TargetPath) then begin SetLength(AMissing, Length(AMissing) + 1); @@ -1115,7 +1131,8 @@ end; function FlattenAudioTrack(AProject: TProject; ATrack: TTrack; const ATotal: Double; - out ASegments: TFlatSegmentArray; out AMissing: TGenRequests): Boolean; + out ASegments: TFlatSegmentArray; out AMissing: TGenRequests; + AWanted: TStrings): Boolean; var C, N: Integer; At: Double; @@ -1160,6 +1177,11 @@ var Req.Duration := BucketFor(Take); Req.TargetPath := GeneratedPath(AProject.CacheDir, Req); Seg.FilePath := Req.TargetPath; + { Нужное и недостающее — разные списки: см. пояснение у + ResolveLayers. Кусок, который делается прямо сейчас, на диске уже + есть, и в недостающее он не попадёт. } + if AWanted <> nil then + AWanted.Add(Req.TargetPath); { Один и тот же файл не заказываем дважды: длинная тишина набирается повторами, и без этой проверки минутный кусок делался бы столько раз, сколько его повторов. } diff --git a/src/uMainForm.pas b/src/uMainForm.pas index 038b877..fb0efd7 100644 --- a/src/uMainForm.pas +++ b/src/uMainForm.pas @@ -2327,11 +2327,29 @@ end; одна из готовящихся вставок в новом списке не нужна, работу можно прекращать. Ошибиться тут не страшно: отменённое, если оно всё же понадобится, закажут заново на следующей пересборке. } +{ Нужна ли ещё та работа, что идёт сейчас. + + Спрашивать по одному только списку недостающего нельзя, и это стоило + повисшей сборки. Недостающее — то, чего нет на диске; а у куска, + который делается прямо сейчас, файл уже есть: ffmpeg создаёт его в + первую же секунду и пишет до конца. Поэтому сведённый кусок в списке + недостающего не появлялся, проверка отвечала «не нужен», работа + отменялась в разгаре — и начиналась заново с нуля. + + На коротком затухании это проходило незаметно: кусок успевал + посчитаться между правками. На затухании в семьдесят секунд, которое + считается полторы минуты, любая следующая правка сбрасывала работу, и + сборка не заканчивалась никогда. + + Поэтому спрашиваем иначе: нужен ли расстановке сам этот путь — неважно, + лежит он уже на диске или ещё пишется. } function TMainForm.GenerationStillNeeded: Boolean; var - Segs, Auds: TFlatSegmentArray; + Segs, Auds, TrackSegs: TFlatSegmentArray; Ovl: TPreviewOverlays; - Missing: TGenRequests; + Missing, TrackMissing: TGenRequests; + Wanted: TStringList; + TotalLen: Double; I, J: Integer; begin Result := False; @@ -2341,6 +2359,33 @@ begin FSettings.PreviewKey) then 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 J := 0 to FGenerate.TargetCount - 1 do if Missing[I].TargetPath = FGenerate.TargetPath(J) then diff --git a/tests/genrestart.sh b/tests/genrestart.sh new file mode 100755 index 0000000..77f18a8 --- /dev/null +++ b/tests/genrestart.sh @@ -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