Прозрачность и переходы видны в предпросмотре
Плеер играет список кусков, а список накладывать не умеет: там побеждает верхний клип целиком. Поэтому участки с прозрачностью и переходами заранее сводятся в отдельные файлы кэша, и предпросмотр играет уже их. Файл считается один раз, имя выводится из описания слоёв — при следующем открытии ждать нечего. Считает их тот же код, что и сборка ролика. Разными путями считать одно и то же нельзя: предпросмотр разошёлся бы с результатом. Для сборки подстановка не применяется: там наложение считается заново с параметрами вывода, и промежуточный файл был бы лишней потерей качества. Проверено на монтаже с клипом в 50% прозрачности, где оба слоя из одного файла: кадр наложения ближе к полусумме двух слоёв (отклонение 13.8), чем к любому из них по отдельности (26.0 и 18.4). Попутно найдено и исправлено: наложения кодировались процессорным x265, потому что ffmpeg выбирает кодировщик по имени кодека, а мы передавали «hevc». Замерено: 9.6 с материала за 11.2 с работы. Теперь и здесь берётся предпочтительный кодировщик, аппаратный. В служебную программу добавлена команда preview: список кусков ровно такой, каким его видит предпросмотр, вместе с готовыми наложениями.
This commit is contained in:
parent
80e6585134
commit
1e00cd9f44
5 changed files with 197 additions and 2 deletions
|
|
@ -318,10 +318,14 @@ begin
|
|||
[SecondsToStr(ALayers[High(ALayers)].Opacity)]);
|
||||
end;
|
||||
|
||||
{ Кодировщик выбирается предпочтительный, а не тот, что ffmpeg возьмёт
|
||||
по имени кодека. Имя «hevc» уводит его на процессорный x265, и участок
|
||||
наложения считался медленнее реального времени: замерено 9.6 с
|
||||
материала за 11.2 с работы. }
|
||||
Result := Concat(Result,
|
||||
['-filter_complex', Graph, '-map', '[out]',
|
||||
'-t', SecondsToStr(ADuration),
|
||||
'-c:v', ACodec, '-r', Format('%d/%d', [AFpsNum, AFpsDen]),
|
||||
'-c:v', PreferredEncoder(ACodec), '-r', Format('%d/%d', [AFpsNum, AFpsDen]),
|
||||
'-pix_fmt', APixFmt]);
|
||||
Result := Concat(Result,
|
||||
['-bf', '0', '-an', '-avoid_negative_ts', 'make_zero',
|
||||
|
|
|
|||
|
|
@ -51,13 +51,15 @@ type
|
|||
Со звуковой вставкой в той же точке -11.7 dB.
|
||||
|
||||
В звуковом файле опорным является каждый кадр, и такой беды нет. }
|
||||
TGenKind = (gkNone, gkBlack, gkImage, gkSilence);
|
||||
TGenKind = (gkNone, gkBlack, gkImage, gkSilence, gkLayers);
|
||||
|
||||
TGenRequest = record
|
||||
Kind: TGenKind;
|
||||
SourcePath: string;
|
||||
Duration: Double;
|
||||
TargetPath: string;
|
||||
{ Только для gkLayers: что с чем смешивать. }
|
||||
Layers: TSegmentLayers;
|
||||
end;
|
||||
|
||||
TGenRequests = array of TGenRequest;
|
||||
|
|
@ -106,12 +108,30 @@ function FlattenProject(AProject: TProject;
|
|||
один раз. }
|
||||
function GeneratedPath(const ACacheDir: string; const AReq: TGenRequest): string;
|
||||
|
||||
{ Заменяет куски с наложением готовыми файлами — для предпросмотра.
|
||||
|
||||
Плеер играет список кусков, а список накладывать не умеет: там побеждает
|
||||
верхний клип целиком. Поэтому участки с прозрачностью и переходами
|
||||
заранее собираются в отдельные файлы, и предпросмотр играет уже их. Файл
|
||||
один раз посчитан и лежит в кэше, так что при следующем открытии ждать
|
||||
нечего.
|
||||
|
||||
Чего ещё не готово — уходит в AMissing тем же путём, что чёрные вставки:
|
||||
окно их закажет и повторит сборку списка.
|
||||
|
||||
Для сборки ролика это не применяется: там наложение считается заново с
|
||||
параметрами вывода, и промежуточный файл был бы лишней потерей
|
||||
качества. }
|
||||
procedure ResolveLayers(AProject: TProject; var ASegments: TFlatSegmentArray;
|
||||
var AMissing: TGenRequests);
|
||||
|
||||
{ Заготовка чёрной вставки самой ходовой длины.
|
||||
|
||||
Делается заранее, сразу после того как стали известны параметры проекта.
|
||||
Иначе первая же дыра на монтаже ждала бы генерации, и в это время
|
||||
предпросмотр играл бы прежнюю расстановку — выглядело бы так, будто
|
||||
удаление клипа или выключение дорожки не действуют. }
|
||||
|
||||
function PrewarmRequests(const ACacheDir: string): TGenRequests;
|
||||
|
||||
implementation
|
||||
|
|
@ -164,6 +184,11 @@ begin
|
|||
Name := Format('black_%d.mkv', [Ms]);
|
||||
gkSilence:
|
||||
Name := Format('silence_%d.m4a', [Ms]);
|
||||
gkLayers:
|
||||
{ Имя выводится из описания слоёв: одно и то же наложение даёт один
|
||||
и тот же файл, и пересчитывать его второй раз не приходится. }
|
||||
Name := Format('mix_%s_%d.mkv',
|
||||
[Copy(MD5Print(MD5String(AReq.SourcePath)), 1, 12), Ms]);
|
||||
gkImage:
|
||||
begin
|
||||
Key := AReq.SourcePath + '|' + IntToStr(Ms);
|
||||
|
|
@ -179,6 +204,62 @@ begin
|
|||
Result := IncludeTrailingPathDelimiter(ACacheDir) + 'gen' + PathDelim + Name;
|
||||
end;
|
||||
|
||||
{ Описание слоёв одной строкой. Из неё выводится имя файла, поэтому в неё
|
||||
входит всё, что влияет на картинку: исходник, место в нём, кадрирование
|
||||
и доли прозрачности. }
|
||||
function LayersKey(const ALayers: TSegmentLayers;
|
||||
const ADuration: Double): string;
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
Result := SecondsToStr(ADuration);
|
||||
for I := 0 to High(ALayers) do
|
||||
Result := Result + '|' + ALayers[I].FilePath +
|
||||
'@' + SecondsToStr(ALayers[I].SrcStart) +
|
||||
'#' + ALayers[I].Filter +
|
||||
'*' + SecondsToStr(ALayers[I].Opacity) +
|
||||
'>' + SecondsToStr(ALayers[I].FadeFrom) +
|
||||
'<' + SecondsToStr(ALayers[I].FadeTo);
|
||||
end;
|
||||
|
||||
procedure ResolveLayers(AProject: TProject; var ASegments: TFlatSegmentArray;
|
||||
var AMissing: TGenRequests);
|
||||
var
|
||||
I: Integer;
|
||||
Req: TGenRequest;
|
||||
begin
|
||||
if AProject = nil then
|
||||
Exit;
|
||||
for I := 0 to High(ASegments) do
|
||||
begin
|
||||
if Length(ASegments[I].Layers) = 0 then
|
||||
Continue;
|
||||
|
||||
Req := Default(TGenRequest);
|
||||
Req.Kind := gkLayers;
|
||||
Req.Duration := ASegments[I].Duration;
|
||||
Req.SourcePath := LayersKey(ASegments[I].Layers, ASegments[I].Duration);
|
||||
Req.Layers := ASegments[I].Layers;
|
||||
Req.TargetPath := GeneratedPath(AProject.CacheDir, Req);
|
||||
|
||||
if not FileExists(Req.TargetPath) then
|
||||
begin
|
||||
SetLength(AMissing, Length(AMissing) + 1);
|
||||
AMissing[High(AMissing)] := Req;
|
||||
Continue;
|
||||
end;
|
||||
|
||||
{ Готовый файл встаёт на место куска целиком: слои в нём уже сведены,
|
||||
кадрировать и пересобирать больше нечего. }
|
||||
ASegments[I].FilePath := Req.TargetPath;
|
||||
ASegments[I].SrcStart := 0;
|
||||
ASegments[I].Filter := '';
|
||||
ASegments[I].Transcode := False;
|
||||
ASegments[I].Layers := nil;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
function PrewarmRequests(const ACacheDir: string): TGenRequests;
|
||||
var
|
||||
Req: TGenRequest;
|
||||
|
|
|
|||
|
|
@ -178,6 +178,9 @@ begin
|
|||
gkSilence:
|
||||
FStageText := Format('Делаю вставку тишины %d из %d',
|
||||
[I + 1, Length(FRequests)]);
|
||||
gkLayers:
|
||||
FStageText := Format('Собираю наложение %d из %d',
|
||||
[I + 1, Length(FRequests)]);
|
||||
end;
|
||||
Publish;
|
||||
|
||||
|
|
@ -203,6 +206,15 @@ begin
|
|||
Args := ArgsSilenceClip(FRequests[I].TargetPath,
|
||||
FRequests[I].Duration, AudioCodec,
|
||||
FMaster.SampleRate, FMaster.Channels);
|
||||
gkLayers:
|
||||
{ Тот же расчёт, что и при сборке ролика: прозрачность —
|
||||
наложением с приглушённым альфа-каналом, переход — фильтром
|
||||
xfade. Разными путями считать одно и то же нельзя, иначе
|
||||
предпросмотр разойдётся с готовым роликом. }
|
||||
Args := ArgsRenderLayers(FRequests[I].Layers,
|
||||
FRequests[I].TargetPath, FRequests[I].Duration,
|
||||
FMaster.Width, FMaster.Height, FMaster.FpsNum, FMaster.FpsDen,
|
||||
FMaster.PixFmt, FMaster.VideoCodec, False);
|
||||
else
|
||||
Continue;
|
||||
end;
|
||||
|
|
|
|||
|
|
@ -1494,6 +1494,11 @@ begin
|
|||
Exit;
|
||||
end;
|
||||
|
||||
{ Участки с прозрачностью и переходами плееру не по зубам: список
|
||||
кусков накладывать не умеет. Подставляем вместо них заранее
|
||||
посчитанные файлы, а чего ещё нет — заказываем. }
|
||||
ResolveLayers(FProject, Segments, Missing);
|
||||
|
||||
{ Каждая звуковая дорожка идёт своим списком кусков: они смешиваются, а
|
||||
не побеждают друг друга. Дыры в каждой дорожке свои, и вставки тишины
|
||||
под них нужны свои — без этого список ссылается на несуществующий
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@
|
|||
render <проект.nlve> <выход>
|
||||
уплотить проект, создать вставки, собрать ролик
|
||||
и сверить число кадров
|
||||
preview <проект.nlve>
|
||||
собрать список кусков так же, как это делает
|
||||
предпросмотр: с готовыми наложениями вместо
|
||||
участков с прозрачностью и переходами
|
||||
audio <проект.nlve>
|
||||
разложить звуковые дорожки проекта на куски и
|
||||
напечатать их вместе со строками edl://
|
||||
|
|
@ -940,6 +944,93 @@ begin
|
|||
end;
|
||||
end;
|
||||
|
||||
|
||||
{ Список кусков предпросмотра.
|
||||
|
||||
Отличается от сборки одним: участки с прозрачностью и переходами плеер
|
||||
показать не может, поэтому они заранее сводятся в отдельные файлы. Здесь
|
||||
видно, какие именно участки и во что превратились. }
|
||||
procedure CmdPreview;
|
||||
var
|
||||
Proj: TProject;
|
||||
Segs, AudioSegs: TFlatSegmentArray;
|
||||
Missing: TGenRequests;
|
||||
Err: string;
|
||||
I: Integer;
|
||||
At: Double;
|
||||
Gen: TGenerateThread;
|
||||
GenWatch: TGenWatcher;
|
||||
Kind: string;
|
||||
begin
|
||||
Proj := TProject.Create;
|
||||
try
|
||||
if not Proj.LoadFromFile(ExpandFileName(ParamStr(2)), Err) then
|
||||
begin
|
||||
WriteLn('Не удалось прочитать проект: ', Err);
|
||||
Halt(1);
|
||||
end;
|
||||
|
||||
if not FlattenProject(Proj, Segs, AudioSegs, Missing) then
|
||||
begin
|
||||
WriteLn('Уплотить нечего.');
|
||||
Halt(1);
|
||||
end;
|
||||
|
||||
ResolveLayers(Proj, Segs, Missing);
|
||||
|
||||
if Length(Missing) > 0 then
|
||||
begin
|
||||
WriteLn('Не хватает вставок: ', Length(Missing), '. Делаю.');
|
||||
Gen := TGenerateThread.Create(Missing, Proj.Master);
|
||||
GenWatch := TGenWatcher.Create(Gen);
|
||||
try
|
||||
Gen.OnProgress := @GenWatch.Progress;
|
||||
Gen.OnLog := @GenWatch.LogLine;
|
||||
Gen.Start;
|
||||
while not Gen.Finished do
|
||||
CheckSynchronize(100);
|
||||
CheckSynchronize(0);
|
||||
if not Gen.Succeeded then
|
||||
begin
|
||||
WriteLn('Не вышло: ', Gen.Message);
|
||||
Halt(1);
|
||||
end;
|
||||
finally
|
||||
GenWatch.Free;
|
||||
Gen.Free;
|
||||
end;
|
||||
|
||||
{ Второй заход: теперь файлы на месте и должны подставиться. }
|
||||
Missing := nil;
|
||||
ResolveLayers(Proj, Segs, Missing);
|
||||
if Length(Missing) > 0 then
|
||||
begin
|
||||
WriteLn('Часть наложений так и не появилась.');
|
||||
Halt(1);
|
||||
end;
|
||||
end;
|
||||
|
||||
At := 0;
|
||||
for I := 0 to High(Segs) do
|
||||
begin
|
||||
if Length(Segs[I].Layers) > 0 then
|
||||
Kind := 'НАЛОЖЕНИЕ (плеер не покажет)'
|
||||
else if Segs[I].Kind = skBlack then
|
||||
Kind := 'чёрное'
|
||||
else
|
||||
Kind := 'кусок ';
|
||||
WriteLn(Format(' [%2d] %s монтаж %8s..%8s из %8s %s',
|
||||
[I, Kind, SecondsToStr(At), SecondsToStr(At + Segs[I].Duration),
|
||||
SecondsToStr(Segs[I].SrcStart), ExtractFileName(Segs[I].FilePath)]));
|
||||
At := At + Segs[I].Duration;
|
||||
end;
|
||||
WriteLn;
|
||||
WriteLn(BuildEdlUri(Segs));
|
||||
finally
|
||||
Proj.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure CmdRender;
|
||||
var
|
||||
Proj: TProject;
|
||||
|
|
@ -1081,6 +1172,8 @@ begin
|
|||
CmdBuild
|
||||
else if (Cmd = 'buildcut') and (ParamCount >= 3) then
|
||||
CmdBuildCut
|
||||
else if (Cmd = 'preview') and (ParamCount >= 2) then
|
||||
CmdPreview
|
||||
else if (Cmd = 'audio') and (ParamCount >= 2) then
|
||||
CmdAudio
|
||||
else if (Cmd = 'render') and (ParamCount >= 3) then
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue