חדשות על מוצרים

שיפור ההפעלה של מדיה: הכירו את טעינה מראש באמצעות Media3 – חלק 1

משך הקריאה: 8 דקות
Mayuri Khinvasara Khabya
מהנדס קשרי מפתחים

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

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

היתרונות העיקריים של טעינה מראש:

  • 🚀 הפעלה מהירה יותר: הסרטונים כבר מוכנים להפעלה, כך שהמעברים בין הפריטים מהירים יותר וההפעלה מתחילה באופן מיידי.
  • 📉 פחות באפרינג: טעינת נתונים מראש מפחיתה את הסיכוי שההפעלה תיעצר, למשל בגלל בעיות ברשת.
  • ✨ חוויית משתמש חלקה יותר: השילוב של הפעלה מהירה יותר ופחות באפרינג יוצר אינטראקציה חלקה יותר שמשתמשים יכולים ליהנות ממנה.

בסדרת המאמרים הזו, שמחולקת לשלושה חלקים, נציג את כלי השירות המתקדמים של Media3 לטעינה (מראש) של רכיבים, ונתעמק בהם.

  • בחלק הראשון נסביר את היסודות: נבין את האסטרטגיות השונות של טעינה מראש שזמינות ב-Media3, נפעיל את PreloadConfiguration ונגדיר את DefaultPreloadManager, כדי לאפשר לאפליקציה לטעון מראש פריטים. בסוף הפוסט הזה בבלוג, תוכלו לטעון מראש ולהפעיל פריטי מדיה עם הדירוג והמשך שהגדרתם.
  • בחלק 2 נתעמק בנושאים מתקדמים יותר של DefaultPreloadManager: שימוש ב-listeners לצורך ניתוח נתונים, שיטות מומלצות שמוכנות לשימוש בסביבת ייצור כמו התבנית sliding window ורכיבים משותפים בהתאמה אישית של DefaultPreloadManager ו-ExoPlayer.
  • בחלק 3, נתעמק בנושא של שמירת נתונים במטמון בדיסק באמצעות DefaultPreloadManager.

טעינה מראש היא הפתרון! 🦸‍♀️

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

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

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

ב-Media3 יש שני ממשקי API עיקריים לטעינה מראש, שכל אחד מהם מתאים לתרחישי שימוש שונים. השלב הראשון הוא לבחור את ה-API הנכון.

1. טעינה מראש של פריטים בפלייליסט באמצעות PreloadConfiguration

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

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

אם אתם עדיין לא בטוחים אם אתם צריכים טעינה מראש, כדאי לנסות את ה-API הזה – הוא לא דורש הרבה מאמץ.

player.preloadConfiguration =
    PreloadConfiguration(/* targetPreloadDurationUs= */ 5_000_000L)

עם PreloadConfiguration שלמעלה, הנגן מנסה לטעון מראש חמש שניות של מדיה עבור הפריט הבא בפלייליסט.

אחרי שמפעילים את ההגדרה, אפשר להשבית אותה שוב באמצעות PreloadConfiguration.DEFAULT:

player.preloadConfiguration = PreloadConfiguration.DEFAULT

2. טעינה מראש של רשימות דינמיות באמצעות PreloadManager

בממשקי משתמש דינמיים כמו פידים אנכיים או קרוסלות, שבהם הפריט הבא נקבע לפי אינטראקציית המשתמש, מתאים להשתמש ב-PreloadManager API. זהו רכיב חדש ועוצמתי ועצמאי בספריית Media3 ExoPlayer, שנועד במיוחד לטעינה מראש באופן יזום. הוא מנהל אוסף של MediaSources פוטנציאליים, נותן להם עדיפות על סמך הקרבה למיקום הנוכחי של המשתמש ומציע שליטה מדויקת על התוכן שנטען מראש, ומתאים לתרחישים מורכבים כמו פידים דינמיים של סרטונים קצרים.

הגדרת PreloadManager

DefaultPreloadManager הוא ההטמעה הקנונית של PreloadManager.

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

val preloadManagerBuilder =
DefaultPreloadManager.Builder(context, targetPreloadStatusControl)
val preloadManager = val preloadManagerBuilder.build()

// Build ExoPlayer with DefaultPreloadManager.Builder
val player = preloadManagerBuilder.buildExoPlayer()

חובה להשתמש באותו builder גם ב-ExoPlayer וגם ב-DefaultPreloadManager, כדי לוודא שהרכיבים שמתחת לפני השטח משותפים בצורה נכונה.

זהו, סיימתם. עכשיו יש לכם מנהל שמוכן לקבל הוראות.

הגדרה של משך ודירוג באמצעות TargetPreloadStatusControl

מה קורה אם רוצים לבצע טעינה מקדימה של סרטון למשך 10 שניות, למשל? אתם יכולים לציין את המיקום של פריטי המדיה בקרוסלה, והמחלקה DefaultPreloadManager נותנת עדיפות לטעינת הפריטים לפי המרחק שלהם מהפריט שהמשתמש מפעיל כרגע.

אם רוצים לשלוט במשך הטעינה מראש של הפריט, אפשר להגדיר זאת באמצעות הערך שמוחזר על ידי DefaultPreloadManager.PreloadStatus.

לדוגמה,

  • פריט 'א' הוא בעדיפות הכי גבוהה, צריך לטעון 5 שניות של סרטון.
  • הפריט 'B' הוא בעדיפות בינונית, אבל כשמגיעים אליו, צריך לטעון 3 שניות של סרטון.
  • פריט C הוא בעדיפות נמוכה יותר, והטעינה מתבצעת רק למעקב.
  • פריט 'ד' הוא בעדיפות נמוכה עוד יותר, רק צריך להתכונן.
  • כל הפריטים האחרים רחוקים, אל תטען מראש שום דבר.

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

import androidx.media3.exoplayer.DefaultPreloadManager.PreloadStatus


class MyTargetPreloadStatusControl(
    currentPlayingIndex: Int = C.INDEX_UNSET
) : TargetPreloadStatusControl<Int,PreloadStatus> {


    // The app is responsible for updating this based on UI state
    override fun getTargetPreloadStatus(index: Int): PreloadStatus? {

        val distance = index - currentPlayingIndex

        // Adjacent items (Next): preload 5 seconds
        if (distance == 1) { 
        // Return a PreloadStatus that is labelled by STAGE_SPECIFIED_RANGE_LOADED and suggest loading // 5000ms from the default start position
                    return PreloadStatus.specifiedRangeLoaded(5000L)
                } 

        // Adjacent items (Previous): preload 3 seconds
        else if (distance == -1) { 
        // Return a PreloadStatus that is labelled by STAGE_SPECIFIED_RANGE_LOADED //and suggest loading 3000ms from the default start position
                    return PreloadStatus.specifiedRangeLoaded(3000L)
                } 

        // Items two positions away: just select tracks
        else if (distance) == 2) {
        // Return a PreloadStatus that is labelled by STAGE_TRACKS_SELECTED
                    return PreloadStatus.TRACKS_SELECTED
                } 

        // Items four positions away: just select prepare
        else if (abs(distance) <= 4) {
        // Return a PreloadStatus that is labelled by STAGE_SOURCE_PREPARED
                    return PreloadStatus.SOURCE_PREPARED
                }

             // All other items are too far away
             return null
            }
}

כדאי לדעת: PreloadManager יכול לשמור בטעינה מראש גם את הפריטים הקודמים וגם את הפריטים הבאים, בעוד ש-PreloadConfiguration יטען מראש רק את הפריטים הבאים.

ניהול פריטים לטעינה מראש

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

1. הוספת קובצי מדיה

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

val initialMediaItems = pullMediaItemsFromService(/* count= */ 20)
for (index in 0 until initialMediaItems.size) {
    preloadManager.add(
        initialMediaItems.get(index),index)
    )
}

חשבון הניהול יתחיל עכשיו לאחזר נתונים עבור MediaItem ברקע.

אחרי ההוספה, אומרים למנהל להעריך מחדש את הרשימה החדשה (רומזים שמשהו השתנה, כמו הוספה או הסרה של פריט, או שהמשתמש עבר להפעיל פריט חדש).

preloadManager.invalidate()

2. אחזור והפעלה של פריט

עכשיו מגיע הלוגיקה העיקרית של ההפעלה. כשהמשתמש מחליט להפעיל את הסרטון הזה, לא צריך ליצור MediaSource חדש. במקום זאת, מבקשים מ-PreloadManager את מה שהוא כבר הכין. אפשר לאחזר את MediaSource מ-Preload Manager באמצעות MediaItem.

אם הפריט שאוחזר מ-PreloadManager הוא null, המשמעות היא שעדיין לא בוצעה טעינה מראש של mediaItem או שהוא לא נוסף ל-PreloadMamager, ולכן בוחרים להגדיר את mediaItem ישירות.

// When a media item is about to displ​​ay on the screen
val mediaSource = preloadManager.getMediaSource(mediaItem)
if (mediaSource!= null) {
  player.setMediaSource(mediaSource)
} else {
  // If mediaSource is null, that mediaItem hasn't been added yet.
  // So, send it directly to the player.
  player.setMediaItem(mediaItem)
}
player.prepare()
// When the media item is displaying at the center of the screen
player.play()

הכנת MediaSource שאוחזר מ-PreloadManager מאפשרת מעבר חלק מטעינה מראש להפעלה, באמצעות הנתונים שכבר נמצאים בזיכרון. כך זמן ההתחלה מתקצר.

3. שמירה על סנכרון המדד הנוכחי עם ממשק המשתמש

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

preloadManager.setCurrentPlayingIndex(currentIndex)
// Need to call invalidate() to update the priorities
preloadManager.invalidate()

4. הסרת פריט

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

// When an item is too far from the current playing index
preloadManager.remove(mediaItem)

אם אתם צריכים למחוק את כל הפריטים בבת אחת, אתם יכולים להתקשר למספר preloadManager.reset().

5. ביטול הקישור של חשבון הניהול

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

// In your Activity's onDestroy() or Composable's onDispose
preloadManager.release()

זמן ההדגמה

רוצים לראות את זה בפעולה? 👍

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

Demo-PreloadManager_2.webp

מה הלאה?

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

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

רוצה לשתף משוב? נשמח לשמוע ממך.

בקרוב נפרסם עוד מידע בנושא. בינתיים, כדאי לנסות להאיץ את האפליקציה שלכם. 🚀

נכתב על ידי:

להמשך הקריאה