התאמה אישית

ליבת הספרייה של 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 מוזרק כשיוצרים את הנגן.

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

התאמה אישית של הנגן

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

הגדרת סטאק הרשת

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

אם יוצרים מכונות עיבוד גרפי ישירות, מעבירים את הערך AsynchronousMediaCodecAdapter.Factory למקלטי ה-constructor‏ 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

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

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