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

שיפור הפעלת המדיה: מידע מעמיק בנושא PreloadManager של Media3 – חלק 2

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

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

  • חלק 1: מבוא לטעינה מראש באמצעות Media3 כולל את היסודות. הסברנו את ההבדל בין PreloadConfiguration לפלייליסטים פשוטים לבין DefaultPreloadManager, שהוא כלי חזק יותר לממשקי משתמש דינמיים. למדתם איך להטמיע את מחזור החיים הבסיסי של ה-API: הוספת מדיה באמצעות add(), אחזור של MediaSource מוכן באמצעות getMediaSource(), ניהול עדיפויות באמצעות setCurrentPlayingIndex() ו-invalidate(), ושחרור משאבים באמצעות remove() ו-release().
  • חלק 2 (הפוסט הזה): בבלוג הזה נסביר על היכולות המתקדמות של DefaultPreloadManager. במאמר מוסבר איך להפיק תובנות באמצעות PreloadManagerListener, איך להטמיע שיטות מומלצות שמוכנות לייצור כמו שיתוף רכיבי ליבה עם ExoPlayer, ואיך לשלוט בדפוס החלון הנע כדי לנהל את הזיכרון בצורה יעילה.
  • חלק 3: בחלק האחרון של הסדרה הזו נסביר איך לשלב את PreloadManager עם מטמון דיסק קבוע, כדי שתוכלו לצמצם את צריכת הנתונים באמצעות ניהול משאבים ולספק חוויה חלקה.

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

האזנה: אחזור נתונים אנליטיים באמצעות PreloadManagerListener

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

ה-PreloadManagerListener מספק שני קריאות חוזרות חיוניות שנותנות תובנות חשובות לגבי תהליך הטעינה מראש והסטטוס שלו.

  • ‫onCompleted(MediaItem mediaItem): הקריאה החוזרת הזו מופעלת אחרי השלמה מוצלחת של בקשת טעינה מראש, כפי שהוגדרה על ידי TargetPreloadStatusControl.
  • ‫onError(PreloadException error): פונקציית הקריאה החוזרת הזו יכולה להיות שימושית לניפוי באגים ולמעקב. הפונקציה מופעלת כשמתרחשת שגיאה בטעינה מראש, והיא מספקת את החריגה המשויכת.

אפשר לרשום listener באמצעות הפעלת method אחת, כמו בדוגמת הקוד הבאה:

val preloadManagerListener = object : PreloadManagerListener {
    override fun onCompleted(mediaItem: MediaItem) {
        // Log success for analytics. 
        Log.d("PreloadAnalytics", "Preload completed for $mediaItem")
    }

    override fun onError( preloadError: PreloadException) {
        // Log the specific error for debugging and monitoring.
        Log.e("PreloadAnalytics", "Preload error ", preloadError)
    }
}

preloadManager.addListener(preloadManagerListener)

חילוץ תובנות מהמאזין 

אפשר לחבר את פונקציות ה-callback של המאזינים לצינור ניתוח הנתונים. העברת האירועים האלה למנוע לניתוח נתונים מאפשרת לכם לקבל תשובות לשאלות מרכזיות כמו:

  • מהו שיעור ההצלחה של הטעינה מראש? (היחס בין אירועי onCompleted לבין סך הניסיונות לטעינה מראש)
  • אילו רשתות CDN או פורמטים של סרטונים מציגים את שיעורי השגיאות הכי גבוהים? (על ידי ניתוח החריגים מ-onError)
  • מהו שיעור השגיאות בטעינה מראש? (היחס בין אירועי onError לבין סך הניסיונות לטעינה מראש)

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

מעבר לניפוי באגים: שימוש ב-onError לחזרה מסודרת לממשק משתמש

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

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

מערכת החברים: למה צריך לשתף רכיבים עם ExoPlayer?

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

הכלי DefaultPreloadManager.Builder נועד להקל על השיתוף הזה, ויש לו ממשקי API ליצירת מופע של PreloadManager ושל נגן מקושר. נבדוק למה צריך לשתף רכיבים כמו BandwidthMeter,‏ LoadControl,‏ TrackSelector ו-Looper. אפשר לעיין בייצוג החזותי של האינטראקציה בין הרכיבים האלה לבין הפעלת התוכן ב-ExoPlayer.

preloadManager2.png

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

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

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

preloadManagerBuilder.setBandwidthMeter(customBandwidthMeter)

שמירה על עקביות באמצעות הרכיבים LoadControl,‏ TrackSelector ו-Renderer המשותפים של ExoPlayer

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

הערה: השיתוף של LoadControl בגרסה האחרונה של Media3‏ (1.8) מבטיח שה-Allocator שלו יוכל להיות משותף בצורה נכונה עם PreloadManager ועם נגן. היכולת להשתמש ב-LoadControl כדי לשלוט ביעילות בטעינה מראש היא תכונה שתהיה זמינה בגרסה הקרובה של Media3 1.9.

preloadManagerBuilder.setLoadControl(customLoadControl)

  • ‫TrackSelector: הרכיב הזה אחראי לבחירת הטראקים (לדוגמה, סרטון ברזולוציה מסוימת, אודיו בשפה ספציפית) שיוטענו ויופעלו. השיתוף מבטיח שהטראקים שנבחרו במהלך הטעינה מראש הם אותם טראקים שהנגן ישתמש בהם. כך נמנע תרחיש בזבזני שבו מתבצעת טעינה מראש של טראק וידאו באיכות 480p, ומיד לאחר מכן הנגן מבטל את הטעינה ומביא טראק באיכות 720p להפעלה.< br /> מנהל הטעינה מראש לא אמור לשתף את אותו מופע של TrackSelector עם הנגן. במקום זאת, הם צריכים להשתמש במופע שונה של TrackSelector אבל באותו יישום. לכן אנחנו מגדירים את TrackSelectorFactory ולא את TrackSelector ב-DefaultPreloadManager.Builder.

preloadManagerBuilder.setTrackSelectorFactory(customTrackSelectorFactory)

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

preloadManagerBuilder.setRenderersFactory(customRenderersFactory)

מידע נוסף על רכיבי ExoPlayer

כלל הזהב: Playback Looper נפוץ ששולט בכל ההפעלות

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

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

ה-PreloadManager מכין אובייקט MediaSource עם שמירת מצב ברקע. כשקוד ממשק המשתמש קורא לפונקציה player.setMediaSource(mediaSource), מתבצעת העברה של האובייקט המורכב הזה עם המצב שלו ממקור המדיה של הטעינה מראש אל הנגן. במקרה כזה, כל PreloadMediaSource מועבר מהמנהל לנגן. כל האינטראקציות וההעברות האלה צריכות להתרחש באותו PlaybackLooper.

אם PreloadManager ו-ExoPlayer פעלו בשרשורים שונים, יכול להתרחש מרוץ תהליכים. יכול להיות שהשרשור של PreloadManager משנה את המצב הפנימי של MediaSource (למשל, כותב נתונים חדשים למאגר) בדיוק ברגע שהשרשור של הנגן מנסה לקרוא ממנו. התוצאה היא התנהגות בלתי צפויה, IllegalStateException שקשה לנפות באגים.

preloadManagerBuilder.setPreloadLooper(playbackLooper)

בואו נראה איך אפשר לשתף את כל הרכיבים שלמעלה בין ExoPlayer לבין DefaultPreloadManager בהגדרה עצמה.

val preloadManagerBuilder =
DefaultPreloadManager.Builder(context, targetPreloadStatusControl)

// Optional - Share components between ExoPlayer and DefaultPreloadManager
preloadManagerBuilder
     .setBandwidthMeter(customBandwidthMeter)
     .setLoadControl(customLoadControl)
     .setMediaSourceFactory(customMediaSourceFactory)
     .setTrackSelectorFactory(customTrackSelectorFactory)
     .setRenderersFactory(customRenderersFactory)
     .setPreloadLooper(playbackLooper)

val preloadManager = val preloadManagerBuilder.build()

טיפ: אם אתם משתמשים ברכיבי ברירת המחדל ב-ExoPlayer כמו DefaultLoadControl וכו', אתם לא צריכים לשתף אותם באופן מפורש עם DefaultPreloadManager. כשיוצרים את מופע ExoPlayer באמצעות buildExoPlayer של DefaultPreloadManager.Builder, הרכיבים האלה מקושרים זה לזה באופן אוטומטי, אם משתמשים בהטמעות ברירת המחדל עם הגדרות ברירת המחדל. אבל אם אתם משתמשים ברכיבים מותאמים אישית או בהגדרות מותאמות אישית, אתם צריכים להודיע במפורש ל-DefaultPreloadManager על כך באמצעות ממשקי ה-API שלמעלה.

טעינה מראש שמוכנה לייצור: התבנית של חלון הזזה

בפיד דינמי, משתמש יכול לגלול בין כמויות כמעט אינסופיות של תוכן. אם מוסיפים סרטונים ל-DefaultPreloadManager באופן רציף בלי אסטרטגיית הסרה מתאימה, בסופו של דבר תתרחש שגיאת OutOfMemoryError. כל MediaSource שנטען מראש מחזיק SampleQueue, שמקצה מאגרי זיכרון. כשהם מצטברים, הם יכולים לנצל את כל נפח ה-heap של האפליקציה. הפתרון הוא אלגוריתם שאתם אולי כבר מכירים, שנקרא חלון נע. התבנית של חלון נע שומרת בזיכרון קבוצה קטנה ונוחה לניהול של פריטים שסמוכים מבחינה לוגית למיקום הנוכחי של המשתמש בפיד. בזמן שהמשתמש גולל, ה'חלון' הזה של פריטים מנוהלים זז איתו, ומוסיף פריטים חדשים שנכנסים לתצוגה, וגם מסיר פריטים שכבר לא בתצוגה.

slidingwindow.png

הטמעה של תבנית החלון הנע

חשוב להבין ש-PreloadManager לא מספק שיטה מובנית של setWindowSize(). חלון הזזה הוא תבנית עיצוב שהמפתחים אחראים להטמיע באמצעות השיטות הפרימיטיביות add()‎ ו-remove()‎. הלוגיקה של האפליקציה צריכה לקשר בין אירועים בממשק המשתמש, כמו גלילה או שינוי דף, לבין קריאות ה-API האלה. אם רוצים הפניה לקוד, אפשר לעיין בדוגמה של socialite שבה מיושם דפוס של חלון נע. הדוגמה כוללת גם PreloadManagerWrapper שמדמה חלון נע.

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

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

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

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

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

סיכום והשלבים הבאים

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

בואו נסכם את המסקנות העיקריות:

  • כדאי להשתמש ב-PreloadManagerListener כדי לאסוף תובנות אנליטיות וליישם טיפול יעיל בשגיאות.
  • כדי לוודא שרכיבים חשובים משותפים, תמיד צריך להשתמש ב-DefaultPreloadManager.Builder יחיד כדי ליצור את מופעי הניהול והנגן.
  • מטמיעים את התבנית של חלון הזזה על ידי ניהול פעיל של קריאות ל-add()‎ ול-remove()‎ כדי למנוע את השגיאה OutOfMemoryError.
  • אפשר להשתמש ב-TargetPreloadStatusControl כדי ליצור אסטרטגיית טעינה מראש חכמה ומדורגת, שמאזנת בין ביצועים לבין צריכת משאבים.

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

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

יש לך משוב לשתף? נשמח לשמוע ממך.

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

נכתב על ידי:
להמשך קריאה