בליבה של ספריית ExoPlayer נמצא הממשק Player. Player חושף פונקציונליות מסורתית של נגן מדיה ברמה גבוהה, כמו היכולת לאגור מדיה, להפעיל, להשהות ולחפש. ההטמעה שמוגדרת כברירת מחדל, ExoPlayer, מיועדת להניח כמה שפחות הנחות לגבי סוג המדיה שמופעלת, אופן האחסון שלה, המיקום שבו היא מאוחסנת ואופן העיבוד שלה (ולכן היא מטילה כמה שפחות הגבלות). במקום להטמיע את הטעינה והעיבוד של המדיה באופן ישיר, הטמעות של ExoPlayer מעבירות את העבודה הזו לרכיבים שמוזרקים כשיוצרים נגן או כשמעבירים מקורות מדיה חדשים לנגן.
הרכיבים המשותפים לכל ההטמעות של ExoPlayer הם:
- מופעים של
MediaSourceשמגדירים את המדיה להפעלה, טוענים את המדיה ומאפשרים לקרוא את המדיה שנטענה. מופע שלMediaSourceנוצר מ-MediaItemעל ידיMediaSource.Factoryבתוך הנגן. אפשר גם להעביר אותם ישירות לנגן באמצעות ה-API של פלייליסטים שמבוססים על מקורות מדיה. - מופע של
MediaSource.FactoryשממירMediaItemל-MediaSource. ה-MediaSource.Factoryמוזרק כשהנגן נוצר. Rendererמופעים שמעבדים רכיבים ספציפיים של המדיה. הם מוזרקים כשהנגן נוצר.TrackSelectorשבוחר טראקים שסופקו על ידיMediaSourceכדי שכלRendererזמין יוכל להשתמש בהם. ה-TrackSelectorמוזרק כשיוצרים את הנגן.LoadControlשקובע מתיMediaSourceמאחסן יותר מדיה במאגר, וכמה מדיה מאוחסנת במאגר. ה-LoadControlמוזרק כשיוצרים את הנגן.-
LivePlaybackSpeedControlששולט במהירות ההפעלה במהלך הפעלות בשידור חי כדי לאפשר לנגן להישאר קרוב לקיזוז מוגדר בשידור חי.LivePlaybackSpeedControlמוזרק כשהנגן נוצר.
הקונספט של הזרקת רכיבים שמטמיעים חלקים מהפונקציונליות של הנגן קיים בכל הספרייה. ההטמעות של חלק מהרכיבים כברירת מחדל מעבירות את העבודה לרכיבים נוספים שהוחדרו. כך אפשר להחליף הרבה רכיבי משנה באופן נפרד בהטמעות שמוגדרות באופן מותאם אישית.
התאמה אישית של הנגן
בקטעים הבאים מתוארות כמה דוגמאות נפוצות להתאמה אישית של הנגן על ידי הזרקת רכיבים.
הגדרת מקבץ הרשת (network stack)
יש לנו דף בנושא התאמה אישית של מקבץ הרשת (network stack) שמשמש את ExoPlayer.
שמירת נתונים במטמון שנטענו מהרשת
עיינו במדריכים בנושא שמירה זמנית במטמון תוך כדי תנועה והורדת מדיה.
התאמה אישית של האינטראקציות עם השרת
יכול להיות שאפליקציות מסוימות ירצו ליירט בקשות ותגובות HTTP. יכול להיות שתרצו להוסיף כותרות של בקשות בהתאמה אישית, לקרוא את כותרות התגובות של השרת, לשנות את מזהי ה-URI של הבקשות וכו'. לדוגמה, האפליקציה יכולה לאמת את עצמה על ידי הוספת אסימון ככותרת כשהיא מבקשת את קטעי המדיה.
בדוגמה הבאה מוסבר איך להטמיע את ההתנהגויות האלה על ידי הזרקת DataSource.Factory בהתאמה אישית ל-DefaultMediaSourceFactory:
Kotlin
val dataSourceFactory = DataSource.Factory { val dataSource = httpDataSourceFactory.createDataSource() // Set a custom authentication request header. dataSource.setRequestProperty("Header", "Value") dataSource } val player = ExoPlayer.Builder(context) .setMediaSourceFactory( DefaultMediaSourceFactory(context).setDataSourceFactory(dataSourceFactory) ) .build()
Java
DataSource.Factory dataSourceFactory = () -> { HttpDataSource dataSource = httpDataSourceFactory.createDataSource(); // Set a custom authentication request header. dataSource.setRequestProperty("Header", "Value"); return dataSource; }; ExoPlayer player = new ExoPlayer.Builder(context) .setMediaSourceFactory( new DefaultMediaSourceFactory(context).setDataSourceFactory(dataSourceFactory)) .build();
בקטע הקוד שלמעלה, ה-HttpDataSource שהוזרק כולל את הכותרת "Header: Value" בכל בקשת HTTP. ההתנהגות הזו קבועה לכל אינטראקציה עם מקור HTTP.
כדי להשתמש בגישה מפורטת יותר, אפשר להוסיף התנהגות של "בדיוק בזמן" באמצעות ResolvingDataSource. בקטע הקוד הבא אפשר לראות איך להוסיף כותרות של בקשות רגע לפני האינטראקציה עם מקור HTTP:
Kotlin
val dataSourceFactory: DataSource.Factory = ResolvingDataSource.Factory(httpDataSourceFactory) { dataSpec: DataSpec -> // Provide just-in-time request headers. dataSpec.withRequestHeaders(getCustomHeaders(dataSpec.uri)) }
Java
DataSource.Factory dataSourceFactory = new ResolvingDataSource.Factory( httpDataSourceFactory, // Provide just-in-time request headers. dataSpec -> dataSpec.withRequestHeaders(getCustomHeaders(dataSpec.uri)));
אפשר גם להשתמש ב-ResolvingDataSource כדי לבצע שינויים ב-URI בזמן אמת, כמו שמוצג בקטע הקוד הבא:
Kotlin
val dataSourceFactory: DataSource.Factory = ResolvingDataSource.Factory(httpDataSourceFactory) { dataSpec: DataSpec -> // Provide just-in-time URI resolution logic. dataSpec.withUri(resolveUri(dataSpec.uri)) }
Java
DataSource.Factory dataSourceFactory = new ResolvingDataSource.Factory( httpDataSourceFactory, // Provide just-in-time URI resolution logic. dataSpec -> dataSpec.withUri(resolveUri(dataSpec.uri)));
התאמה אישית של הטיפול בשגיאות
הטמעה של LoadErrorHandlingPolicy בהתאמה אישית מאפשרת לאפליקציות להתאים אישית את האופן שבו ExoPlayer מגיב לשגיאות טעינה. לדוגמה, יכול להיות שתרצו שהאפליקציה תיכשל במהירות במקום לנסות שוב ושוב, או שתרצו להתאים אישית את לוגיקת ההשהיה לפני ניסיון חוזר (back-off) שקובעת כמה זמן הנגן ימתין בין כל ניסיון חוזר. בקטע הקוד הבא אפשר לראות איך מטמיעים לוגיקה מותאמת אישית של השהיה לפני ניסיון חוזר:
Kotlin
val loadErrorHandlingPolicy: LoadErrorHandlingPolicy = object : DefaultLoadErrorHandlingPolicy() { override fun getRetryDelayMsFor( loadErrorInfo: LoadErrorHandlingPolicy.LoadErrorInfo ): Long { // Implement custom back-off logic here. return 0 } } val player = ExoPlayer.Builder(context) .setMediaSourceFactory( DefaultMediaSourceFactory(context).setLoadErrorHandlingPolicy(loadErrorHandlingPolicy) ) .build()
Java
LoadErrorHandlingPolicy loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy() { @Override public long getRetryDelayMsFor(LoadErrorHandlingPolicy.LoadErrorInfo loadErrorInfo) { // Implement custom back-off logic here. return 0; } }; ExoPlayer player = new ExoPlayer.Builder(context) .setMediaSourceFactory( new DefaultMediaSourceFactory(context) .setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)) .build();
הארגומנט LoadErrorInfo מכיל מידע נוסף על הטעינה שנכשלה, כדי להתאים אישית את הלוגיקה על סמך סוג השגיאה או הבקשה שנכשלה.
התאמה אישית של דגלי חילוץ
אפשר להשתמש בדגלי חילוץ כדי להתאים אישית את האופן שבו פורמטים ספציפיים מחולצים ממדיה פרוגרסיבית. אפשר להגדיר אותם ב-DefaultExtractorsFactory שסופק ל-DefaultMediaSourceFactory. בדוגמה הבאה מועבר דגל שמפעיל חיפוש מבוסס-אינדקס בסטרימינג של MP3.
Kotlin
val extractorsFactory = DefaultExtractorsFactory().setMp3ExtractorFlags(Mp3Extractor.FLAG_ENABLE_INDEX_SEEKING) val player = ExoPlayer.Builder(context) .setMediaSourceFactory(DefaultMediaSourceFactory(context, extractorsFactory)) .build()
Java
DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory().setMp3ExtractorFlags(Mp3Extractor.FLAG_ENABLE_INDEX_SEEKING); ExoPlayer player = new ExoPlayer.Builder(context) .setMediaSourceFactory(new DefaultMediaSourceFactory(context, extractorsFactory)) .build();
הפעלת חיפוש קצב העברת נתונים קבוע
בסטרימינג של MP3, ADTS ו-AMR, אפשר להפעיל חיפוש משוער באמצעות הנחה של קצב העברת נתונים קבוע עם דגלי FLAG_ENABLE_CONSTANT_BITRATE_SEEKING.
אפשר להגדיר את הדגלים האלה למחלצים בודדים באמצעות השיטות הנפרדות של DefaultExtractorsFactory.setXyzExtractorFlags, כפי שמתואר למעלה. כדי להפעיל חיפוש של קצב העברת נתונים קבוע בכל המפענחים שתומכים בכך, צריך להשתמש ב-DefaultExtractorsFactory.setConstantBitrateSeekingEnabled.
Kotlin
val extractorsFactory = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true)
Java
DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true);
לאחר מכן אפשר להחדיר את ExtractorsFactory באמצעות DefaultMediaSourceFactory, כפי שמתואר למעלה בקטע "התאמה אישית של דגלי חילוץ".
הפעלת תור אסינכרוני במאגר נתונים זמני (buffer queue)
הוספה אסינכרונית של תור במאגר נתונים זמני (buffer queue) היא שיפור בצינור עיבוד הנתונים של ExoPlayer, שמפעיל מכונות של MediaCodec במצב אסינכרוני ומשתמש בשרשורים נוספים כדי לתזמן את הפענוח והעיבוד של הנתונים. הפעלת ההגדרה הזו יכולה לצמצם את מספר הפריימים שהוחמצו ואת מספר המקרים שבהם האודיו לא מסונכרן.
התכונה "תור אסינכרוני במאגר נתונים זמני" מופעלת כברירת מחדל במכשירים עם Android 12 (רמת API 31) ומעלה, ואפשר להפעיל אותה ידנית החל מ-Android 6.0 (רמת API 23). כדאי להפעיל את התכונה במכשירים ספציפיים שבהם אתם מבחינים בפריימים שהוחמצו או בבעיות בסנכרון האודיו, במיוחד כשמפעילים תוכן שמוגן באמצעות DRM או תוכן עם קצב פריימים גבוה.
במקרה הפשוט ביותר, צריך להוסיף DefaultRenderersFactory לנגן באופן הבא:
Kotlin
val renderersFactory = DefaultRenderersFactory(context).forceEnableMediaCodecAsynchronousQueueing() val exoPlayer = ExoPlayer.Builder(context, renderersFactory).build()
Java
DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(context).forceEnableMediaCodecAsynchronousQueueing(); ExoPlayer exoPlayer = new ExoPlayer.Builder(context, renderersFactory).build();
אם אתם יוצרים מופעים של רכיבי רינדור באופן ישיר, העבירו את new
DefaultMediaCodecAdapter.Factory(context).forceEnableAsynchronous() אל ה-constructors של MediaCodecVideoRenderer ו-MediaCodecAudioRenderer.
התאמה אישית של פעולות באמצעות ForwardingSimpleBasePlayer
אפשר להתאים אישית חלק מההתנהגות של מופע Player על ידי הוספה שלו למחלקת משנה של ForwardingSimpleBasePlayer. המחלקה הזו מאפשרת ליירט "פעולות" ספציפיות, במקום להטמיע ישירות שיטות של Player. כך אפשר להבטיח התנהגות עקבית של play(), pause() ו-setPlayWhenReady(boolean), למשל. הוא גם מוודא שכל השינויים במצב מופצים בצורה נכונה למופעי Player.Listener רשומים. ברוב תרחישי השימוש בהתאמה אישית, עדיף להשתמש ב-ForwardingSimpleBasePlayer ולא ב-ForwardingPlayer, שמועד יותר לשגיאות, בגלל הבטחות העקביות האלה.
לדוגמה, כדי להוסיף לוגיקה מותאמת אישית כשההפעלה מתחילה או נפסקת:
Kotlin
class PlayerWithCustomPlay(player: Player) : ForwardingSimpleBasePlayer(player) { override fun handleSetPlayWhenReady(playWhenReady: Boolean): ListenableFuture<*> { // Add custom logic return super.handleSetPlayWhenReady(playWhenReady) } }
Java
public static final class PlayerWithCustomPlay extends ForwardingSimpleBasePlayer { public PlayerWithCustomPlay(Player player) { super(player); } @Override protected ListenableFuture<?> handleSetPlayWhenReady(boolean playWhenReady) { // Add custom logic return super.handleSetPlayWhenReady(playWhenReady); } }
או כדי לא לאפשר את הפקודה SEEK_TO_NEXT (ולוודא ש-Player.seekToNext לא מבצעת פעולה):
Kotlin
class PlayerWithoutSeekToNext(player: Player) : ForwardingSimpleBasePlayer(player) { override fun getState(): State { val state = super.getState() return state .buildUpon() .setAvailableCommands( state.availableCommands.buildUpon().remove(COMMAND_SEEK_TO_NEXT).build() ) .build() } // We don't need to override handleSeek, because it is guaranteed not to be called for // COMMAND_SEEK_TO_NEXT since we've marked that command unavailable. }
Java
public static final class PlayerWithoutSeekToNext extends ForwardingSimpleBasePlayer { public PlayerWithoutSeekToNext(Player player) { super(player); } @Override protected State getState() { State state = super.getState(); return state .buildUpon() .setAvailableCommands( state.availableCommands.buildUpon().remove(COMMAND_SEEK_TO_NEXT).build()) .build(); } // We don't need to override handleSeek, because it is guaranteed not to be called for // COMMAND_SEEK_TO_NEXT since we've marked that command unavailable. }
התאמה אישית של MediaSource
בדוגמאות הקודמות מוזרקים רכיבים בהתאמה אישית לשימוש במהלך ההפעלה של כל האובייקטים מסוג MediaItem שמועברים לנגן. אם נדרשת התאמה אישית מפורטת, אפשר גם להוסיף רכיבים בהתאמה אישית למופעים ספציפיים של MediaSource, שאפשר להעביר ישירות לנגן. בדוגמה הבאה מוסבר איך להתאים אישית ProgressiveMediaSource כדי להשתמש ב-DataSource.Factory, ב-ExtractorsFactory וב-LoadErrorHandlingPolicy בהתאמה אישית:
Kotlin
val mediaSource = ProgressiveMediaSource.Factory(customDataSourceFactory, customExtractorsFactory) .setLoadErrorHandlingPolicy(customLoadErrorHandlingPolicy) .createMediaSource(MediaItem.fromUri(streamUri))
Java
ProgressiveMediaSource mediaSource = new ProgressiveMediaSource.Factory(customDataSourceFactory, customExtractorsFactory) .setLoadErrorHandlingPolicy(customLoadErrorHandlingPolicy) .createMediaSource(MediaItem.fromUri(streamUri));
יצירת רכיבים בהתאמה אישית
הספרייה מספקת הטמעות של רכיבי ברירת המחדל שמופיעים בתחילת הדף הזה, לתרחישים נפוצים לדוגמה. ExoPlayer יכול להשתמש ברכיבים האלה, אבל יכול גם להשתמש בהטמעות בהתאמה אישית אם נדרשות התנהגויות לא סטנדרטיות. הנה כמה תרחישים לדוגמה שבהם אפשר להשתמש בהטמעות בהתאמה אישית:
-
Renderer– יכול להיות שתרצו להטמיעRendererבהתאמה אישית כדי לטפל בסוג מדיה שלא נתמך בהטמעות שמוגדרות כברירת מחדל בספרייה. TrackSelector– הטמעה שלTrackSelectorבהתאמה אישית מאפשרת למפתחי אפליקציות לשנות את האופן שבו נבחרים טראקים שנחשפים על ידיMediaSourceלצריכה על ידי כל אחד מה-Rendererהזמינים.-
LoadControl– הטמעה שלLoadControlבהתאמה אישית מאפשרת למפתחי אפליקציות לשנות את מדיניות הטעינה של הנגן. -
Extractor– אם אתם צריכים לתמוך בפורמט קונטיינר שלא נתמך כרגע בספרייה, כדאי להטמיע מחלקתExtractorבהתאמה אישית. MediaSource– הטמעה של מחלקתMediaSourceבהתאמה אישית יכולה להתאים אם רוצים לקבל דגימות מדיה כדי להזין אותן למעבדי ה-rendering באופן מותאם אישית, או אם רוצים להטמיע התנהגות קומפוזיציה שלMediaSourceבהתאמה אישית.-
MediaSource.Factory– הטמעה שלMediaSource.Factoryבהתאמה אישית מאפשרת לאפליקציה להתאים אישית את האופן שבוMediaSourceנוצר מ-MediaItem. -
DataSource– חבילת ה-upstream של ExoPlayer כבר מכילה כמה הטמעות שלDataSourceלתרחישים שונים לדוגמה. יכול להיות שתרצו להטמיע מחלקתDataSourceמשלכם כדי לטעון נתונים בדרך אחרת, למשל באמצעות פרוטוקול בהתאמה אישית, באמצעות מחסנית HTTP בהתאמה אישית או ממטמון מתמיד בהתאמה אישית.
כשיוצרים רכיבים בהתאמה אישית, מומלץ:
- אם רכיב בהתאמה אישית צריך לדווח על אירועים בחזרה לאפליקציה, מומלץ לעשות זאת באמצעות אותו מודל כמו רכיבי ExoPlayer קיימים, למשל באמצעות מחלקות
EventDispatcherאו העברתHandlerיחד עם מאזין ל-constructor של הרכיב. - אנחנו ממליצים שרכיבים בהתאמה אישית ישתמשו באותו מודל כמו רכיבי ExoPlayer קיימים, כדי לאפשר לאפליקציה לשנות את ההגדרות במהלך ההפעלה. כדי לעשות את זה, רכיבים בהתאמה אישית צריכים להטמיע את
PlayerMessage.Targetולקבל שינויים בהגדרות ב-methodhandleMessage. קוד האפליקציה צריך להעביר שינויים בהגדרות על ידי קריאה ל-methodcreateMessageשל ExoPlayer, הגדרת ההודעה ושליחה שלה לרכיב באמצעותPlayerMessage.send. שליחת הודעות שיועברו בשרשור ההפעלה מבטיחה שהן יבוצעו לפי הסדר עם כל פעולה אחרת שמתבצעת בנגן.