שליטה בהפעלה ופרסום באמצעות MediaSession

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

הפקודות יכולות להגיע מלחצנים פיזיים, כמו לחצן ההפעלה באוזניות או בשלט הרחוק של הטלוויזיה. יכול להיות שהן יגיעו גם מאפליקציות לקוח שיש להן בקר מדיה, למשל, להורות על "pause" (השהיה) ל-Google Assistant. סשן המדיה מעביר את הפקודות האלה לנגן של אפליקציית המדיה.

מתי כדאי לבחור סשן מדיה

כשמטמיעים את MediaSession, המשתמשים יכולים לשלוט בהפעלה:

  • דרך האוזניות שלהם. לרוב יש באוזניות לחצנים או אינטראקציות מגע שמשתמשים יכולים לבצע כדי להפעיל או להשהות מדיה או לעבור לטראק הבא או הקודם.
  • על ידי דיבור אל Google Assistant. דפוס נפוץ הוא לומר "Ok Google, pause" כדי להשהות כל מדיה שפועלת כרגע במכשיר.
  • באמצעות שעון Wear OS. כך ניתן לגשת בקלות לפקדי ההפעלה הנפוצים ביותר בזמן ההפעלה בטלפון.
  • באמצעות פקדי המדיה. בקרוסלה הזו מוצגים הפקדים של כל סשן מדיה פעיל.
  • ב-טלוויזיה. מאפשרת לבצע פעולות באמצעות לחצני הפעלה פיזיים, לשלוט בהפעלה בפלטפורמה ולנהל את צריכת החשמל (לדוגמה, אם הטלוויזיה, פס האודיו או מקלט ה-A/V נכבים או שהקלט משתנה, ההפעלה אמורה להיפסק באפליקציה).
  • וכל תהליך חיצוני אחר שצריך להשפיע על ההפעלה.

זה שימושי בהרבה תרחישי שימוש. במיוחד מומלץ להשתמש ב-MediaSession במקרים הבאים:

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

עם זאת, לא כל תרחישי לדוגמה מתאימים ל-MediaSession. כדאי להשתמש רק ב-Player במקרים הבאים:

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

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

יצירת סשן מדיה

סשן מדיה נמצא לצד הנגן שהוא מנהל. אפשר ליצור סשן מדיה עם אובייקט Context ואובייקט Player. צריך ליצור ולהפעיל סשן מדיה לפי הצורך, למשל באמצעות שיטת מחזור החיים onStart() או onResume() של Activity או Fragment, או באמצעות השיטה onCreate() של Service שבבעלותו סשן המדיה והנגן המשויך אליו.

כדי ליצור סשן מדיה, צריך לאתחל את Player ולספק אותו ל-MediaSession.Builder באופן הבא:

Kotlin

val player = ExoPlayer.Builder(context).build()
val mediaSession = MediaSession.Builder(context, player).build()

Java

ExoPlayer player = new ExoPlayer.Builder(context).build();
MediaSession mediaSession = new MediaSession.Builder(context, player).build();

טיפול אוטומטי במצבים

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

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

מזהה סשן ייחודי

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

אם אפליקציה רוצה לנהל כמה מופעים של סשנים בו-זמנית, היא צריכה לוודא שמזהה הסשן של כל סשן יהיה ייחודי. אפשר להגדיר את מזהה הסשן בזמן היצירה שלו באמצעות MediaSession.Builder.setId(String id).

אם IllegalStateException גורם לקריסה של האפליקציה עם הודעת השגיאה IllegalStateException: Session ID must be unique. ID=, סביר להניח שסשן נוצר באופן בלתי צפוי לפני שמכונה שנוצרה בעבר עם אותו מזהה שוחררה. כדי למנוע הדלפה של סשנים עקב שגיאת תכנות, מקרים כאלה מזוהים ומקבלים הודעה על ידי הפעלת חריגה.

הענקת שליטה ללקוחות אחרים

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

תרשים שמציג את האינטראקציה בין MediaSession לבין MediaController.
איור 1: בקר המדיה מאפשר להעביר פקודות ממקורות חיצוניים לסשן המדיה.

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

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

שיטות אחרות של קריאה חוזרת מאפשרות לטפל, למשל, בבקשות לפקודות הפעלה בהתאמה אישית ולשינוי הפלייליסט). באופן דומה, קריאות החזרה האלה כוללות אובייקט ControllerInfo כדי שתוכלו לשנות את התשובה לכל בקשה בנפרד לכל בקר.

שינוי הפלייליסט

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

כשמוסיפים פריטים חדשים לפלייליסט, בדרך כלל הנגן דורש מכונות MediaItem עם URI מוגדר כדי שיהיה אפשר להפעיל אותם. כברירת מחדל, פריטים חדשים שנוספו מועברים באופן אוטומטי לשיטות של הנגן, כמו player.addMediaItem, אם מוגדר להם URI.

אם רוצים להתאים אישית את המופעים של MediaItem שנוספו לנגן, אפשר לשנות את הערך של onAddMediaItems(). צריך לבצע את השלב הזה אם רוצים לתמוך בבקשות מדיה של בקרי מדיה ללא URI מוגדר. במקום זאת, בדרך כלל מוגדר ב-MediaItem אחד או יותר מהשדות הבאים כדי לתאר את המדיה המבוקשת:

  • MediaItem.id: מזהה כללי שמזהה את המדיה.
  • MediaItem.RequestMetadata.mediaUri: URI של בקשה שעשוי להשתמש בסכימה מותאמת אישית, ושהנגן לא יכול להפעיל אותו ישירות.
  • MediaItem.RequestMetadata.searchQuery: שאילתת חיפוש טקסטואלית, למשל מ-Google Assistant.
  • MediaItem.MediaMetadata: מטא-נתונים מובנים כמו 'שם' או 'אומן'.

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

ניהול של פריסה בהתאמה אישית ופקודות בהתאמה אישית

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

הגדרת פריסה מותאמת אישית של הסשן

כדי לציין לאפליקציות לקוח אילו פקדי הפעלה רוצים להציג למשתמשים, מגדירים את הפריסה המותאמת אישית של הסשן כשיוצרים את MediaSession בשיטה onCreate() של השירות.

Kotlin

override fun onCreate() {
  super.onCreate()

  val likeButton = CommandButton.Builder()
    .setDisplayName("Like")
    .setIconResId(R.drawable.like_icon)
    .setSessionCommand(SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING))
    .build()
  val favoriteButton = CommandButton.Builder()
    .setDisplayName("Save to favorites")
    .setIconResId(R.drawable.favorite_icon)
    .setSessionCommand(SessionCommand(SAVE_TO_FAVORITES, Bundle()))
    .build()

  session =
    MediaSession.Builder(this, player)
      .setCallback(CustomMediaSessionCallback())
      .setCustomLayout(ImmutableList.of(likeButton, favoriteButton))
      .build()
}

Java

@Override
public void onCreate() {
  super.onCreate();

  CommandButton likeButton = new CommandButton.Builder()
    .setDisplayName("Like")
    .setIconResId(R.drawable.like_icon)
    .setSessionCommand(new SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING))
    .build();
  CommandButton favoriteButton = new CommandButton.Builder()
    .setDisplayName("Save to favorites")
    .setIconResId(R.drawable.favorite_icon)
    .setSessionCommand(new SessionCommand(SAVE_TO_FAVORITES, new Bundle()))
    .build();

  Player player = new ExoPlayer.Builder(this).build();
  mediaSession =
      new MediaSession.Builder(this, player)
          .setCallback(new CustomMediaSessionCallback())
          .setCustomLayout(ImmutableList.of(likeButton, favoriteButton))
          .build();
}

הצהרה על הנגן הזמין ופקודות בהתאמה אישית

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

אתם יכולים להגדיר אילו פקודות מותאמות אישית של סשן יהיו זמינות ל-MediaController כשהוא יתחבר לסשן המדיה שלכם. כדי לעשות זאת, צריך לשנות את הערך של MediaSession.Callback.onConnect(). הגדרה והחזרה של קבוצת הפקודות הזמינות כשמאשרים בקשת חיבור מ-MediaController בשיטת הקריאה החוזרת של onConnect:

Kotlin

private inner class CustomMediaSessionCallback: MediaSession.Callback {
  // Configure commands available to the controller in onConnect()
  override fun onConnect(
    session: MediaSession,
    controller: MediaSession.ControllerInfo
  ): MediaSession.ConnectionResult {
    val sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
        .add(SessionCommand(SAVE_TO_FAVORITES, Bundle.EMPTY))
        .build()
    return AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build()
  }
}

Java

class CustomMediaSessionCallback implements MediaSession.Callback {
  // Configure commands available to the controller in onConnect()
  @Override
  public ConnectionResult onConnect(
    MediaSession session,
    ControllerInfo controller) {
    SessionCommands sessionCommands =
        ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
            .add(new SessionCommand(SAVE_TO_FAVORITES, new Bundle()))
            .build();
    return new AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
}

כדי לקבל בקשות לפקודות בהתאמה אישית מ-MediaController, צריך לעקוף את ה-method onCustomCommand() ב-Callback.

Kotlin

private inner class CustomMediaSessionCallback: MediaSession.Callback {
  ...
  override fun onCustomCommand(
    session: MediaSession,
    controller: MediaSession.ControllerInfo,
    customCommand: SessionCommand,
    args: Bundle
  ): ListenableFuture<SessionResult> {
    if (customCommand.customAction == SAVE_TO_FAVORITES) {
      // Do custom logic here
      saveToFavorites(session.player.currentMediaItem)
      return Futures.immediateFuture(
        SessionResult(SessionResult.RESULT_SUCCESS)
      )
    }
    ...
  }
}

Java

class CustomMediaSessionCallback implements MediaSession.Callback {
  ...
  @Override
  public ListenableFuture<SessionResult> onCustomCommand(
    MediaSession session, 
    ControllerInfo controller,
    SessionCommand customCommand,
    Bundle args
  ) {
    if(customCommand.customAction.equals(SAVE_TO_FAVORITES)) {
      // Do custom logic here
      saveToFavorites(session.getPlayer().getCurrentMediaItem());
      return Futures.immediateFuture(
        new SessionResult(SessionResult.RESULT_SUCCESS)
      );
    }
    ...
  }
}

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

עדכון פריסה מותאמת אישית אחרי אינטראקציה של משתמש

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

Kotlin

val removeFromFavoritesButton = CommandButton.Builder()
  .setDisplayName("Remove from favorites")
  .setIconResId(R.drawable.favorite_remove_icon)
  .setSessionCommand(SessionCommand(REMOVE_FROM_FAVORITES, Bundle()))
  .build()
mediaSession.setCustomLayout(ImmutableList.of(likeButton, removeFromFavoritesButton))

Java

CommandButton removeFromFavoritesButton = new CommandButton.Builder()
  .setDisplayName("Remove from favorites")
  .setIconResId(R.drawable.favorite_remove_icon)
  .setSessionCommand(new SessionCommand(REMOVE_FROM_FAVORITES, new Bundle()))
  .build();
mediaSession.setCustomLayout(ImmutableList.of(likeButton, removeFromFavoritesButton));

התאמה אישית של התנהגות פקודות ההפעלה

כדי להתאים אישית את ההתנהגות של פקודה שהוגדרה בממשק Player, כמו play() או seekToNext(), צריך לעטוף את Player ב-ForwardingPlayer.

Kotlin

val player = ExoPlayer.Builder(context).build()

val forwardingPlayer = object : ForwardingPlayer(player) {
  override fun play() {
    // Add custom logic
    super.play()
  }

  override fun setPlayWhenReady(playWhenReady: Boolean) {
    // Add custom logic
    super.setPlayWhenReady(playWhenReady)
  }
}

val mediaSession = MediaSession.Builder(context, forwardingPlayer).build()

Java

ExoPlayer player = new ExoPlayer.Builder(context).build();

ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player) {
  @Override
  public void play() {
    // Add custom logic
    super.play();
  }

  @Override
  public void setPlayWhenReady(boolean playWhenReady) {
    // Add custom logic
    super.setPlayWhenReady(playWhenReady);
  }
};

MediaSession mediaSession =
  new MediaSession.Builder(context, forwardingPlayer).build();

מידע נוסף על ForwardingPlayer זמין במדריך של ExoPlayer בנושא התאמה אישית.

זיהוי הבקר המבקש של פקודת נגן

כשקריאה לשיטה Player מגיעה מ-MediaController, אפשר לזהות את מקור הקריאה באמצעות MediaSession.controllerForCurrentRequest ולקבל את ControllerInfo של הבקשה הנוכחית:

Kotlin

class CallerAwareForwardingPlayer(player: Player) :
  ForwardingPlayer(player) {

  override fun seekToNext() {
    Log.d(
      "caller",
      "seekToNext called from package ${session.controllerForCurrentRequest?.packageName}"
    )
    super.seekToNext()
  }
}

Java

public class CallerAwareForwardingPlayer extends ForwardingPlayer {
  public CallerAwareForwardingPlayer(Player player) {
    super(player);
  }

  @Override
  public void seekToNext() {
    Log.d(
        "caller",
        "seekToNext called from package: "
            + session.getControllerForCurrentRequest().getPackageName());
    super.seekToNext();
  }
}

תגובה ללחיצה על לחצני מדיה

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

אפליקציה יכולה לשנות את התנהגות ברירת המחדל על ידי שינוי הערך של MediaSession.Callback.onMediaButtonEvent(Intent). במקרה כזה, האפליקציה יכולה או צריכה לטפל בכל הפרטים הספציפיים של ה-API בעצמה.

טיפול בשגיאות ודיווח עליהן

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

שגיאות הפעלה חמורות

הנגן מדווח על שגיאת הפעלה קריטית לסשן, ואז הוא מדווח לבקרים כדי לבצע קריאה דרך Player.Listener.onPlayerError(PlaybackException) ו-Player.Listener.onPlayerErrorChanged(@Nullable PlaybackException).

במקרה כזה, מצב ההפעלה עובר ל-STATE_IDLE ו-MediaController.getPlaybackError() מחזיר את הערך PlaybackException שגרם למעבר. אמצעי בקרה יכול לבדוק את PlayerException.errorCode כדי לקבל מידע על הסיבה לשגיאה.

כדי לאפשר יכולת פעולה הדדית, שגיאה קטלנית משוכפלת ל-PlaybackStateCompat של סשן הפלטפורמה על ידי מעבר המצב שלה ל-STATE_ERROR והגדרת קוד השגיאה וההודעה בהתאם ל-PlaybackException.

התאמה אישית של שגיאה קריטית

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

Kotlin

val forwardingPlayer = ErrorForwardingPlayer(player)
val session = MediaSession.Builder(context, forwardingPlayer).build()

Java

Player forwardingPlayer = new ErrorForwardingPlayer(player);
MediaSession session =
    new MediaSession.Builder(context, forwardingPlayer).build();

הנגן המעביר רושם Player.Listener לנגן בפועל ומעביר קריאות חזרה שמדווחות על שגיאה. לאחר מכן, המערכת מעבירה את הערך המותאם אישית של PlaybackException למאזינים שרשומים בנגן ההעברה. כדי שהפעולה הזו תפעל, הנגן המעביר מבטל את ההגדרות של Player.addListener ו-Player.removeListener כדי לקבל גישה למאזינים, שבאמצעותם הוא יכול לשלוח קוד שגיאה, הודעה או פרטים נוספים בהתאמה אישית:

Kotlin

class ErrorForwardingPlayer(private val context: Context, player: Player) :
  ForwardingPlayer(player) {

  private val listeners: MutableList<Player.Listener> = mutableListOf()

  private var customizedPlaybackException: PlaybackException? = null

  init {
    player.addListener(ErrorCustomizationListener())
  }

  override fun addListener(listener: Player.Listener) {
    listeners.add(listener)
  }

  override fun removeListener(listener: Player.Listener) {
    listeners.remove(listener)
  }

  override fun getPlayerError(): PlaybackException? {
    return customizedPlaybackException
  }

  private inner class ErrorCustomizationListener : Player.Listener {

    override fun onPlayerErrorChanged(error: PlaybackException?) {
      customizedPlaybackException = error?.let { customizePlaybackException(it) }
      listeners.forEach { it.onPlayerErrorChanged(customizedPlaybackException) }
    }

    override fun onPlayerError(error: PlaybackException) {
      listeners.forEach { it.onPlayerError(customizedPlaybackException!!) }
    }

    private fun customizePlaybackException(
      error: PlaybackException,
    ): PlaybackException {
      val buttonLabel: String
      val errorMessage: String
      when (error.errorCode) {
        PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW -> {
          buttonLabel = context.getString(R.string.err_button_label_restart_stream)
          errorMessage = context.getString(R.string.err_msg_behind_live_window)
        }
        // Apps can customize further error messages by adding more branches.
        else -> {
          buttonLabel = context.getString(R.string.err_button_label_ok)
          errorMessage = context.getString(R.string.err_message_default)
        }
      }
      val extras = Bundle()
      extras.putString("button_label", buttonLabel)
      return PlaybackException(errorMessage, error.cause, error.errorCode, extras)
    }

    override fun onEvents(player: Player, events: Player.Events) {
      listeners.forEach {
        it.onEvents(player, events)
      }
    }
    // Delegate all other callbacks to all listeners without changing arguments like onEvents.
  }
}

Java

private static class ErrorForwardingPlayer extends ForwardingPlayer {

  private final Context context;
  private List<Player.Listener> listeners;
  @Nullable private PlaybackException customizedPlaybackException;

  public ErrorForwardingPlayer(Context context, Player player) {
    super(player);
    this.context = context;
    listeners = new ArrayList<>();
    player.addListener(new ErrorCustomizationListener());
  }

  @Override
  public void addListener(Player.Listener listener) {
    listeners.add(listener);
  }

  @Override
  public void removeListener(Player.Listener listener) {
    listeners.remove(listener);
  }

  @Nullable
  @Override
  public PlaybackException getPlayerError() {
    return customizedPlaybackException;
  }

  private class ErrorCustomizationListener implements Listener {

    @Override
    public void onPlayerErrorChanged(@Nullable PlaybackException error) {
      customizedPlaybackException =
          error != null ? customizePlaybackException(error, context) : null;
      for (int i = 0; i < listeners.size(); i++) {
        listeners.get(i).onPlayerErrorChanged(customizedPlaybackException);
      }
    }

    @Override
    public void onPlayerError(PlaybackException error) {
      for (int i = 0; i < listeners.size(); i++) {
        listeners.get(i).onPlayerError(checkNotNull(customizedPlaybackException));
      }
    }

    private PlaybackException customizePlaybackException(
        PlaybackException error, Context context) {
      String buttonLabel;
      String errorMessage;
      switch (error.errorCode) {
        case PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW:
          buttonLabel = context.getString(R.string.err_button_label_restart_stream);
          errorMessage = context.getString(R.string.err_msg_behind_live_window);
          break;
        // Apps can customize further error messages by adding more case statements.
        default:
          buttonLabel = context.getString(R.string.err_button_label_ok);
          errorMessage = context.getString(R.string.err_message_default);
          break;
      }
      Bundle extras = new Bundle();
      extras.putString("button_label", buttonLabel);
      return new PlaybackException(errorMessage, error.getCause(), error.errorCode, extras);
    }

    @Override
    public void onEvents(Player player, Events events) {
      for (int i = 0; i < listeners.size(); i++) {
        listeners.get(i).onEvents(player, events);
      }
    }
    // Delegate all other callbacks to all listeners without changing arguments like onEvents.
  }
}

שגיאות לא חמורות

אפליקציה יכולה לשלוח שגיאות לא קטלניות שלא נובעות מחריגה טכנית לכל הבקרים או לבקר ספציפי:

Kotlin

val sessionError = SessionError(
  SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
  context.getString(R.string.error_message_authentication_expired),
)

// Sending a nonfatal error to all controllers.
mediaSession.sendError(sessionError)

// Interoperability: Sending a nonfatal error to the media notification controller to set the
// error code and error message in the playback state of the platform media session.
mediaSession.mediaNotificationControllerInfo?.let {
  mediaSession.sendError(it, sessionError)
}

Java

SessionError sessionError = new SessionError(
    SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
    context.getString(R.string.error_message_authentication_expired));

// Sending a nonfatal error to all controllers.
mediaSession.sendError(sessionError);

// Interoperability: Sending a nonfatal error to the media notification controller to set the
// error code and error message in the playback state of the platform media session.
ControllerInfo mediaNotificationControllerInfo =
    mediaSession.getMediaNotificationControllerInfo();
if (mediaNotificationControllerInfo != null) {
  mediaSession.sendError(mediaNotificationControllerInfo, sessionError);
}

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

קבלת שגיאות לא קטלניות

MediaController מקבל שגיאה לא קריטית על ידי הטמעת MediaController.Listener.onError:

Kotlin

val future = MediaController.Builder(context, sessionToken)
  .setListener(object : MediaController.Listener {
    override fun onError(controller: MediaController, sessionError: SessionError) {
      // Handle nonfatal error.
    }
  })
  .buildAsync()

Java

MediaController.Builder future =
    new MediaController.Builder(context, sessionToken)
        .setListener(
            new MediaController.Listener() {
              @Override
              public void onError(MediaController controller, SessionError sessionError) {
                // Handle nonfatal error.
              }
            });