חדשות על המוצר

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

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

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

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

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

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

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

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

הטעינה מראש באה לעזרה! 🦸‍♀️

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

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

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

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

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

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

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

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

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

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

אחרי שמביעים הסכמה, אפשר להשבית שוב את הטעינה מראש של הפלייליסט באמצעות PreloadConfiguration.DEFAULT:

player.preloadConfiguration = PreloadConfiguration.DEFAULT

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

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

הגדרת PreloadManager

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

ה-builder של 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 עדיין לא נטען מראש או נוסף ל-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. Keep the current index in sync with the UI

הפיד או הרשימה שלנו יכולים להיות דינמיים, ולכן חשוב להודיע ל-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

המאמר הבא

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

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

רוצים לשלוח לנו משוב? נשמח לשמוע ממך.

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

כותב/ת המסמך:
להמשך קריאה