Отбивку или текстовый блок проще размножить копией, чем заводить заново через меню и настраивать оформление с нуля. Копируются выделенные клипы, вставляются по курсору. Связанные берутся вместе с парой — у картинки со звуком иначе уехала бы половина, — но связка у копий своя: копия и подлинник не должны ездить вместе. Взаимное расположение сохраняется, набор сдвигается к курсору. Дорожка выбирается сама: сначала та, откуда копировали, если это время на ней свободно; потом любая подходящая по виду; в крайнем случае заводится новая. Наложить копию на соседа по дорожке нельзя — это означало бы переход, которого никто не заказывал. В модели появился TClip.CopyAllFrom: полная копия, кроме места на монтаже. Проверка в lfmcheck перебирает перенесённые свойства и отдельно убеждается, что место и связка не переносятся.
58 KiB
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 |
| Разрезать точно (перекодирует стык) | Shift+S |
| Удалить выделенное | Delete |
| Выделить всё | Ctrl+A |
| Схлопнуть дыру под курсором | 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, который схлопывает дыру под курсором. То есть выключенная кнопка означает лишь «не двигать ничего само собой при перетаскивании».
Во время воспроизведения курсор держится посередине окна, а монтаж едет под ним. При нажатии на пуск, если курсор оказался за краем видимой части, вид подтягивается к нему — масштаб при этом не меняется.
На каждом клипе проходит линия громкости с ручкой посередине видимой его части: тянешь ручку вверх и вниз — меняется громкость, от тишины до двойной. Ручка стоит посередине именно видимой части, а не всего клипа: до середины часового клипа, уехавшей за край экрана, мышью не дотянуться.
Громкость, отличная от обычной, — единственное место, где звук перекодируется: изменить громкость, не тронув сам звук, нельзя. Видео при этом по-прежнему копируется. Проверено: клип с громкостью 30% дал в ролике уровень на 10.5 дБ ниже исходного при ожидаемых 10.46 дБ.
Полоски по краям клипа показывают тип реза: серая — притянут к опорному кадру, голубая — точный, стык будет перекодирован при сборке. Если край подвинуть руками и он не попадёт на опорный кадр, рез сам становится точным: раз человек выбрал конкретный кадр, притягивать его к соседнему неправильно.
В списке файлов слева показывается кадр из каждого файла, его параметры и длительность. Кадры делаются в фоне и запоминаются в кэше проекта.
На клипах видна форма звука. Считается она по заранее разобранным пикам — сто значений в секунду, каждое есть наибольший модуль отсчёта за свои десять миллисекунд. Рисовать по настоящим отсчётам нельзя: в секунде их 48 тысяч, а в клипе на экране сотни пикселей.
Разбор идёт в фоне и запоминается в кэше проекта. Для записи на 892 с это около 13 секунд — столько занимает чтение шести гигабайт с диска, и быстрее не выйдет; при следующем открытии проекта пики берутся готовыми.
Shift со стрелками вверх и вниз растягивает форму по высоте. Звук бывает тихим, и угадывать за человека, во сколько раз растянуть картинку, неправильно. Усиление меняет только рисунок, самого звука не касается, и запоминается между запусками.
Файл не совпадает с проектом
Проект имеет одно разрешение и один набор кодеков на всё — иначе куски не склеить копированием. Файл с другими параметрами можно добавить, но его куски скопировать не выйдет.
Пересобираются только они. Остальной материал по-прежнему копируется без потерь и почти мгновенно. Ролик целиком не перекодируется никогда.
Как именно кадр ложится в кадр проекта, решает окно «Кадрирование и поворот»: вписать целиком с полями, заполнить кадр с обрезкой или растянуть, плюс поворот на 90, отражение, увеличение и смещение.
Слева в окне — кадр источника с рамкой: её тянут мышью за середину и за правый нижний угол, и она держит пропорции кадра проекта. Справа — настоящий результат, пропущенный через ту же цепочку фильтров, что пойдёт в дело.
Настройка принадлежит клипу, а открывается кнопкой в правом нижнем видимом углу самого клипа. Именно видимом: у клипа длиной в час настоящий угол почти всегда за краем экрана. Два куска одного файла можно кадрировать по-разному.
У файла в библиотеке хранится такая же настройка, но она лишь заготовка: её берёт новый клип при добавлении. Правится она из контекстного меню библиотеки.
По умолчанию кадр вписывается целиком. Растяжение надо выбрать руками: раньше оно применялось молча, и вертикальное видео размазывалось по ширине.
«Приведение к параметрам проекта» делает ту же работу один раз при добавлении: файл перекодируется целиком, зато каждая последующая сборка остаётся быстрой.
Что и почему происходит с каждым файлом, видно в окне «Проект → Сведения о проекте»: параметры проекта, параметры каждого файла, выбранное вписывание и вердикт — копируется, пересобирается или из него делается клип. Там же кнопка полного ответа ffprobe.
Что показывает предпросмотр
Предпросмотр показывает кадр проекта, а не кадр исходника. Файл другого соотношения сторон виден с полями по краям — ровно так, как он попадёт в готовый ролик. Проверено сравнением: кадр предпросмотра и кадр сборки совпадают бит в бит.
Само по себе это не бесплатно, поэтому включается только когда есть что вписывать. Разное разрешение при одинаковом соотношении сторон (скажем, 3840x2160 в проекте 2688x1512) ничего не требует: mpv сам впишет такой файл в панель, и картинка совпадёт до пикселя.
Когда вписывание нужно, декодирование переключается на вариант с
копированием кадров в обычную память — цепочка фильтров не умеет работать
с кадрами, лежащими в памяти видеокарты. Аппаратное декодирование при этом
сохраняется, видно в журнале: nvdec без вписывания, nvdec-copy с ним.
Замер на материале 2688x1512 при 60 к/с: 327 кадров в секунду без фильтра,
230 с настоящим масштабированием — запас четырёхкратный.
Чего предпросмотр не умеет: показать собственную настройку кадрирования файла. Цепочка фильтров у mpv одна на всю склейку, а заполнение с обрезкой, поворот и смещение задаются для каждого файла отдельно — выбрать между ними общий фильтр не может, поэтому он всегда вписывает целиком. Точную картинку для такого файла показывает окно «Кадрирование и поворот».
Цвета окна
Все цвета заданы в 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. Громкость клипов подставляется в ту
же цепочку — иначе предпросмотр врал бы про баланс. Проверено: в
предпросмотре и в готовом ролике уровни совпадают до десятой доли
децибела.
Два клипа на одной дорожке, наползшие друг на друга, дают переход: один плавно перетекает в другой. На разных дорожках перекрытие означает не переход, а наложение — там по-прежнему побеждает верхний, если он непрозрачен.
Различие не косметическое. Если бы кроссфейдом считалось любое перекрытие, то в проекте, где клипы на разных дорожках перекрываются постоянно, каждое такое место пришлось бы пересобирать.
У клипа есть плавное появление и уход. Для картинки это затемнение, для звука — нарастание громкости.
Всё это стоит перекодирования — но только своего участка. Перед сборкой программа сообщает, сколько секунд из скольких придётся пересобрать, и даёт отказаться.
Звук при наложении сводится прямо в отсчётах, без фильтров ffmpeg: доля каждого слоя считается на каждый отсчёт, и длина результата получается ровно такой, какая нужна.
Текст режет кусок, а не пересобирает его целиком
Вжигание надписи не делает весь кусок пересобираемым. Кусок режется на части: до надписи, под надписью и после. Пересобирается только середина, остальное копируется. Надпись в тридцать секунд внутри сорокаминутного куска раньше стоила сорока минут пересборки — половины ролика.
Часть, которая начинается не с начала куска, помечается точным резом: копия пошла бы от ближайшего опорного кадра и сдвинула бы картинку. Сборка перекодирует огрызок до опорного кадра, а дальше снова копирует — доли секунды вместо минут.
Обрезки короче секунды не копируются, а дожигаются вместе с надписью: возни с опорными кадрами там больше, чем выгоды.
Качество пересобираемых кусков
Куски, которые нельзя скопировать, целятся в наибольший битрейт среди файлов проекта. Без этого кодировщик берёт своё значение по умолчанию, и оно скромное: на съёмке 2688x1512 с 56 Мбит/с NVENC выдавал 5.9 Мбит/с — пересобранный кусок бросался в глаза среди скопированных соседей.
Кроме среднего задаётся потолок в полтора раза выше и буфер вдвое: без них кодировщик разгоняется на сложных кадрах ровно настолько, насколько позволит средний, и мелкие детали всё равно теряются.
Берётся именно наибольший, а не средний: пересобранный кусок стоит рядом со скопированными, и если он окажется хуже их, разница видна именно на нём. Лишнее место дешевле.
Место на диске
Куски весят примерно столько же, сколько готовый ролик, а на длинном материале и больше: на часовой съёмке набралось 110 ГБ. Поэтому:
- картинка не собирается в промежуточный файл. Звук собирается раньше, и склейка сразу кладёт всё вместе в конечный файл. Раньше получалось два полных ролика на диске одновременно — 91 ГБ в кэше и 91 ГБ рядом — и лишний проход копирования ради одной звуковой дорожки;
- после удачной сборки куски удаляются, и в журнале сказано, сколько места освободилось. После неудачной — остаются: с них продолжают.
Оставить их можно настройкой keep_parts в файле настроек: пригодится,
когда разбираешься, из чего собрался ролик.
Дыры и вставки
Промежуток короче половины кадра проекта дырой не считается: между клипами, сдвинутыми встык мышью, остаются микросекунды, и заказывать под них чёрную вставку не нужно. Мышь при этом кладёт клипы на кадровую сетку — между кадрами времени не существует.
Готовые вставки делаются двух длин, 15 и 60 секунд. Дыра длиннее минуты набирается повторами одного и того же минутного файла: и список кусков предпросмотра, и список склейки повторы принимают. Раньше лестница длин доходила до восьми часов, и дыра в сорок минут требовала двухчасового чёрного файла — программа выглядела зависшей.
Чёрное кодируется отдельным скромным потоком в мегабит: целиться в битрейт съёмки там нечему.
Если правка монтажа сделала идущую генерацию ненужной, она прекращается: программа проверяет, нужна ли ещё та вставка, что делается сейчас.
Прерванная сборка
Каждый доведённый до конца кусок помечается в списке рядом с самими
кусками (готово.txt в кэше). Если сборка не дошла до конца — упала,
была отменена или программу закрыли, — при следующей сборке в тот же файл
программа предложит продолжить с готовых кусков.
Проверяется не только наличие файла: сборку могли прервать посреди записи, и файл оказался бы обрезанным. Поэтому куском считается только тот, который успел попасть в список.
Первая строка списка — отпечаток плана: пути, места и длины всех кусков. Не совпал — значит монтаж с тех пор менялся, и готовые куски не подойдут; программа скажет об этом и соберёт заново.
Сколько осталось
Окно сборки показывает, сколько прошло и сколько примерно осталось. Оценка простая: если четверть пути заняла минуту, весь путь займёт четыре. На деле скорость меняется — копируемые куски идут мгновенно, пересобираемые медленно, — поэтому число называется «примерно» и пересчитывается на каждом шаге. Даже такая оценка отвечает на главный вопрос: успею ли я сходить за чаем.
Как собирается ролик
- Монтаж уплощается в последовательность непересекающихся кусков. Перекрытия разрешаются в пользу верхней дорожки, дыры превращаются в чёрные вставки.
- Картинки и дыры материализуются в клипы в параметрах проекта. Иначе их нечем показать: подсунуть mpv картинку или генератор прямо в списке кусков нельзя, он на этом зависает. Заготовки делаются готовых длин (15 с, 60 с, 5 мин и так далее), а нужный кусок из них вырезается — тогда любое движение клипа не требует новой генерации.
- Начало каждого куска притягивается к опорному кадру, потому что копировать можно только с него. Конец притягивать не нужно — при копировании он и так получается кадроточным.
- При точном резе огрызок от точки реза до следующего опорного кадра перекодируется, остальное копируется.
- Куски склеиваются concat-демультиплексором, результат сверяется: сумма пакетов кусков обязана совпасть с числом пакетов в готовом файле.
Напрашивающийся способ — один проход с директивами 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.
Лишний кадр на стыке точного реза. При копировании -to работает как
«строго меньше», при перекодировании — как «меньше либо равно». Кадр на
границе попадал в ролик дважды: 601 вместо 600. Лечится отступом на
полкадра от границы.
B-кадры во вставках. Двунаправленные кадры от NVENC при переносе в
Matroska давали метки декодирования, идущие назад, и двадцать совпадающих
меток на шестьсот кадров. Коротким вставкам B-кадры не нужны, поэтому они
запрещены (-bf 0).
Вставка под каждую длину дыры. Сначала под каждую дыру делался чёрный клип ровно её длины. Любое движение клипа меняло длину дыры, требовало новой генерации, и до её готовности предпросмотр играл прежнюю расстановку. Выглядело так, будто выключение дорожки или удаление клипа не действуют. Лечится заготовками готовых длин, из которых вырезается нужный кусок.
Первая загрузка начинала воспроизведение. У незагруженного 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. Записанный по привычке код даёт цвет с переставленными
красным и синим.
Имена, занятые предками. Локальная переменная перекрывает одноимённое
свойство класса: Text и Position заняты у TControl, Finished — у
TThread, Name и Caption — у формы. Компилятор ловит это как
«Duplicate identifier», но причину не объясняет.
Консольный инструмент проверок
Часть вещей проверяется числами, а не глазами. Сборка: ./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 <проект> <выход>
Команды возвращают ненулевой код при расхождении, поэтому годятся для автоматической проверки.
Структура
| Файл | Что это |
|---|---|
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 |
Предупреждение о несовпадении параметров |
build.sh |
Сборка целями |
tests/ |
Консольный инструмент проверок |
vendor/mpv-headers/ |
Заголовки mpv 0.37.0 — эталон для биндингов |
Чего пока нет
Раздельные звуковые дорожки. Звук идёт с того же клипа, что и картинка. Громкость у клипа своя, но смешивать звук с нескольких дорожек нельзя: это потребовало бы второго прохода по всему материалу. Модель данных к разделению готова, само разделение не сделано.
Осциллограмма звука на клипах.
Переходы, эффекты, изменение скорости. Всё это требует перекодирования и в замысел не входит.