קצב מסגרות

ה-API של קצב הפריימים מאפשר לאפליקציות להודיע לפלטפורמת Android על קצב הפריימים המיועד שלהן, והוא זמין באפליקציות שמטרגטות את Android 11 (רמת API 30) ואילך. בעבר, רוב המכשירים תמכו רק בקצב רענון מסך אחד, בדרך כלל 60Hz, אבל המצב הזה השתנה. מכשירים רבים תומכים עכשיו בקצבי רענון נוספים כמו 90Hz או 120Hz. חלק מהמכשירים תומכים במעברים חלקים בין קצב רענון שונה, בעוד שבמכשירים אחרים מוצג מסך שחור לזמן קצר, בדרך כלל למשך שנייה.

המטרה העיקרית של ה-API היא לאפשר לאפליקציות לנצל בצורה טובה יותר את כל שיעורי הרענון הנתמכים של המסך. לדוגמה, אפליקציה שמפעילה וידאו ב-24Hz שקוראת ל-setFrameRate() עשויה לשנות את קצב הרענון של המסך מ-60Hz ל-120Hz. קצב הרענון החדש מאפשר הפעלה חלקה ללא רעידות של סרטונים באיכות 24Hz, בלי צורך ב-3:2 pulldown, כפי שנדרש להפעלת אותו סרטון במסך 60Hz. כך חוויית המשתמש משתפרת.

שימוש בסיסי

ב-Android יש כמה דרכים לגשת למשטחים ולשלוט בהם, ולכן יש כמה גרסאות של ה-API‏ setFrameRate(). כל גרסה של ה-API מקבלת את אותם פרמטרים ופועלת באותו אופן כמו שאר הגרסאות:

האפליקציה לא צריכה לקחת בחשבון את קצבי הרענון הנתמכים של המסך, שאפשר לקבל באמצעות קריאה ל-Display.getSupportedModes(), כדי לבצע קריאה בטוחה ל-setFrameRate(). לדוגמה, גם אם המכשיר תומך רק ב-60Hz, צריך להפעיל את setFrameRate() עם קצב הפריימים המועדף על האפליקציה. במכשירים שלא מתאים להם קצב רענון טוב יותר של המסך לקצב הפריימים של האפליקציה, קצב הרענון של המסך לא ישתנה.

כדי לבדוק אם קריאה ל-setFrameRate() גורמת לשינוי בקצב הרענון של המסך, צריך להירשם לקבלת התראות על שינויים במסך באמצעות קריאה למספר DisplayManager.registerDisplayListener() או AChoreographer_registerRefreshRateCallback().

כשקוראים לפונקציה setFrameRate(), מומלץ להעביר את קצב הפריימים המדויק במקום לעגל למספר שלם. לדוגמה, כשמריצים רינדור של סרטון שצולם ב-29.97Hz, צריך להעביר את הערך 29.97 ולא לעגל ל-30.

באפליקציות וידאו, צריך להגדיר את פרמטר התאימות שמוענק ל-setFrameRate() לערך Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE כדי לספק רמז נוסף לפלטפורמת Android שהאפליקציה תשתמש בתפריט הנפתח כדי להתאים לקצב רענון המסך שאינו תואם (מה שעלול לגרום לתנודות).

בתרחישים מסוימים, הצגת הפריימים תפסיק לשטח הסרטון, אבל היא תישאר גלויה במסך למשך זמן מה. תרחישים נפוצים כוללים מקרים שבהם ההפעלה מגיעה לסוף הסרטון או מקרים שבהם המשתמש משהה את ההפעלה. במקרים כאלה, צריך להפעיל את setFrameRate() עם הפרמטר של קצב הפריימים מוגדר ל-0 כדי למחוק את ההגדרה של קצב הפריימים של ה-Surface ולהחזיר אותה לערך ברירת המחדל. אין צורך לנקות את הגדרת קצב הפריימים באופן הזה כשמשמידים את פני השטח, או כשפני השטח מוסתרים כי המשתמש עובר לאפליקציה אחרת. צריך לנקות את הגדרת קצב הפריימים רק כשפני השטח נותרים גלויים בלי שימוש בהם.

מעבר לא חלק בין קצב פריימים

במכשירים מסוימים, כשמחליפים קצב רענון עלולות להיות הפרעות חזותיות כמו מסך שחור למשך שנייה או שתיים. התרחיש הזה קורה בדרך כלל בממירים, במסכי טלוויזיה ובמכשירים דומים. כברירת מחדל, מסגרת Android לא עוברת בין המצבים כשקוראים לממשק ה-API Surface.setFrameRate() כדי למנוע הפרעות חזותיות כאלה.

חלק מהמשתמשים מעדיפים הפרעה חזותית בתחילת הסרטון ובסופו, במיוחד בסרטונים ארוכים. כך קצב הרענון של המסך תואם לקצב הפריימים של הסרטון, ונמנע מיצירת ארטיפקטים של המרה של קצב הפריימים כמו רטט של 3:2 עם רטט עבור הפעלת סרט.

לכן, אפשר להפעיל את המעבר בין שיעורי רענון לא חלקים רק אם גם המשתמש וגם האפליקציות מביעים הסכמה:

מומלץ תמיד להשתמש ב-CHANGE_FRAME_RATE_ALWAYS בסרטונים ארוכים כמו סרטים. הסיבה לכך היא שהיתרון של התאמת קצב הפריימים של הסרטון גובר על ההפרעה שמתרחשת כשמשנים את קצב הרענון.

המלצות נוספות

מומלץ לפעול לפי ההמלצות הבאות בתרחישים נפוצים.

מספר משטחים

פלטפורמת Android תוכננה לטפל בצורה נכונה בתרחישים שבהם יש כמה משטחים עם הגדרות שונות של קצב פריימים. אם באפליקציה יש כמה משטחים עם שיעורי פריימים שונים, צריך להפעיל את setFrameRate() עם שיעור הפריימים הנכון לכל משטח. גם אם המכשיר מפעיל מספר אפליקציות בו-זמנית, באמצעות מסך מפוצל או מצב 'תמונה בתוך תמונה', כל אפליקציה יכולה לקרוא ל-setFrameRate() בבטחה לפלטפורמות שלה.

הפלטפורמה לא משתנה לקצב הפריימים של האפליקציה

גם אם המכשיר תומך בקצב הפריימים שהאפליקציה מציינת בקריאה ל-setFrameRate(), יש מקרים שבהם המכשיר לא יעביר את המסך לקצב הרענון הזה. לדוגמה, יכול להיות שהגדרת קצב פריימים שונה למשטח עם עדיפות גבוהה יותר או שהמכשיר נמצא במצב 'חיסכון בסוללה' (הגדרת הגבלה על קצב הרענון של המסך כדי לחסוך בצריכת הסוללה). האפליקציה עדיין צריכה לפעול בצורה תקינה כשהמכשיר לא מחליף את קצב הרענון של המסך להגדרת קצב הפריימים של האפליקציה, גם אם המכשיר עושה זאת בנסיבות רגילות.

האפליקציה יכולה להחליט איך להגיב כשקצב הרענון של המסך לא תואם לקצב הפריימים של האפליקציה. בסרטון, קצב הפריימים קבוע וקבוע לפי קצב הפריימים של סרטון המקור, ויהיה צורך בתנועה למטה כדי להציג את תוכן הסרטון. במקום זאת, המשחק עשוי לנסות לפעול בקצב הרענון של המסך במקום להישאר בקצב הפריימים המועדף עליו. האפליקציה לא צריכה לשנות את הערך שהיא מעבירה ל-setFrameRate() על סמך מה שהפלטפורמה עושה. קצב הפריימים צריך להישאר מוגדר לקצב הפריימים המועדף של האפליקציה, בלי קשר לאופן שבו האפליקציה מטפלת במקרים שבהם הפלטפורמה לא משתנה בהתאם לבקשה של האפליקציה. כך, אם התנאים במכשיר ישתנו ויאפשרו להשתמש בקצבי רענון נוספים של המסך, בפלטפורמה יהיה המידע הנכון כדי לעבור לקצב הפריימים המועדף של האפליקציה.

במקרים שבהם האפליקציה לא פועלת או לא פועלת בקצב הרענון של התצוגה, האפליקציה צריכה לציין חותמות זמן של המצגת לכל פריים באמצעות אחד מהמנגנונים של הפלטפורמה להגדרת חותמות זמן של המצגת:

השימוש בחותמות הזמן האלה מונע מהפלטפורמה להציג פריים של האפליקציה מוקדם מדי, מה שעלול לגרום לתנודות מיותרות. השימוש הנכון בחותמות זמן של הצגת הפריימים הוא קצת מסובך. למידע נוסף על מניעת רעידות, כדאי לעיין במדריך לקצב פריימים, ומומלץ להשתמש בספריית Android Frame Pacing.

במקרים מסוימים, הפלטפורמה עשויה לעבור למכפלה של קצב הפריימים באפליקציה שצוינה ב-setFrameRate(). לדוגמה, אפליקציה עשויה להפעיל את setFrameRate() עם 60Hz, והמכשיר עשוי להעביר את התצוגה ל-120Hz. אחת מהסיבות לכך היא שאפליקציה אחרת כוללת משטח עם הגדרת קצב פריימים של 24Hz. במקרה כזה, הפעלת המסך ב-120Hz תאפשר להפעיל גם את פני השטח של 60Hz וגם את פני השטח של 24Hz בלי צורך ב-pulldown.

כשהתצוגה פועלת במכפיל של קצב הפריימים של האפליקציה, האפליקציה צריכה לציין חותמות זמן של הצגה לכל פריים כדי למנוע רעידות מיותרות. במשחקים, הספרייה של Android Frame Pacing עוזרת להגדיר בצורה נכונה חותמות זמן של הצגת פריימים.

setFrameRate()‎ לעומת preferredDisplayModeId

WindowManager.LayoutParams.preferredDisplayModeId היא דרך נוספת שבה אפליקציות יכולות לציין את קצב הפריימים שלהן לפלטפורמה. באפליקציות מסוימות רוצים לשנות רק את קצב הרענון של המסך, ולא לשנות הגדרות אחרות של מצב התצוגה, כמו רזולוציית המסך. באופן כללי, כדאי להשתמש ב-setFrameRate() במקום ב-preferredDisplayModeId. קל יותר להשתמש בפונקציה setFrameRate() כי האפליקציה לא צריכה לחפש ברשימת מצבי התצוגה כדי למצוא מצב עם קצב פריימים ספציפי.

באמצעות setFrameRate() יש לפלטפורמה יותר הזדמנויות לבחור קצב פריימים תואם בתרחישים שבהם יש מספר פלטפורמות שפועלות בקצב פריימים שונה. לדוגמה, נניח ששתי אפליקציות פועלות במצב מסך מפוצל ב-Pixel 4, כאשר באחת מהן מוצג סרטון באיכות 24Hz ובשנייה מוצגת רשימה שאפשר לגלול בה. Pixel 4 תומך בשני קצב רענון של התצוגה: 60Hz ו-90Hz. באמצעות ה-API של preferredDisplayModeId, שטח התצוגה של הסרטון מאלץ לבחור בין 60Hz ל-90Hz. כשמפעילים את setFrameRate() עם 24Hz, פלטפורמת הווידאו מספקת לפלטפורמה מידע נוסף על קצב הפריימים של סרטון המקור, ומאפשרת לפלטפורמה לבחור 90Hz כקצב הרענון של התצוגה, שהוא טוב יותר מ-60Hz בתרחיש הזה.

עם זאת, יש תרחישים שבהם צריך להשתמש ב-preferredDisplayModeId במקום ב-setFrameRate(), למשל:

  • אם האפליקציה רוצה לשנות את הרזולוציה או הגדרות אחרות של מצב התצוגה, צריך להשתמש ב-preferredDisplayModeId.
  • הפלטפורמה תשנה את מצבי התצוגה בתגובה לקריאה ל-setFrameRate() רק אם מתג המצב קל ולא סביר שהמשתמשים יראו אותו. אם האפליקציה מעדיפה לשנות את קצב הרענון של המסך גם אם נדרש מעבר למצב כבד (למשל במכשיר Android TV), צריך להשתמש ב-preferredDisplayModeId.
  • באפליקציות שלא עובדות בקצב פריימים כפול מקצב הפריימים של האפליקציה, שמחייבת הגדרה של חותמות זמן להצגתן בכל פריים, צריך להשתמש ב-preferredDisplayModeId.

setFrameRate() לעומת preferredRefreshRate

WindowManager.LayoutParams#preferredRefreshRate מגדיר קצב פריימים מועדף בחלון של האפליקציה, והקצב רלוונטי לכל הפלטפורמות בחלון. האפליקציה צריכה לציין את קצב הפריימים המועדף עליה, ללא קשר לקצבי הרענון הנתמכים במכשיר, בדומה ל-setFrameRate(), כדי לתת לתזמון רמז טוב יותר לגבי קצב הפריימים המיועד של האפליקציה.

המערכת מתעלמת מ-preferredRefreshRate ב-Surfaces שמשתמשים ב-setFrameRate(). באופן כללי, מומלץ להשתמש ב-setFrameRate() אם אפשר.

preferredRefreshRate לעומת preferredDisplayModeId

אם אפליקציות רוצות לשנות רק את קצב הרענון המועדף, עדיף להשתמש ב-preferredRefreshRate ולא ב-preferredDisplayModeId.

הימנעות משימוש בתדירות גבוהה מדי ב-setFrameRate()

הקריאה setFrameRate() לא יקרה מאוד מבחינת הביצועים, אבל כדאי להימנע משליחת קריאה ל-setFrameRate() בכל פריים או כמה פעמים בשנייה. סביר להניח שקריאות ל-setFrameRate() יגרמו לשינוי בקצב הרענון של המסך, מה שעלול לגרום לירידה בפריים במהלך המעבר. צריך לבדוק מראש את קצב הפריימים הנכון ולהפעיל את setFrameRate() פעם אחת.

שימוש במשחקים או באפליקציות אחרות שאינן וידאו

למרות שסרטון הוא התרחיש לדוגמה העיקרי של ה-API של setFrameRate(), אפשר להשתמש בו גם באפליקציות אחרות. לדוגמה, משחק שאמור לפעול במהירות של עד 60Hz (כדי לצמצם את צריכת החשמל ולהאריך את סשני המשחק) יכול להפעיל את Surface.setFrameRate(60, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT). כך, מכשיר שפועל במהירות 90Hz כברירת מחדל יפעל במהירות 60Hz בזמן שהמשחק פעיל, וכך ימנע את התנודות שעלולות להתרחש אם המשחק יפעל במהירות 60Hz בזמן שהמסך יפעל במהירות 90Hz.

שימוש ב-FRAME_RATE_COMPATIBILITY_FIXED_SOURCE

FRAME_RATE_COMPATIBILITY_FIXED_SOURCE מיועד רק לאפליקציות וידאו. לשימוש שאינו בסרטונים, משתמשים ב-FRAME_RATE_COMPATIBILITY_DEFAULT.

בחירת אסטרטגיה לשינוי של קצב הפריימים

  • מומלץ מאוד שאפליקציות שיציגו סרטונים ארוכים כמו סרטים, יקראו לפונקציה setFrameRate(fps, FRAME_RATE_COMPATIBILITY_FIXED_SOURCE, CHANGE_FRAME_RATE_ALWAYS), כאשר fps הוא קצב הפריימים של הסרטון.
  • אנחנו לא ממליצים לאפליקציות לבצע קריאה ל-setFrameRate() עם CHANGE_FRAME_RATE_ALWAYS כשצפוי שהפעלת הסרטון תימשך כמה דקות או פחות.

דוגמה לשילוב באפליקציות להפעלת סרטונים

אנחנו ממליצים לבצע את השלבים הבאים כדי לשלב מתגים של קצב רענון באפליקציות להפעלת וידאו:

  1. קובעים את changeFrameRateStrategy:
    1. אם מפעילים סרטון ארוך, כמו סרט, משתמשים ב-MATCH_CONTENT_FRAMERATE_ALWAYS
    2. אם מפעילים סרטון קצר, כמו טריילר של סרט, משתמשים ב-CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS
  2. אם הערך של changeFrameRateStrategy הוא CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS, עוברים לשלב 4.
  3. כדי לזהות אם עומדת להתרחש החלפה לא חלקה של קצב הרענון, צריך לוודא ששני העובדות הבאות מתקיימות:
    1. לא ניתן להחליף מצב בצורה חלקה מקצב הרענון הנוכחי (נקרא לזה C) לקצב הפריימים של הסרטון (נקרא לו V). המצב הזה יקרה אם C ו-V שונים ו-Display.getMode().getAlternativeRefreshRates לא מכיל מכפלה של V.
    2. המשתמש הביע הסכמה לשינויים בקצב הרענון שלא מתבצעים בצורה חלקה. כדי לבדוק זאת, צריך לבדוק אם הפונקציה DisplayManager.getMatchContentFrameRateUserPreference מחזירה את הערך MATCH_CONTENT_FRAMERATE_ALWAYS.
  4. כדי שהמעבר יהיה חלק, צריך לבצע את הפעולות הבאות:
    1. קוראים לפונקציה setFrameRate ומעבירים אותה ל-fps, ל-FRAME_RATE_COMPATIBILITY_FIXED_SOURCE ול-changeFrameRateStrategy, כאשר fps הוא קצב הפריימים של הסרטון.
    2. הפעלת הסרטון
  5. אם עומד להתרחש שינוי לא חלק במצב, יש לבצע את הפעולות הבאות:
    1. הצגת UX כדי לשלוח הודעה למשתמש. שימו לב שמומלץ להטמיע למשתמשים דרך לסגור את חוויית המשתמש, ולדלג על העיכוב הנוסף בשלב 5. הסיבה לכך היא שהעיכוב המומלץ שלנו גדול מהנדרש במסכים עם זמני החלפה מהירים יותר.
    2. קוראים ל-setFrameRate ומעבירים אליו את הערכים fps,‏ FRAME_RATE_COMPATIBILITY_FIXED_SOURCE ו-CHANGE_FRAME_RATE_ALWAYS, כאשר fps הוא קצב הפריימים של הסרטון.
    3. ממתינים לקריאה החוזרת (callback) של onDisplayChanged.
    4. ממתינים 2 שניות עד שהמעבר בין המצבים יושלם.
    5. הפעלת הסרטון

הקוד המדומה בלבד שתומך במעבר חלק הוא:

SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
transaction.setFrameRate(surfaceControl,
    contentFrameRate,
    FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
    CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS);
transaction.apply();
beginPlayback();

הקוד המדומה שתומך במעבר חלק ובמעבר לא חלק כפי שמתואר למעלה הוא:

SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
if (isSeamlessSwitch(contentFrameRate)) {
  transaction.setFrameRate(surfaceControl,
      contentFrameRate,
      FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
      CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS);
  transaction.apply();
  beginPlayback();
} else if (displayManager.getMatchContentFrameRateUserPreference()
      == MATCH_CONTENT_FRAMERATE_ALWAYS) {
  showRefreshRateSwitchUI();
  sleep(shortDelaySoUserSeesUi);
  displayManager.registerDisplayListener();
  transaction.setFrameRate(surfaceControl,
      contentFrameRate,
      FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
      CHANGE_FRAME_RATE_ALWAYS);
  transaction.apply();
  waitForOnDisplayChanged();
  sleep(twoSeconds);
  hideRefreshRateSwitchUI();
  beginPlayback();
}