Честное предупреждение о контейнере и отметка недописанной сборки
У MP4 два неудобства, о которых человек узнавал поздно: прерванную сборку в нём продолжить нельзя вовсе, а перекодированный стык точного реза воспроизводится правильно не везде. Теперь об обоих говорится до начала работы, одним окном с кнопками «Собрать в MKV», «Всё равно mp4», «Не собирать». Признак точного реза заодно исправлен: смотрели на отметку edPrecise, а резы стали точными всегда — предупреждение молчало. Теперь признак — клип, начинающийся не с начала файла. Продолжить прерванную сборку предлагается по отметке рядом с файлом, а не по его наличию: оборванный MKV открывается и играет, и отличить его от готового больше нечем. Отметка ставится при неудаче, снимается при успехе и хранит число записанных кадров — его и показывают в вопросе. Ещё: на каждом стыке кусков приёмник писал «Packet corrupt», по паре строк на кусок. Данные целы, но журнал забивался, и настоящую ошибку в нём было не найти. Лечится ключом -mpegts_flags +initial_discontinuity у отправителя: сверено по кадрам и по md5, результат тот же.
This commit is contained in:
parent
4522f0000a
commit
f841ed79ec
4 changed files with 170 additions and 11 deletions
|
|
@ -834,6 +834,14 @@ Wine с новой библиотекой не грузилось ничего.
|
|||
`TThread`, `Name` и `Caption` — у формы. Компилятор ловит это как
|
||||
«Duplicate identifier», но причину не объясняет.
|
||||
|
||||
**Стык кусков в потоке выглядит как порча.** Каждый кусок — это
|
||||
отдельный поток MPEG-TS со своим счётчиком непрерывности, и на стыке
|
||||
приёмник пишет «Packet corrupt», по паре строк на кусок. Данные при этом
|
||||
целы, но журнал сборки забивается жалобами, из-за которых не найти
|
||||
настоящую ошибку. Лечится ключом `-mpegts_flags +initial_discontinuity`
|
||||
у отправителя: он помечает начало куска как разрыв, жалобы исчезают, а
|
||||
результат тот же — сверено по кадрам и по md5.
|
||||
|
||||
**Приёмник двигает весь ролик.** `-avoid_negative_ts make_zero` у
|
||||
приёмника сдвигает поток так, чтобы самая ранняя метка декодирования
|
||||
стала нулём, — а у первого куска она уходит в минус на задержку
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue