From 0bbff345ba15121877419d86b29c4d78b92c5831 Mon Sep 17 00:00:00 2001 From: 4aiman Date: Mon, 31 Aug 2026 12:41:26 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B2=D0=B5=D1=80=D0=BB=D0=B5=D0=B9=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D0=BF=D0=BE=D1=80=D1=8F=D0=B4=D0=BA=D1=83=20?= =?UTF-8?q?=D0=B4=D0=BE=D1=80=D0=BE=D0=B6=D0=B5=D0=BA,=20=D1=82=D0=B5?= =?UTF-8?q?=D0=BA=D1=81=D1=82=20=D0=B2=20=D0=BE=D0=BA=D0=BD=D0=B5,=20?= =?UTF-8?q?=D0=B8=D0=BC=D0=B5=D0=BD=D0=B0=20=D0=B4=D0=BE=D1=80=D0=BE=D0=B6?= =?UTF-8?q?=D0=B5=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Миникарта была видна в окне, но не в ролике. Предпросмотр рисовал оверлей поверх всего, не глядя на порядок дорожек, а сборка живёт по правилу «верхний клип побеждает целиком»: фотография лежала выше оверлея и в ролике его закрывала. Теперь окно считает так же — оверлей показывается только там, где его не закрывает сплошной клип с дорожки выше, а закрытый посередине разбивается на несколько окон показа. Текст не появлялся в окне. Разметка лежит в файле с одним именем на весь проект, а плеер сравнивал имена: имя прежнее — перечитывать нечего. Правка доходила до окна только вместе с перезагрузкой списка кусков. Теперь сравнивается сама разметка, и при изменении файл перечитывается. Отдельно проверено, что цепочка lavfi-complex субтитрам не мешает. Две дорожки Т2. Имя новой дорожке давалось по счёту дорожек своего вида плюс единица, и после удаления средней счёт совпадал с занятым именем. Имя подбирается свободное, а повторы из старых проектов разводятся при чтении. Проверки 11 и 12 в nlvetool flattest. --- src/uFlatten.pas | 134 ++++++++++++++++++++++++++++++++++-------- src/uMainForm.pas | 34 +++++++---- src/uMpvPlayer.pas | 9 ++- src/uProject.pas | 57 ++++++++++++++++-- src/uTimelineCtrl.pas | 9 +-- tests/nlvetool.lpr | 59 +++++++++++++++++++ 6 files changed, 252 insertions(+), 50 deletions(-) diff --git a/src/uFlatten.pas b/src/uFlatten.pas index b8ac238..e97a318 100644 --- a/src/uFlatten.pas +++ b/src/uFlatten.pas @@ -38,6 +38,9 @@ uses Classes, SysUtils, uAppTypes, uProject, uMediaProbe, uTransform; type + { Набор отрезков времени: им описывают, где оверлей виден. } + TDoubleArray = array of Double; + { Клип, который в предпросмотре кладётся поверх основы. Место в кадре — то же самое, по чему сборка обкладывает картинку @@ -1086,6 +1089,52 @@ begin end; end; +{ Вырезает из набора отрезков указанный. Отрезок посередине делит тот, в + который попал, надвое: оверлей, закрытый в середине, показывается двумя + окнами. } +procedure CutRange(var AFrom, ATo: TDoubleArray; var ACount: Integer; + const ACutFrom, ACutTo: Double); +var + I, N: Integer; + NewFrom, NewTo: array of Double; + + procedure Push(const A, B: Double); + begin + if B <= A + 0.001 then + Exit; + NewFrom[N] := A; + NewTo[N] := B; + Inc(N); + end; + +begin + if ACutTo <= ACutFrom then + Exit; + SetLength(NewFrom, ACount * 2 + 2); + SetLength(NewTo, ACount * 2 + 2); + N := 0; + for I := 0 to ACount - 1 do + begin + { Не пересекаются — отрезок остаётся как был. } + if (ACutTo <= AFrom[I] + 0.001) or (ACutFrom >= ATo[I] - 0.001) then + Push(AFrom[I], ATo[I]) + else + begin + Push(AFrom[I], ACutFrom); + Push(ACutTo, ATo[I]); + end; + end; + + ACount := N; + SetLength(AFrom, N); + SetLength(ATo, N); + for I := 0 to N - 1 do + begin + AFrom[I] := NewFrom[I]; + ATo[I] := NewTo[I]; + end; +end; + { Клип кладётся поверх, а не побеждает целиком, если он не сплошной: с выбитым цветом или полупрозрачный. } function IsOverlayClip(AClip: TClip): Boolean; @@ -1102,11 +1151,13 @@ function FlattenPreview(AProject: TProject; var Hidden: TFPList; MissA: TGenRequests; - T, C, I, N: Integer; - Track: TTrack; + T, C, I, J, K, N: Integer; + Track, Above: TTrack; Clip: TClip; Item: TMediaItem; Total: Double; + RangeA, RangeB: TDoubleArray; + RangeCount: Integer; Ov: TPreviewOverlay; { Занято ли это время другим оверлеем. } @@ -1163,29 +1214,66 @@ begin Hidden.Add(Clip); - Ov := Default(TPreviewOverlay); - Ov.TlStart := Clip.TlStart; - Ov.TlEnd := Clip.TlEnd; - Ov.FilePath := Item.EffectivePath; - Ov.SrcStart := Clip.SrcIn; - Ov.Filter := OverlayFilter(Clip.Transform, - Item.Info.Width, Item.Info.Height, - AProject.Master.Width, AProject.Master.Height, - Ov.X, Ov.Y, Ov.Width, Ov.Height); - { Порядок тот же, что при сборке: сперва вписали в кадр, потом - выбили цвет, потом приглушили. Иначе смягчение края разошлось бы - с тем, что даст готовый ролик. } - if AWithKey then + { Оверлей виден не всюду, где лежит: сверху его может закрывать + сплошной клип, и в готовом ролике его там не будет — побеждает + верхняя дорожка целиком. Показывать в окне то, чего не будет в + ролике, нельзя: именно из-за такого расхождения человек собирает + ролик и не находит в нём миникарты. + + Закрывать могут и посередине — тогда от оверлея остаётся + несколько отрезков, и каждый становится своим окном показа. } + RangeCount := 1; + SetLength(RangeA, 1); + SetLength(RangeB, 1); + RangeA[0] := Clip.TlStart; + RangeB[0] := Clip.TlEnd; + + for K := 0 to T - 1 do begin - if Clip.ColorKey.Enabled then - Ov.Filter := Ov.Filter + ',' + ColorKeyFilter(Clip.ColorKey.Color, - Clip.ColorKey.Similarity, Clip.ColorKey.Blend); - if OpacityFilter(Clip.Opacity) <> '' then - Ov.Filter := Ov.Filter + ',' + OpacityFilter(Clip.Opacity); + Above := AProject.Tracks[K]; + if Above.Kind <> tkVideo then + Continue; + if not AProject.TrackAudible(Above) then + Continue; + for J := 0 to Above.Count - 1 do + { Клип с ключом или прозрачностью сам ничего не закрывает. } + if not IsOverlayClip(Above[J]) then + CutRange(RangeA, RangeB, RangeCount, + Above[J].TlStart, Above[J].TlEnd); + end; + + for J := 0 to RangeCount - 1 do + begin + if RangeB[J] <= RangeA[J] + 0.001 then + Continue; + if OverlapsTaken(RangeA[J], RangeB[J]) then + Continue; + + Ov := Default(TPreviewOverlay); + Ov.TlStart := RangeA[J]; + Ov.TlEnd := RangeB[J]; + Ov.FilePath := Item.EffectivePath; + Ov.SrcStart := Clip.SrcIn + (RangeA[J] - Clip.TlStart); + Ov.Filter := OverlayFilter(Clip.Transform, + Item.Info.Width, Item.Info.Height, + AProject.Master.Width, AProject.Master.Height, + Ov.X, Ov.Y, Ov.Width, Ov.Height); + { Порядок тот же, что при сборке: сперва вписали в кадр, потом + выбили цвет, потом приглушили. Иначе смягчение края разошлось + бы с тем, что даст готовый ролик. } + if AWithKey then + begin + if Clip.ColorKey.Enabled then + Ov.Filter := Ov.Filter + ',' + ColorKeyFilter(Clip.ColorKey.Color, + Clip.ColorKey.Similarity, Clip.ColorKey.Blend); + if OpacityFilter(Clip.Opacity) <> '' then + Ov.Filter := Ov.Filter + ',' + OpacityFilter(Clip.Opacity); + end; + + SetLength(AOverlays, N + 1); + AOverlays[N] := Ov; + Inc(N); end; - SetLength(AOverlays, N + 1); - AOverlays[N] := Ov; - Inc(N); end; end; diff --git a/src/uMainForm.pas b/src/uMainForm.pas index b82c17a..d7a05b7 100644 --- a/src/uMainForm.pas +++ b/src/uMainForm.pas @@ -101,6 +101,8 @@ type { Наложение для плеера: список кусков оверлея и окна показа. } { Журнал текущей сборки: файл рядом с готовым роликом. } FExportLogPath: string; + { Разметка текста, отданная плееру в прошлый раз. } + FLastAss: string; { Проект открыт из командной строки: спросить про клипы на чужих дорожках можно только когда окно уже показано. } FCheckMisplacedOnShow: Boolean; @@ -903,7 +905,7 @@ begin FProject.BeginChange; if Track = nil then - Track := FProject.AddTrackOfKind('Т1', tkText); + Track := FProject.AddTrackOfKind(FProject.UniqueTrackName('Т'), tkText); Clip := TClip.Create; Clip.Id := FProject.NewId('c'); @@ -1707,8 +1709,7 @@ begin if ATrackIndex < 0 then begin - FProject.AddTrack('V' + - IntToStr(FProject.TrackCountOf(tkVideo) + 1)); + FProject.AddTrack(FProject.UniqueTrackName('V')); ATrackIndex := FProject.TrackCount - 1; Log('Заведена дорожка ' + FProject.Tracks[ATrackIndex].Name); end; @@ -1738,11 +1739,9 @@ begin if Track = nil then begin if NeedKind = tkAudio then - Track := FProject.AddTrackOfKind('A' + - IntToStr(FProject.TrackCountOf(tkAudio) + 1), tkAudio) + Track := FProject.AddTrackOfKind(FProject.UniqueTrackName('A'), tkAudio) else - Track := FProject.AddTrack('V' + - IntToStr(FProject.TrackCountOf(tkVideo) + 1)); + Track := FProject.AddTrack(FProject.UniqueTrackName('V')); end; Log(Format('Дорожка %s не для этого вида клипов — положил на %s.', [Wanted, Track.Name])); @@ -2894,18 +2893,30 @@ end; Пересобирать цепочку на каждый кадр нельзя — она создаётся заново и звук щёлкает. Поэтому только при изменении. } +{ Разметка текста для предпросмотра. + + Сравнивается сама разметка, а не имя файла: имя у неё одно на весь + проект, и правка текста его не меняет. Плеер же, увидев прежнее имя, + файл не перечитывал — и новая надпись в окне не появлялась вовсе, пока + не перезагрузится весь список кусков. } procedure TMainForm.UpdateSubtitles; var - Path: string; + { Не Text: так называется свойство формы. } + Path, Markup: string; begin if (FProject = nil) or (FPlayer = nil) then Exit; if FProject.CacheDir = '' then Exit; + Markup := BuildAss(FProject); + if Markup = FLastAss then + Exit; + FLastAss := Markup; + Path := IncludeTrailingPathDelimiter(FProject.CacheDir) + 'subs.ass'; - if WriteAssFile(FProject, Path) then - FPlayer.SetSubtitleFile(Path) + if (Markup <> '') and WriteAssFile(FProject, Path) then + FPlayer.SetSubtitleFile(Path, True) else FPlayer.SetSubtitleFile(''); end; @@ -3529,8 +3540,7 @@ end; procedure TMainForm.MenuAddTrack(Sender: TObject); begin FProject.BeginChange; - FProject.AddTrack('V' + - IntToStr(FProject.TrackCountOf(tkVideo) + 1)); + FProject.AddTrack(FProject.UniqueTrackName('V')); FTimeline.Invalidate; UpdateCaption; end; diff --git a/src/uMpvPlayer.pas b/src/uMpvPlayer.pas index 20ac3b5..8fdc762 100644 --- a/src/uMpvPlayer.pas +++ b/src/uMpvPlayer.pas @@ -218,7 +218,10 @@ type Держится отдельно от списка кусков: текст правится чаще, чем монтаж, и перезагружать из-за него картинку незачем. } - procedure SetSubtitleFile(const APath: string); + { AForce — перечитать файл, даже если имя прежнее. Нужно всегда, когда + разметка переписана: имя у неё одно на весь проект, и по имени + изменение не видно. } + procedure SetSubtitleFile(const APath: string; AForce: Boolean = False); end; implementation @@ -1096,9 +1099,9 @@ end; подключаем новые, не трогая воспроизведение. При загрузке нового списка кусков mpv забывает все внешние дорожки, поэтому там подключение повторяется — см. SelectExternalAudio. } -procedure TMpvPlayer.SetSubtitleFile(const APath: string); +procedure TMpvPlayer.SetSubtitleFile(const APath: string; AForce: Boolean); begin - if FSubtitlePath = APath then + if (FSubtitlePath = APath) and (not AForce) then Exit; FSubtitlePath := APath; if FHandle = nil then diff --git a/src/uProject.pas b/src/uProject.pas index 6aac70c..296bb31 100644 --- a/src/uProject.pas +++ b/src/uProject.pas @@ -282,6 +282,14 @@ type { Номер новой связки. Отдельным именем, чтобы не пришлось помнить префикс в каждом месте, где связка заводится. } function NewGroupId: string; + { Имя, которого ещё нет ни у одной дорожки: V1, V2, ... Считать + дорожки нужного вида и прибавлять единицу нельзя — после удаления + средней счёт совпадёт с уже занятым именем, и в окне окажутся две + дорожки Т2. } + function UniqueTrackName(const APrefix: string): string; + { Разводит одинаковые имена дорожек, доставшиеся от старых проектов. + Имя — только подпись, но две дорожки Т2 в окне сбивают с толку. } + procedure EnsureUniqueTrackNames; function AddTrack(const AName: string): TTrack; function AddTrackOfKind(const AName: string; AKind: TTrackKind): TTrack; function FindTrack(const AId: string): TTrack; @@ -717,6 +725,45 @@ begin FModified := True; end; +function TProject.UniqueTrackName(const APrefix: string): string; +var + N, I: Integer; + Taken: Boolean; +begin + N := 1; + repeat + Result := APrefix + IntToStr(N); + Taken := False; + for I := 0 to FTracks.Count - 1 do + if TTrack(FTracks[I]).Name = Result then + begin + Taken := True; + Break; + end; + Inc(N); + until not Taken; +end; + +procedure TProject.EnsureUniqueTrackNames; +var + I, J: Integer; + Prefix: string; +begin + for I := 0 to FTracks.Count - 1 do + for J := 0 to I - 1 do + if TTrack(FTracks[J]).Name = TTrack(FTracks[I]).Name then + begin + case TTrack(FTracks[I]).Kind of + tkAudio: Prefix := 'A'; + tkText: Prefix := 'Т'; + else + Prefix := 'V'; + end; + TTrack(FTracks[I]).Name := UniqueTrackName(Prefix); + Break; + end; +end; + function TProject.AddTrack(const AName: string): TTrack; begin Result := AddTrackOfKind(AName, tkVideo); @@ -1083,13 +1130,10 @@ begin if Target = nil then case Need of - tkAudio: Target := AddTrackOfKind('A' + - IntToStr(TrackCountOf(tkAudio) + 1), tkAudio); - tkText: Target := AddTrackOfKind('Т' + - IntToStr(TrackCountOf(tkText) + 1), tkText); + tkAudio: Target := AddTrackOfKind(UniqueTrackName('A'), tkAudio); + tkText: Target := AddTrackOfKind(UniqueTrackName('Т'), tkText); else - Target := AddTrackOfKind('V' + - IntToStr(TrackCountOf(tkVideo) + 1), tkVideo); + Target := AddTrackOfKind(UniqueTrackName('V'), tkVideo); end; Track.Extract(Clip); @@ -1737,6 +1781,7 @@ begin MigrateAudio; FixClipKinds; + EnsureUniqueTrackNames; Settings := Obj.Find('settings', jtObject) as TJSONObject; if Settings <> nil then diff --git a/src/uTimelineCtrl.pas b/src/uTimelineCtrl.pas index 425f48a..87cc45d 100644 --- a/src/uTimelineCtrl.pas +++ b/src/uTimelineCtrl.pas @@ -2084,13 +2084,10 @@ begin (not FTrackAddedDuringDrag) then begin case TProject.TrackKindFor(FDragClip.Kind) of - tkAudio: FProject.AddTrackOfKind('A' + - IntToStr(FProject.TrackCountOf(tkAudio) + 1), tkAudio); - tkText: FProject.AddTrackOfKind('Т' + - IntToStr(FProject.TrackCountOf(tkText) + 1), tkText); + tkAudio: FProject.AddTrackOfKind(FProject.UniqueTrackName('A'), tkAudio); + tkText: FProject.AddTrackOfKind(FProject.UniqueTrackName('Т'), tkText); else - FProject.AddTrack('V' + - IntToStr(FProject.TrackCountOf(tkVideo) + 1)); + FProject.AddTrack(FProject.UniqueTrackName('V')); end; FTrackAddedDuringDrag := True; Idx := FProject.TrackCount - 1; diff --git a/tests/nlvetool.lpr b/tests/nlvetool.lpr index b155346..c66acae 100644 --- a/tests/nlvetool.lpr +++ b/tests/nlvetool.lpr @@ -854,6 +854,65 @@ begin end; WriteLn; + { --- 11. Оверлей под сплошным клипом в окне не показывается --- } + WriteLn(' 11. Оверлей закрыт сверху сплошным клипом'); + Proj := MakeProject(3, Media); + try + Proj.Master := Media.Info; + { Снизу вверх: V3 — съёмка, V2 — оверлей с ключом на весь ролик, + V1 — фотография поверх куска. Так и было в живом проекте. } + Proj.AddClip(Proj.Tracks[2], Media, 0, 20, 0); + Clip := Proj.AddClip(Proj.Tracks[1], Media, 0, 20, 0); + Key := DefaultColorKey; + Key.Enabled := True; + Clip.ColorKey := Key; + Clip := Proj.AddClip(Proj.Tracks[0], Media, 0, 5, 5); + Clip.Kind := ckImage; + + Check(FlattenPreview(Proj, Segs, AudioSegs, Overlays, Missing), + 'уплощение для предпросмотра прошло'); + { Фотография лежит с 5 по 10 секунду и закрывает оверлей: показывать + его там нельзя — в ролике его там не будет. Значит, от оверлея + остаются два окна: до фотографии и после. } + Check(Length(Overlays) = 2, 'оверлей показывается двумя окнами: ' + + IntToStr(Length(Overlays))); + if Length(Overlays) = 2 then + begin + Check(Abs(Overlays[0].TlStart) < 0.001, 'первое окно с начала'); + Check(Abs(Overlays[0].TlEnd - 5) < 0.001, 'и до фотографии'); + Check(Abs(Overlays[1].TlStart - 10) < 0.001, 'второе после неё'); + Check(Abs(Overlays[1].TlEnd - 20) < 0.001, 'и до конца'); + Check(Abs(Overlays[1].SrcStart - 10) < 0.001, + 'и берётся с того же места исходника'); + end; + finally + Proj.Free; + end; + WriteLn; + + { --- 12. Имена дорожек не повторяются --- } + WriteLn(' 12. Имена дорожек'); + Proj := MakeProject(3, Media); + try + Check(Proj.UniqueTrackName('V') = 'V4', 'следующее имя V4: ' + + Proj.UniqueTrackName('V')); + { Убираем среднюю: счёт дорожек снова три, но имя V3 уже занято, и + считать их для имени нельзя. } + Proj.RemoveTrack(1); + Check(Proj.UniqueTrackName('V') = 'V2', 'после удаления средней — V2: ' + + Proj.UniqueTrackName('V')); + Proj.AddTrack(Proj.UniqueTrackName('V')); + Check(Proj.Tracks[2].Name = 'V2', 'новая дорожка названа V2'); + + { Одинаковые имена, доставшиеся от старого проекта, разводятся. } + Proj.Tracks[2].Name := 'V1'; + Proj.EnsureUniqueTrackNames; + Check(Proj.Tracks[2].Name <> 'V1', 'повтор разведён: ' + Proj.Tracks[2].Name); + finally + Proj.Free; + end; + WriteLn; + WriteLn; if FailCount = 0 then begin