התאמה אישית

בליבה של ספריית 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 ולקבל שינויים בהגדרות ב-method‏ handleMessage. קוד האפליקציה צריך להעביר שינויים בהגדרות על ידי קריאה ל-method‏ createMessage של ExoPlayer, הגדרת ההודעה ושליחה שלה לרכיב באמצעות PlayerMessage.send. שליחת הודעות שיועברו בשרשור ההפעלה מבטיחה שהן יבוצעו לפי הסדר עם כל פעולה אחרת שמתבצעת בנגן.