Розробка онлайн-редактора відео
Браузерний редактор відео — одна з технічно важких завдань у фронтенд-розробці. Ключове рішення на початку: де відбувається рендеринг фінального відео — в браузері або на сервері. Весь стек залежить від цього.
Браузер vs Сервер: де рендерити
Client-side рендеринг (WebCodecs API + FFmpeg.wasm):
- Не вимагає серверних потужностей для рендерингу
- Обмежений продуктивністю CPU користувача
- FFmpeg.wasm: 10 хвилин відео рендерятся ~5–15 хвилин
- Підтримка WebCodecs: Chrome 94+, Firefox 130+, Safari 16.4+
Server-side рендеринг (Remotion + FFmpeg):
- Рендеринг на потужних серверах (GPU опціонально)
- Користувач не чекає — отримує готовий файл за посиланням
- Remotion вміє рендерити React-компоненти в відео
- Добре масштабується через AWS Lambda
Для комерційного продукту з довгими відео — серверний рендеринг. Для легкого інструмента (короткі клипи до 2 хвилин) — клієнтський.
Timeline (Шкала часу)
Центральна UI-концепція — таймлайн з дорожками. Структура даних:
interface VideoProject {
id: string;
duration: number; // секунди
fps: number; // 24 | 30 | 60
width: number;
height: number;
tracks: Track[];
}
interface Track {
id: string;
type: 'video' | 'audio' | 'text' | 'image' | 'effect';
clips: Clip[];
muted: boolean;
locked: boolean;
volume: number; // 0–1
}
interface Clip {
id: string;
trackId: string;
assetId: string; // посилання на завантажений файл
startTime: number; // позиція на таймлайні (секунди)
duration: number; // тривалість клипу
trimStart: number; // обрізка початку вихідного файлу
trimEnd: number; // обрізка кінця
speed: number; // 0.25 – 4.0
opacity: number;
transform?: ClipTransform;
filters?: VideoFilter[];
}
Попередній перегляд у браузері
Для попереднього перегляду перед рендерингом використовуємо HTML5 <video> з синхронізацією через currentTime:
const PreviewPlayer: React.FC = () => {
const { currentTime, isPlaying, tracks } = useEditorStore();
const videoRefs = useRef<Map<string, HTMLVideoElement>>(new Map());
useEffect(() => {
// Синхронізуємо всі відеоклипи з таймлайном
tracks.forEach(track => {
track.clips.forEach(clip => {
const video = videoRefs.current.get(clip.id);
if (!video) return;
const clipTime = currentTime - clip.startTime;
const isActive = clipTime >= 0 && clipTime <= clip.duration;
video.style.display = isActive ? 'block' : 'none';
if (isActive) {
const targetTime = clip.trimStart + clipTime * clip.speed;
if (Math.abs(video.currentTime - targetTime) > 0.05) {
video.currentTime = targetTime;
}
isPlaying ? video.play() : video.pause();
} else {
video.pause();
}
});
});
}, [currentTime, isPlaying]);
};
Скреблінг по таймлайну:
const Timeline: React.FC = () => {
const { setCurrentTime, duration } = useEditorStore();
const railRef = useRef<HTMLDivElement>(null);
const handleMouseDown = (e: React.MouseEvent) => {
const handleMove = (e: MouseEvent) => {
const rect = railRef.current!.getBoundingClientRect();
const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
setCurrentTime(pct * duration);
};
document.addEventListener('mousemove', handleMove);
document.addEventListener('mouseup', () => {
document.removeEventListener('mousemove', handleMove);
}, { once: true });
};
return (
<div ref={railRef} className="timeline-rail" onMouseDown={handleMouseDown}>
<TimelinePlayhead />
</div>
);
};







