התאמה אישית

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

  • מופעי MediaSource שמגדירים את המדיה להפעלה, טוענים את המדיה ומאפשרים לקרוא את המדיה הטעונה. מופע של MediaSource נוצר מתוך MediaItem על ידי MediaSource.Factory בתוך הנגן. אפשר גם להעביר אותם ישירות לנגן באמצעות media source based playlist API.
  • מופע של MediaSource.Factory שממיר MediaItem ל-MediaSource. התג MediaSource.Factory מוזרק כשיוצרים את נגן.
  • Renderer instances שמציגים רכיבים בודדים של המדיה. הם מוזרקים כשהנגן נוצר.
  • TrackSelector שבוחר רצועות שסופקו על ידי MediaSource לצריכה על ידי כל Renderer זמין. TrackSelector מוזרק כשהנגן נוצר.
  • LoadControl שקובע מתי MediaSource מאגר המדיה יטען עוד מדיה, וכמה מדיה תיטען. ‫LoadControl מוזרק כשהשחקן נוצר.
  • LivePlaybackSpeedControl ששולט במהירות ההפעלה במהלך שידורים חיים כדי לאפשר לנגן להישאר קרוב להיסטוריית השידור החי שהוגדרה. ‫A LivePlaybackSpeedControl מוזרק כשהשחקן נוצר.

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

התאמה אישית של שחקנים

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

הגדרת מחסנית הרשת

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

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. אפשר להגדיר את הדגלים האלה לחילוץ נתונים בודד באמצעות השיטות individual DefaultExtractorsFactory.setXyzExtractorFlags שמתוארות למעלה. כדי להפעיל חיפוש קצב העברת נתונים קבוע לכל כלי החילוץ שתומכים בו, משתמשים ב-DefaultExtractorsFactory.setConstantBitrateSeekingEnabled.

Kotlin

val extractorsFactory = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true)

Java

DefaultExtractorsFactory extractorsFactory =
    new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true);

אחר כך אפשר להוסיף את ExtractorsFactory דרך DefaultMediaSourceFactory כמו שמתואר למעלה לגבי התאמה אישית של דגלי חילוץ.

הפעלה של תור אסינכרוני של מאגרי נתונים

התור האסינכרוני של מאגרים הוא שיפור בצינור העיבוד של 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 שילוב.MediaSource
  • MediaSource.Factory – הטמעה של MediaSource.Factory מותאם אישית מאפשרת לאפליקציה להתאים אישית את האופן שבו נוצר MediaSource מתוך MediaItem.
  • DataSource – חבילת ה-upstream של ExoPlayer כבר מכילה מספר הטמעות של DataSource לתרחישי שימוש שונים. יכול להיות שתרצו להטמיע מחלקה משלכם DataSource כדי לטעון נתונים בדרך אחרת, למשל באמצעות פרוטוקול מותאם אישית, באמצעות מחסנית HTTP מותאמת אישית או ממטמון מותאם אישית מתמשך.

כשיוצרים רכיבים בהתאמה אישית, מומלץ:

  • אם רכיב מותאם אישית צריך לדווח על אירועים בחזרה לאפליקציה, מומלץ לעשות זאת באמצעות אותו מודל כמו רכיבי ExoPlayer קיימים, למשל באמצעות מחלקות EventDispatcher או העברת Handler יחד עם מאזין לקונסטרוקטור של הרכיב.
  • מומלץ להשתמש באותו מודל ברכיבים מותאמים אישית כמו ברכיבי ExoPlayer קיימים, כדי לאפשר לאפליקציה להגדיר מחדש את הרכיבים במהלך ההפעלה. כדי לעשות את זה, רכיבים מותאמים אישית צריכים להטמיע את PlayerMessage.Target ולקבל שינויים בהגדרות בשיטה handleMessage. קוד האפליקציה צריך להעביר שינויים בהגדרות על ידי קריאה לשיטה createMessage של ExoPlayer, הגדרת ההודעה ושליחתה לרכיב באמצעות PlayerMessage.send. שליחת הודעות להצגה בשרשור ההפעלה מבטיחה שהן יבוצעו לפי הסדר עם כל פעולה אחרת שמבוצעת בנגן.