diff --git a/README.md b/README.md index 58f92ce..9e67f6b 100644 --- a/README.md +++ b/README.md @@ -834,6 +834,14 @@ Wine с новой библиотекой не грузилось ничего. `TThread`, `Name` и `Caption` — у формы. Компилятор ловит это как «Duplicate identifier», но причину не объясняет. +**Стык кусков в потоке выглядит как порча.** Каждый кусок — это +отдельный поток MPEG-TS со своим счётчиком непрерывности, и на стыке +приёмник пишет «Packet corrupt», по паре строк на кусок. Данные при этом +целы, но журнал сборки забивается жалобами, из-за которых не найти +настоящую ошибку. Лечится ключом `-mpegts_flags +initial_discontinuity` +у отправителя: он помечает начало куска как разрыв, жалобы исчезают, а +результат тот же — сверено по кадрам и по md5. + **Приёмник двигает весь ролик.** `-avoid_negative_ts make_zero` у приёмника сдвигает поток так, чтобы самая ранняя метка декодирования стала нулём, — а у первого куска она уходит в минус на задержку diff --git a/src/uExport.pas b/src/uExport.pas index d688f03..13a8a6f 100644 --- a/src/uExport.pas +++ b/src/uExport.pas @@ -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; diff --git a/src/uFfmpegCmd.pas b/src/uFfmpegCmd.pas index e4a79f9..bb7735d 100644 --- a/src/uFfmpegCmd.pas +++ b/src/uFfmpegCmd.pas @@ -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; diff --git a/src/uMainForm.pas b/src/uMainForm.pas index e148d85..baaa7f3 100644 --- a/src/uMainForm.pas +++ b/src/uMainForm.pas @@ -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;