Честное предупреждение о контейнере и отметка недописанной сборки

У MP4 два неудобства, о которых человек узнавал поздно: прерванную
сборку в нём продолжить нельзя вовсе, а перекодированный стык точного
реза воспроизводится правильно не везде. Теперь об обоих говорится до
начала работы, одним окном с кнопками «Собрать в MKV», «Всё равно mp4»,
«Не собирать».

Признак точного реза заодно исправлен: смотрели на отметку edPrecise,
а резы стали точными всегда — предупреждение молчало. Теперь признак —
клип, начинающийся не с начала файла.

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

Ещё: на каждом стыке кусков приёмник писал «Packet corrupt», по паре
строк на кусок. Данные целы, но журнал забивался, и настоящую ошибку в
нём было не найти. Лечится ключом -mpegts_flags +initial_discontinuity
у отправителя: сверено по кадрам и по md5, результат тот же.
This commit is contained in:
4aiman 2026-09-30 16:48:31 +03:00
commit f841ed79ec
4 changed files with 170 additions and 11 deletions

View file

@ -834,6 +834,14 @@ Wine с новой библиотекой не грузилось ничего.
`TThread`, `Name` и `Caption` — у формы. Компилятор ловит это как
«Duplicate identifier», но причину не объясняет.
**Стык кусков в потоке выглядит как порча.** Каждый кусок — это
отдельный поток MPEG-TS со своим счётчиком непрерывности, и на стыке
приёмник пишет «Packet corrupt», по паре строк на кусок. Данные при этом
целы, но журнал сборки забивается жалобами, из-за которых не найти
настоящую ошибку. Лечится ключом `-mpegts_flags +initial_discontinuity`
у отправителя: он помечает начало куска как разрыв, жалобы исчезают, а
результат тот же — сверено по кадрам и по md5.
**Приёмник двигает весь ролик.** `-avoid_negative_ts make_zero` у
приёмника сдвигает поток так, чтобы самая ранняя метка декодирования
стала нулём, — а у первого куска она уходит в минус на задержку

View file

@ -226,6 +226,8 @@ type
function SendPrefix: Boolean;
{ Решает судьбу двух файлов: собранного сейчас и лежавшего раньше. }
procedure FinishResume(ASucceeded: Boolean; const AWork: string);
{ Ставит или снимает отметку «файл недописан». }
procedure MarkUnfinished(AFrames: Int64);
procedure Verify;
{ Сборка звука: куски сводятся в сырой поток и сжимаются одним
проходом. Сырой поток нужен для точности — стык получается с
@ -288,6 +290,15 @@ type
{ Каталог для временных кусков рядом с готовым файлом. }
function CacheDirFor(const AOutputPath: string): string;
{ Путь к отметке «сборка оборвалась, файл недописан». Она лежит рядом с
самим файлом: недописанный MKV открывается и играет, и отличить его от
готового больше нечем. Внутри — число кадров, которые в нём есть. }
function UnfinishedMark(const AOutputPath: string): string;
{ Сколько кадров записано в недописанный файл по его отметке. Ноль —
отметки нет, значит файл готов или его вовсе нет. }
function UnfinishedFrames(const AOutputPath: string): Int64;
implementation
uses
@ -326,6 +337,32 @@ begin
PathDelim;
end;
function UnfinishedMark(const AOutputPath: string): string;
begin
Result := AOutputPath + '.недописано';
end;
function UnfinishedFrames(const AOutputPath: string): Int64;
var
L: TStringList;
begin
Result := 0;
if not FileExists(UnfinishedMark(AOutputPath)) then
Exit;
L := TStringList.Create;
try
try
L.LoadFromFile(UnfinishedMark(AOutputPath));
if L.Count > 0 then
Result := StrToInt64Def(Trim(L[0]), 0);
except
Result := 0;
end;
finally
L.Free;
end;
end;
destructor TExportThread.Destroy;
begin
FDone.Free;
@ -1325,6 +1362,13 @@ begin
Exit;
end;
if UnfinishedFrames(FOutputPath) <= 0 then
begin
Log('Рядом с файлом нет отметки о незаконченной сборке — считаю его ' +
'готовым и собираю заново.');
Exit;
end;
Good := CountPackets(FOutputPath);
if Good <= 0 then
begin
@ -1391,6 +1435,7 @@ begin
AWork;
Log(FReport.Message);
end;
DeleteFile(UnfinishedMark(FOutputPath));
Exit;
end;
@ -1399,6 +1444,7 @@ begin
{ Прошлого файла не было — недописанный занимает его место, с него и
продолжат. }
RenameFile(AWork, FOutputPath);
MarkUnfinished(CountPackets(FOutputPath, True));
Exit;
end;
@ -1410,6 +1456,7 @@ begin
RenameFile(AWork, FOutputPath);
Log(Format('Недописанного стало больше: %d кадров против %d.',
[New_, Old]));
MarkUnfinished(New_);
end
else
begin
@ -1417,6 +1464,35 @@ begin
if Old > 0 then
Log(Format('Прошлый недописанный файл полнее (%d кадров против %d), ' +
'он и оставлен.', [Old, New_]));
MarkUnfinished(Old);
end;
end;
{ Отметка «файл недописан» рядом с ним самим.
Нужна затем, чтобы отличить оборванную сборку от готового ролика: по
самому файлу это не видно — недописанный MKV открывается и играет.
Без отметки программа предлагала бы продолжить сборку поверх готового
фильма. }
procedure TExportThread.MarkUnfinished(AFrames: Int64);
var
F: TextFile;
begin
if AFrames <= 0 then
begin
DeleteFile(UnfinishedMark(FOutputPath));
Exit;
end;
try
AssignFile(F, UnfinishedMark(FOutputPath));
Rewrite(F);
try
WriteLn(F, AFrames);
finally
CloseFile(F);
end;
except
{ Не записалось — в следующий раз просто соберётся заново. }
end;
end;

View file

@ -643,9 +643,17 @@ begin
Поэтому здесь сдвиг к нулю выключен: место куску задаёт offset, а
отрицательные метки в самом начале ролика подберёт приёмник — у него
make_zero свой. }
{ initial_discontinuity помечает начало куска как разрыв потока.
Без него приёмник видит, что счётчик непрерывности прыгнул, и на
каждом стыке пишет «Packet corrupt» — по паре строк на кусок. Данные
при этом целы (сверено побитово), но журнал сборки забивается
жалобами, из-за которых потом не найти настоящую ошибку. С флагом
жалоб нет, а результат тот же: 240 кадров и одинаковый md5. }
Result := ['-avoid_negative_ts', 'disabled',
'-output_ts_offset', SecondsToStr(AAt),
'-muxdelay', '0', '-muxpreload', '0',
'-mpegts_flags', '+initial_discontinuity',
'-f', 'mpegts', 'pipe:1'];
end;

View file

@ -418,6 +418,9 @@ type
{ Строка и в общий журнал программы, и в журнал этой сборки. }
procedure LogExport(const AText: string);
{ Предлагает продолжить прерванную сборку с готовых кусков. }
{ Предупреждение о выбранном контейнере. False — человек передумал
собирать. }
function AskContainer(AHasPrecise: Boolean): Boolean;
function AskResume(const AOutputPath: string): Boolean;
procedure NormalizeProgress(Sender: TObject);
procedure NormalizeLog(Sender: TObject);
@ -4596,12 +4599,17 @@ begin
{ Если есть хоть один точный рез, предупреждаем про контейнер: в MP4
перекодированный кусок и скопированный уживаются не всегда, а MKV
к смене параметров потока терпимее. }
к смене параметров потока терпимее.
Признак — клип, начинающийся не с начала файла: попасть началом ровно
на опорный кадр можно только случайно, а во всех прочих случаях
огрызок до ближайшего опорного кадра перекодируется. Отметка edPrecise
сюда больше не годится: резы теперь точные всегда, независимо от неё. }
HasPrecise := False;
for T := 0 to FProject.TrackCount - 1 do
for C := 0 to FProject.Tracks[T].Count - 1 do
if (FProject.Tracks[T][C].EdgeIn = edPrecise) or
(FProject.Tracks[T][C].EdgeOut = edPrecise) then
if (FProject.Tracks[T][C].Kind = ckVideo) and
(FProject.Tracks[T][C].SrcIn > 0.0001) then
HasPrecise := True;
{ Куда собирать, в каком контейнере и что делать с текстом — один
@ -4635,14 +4643,8 @@ begin
end;
end;
if HasPrecise and
(LowerCase(ExtractFileExt(FExportChoice.Path)) <> '.mkv') then
if ThemedMessageDlg('nlve',
'В монтаже есть точные резы. В MP4 перекодированный стык может ' +
'воспроизводиться неправильно — надёжнее MKV.' + LineEnding +
LineEnding + 'Всё равно собрать в выбранный контейнер?',
mtWarning, [mbYes, mbNo], 0) <> mrYes then
Exit;
if not AskContainer(HasPrecise) then
Exit;
{ Сколько материала придётся пересобрать — считаем уже по тому списку,
который пойдёт в сборку: с участком он короче, и обещать человеку
@ -4844,6 +4846,49 @@ end;
попытками, и тогда «готовые» куски — от другой расстановки. Сама сборка
это проверит по отпечатку плана и при несовпадении соберёт заново, но
знать об этом человек должен заранее. }
{ Выбранный контейнер и что он за собой тянет.
У MP4 два недостатка, о которых человек узнаёт обычно поздно. Первый:
оглавление пишется в конец файла, и оборванная сборка не открывается
вовсе — продолжить её нельзя, только собрать заново. На четырёхчасовом
фильме это стоит часов работы. Второй: перекодированный стык точного
реза в нём воспроизводится не везде правильно.
Поэтому предлагается MKV — он лишён обоих, — а переупаковать его в MP4
можно потом, копированием потоков, без перекодирования. }
function TMainForm.AskContainer(AHasPrecise: Boolean): Boolean;
var
Text_, Ext: string;
begin
Result := True;
Ext := LowerCase(ExtractFileExt(FExportChoice.Path));
if Ext = '.mkv' then
Exit;
Ext := Copy(Ext, 2, 10);
Text_ := 'Прерванную сборку в ' + Ext + ' продолжить не удастся: ' +
'оглавление пишется в конец файла, и оборванный файл не открывается ' +
'вовсе. На длинном фильме это значит собирать всё заново.' + LineEnding;
if AHasPrecise then
Text_ := Text_ + 'К тому же в монтаже есть точные резы, а ' +
'перекодированный стык в этом контейнере воспроизводится ' +
'правильно не везде.' + LineEnding;
Text_ := Text_ + 'MKV этого лишён, а переупаковать его потом в ' + Ext +
' можно без перекодирования — «Перепаковка файлов...» в меню «Файл».';
case ThemedChoiceDlg('nlve', Text_,
['Собрать в MKV', 'Всё равно ' + Ext, 'Не собирать']) of
0:
begin
FExportChoice.Path := ChangeFileExt(FExportChoice.Path, '.mkv');
Log('Собираю в MKV: ' + FExportChoice.Path);
end;
1: ;
else
Result := False;
end;
end;
function TMainForm.AskResume(const AOutputPath: string): Boolean;
var
DonePath: string;
@ -4851,6 +4896,28 @@ var
N: Integer;
begin
Result := False;
{ Сборка потоком не оставляет кусков: продолжать там можно с самого
недописанного файла. Что он недописан, видно по отметке рядом — сам
по себе оборванный MKV открывается и играет. }
N := UnfinishedFrames(AOutputPath);
if (N > 0) and FileExists(AOutputPath) and
(LowerCase(ExtractFileExt(AOutputPath)) = '.mkv') then
begin
Result := ThemedMessageDlg('nlve',
Format('Прошлая сборка этого файла оборвалась, в нём %d %s.',
[N, Plural(N, 'кадр', 'кадра', 'кадров')]) +
LineEnding + LineEnding +
'Да — продолжить с того места, где оборвалось.' + LineEnding +
'Нет — собрать заново с начала, файл будет заменён.' + LineEnding +
LineEnding +
'Если монтаж с тех пор менялся, продолжать нечего: собранная часть ' +
'сделана по прежнему монтажу.',
mtConfirmation, [mbYes, mbNo], 0) = mrYes;
if Result then
Exit;
end;
DonePath := CacheDirFor(AOutputPath) + 'готово.txt';
if not FileExists(DonePath) then
Exit;