diff --git a/.gitignore b/.gitignore index dc3df30..73060c4 100644 --- a/.gitignore +++ b/.gitignore @@ -92,3 +92,7 @@ shot*.png # Мусор компоновщика Lazarus. link*.res + +# Номер сборки и созданный из него файл: про эту машину, не про историю. +/build.number +/src/uBuildInfo.inc diff --git a/README.md b/README.md index 03471bb..31073c2 100644 --- a/README.md +++ b/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`. +Оба файла в репозиторий не входят: номер про эту машину, а не про +историю проекта. + ## Структура | Файл | Что это | diff --git a/build.sh b/build.sh index 2c2d87d..233d639 100755 --- a/build.sh +++ b/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" < 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 diff --git a/src/uMainForm.pas b/src/uMainForm.pas index 1fb2793..2e146fc 100644 --- a/src/uMainForm.pas +++ b/src/uMainForm.pas @@ -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; diff --git a/src/uMpvPlayer.pas b/src/uMpvPlayer.pas index 9d7ebdd..34c9cd6 100644 --- a/src/uMpvPlayer.pas +++ b/src/uMpvPlayer.pas @@ -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; { Субтитры подключаются двумя путями, и оба нужны. diff --git a/tests/nlvetool.lpr b/tests/nlvetool.lpr index a10b681..d9488b5 100644 --- a/tests/nlvetool.lpr +++ b/tests/nlvetool.lpr @@ -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