Чому попередній перегляд відео бреше про експорт
Ваш відеоредактор виглядає завершеним. Попередній перегляд чіткий, підпис знаходиться саме там, куди його перетягнув користувач, а часова шкала плавно прокручується. Потім користувач натискає Export, і підпис повертається не на те місце, не того розміру, іноді повністю обрізаний. Нічого не вийшло з ладу, немає помилки в журналі — попередній перегляд і файл просто не збігаються.
Це найпоширеніша помилка в кожному відеоредакторі, і це скоріше проблема моделювання даних, ніж рендерингу. Нижче описано звичку, яка унеможливлює цю помилку, як правильно масштабувати кадри за співвідношенням сторін (aspect ratio) і як підтримувати швидке перетягування на дешевих телефонах. Ці ідеї застосовні в будь-якій мові або UI фреймворку.
Співвідношення сторін (Aspect Ratio) — це форма, Роздільна здатність (Resolution) — це розмір
Більшість помилок з кадрами починаються тут. Співвідношення сторін (aspect ratio) описує форму кадру і нічого більше: 9:16 — високий, 16:9 — широкий. Роздільна здатність (resolution) описує розмір у пікселях, наприклад 1080x1920. Одна форма підтримує багато розмірів, тому ці два значення ніколи не взаємозамінні.
| Співвідношення сторін | Числове значення | Типове використання |
| 16:9 | 1.78 | YouTube, альбомна вебсторінка |
| 9:16 | 0.56 | Reels, TikTok, Shorts |
| 1:1 | 1.00 | Квадратні публікації в стрічці |
| 4:5 | 0.80 | Портретні публікації в стрічці |
Зберігайте співвідношення як звичайний текст і перетворюйте його на число лише тоді, коли виконуєте математичні операції. Збережіть цільову висоту для експорту, потім виведіть ширину зі співвідношення та примусово зробіть обидва числа парними, перш ніж вони досягнуть кодера (encoder). Непарні розміри є прихованою причиною дивовижної кількості невдалих експортів.
height = width / ratioToNumber(ratio) // масштабування вікна попереднього перегляду
function widthFromHeight(ratio, height): raw = height * ratioToNumber(ratio) return makeEven(round(raw)) // encoders вимагають парні сторони
// 9:16 при висоті 1080 -> 608 x 1080// 16:9 при висоті 1080 -> 1920 x 1080
Зберігайте позиції як дробові значення, а не пікселі
Редактор існує у трьох світах з трьома різними розмірами: збережений документ, попередній перегляд на екрані та експортований файл. Документ є єдиним джерелом істини — інші два є лише його візуалізаціями в іншому масштабі.
Project -> Clip (джерело, обрізка, фільтри) -> Overlay (текст / стікер)
Документ Рендерер попереднього перегляду Рендерер експорту x = 0.5, y = 0.9 --> x * 360 px --> x * 1080 px --> MP4 | ^ ^ +------------------------+------------------------+ одна збережена фракція, одна формула, два розміри
Якщо ви зберігаєте 540 пікселів, це число буде правильним лише на пристрої, на якому воно було виміряне. Якщо ви зберігаєте 0.5, це означає "горизонтальний центр" для будь-якого розміру назавжди. Кожна позиція, зсув і масштаб у моделі повинні бути дробовим значенням від 0 до 1.
type Overlay { x: number // 0.0-1.0 по горизонталі (0.5 = центр) y: number // 0.0-1.0 по вертикалі (0.9 = близько до низу) scale: number // 1.0 = нормальний розмір rotation: number // градуси startTimeMs: number // коли з'являється endTimeMs: number // коли зникає}
Тоді експорт стає майже нудним, що й є метою. Рендерер використовує ту ж формулу, що й попередній перегляд, але з більшим множником: px = overlay.x * exportWidth. FFmpeg сам обробляє кадр, а його вираз для центрування (ow-iw)/2 є тією ж арифметикою, яку попередній перегляд використовує для letterbox. Оскільки збережене значення ніколи не змінювалося, підпис розміщується точно в потрібному місці.
ffmpeg -i input.mp4 \ -vf "scale=1080:1920:force_original_aspect_ratio=decrease,\ pad=1080:1920:(ow-iw)/2:(oh-ih)/2:0xD3D3D3" \ -c:v libx264 -crf 23 -pix_fmt yuv420p -c:a aac output.mp4
Відображення попереднього перегляду за один прохід
Вимірюйте вікно попереднього перегляду під час виконання, замість того, щоб жорстко кодувати його, оскільки телефон, планшет і змінене вікно робочого столу дають різні розміри. Відображайте кожен overlay за один прохід canvas, замість того, щоб монтувати кожен як окремий елемент UI — один прохід залишається плавним, поки рухається палець.
Два правила забезпечують коректність циклу. Пропускайте будь-який overlay, що знаходиться поза його часовим діапазоном, і завжди поєднуйте save() з restore(), щоб один елемент не міг передати своє перетворення наступному.
function drawPreview(canvas, overlays, currentTime, box): for each overlay in overlays: if currentTime < overlay.startTimeMs: пропустити if currentTime > overlay.endTimeMs: пропустити
px = overlay.x * box.width // ключова формула py = overlay.y * box.height
canvas.save() canvas.move(px, py) canvas.rotate(overlay.rotation) canvas.resize(overlay.scale) canvas.drawText(overlay.content) canvas.restore() // ніколи не є необов'язковим
Забезпечення плавності перетягування за допомогою двоступеневого стану
Палець, що перетягує, повідомляє приблизно шістдесят позицій на секунду. Якщо кожна з них записується до вашого project store, ви платите за валідацію, зберігання та повну перебудову стану шістдесят разів на секунду, і перетягування помітно затинається. Натомість розділіть роботу на два рівні:
- Рівень 1, ефемерний: невелика
liveDragкарта, що містить поточне положення пальця. Оновлюйте її при кожному русі та перемальовуйте canvas. Більше нічого не запускається. - Рівень 2, стійкий: після завершення перетягування, зафіксуйте кінцеве дробове значення для overlay в моделі проекту, очистіть запис
liveDragі запишіть один крок скасування.
Переваги виходять за межі частоти кадрів (frame rate). Історія скасувань залишається корисною, тому що перетягування створює один запис, а не сотні, а автозбереження припиняє інтенсивний доступ до диска. Саме так Figma і Canva підтримують чуйність прямої маніпуляції.
Контекст реального світу
У MicrocosmWorks ми взялися за редактор коротких відео, у якому підписи зміщувалися під час експорту. Команда зберігала позиції overlay як пікселі пристрою, зчитані безпосередньо з canvas попереднього перегляду, тому підпис, створений на маленькому телефоні, виявлявся високо і маленьким у файлі 1080p, а зміна співвідношення сторін (aspect ratio) повністю виштовхувала деякі overlay за межі кадру.
Ми перевели модель на нормалізовані координати від 0 до 1, змусили обидва рендерери використовувати один помічник «дріб-на-розмір» і забезпечили парні вихідні розміри, отримані зі збереженого співвідношення. Попередній перегляд та експорт збігалися на кожному тестовому пристрої. Перенесення фіксації перетягування на момент його завершення усунуло затримку, про яку повідомляли користувачі на низькопродуктивному обладнанні Android. Ви можете побачити, які короткі відео та роботи з редагування були результатом цього, у нашому портфоліо проектів.
Висновок
Попередній перегляд та експорт — це дві візуалізації одного документа, тому надайте їм одне джерело істини. Зберігайте позиції як дробові значення, використовуйте ту саму формулу «дріб-на-розмір» в обох рендерерах, виводьте ширину зі співвідношення сторін (aspect ratio), зберігайте парні розміри та фіксуйте зміни перетягування лише тоді, коли палець відпускається. Ці п'ять звичок усувають цілу категорію помилок ще до їхнього написання.
Створення медіа-движків, де інтерактивний перегляд і кінцевий файл збігаються, є основним напрямком нашої інженерної роботи з відео та стрімінгу в MicrocosmWorks.
Випускаєте відео- або контент-редактор і хочете, щоб попередній перегляд та експорт дійсно збігалися? Ми вирішили саме цей клас помилок в інструментах для редагування коротких відео. Зверніться до нашої інженерної команди →
Читайте більше від нашої команди
1. Оптимізація логотипу каналу для різних роздільних здатностей відео
2. Сфотографуйте тарілку, зафіксуйте прийом їжі: Конвеєр харчування на основі комп'ютерного зору
3. Оптимізація логотипу каналу для різних роздільних здатностей відео

