Продолжение прерванной сборки потоком — с недописанного файла

Потоком кэша кусков нет, и продолжать было не с чего. Но недописанный
файл сам по себе — уже собранная часть фильма: в нём видно, сколько
целых кадров успело записаться. Программа отступает до границы
последнего целого куска, переносит эту часть и досылает остаток.

Дописать в конец существующего MKV нельзя, поэтому продолжение пишется
в новый файл. MP4 продолжать нельзя вовсе: оглавление в конце, и
оборванный файл не открывается — об этом говорится прямо.

Готовый файл теперь цел до последнего: сборка идёт в файл рядом и
занимает место готового, только когда дописана. Сорвавшаяся попытка не
отбирает уже собранное — из двух недописанных остаётся тот, где кадров
больше, и считаются они даже после запроса остановки.

Отдельно пришлось снять сдвиг, который приёмник накладывает на весь
ролик: перенесённая часть несла его в себе, и куски за ней задваивали
кадр. Замерено: обрыв в четырёх разных местах, продолженная сборка
каждый раз совпала с собранной за один раз попиксельно.

В nlvetool у render появились --cancel-after=<сек> и сочетание слов:
cache resume проверяет, что старый путь через кэш не сломался.
This commit is contained in:
4aiman 2026-09-30 16:37:28 +03:00
commit 4522f0000a
4 changed files with 346 additions and 35 deletions

View file

@ -582,15 +582,34 @@ Linux потом искала по нему `.dll`.
## Прерванная сборка
Каждый доведённый до конца кусок помечается в списке рядом с самими
кусками (`готово.txt` в кэше). Если сборка не дошла до конца — упала,
была отменена или программу закрыли, — при следующей сборке в тот же файл
программа предложит продолжить с готовых кусков.
Если сборка не дошла до конца — упала, была отменена или программу
закрыли, — при следующей сборке в тот же файл программа предложит
продолжить.
Проверяется не только наличие файла: сборку могли прервать посреди
записи, и файл оказался бы обрезанным. Поэтому куском считается только
тот, который успел попасть в список.
**При сборке потоком** продолжают с самого недописанного файла: он и есть
уже собранная часть фильма, и в нём видно, сколько целых кадров успело
записаться. Программа отступает назад до границы последнего целого куска,
переносит эту часть в новый файл и досылает остаток. Собранное таким
образом совпадает с собранным за один раз попиксельно.
Дописать в конец существующего MKV нельзя — Matroska этого не даёт, —
поэтому продолжение всегда пишется в новый файл, и на диске в этот момент
лежат оба. Места нужно вдвое больше готового ролика.
**MP4 продолжать нельзя вовсе:** оглавление пишется в конец, и оборванный
файл не открывается — `moov atom not found`. Программа говорит об этом
прямо и собирает заново. Если сборка длинная, лучше собрать в MKV, а потом
переупаковать в MP4 окном ремукса: это копирование потоков, без
перекодирования.
Готовый файл до последнего цел: сборка идёт в файл рядом и занимает место
готового, только когда дописана. Сорвавшаяся попытка не отбирает то, что
уже было собрано, — из двух недописанных остаётся тот, где кадров больше.
**При сборке через кэш кусков** продолжают иначе: каждый доведённый до
конца кусок помечается в списке рядом с самими кусками (`готово.txt`).
Проверяется не только наличие файла — сборку могли прервать посреди
записи; куском считается только тот, который успел попасть в список.
Первая строка списка — отпечаток плана: пути, места и длины всех кусков.
Не совпал — значит монтаж с тех пор менялся, и готовые куски не подойдут;
программа скажет об этом и соберёт заново.
@ -815,6 +834,15 @@ Wine с новой библиотекой не грузилось ничего.
`TThread`, `Name` и `Caption` — у формы. Компилятор ловит это как
«Duplicate identifier», но причину не объясняет.
**Приёмник двигает весь ролик.** `-avoid_negative_ts make_zero` у
приёмника сдвигает поток так, чтобы самая ранняя метка декодирования
стала нулём, — а у первого куска она уходит в минус на задержку
кодировщика, и весь ролик встаёт на 21 мс позже. Само по себе это
безобидно, но продолжение прерванной сборки несёт этот сдвиг в
перенесённой части, и куски за ней, размещённые по счёту кадров от нуля,
налезают на неё на кадр. Поэтому при переносе сдвиг снимается
отрицательным `-output_ts_offset`, а приёмник накладывает его заново.
**Сдвиг к нулю отменяет сдвиг по времени.** При сборке потоком место
куску задаёт `-output_ts_offset`, но `-avoid_negative_ts make_zero`
считается уже после него и двигает кусок обратно в ноль — ключ просто

View file

@ -157,6 +157,19 @@ type
куска: считать кадрами, а не складывая секунды, — единственный
способ не накопить ошибку округления на тысяче кусков. }
FSentFrames: Int64;
{ Продолжение прерванной сборки: недописанный файл прошлого раза,
сколько кадров в нём читается и отправлена ли уже его целая часть. }
FResumeSource: string;
FResumeGood: Int64;
FPrefixSent: Boolean;
{ На сколько прошлая сборка сдвинула весь ролик вперёд. Приёмник
двигает поток так, чтобы самая ранняя метка декодирования стала
нулём, а у первого куска она уходит в минус на задержку кодировщика.
При переносе этот сдвиг снимается, иначе продолжение встало бы
на него внахлёст. }
FResumeShift: Double;
{ Приёмник метки первого кадра при замере сдвига. }
FFirstVideoPts: Double;
function DistinctSourceCount: Integer;
procedure ChooseIntermediateFormat;
@ -187,15 +200,32 @@ type
procedure PacketCountLine(const ALine: string);
function RunFfmpeg(const AArgs: array of string; const AWhat: string): Boolean;
{ Один кусок: в файл кэша или в трубу — смотря как собираем. }
function RunPart(const AArgs: array of string; const AWhat: string): Boolean;
function CountPackets(const APath: string): Int64;
{ Один кусок: в файл кэша или в трубу — смотря как собираем.
AFrames нужен продолжению: по нему видно, лежит ли этот кусок
целиком в том, что успело записаться прошлый раз. }
function RunPart(const AArgs: array of string; const AWhat: string;
AFrames: Integer): Boolean;
{ Сколько пакетов картинки в файле. AEvenIfCancelled — считать даже
после того, как работу попросили прекратить: по этому числу
решается, какой из недописанных файлов оставить, и остановка не
повод терять уже собранное. }
function CountPackets(const APath: string;
AEvenIfCancelled: Boolean = False): Int64;
function ExtractSegments: Boolean;
function WriteConcatList(out AListPath: string): Boolean;
function ConcatAll(const AListPath: string): Boolean;
{ Сборка потоком: приёмник запускается один раз, куски идут в него
по очереди, на диск ложится только готовый файл. }
function StreamAll: Boolean;
{ Готовит продолжение: смотрит недописанный файл прошлого раза. }
procedure PrepareResume;
{ Самая ранняя метка картинки в файле. Меньше нуля — не удалось. }
function FirstVideoTime(const APath: string): Double;
procedure FirstVideoLine(const ALine: string);
{ Отправляет приёмнику целую часть недописанного файла. }
function SendPrefix: Boolean;
{ Решает судьбу двух файлов: собранного сейчас и лежавшего раньше. }
procedure FinishResume(ASucceeded: Boolean; const AWork: string);
procedure Verify;
{ Сборка звука: куски сводятся в сырой поток и сжимаются одним
проходом. Сырой поток нужен для точности — стык получается с
@ -592,13 +622,33 @@ end;
журнал с ходом работы приходит одним потоком. Всё остальное — те же
проверки и тот же учёт работы. }
function TExportThread.RunPart(const AArgs: array of string;
const AWhat: string): Boolean;
const AWhat: string; AFrames: Integer): Boolean;
var
Res: TProcResult;
begin
if not FStreaming then
Exit(RunFfmpeg(AArgs, AWhat));
{ Продолжение прерванной сборки.
Пока очередной кусок целиком помещается в то, что успело записаться
прошлый раз, делать его заново незачем: он придёт вместе с переносом
собранного. Первый же кусок, который не помещается, и есть граница —
перед ним переносим целую часть старого файла и дальше работаем как
обычно. }
if (FResumeGood > 0) and (not FPrefixSent) then
begin
if FSentFrames + AFrames <= FResumeGood then
begin
Inc(FReusedParts);
FDoneWork := FDoneWork + FStepWork;
PublishProgress;
Exit(True);
end;
if not SendPrefix then
Exit(False);
end;
Log('> ' + CommandToString(FfmpegExe, AArgs));
FStepPosition := 0;
@ -628,13 +678,18 @@ begin
Result := True;
end;
function TExportThread.CountPackets(const APath: string): Int64;
function TExportThread.CountPackets(const APath: string;
AEvenIfCancelled: Boolean): Int64;
var
Res: TProcResult;
Cancel: TProcCancelQuery;
begin
FPacketCount := -1;
Cancel := @CancelQuery;
if AEvenIfCancelled then
Cancel := nil;
Res := RunProcess(FfprobeExe, ArgsCountVideoPackets(APath),
@PacketCountLine, nil, @CancelQuery);
@PacketCountLine, nil, Cancel);
if (not Res.Started) or (Res.ExitCode <> 0) then
Exit(-1);
Result := FPacketCount;
@ -823,14 +878,14 @@ var
Format('part_%.3d_%s%s', [AIndex, ASuffix, FIntermediateExt]);
end;
{ Время, на которое встанет эта часть в общем потоке. Отрицательное —
{ Время, на которое встанет эта часть в общем потоке. NotPiped —
значит пишем в файл, и время куску не нужно: он начинается с нуля. }
function PartAt: Double;
begin
if FStreaming then
Result := SentTime
else
Result := -1;
Result := NotPiped;
end;
{ Часть ушла — двигаем счётчик отправленных кадров. }
@ -895,7 +950,8 @@ begin
if not RunPart(ArgsExtract(FSegments[I].FilePath, Dst,
0, FSegments[I].Duration, FIntermediateFormat,
FSegments[I].Volume, FMaster.AudioCodec, Rep.WantFrames, WithAudio,
PartAt), 'Подготовка вставки ' + IntToStr(I + 1)) then
PartAt), 'Подготовка вставки ' + IntToStr(I + 1),
Rep.WantFrames) then
Exit;
Rep.TempPath := Dst;
@ -930,7 +986,7 @@ begin
PreferredEncoder(FMaster.VideoCodec),
FIntermediateFormat = 'mpegts', FMaster.VideoBitrate,
FSegments[I].SubFile, PartAt),
'Наложение в куске ' + IntToStr(I + 1)) then
'Наложение в куске ' + IntToStr(I + 1), Rep.WantFrames) then
Exit;
Rep.TempPath := Dst;
@ -992,7 +1048,7 @@ begin
FMaster.SampleRate, FMaster.Channels, FSegments[I].Volume,
FIntermediateFormat, FSegments[I].Filter, FSegments[I].SubFile,
FMaster.VideoBitrate, PartAt),
'Пересборка куска ' + IntToStr(I + 1)) then
'Пересборка куска ' + IntToStr(I + 1), Rep.WantFrames) then
Exit;
Rep.TempPath := Dst;
@ -1027,7 +1083,7 @@ begin
if not RunPart(ArgsExtract(FSegments[I].FilePath, Dst,
FSegments[I].SrcStart, SegEnd, FIntermediateFormat,
FSegments[I].Volume, FMaster.AudioCodec, WantFrames, WithAudio,
PartAt), 'Вырезание куска ' + IntToStr(I + 1)) then
PartAt), 'Вырезание куска ' + IntToStr(I + 1), WantFrames) then
Exit;
Rep.ActualStart := FSegments[I].SrcStart;
@ -1076,7 +1132,7 @@ begin
FMaster.VideoCodec, FMaster.PixFmt, FMaster.AudioCodec,
FMaster.SampleRate, FMaster.Channels, FIntermediateFormat,
HeadFrames, WithAudio, FMaster.VideoBitrate, PartAt),
'Перекодирование стыка куска ' + IntToStr(I + 1)) then
'Перекодирование стыка куска ' + IntToStr(I + 1), HeadFrames) then
Exit;
Rep.ActualStart := FSegments[I].SrcStart;
@ -1099,7 +1155,7 @@ begin
if not RunPart(ArgsExtract(FSegments[I].FilePath, Dst,
EncodeUntil, SegEnd, FIntermediateFormat,
FSegments[I].Volume, FMaster.AudioCodec, TailFrames, WithAudio,
PartAt), 'Вырезание куска ' + IntToStr(I + 1)) then
PartAt), 'Вырезание куска ' + IntToStr(I + 1), TailFrames) then
Exit;
Rep.ActualStart := EncodeUntil;
@ -1215,13 +1271,176 @@ end;
этого он дописывает оглавление и завершается.
Звук должен быть готов заранее: приёмник берёт его вторым входом. }
{ Продолжение прерванной сборки при работе потоком.
Кэша кусков тут нет, и продолжать не с чего — кроме самого
недописанного файла. Он и есть уже собранная часть фильма: в нём видно,
сколько целых кадров успело записаться.
Дописать в него нельзя: Matroska не даёт приписывать в конец. Поэтому
он отодвигается в сторону, продолжение собирается в новый файл, и целая
часть старого переливается туда первой — ровно так же, как обычный
кусок.
MP4 сюда не попадает: его оглавление пишется в конец, и оборванный файл
не открывается вовсе. }
procedure TExportThread.FirstVideoLine(const ALine: string);
var
Value: Double;
begin
if not TryStrToFloat(Trim(ALine), Value, InvariantFormat) then
Exit;
if (FFirstVideoPts < 0) or (Value < FFirstVideoPts) then
FFirstVideoPts := Value;
end;
function TExportThread.FirstVideoTime(const APath: string): Double;
var
Res: TProcResult;
begin
FFirstVideoPts := -1;
Res := RunProcess(FfprobeExe, ArgsFirstVideoTimes(APath),
@FirstVideoLine, nil, @CancelQuery);
if (not Res.Started) or (Res.ExitCode <> 0) then
Exit(-1);
Result := FFirstVideoPts;
end;
procedure TExportThread.PrepareResume;
var
Good: Int64;
begin
FResumeSource := '';
FResumeGood := 0;
FResumeShift := 0;
FPrefixSent := False;
if CancelQuery or not (FResume and FileExists(FOutputPath)) then
Exit;
if LowerCase(ExtractFileExt(FOutputPath)) <> '.mkv' then
begin
Log('Продолжить нельзя: оборванный ' +
Copy(ExtractFileExt(FOutputPath), 2, 10) + ' не читается. Собираю заново.');
Exit;
end;
Good := CountPackets(FOutputPath);
if Good <= 0 then
begin
Log('В недописанном файле читаемых кадров нет, собираю заново.');
Exit;
end;
FResumeSource := FOutputPath;
FResumeGood := Good;
FResumeShift := FirstVideoTime(FOutputPath);
if FResumeShift < 0 then
FResumeShift := 0;
Log(Format('В недописанном файле читается %d кадров — продолжаю с ' +
'ближайшей границы куска. Ролик в нём начинается с %s.',
[Good, SecondsToStr(FResumeShift)]));
end;
function TExportThread.SendPrefix: Boolean;
var
Upto: Double;
begin
FPrefixSent := True;
if FSentFrames <= 0 then
begin
{ Первый же кусок не влез целиком — брать из старого файла нечего. }
Log('Целых кусков в недописанном файле не нашлось, собираю заново.');
Exit(True);
end;
SetStage('Переношу собранное прошлый раз');
{ Времени с запасом: длину задаёт число кадров, а -to здесь только
ограничитель на случай, если кадров в файле окажется больше. Отсчёт
от сдвига: в старом файле ролик начинается не с нуля. }
Upto := FResumeShift + (FSentFrames + 2) * FMaster.FpsDen / FMaster.FpsNum;
FStepWork := SentTime;
{ Сдвиг снимается: куски, которые пойдут следом, встают на свои места
по счёту кадров от нуля, и перенесённое должно лечь на ту же сетку.
Приёмник потом сдвинет всё обратно — ровно так же, как при сборке с
начала. }
Result := RunPart(ArgsExtract(FResumeSource, 'pipe:1', 0, Upto,
'mpegts', 1, '', FSentFrames, False, -FResumeShift),
'Перенос собранного прошлый раз', 0);
if Result then
Log(Format('Перенесено %d кадров, продолжаю с %s.',
[FSentFrames, SecondsToStr(SentTime)]));
end;
{ Что делать с двумя файлами по итогам сборки: тем, что писали сейчас, и
тем, что лежал раньше.
Удача — новый занимает место готового. Неудача — остаётся тот из двух,
в котором кадров больше: с него и продолжат в следующий раз. Прошлую
работу нельзя терять из-за того, что новая попытка сорвалась раньше. }
procedure TExportThread.FinishResume(ASucceeded: Boolean; const AWork: string);
var
New_, Old: Int64;
begin
if ASucceeded then
begin
DeleteFile(FOutputPath);
if not RenameFile(AWork, FOutputPath) then
begin
FReport.Message := 'Ролик собран, но переименовать его не удалось: ' +
AWork;
Log(FReport.Message);
end;
Exit;
end;
if not FileExists(FOutputPath) then
begin
{ Прошлого файла не было — недописанный занимает его место, с него и
продолжат. }
RenameFile(AWork, FOutputPath);
Exit;
end;
New_ := CountPackets(AWork, True);
Old := CountPackets(FOutputPath, True);
if New_ > Old then
begin
DeleteFile(FOutputPath);
RenameFile(AWork, FOutputPath);
Log(Format('Недописанного стало больше: %d кадров против %d.',
[New_, Old]));
end
else
begin
DeleteFile(AWork);
if Old > 0 then
Log(Format('Прошлый недописанный файл полнее (%d кадров против %d), ' +
'он и оставлен.', [Old, New_]));
end;
end;
function TExportThread.StreamAll: Boolean;
var
Args: TArgs;
Work: string;
Code: Integer;
begin
Result := False;
Args := ArgsStreamMux(FAudioPath, FSubtitlePath, FOutputPath);
PrepareResume;
{ Пишем не сразу в готовый файл, а рядом.
Так прошлая работа цела до последнего: недописанный файл прошлого
раза — это уже собранная часть фильма, и стереть её новой попыткой,
которая сорвётся раньше, значило бы отобрать у человека то, что уже
посчитано. Место готового новый файл займёт, только когда будет
дописан. }
{ Расширение то же, что у готового: по нему ffmpeg выбирает контейнер. }
Work := ChangeFileExt(FOutputPath, '.сборка' + ExtractFileExt(FOutputPath));
DeleteFile(Work);
Args := ArgsStreamMux(FAudioPath, FSubtitlePath, Work);
Log('> ' + CommandToString(FfmpegExe, Args) + ' (приёмник)');
FSink := TProcSink.Create(FfmpegExe, Args, nil, @ErrorLine);
@ -1251,6 +1470,7 @@ begin
Result := True;
finally
FreeAndNil(FSink);
FinishResume(Result, Work);
end;
end;

View file

@ -35,6 +35,13 @@ uses
type
TArgs = array of string;
const
{ Значение AAt у сборщиков аргументов, означающее «пишем в файл, а не в
трубу». Отдельная величина, а не просто «меньше нуля»: время куска в
потоке бывает и отрицательным — так с перенесённого куска снимается
сдвиг, который прошлая сборка наложила на весь ролик. }
NotPiped = -1000000.0;
{ Пути к инструментам. Ищутся один раз и запоминаются. }
function FfmpegExe: string;
function FfprobeExe: string;
@ -71,7 +78,7 @@ function ToolsAvailable(out AError: string): Boolean;
function ArgsExtract(const ASrc, ADst: string; AStart, AEnd: Double;
const AFormat: string; AVolume: Double = 1;
const AAudioCodec: string = ''; AFrames: Integer = 0;
AWithAudio: Boolean = True; AAt: Double = -1): TArgs;
AWithAudio: Boolean = True; AAt: Double = NotPiped): TArgs;
{ Перепаковать файл целиком в заданный формат, копируя потоки. }
function ArgsCopyTo(const ASrc, ADst, AFormat: string): TArgs;
@ -88,7 +95,7 @@ function ArgsEncodeFragment(const ASrc, ADst: string; AStart, AEnd: Double;
const AVideoCodec, APixFmt, AAudioCodec: string;
ASampleRate, AChannels: Integer; const AFormat: string;
AFrames: Integer = 0; AWithAudio: Boolean = True;
ABitrate: Int64 = 0; AAt: Double = -1): TArgs;
ABitrate: Int64 = 0; AAt: Double = NotPiped): TArgs;
{ Пересобрать кусок в параметры проекта.
@ -101,7 +108,7 @@ function ArgsTranscodeSegment(const ASrc, ADst: string; AStart, AEnd: Double;
ASampleRate, AChannels: Integer; AVolume: Double;
const AFormat: string; const AFilter: string = '';
const ASubFile: string = ''; ABitrate: Int64 = 0;
AAt: Double = -1): TArgs;
AAt: Double = NotPiped): TArgs;
{ Путь внутри строки фильтра. Двоеточие там разделяет настройки, обратная
косая и кавычка имеют своё значение — всё это надо обезвредить, иначе
@ -197,7 +204,7 @@ function ArgsRenderLayers(const ALayers: array of TSegmentLayer;
AWidth, AHeight, AFpsNum, AFpsDen: Integer;
const APixFmt, ACodec: string; AIsTs: Boolean;
ABitrate: Int64 = 0; const ASubFile: string = '';
AAt: Double = -1): TArgs;
AAt: Double = NotPiped): TArgs;
{ Сшивание готовой картинки с готовым звуком. Оба потока копируются.
@ -227,6 +234,10 @@ function ArgsFirstPacketTime(const ASrc: string): TArgs;
индекса опорных кадров для контейнеров, кроме MP4. }
function ArgsPacketFlags(const ASrc: string): TArgs;
{ Метки первых кадров картинки: по ним видно, с какого времени начинается
файл. Нужно продолжению прерванной сборки. }
function ArgsFirstVideoTimes(const ASrc: string): TArgs;
{ Название кодировщика для кодека.
Предпочитается аппаратный NVENC: на этой машине он кодирует HEVC в
@ -608,7 +619,7 @@ end;
24.0. }
function OutputTail(const AFormat, ADst: string; AAt: Double): TArgs;
begin
if AAt < 0 then
if AAt <= NotPiped then
begin
{ Метки времени вырезанного куска должны начинаться с нуля, иначе при
склейке куски разъедутся по времени. }
@ -653,7 +664,7 @@ end;
{ Ход работы и хвост вывода разом: они всегда меняются вместе. }
function TailArgs(const AFormat, ADst: string; AAt: Double): TArgs;
begin
if AAt < 0 then
if AAt <= NotPiped then
Result := ProgressArgs
else
Result := ProgressArgsToStderr;
@ -1064,6 +1075,17 @@ begin
'-read_intervals', '%+0.2', ASrc];
end;
function ArgsFirstVideoTimes(const ASrc: string): TArgs;
begin
{ Первые полсекунды картинки: по ним видно, с какой метки файл
начинается. Не один пакет, а несколько — у записи с B-кадрами первый
по порядку хранения кадр показывается не первым, и самая ранняя метка
может оказаться у второго или третьего. }
Result := ['-v', 'error', '-select_streams', 'v:0',
'-show_entries', 'packet=pts_time', '-of', 'csv=p=0',
'-read_intervals', '%+0.5', ASrc];
end;
function ArgsPacketFlags(const ASrc: string): TArgs;
begin
Result := ['-v', 'error', '-select_streams', 'v:0',

View file

@ -21,9 +21,12 @@
собрать проект-образец: видео внизу, картинка
сверху посередине, дыра в начале
render <проект.nlve> <выход> [burn|keep|resume|cache]
[--cancel-after=<сек>]
уплотить проект, создать вставки, собрать ролик
и сверить число кадров; cache — собрать
по-старому, через кэш кусков
по-старому, через кэш кусков; --cancel-after
оборвёт сборку на полпути, а resume продолжит
её с недописанного файла
subs <проект.nlve>
собрать разметку субтитров из текстовых клипов
и напечатать её
@ -1525,8 +1528,34 @@ var
Rep: TExportReport;
I: Integer;
WantSum: Int64;
Ok: Boolean;
Ok, Stopped: Boolean;
CancelAfter, Waited: Double;
{ Слова после выхода идут в любом порядке и сочетаются: cache resume —
это «по-старому и продолжая». }
function HasWord(const AWord: string): Boolean;
var
J: Integer;
begin
Result := False;
for J := 4 to ParamCount do
if LowerCase(ParamStr(J)) = AWord then
Exit(True);
end;
begin
{ Отдельно от слов может стоять --cancel-after=<сек>: оборвать сборку
на полпути, чтобы было что продолжать. }
CancelAfter := 0;
for I := 4 to ParamCount do
if Copy(ParamStr(I), 1, 15) = '--cancel-after=' then
if not TryStrToFloat(Copy(ParamStr(I), 16, MaxInt), CancelAfter,
InvariantFormat) then
begin
WriteLn('Не удалось разобрать значение --cancel-after');
Halt(2);
end;
Proj := TProject.Create;
try
if not Proj.LoadFromFile(ExpandFileName(ParamStr(2)), Err) then
@ -1588,7 +1617,7 @@ begin
{ Вжигание текста — последним, уже по окончательному списку кусков:
повторное уплощение стёрло бы отметки о разметке. Так же устроено и
окно сборки. }
if (ParamCount >= 4) and (LowerCase(ParamStr(4)) = 'burn') then
if HasWord('burn') then
WriteLn('вжигаю текст, пересобрать придётся ',
SecondsToStr(BurnTextIntoSegments(Proj, Segs, Proj.CacheDir)), ' с');
@ -1597,22 +1626,34 @@ begin
чего собрался ролик. }
Thread := TExportThread.Create(Segs, AudioSegs,
ExpandFileName(ParamStr(3)),
(ParamCount >= 4) and (LowerCase(ParamStr(4)) = 'keep'),
HasWord('keep'),
Proj.Master, Default(TEncodeProfile));
{ Словом resume просят продолжить прерванную сборку: готовые куски
берутся с диска, а не считаются заново. }
Thread.Resume := (ParamCount >= 4) and (LowerCase(ParamStr(4)) = 'resume');
Thread.Resume := HasWord('resume');
{ Словом cache просят собрать по-старому, через кэш кусков: так два
способа сверяются между собой на одном и том же монтаже. }
Thread.AllowStream := not ((ParamCount >= 4) and
(LowerCase(ParamStr(4)) = 'cache'));
Thread.AllowStream := not HasWord('cache');
Watcher := TExportWatcher.Create(Thread);
try
Thread.OnProgress := @Watcher.Progress;
Thread.OnLog := @Watcher.LogLine;
Thread.Start;
{ Остановка на полпути: так получается недописанный файл, с
которого потом просят продолжить. }
Waited := 0;
Stopped := False;
while not Thread.Finished do
begin
CheckSynchronize(100);
Waited := Waited + 0.1;
if (CancelAfter > 0) and (not Stopped) and (Waited >= CancelAfter) then
begin
Stopped := True;
WriteLn('[тест] запрашиваю остановку');
Thread.RequestCancel;
end;
end;
CheckSynchronize(0);
Rep := Thread.Report;
finally