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

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

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

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

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

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

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

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

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

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

עריכת הפלייליסט

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

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

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

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

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

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

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

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

כשקריאה ל-method 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 לנגן בפועל ומיירט קריאות חוזרות (callback) שמדווחות על שגיאה. התאמה אישית לאחר מכן, 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.
              }
            });