Нелинейный видеоредактор без перекодирования
Куски видео вырезаются и склеиваются копированием потока: сборка ролика
занимает время копирования файла, а не время рендера. Композитинга нет
принципиально — при перекрытии побеждает клип с верхней дорожки целиком,
и картинкой, и звуком. Именно это ограничение и делает сборку копированием
возможной.
Что внутри:
- монтажный стол со своей отрисовкой: дорожки, масштаб, прилипание,
схлопывание, громкость на клипе;
- предпросмотр прямо с монтажа через libmpv и виртуальный список кусков,
без промежуточных файлов;
- сборка через ffmpeg с точными резами: перекодируется только огрызок от
реза до ближайшего опорного кадра, остальное копируется;
- разбор заголовка MP4 для поиска опорных кадров — в 500 раз быстрее
опроса ffprobe;
- перепаковка контейнеров, кадрирование и поворот, сведения о проекте;
- свои цвета всего окна: системные элементы под Windows цвет не берут,
поэтому кнопки, полоса меню, журнал и полосы прокрутки нарисованы сами;
- окно зависимостей: недостающие ffmpeg, ffprobe и libmpv скачиваются с
официальных источников;
- кросс-сборка под Windows и работа под Wine.
Первый коммит: до этого история не велась.
2026-08-19 18:36:16 +03:00
|
|
|
|
{
|
|
|
|
|
|
uTransform — как вписать кадр исходника в кадр проекта.
|
|
|
|
|
|
|
|
|
|
|
|
Зачем это нужно. Проект имеет одно разрешение на всё — иначе куски не
|
|
|
|
|
|
склеить копированием. Файл с другим разрешением или другим соотношением
|
|
|
|
|
|
сторон надо во что-то превратить, и вариантов ровно три: вписать целиком
|
|
|
|
|
|
с полями, заполнить кадр с обрезкой краёв, растянуть с искажением
|
|
|
|
|
|
пропорций. Раньше молча делалось третье, и вертикальное видео
|
|
|
|
|
|
превращалось в размазанное по ширине — это была ошибка.
|
|
|
|
|
|
|
|
|
|
|
|
Все размеры считаются числами здесь, а не выражениями внутри фильтра
|
|
|
|
|
|
ffmpeg. Выражения там пишутся через экранированные запятые и читаются
|
|
|
|
|
|
тяжело, а проверить посчитанные числа можно глазами прямо в журнале.
|
|
|
|
|
|
|
|
|
|
|
|
Преобразование хранится у файла, а не у клипа: оно описывает, как этот
|
|
|
|
|
|
материал вообще ложится в проект, и одинаково для всех его кусков.
|
|
|
|
|
|
}
|
|
|
|
|
|
unit uTransform;
|
|
|
|
|
|
|
|
|
|
|
|
{$mode objfpc}{$H+}
|
|
|
|
|
|
|
|
|
|
|
|
interface
|
|
|
|
|
|
|
|
|
|
|
|
uses
|
|
|
|
|
|
Classes, SysUtils;
|
|
|
|
|
|
|
|
|
|
|
|
type
|
|
|
|
|
|
{ Как соотносятся кадр исходника и кадр проекта. }
|
|
|
|
|
|
TFitMode = (
|
|
|
|
|
|
fmFit, // вписать целиком, по краям поля
|
|
|
|
|
|
fmFill, // заполнить кадр, лишнее обрезать
|
|
|
|
|
|
fmStretch // растянуть, пропорции исказятся
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
TMediaTransform = record
|
|
|
|
|
|
Fit: TFitMode;
|
|
|
|
|
|
{ Поворот по часовой стрелке: 0, 90, 180 или 270. }
|
|
|
|
|
|
Rotation: Integer;
|
|
|
|
|
|
FlipH: Boolean;
|
|
|
|
|
|
FlipV: Boolean;
|
|
|
|
|
|
{ Дополнительное увеличение поверх выбранного режима. Единица —
|
|
|
|
|
|
без увеличения. }
|
|
|
|
|
|
Zoom: Double;
|
|
|
|
|
|
{ Смещение кадра в долях от размера кадра проекта. Ноль — по центру. }
|
|
|
|
|
|
OffsetX: Double;
|
|
|
|
|
|
OffsetY: Double;
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
|
|
function DefaultTransform: TMediaTransform;
|
|
|
|
|
|
|
|
|
|
|
|
{ Нужно ли вообще что-то делать: совпадающий по размеру кадр без поворота
|
|
|
|
|
|
и смещений трогать незачем. }
|
|
|
|
|
|
function TransformIsIdentity(const AT: TMediaTransform;
|
|
|
|
|
|
ASrcWidth, ASrcHeight, ADstWidth, ADstHeight: Integer): Boolean;
|
|
|
|
|
|
|
|
|
|
|
|
{ Цепочка фильтров ffmpeg. Пустая строка означает «ничего делать не надо». }
|
|
|
|
|
|
function TransformFilter(const AT: TMediaTransform;
|
|
|
|
|
|
ASrcWidth, ASrcHeight, ADstWidth, ADstHeight: Integer): string;
|
|
|
|
|
|
|
|
|
|
|
|
{ Цепочка для предпросмотра: вписать любой кадр в кадр проекта.
|
|
|
|
|
|
|
|
|
|
|
|
От TransformFilter отличается двумя вещами. Во-первых, размеры исходника
|
|
|
|
|
|
здесь неизвестны — цепочка одна на весь предпросмотр, а куски в нём из
|
|
|
|
|
|
разных файлов, поэтому вместо посчитанных чисел идут выражения самого
|
|
|
|
|
|
ffmpeg. Во-вторых, она знает только вписывание целиком: заполнение с
|
|
|
|
|
|
обрезкой, поворот и смещение задаются для каждого файла отдельно, а
|
|
|
|
|
|
фильтр общий, и выбрать между ними он не может.
|
|
|
|
|
|
|
|
|
|
|
|
Отсюда честное ограничение: предпросмотр показывает кадр проекта верно,
|
|
|
|
|
|
но собственную настройку кадрирования файла не учитывает. Её видно в
|
|
|
|
|
|
окне кадрирования и в готовом ролике. }
|
|
|
|
|
|
function PreviewFitFilter(ADstWidth, ADstHeight: Integer): string;
|
|
|
|
|
|
|
|
|
|
|
|
function FitModeToStr(AMode: TFitMode): string;
|
|
|
|
|
|
function StrToFitMode(const AValue: string): TFitMode;
|
|
|
|
|
|
function FitModeCaption(AMode: TFitMode): string;
|
|
|
|
|
|
|
|
|
|
|
|
implementation
|
|
|
|
|
|
|
|
|
|
|
|
uses
|
|
|
|
|
|
Math, uAppTypes;
|
|
|
|
|
|
|
|
|
|
|
|
function DefaultTransform: TMediaTransform;
|
|
|
|
|
|
begin
|
|
|
|
|
|
Result.Fit := fmFit;
|
|
|
|
|
|
Result.Rotation := 0;
|
|
|
|
|
|
Result.FlipH := False;
|
|
|
|
|
|
Result.FlipV := False;
|
|
|
|
|
|
Result.Zoom := 1;
|
|
|
|
|
|
Result.OffsetX := 0;
|
|
|
|
|
|
Result.OffsetY := 0;
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
|
|
function FitModeToStr(AMode: TFitMode): string;
|
|
|
|
|
|
begin
|
|
|
|
|
|
case AMode of
|
|
|
|
|
|
fmFill: Result := 'fill';
|
|
|
|
|
|
fmStretch: Result := 'stretch';
|
|
|
|
|
|
else
|
|
|
|
|
|
Result := 'fit';
|
|
|
|
|
|
end;
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
|
|
function StrToFitMode(const AValue: string): TFitMode;
|
|
|
|
|
|
begin
|
|
|
|
|
|
if AValue = 'fill' then
|
|
|
|
|
|
Result := fmFill
|
|
|
|
|
|
else if AValue = 'stretch' then
|
|
|
|
|
|
Result := fmStretch
|
|
|
|
|
|
else
|
|
|
|
|
|
Result := fmFit;
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
|
|
function FitModeCaption(AMode: TFitMode): string;
|
|
|
|
|
|
begin
|
|
|
|
|
|
case AMode of
|
|
|
|
|
|
fmFill: Result := 'Заполнить кадр (лишнее обрезается)';
|
|
|
|
|
|
fmStretch: Result := 'Растянуть (пропорции исказятся)';
|
|
|
|
|
|
else
|
|
|
|
|
|
Result := 'Вписать целиком (по краям поля)';
|
|
|
|
|
|
end;
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
|
|
function TransformIsIdentity(const AT: TMediaTransform;
|
|
|
|
|
|
ASrcWidth, ASrcHeight, ADstWidth, ADstHeight: Integer): Boolean;
|
|
|
|
|
|
begin
|
|
|
|
|
|
Result := (AT.Rotation mod 360 = 0) and (not AT.FlipH) and (not AT.FlipV) and
|
|
|
|
|
|
(Abs(AT.Zoom - 1) < 0.001) and (Abs(AT.OffsetX) < 0.001) and
|
|
|
|
|
|
(Abs(AT.OffsetY) < 0.001) and
|
|
|
|
|
|
(ASrcWidth = ADstWidth) and (ASrcHeight = ADstHeight);
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
|
|
function PreviewFitFilter(ADstWidth, ADstHeight: Integer): string;
|
|
|
|
|
|
var
|
|
|
|
|
|
W, H: string;
|
|
|
|
|
|
begin
|
|
|
|
|
|
Result := '';
|
|
|
|
|
|
if (ADstWidth < 2) or (ADstHeight < 2) then
|
|
|
|
|
|
Exit;
|
|
|
|
|
|
W := IntToStr(ADstWidth);
|
|
|
|
|
|
H := IntToStr(ADstHeight);
|
|
|
|
|
|
|
|
|
|
|
|
{ force_original_aspect_ratio=decrease вписывает кадр целиком, не искажая
|
|
|
|
|
|
пропорции; pad добавляет поля до размера проекта; setsar=1 убирает
|
|
|
|
|
|
неквадратный пиксель, иначе mpv растянет уже готовый кадр обратно. }
|
|
|
|
|
|
Result :=
|
|
|
|
|
|
'scale=' + W + ':' + H + ':force_original_aspect_ratio=decrease,' +
|
|
|
|
|
|
'pad=' + W + ':' + H + ':(ow-iw)/2:(oh-ih)/2,' +
|
|
|
|
|
|
'setsar=1';
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
|
|
{ Кодировщики требуют чётных сторон: цветность в yuv420p хранится вдвое
|
|
|
|
|
|
прореженной, и нечётный размер ей некуда деть. }
|
|
|
|
|
|
function MakeEven(AValue: Integer): Integer;
|
|
|
|
|
|
begin
|
|
|
|
|
|
Result := AValue;
|
|
|
|
|
|
if Odd(Result) then
|
|
|
|
|
|
Inc(Result);
|
|
|
|
|
|
if Result < 2 then
|
|
|
|
|
|
Result := 2;
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
|
|
function TransformFilter(const AT: TMediaTransform;
|
|
|
|
|
|
ASrcWidth, ASrcHeight, ADstWidth, ADstHeight: Integer): string;
|
|
|
|
|
|
var
|
|
|
|
|
|
Chain: string;
|
|
|
|
|
|
K, KFit, KFill: Double;
|
|
|
|
|
|
SW, SH: Integer;
|
|
|
|
|
|
CW, CH, CX, CY: Integer;
|
Кадрирование: поворот после обрезки, рамка не расходится с настройкой
Поворот и отражение применялись к исходному кадру, а обрезка шла после
них. Из-за этого рамка показывала одно место, а вырезалось другое.
Теперь наоборот: сначала выбирается кусок, потом он поворачивается и
отражается. Если после поворота кусок лёг поперёк кадра проекта, он
вписывается целиком с полями.
Проверено: с отражением левый и правый края вырезанного куска меняются
местами, то есть отражается именно он, а не оригинал.
Вторая причина расхождения — предел ползунков смещения. Рамку можно было
утащить туда, куда ползунок не дотягивается: рамка рисовалась от
неограниченного значения, а фильтр считался от обрезанного. Предел
расширен вчетверо, а рамка теперь пересчитывается обратно из настройки —
если та упёрлась, это сразу видно.
Отдельно: разбор звука больше не держит главный поток. Перезапуск ждал
окончания текущего файла, а это чтение гигабайтов — окно всё это время не
отвечало, и меню не открывалось вовсе. Теперь проход доигрывает сам и
запускается заново, а идёт он с наинизшим приоритетом.
2026-08-20 19:23:54 +03:00
|
|
|
|
PW, PH, PX, PY: Integer;
|
Нелинейный видеоредактор без перекодирования
Куски видео вырезаются и склеиваются копированием потока: сборка ролика
занимает время копирования файла, а не время рендера. Композитинга нет
принципиально — при перекрытии побеждает клип с верхней дорожки целиком,
и картинкой, и звуком. Именно это ограничение и делает сборку копированием
возможной.
Что внутри:
- монтажный стол со своей отрисовкой: дорожки, масштаб, прилипание,
схлопывание, громкость на клипе;
- предпросмотр прямо с монтажа через libmpv и виртуальный список кусков,
без промежуточных файлов;
- сборка через ffmpeg с точными резами: перекодируется только огрызок от
реза до ближайшего опорного кадра, остальное копируется;
- разбор заголовка MP4 для поиска опорных кадров — в 500 раз быстрее
опроса ffprobe;
- перепаковка контейнеров, кадрирование и поворот, сведения о проекте;
- свои цвета всего окна: системные элементы под Windows цвет не берут,
поэтому кнопки, полоса меню, журнал и полосы прокрутки нарисованы сами;
- окно зависимостей: недостающие ffmpeg, ffprobe и libmpv скачиваются с
официальных источников;
- кросс-сборка под Windows и работа под Wine.
Первый коммит: до этого история не велась.
2026-08-19 18:36:16 +03:00
|
|
|
|
Rot: Integer;
|
|
|
|
|
|
|
|
|
|
|
|
procedure Add(const AFilter: string);
|
|
|
|
|
|
begin
|
|
|
|
|
|
if Chain <> '' then
|
|
|
|
|
|
Chain := Chain + ',';
|
|
|
|
|
|
Chain := Chain + AFilter;
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
|
|
begin
|
|
|
|
|
|
Result := '';
|
|
|
|
|
|
if (ASrcWidth <= 0) or (ASrcHeight <= 0) or
|
|
|
|
|
|
(ADstWidth <= 0) or (ADstHeight <= 0) then
|
|
|
|
|
|
Exit;
|
|
|
|
|
|
|
|
|
|
|
|
Chain := '';
|
|
|
|
|
|
|
Кадрирование: поворот после обрезки, рамка не расходится с настройкой
Поворот и отражение применялись к исходному кадру, а обрезка шла после
них. Из-за этого рамка показывала одно место, а вырезалось другое.
Теперь наоборот: сначала выбирается кусок, потом он поворачивается и
отражается. Если после поворота кусок лёг поперёк кадра проекта, он
вписывается целиком с полями.
Проверено: с отражением левый и правый края вырезанного куска меняются
местами, то есть отражается именно он, а не оригинал.
Вторая причина расхождения — предел ползунков смещения. Рамку можно было
утащить туда, куда ползунок не дотягивается: рамка рисовалась от
неограниченного значения, а фильтр считался от обрезанного. Предел
расширен вчетверо, а рамка теперь пересчитывается обратно из настройки —
если та упёрлась, это сразу видно.
Отдельно: разбор звука больше не держит главный поток. Перезапуск ждал
окончания текущего файла, а это чтение гигабайтов — окно всё это время не
отвечало, и меню не открывалось вовсе. Теперь проход доигрывает сам и
запускается заново, а идёт он с наинизшим приоритетом.
2026-08-20 19:23:54 +03:00
|
|
|
|
{ Порядок действий: сначала выбирается кусок кадра, и только потом он
|
|
|
|
|
|
поворачивается и отражается.
|
|
|
|
|
|
|
|
|
|
|
|
Так это устроено в монтажных программах, и так понятнее: рамка
|
|
|
|
|
|
показывает, что́ останется, а поворот применяется к оставшемуся. При
|
|
|
|
|
|
обратном порядке обрезался бы уже перевёрнутый кадр, и в рамку
|
|
|
|
|
|
попадало бы совсем не то, что видно. }
|
Нелинейный видеоредактор без перекодирования
Куски видео вырезаются и склеиваются копированием потока: сборка ролика
занимает время копирования файла, а не время рендера. Композитинга нет
принципиально — при перекрытии побеждает клип с верхней дорожки целиком,
и картинкой, и звуком. Именно это ограничение и делает сборку копированием
возможной.
Что внутри:
- монтажный стол со своей отрисовкой: дорожки, масштаб, прилипание,
схлопывание, громкость на клипе;
- предпросмотр прямо с монтажа через libmpv и виртуальный список кусков,
без промежуточных файлов;
- сборка через ffmpeg с точными резами: перекодируется только огрызок от
реза до ближайшего опорного кадра, остальное копируется;
- разбор заголовка MP4 для поиска опорных кадров — в 500 раз быстрее
опроса ffprobe;
- перепаковка контейнеров, кадрирование и поворот, сведения о проекте;
- свои цвета всего окна: системные элементы под Windows цвет не берут,
поэтому кнопки, полоса меню, журнал и полосы прокрутки нарисованы сами;
- окно зависимостей: недостающие ffmpeg, ffprobe и libmpv скачиваются с
официальных источников;
- кросс-сборка под Windows и работа под Wine.
Первый коммит: до этого история не велась.
2026-08-19 18:36:16 +03:00
|
|
|
|
|
|
|
|
|
|
{ --- Масштаб --- }
|
|
|
|
|
|
if AT.Fit = fmStretch then
|
|
|
|
|
|
begin
|
|
|
|
|
|
SW := MakeEven(ADstWidth);
|
|
|
|
|
|
SH := MakeEven(ADstHeight);
|
|
|
|
|
|
end
|
|
|
|
|
|
else
|
|
|
|
|
|
begin
|
Кадрирование: поворот после обрезки, рамка не расходится с настройкой
Поворот и отражение применялись к исходному кадру, а обрезка шла после
них. Из-за этого рамка показывала одно место, а вырезалось другое.
Теперь наоборот: сначала выбирается кусок, потом он поворачивается и
отражается. Если после поворота кусок лёг поперёк кадра проекта, он
вписывается целиком с полями.
Проверено: с отражением левый и правый края вырезанного куска меняются
местами, то есть отражается именно он, а не оригинал.
Вторая причина расхождения — предел ползунков смещения. Рамку можно было
утащить туда, куда ползунок не дотягивается: рамка рисовалась от
неограниченного значения, а фильтр считался от обрезанного. Предел
расширен вчетверо, а рамка теперь пересчитывается обратно из настройки —
если та упёрлась, это сразу видно.
Отдельно: разбор звука больше не держит главный поток. Перезапуск ждал
окончания текущего файла, а это чтение гигабайтов — окно всё это время не
отвечало, и меню не открывалось вовсе. Теперь проход доигрывает сам и
запускается заново, а идёт он с наинизшим приоритетом.
2026-08-20 19:23:54 +03:00
|
|
|
|
KFit := Min(ADstWidth / ASrcWidth, ADstHeight / ASrcHeight);
|
|
|
|
|
|
KFill := Max(ADstWidth / ASrcWidth, ADstHeight / ASrcHeight);
|
Нелинейный видеоредактор без перекодирования
Куски видео вырезаются и склеиваются копированием потока: сборка ролика
занимает время копирования файла, а не время рендера. Композитинга нет
принципиально — при перекрытии побеждает клип с верхней дорожки целиком,
и картинкой, и звуком. Именно это ограничение и делает сборку копированием
возможной.
Что внутри:
- монтажный стол со своей отрисовкой: дорожки, масштаб, прилипание,
схлопывание, громкость на клипе;
- предпросмотр прямо с монтажа через libmpv и виртуальный список кусков,
без промежуточных файлов;
- сборка через ffmpeg с точными резами: перекодируется только огрызок от
реза до ближайшего опорного кадра, остальное копируется;
- разбор заголовка MP4 для поиска опорных кадров — в 500 раз быстрее
опроса ffprobe;
- перепаковка контейнеров, кадрирование и поворот, сведения о проекте;
- свои цвета всего окна: системные элементы под Windows цвет не берут,
поэтому кнопки, полоса меню, журнал и полосы прокрутки нарисованы сами;
- окно зависимостей: недостающие ffmpeg, ffprobe и libmpv скачиваются с
официальных источников;
- кросс-сборка под Windows и работа под Wine.
Первый коммит: до этого история не велась.
2026-08-19 18:36:16 +03:00
|
|
|
|
if AT.Fit = fmFill then
|
|
|
|
|
|
K := KFill
|
|
|
|
|
|
else
|
|
|
|
|
|
K := KFit;
|
|
|
|
|
|
if AT.Zoom > 0 then
|
|
|
|
|
|
K := K * AT.Zoom;
|
Кадрирование: поворот после обрезки, рамка не расходится с настройкой
Поворот и отражение применялись к исходному кадру, а обрезка шла после
них. Из-за этого рамка показывала одно место, а вырезалось другое.
Теперь наоборот: сначала выбирается кусок, потом он поворачивается и
отражается. Если после поворота кусок лёг поперёк кадра проекта, он
вписывается целиком с полями.
Проверено: с отражением левый и правый края вырезанного куска меняются
местами, то есть отражается именно он, а не оригинал.
Вторая причина расхождения — предел ползунков смещения. Рамку можно было
утащить туда, куда ползунок не дотягивается: рамка рисовалась от
неограниченного значения, а фильтр считался от обрезанного. Предел
расширен вчетверо, а рамка теперь пересчитывается обратно из настройки —
если та упёрлась, это сразу видно.
Отдельно: разбор звука больше не держит главный поток. Перезапуск ждал
окончания текущего файла, а это чтение гигабайтов — окно всё это время не
отвечало, и меню не открывалось вовсе. Теперь проход доигрывает сам и
запускается заново, а идёт он с наинизшим приоритетом.
2026-08-20 19:23:54 +03:00
|
|
|
|
SW := MakeEven(Round(ASrcWidth * K));
|
|
|
|
|
|
SH := MakeEven(Round(ASrcHeight * K));
|
Нелинейный видеоредактор без перекодирования
Куски видео вырезаются и склеиваются копированием потока: сборка ролика
занимает время копирования файла, а не время рендера. Композитинга нет
принципиально — при перекрытии побеждает клип с верхней дорожки целиком,
и картинкой, и звуком. Именно это ограничение и делает сборку копированием
возможной.
Что внутри:
- монтажный стол со своей отрисовкой: дорожки, масштаб, прилипание,
схлопывание, громкость на клипе;
- предпросмотр прямо с монтажа через libmpv и виртуальный список кусков,
без промежуточных файлов;
- сборка через ffmpeg с точными резами: перекодируется только огрызок от
реза до ближайшего опорного кадра, остальное копируется;
- разбор заголовка MP4 для поиска опорных кадров — в 500 раз быстрее
опроса ffprobe;
- перепаковка контейнеров, кадрирование и поворот, сведения о проекте;
- свои цвета всего окна: системные элементы под Windows цвет не берут,
поэтому кнопки, полоса меню, журнал и полосы прокрутки нарисованы сами;
- окно зависимостей: недостающие ffmpeg, ffprobe и libmpv скачиваются с
официальных источников;
- кросс-сборка под Windows и работа под Wine.
Первый коммит: до этого история не велась.
2026-08-19 18:36:16 +03:00
|
|
|
|
end;
|
|
|
|
|
|
Add(Format('scale=%d:%d', [SW, SH]));
|
|
|
|
|
|
|
|
|
|
|
|
{ --- Обрезка того, что вылезло за кадр --- }
|
|
|
|
|
|
CW := Min(SW, MakeEven(ADstWidth));
|
|
|
|
|
|
CH := Min(SH, MakeEven(ADstHeight));
|
|
|
|
|
|
if (CW < SW) or (CH < SH) then
|
|
|
|
|
|
begin
|
|
|
|
|
|
{ Смещение задаётся в долях кадра проекта и прижимается к границам:
|
|
|
|
|
|
выйти за пределы картинки нельзя, ffmpeg на это ругается. }
|
|
|
|
|
|
CX := Round((SW - CW) / 2 + AT.OffsetX * ADstWidth);
|
|
|
|
|
|
CY := Round((SH - CH) / 2 + AT.OffsetY * ADstHeight);
|
|
|
|
|
|
CX := Max(0, Min(SW - CW, CX));
|
|
|
|
|
|
CY := Max(0, Min(SH - CH, CY));
|
|
|
|
|
|
Add(Format('crop=%d:%d:%d:%d', [CW, CH, CX, CY]));
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
Кадрирование: поворот после обрезки, рамка не расходится с настройкой
Поворот и отражение применялись к исходному кадру, а обрезка шла после
них. Из-за этого рамка показывала одно место, а вырезалось другое.
Теперь наоборот: сначала выбирается кусок, потом он поворачивается и
отражается. Если после поворота кусок лёг поперёк кадра проекта, он
вписывается целиком с полями.
Проверено: с отражением левый и правый края вырезанного куска меняются
местами, то есть отражается именно он, а не оригинал.
Вторая причина расхождения — предел ползунков смещения. Рамку можно было
утащить туда, куда ползунок не дотягивается: рамка рисовалась от
неограниченного значения, а фильтр считался от обрезанного. Предел
расширен вчетверо, а рамка теперь пересчитывается обратно из настройки —
если та упёрлась, это сразу видно.
Отдельно: разбор звука больше не держит главный поток. Перезапуск ждал
окончания текущего файла, а это чтение гигабайтов — окно всё это время не
отвечало, и меню не открывалось вовсе. Теперь проход доигрывает сам и
запускается заново, а идёт он с наинизшим приоритетом.
2026-08-20 19:23:54 +03:00
|
|
|
|
{ --- Поворот и отражение оставшегося куска --- }
|
|
|
|
|
|
Rot := ((AT.Rotation mod 360) + 360) mod 360;
|
|
|
|
|
|
case Rot of
|
|
|
|
|
|
90:
|
|
|
|
|
|
begin
|
|
|
|
|
|
{ transpose=1 — поворот на 90 по часовой стрелке. }
|
|
|
|
|
|
Add('transpose=1');
|
|
|
|
|
|
PW := CH; PH := CW;
|
|
|
|
|
|
end;
|
|
|
|
|
|
180:
|
|
|
|
|
|
begin
|
|
|
|
|
|
{ Два поворота подряд дешевле и точнее, чем поворот на
|
|
|
|
|
|
произвольный угол: последний пересчитывает каждый пиксель. }
|
|
|
|
|
|
Add('transpose=1,transpose=1');
|
|
|
|
|
|
PW := CW; PH := CH;
|
|
|
|
|
|
end;
|
|
|
|
|
|
270:
|
|
|
|
|
|
begin
|
|
|
|
|
|
Add('transpose=2');
|
|
|
|
|
|
PW := CH; PH := CW;
|
|
|
|
|
|
end;
|
|
|
|
|
|
else
|
|
|
|
|
|
PW := CW; PH := CH;
|
|
|
|
|
|
end;
|
|
|
|
|
|
if AT.FlipH then
|
|
|
|
|
|
Add('hflip');
|
|
|
|
|
|
if AT.FlipV then
|
|
|
|
|
|
Add('vflip');
|
|
|
|
|
|
|
|
|
|
|
|
{ --- Приведение к кадру проекта --- }
|
|
|
|
|
|
|
|
|
|
|
|
{ После поворота на 90 кусок лежит поперёк и в кадр проекта уже не
|
|
|
|
|
|
вписывается. Вписываем целиком, с полями. }
|
|
|
|
|
|
if (PW > ADstWidth) or (PH > ADstHeight) then
|
|
|
|
|
|
begin
|
|
|
|
|
|
K := Min(ADstWidth / PW, ADstHeight / PH);
|
|
|
|
|
|
PW := MakeEven(Round(PW * K));
|
|
|
|
|
|
PH := MakeEven(Round(PH * K));
|
|
|
|
|
|
Add(Format('scale=%d:%d', [PW, PH]));
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
|
|
if (PW < ADstWidth) or (PH < ADstHeight) then
|
Нелинейный видеоредактор без перекодирования
Куски видео вырезаются и склеиваются копированием потока: сборка ролика
занимает время копирования файла, а не время рендера. Композитинга нет
принципиально — при перекрытии побеждает клип с верхней дорожки целиком,
и картинкой, и звуком. Именно это ограничение и делает сборку копированием
возможной.
Что внутри:
- монтажный стол со своей отрисовкой: дорожки, масштаб, прилипание,
схлопывание, громкость на клипе;
- предпросмотр прямо с монтажа через libmpv и виртуальный список кусков,
без промежуточных файлов;
- сборка через ffmpeg с точными резами: перекодируется только огрызок от
реза до ближайшего опорного кадра, остальное копируется;
- разбор заголовка MP4 для поиска опорных кадров — в 500 раз быстрее
опроса ffprobe;
- перепаковка контейнеров, кадрирование и поворот, сведения о проекте;
- свои цвета всего окна: системные элементы под Windows цвет не берут,
поэтому кнопки, полоса меню, журнал и полосы прокрутки нарисованы сами;
- окно зависимостей: недостающие ffmpeg, ffprobe и libmpv скачиваются с
официальных источников;
- кросс-сборка под Windows и работа под Wine.
Первый коммит: до этого история не велась.
2026-08-19 18:36:16 +03:00
|
|
|
|
begin
|
Кадрирование: поворот после обрезки, рамка не расходится с настройкой
Поворот и отражение применялись к исходному кадру, а обрезка шла после
них. Из-за этого рамка показывала одно место, а вырезалось другое.
Теперь наоборот: сначала выбирается кусок, потом он поворачивается и
отражается. Если после поворота кусок лёг поперёк кадра проекта, он
вписывается целиком с полями.
Проверено: с отражением левый и правый края вырезанного куска меняются
местами, то есть отражается именно он, а не оригинал.
Вторая причина расхождения — предел ползунков смещения. Рамку можно было
утащить туда, куда ползунок не дотягивается: рамка рисовалась от
неограниченного значения, а фильтр считался от обрезанного. Предел
расширен вчетверо, а рамка теперь пересчитывается обратно из настройки —
если та упёрлась, это сразу видно.
Отдельно: разбор звука больше не держит главный поток. Перезапуск ждал
окончания текущего файла, а это чтение гигабайтов — окно всё это время не
отвечало, и меню не открывалось вовсе. Теперь проход доигрывает сам и
запускается заново, а идёт он с наинизшим приоритетом.
2026-08-20 19:23:54 +03:00
|
|
|
|
PX := Max(0, (ADstWidth - PW) div 2);
|
|
|
|
|
|
PY := Max(0, (ADstHeight - PH) div 2);
|
Нелинейный видеоредактор без перекодирования
Куски видео вырезаются и склеиваются копированием потока: сборка ролика
занимает время копирования файла, а не время рендера. Композитинга нет
принципиально — при перекрытии побеждает клип с верхней дорожки целиком,
и картинкой, и звуком. Именно это ограничение и делает сборку копированием
возможной.
Что внутри:
- монтажный стол со своей отрисовкой: дорожки, масштаб, прилипание,
схлопывание, громкость на клипе;
- предпросмотр прямо с монтажа через libmpv и виртуальный список кусков,
без промежуточных файлов;
- сборка через ffmpeg с точными резами: перекодируется только огрызок от
реза до ближайшего опорного кадра, остальное копируется;
- разбор заголовка MP4 для поиска опорных кадров — в 500 раз быстрее
опроса ffprobe;
- перепаковка контейнеров, кадрирование и поворот, сведения о проекте;
- свои цвета всего окна: системные элементы под Windows цвет не берут,
поэтому кнопки, полоса меню, журнал и полосы прокрутки нарисованы сами;
- окно зависимостей: недостающие ffmpeg, ffprobe и libmpv скачиваются с
официальных источников;
- кросс-сборка под Windows и работа под Wine.
Первый коммит: до этого история не велась.
2026-08-19 18:36:16 +03:00
|
|
|
|
Add(Format('pad=%d:%d:%d:%d', [ADstWidth, ADstHeight, PX, PY]));
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
|
|
{ Пиксель считаем квадратным: иначе плеер растянет кадр по своему
|
|
|
|
|
|
разумению, и результат разойдётся с тем, что видно в предпросмотре. }
|
|
|
|
|
|
Add('setsar=1');
|
|
|
|
|
|
Result := Chain;
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
|
|
end.
|