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

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

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

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

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

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

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

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

  • בחלק הראשון נסביר את היסודות: נבין את האסטרטגיות השונות של טעינה מראש שזמינות ב-Media3, נפעיל את PreloadConfiguration ונגדיר את DefaultPreloadManager, כדי לאפשר לאפליקציה לטעון מראש פריטים. בסוף הפוסט הזה בבלוג, תוכלו לטעון מראש ולהפעיל פריטי מדיה עם הדירוג והמשך שהגדרתם.
  • בחלק 2 נתעמק בנושאים מתקדמים יותר של DefaultPreloadManager: שימוש ב-listeners לצורך ניתוח נתונים, סקירת שיטות מומלצות שמוכנות לשימוש בסביבת ייצור כמו התבנית של חלון הזזה ורכיבים משותפים בהתאמה אישית של 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, שנועד במיוחד לטעינה מראש באופן יזום. הוא מנהל אוסף של MediaSource פוטנציאליים, מתעדף אותם על סמך הקרבה למיקום הנוכחי של המשתמש ומציע שליטה גרנולרית על מה לטעון מראש, ומתאים לתרחישים מורכבים כמו פידים דינמיים של סרטונים קצרים.

הגדרת 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 שניות של סרטון.
  • הפריט 'ב' הוא בעדיפות בינונית, אבל כשמגיעים אליו, צריך לטעון 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 עדיין לא נטען מראש או לא נוסף ל-PreloadManager, ולכן בוחרים להגדיר את 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.

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

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

נכתב על ידי:

להמשך הקריאה