פיתוח עורך וידאו מקוון: עריכה ועיבוד בדפדפן
כשלקוח מבקש עריכת וידאו מבוססת דפדפן, המכשול הראשון הוא עיבוד הפלט הסופי. עיבוד קליפ של דקה אחת בצד הלקוח באמצעות FFmpeg.wasm עובד, אבל וידאו של עשר דקות יקפיא אפילו מחשב נייד חזק. טיפלנו בזה במספר פרויקטים ופיתחנו ארכיטקטורה שמתרחבת. מאמר זה מכסה החלטות ארכיטקטוניות מרכזיות: סוג העיבוד, מימוש ציר הזמן והעלאת קבצים.
דפדפן מול שרת: איפה לעבד?
הבחירה בין עיבוד בצד הלקוח לעיבוד בצד השרת מגדירה את כל המחסן הטכנולוגי וחוויית המשתמש. הנה השוואה:
| פרמטר | צד לקוח (WebCodecs + FFmpeg.wasm) | צד שרת (Remotion + FFmpeg) |
|---|---|---|
| תלות במעבד | מעבד המשתמש | אין |
| משך וידאו מקסימלי | ~2 דקות | ללא הגבלה |
| זמן עיבוד ל-10 דקות (מעבד i7) | 5–15 דקות | ~2 דקות על GPU |
| קנה מידה | וידאו אחד בכל פעם | משימות מקבילות (Lambda) |
| תמיכת דפדפנים | Chrome 94+, Firefox 130+, Safari 16.4+ | כל דפדפן (קובץ דרך קישור) |
| עלות תשתית | אפס | עלויות שרת (חיסכון של עד 40% בעומס גבוה) |
עיבוד בצד הלקוח (WebCodecs API + FFmpeg.wasm): אין צורך בכוח שרת; כל העומס על מעבד המשתמש. FFmpeg.wasm מעבד 10 דקות של וידאו ב-5–15 דקות בזמן אמת (תלוי במעבד). WebCodecs API נתמך ב-Chrome 94+, Firefox 130+ ו-Safari 16.4+. אידיאלי לקליפים קצרים עד 2 דקות—ללא עיכובים, ללא עלויות שרת.
עיבוד בצד השרת (Remotion + FFmpeg): עיבוד על שרתי GPU; המשתמש מקבל קובץ מוכן דרך קישור. Remotion מתאר וידאו כרכיבי React, ומציע גמישות ושימוש חוזר. מתרחב דרך AWS Lambda—עיבוד במקטעים מקבילים. למוצר מסחרי עם וידאו ארוך מ-2 דקות, זו הדרך האמינה היחידה. בפרויקטים שלנו, אנחנו בדרך כלל בוחרים בצד השרת: זה נותן זמני עיבוד צפויים ולא מרוקן את הסוללה של המשתמש. אבל לעורך מינימלי לקליפים קצרים, האפשרות של צד הלקוח מהירה וזולה יותר.
תיעוד רשמי של Remotion: https://remotion.dev/docs/
איך בנוי ציר הזמן?
מושג ה-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[];
}הנתונים נשמרים במנהל מצב (Zustand או Redux) ומסונכרנים עם מסד הנתונים בשמירה. גרירה ושחרור מיושמת באמצעות 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[]; } —היא מטפלת בצורה אמינה בהזזת קליפים בין רצועות.
איך לפצל קליפ על ציר הזמן?
פיצול הוא פעולה בסיסית. הנה האלגוריתם שלב אחר שלב:
- קבע את נקודת הפיצול בשניות ביחס לציר הזמן (לדוגמה, בעת גרירת הסמן).
- חשב splitPoint = (atTime - clip.startTime) + clip.trimStart.
- צור שני קליפים חדשים: שמאלי עם trimEnd = splitPoint וימני עם trimStart = splitPoint.
- מחק את הקליפ המקורי והוסף את החדשים.
קוד:
const splitClip = (clipId: string, atTime: number) => {
const clip = getClip(clipId);
const splitPoint = atTime - clip.startTime + clip.trimStart;
const leftClip: Clip = {
...clip,
id: uuid(),
duration: atTime - clip.startTime,
trimEnd: splitPoint
};
const rightClip: Clip = {
...clip,
id: uuid(),
startTime: atTime,
duration: clip.startTime + clip.duration - atTime,
trimStart: splitPoint,
};
removeClip(clipId);
addClip(leftClip);
addClip(rightClip);
}; תצוגה מקדימה בדפדפן
לתצוגה מקדימה, אנו משתמשים ב-HTML5 @dnd-kit עם סנכרון דרך const splitClip = (clipId: string, atTime: number) => { const clip = getClip(clipId); const splitPoint = atTime - clip.startTime + clip.trimStart; const leftClip: Clip = { ...clip, id: uuid(), duration: atTime - clip.startTime, trimEnd: splitPoint }; const rightClip: Clip = { ...clip, id: uuid(), startTime: atTime, duration: clip.startTime + clip.duration - atTime, trimStart: splitPoint, }; removeClip(clipId); addClip(leftClip); addClip(rightClip); }; . הנה הוק מרכזי:
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]);
// ...
};גלילה על ציר הזמן מתבצעת דרך mousedown/mousemove—חישוב המיקום בשניות מרוחב המיכל.
קליפים: גרירה, חיתוך, פיצול
גרירה: בעת גרירה, חשב את הדלתא ועדכן את startTime תוך התחשבות בפיקסלים לשנייה. הגבל לגבולות הפרויקט ולקליפים סמוכים.
חיתוך: מיושם בדומה לפיצול, אבל ללא מחיקת הקליפ המקורי—רק שינוי של trimStart/trimEnd.
עיבוד בצד השרת עם Remotion
Remotion מאפשר לתאר וידאו כרכיב React. אנו מעבירים את הפרויקט דרך <video> ומעבדים על השרת:
// Компонент для рендеринга
const VideoComposition: React.FC<{ project: VideoProject }> = ({ project }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const currentTime = frame / fps;
return (
<AbsoluteFill style={{ background: '#000' }}>
{project.tracks.flatMap(track =>
track.clips.map(clip => {
const clipTime = currentTime - clip.startTime;
if (clipTime < 0 || clipTime > clip.duration) return null;
return (
<OffthreadVideo
key={clip.id}
src={clip.assetUrl}
startFrom={Math.round(clip.trimStart * fps)}
style={{ opacity: clip.opacity }}
/>
);
})
)}
</AbsoluteFill>
);
};
אנו מפעילים עיבוד דרך ה-API באמצעות currentTime לקנה מידה. בפרויקט אחד, צמצמנו את זמן העיבוד הממוצע לוידאו של 10 דקות מ-15 דקות (צד לקוח) לפחות מ-2 דקות באמצעות גישה זו.
העלאת נכסים
קבצים גדולים (וידאו, אודיו) מועלים ישירות ל-S3 דרך כתובות URL חתומות מראש—תוך עקיפת שרת היישום:
// 1. Запрашиваем presigned URL
const { uploadUrl, key } = await api.post('/api/editor/upload-url', {
filename: file.name,
contentType: file.type,
size: file.size,
});
// 2. Загружаем напрямую в S3 с прогрессом
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
setProgress(Math.round(e.loaded / e.total * 100));
});
xhr.open('PUT', uploadUrl);
xhr.setRequestHeader('Content-Type', file.type);
xhr.send(file);
גישה זו מורידה עומס מהשרת ומגבירה את מהירות ההעלאה. צור קשר כדי לדון בפרטי הפרויקט שלך.
התהליך שלנו וזמנים
אנו פועלים לפי תהליך מובנה: איסוף נתונים → ביקורת/ניתוח → עיצוב → הערכת עלות → פיתוח → בדיקות → השקה. העורך המינימלי (ציר זמן, חיתוך, תצוגה מקדימה, ייצוא דרך Remotion) לוקח 13–17 ימי עסקים. פונקציונליות מלאה עם אפקטים, מעברים ומיקסר אודיו לוקחת 20–25 ימים. העלות הסופית נקבעת לאחר בריף.
מה כלול בתוצאה:
- ארכיטקטורה וקוד פרויקט עם תיעוד API.
- הוראות פריסה (Docker Compose, .env).
- גישה למאגר ו-CI/CD.
- אחריות ל-12 חודשים על הקוד ותמיכה במהלך היישום.
הנה פירוט שלבי הפיתוח:
| שלב | זמן |
|---|---|
| ציר זמן (גרירה, חיתוך, פיצול) | 4–5 ימים |
| תצוגה מקדימה (סנכרון וידאו) | 3–4 ימים |
| העלאת נכסים (S3 presigned) | יום אחד |
| שכבות טקסט, תמונות | 2–3 ימים |
| עיבוד Remotion + סטטוס משימה | 3–4 ימים |
| אודיו (עוצמה, השתקה, דעיכה) | יומיים |
| אפקטים ופילטרים (בהירות, ניגודיות) | 2–3 ימים |
יש לנו ניסיון של 10+ שנים והשלמנו מעל 40 פרויקטים בפיתוח web, כולל מספר עורכים מקוונים. בקש פיתוח מותאם אישית—נתאים את עצמנו לכל הדרישות שלך.







