למה תצוגת הווידאו המקדימה שלך משקרת לגבי הייצוא
עורך הווידאו שלך נראה גמור. התצוגה המקדימה חדה, הכיתוב יושב בדיוק היכן שהמשתמש גרר אותו, וציר הזמן גולל בצורה חלקה. ואז המשתמש לוחץ על Export, והכיתוב חוזר למקום הלא נכון, בגודל הלא נכון, לעיתים נחתך לגמרי מהקצה. שום דבר לא קרס, אין שגיאה ב-log – התצוגה המקדימה והקובץ פשוט לא מסכימים.
זוהי השגיאה הנפוצה ביותר בכל עורך וידאו, והיא בעיית מידול נתונים ולא בעיית רינדור. להלן ההרגל היחיד שהופך את השגיאה לבלתי אפשרית, כיצד לשנות את גודל הפריימים נכון מיחס גובה-רוחב (aspect ratio), וכיצד לשמור על גרירה מהירה בטלפונים זולים. הרעיונות תקפים בכל שפה או UI framework.
Aspect Ratio הוא צורה, Resolution הוא גודל
רוב שגיאות הפריימים מתחילות כאן. Aspect ratio מתאר את צורת הפריימ, ולא שום דבר אחר: 9:16 הוא גבוה, 16:9 הוא רחב. Resolution מתאר את הגודל בפיקסלים, כמו 1080x1920. צורה אחת תומכת בגדלים רבים, ולכן שני הערכים אינם ניתנים להחלפה.
| יחס גובה-רוחב (Aspect ratio) | ערך מספרי | שימוש אופייני |
| 16:9 | 1.78 | YouTube, ווב במצב רוחב |
| 9:16 | 0.56 | Reels, TikTok, Shorts |
| 1:1 | 1.00 | פוסטים מרובעים בפיד |
| 4:5 | 0.80 | פוסטים אנכיים בפיד |
אחסן את ה-ratio כטקסט רגיל והמר אותו למספר רק כשאתה מבצע חישובים. אחסן גובה יעד ל-export, ולאחר מכן גזור את הרוחב מה-ratio, וכפה על שני המספרים להיות זוגיים לפני שהם מגיעים למקודד. מימדים אי-זוגיים הם הגורם השקט למספר מפתיע של ייצואים כושלים.
height = width / ratioToNumber(ratio) // קביעת גודל תיבת התצוגה המקדימה
function widthFromHeight(ratio, height): raw = height * ratioToNumber(ratio) return makeEven(round(raw)) // מקודדים דורשים צדדים זוגיים
// 9:16 at 1080 tall -> 608 x 1080// 16:9 at 1080 tall -> 1920 x 1080
אחסן מיקומים כשברים, לעולם לא כפיקסלים
עורך חי בשלושה עולמות בגדלים שונים: המסמך השמור, התצוגה המקדימה על המסך, והקובץ המיוצא. המסמך הוא מקור האמת היחיד – שני האחרים הם רק רינדורים שלו בקנה מידה שונה.
Project -> Clip (מקור, חיתוך, פילטרים) -> Overlay (טקסט / מדבקה)
מסמך מנגן תצוגה מקדימה מנגן ייצוא x = 0.5, y = 0.9 --> x * 360 px --> x * 1080 px --> MP4 | ^ ^ +------------------------+------------------------+ שבר אחד מאוחסן, נוסחה אחת, שני גדלים
אם אתה מאחסן 540 פיקסלים, המספר הזה נכון רק במכשיר שבו הוא נמדד. אם אתה מאחסן 0.5, זה אומר "המרכז האופקי" בכל גודל, תמיד. כל position, offset ו-scale במודל צריכים להיות שבר בין 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 // מתי נעלם}
ה-Export הופך אז כמעט משעמם, וזו כל הנקודה. ה-renderer משתמש באותה נוסחה כמו התצוגה המקדימה עם מכפיל גדול יותר: 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
צייר את התצוגה המקדימה במעבר יחיד
מדוד את תיבת התצוגה המקדימה בזמן ריצה במקום לקודד אותה "קשה" (hard-coding), מכיוון שטלפון, טאבלט וחלון שולחן עבודה בגודל שונה כולם נותנים לך גודל אחר. צייר כל overlay במעבר canvas אחד במקום להרכיב כל אחד כאלמנט UI נפרד – מעבר יחיד נשאר חלק בזמן שאצבע זזה.
שני כללים שומרים על הלולאה עובדת נכון. דלג על כל overlay שנמצא מחוץ לטווח הזמן שלו, ותמיד צמד save() עם restore() כך שפריט אחד לא יוכל "לדלוף" את הטרנספורמציה שלו לפריט הבא.
function drawPreview(canvas, overlays, currentTime, box): for each overlay in overlays: if currentTime < overlay.startTimeMs: skip // דלג if currentTime > overlay.endTimeMs: skip // דלג
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() // לעולם לא אופציונלי
שמור על גרירה חלקה עם מצב דו-שכבתי (Two-Tier State)
אצבע גוררת מדווחת על כשישים מיקומים בשנייה. אם כל אחד מהם נכתב למאגר הפרויקט שלך, אתה משלם על ולידציה (validation), עמידות (persistence) ובנייה מחדש מלאה של המצב שישים פעמים בשנייה, והגרירה מגמגמת באופן נראה לעין. במקום זאת, פצל את העבודה לשתי שכבות:
- שכבה 1, ארעית (ephemeral): מפת
liveDragקטנה המחזיקה את מיקום האצבע כרגע. עדכן אותה בכל תזוזה וצייר מחדש את ה-canvas. שום דבר אחר לא רץ. - שכבה 2, עמידה (durable): בסיום הגרירה, בצע commit לשבר הסופי ל-overlay במודל הפרויקט, נקה את ערך ה-
liveDrag, ותעד צעדundoאחד.
התמורה חורגת מקצב הפריימים. היסטוריית ה-undo נשארת שימושית מכיוון שגרירה מייצרת ערך אחד במקום מאות, וה-autosave מפסיק להעמיס על הדיסק. כך Figma ו-Canva שומרות על תגובתיות המניפולציה הישירה.
הקשר מהעולם האמיתי
ב- MicrocosmWorks, קיבלנו עורך וידאו קצר טווח שהכיתובים בו זזו ב-export. הצוות שמר את מיקומי ה-overlay כפיקסלים של המכשיר שנקראו ישירות מ-canvas התצוגה המקדימה, כך שכיתוב שנוצר בטלפון קטן הופיע גבוה וקטן בקובץ ה-1080p, והחלפת aspect ratio הוציאה חלק מה-overlays מחוץ לפריים לחלוטין.
העברנו את המודל לקואורדינטות מנורמלות בין 0 ל-1, גרמנו לשני ה-renderers לחלוק עוזר (helper) יחיד של 'שבר כפול גודל', ואכפנו מימדי פלט זוגיים הנגזרים מה-ratio המאוחסן. תצוגה מקדימה ו-export תאמו בכל מכשיר בדיקה. העברת ה-commit של הגרירה לסיום הגרירה (drag-end) הסירה את הלאג (lag) שמשתמשים דיווחו עליו בחומרת Android נמוכה יותר. אתם יכולים לראות את סוג עבודות עריכת הווידאו הקצר שממנו צמח הדבר ב-תיק העבודות שלנו.
מסקנה
תצוגה מקדימה ו-export הם שני רינדורים של מסמך אחד, אז תנו להם מקור אמת אחד. אחסנו מיקומים כשברים, השתמשו באותה נוסחה של 'שבר כפול גודל' בשני ה-renderers, גזרו רוחב מ-aspect ratio, שמרו על מימדים זוגיים, ובצעו commit לשינויי גרירה רק כאשר האצבע מורמת. חמשת ההרגלים הללו מסירים קטגוריה שלמה של באגים לפני שהם נכתבים.
בניית מנועי מדיה שבהם התצוגה האינטראקטיבית והקובץ הסופי תואמים היא מוקד מרכזי בעבודת הנדסת וידאו וסטרימינג שלנו ב-MicrocosmWorks.
משיקים עורך וידאו או תוכן ורוצים שהתצוגה המקדימה וה-export יתאמו באמת? פתרנו את סוג הבאג הספציפי הזה בכלי עריכה קצרים. דברו עם צוות ההנדסה שלנו ←
קראו עוד מהצוות שלנו
1. אופטימיזציה של לוגו ערוץ עבור רזולוציות וידאו שונות
2. צלם צלחת, רשום ארוחה: צינור תזונה מבוסס ראייה ממוחשבת (Computer-Vision)

