Кадр для окна пишется в кэш проекта, а кэш заводится при первой сборке
вставок. Кадрирование открывают и раньше — тогда ffmpeg падал с «No such
file or directory», а окно показывало «Не удалось получить кадр» вместе с
цепочкой фильтров. Выглядело так, будто дело в цепочке; дело было в
отсутствующей папке.
Замерено на том же файле и той же цепочке: в существующей папке команда
отдаёт кадр в 320 КБ, в несуществующей — код выхода 254.
Папка теперь создаётся перед записью. Не создалась — кадр пишется во
временный файл, чтобы окно всё равно показало картинку.
Проверка tests/cropdialog.sh: проект без папки кэша, щелчок по кнопке
кадрирования, смотрим, появился ли файл кадра. Папка убирается прямо
перед щелчком: иначе её успевает создать заготовка чёрной вставки, и
проверка меряет не то. На прежней сборке падает.
Клип, не заполняющий кадр, стал уходить плееру отдельной дорожкой, и
весь показ пошёл через цепочку фильтров. Цепочка работает всё время
показа и считается процессором: на кадре 2688x1512 видео начало
отставать от звука.
Теперь плееру достаются только прозрачные клипы и клипы с ключом — ради
них это оправдано, их участки бывают длинными, и пересобирать минуты
материала дорого. Клип, который просто не заполняет кадр, сводится
заранее вместе с тем, что под ним: предпросмотр играет обычный файл и
показывает ровно то, что даст сборка.
Попутно, из разбора того же случая:
— заполнитель до первого наложения нарезается по длине файла. Одной
записью mpv её обрезает («entry 0 uses 7082.687000 seconds, but file
has only 15.021000»), дорожка оказывается короче монтажа и не
совмещается с ним по времени;
— появление заготовки вызывает перезагрузку списка: строка списка при
этом не меняется, и без признака готовности плеер так и оставался без
дорожки наложения;
— в журнал пишется, сколько окон наложения и готовы ли файлы, а также
собранная цепочка показа и неудача video-add. Без этого «картинки
нет» нечем объяснить.
В заголовке окна теперь номер сборки: nlve 42 — фильм.nlve. Он растёт на
единицу при каждой сборке. По нему сразу видно, та ли это сборка, где
правка уже есть, — сегодня время ушло на то, что проверяли старым
двоичным файлом. Счётчик в build.number, в репозиторий не входит.
Проверено на монтаже с картинкой на 300-й секунде: в окне она лежит
поверх видео, цепочки показа нет.
В окне картинки не было вовсе, хотя в ролик она попадала. Плеер получал
сам файл, а его декодер на этом webp спотыкался: «webp: invalid TIFF
header in Exif data» — накладывать стало нечего. У неподвижного кадра
есть и вторая беда: в списке кусков у него нет длины.
Теперь наложению отдаётся заготовка — обычное видео нужной длины,
сделанное тем же путём, что и остальные вставки, только цепочкой без
полей: поля тут вредны, чёрное закрыло бы нижнее видео. Масштаб уже
внутри куска, плееру остаётся поставить его на место.
Заготовки наложения собираются отдельным списком: уплощение ниже
заполняет список недостающего заново и затёрло бы их.
Проверено на том самом файле: в окне картинка легла поверх видео, в
ролике слева нижнее видео (253, 0, 0), справа картинка. Числовые
проверки в flattest: кусок собирается слоями, у верхнего слоя полей нет,
предпросмотру отдан кусок из кэша, и он заказан к созданию.
Уменьшенная и сдвинутая картинка выходила в ролик с чёрными полями, и
нижнюю дорожку закрывал не сам клип, а эти поля. Так же вело себя и
уменьшенное видео. Между тем уменьшают клип ровно затем, чтобы под ним
было видно другое.
Теперь «не заполняет кадр» означает то же, что «не сплошной»: наравне с
выбитым цветом и прозрачностью. Такой клип уходит в накладываемые —
предпросмотр рисует его поверх нижнего силами плеера, сборка собирает
участок слоями, где у верхнего слоя цепочка без полей и место, куда он
встаёт. Класть не на что — поля остаются полями, как и были.
Решение одно на оба пути: IsOverlayClip, и в нём TransformFillsFrame
считает по тем же числам, что уходят в фильтр.
Заодно две вещи, всплывшие по дороге:
— у картинки своя ветка уплощения, и слои там не строились вовсе: клип с
прозрачностью в окне накладывался, а в ролике нет. Теперь строятся.
— в сборке ветка «готовая вставка» стояла раньше ветки слоёв, и кусок со
слоями уходил в неё за несуществующей заготовкой. Заготовка для такого
куска больше и не заказывается: это были бы минуты впустую.
Проверка tests/imagecrop.sh получила второй случай: под картинкой видео,
вокруг неё обязано быть видно его. На прежней сборке там чёрное.
Наезд двух клипов на одной дорожке — заказ на переход, то есть обычное
дело. Нажатие же всегда попадало в верхний по списку, и нижний клип
нельзя было ни выделить, ни потащить: чтобы до него добраться,
приходилось сперва убирать верхний.
Правило такое. Нажатие берёт тот из накрытых клипов, который уже выделен,
иначе верхний. Щелчок без движения по уже выделенному переводит выделение
на следующий, после последнего снова на верхний. Переключение сделано на
отпускании намеренно: пока кнопку не отпустили, неизвестно, щелчок это
или начало протяжки, а менять клип под рукой посреди протяжки нельзя.
Благодаря этому протяжка тащит именно то, что перед ней выбрали
щелчками.
Строка состояния говорит, сколько клипов под указателем: перебор, о
котором не сказали, — тайное знание.
Отбор вынесен в TTrack.ClipsAt, числовая проверка — в lfmcheck.
tests/pickcycle.sh щёлкает дважды и тащит: на прежней сборке уезжает
верхний клип, а нижний едет за ним схлопыванием.
На монтаже рисовался косой крест через целый клип, строка состояния
обещала переход, а наложения не было: ролик собирался верно. Крест уходил
сам после Ctrl+Z, после любого действия с отменой и после схлопывания.
Это был не наезд, а сбитый порядок клипов в списке дорожки. Список
отсортирован по времени начала, и соседи по списку обязаны быть соседями
по времени — на этом стоят и отрисовка креста, и сообщение о наезде.
Перетаскивание же двигает не одну дорожку: связка ведёт звук за
картинкой, схлопывание ведёт соседей, в режиме «все дорожки» — сразу на
нескольких. Сортировалась только та дорожка, с которой тащили. Клип,
уехавший с конца монтажа в середину, оставался в списке чужой дорожки
последним, и её соседями по списку становились клипы, далёкие друг от
друга по времени.
Отмена и схлопывание лечили ровно потому, что пересортировывают списки.
Сборка всё это время была права: она идёт по границам и сортирует
дорожку сама.
Теперь порядок возвращается всем дорожкам, чьи клипы поехали
(SortMovedTracks). Оба места, считающие соседей по списку, заодно
пропускают пару, идущую не по времени: страховка от того же симптома в
другом месте.
Проверка tests/dragorder.sh тащит связку за звук с конца монтажа в
середину и читает порядок из сохранённого файла. На прежней сборке
дорожка картинки остаётся в порядке 0, 14, 7.991 — проверка падает.
Описание пополнено тремя абзацами о новом поведении и разделом о том,
как гонять проверки с окном: где взять файл доступа невидимого экрана и
почему якорю увеличения нужен экран шире 1280.
Лог сессии и утверждённый план положены в проект: файлы планов в
~/.claude/plans ротируются, а решения должны переживать сессию.
Четыре проверки, которые гоняют саму программу, собраны в одном месте с
припиской: каждую стоит прогонять и на прежней сборке, иначе она не
проверяет ничего. Названы монтажи, которые им нужны, и почему их нет в
репозитории.
В таблицу модулей дописаны десять, которых там не было: текст и его
замер, разметка для libass, окна ключа, перекодирования, сборки и «о
программе», страница текста со ссылками, запуск ffmpeg с ходом работы и
вшитые значки.
Показатель висел на пяти процентах, потом прыгал к семидесяти девяти,
потом сразу готово. Виноват не ffmpeg: замерено, что он отдаёт ход
безупречно ровно — блок каждые полсекунды, 7%, 14%, 20%, 27% и дальше
без единого скачка.
Блок заканчивается строкой «progress=continue», в которой места нет:
она сообщает только, что работа продолжается, и разбор честно оставляет
переданное число как было. Я передавал туда ноль, поэтому каждый блок
заканчивался обнулением доли. Показатель стоял и дёргался вверх лишь
тогда, когда опрос попадал между строкой со временем и этой.
Теперь отсчёт начинается с уже известного места — как это давно делает
сборка ролика. Проверено глазами на невидимом экране: шесть снимков
подряд через полторы секунды дают 54, 65, 74, 82, 91, 99 процентов,
полоса растёт вместе с числом.
Ловушку закрепляет проверка в tests/lfmcheck: «progress=continue» и
«progress=end» узнаются, но времени не трогают.
Зона захвата совпадала с нарисованным квадратиком — девять точек на
девять. Пока затухание нулевое, ручка стоит в углу клипа и целиться есть
куда; стоит её сдвинуть, она висит посреди клипа сама по себе, и попасть
в неё почти нельзя.
Зона захвата теперь шире нарисованного: по шесть точек в стороны и семь
вниз, внутрь клипа, — рука уводит курсор туда, а не выше клипа. Если на
узком клипе зоны обеих ручек налезли друг на друга, берётся та, к которой
ближе.
Заодно над зоной меняется курсор. Раньше над ручкой он был такой же, как
над всем клипом, и где она — узнать было нельзя.
Проверено протяжкой мыши на невидимом экране (tests/fadegrab.sh): первый
захват за угол даёт затухание 1.387 с, второй — с прицелом на пять точек
ниже и четыре в стороне от середины ручки — доводит до 2.261 с. На сборке
из прошлого коммита второй захват не делает ничего.
Полоса ползла по догадке, а подпись показывала замер — и человек видел
полосу, заполненную на девять десятых, рядом с нулём процентов. Что в
этот момент происходит, по такой паре понять нельзя.
Теперь доля одна на обоих. Берётся она у самого ffmpeg: сборка участка с
затуханием идёт минуту и дольше, а «кусок 1 из 1» всё это время не
меняется, поэтому ffmpeg спрашивают ключом -progress, докуда он дошёл.
Пока он не ответил ни разу, полоса трогается с места на пару процентов и
там стоит — пустая полоса и есть правда о только что начатой работе.
Догадка заодно перестала закрывать промежуток до вехи целиком: предел —
девять десятых, остаток виден глазом. Проверка на это добавлена в
tests/progress.
Полоса в окне сборки была системной, и под gtk2 заполнялась ступенями по
восемь кусков на ширину. Теперь там та же своя полоса, что и внизу
главного окна, — заполняется сплошь. Чтобы она читалась из файла формы,
у неё опубликованы Anchors, Align и Visible: читатель формы берёт только
опубликованные свойства, и без этого окно падало при открытии.
Проверено глазами на невидимом экране: «Собираю наложение 1 из 1 — 2%»
при полосе в 2%, через четыре секунды 69% при полосе в 69%.
Затухание в полторы-две минуты собиралось минутами, и всё это время
предпросмотр ждёт. Виноваты оказались не фильтры: на десяти секундах
2688x1512 цепочка со сведением стоит 13.5 с, а вовсе без фильтров —
12.7 с. Время уходит на разбор HEVC процессором.
Ключ -hwaccel cuda перед входом убирает почти всё: настоящее затухание
на семьдесят секунд собирается 18.3 с вместо 75.9 с, картинка бит в бит
та же (одинаковый md5 потока, 4196 кадров там и там).
Просится он только там, где кодировать будет та же карта — то есть
выбран NVENC. На машине без неё ключ не появляется вовсе. Сверх того у
подготовки вставок есть откат: не пошло — повторяем на процессоре, как
это уже делает склейка с перекодированием.
Сборка ролика пока по-старому: там кусок с наложением идёт в общий
поток, и повторить его после отказа посреди работы нельзя — данные уже
ушли в трубу. Это отдельная задача.
Затухание в семьдесят секунд не собиралось никогда: сборка сведённого
куска шла полторы минуты, и любая правка в это время сбрасывала её на
ноль. В журнале это видно как «Расстановка изменилась» и тот же самый
запуск ffmpeg по кругу.
Причина — в вопросе, который задавался. «Нужна ли ещё эта работа»
проверялось по списку недостающего, а недостающее у нас — это то, чего
нет на диске. У куска, который делается прямо сейчас, файл уже есть:
ffmpeg создаёт его в первую же секунду. Кусок в список не попадал, ответ
был «не нужна», работа отменялась.
Теперь уплощение отдельно отдаёт список нужного — независимо от того,
лежит файл на диске или ещё пишется. Заодно закрыта та же дыра со
вставками тишины: проверка их вообще не видела, потому что звуковые
дорожки считаются отдельным проходом.
Проверено обеими сторонами (tests/genrestart.sh, на настоящем материале,
затухание 70 с): пять резов в стороне от затухания — один запуск и ни
одной отмены, было шесть запусков и десять отмен. Удалили всё — работа
прекращается за две секунды, как и прежде.
Клип со своим кадрированием играл в окне во весь кадр: цепочка фильтров
у mpv одна на весь список кусков, а включить её на одном куске и
выключить на следующем нечем — из фильтров ffmpeg по времени
переключается только наложение.
Обошлось без цепочки вовсе. У вывода mpv своя обрезка, и делает её
видеокарта: курсор зашёл в клип с кадрированием — выставили, вышел —
сняли. Ни перестройки цепочки, ни копирования кадров из памяти
видеокарты в обычную.
Рамку источника считает теперь один код на две стороны: по ней окно
«Кадрирование и поворот» рисует рамку, и её же плеер отдаёт как обрезку.
Проверка в tests/lfmcheck сверяет рамку с фильтром сборки: увеличение
2.77 даёт 970x546 посередине, и это ровно то, что вырезает
crop=2688:1512:2379:1338 после scale=7446:4188.
Проверено глазами на невидимом экране, на настоящем материале с дрона:
картинка в окне совпала с готовым роликом, на границе с обычным клипом
обрезка снимается и возвращается. Сверено со сборкой из прошлого
коммита — там на том же месте кадр без кадрирования.
Не покрыто и записано в BACKLOG: отражение (свойства для него нет),
растяжение и участки с наложением — там кадр уже сведён цепочкой.
Ctrl с колесом должен оставлять на месте то, на что смотрит мышь, а
оставлял точку левее — ровно на левый край таймлайна. При мыши у левого
края опора и вовсе уходила за пределы окна.
Причина: MousePos у DoMouseWheel по описанию экранный, но под gtk2 в нём
уже клиентские координаты, и ScreenToClient переводил их второй раз.
Берём Mouse.CursorPos — он экранный в любой системе.
Замерено на невидимом экране: до правки мышь в 700, 1000 и 1300 давала
опору в 260, 560 и 860; после — ровно 700, 1000 и 1300. Проверка
оформлена в tests/zoomanchor.py и прогнана на обеих сборках: на прежней
она падает, на новой проходит.
У MP4 два неудобства, о которых человек узнавал поздно: прерванную
сборку в нём продолжить нельзя вовсе, а перекодированный стык точного
реза воспроизводится правильно не везде. Теперь об обоих говорится до
начала работы, одним окном с кнопками «Собрать в MKV», «Всё равно mp4»,
«Не собирать».
Признак точного реза заодно исправлен: смотрели на отметку edPrecise,
а резы стали точными всегда — предупреждение молчало. Теперь признак —
клип, начинающийся не с начала файла.
Продолжить прерванную сборку предлагается по отметке рядом с файлом, а
не по его наличию: оборванный MKV открывается и играет, и отличить его
от готового больше нечем. Отметка ставится при неудаче, снимается при
успехе и хранит число записанных кадров — его и показывают в вопросе.
Ещё: на каждом стыке кусков приёмник писал «Packet corrupt», по паре
строк на кусок. Данные целы, но журнал забивался, и настоящую ошибку в
нём было не найти. Лечится ключом -mpegts_flags +initial_discontinuity
у отправителя: сверено по кадрам и по md5, результат тот же.
Потоком кэша кусков нет, и продолжать было не с чего. Но недописанный
файл сам по себе — уже собранная часть фильма: в нём видно, сколько
целых кадров успело записаться. Программа отступает до границы
последнего целого куска, переносит эту часть и досылает остаток.
Дописать в конец существующего MKV нельзя, поэтому продолжение пишется
в новый файл. MP4 продолжать нельзя вовсе: оглавление в конце, и
оборванный файл не открывается — об этом говорится прямо.
Готовый файл теперь цел до последнего: сборка идёт в файл рядом и
занимает место готового, только когда дописана. Сорвавшаяся попытка не
отбирает уже собранное — из двух недописанных остаётся тот, где кадров
больше, и считаются они даже после запроса остановки.
Отдельно пришлось снять сдвиг, который приёмник накладывает на весь
ролик: перенесённая часть несла его в себе, и куски за ней задваивали
кадр. Замерено: обрыв в четырёх разных местах, продолженная сборка
каждый раз совпала с собранной за один раз попиксельно.
В nlvetool у render появились --cancel-after=<сек> и сочетание слов:
cache resume проверяет, что старый путь через кэш не сломался.
Раньше каждый кусок монтажа вырезался в отдельный файл, и только потом
список этих файлов шёл на склейку. Кэш весил столько же, сколько готовый
фильм — замерено 110 ГБ на часе материала, — а диск писал всё дважды.
Теперь каждый кусок режется своим ffmpeg прямо в MPEG-TS на стандартный
вывод, все потоки подряд переливаются в один принимающий ffmpeg, а тот
пишет готовый файл. Место куску задаёт -output_ts_offset, считаемый в
кадрах от начала. На диск ложится только результат и звуковая дорожка.
Через кэш по-прежнему собирается то, что потоком не выходит: кодек, не
переносимый в MPEG-TS, и пересборка всего ролика под площадку.
Сверено на четырёх монтажах, включая настоящий материал с дрона
(HEVC 2688x1512, 55 Мбит/с, файлы с шестью потоками): картинка совпала
попиксельно, PSNR бесконечен на каждом кадре, длина и размер те же.
Сверка шла и с прошлым выпуском, а не только нового пути со старым.
На отрывке в 17.5 с: 14.2 с и 421 КБ на диске против 17.5 с и 152 МБ.
По дороге найдены две ловушки, обе записаны в README:
-avoid_negative_ts make_zero отменяет -output_ts_offset, а -fflags
+igndts у приёмника сминает кадры на стыках.
Две беды у всех кнопок нижней полосы.
Всплывающая подсказка появлялась прямо поверх кнопки, о которой
рассказывает, и перехватывала щелчок: нажать кнопку было нельзя.
Подсказка переехала в строку состояния — она в той же полосе, ничего не
закрывает и меняется сразу после щелчка, не дожидаясь нового наведения.
Строка состояния теперь занимает всё свободное место справа, пока рядом
не показан ход работы, а самые длинные подсказки укорочены: раньше их
обрезало на полуслове.
Второй щелчок подряд виджетсет отдавал как двойной, и кнопка его не
видела: быстрая очередь щелчков срабатывала через раз. У кнопки убран
csDoubleClicks, а на случай, если двойной всё же дойдёт, он считается
за обычный.
Проверено: tests/lfmcheck, раздел «Кнопки нижней панели», и
tests/buttonclicks.sh мышью на невидимом экране — по всем шести
переключателям очереди из 1-5 щелчков отрабатываются полностью.
Режимов выделения стало два, переключаются клавишей D и кнопкой внизу,
режим показан её значком.
Рамкой по клипам — как было. По времени — протяжка отмечает промежуток по
всем дорожкам сразу, он остаётся виден пунктиром, и S режет все дорожки по
обоим его краям. Выделенной после этого остаётся ровно середина: следующим
действием её обычно удаляют, а выделение на старых клипах убрало бы не то.
Курсор теперь идёт за резом: после S встаёт на место реза (в режиме по
времени — на начало промежутка), после удаления куска — на место стыка,
после схлопывания дыры — туда же. Раньше в это место приходилось попадать
мышью заново.
Рез кладётся на кадровую сетку: между кадрами времени нет, и рез на доле
кадра оставлял бы в монтаже время, которого не бывает.
Выделение рамкой требует настоящего перекрытия, а не касания краями:
сосед, кончающийся ровно на краю рамки, попадал в выделение из-за разницы
в последнем знаке дробей.
Кнопка журнала и строка состояния сдвинуты вправо: переключатель выделения
встал в один ряд с прилипанием и схлопыванием.
Значки — icons8, набор ios-glyphs, авторство дописано в attribution.txt.
Набора «ios26» на их раздаче нет (404), взят ближайший по виду к нынешним.
Проверки: рез по курсору и по краям промежутка, кадровая сетка, выделение
середины, курсор после удаления и после схлопывания.
Меню «Проект» → «Проверить и починить проект...». Показывает найденное и
чинит только по согласию, отменяется через Ctrl+Z.
Ищутся клипы мимо кадровой сетки, клипы на дорожках чужого вида и
повторяющиеся имена дорожек. Первое — наследство: в старых проектах
времена записаны как попало (2.457599 с при 59.94 к/с — это 147.31 кадра),
и пока монтаж просит доли кадра, предпросмотр и сборка округляют их
каждый по-своему. Сдвиг при починке не больше полкадра.
Края отмеченного участка при сборке тоже кладутся на сетку: подрезка
возвращала в крайние куски доли кадра, которые уплощение только что
убрало.
Проверено на копии настоящего проекта: 82 клипа, после починки мимо сетки
ни одного, клипы остались встык, общая длина изменилась на 0.028 с за два
часа; Ctrl+Z возвращает как было. Плюс проверка в lfmcheck, включая клип
короче кадра — он не должен схлопнуться в ничто.
Рез всегда точный. Раньше начало куска притягивалось к ближайшему
опорному кадру, а звук брался из непритянутой точки: по журналам сборки
средняя притяжка 0.24 с, худшая 0.99 с, и на длинном куске этот сдвиг
держится все его минуты. Плата за точность — доли секунды перекодирования
на рез. Второй вид реза (Shift+S) стал тем же самым и убран.
Разбор заголовка MP4 не учитывал список правок (elst): метки опорных
кадров уезжали на задержку показа, на пробе два кадра. Сверка быстрого
пути с ffprobe теперь совпадает точно, в том числе на записи с B-кадрами.
Куску заказывается точное число кадров, а не только время: при
копировании ffmpeg отмеряет конец по времени декодирования, и у записи с
B-кадрами кусок выходил длиннее.
Склейке длительность каждого куска задаётся явно. Последний пакет
скопированного куска показывается позже прочих, контейнер считал его в
длительность, и время картинки растягивалось — 0.05 с на кусок.
Звука в кусках картинки больше нет: он не используется, а в MPEG-TS ещё и
сбивает выравнивание склейки на 36 мс.
Границы кусков ложатся на кадровую сетку: картинка выходит целым числом
кадров, звук набирается до отсчёта, и доли кадра разводили их в стороны.
Разогрев кодировщика звука компенсируется: сжатый AAC начинается со
служебных отсчётов, MP4 помечает их пропускаемыми, Matroska — нет, и звук
ехал на 21.3 мс позже. Величина меряется пробным сжатием.
Продолжение прерванной сборки чинится заодно: готовность проверяется у
каждой части, а не у куска целиком. При точном резе частей две, и вторая
при продолжении терялась.
В журнал сборки добавлена проверка синхронности: сколько кадров вышло и
сколько просил монтаж, с вердиктом и худшей частью.
Замер (tests/syncprobe.sh, исходник со вспышкой и щелчком на каждой
секунде): было до 0.6 с и росло по ходу ролика, стало 0.02-0.04 с без
накопления при точности измерения 0.017 с.
Сохранение теперь отвечает в строке состояния: «Проект сохранён: имя,
время». Раньше об этом знал только журнал, а он по умолчанию закрыт —
нажал Ctrl+S и не знаешь, дошло ли до файла.
Проект пишется во временный файл рядом и подставляется целиком; прежний
остаётся как .bak. Оборванная запись больше не может оставить огрызок
вместо проекта.
Автосохранение: пока есть несохранённое, раз в полминуты рядом с
проектом пишется копия .autosave. При открытии программа сравнивает её
время с временем проекта и, если копия новее, показывает оба времени и
предлагает её открыть. После сохранения проекта копия убирается.
Журнал рядом с программой больше не переписывается при каждом запуске:
запуски дописываются с заголовком, файл урезается при переросте. Прошлый
запуск — как раз тот, в котором обычно и надо разбираться.
Клавиша \ подтягивает окно к курсору и ставит его посередине: её жмут
после реза или выделения. Переключатель «курсор посередине» остался
кнопкой с целью.
Рамка выделения: протяжка по пустому месту дорожек обтягивает клипы,
Delete убирает всё, что она задела. Щелчок без движения по-прежнему
ставит курсор.
Рез по курсору (S) режет все дорожки, а не ту, что под указателем мыши:
стоило указателю оказаться над пустой дорожкой, и клавиша молча не
делала ничего. Теперь рез отвечает числом разрезанных клипов.
Курсор после правки монтажа улетал в конец: при перезагрузке списка
кусков mpv сбрасывает отсчёт времени, программа ждала перемотки, не
дожидалась и подтягивала курсор к плееру. На паузе курсор теперь
остаётся там, куда его поставил человек. Это и делало Ctrl+Shift+R
неудобным: после каждого схлопывания приходилось целиться заново.
Схлопывание дыры ищет её с допуском и по всем дорожкам, а не только под
мышью, и называет дорожку в строке состояния.
Ответ на команду держится в строке состояния несколько секунд: раньше
его через долю секунды затирало сообщение о пересборке предпросмотра.
Проверки: рамка, рез всех дорожек, допуск при схлопывании (lfmcheck).
Отбивку или текстовый блок проще размножить копией, чем заводить заново
через меню и настраивать оформление с нуля.
Копируются выделенные клипы, вставляются по курсору. Связанные берутся
вместе с парой — у картинки со звуком иначе уехала бы половина, — но
связка у копий своя: копия и подлинник не должны ездить вместе. Взаимное
расположение сохраняется, набор сдвигается к курсору.
Дорожка выбирается сама: сначала та, откуда копировали, если это время на
ней свободно; потом любая подходящая по виду; в крайнем случае заводится
новая. Наложить копию на соседа по дорожке нельзя — это означало бы
переход, которого никто не заказывал.
В модели появился TClip.CopyAllFrom: полная копия, кроме места на
монтаже. Проверка в lfmcheck перебирает перенесённые свойства и отдельно
убеждается, что место и связка не переносятся.
При выключенном «курсоре посередине» окно всё равно подтягивалось к
курсору, стоило тому выйти за край видимой части. Со стороны это выглядело
так: Shift с колесом, первый поворот прокручивает, второй возвращает
обратно. Человек отматывает осознанно, и возвращать его силой нельзя.
Теперь выключенный переключатель означает именно неподвижное окно.
Подтягивание при пуске воспроизведения осталось: там курсор надо привести
в поле зрения.
Проверено на невидимом экране при идущем воспроизведении: оба поворота
колеса двигают линейку (1651 и 1581 изменившихся точек), и к исходному
виду она не возвращается.
Дыра в сорок минут требовала двухчасового чёрного файла: лестница готовых
длин доходила до восьми часов, и NVENC кодировал такое минутами. Отсюда
«Делаю чёрную вставку 1 из 3», которое не кончается. Теперь длины две, 15
и 60 секунд, длинная дыра набирается повторами минутного файла, а само
чёрное кодируется мегабитом вместо полусотни.
Промежуток короче половины кадра дырой больше не считается: между
клипами, сдвинутыми встык мышью, оставались микросекунды. Мышь при этом
кладёт клипы на кадровую сетку — между кадрами времени не существует.
Правка монтажа прекращает генерацию, ставшую ненужной: раньше закрытая
дыра не отменяла уже начатую работу.
Ctrl+Shift+R работает без мыши: дыра ищется на активной дорожке, если
указатель не над дорожками, и команда отвечает в строке состояния, а не
молчит.
Вжигание текста режет кусок на части и пересобирает только середину.
Надпись в тридцать секунд внутри сорокаминутного куска стоила сорока
минут пересборки. Часть после надписи помечается точным резом, иначе
копия пошла бы от опорного кадра и картинка сдвинулась бы.
Проверки: flattest 13 и 14, схлопывание без мыши в lfmcheck, сборка с
надписью внутри длинного куска — 1.000000 с пересборки вместо 6.
Картинка собиралась в промежуточный файл, а потом сшивалась со звуком в
конечный. На часовом ролике это 91 ГБ в кэше плюс 91 ГБ рядом и лишний
полный проход копирования — ради того, чтобы приписать звуковую дорожку.
Теперь звук собирается раньше картинки, а склейка сразу кладёт всё вместе
в конечный файл. Тем же путём пошла и сборка с перекодированием.
Промежуточного файла нет вовсе, проход на один меньше.
Куски после удачной сборки удаляются, и в журнале сказано, сколько места
освободилось: на часовой съёмке их набирается больше сотни гигабайт, а
нужны они только чтобы продолжить прерванную сборку. После неудачной
сборки они остаются — с них и продолжают. Настройка keep_parts оставляет
их всегда.
Копирование оформления: Ctrl+Shift+C берёт с выделенного клипа
кадрирование, поворот, увеличение, смещение, прозрачность, ключ,
затухания и громкость, Ctrl+Shift+V применяет их ко всем выделенным. Вид
клипа и текст не переносятся: свойства копируют между разными клипами.
Порядок дорожек: слева в заголовке две стрелки, у крайних дорожек
соответствующая нарисована тускло и не нажимается. Двойной щелчок по
названию переименовывает дорожку — на монтаже из десятка дорожек «карта»
и «титры» читаются лучше, чем V1 и Т1. То же есть и в меню по правому
щелчку. Ввод имени — своим тёмным окном: системный InputQuery выглядел бы
белым пятном.
Окно сборки показывает, сколько прошло и сколько примерно осталось.
Оценка по пройденной доле; скорость на деле меняется, поэтому «примерно»
и пересчёт на каждом шаге.
Прерванная сборка продолжается с готовых кусков. Каждый доведённый до
конца кусок помечается в списке рядом с ними: одного наличия файла мало —
сборку могли прервать посреди записи. Первая строка списка — отпечаток
плана; не совпал, значит монтаж менялся, и всё собирается заново.
Проверено: четыре куска взяты готовыми, два пересобраны, склейка не
потеряла ни одного кадра; при изменённом монтаже готовые куски отвергнуты.
Вжигание не доходило до сгенерированных вставок. Картинки и чёрное
отбирались первой же веткой сборки — до проверки на пересборку — и
копировались целиком вместе с отметкой о разметке. Галка вжигания стояла,
кусок в журнале был помечен «с текстом», а в ролике надписи не было.
Причину показал журнал сборки рядом с файлом: помечено «пересобирается»,
а команда — копирование.
Теперь вставка с разметкой идёт общей веткой пересборки, и время у неё
берётся своё: она начинается с нуля, а не с середины монтажа.
У линейки появился верхний этаж в двенадцать точек — про отметки.
Протяжка по нему отмечает участок, даже если отметок не было; щелчок без
движения их снимает. Уголки по краям тянутся, как и раньше.
Заголовок дорожки: правый щелчок открывает меню — поднять, опустить,
удалить (с клипами — с подтверждением). Заголовок можно и потащить:
порядок дорожек есть порядок перекрытия, и менять его перетаскиванием
привычнее, чем через меню.
Отмеченный участок. Клавиши I и O ставят начало и конец по курсору,
полоса под линейкой показывает отмеченное, сборка собирает только его.
Без этого проверить правку на длинном монтаже нельзя: каждая проверка
стоит столько же, сколько идёт весь ролик. Отметки живут в проекте.
Разметка текста при этом берётся с начала участка, иначе надписи легли бы
со сдвигом.
Фотография пропадала из ролика. Кусок с наложением подаёт слои в ffmpeg
как есть, а одиночная картинка даёт ровно один кадр: кусок в три секунды
получался длиной 0.016667 с. Теперь у слоя есть признак «это картинка», и
она повторяется ключом -loop с частотой кадров проекта.
Текст пропадал там же. Вжигание помечает кусок разметкой, но ветка
наложения про неё не знала. Пока оверлей лежит на всём ролике, этой
веткой идут все куски — и текста не было нигде. Теперь разметка вжигается
последним звеном цепочки, поверх сведённой картинки.
Качество пересобранных кусков. Битрейт не задавался вовсе, и кодировщик
брал своё значение по умолчанию: 5.9 Мбит/с там, где исходник 56. Теперь
пересборка целится в наибольший битрейт среди файлов проекта, с потолком
в полтора раза выше и буфером вдвое. Тем же битрейтом делаются вставки из
фотографий.
Проверки: обрезка списка кусков по участку (nlvetool flattest, 9),
сборка фотографии под наложением и вжигание текста поверх наложения
(nlvetool render ... burn).
Оверлей показывался как есть, со своим фоном: чтобы увидеть, что выйдет
после сборки, приходилось собирать. Теперь выбитый цвет и приглушение
считаются в той же цепочке плеера, на лету.
Строки фильтров стали общими со сборкой (ColorKeyFilter, OpacityFilter
в uTransform): окно и готовый ролик больше не могут разойтись по тому,
как записан цвет или порог.
Переключатель в меню «Вид», состояние переживает перезапуск. Замер на
2688x1512, 600 кадров: основа 2.07 с, с наложением 2.43 с, с ключом и
прозрачностью 2.66 с — вчетверо быстрее реального времени, но на слабой
машине это заметнее, поэтому выключить можно.
В бэклог записано окно настроек: переключателей, которые стоят машинного
времени, набралось много, и живут они кто где — кнопками внизу, в меню,
а выбор кодировщика вообще нигде.
Курсор воспроизведения по щелчку вставал ровно туда, куда попала мышь.
В стык двух кусков так не попасть: на общем плане одна точка экрана —
это секунды материала, а ставят курсор чаще всего именно на стык.
Теперь он притягивается к границам клипов на любой дорожке и к началу
монтажа. Работает и при щелчке по клипу, и по линейке, и по пустому
месту, и при протяжке. Shift меняет решение на противоположное — как у
прилипания клипов.
Переключатель свой, со значком клея, рядом с магнитом: клипы липнут при
перетаскивании, курсор — при щелчке, это разные привычки, и одно бывает
нужно без другого. Состояние переживает перезапуск.
Расчёт открыт наружу и проверяется числами: семь проверок в lfmcheck,
от «стык рядом — притянуло» до «выключено — не притягивает».
Внутри проект — обычный JSON, и система считала его просто текстом:
открывала редактором и рисовала значок текстового файла.
Теперь у него свой тип application/x-nlve-project, свой значок (лист с
загнутым углом и знаком программы) и открытие двойным щелчком. Тип
объявлен наследником application/json, но не простого текста — иначе
редактор снова оказался бы программой по умолчанию.
Всё, что нужно для встраивания в рабочий стол, лежит в packaging и
ставится скриптом tools/install-desktop.sh в домашний каталог, без прав
администратора. Раньше пункт меню и значки существовали только на одной
машине и нигде не были записаны.
Чёрное окно и полная тишина оказались следствием того, что цепочка
смешивания звуковых дорожек оставалась от прошлой загрузки. Она
ссылается на дорожки по номерам, а в новом файле таких номеров нет.
Проверено на живом mpv через канал управления: с оставшейся цепочкой
следующая загрузка даёт ноль дорожек, размер кадра недоступен, не играет
ничего. После снятия — кадр 320, звук выбран.
Снимать после загрузки поздно: ломается сама загрузка. Теперь снимается
перед ней.
Проверена вся последовательность: две звуковые дорожки, затем одна, затем
без внешнего звука. Кадр на месте во всех трёх случаях.
Правило «верхний побеждает» я перенёс на звук механически, и это была
ошибка: оно придумано для картинки, где и делает возможной сборку
копированием. Для звука оно бессмысленно — под голос нельзя было положить
фоновую музыку. И стоит смешивание ничего: звук всё равно перекодируется.
В сборке куски со звуком нескольких дорожек сводятся тем же путём, что
переходы: слоями, каждый со своей громкостью.
В предпросмотре сложнее: плеер играет одну звуковую дорожку за раз.
Поэтому каждая дорожка монтажа подаётся своим списком кусков, а смесь
собирается цепочкой фильтров плеера. Громкость клипов подставляется в ту
же цепочку — иначе предпросмотр врал бы про баланс.
Проверено: два тона на разных дорожках, второй с громкостью 50%. В
готовом ролике -21.2 и -27.1 dB, в предпросмотре -21.2 и -27.2. Совпало.
Найдена по дороге ошибка в собственном условии: я считал слои без
победителя и потому обнулял список, когда звучали ровно две дорожки.
Найдена настоящая причина обеих жалоб на звук. Свойство audio-files,
заданное перед загрузкой, не подключает внешний список кусков вовсе:
проверено на живом mpv через его канал управления — после установки
свойства дорожек по-прежнему две, обе не внешние. Играл звук, встроенный
в саму картинку.
Отсюда обе стороны: звук был слышен там, где на монтаже его нет (шёл из
видеофайла), и не был слышен там, где есть (в этом месте картинкой была
чёрная вставка, а она молчит).
Правильный способ — команда audio-add с признаком select после загрузки.
Проверено: дорожек становится четыре, внешняя звуковая выбрана,
внутренняя снята с выбора.
Если подключить не удалось, в журнал пишется предупреждение. Глушить звук
на этот случай нельзя — прошлая попытка так сделать оставила без звука
весь монтаж.
Ползунок на клипе картинки теперь задаёт непрозрачность, на звуковом —
громкость. Уплощение выдаёт для таких мест не одного победителя, а слои.
Слои появляются в трёх случаях: клип полупрозрачен и сквозь него виден
нижний; два клипа на одной дорожке наползли друг на друга и должны
перетечь один в другой; у клипа задано плавное появление или уход.
Перекрытие на разных дорожках переходом не считается — там по-прежнему
побеждает верхний. Иначе в проекте, где дорожки перекрываются постоянно,
каждое такое место пришлось бы пересобирать.
Картинка собирается фильтрами ffmpeg: постоянная доля через overlay,
переход через xfade, затухание через fade. Звук сводится прямо в
отсчётах — доля каждого слоя считается на каждый отсчёт, и длина
результата получается ровно такой, какая нужна.
Перед сборкой программа сообщает, сколько секунд из скольких придётся
пересобрать, и даёт отказаться.
Проверено замерами:
- красный с непрозрачностью 50% поверх синего дал (127, 0, 126);
- переход на одной дорожке: синий плавно перешёл в красный за секунду;
- звуковой переход: -24 dB, -27.5, -35, -51, тишина;
- появление за 2 секунды: пересобрано 2 секунды из 6, остальное
скопировано.
Найдено по дороге: участок с наложением склеивался с соседним куском и
терял слои — в условии склейки они не учитывались.
Настройка кадрирования переехала с файла на клип: два куска одного файла
бывает нужно кадрировать по-разному. У файла осталась такая же, но лишь
как заготовка — её берёт новый клип при добавлении. Проекты, записанные
раньше, читаются: клип без своей настройки берёт её у файла.
Открывается кнопкой в правом нижнем видимом углу клипа. Именно видимом:
у клипа длиной в час настоящий угол почти всегда за краем экрана — та же
задача, что уже решена для ручки громкости.
В окне слева появился кадр источника с рамкой, которую тянут мышью за
середину и за правый нижний угол. Рамка держит пропорции кадра проекта и
пересчитывается в увеличение и смещение; ползунки и рамка следуют друг за
другом.
Кусок пересобирается теперь не только когда файл не совпадает с проектом,
но и когда у клипа своё кадрирование.
Проверено сборкой: первый кусок без кадрирования скопирован, второй с
увеличением вдвое пересобран через scale=1280:720,crop=640:360:384:180.
154 пакета вошло, 154 вышло, декодирование без ошибок.
Заодно: правый щелчок по списку файлов теперь выделяет файл под курсором.
Без этого меню без выделения отключалось целиком и выглядело так, будто
его нет. Цвет формы звука сменён на пастельно-жёлтый.
Для каждого файла один раз строится список пиков: сто значений в секунду,
каждое есть наибольший модуль отсчёта за свои десять миллисекунд. Рисовать
по настоящим отсчётам нельзя — в секунде их 48 тысяч, а в клипе на экране
сотни пикселей.
Разбор идёт в отдельном потоке и запоминается в кэше проекта. Через
временный файл, а не по трубе: запуск внешних программ режет вывод на
строки, а тут поток байтов, где перевод строки — такой же отсчёт.
Shift со стрелками вверх и вниз растягивает форму по высоте. Звук бывает
тихим, и угадывать за человека нужное усиление неправильно. Меняется
только рисунок, значение сохраняется между запусками.
Проверено: файл «4 с тона, 4 с тишины, 4 с тона» даёт форму в первой и
последней трети и пустоту посередине. Полная амплитуда даёт 254 из 255.
Разбор записи на 892 с занимает 12.7 с — это чтение 6.3 ГБ с диска,
быстрее не выйдет. Первоначальный критерий «меньше двух секунд» в плане
был занижен и исправлен по замеру.
Заодно снят риск четвёртого этапа: mpv принимает edl:// как внешнюю
звуковую дорожку и берёт звук именно из неё. Проверено встречными
списками — картинке «тишина, потом тон», звуку «тон, потом тишина».
Куски видео вырезаются и склеиваются копированием потока: сборка ролика
занимает время копирования файла, а не время рендера. Композитинга нет
принципиально — при перекрытии побеждает клип с верхней дорожки целиком,
и картинкой, и звуком. Именно это ограничение и делает сборку копированием
возможной.
Что внутри:
- монтажный стол со своей отрисовкой: дорожки, масштаб, прилипание,
схлопывание, громкость на клипе;
- предпросмотр прямо с монтажа через libmpv и виртуальный список кусков,
без промежуточных файлов;
- сборка через ffmpeg с точными резами: перекодируется только огрызок от
реза до ближайшего опорного кадра, остальное копируется;
- разбор заголовка MP4 для поиска опорных кадров — в 500 раз быстрее
опроса ffprobe;
- перепаковка контейнеров, кадрирование и поворот, сведения о проекте;
- свои цвета всего окна: системные элементы под Windows цвет не берут,
поэтому кнопки, полоса меню, журнал и полосы прокрутки нарисованы сами;
- окно зависимостей: недостающие ffmpeg, ffprobe и libmpv скачиваются с
официальных источников;
- кросс-сборка под Windows и работа под Wine.
Первый коммит: до этого история не велась.