# nlve Нелинейный видеоредактор без перекодирования. Куски видео вырезаются и склеиваются копированием потока, поэтому сборка ролика занимает время копирования файла, а не время рендера. Композитинга нет принципиально: когда клипы перекрываются во времени, побеждает тот, что на верхней дорожке — целиком, и картинкой, и звуком. Именно это ограничение и делает возможной сборку копированием. ## Сборка ``` ./build.sh редактор под эту систему ./build.sh windows редактор под Windows (кросс-сборка) ./build.sh tool консольный инструмент проверок ./build.sh test инструмент плюс прогон проверок ./build.sh all всё перечисленное ./build.sh run собрать и запустить ./build.sh clean убрать промежуточные файлы ``` Используется тулчейн fpcupdeluxe, а не системные fpc и Lazarus: системный Lazarus другой версии, и собранные им модули LCL с нашими несовместимы. Другой корень задаётся переменной: `FPCUP=/путь ./build.sh`. То же самое вручную: ``` /home/chaiman/fpcupdeluxe/lazarus/lazbuild --primary-config-path=/home/chaiman/fpcupdeluxe/config_lazarus nlve.lpi /home/chaiman/fpcupdeluxe/lazarus/lazbuild --primary-config-path=/home/chaiman/fpcupdeluxe/config_lazarus --cpu=x86_64 --os=win64 --ws=win32 nlve.lpi ``` Требования во время работы: `libmpv.so.2` (пакет `mpv`), `ffmpeg` и `ffprobe` в `PATH`. Линковки с libav* нет — ffmpeg вызывается внешним процессом. ## Запуск ``` ./nlve # пустой проект ./nlve <проект.nlve> # открыть проект ./nlve <видео> <картинка> ... # добавить файлы в новый проект ``` Окно перепаковки открывается из меню «Файл». Здесь раньше значился запуск `./nlve remux <файл>...` — в коде этого нет и не было: слово `remux` не является именем файла и молча пропускалось. ### Встраивание в рабочий стол ``` tools/install-desktop.sh # берёт ./nlve рядом с репозиторием tools/install-desktop.sh /путь/к/nlve ``` Скрипт кладёт пункт меню, значки и описание типа файла — всё в домашний каталог, без прав администратора. После этого `*.nlve` перестают быть просто текстом: у них своё имя типа (`application/x-nlve-project`), свой значок и открытие двойным щелчком. Проверяется так: ``` xdg-mime query filetype проект.nlve # application/x-nlve-project xdg-mime query default application/x-nlve-project ``` Внутри проект — обычный JSON, и тип объявлен наследником `application/json`. Наследником простого текста делать не стали: тогда файл снова открывался бы редактором по умолчанию. Файловый менеджер может показать новый значок не сразу — ему нужно перечитать базу. ## Управление | Действие | Клавиши | |---|---| | Пуск и пауза | Пробел | | В начало / конец монтажа | Home / End | | Показать, где курсор (окно едет к нему) | \ | | Разрезать по курсору (все дорожки) | S | | Удалить выделенное | Delete | | Выделить всё | Ctrl+A | | Выделить областью | протяжка по пустому месту дорожек | | Режим выделения: рамкой или по времени | D | | Схлопнуть дыру под курсором | Ctrl+Shift+R | | Отменить, повторить | Ctrl+Z, Ctrl+Y | | Масштаб таймлайна, от точки под мышкой | Ctrl + колесо мыши | | Прокрутка по дорожкам | колесо мыши | | Прокрутка по времени | Shift + колесо, средняя кнопка | | Прокрутка журнала вбок | Shift + колесо над журналом | | Растянуть форму звука по высоте | Shift + стрелки вверх и вниз | | Выключить дорожку / оставить только её | кнопки М и С в заголовке | | Громкость клипа | тянуть ручку на клипе вверх-вниз | | Автосхлопывание вкл/выкл | Ctrl+K | | Копировать и вставить клипы | Ctrl+C, Ctrl+V | | Скопировать и применить оформление | Ctrl+Shift+C, Ctrl+Shift+V | | Связать или развязать клипы | Ctrl+G | | Отменить прилипание на время | Shift при перетаскивании | | Вписать проект целиком | Ctrl+0 | | Новый, открыть, сохранить проект | Ctrl+N, Ctrl+O, Ctrl+S | | Добавить файлы | Ctrl+I | | Собрать ролик | Ctrl+E | Файлы попадают в проект через меню «Файл» или перетаскиванием прямо в окно программы. Место броска решает, что произойдёт: бросили над монтажом — файл и добавится в проект, и ляжет на дорожку под указателем; в любом другом месте — только добавится в проект. Файл из библиотеки кладётся на монтаж перетаскиванием либо через контекстное меню списка. В нём: **добавить на дорожку** (подменю со списком дорожек), **кадрирование и поворот**, **убрать со всех дорожек** (файл остаётся в проекте), **удалить из проекта**. Дыры при этом намеренно не схлопываются: убрать клип и переставить всё остальное — два разных намерения. Если бросить клип ниже всех дорожек, заведётся новая. В заголовке каждой дорожки слева — её название, число клипов и две кнопки: **М** выключает дорожку целиком (она не идёт ни в предпросмотр, ни в сборку), **С** оставляет только её, заглушив остальные. Щелчок по заголовку делает дорожку выбранной — на неё кладёт файл кнопка. Клип переносится перетаскиванием, в том числе на другую дорожку и на пустое место под последней — тогда дорожка тоже добавится. За края клипа он растягивается и укорачивается. Картинку можно растянуть на любую длину, видео — только в пределах исходника. Если тащить к краю окна, монтаж едет следом сам: иначе при большом увеличении кусок упирается в край, и его приходится переносить в несколько приёмов — отпустить, прокрутить, взять снова. Файл из списка проекта встаёт **на курсор**, а не в конец дорожки. Если место занято, монтаж раздвигается: всё от курсора и правее едет вправо на длину нового клипа, на всех дорожках сразу — звук и надписи обязаны уехать вместе с картинкой. Строка состояния говорит, сколько клипов отодвинуто и на сколько. Щелчок по клипу переставляет курсор воспроизведения. Именно щелчок: если кнопку нажали и потянули, курсор остаётся на месте, в том числе во время воспроизведения. **Прилипание клипов** (кнопка с магнитом). Края клипов притягиваются друг к другу и к курсору. Shift во время перетаскивания меняет решение на противоположное: при включённом прилипании временно отключает, при выключенном — временно включает. Так одна клавиша полезна в обоих режимах, и не нужно помнить, какой сейчас. **Прилипание курсора** (кнопка с клеем). Курсор, поставленный щелчком, притягивается к границам клипов на любой дорожке. Без этого в стык не попасть: на общем плане одна точка экрана — это секунды материала, а ставят курсор чаще всего именно на стык. Shift при щелчке работает так же — меняет решение на противоположное. Переключателя два, а не один, потому что это разные привычки: клипы липнут при перетаскивании, курсор — при щелчке, и одно бывает нужно без другого. **Курсор посередине** (кнопка с целью). Включено — при воспроизведении монтаж едет под курсором, и всегда видно, что будет дальше. Выключено — окно не двигается вовсе, даже когда курсор ушёл за край: отмотал Shift с колесом — там и останешься. Раньше в выключенном состоянии окно всё равно подтягивалось к курсору, стоило тому выйти из видимой части, и прокрутка отскакивала назад со второго поворота колеса. **Ключ и прозрачность в предпросмотре** (меню «Вид»). Включено — окно показывает то же, что даст сборка: выбитый цвет и приглушение считаются на лету. Выключено — оверлей виден целиком, со своим фоном: так понятно, куда он встал и где его края. Считается это по площади самого оверлея, а не всего кадра. Замер на 2688x1512 (600 кадров, десять секунд материала): основа 2.07 с, с наложением 2.43 с, с ключом и прозрачностью 2.66 с. То есть наложение стоит 0.36 с, ключ добавляет ещё 0.23 с — вчетверо быстрее реального времени. На слабой машине это может быть заметнее, потому выключатель и есть. **Отмеченный участок.** У линейки два этажа. Верхний, в двенадцать точек, — про отметки: протяжка по нему отмечает участок, даже если отметок не было, а щелчок без движения их снимает. По краям участка стоят жёлтые уголки, за них тянут. Нижний этаж — про курсор, как и раньше. Клавиши тоже остались: `I` ставит начало по курсору, `O` — конец; оба пункта есть в меню «Правка», там же снятие отметок. Пока он отмечен, сборка собирает только его — это единственный способ проверить правку на длинном монтаже, не тратя на каждую проверку столько же времени, сколько идёт сам ролик. Отметки живут в файле проекта: участок относится к монтажу, а не к привычкам человека. **Дорожки.** Слева в заголовке две стрелки: поднять и опустить. У верхней дорожки некуда вверх, у нижней вниз — такая стрелка нарисована тускло и не нажимается. Заголовок можно и просто потащить мышью. Двойной щелчок по названию переименовывает дорожку: на монтаже из десятка дорожек «карта», «звук мотора» и «титры» читаются несравнимо лучше, чем V1, A2 и Т1. Правый щелчок по заголовку открывает меню: поднять, опустить, переименовать, удалить. Удаление дорожки с клипами спрашивает подтверждение и называет число клипов. **Копирование клипов.** Ctrl+C копирует выделенные клипы, Ctrl+V кладёт копии по курсору. Отбивку или текстовый блок так размножить проще, чем заводить заново и настраивать оформление с нуля. Связанные клипы копируются с парой: у картинки со звуком иначе уехала бы половина. Связка у копий своя — копия и подлинник не ездят вместе. Взаимное расположение сохраняется: несколько клипов встают тем же узором, сдвинутым к курсору. Дорожка выбирается сама: сначала та, откуда копировали, если это время на ней свободно; потом любая подходящая по виду; в крайнем случае заводится новая. Класть копию поверх соседа нельзя — на одной дорожке это означает переход, а его никто не заказывал. **Копирование свойств клипа.** Ctrl+Shift+C берёт с выделенного клипа всё оформление — кадрирование, поворот, увеличение, смещение, прозрачность, цветовой ключ, затухания, громкость, — Ctrl+Shift+V применяет их ко всем выделенным. Вид клипа и его текст при этом не переносятся: свойства копируют между разными клипами, и превращать картинку в надпись незачем. **Схлопывание.** Кнопка внизу показывает и режим, и состояние: утоплена — автосхлопывание включено, и тогда при перетаскивании клипа всё, что начинается не раньше него, едет следом. Стрелка рядом открывает выбор режима: только текущая дорожка или все. Ctrl+K включает и выключает. Режим действует и при выключенном автосхлопывании — по нему работает Ctrl+Shift+R, который схлопывает дыру под курсором. То есть выключенная кнопка означает лишь «не двигать ничего само собой при перетаскивании». Во время воспроизведения курсор держится посередине окна, а монтаж едет под ним. При нажатии на пуск, если курсор оказался за краем видимой части, вид подтягивается к нему — масштаб при этом не меняется. На каждом клипе проходит линия громкости с ручкой посередине видимой его части: тянешь ручку вверх и вниз — меняется громкость, от тишины до двойной. Ручка стоит посередине именно видимой части, а не всего клипа: до середины часового клипа, уехавшей за край экрана, мышью не дотянуться. **Затухания тянутся за ручки в верхних углах клипа.** Взять их можно и промахнувшись: зона захвата шире нарисованного квадратика и уходит внутрь клипа. Пока затухание нулевое, ручка стоит в самом углу и целиться есть куда; стоит её сдвинуть — она висит посреди клипа сама по себе, и попасть в девять точек почти нельзя. Над зоной захвата меняется курсор — по нему и видно, где взять (`tests/fadegrab.sh`). Громкость, отличная от обычной, — единственное место, где звук перекодируется: изменить громкость, не тронув сам звук, нельзя. Видео при этом по-прежнему копируется. Проверено: клип с громкостью 30% дал в ролике уровень на 10.5 дБ ниже исходного при ожидаемых 10.46 дБ. Полоски по краям клипа показывают тип реза: серая — притянут к опорному кадру, голубая — точный, стык будет перекодирован при сборке. Если край подвинуть руками и он не попадёт на опорный кадр, рез сам становится точным: раз человек выбрал конкретный кадр, притягивать его к соседнему неправильно. В списке файлов слева показывается кадр из каждого файла, его параметры и длительность. Кадры делаются в фоне и запоминаются в кэше проекта. На клипах видна форма звука. Считается она по заранее разобранным пикам — сто значений в секунду, каждое есть наибольший модуль отсчёта за свои десять миллисекунд. Рисовать по настоящим отсчётам нельзя: в секунде их 48 тысяч, а в клипе на экране сотни пикселей. Разбор идёт в фоне и запоминается в кэше проекта. Для записи на 892 с это около 13 секунд — столько занимает чтение шести гигабайт с диска, и быстрее не выйдет; при следующем открытии проекта пики берутся готовыми. **Shift со стрелками вверх и вниз растягивает форму по высоте.** Звук бывает тихим, и угадывать за человека, во сколько раз растянуть картинку, неправильно. Усиление меняет только рисунок, самого звука не касается, и запоминается между запусками. ## Сохранение и автосохранение **Сохранение видно.** После Ctrl+S внизу появляется «Проект сохранён: имя, время». Раньше об этом говорила только строчка в журнале, а журнал по умолчанию закрыт: нажал — и непонятно, дошло до файла или нет. **Запись не оставляет огрызков.** Проект пишется во временный файл рядом и подставляется на место настоящего, только когда записан целиком. Прежний файл при этом становится запасным — `имя.nlve.bak`. Падение или кончившееся место посреди записи больше не оставляют половину проекта. **Копия пишется сама.** Пока в проекте есть несохранённое, раз в полминуты рядом появляется `имя.nlve.autosave`. Это не сохранение: файл проекта не трогается, признак «изменён» остаётся. При открытии проекта программа смотрит, нет ли рядом копии новее его самого. Есть — значит прошлая работа до файла не дошла, и программа показывает оба времени и предлагает открыть копию. Отказ копию не удаляет: она лежит, пока проект не сохранят заново. После сохранения копия не нужна и убирается. ## Кнопки внизу Переключатели в нижней полосе — прилипание, прилипание курсора, курсор посередине, автосхлопывание с выбором режима, режим выделения, журнал. **Подсказка идёт в строку состояния, а не во всплывающее окно.** Всплывающее окно появлялось прямо поверх кнопки, о которой рассказывало, и перехватывало щелчок: нажать кнопку было нельзя, пока подсказка не уйдёт. Теперь наведение мыши пишет ту же фразу в строку состояния справа от кнопок — она ничего не закрывает. Щелчок по кнопке подсказку тут же переписывает: «Журнал виден» становится «Журнал спрятан». Строка состояния занимает всё свободное место справа и ужимается только на время, пока рядом показан ход работы. **Каждый щелчок считается.** Раньше второй щелчок подряд оконная библиотека отдавала как двойной, и до кнопки он не доходил: нажимаешь журнал три раза быстро — срабатывает один. Теперь двойных щелчков кнопкам не приходит вовсе, и любая очередь щелчков отрабатывается вся (`tests/lfmcheck`, раздел «Кнопки нижней панели»; `tests/buttonclicks.sh` проверяет то же самое мышью на невидимом экране). ## Два режима выделения Кнопка внизу (справа от схлопывания) и клавиша `D`. Режим показан значком: рамка — выделение клипов, промежуток со стрелкой — выделение по времени. **Рамкой по клипам.** Протяжка по пустому месту дорожек выделяет всё, что рамка задела. Обычное выделение: выделил, нажал Delete. **По времени.** Протяжка отмечает промежуток от и до — по всем дорожкам сразу, сколько бы их ни было. Промежуток виден пунктиром и после того, как кнопку отпустили: по нему режет `S`. Зачем второй режим: вырезать из фильма кусок целиком. Отметил промежуток, нажал `S` — все дорожки разрезаны в двух местах, по краям промежутка, и выделенной осталась ровно середина. Дальше Delete, и при включённом автосхлопывании дыра закрывается сама. Края промежутка кладутся на кадровую сетку: между кадрами времени нет. **Курсор идёт за резом.** После `S` он встаёт ровно на место реза (в режиме по времени — на начало промежутка), после удаления куска и схлопывания — на место стыка. Дальше с этим местом и работают: следующий рез, схлопывание дыры, просмотр перехода. Раньше курсор оставался там, где был, и в место реза приходилось попадать мышью заново. ## Починка проекта Меню «Проект» → **Проверить и починить проект...**. Показывает, что нашла, и чинит только по согласию; отменяется через Ctrl+Z. Что ищется: - **клипы мимо кадровой сетки.** Между кадрами времени нет, а в старых проектах времена записаны как попало: 2.457599 с при 59.94 к/с — это 147.31 кадра. Картинка всё равно выйдет целым числом кадров, звук наберётся с точностью до отсчёта, и предпросмотр со сборкой не смогут совпасть в точности. Починка двигает клип не больше чем на полкадра; на проекте из 82 клипов общая длина изменилась на 0.028 с, то есть на полтора кадра за два часа; - **клипы на дорожках чужого вида.** Такой клип нарисован на монтаже, но в готовый ролик не попадает. То же самое предлагается при открытии проекта, здесь — по требованию; - **дорожки с повторяющимися именами.** Для синхронности звука с картинкой чинить проект не нужно: рез правится при сборке, и старый проект соберётся сходящимся. Починка — про то, чтобы монтаж не хранил невозможных времён. ## Файл не совпадает с проектом Проект имеет одно разрешение и один набор кодеков на всё — иначе куски не склеить копированием. Файл с другими параметрами можно добавить, но его куски скопировать не выйдет. Пересобираются **только они**. Остальной материал по-прежнему копируется без потерь и почти мгновенно. Ролик целиком не перекодируется никогда. Как именно кадр ложится в кадр проекта, решает окно «Кадрирование и поворот»: вписать целиком с полями, заполнить кадр с обрезкой или растянуть, плюс поворот на 90, отражение, увеличение и смещение. Слева в окне — кадр источника с рамкой: её тянут мышью за середину и за правый нижний угол, и она держит пропорции кадра проекта. Справа — настоящий результат, пропущенный через ту же цепочку фильтров, что пойдёт в дело. **Настройка принадлежит клипу**, а открывается кнопкой в правом нижнем видимом углу самого клипа. Именно видимом: у клипа длиной в час настоящий угол почти всегда за краем экрана. Два куска одного файла можно кадрировать по-разному. У файла в библиотеке хранится такая же настройка, но она лишь заготовка: её берёт новый клип при добавлении. Правится она из контекстного меню библиотеки. По умолчанию кадр вписывается целиком. Растяжение надо выбрать руками: раньше оно применялось молча, и вертикальное видео размазывалось по ширине. «Приведение к параметрам проекта» делает ту же работу один раз при добавлении: файл перекодируется целиком, зато каждая последующая сборка остаётся быстрой. Что и почему происходит с каждым файлом, видно в окне «Проект → Сведения о проекте»: параметры проекта, параметры каждого файла, выбранное вписывание и вердикт — копируется, пересобирается или из него делается клип. Там же кнопка полного ответа ffprobe. ### Что показывает предпросмотр Предпросмотр показывает **кадр проекта**, а не кадр исходника. Файл другого соотношения сторон виден с полями по краям — ровно так, как он попадёт в готовый ролик. Проверено сравнением: кадр предпросмотра и кадр сборки совпадают бит в бит. Само по себе это не бесплатно, поэтому включается только когда есть что вписывать. Разное разрешение при одинаковом соотношении сторон (скажем, 3840x2160 в проекте 2688x1512) ничего не требует: mpv сам впишет такой файл в панель, и картинка совпадёт до пикселя. Когда вписывание нужно, декодирование переключается на вариант с копированием кадров в обычную память — цепочка фильтров не умеет работать с кадрами, лежащими в памяти видеокарты. Аппаратное декодирование при этом сохраняется, видно в журнале: `nvdec` без вписывания, `nvdec-copy` с ним. Замер на материале 2688x1512 при 60 к/с: 327 кадров в секунду без фильтра, 230 с настоящим масштабированием — запас четырёхкратный. **Кадрирование клипа окно показывает.** Цепочка фильтров у mpv одна на всю склейку, а кадрирование у каждого клипа своё, и включать его по времени в цепочке нечем: из фильтров ffmpeg по времени переключается только наложение. Зато у вывода есть своя обрезка, и делает её видеокарта даром. Курсор зашёл в клип со своим кадрированием — обрезка выставляется, вышел — снимается. Ни лишней цепочки, ни копирования кадров из памяти видеокарты. Рамку считает один и тот же код, что рисует её в окне «Кадрирование и поворот», — иначе окно обещало бы одно, а предпросмотр показывал другое (`TransformSourceRect` в `uTransform`, проверка в `tests/lfmcheck`). Чего это не покрывает: отражение (свойства для него у вывода нет), растяжение (там кадр жмётся неравномерно, рамкой не описать) и участки с наложением — там кадр уже сведён цепочкой, и обрезка резала бы сведённое целиком, вместе с нижним слоем. Для таких клипов точную картинку по-прежнему показывает окно «Кадрирование и поворот». ## Цвета окна Все цвета заданы в `uTheme` и применяются принудительно, а не берутся у системы. Причина простая: под Linux вид назначает тема GTK, под Windows — тема оформления, и на светлой теме тёмный таймлайн оказывается вставленным в белую рамку. Что для этого пришлось сделать: - **Свои кнопки** (`TThemedButton`). Системная кнопка под Windows свой цвет не отдаёт: LCL рисует её через службу оформления, и та кладёт светлую подложку поверх заданного цвета. Проверено по исходникам LCL — `TCustomSpeedButton.PaintBackground` всегда вызывает `ThemeServices.DrawElement`. Поведение обычной кнопки сохранено: `Default`, `Cancel`, `ModalResult`, работа по Enter и Esc. - **Своя полоса меню** из плоских кнопок со всплывающими списками. Системную полосу рисует Windows, и её фон нашим цветом не закрашивается: `SetMenuInfo` меняет подложку пунктов, но не самой полосы. Сочетания клавиш при этом не теряются — их ищет `TMenu.IsShortCut`, вызываемый из `TMainForm.FormShortCut`. Ценой стал переход по меню клавишей Alt: его больше нет. - **Своё окно сообщения** (`ThemedMessageDlg`) вместо `MessageDlg`. - **Отрисовка пунктов меню и строк выпадающих списков** своими руками. Чего сделать нельзя, и это надо знать: - полосы прокрутки рисует система, и цвет им задать нечем. Единственный способ получить их в своих цветах — не пользоваться системными вовсе. Так и сделано: журнал (`uLogView`) и таблицы (`uThemedScroll`) обходятся своими. Осталась системная полоса у списка файлов слева — там её нечем выключить, не переписав сам список; - рамка окна и её кнопки принадлежат оконному менеджеру; - под gtk2 пункты меню своими руками не рисуются — в LCL это прямо помечено как неподдерживаемое (`interfaces/gtk2/issues.xml`), там их цвет назначает тема GTK. Под Windows рисуются, там это и нужно. ## Зависимости и запуск под Wine Программе нужны `ffmpeg`, `ffprobe` и `libmpv`. Под Linux они ставятся пакетным менеджером. Под Windows ставить нечем, поэтому файлы кладутся рядом с программой, а меню «Файл → Зависимости...» умеет их скачать. Список и адреса лежат в `deps-windows.json` или `deps-linux.json` рядом с программой — имя своё для каждой системы. Общий файл приводил к тому, что запущенная под Wine программа записывала windows-список, а сборка под Linux потом искала по нему `.dll`. Где имя файла сборки меняется с каждой датой, адрес не задаётся: программа спрашивает последний выпуск у GitHub. Архивы `.7z` распаковываются внешним `7zr.exe` — он тоже есть в списке и качается одним файлом, поэтому замкнутого круга не возникает. Под Wine программа узнаёт себя по наличию `wine_get_version` в `ntdll` и настраивается сразу: - **вывод через OpenGL**, а не Direct3D 11: последний в Wine падает при создании контекста, ещё до первого кадра; - **аппаратное декодирование выключено**. Дело не в бесполезных попытках: для DXVA2 mpv создаёт устройство Direct3D 9, а Wine при его создании сбрасывает гамму экрана в единицу — настройку всей системы. Замерено: с `auto-safe` гамма 0.89 превращалась в 1.0 при каждом открытии проекта, с `no` остаётся нетронутой. На настоящей Windows обе настройки остаются обычными. Журнал дублируется в `nlve.log` рядом с программой. Нужно ровно для случая, когда программа падает до появления окна: иначе всё, что она о себе рассказала, пропадает вместе с окном. ## Наложение, переходы и прозрачность Ползунок на клипе картинки задаёт **непрозрачность**, на звуковом — громкость. Клип с непрозрачностью меньше единицы смешивается с тем, что под ним. **Звуковые дорожки смешиваются.** Правило «верхний побеждает» придумано для картинки — оно и делает возможной сборку копированием. Для звука оно бессмысленно: без смешивания нельзя положить фоновую музыку под голос. И стоит оно ничего, потому что звук всё равно перекодируется. Плеер играет одну звуковую дорожку за раз, поэтому в предпросмотре смесь собирается его же цепочкой фильтров: каждая дорожка монтажа подаётся своим списком кусков, дальше `amix`. Громкость клипов подставляется в ту же цепочку — иначе предпросмотр врал бы про баланс. Проверено: в предпросмотре и в готовом ролике уровни совпадают до десятой доли децибела. Два клипа **на одной дорожке**, наползшие друг на друга, дают переход: один плавно перетекает в другой. На разных дорожках перекрытие означает не переход, а наложение — там по-прежнему побеждает верхний, если он сплошной. Сплошным клип перестаёт быть по трём причинам: выбит цвет, задана прозрачность или **он не заполняет кадр**. Последнее — то же самое, сказанное иначе: уменьшенный или сдвинутый клип занимает часть кадра, а вокруг остаётся пустое место, которое сборка обкладывает чёрными полями. Поле непрозрачно, и нижнюю дорожку закрывает не сам клип, а его поля — поэтому такой клип кладётся поверх нижнего, а не вместо него. Уменьшают клип ровно затем, чтобы увидеть под ним другое. Класть не на что — остаются поля. Отсюда же берётся картинка в углу кадра и два видео рядом: достаточно уменьшить и сдвинуть клипы на разных дорожках. В месте наезда под указателем два клипа. Первый щелчок берёт верхний, повторный по тому же месту переводит выделение на следующий, после последнего снова на верхний; строка состояния говорит, сколько их там. Нажатие для перетаскивания берёт именно выделенный — иначе нижний клип нельзя было бы ни выделить, ни потащить, не убрав сперва верхний. Различие не косметическое. Если бы кроссфейдом считалось любое перекрытие, то в проекте, где клипы на разных дорожках перекрываются постоянно, каждое такое место пришлось бы пересобирать. Наезд не остаётся незамеченным: после правки строка состояния называет дорожку, величину перекрытия и сами клипы. Переход заказывают мышью, а промахнуться легко — на общем плане одна точка экрана это секунды, — и раньше он появлялся сюрпризом: человек искал, почему сборка опять не кончается. Меряется наезд той же мерой, что и в сборке: короче половины кадра — не наезд, и говорить о нём незачем. Косой крест наезда и это сообщение берут соседей из списка клипов дорожки, а он отсортирован по времени. Держать этот порядок обязан каждый, кто клипы двигает: перетаскивание ведёт за собой связку на звуковой дорожке и соседей при схлопывании, и все их дорожки пересортировываются (`SortMovedTracks`). Пока сортировалась только та дорожка, с которой тащили, монтаж рисовал крест через целый клип там, где ничего не накладывалось, и обещал переход, которого никто не заказывал. Сама сборка при этом не начинается с первым же движением. Список кусков пересобирается сразу, а ffmpeg запускается, когда монтаж постоял тихо полторы секунды (`GenerateQuietMs`) и клип никто не держит мышью. Иначе переход считался бы заново на каждое движение руки, а посчитанное отправлялось бы в корзину. У клипа есть плавное появление и уход. Для картинки это затемнение, для звука — нарастание громкости. **Всё это стоит перекодирования** — но только своего участка. Перед сборкой программа сообщает, сколько секунд из скольких придётся пересобрать, и даёт отказаться. Звук при наложении сводится прямо в отсчётах, без фильтров ffmpeg: доля каждого слоя считается на каждый отсчёт, и длина результата получается ровно такой, какая нужна. ## Текст режет кусок, а не пересобирает его целиком Вжигание надписи не делает весь кусок пересобираемым. Кусок режется на части: до надписи, под надписью и после. Пересобирается только середина, остальное копируется. Надпись в тридцать секунд внутри сорокаминутного куска раньше стоила сорока минут пересборки — половины ролика. Часть, которая начинается не с начала куска, помечается точным резом: копия пошла бы от ближайшего опорного кадра и сдвинула бы картинку. Сборка перекодирует огрызок до опорного кадра, а дальше снова копирует — доли секунды вместо минут. Обрезки короче секунды не копируются, а дожигаются вместе с надписью: возни с опорными кадрами там больше, чем выгоды. ## Качество пересобираемых кусков Куски, которые нельзя скопировать, целятся в наибольший битрейт среди файлов проекта. Без этого кодировщик берёт своё значение по умолчанию, и оно скромное: на съёмке 2688x1512 с 56 Мбит/с NVENC выдавал 5.9 Мбит/с — пересобранный кусок бросался в глаза среди скопированных соседей. Кроме среднего задаётся потолок в полтора раза выше и буфер вдвое: без них кодировщик разгоняется на сложных кадрах ровно настолько, насколько позволит средний, и мелкие детали всё равно теряются. Берётся именно наибольший, а не средний: пересобранный кусок стоит рядом со скопированными, и если он окажется хуже их, разница видна именно на нём. Лишнее место дешевле. ## Место на диске Куски весят примерно столько же, сколько готовый ролик, а на длинном материале и больше: на часовой съёмке набралось 110 ГБ. Поэтому: - **картинка не собирается в промежуточный файл.** Звук собирается раньше, и склейка сразу кладёт всё вместе в конечный файл. Раньше получалось два полных ролика на диске одновременно — 91 ГБ в кэше и 91 ГБ рядом — и лишний проход копирования ради одной звуковой дорожки; - **после удачной сборки куски удаляются,** и в журнале сказано, сколько места освободилось. После неудачной — остаются: с них продолжают. Оставить их можно настройкой `keep_parts` в файле настроек: пригодится, когда разбираешься, из чего собрался ролик. ## Дыры и вставки Промежуток короче половины кадра проекта дырой не считается: между клипами, сдвинутыми встык мышью, остаются микросекунды, и заказывать под них чёрную вставку не нужно. Мышь при этом кладёт клипы на кадровую сетку — между кадрами времени не существует. Готовые вставки делаются двух длин, 15 и 60 секунд. Дыра длиннее минуты набирается повторами одного и того же минутного файла: и список кусков предпросмотра, и список склейки повторы принимают. Раньше лестница длин доходила до восьми часов, и дыра в сорок минут требовала двухчасового чёрного файла — программа выглядела зависшей. Чёрное кодируется отдельным скромным потоком в мегабит: целиться в битрейт съёмки там нечему. Если правка монтажа сделала идущую генерацию ненужной, она прекращается: программа проверяет, нужна ли ещё та вставка, что делается сейчас. ## Прерванная сборка Если сборка не дошла до конца — упала, была отменена или программу закрыли, — при следующей сборке в тот же файл программа предложит продолжить. **При сборке потоком** продолжают с самого недописанного файла: он и есть уже собранная часть фильма, и в нём видно, сколько целых кадров успело записаться. Программа отступает назад до границы последнего целого куска, переносит эту часть в новый файл и досылает остаток. Собранное таким образом совпадает с собранным за один раз попиксельно. Дописать в конец существующего MKV нельзя — Matroska этого не даёт, — поэтому продолжение всегда пишется в новый файл, и на диске в этот момент лежат оба. Места нужно вдвое больше готового ролика. **MP4 продолжать нельзя вовсе:** оглавление пишется в конец, и оборванный файл не открывается — `moov atom not found`. Программа говорит об этом прямо и собирает заново. Если сборка длинная, лучше собрать в MKV, а потом переупаковать в MP4 окном ремукса: это копирование потоков, без перекодирования. Готовый файл до последнего цел: сборка идёт в файл рядом и занимает место готового, только когда дописана. Сорвавшаяся попытка не отбирает то, что уже было собрано, — из двух недописанных остаётся тот, где кадров больше. **При сборке через кэш кусков** продолжают иначе: каждый доведённый до конца кусок помечается в списке рядом с самими кусками (`готово.txt`). Проверяется не только наличие файла — сборку могли прервать посреди записи; куском считается только тот, который успел попасть в список. Первая строка списка — отпечаток плана: пути, места и длины всех кусков. Не совпал — значит монтаж с тех пор менялся, и готовые куски не подойдут; программа скажет об этом и соберёт заново. ## Сколько осталось Окно сборки показывает, сколько прошло и сколько примерно осталось. Оценка простая: если четверть пути заняла минуту, весь путь займёт четыре. На деле скорость меняется — копируемые куски идут мгновенно, пересобираемые медленно, — поэтому число называется «примерно» и пересчитывается на каждом шаге. Даже такая оценка отвечает на главный вопрос: успею ли я сходить за чаем. **Полоса и число — одно и то же число.** Показатель работы внизу главного окна подписан процентами, и это ровно та доля, которую он нарисовал. Раньше полоса ползла по догадке, а подпись показывала замер: человек видел полосу, заполненную на девять десятых, рядом с нулём процентов, и понять по такой паре было нельзя ничего. **Доля берётся у ffmpeg, а не угадывается.** Сборка участка с наложением или затуханием может идти минуту и дольше, и всё это время «кусок 1 из 1» не меняется. Поэтому ffmpeg спрашивают, докуда он дошёл (`-progress`), и доля считается по его ответу. Пока он не ответил ни разу, полоса трогается с места на пару процентов и там стоит: пустая полоса и есть правда о только что начатой работе. **Обе полосы рисует один и тот же код.** Полоса в окне сборки раньше была системной (`TProgressBar`), и под gtk2 она заполнялась ступенями — восемь кусков на всю ширину. Теперь и там, и внизу главного окна стоит своя `TThemedProgress`, которая заполняется сплошь. ## Как собирается ролик 1. Монтаж уплощается в последовательность непересекающихся кусков. Перекрытия разрешаются в пользу верхней дорожки, дыры превращаются в чёрные вставки. 2. Картинки и дыры материализуются в клипы в параметрах проекта. Иначе их нечем показать: подсунуть mpv картинку или генератор прямо в списке кусков нельзя, он на этом зависает. Заготовки делаются готовых длин (15 с, 60 с, 5 мин и так далее), а нужный кусок из них вырезается — тогда любое движение клипа не требует новой генерации. Своё кадрирование картинки — увеличение, смещение, поворот — запекается прямо в эту заготовку, той же цепочкой фильтров, что показывает окно «Кадрирование и поворот». Поэтому в кэше лежит столько заготовок, сколько разных кадрирований у этой картинки, и предпросмотр с роликом показывают одно и то же. Заготовка нужна не всегда: если картинка не заполняет кадр, а под ней что-то лежит, участок собирается наложением, и чёрным полям взяться неоткуда. Заготовка с полями остаётся для случая, когда класть не на что. Предпросмотр идёт тем же путём: участок с наложением сводится заранее и играется обычным файлом. Силами плеера рисуются только прозрачные клипы и клипы с ключом — их участки бывают длинными, и пересобирать минуты материала ради них дорого. Цепочка плеера работает всё время показа и считается процессором: на кадре 2688x1512 видео начинает отставать от звука, поэтому ради клипа, который просто не заполняет кадр, её не включают. Картинка, которую всё же рисует плеер, отдаётся ему заготовкой без полей, а не самим файлом: своих декодеров ему хватает не на всё (настоящий webp из сети дал «webp: invalid TIFF header in Exif data»), да и у неподвижного кадра нет длины в списке кусков. 3. Границы кусков кладутся на кадровую сетку проекта: между кадрами времени нет, а звук набирается с точностью до отсчёта, и доли кадра разводят их в стороны. 4. Рез всегда точный: огрызок от точки реза до ближайшего опорного кадра перекодируется, остальное копируется. Копировать можно только с опорного кадра, поэтому «резать где угодно и ничего не перекодировать» не бывает — но платить приходится долями секунды на рез. 5. Каждому куску заказывается точное число кадров (`-frames:v`), а не только время: при копировании ffmpeg отмеряет конец по времени декодирования, и у записи с B-кадрами кусок выходит длиннее запрошенного. 6. Куски идут в готовый файл потоком. Каждый режется своим ffmpeg прямо в MPEG-TS на стандартный вывод, все потоки подряд переливаются в один принимающий ffmpeg, а тот пишет готовый файл. Место куску в общем потоке задаёт `-output_ts_offset`, считаемое в кадрах от начала. На диск не ложится ничего, кроме результата и звуковой дорожки. 7. Результат сверяется дважды: сумма кадров кусков обязана совпасть с числом пакетов в готовом файле, а число кадров — с тем, что просил монтаж. Второе и есть проверка синхронности звука с картинкой; вердикт пишется в журнал сборки. Звук собирается раньше картинки: приёмник берёт его вторым входом. В кусках картинки звука нет — звуковая дорожка собирается отдельно и точнее, а лишний поток в куске MPEG-TS ещё и сбивает тайминг склейки: замерено 36 мс на стык. **Через кэш кусков** собирается то, что потоком не выходит: когда кодек проекта не переносится в MPEG-TS или когда весь ролик пересобирается под площадку одним кодировщиком. Тогда каждый кусок пишется в отдельный файл, а склейка идёт concat-демультиплексором с явно указанной длительностью каждого. Этот путь и был единственным раньше; оба дают побитово один и тот же ролик — сверено на материале с дрона, PSNR бесконечен на каждом кадре. Разница в цене: на отрывке в 17.5 с кэш весил 152 МБ против 421 КБ, а времени ушло 17.5 с против 14.2 с. Напрашивающийся способ — один проход с директивами `inpoint` и `outpoint`, без временных файлов — проверен и отвергнут: он давал 574 кадра вместо 600 и не пересчитывал метки времени. ## Ловушки, на которые уже наступили Все перечисленные ниже вещи найдены замерами, а не рассуждениями. **Локаль.** libmpv отказывается создаваться, если числовая часть локали процесса не `C`: `mpv_create` возвращает nil. В русской локали десятичный разделитель запятая, и разбор чисел внутри mpv ломается. Лечится вызовом `ForceCNumericLocale` до `mpv_create` (`uMpvLib`). **Панель без окна.** Под gtk2 панель LCL не имеет собственного окна X11, и запрос окна возвращает окно формы. Отдав такой идентификатор mpv, получаем видео поверх всего интерфейса. Поэтому под X11 создаётся собственное дочернее окно по границам панели (`EnsureVideoWindow` в `uMpvPlayer`). **Относительные пути в списке склейки.** Путь с префиксом `file:` внутри списка concat разбирается относительно текущего рабочего каталога, а не относительно самого списка. Все пути приводятся к абсолютным сразу. **Смешивание кусков от разных кодировщиков.** MP4 и Matroska хранят набор параметров кодека один на дорожку. Как только рядом оказываются скопированный кусок из камеры и созданная нами вставка, декодер применяет параметры первого ко всем: замерено 384 ошибки декодера HEVC при верном числе кадров. Лечится склейкой через промежуточный MPEG-TS, где параметры повторяются перед каждым опорным кадром — стало 0 ошибок. Способ выбирается автоматически: один источник и никаких точных резов — прямая склейка, иначе MPEG-TS. Резы теперь всегда точные, так что на деле это почти всегда MPEG-TS. **Лишний кадр на стыке точного реза.** При копировании `-to` работает как «строго меньше», при перекодировании — как «меньше либо равно». Кадр на границе попадал в ролик дважды: 601 вместо 600. Лечится отступом на полкадра от границы. **B-кадры во вставках.** Двунаправленные кадры от NVENC при переносе в Matroska давали метки декодирования, идущие назад, и двадцать совпадающих меток на шестьсот кадров. Коротким вставкам B-кадры не нужны, поэтому они запрещены (`-bf 0`). **Звук отставал от картинки — четыре причины сразу.** Разбор занял день, и каждая находка мерялась, а не угадывалась (`tests/syncprobe.sh`: исходник со вспышкой и щелчком на каждой секунде, монтаж с резами не на опорных кадрах, поиск обоих следов в готовом ролике). 1. *Притянутый рез.* Начало куска картинки сдвигалось на ближайший опорный кадр, а звук брался из непритянутой точки: в журналах пользователя средняя притяжка 0.24 с, худшая 0.99 с. Лечится точным резом всегда. 2. *Список правок MP4.* Разбор заголовка не учитывал блок `elst`, и метки опорных кадров уезжали на задержку показа — на пробе два кадра. Рез просил перекодировать до метки, которая опорным кадром не была. 3. *Конец куска при копировании.* `-to` отмеряет по времени декодирования, и у записи с B-кадрами кусок выходил длиннее запрошенного. Лечится явным числом кадров (`-frames:v`). 4. *Длительность куска в склейке.* Последний пакет скопированного куска показывается позже всех прочих, контейнер считает его в длительность, и склейка растягивала время — 0.05 с на кусок. Лечится директивой `duration` в списке склейки. Туда же: звук внутри кусков картинки сбивал выравнивание на 36 мс, теперь его там нет. Плюс «разогрев» кодировщика звука: сжатый AAC начинается со служебных отсчётов, MP4 помечает их пропускаемыми, Matroska такой пометки не хранит — звук ехал на 21.3 мс позже. Величина меряется пробным сжатием и вычитается. Итог замера на образце: было до 0.6 с и росло по ходу ролика, стало 0.02-0.04 с без накопления, при точности самого измерения 0.017 с. **Вставка под каждую длину дыры.** Сначала под каждую дыру делался чёрный клип ровно её длины. Любое движение клипа меняло длину дыры, требовало новой генерации, и до её готовности предпросмотр играл прежнюю расстановку. Выглядело так, будто выключение дорожки или удаление клипа не действуют. Лечится заготовками готовых длин, из которых вырезается нужный кусок. **Первая загрузка начинала воспроизведение.** У незагруженного mpv признак паузы снят, поэтому первый же список кусков ехал сам. Лечится опцией `pause=yes` при создании плеера. **Молчаливое растяжение кадра.** Приведение файла к проекту задавало размер ключом `-s`, а он растягивает кадр не считаясь с соотношением сторон. Вертикальное видео превращалось в размазанное по ширине. Теперь размер задаётся цепочкой фильтров, а вписывание выбирается осознанно. **Фильтр mpv и кадры в памяти видеокарты.** Цепочка фильтров ffmpeg работает с обычной памятью, а при аппаратном декодировании кадры остаются в памяти видеокарты. Граф фильтров тогда просто не собирается: `Impossible to convert between the formats supported by the filter 'mpv_src_in0'`. Лечится переключением на `hwdec=auto-copy`, пока фильтр нужен. Ловушка была не в этом, а в том, как я это проверял: из командной строки mpv сам выбрал `nvdec-copy`, всё работало, и ошибки не было видно. Она вылезла только на снимке окна работающего редактора. Проверять такое надо там, где оно живёт, а не в удобной обстановке. **Скрипт сборки соврал об успехе.** `PIPESTATUS` живёт ровно до следующей команды, а в разборе результата стояло двоеточие в ветке `then` — оно и затирало состояние конвейера. Сборка падала, скрипт докладывал «готово». Читать `PIPESTATUS` надо сразу и в переменную. **Модуль Windows конфликтует именами.** Он объявляет свои `TRect`, `Point` и `RegisterClass`, а имена в Паскале нечувствительны к регистру — обычные вызовы LCL начинают разбираться как системные, с невнятными ошибками вида «ожидалось `)`». Подключать его целиком не нужно: `SetMenuInfo` и `MENUINFO` объявлены в `uTheme` напрямую. **Знак «▼» в шрифте Windows.** Вместо стрелки выходил пустой квадрат. Треугольник рисуется сам (`TThemedButton.Arrow`). **Раскраска до построения меню.** `ApplyDarkTheme` вызывался в начале `FormCreate`, а меню строится в середине — и цветов не получало. Вызов должен идти последним. **Исключения вещественной арифметики.** Паскаль по умолчанию требует сообщать о каждой неточности вычислений. libmpv и ffmpeg внутри считают как им удобно и на это не рассчитывают: при создании плеера вылезало «Invalid floating point operation», и предпросмотр не запускался. Лечится `SetExceptionMask` после загрузки библиотеки. **Цепочку смешивания надо снимать до загрузки.** Она ссылается на дорожки по номерам, и оставшаяся от прошлого раза не даёт открыть новый файл вовсе: проверено на живом mpv — после такой загрузки дорожек ноль, кадра нет, не играет ничего. Выглядит это как чёрное окно и полная тишина, и на плеер не похоже совсем. Снимать её после загрузки поздно: ломается сама загрузка. **Внешняя звуковая дорожка не подключается свойством.** Задать `audio-files` перед загрузкой недостаточно: список дорожек после загрузки остаётся прежним, внешний файл не подключается вовсе, и играет звук, встроенный в саму картинку. Проверено на живом mpv через его канал управления: после установки свойства дорожек по-прежнему две, обе не внешние. Из-за этого звук был слышен там, где на монтаже его нет. Попытка защититься, отключая звук при ненайденной внешней дорожке, сделала хуже — звука не стало нигде. Правильный способ — команда `audio-add` с признаком `select` после загрузки: она добавляет дорожку, выбирает её и снимает выбор с внутренней. Проверено: дорожек становится четыре, внутренняя звуковая помечается невыбранной. **Аргументы loadfile.** В mpv 0.38 у команды появился четвёртый аргумент — номер позиции в списке, и настройки файла сдвинулись на пятое место. Прежний вызов стал ошибкой «The loadfile option must be an integer», и под Wine с новой библиотекой не грузилось ничего. Гадать по номеру версии не нужно: те же настройки задаются свойствами перед загрузкой, и называются они одинаково во всех версиях. **Модуль Windows конфликтует именами.** Он объявляет свои `TRect`, `Point`, `DeleteFile`, `GetTempFileName` и `RegisterClass`, а имена в Паскале нечувствительны к регистру — обычные вызовы LCL начинают разбираться как системные, с невнятными ошибками вида «ожидалось `)`». Подключать его целиком не нужно: `SetMenuInfo` и `MENUINFO` объявлены в `uTheme` напрямую. За сессию эта ловушка сработала четыре раза. **Порядок байт в TColor.** LCL хранит цвет как `$00BBGGRR`, а не как привычный RGB. Записанный по привычке код даёт цвет с переставленными красным и синим. **Последняя строка блока о ходе работы обнуляет место.** ffmpeg шлёт ход блоками, и заканчивается блок строкой `progress=continue`, в которой места нет — она сообщает только, что работа продолжается. Разбор честно оставляет переданное число как было, поэтому передавать туда надо уже известное место, а не ноль. С нулём доля обнуляется в конце каждого блока: показатель стоит на месте и дёргается вверх лишь тогда, когда опрос попадёт между строкой со временем и этой — «висит на пяти процентах, потом скачет до семидесяти девяти». Сам ffmpeg при этом отдаёт ход безупречно ровно: блок каждые полсекунды, замерено. **Разбор HEVC на процессоре — самое дорогое в сведении слоёв.** Не фильтры, как кажется: на десяти секундах 2688x1512 цепочка со сведением стоит 13.5 с, а без всяких фильтров — 12.7 с. Всё время уходит на разбор исходника. Ключ `-hwaccel cuda` перед входом убирает его почти целиком: затухание на семьдесят секунд собирается 18.3 с вместо 75.9 с, картинка бит в бит та же. Просить его стоит только там, где кодирует та же карта (выбран NVENC): иначе на машине без неё пришлось бы ловить отказ. **Список недостающего — не список нужного.** Недостающее у нас — это то, чего нет на диске. Но у куска, который делается прямо сейчас, файл уже есть: ffmpeg создаёт его в первую же секунду и пишет до конца. Проверка «нужна ли ещё эта работа» спрашивала по списку недостающего — и отвечала «не нужна» про то, что делается. Работа отменялась в разгаре и начиналась заново. На коротком затухании это проходило незаметно, на затухании в семьдесят секунд сборка не заканчивалась никогда: любая правка сбрасывала её. Теперь уплощение отдельно отдаёт список нужного, независимо от того, лежит файл на диске или ещё пишется (`tests/genrestart.sh`). **MousePos у колеса мыши — не экранный.** По описанию `DoMouseWheel` получает точку в координатах экрана, но под gtk2 в ней уже клиентские: `ScreenToClient` переводит их второй раз, и точка уезжает влево ровно на левый край элемента. У масштаба таймлайна это выглядело так, будто он берётся не от мыши: замерено на невидимом экране — мышь в 700, 1000 и 1300, опора в 260, 560 и 860, всегда на 440 левее. Лечится обращением к `Mouse.CursorPos`: он экранный в любой системе, и перевод остаётся один. **Имена, занятые предками.** Локальная переменная перекрывает одноимённое свойство класса: `Text` и `Position` заняты у `TControl`, `Finished` — у `TThread`, `Name` и `Caption` — у формы. Компилятор ловит это как «Duplicate identifier», но причину не объясняет. **Стык кусков в потоке выглядит как порча.** Каждый кусок — это отдельный поток MPEG-TS со своим счётчиком непрерывности, и на стыке приёмник пишет «Packet corrupt», по паре строк на кусок. Данные при этом целы, но журнал сборки забивается жалобами, из-за которых не найти настоящую ошибку. Лечится ключом `-mpegts_flags +initial_discontinuity` у отправителя: он помечает начало куска как разрыв, жалобы исчезают, а результат тот же — сверено по кадрам и по md5. **Приёмник двигает весь ролик.** `-avoid_negative_ts make_zero` у приёмника сдвигает поток так, чтобы самая ранняя метка декодирования стала нулём, — а у первого куска она уходит в минус на задержку кодировщика, и весь ролик встаёт на 21 мс позже. Само по себе это безобидно, но продолжение прерванной сборки несёт этот сдвиг в перенесённой части, и куски за ней, размещённые по счёту кадров от нуля, налезают на неё на кадр. Поэтому при переносе сдвиг снимается отрицательным `-output_ts_offset`, а приёмник накладывает его заново. **Сдвиг к нулю отменяет сдвиг по времени.** При сборке потоком место куску задаёт `-output_ts_offset`, но `-avoid_negative_ts make_zero` считается уже после него и двигает кусок обратно в ноль — ключ просто перестаёт действовать. Замерено: все шесть кусков встали в начало, картинка разошлась с эталоном (PSNR 18 дБ) при верном числе кадров и тех же пакетах. В потоке сдвиг к нулю выключен; отрицательные метки в самом начале подбирает приёмник, у него make_zero свой. **`-fflags +igndts` сминает кадры на стыках.** С ним ffmpeg выбрасывает пришедшие метки декодирования и считает свои, а материал с B-кадрами хранится не в том порядке, в каком показывается. На стыке появлялось по три кадра с одной меткой, картинка расходилась с эталоном. Без этого ключа метки идут шаг в шаг, и ни одной жалобы на непоследовательность. **Двойной щелчок съедает второй щелчок.** Пока у элемента есть `csDoubleClicks`, второй щелчок подряд приходит не как `Click`, а как `DblClick`, и обработчик кнопки его не видит: человек щёлкает три раза, срабатывает один. Лечится двумя строками в конструкторе: `ControlStyle := ControlStyle - [csDoubleClicks]` и `DblClick`, переопределённый в `Click`, — на случай, если двойной всё же дойдёт. **Всплывающая подсказка перехватывает щелчок.** Окно подсказки в gtk2 встаёт поверх кнопки, о которой рассказывает, и щелчок попадает в него, а не в кнопку. У кнопок в тесной полосе `ShowHint` выключен, а текст подсказки идёт в строку состояния (`UseStatusHints` в `uMainForm`). ## Консольный инструмент проверок Часть вещей проверяется числами, а не глазами. Сборка: `./build.sh tool`. ``` ./tests/nlvetool kf <файл> индекс опорных кадров ./tests/nlvetool kfcheck <файл> сверка быстрого пути с ffprobe ./tests/nlvetool flattest проверка уплощения монтажа ./tests/nlvetool edl <файл> <старт> <длина> [...] ./tests/nlvetool export <файл> <выход> <старт> <длина> [...] ./tests/nlvetool export --cancel-after=<сек> ... проверка остановки ./tests/nlvetool remux <файл>... <расширение> ./tests/nlvetool build <проект> <видео> [<картинка>] ./tests/nlvetool buildcut <проект> <видео> ./tests/nlvetool render <проект> <выход> ./tests/nlvetool syncproj <проект> <видео> монтаж для замера синхронности ``` Команды возвращают ненулевой код при расхождении, поэтому годятся для автоматической проверки. Отдельно — замер синхронности звука и картинки: ``` ./tests/syncprobe.sh весь прогон: образец, сборка, замер ./tests/syncprobe.sh measure ФАЙЛ замер готового файла ``` Как он устроен. Делается исходник, где ровно на каждой секунде в кадре белая вспышка, а в звуке щелчок. Из него собирается монтаж, у которого начала кусков заведомо не попадают на опорные кадры. В готовом ролике ищутся времена вспышек и щелчков: если они разошлись — звук уехал от картинки, и видно, на сколько и с какого куска. Времена щелчков считаются по самим отсчётам (`tests/clicktimes.py`), а не оконным анализом: окно двигается кратно своему размеру, и его собственный уход выглядит как накапливающийся рассинхрон, которого нет. На это уже попались один раз. Ещё одиннадцать проверок гоняют саму программу на невидимом экране (`Xvfb :99`). Рабочий экран они не занимают: ``` ./tests/buttonclicks.sh <окно> каждый щелчок по кнопке считается ./tests/zoomanchor.py <окно> масштаб берётся от точки под мышкой ./tests/genrestart.sh [<путь к nlve>] сборка вставки не сбрасывается зря ./tests/fadegrab.sh [<путь к nlve>] ручку затухания видно и во второй раз ./tests/insertat.sh [<путь к nlve>] файл из списка встаёт на курсор ./tests/dragscroll.sh [<путь к nlve>] монтаж едет следом за клипом у кромки ./tests/genquiet.sh [<путь к nlve>] тяжёлая сборка ждёт конца правки ./tests/dropgap.sh [<путь к nlve>] клип садится в дыру без наезда ./tests/dragorder.sh [<путь к nlve>] связка переехала — порядок цел ./tests/pickcycle.sh [<путь к nlve>] щелчок перебирает наехавшие клипы ./tests/cropdialog.sh [<путь к nlve>] окно кадрирования показывает кадр ``` Отдельно от них `tests/imagecrop.sh` — сквозная, без окна: делает картинку, кладёт её клипом с уменьшением и смещением, собирает секунду ролика и смотрит на точки готового кадра. Случая два: класть не на что — вокруг картинки чёрные поля; под картинкой видео — вокруг неё видно его, а не поля. Ей нужны `tests/nlvetool` (`./build.sh test`), ffmpeg и python с PIL. `cropdialog.sh` строит свой монтаж сам, но ему нужна настоящая съёмка: кадр для окна берётся из файла. Путь задаётся переменной `NLVE_CROP_SOURCE`. Остальным нужен монтаж в `test/поток/`: `длинный-фейд.nlve` — один клип с затуханием секунд на семьдесят (такой кусок сводится около полутора минут, и всё это время человек продолжает работать), `пара.nlve` — два клипа без затуханий. Путь к первому задаётся переменной `NLVE_FADE_PROJECT`. В репозиторий они не входят, потому что ссылаются на съёмку, которой здесь нет. `genrestart.sh` съёмку действительно открывает и, не найдя её, честно говорит об этом и отказывается считать себя пройденной; остальным хватает самого монтажа. Экран поднимается через `xvfb-run`, и у него свой файл доступа. Проверки находят его сами по строке запуска `Xvfb`, так что отдельно задавать `XAUTHORITY` не нужно; чужой файл из окружения они не берут — с ним xdotool не видит ни одного окна и проверка «проходит», ничего не проверив. `zoomanchor.py` целится мышью в точку 1300, поэтому ему нужен экран шире: `Xvfb :98 -screen 0 1600x1000x24` и `ЭКРАН=:98`. Каждую из них стоит прогонять и на прежней сборке: проверка, которая проходит везде, не проверяет ничего. Все одиннадцать на сборке до своей правки падают. Замерено: клип у кромки уезжает на 0.7 с против 4.2 с, файл из списка падает в конец дорожки вместо курсора, сборка начинается через 0.2 с вместо 1.6 с, клип в дыру садится с наездом 0.007383 с, дорожка картинки после переноса связки остаётся в порядке 0, 14, 7.991 — не по времени, второй щелчок по наезду тащит верхний клип вместо нижнего, а картинка с уменьшением 50% выходит в ролик во весь кадр. ## Номер сборки В заголовке окна стоит номер: `nlve 42 — фильм.nlve`. Он растёт на единицу при каждой сборке редактора (`build.sh`), и по нему сразу видно, та ли это сборка, где нужная правка уже есть. Без него проверяют старым двоичным файлом, видят прежнее поведение, и время уходит на выяснение, у кого что не работает. Счётчик лежит в `build.number`, из него создаётся `src/uBuildInfo.inc`. Оба файла в репозиторий не входят: номер про эту машину, а не про историю проекта. ## Структура | Файл | Что это | |---|---| | `src/uAppTypes.pas` | Базовые типы, форматирование чисел без учёта локали | | `src/uSettings.pas` | Настройки между запусками (JSON) | | `src/uProcRunner.pas` | Запуск ffmpeg и ffprobe, разбор вывода, остановка | | `src/uFfmpegCmd.pas` | Сборка аргументов ffmpeg, разбор его прогресса | | `src/uMediaProbe.pas` | Что за файл нам дали и чем он отличается от проекта | | `src/uKeyframeIndex.pas` | Где опорные кадры: разбор заголовка MP4 и запасной путь | | `src/uProject.pas` | Модель монтажа, JSON проекта, отмена действий | | `src/uFlatten.pas` | Монтаж в плоский список кусков | | `src/uGenerate.pas` | Клипы для картинок и дыр | | `src/uExport.pas` | Вырезание, склейка, сверка числа кадров | | `src/uRemuxQueue.pas` | Очередь перепаковки | | `src/uEdl.pas` | Сборка `edl://` для предпросмотра | | `src/uMpvLib.pas` | Объявления libmpv, динамическая загрузка | | `src/uMpvPlayer.pas` | Плеер: встраивание в окно, воспроизведение, события | | `src/uTimelineCtrl.pas` | Компонент монтажного стола | | `src/uTransform.pas` | Как кадр исходника ложится в кадр проекта | | `src/uPanCropForm.pas` | Окно кадрирования и поворота | | `src/uProjectInfoForm.pas` | Сведения о проекте и ответ ffprobe | | `src/uThumbs.pas` | Кадры-миниатюры для списка файлов | | `src/uWaveform.pas` | Пики звука для формы волны на клипах | | `src/uTheme.pas` | Цвета окна, своя кнопка, своё окно сообщения | | `src/uLogView.pas` | Журнал: своя отрисовка, свои полосы прокрутки | | `src/uThemedScroll.pas` | Полоса прокрутки и обвязка для таблиц | | `src/uDeps.pas` | Что нужно программе, где искать, как ставить | | `src/uDownload.pas` | Загрузка по сети: WinHTTP под Windows, curl под Linux | | `src/uDepsForm.pas` | Окно «Зависимости» | | `src/uMainForm.pas` | Главное окно | | `src/uProgressForm.pas` | Ход долгой операции | | `src/uRemuxForm.pas` | Окно перепаковки | | `src/uMismatchForm.pas` | Предупреждение о несовпадении параметров | | `src/uTextForm.pas` | Надпись на клипе: текст и оформление | | `src/uTextMeasure.pas` | Точный замер ширины текста для подбора размера | | `src/uSubtitles.pas` | Текст поверх картинки: разметка для libass | | `src/uColorKeyForm.pas` | Выбор цвета, который станет прозрачным | | `src/uEncodeForm.pas` | Настройки сборки с перекодированием | | `src/uExportForm.pas` | Куда собирать, в каком контейнере, что с текстом | | `src/uAboutForm.pas` | О программе: возможности и чем она обязана чужому | | `src/uArticleView.pas` | Страница текста со ссылками | | `src/uFfmpegJob.pas` | Один запуск ffmpeg в потоке, с ходом работы и отменой | | `src/uIconData.pas` | Значки, вшитые в программу | | `build.sh` | Сборка целями | | `tests/` | Консольный инструмент проверок | | `vendor/mpv-headers/` | Заголовки mpv 0.37.0 — эталон для биндингов | ## Чего пока нет **Раздельные звуковые дорожки.** Звук идёт с того же клипа, что и картинка. Громкость у клипа своя, но смешивать звук с нескольких дорожек нельзя: это потребовало бы второго прохода по всему материалу. Модель данных к разделению готова, само разделение не сделано. **Осциллограмма звука на клипах.** **Переходы, эффекты, изменение скорости.** Всё это требует перекодирования и в замысел не входит.