بازپخش پس‌زمینه‌ای با MediaSessionService

اغلب مطلوب است که رسانه درحالی‌که برنامه در پیش‌زمینه نیست پخش شود. برای مثال، پخش‌کننده موسیقی معمولاً وقتی کاربر دستگاهش را قفل کرده است یا از برنامه دیگری استفاده می‌کند، به پخش موسیقی ادامه می‌دهد. کتابخانه Media3 مجموعه‌ای از رابط‌ها را ارائه می‌دهد که به شما امکان می‌دهد از بازپخش پس‌زمینه پشتیبانی کنید.

استفاده از MediaSessionService

برای فعال کردن بازپخش پس‌زمینه‌ای، باید Player و MediaSession را در یک سرویس جداگانه قرار دهید. این کار به دستگاه اجازه می‌دهد حتی زمانی که برنامه شما در پیش‌زمینه نیست، به ارائه رسانه ادامه دهد.

MediaSessionService به جلسه رسانه اجازه می‌دهد به‌طور جداگانه از فعالیت برنامه اجرا شود
شکل ۱: MediaSessionService به جلسه رسانه اجازه می‌دهد جدا از فعالیت برنامه اجرا شود

هنگام میزبانی پخش‌کننده در «سرویس»، باید از MediaSessionService استفاده کنید. برای انجام این کار، کلاسی ایجاد کنید که MediaSessionService را گسترش دهد و جلسه رسانه خود را در داخل آن ایجاد کنید.

استفاده از MediaSessionService به مشتریان خارجی مانند «دستیار Google»، کنترل‌های رسانه سیستم، دکمه‌های رسانه در دستگاه‌های جانبی، یا دستگاه‌های همراه مانند Wear OS امکان می‌دهد سرویس شما را کشف کنند، به آن متصل شوند، و بازپخش را کنترل کنند، بدون اینکه به فعالیت واسط کاربر برنامه شما دسترسی داشته باشند. درواقع، چندین برنامه کارخواه می‌توانند به‌طور هم‌زمان به یک MediaSessionService متصل شوند، و هر برنامه MediaController خودش را داشته باشد.

پیاده‌سازی چرخه حیات سرویس

باید دو روش چرخه حیات سرویستان را پیاده‌سازی کنید:

  • وقتی اولین کنترل‌کننده درحال اتصال است و سرویس نمونه‌سازی و شروع می‌شود، onCreate() فراخوانده می‌شود. اینجا بهترین مکان برای ساختن Player و MediaSession است.
  • وقتی سرویس درحال متوقف شدن است، onDestroy() فراخوانی می‌شود. همه منابع ازجمله پخش‌کننده و جلسه باید منتشر شوند.

می‌توانید به‌طور اختیاری onTaskRemoved(Intent) را لغو کنید تا آنچه را که اتفاق می‌افتد سفارشی‌سازی کنید وقتی کاربر برنامه را از تکالیف اخیر رد می‌کند. به‌طور پیش‌فرض، اگر بازپخش درحال انجام باشد، سرویس درحال اجرا باقی می‌ماند و درغیراین‌صورت متوقف می‌شود.

کاتلین

class PlaybackService : MediaSessionService() {
  private var mediaSession: MediaSession? = null

  // Create your Player and MediaSession in the onCreate lifecycle event
  override fun onCreate() {
    super.onCreate()
    val player = ExoPlayer.Builder(this).build()
    mediaSession = MediaSession.Builder(this, player).build()
  }

  // Remember to release the player and media session in onDestroy
  override fun onDestroy() {
    mediaSession?.run {
      player.release()
      release()
      mediaSession = null
    }
    super.onDestroy()
  }

  override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
    mediaSession
}

جاوا

class PlaybackService extends MediaSessionService {
  private MediaSession mediaSession = null;

  // Create your Player and MediaSession in the onCreate lifecycle event
  @Override
  public void onCreate() {
    super.onCreate();
    ExoPlayer player = new ExoPlayer.Builder(this).build();
    mediaSession = new MediaSession.Builder(this, player).build();
  }

  // Remember to release the player and media session in onDestroy
  @Override
  public void onDestroy() {
    mediaSession.getPlayer().release();
    mediaSession.release();
    mediaSession = null;
    super.onDestroy();
  }

  @Override
  public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) {
    return mediaSession;
  }
}

به‌عنوان جایگزینی برای ادامه بازپخش در پس‌زمینه، می‌توانید سرویس را در هر موردی که کاربر برنامه را رد می‌کند متوقف کنید:

کاتلین

@OptIn(UnstableApi::class)
override fun onTaskRemoved(rootIntent: Intent?) {
  pauseAllPlayersAndStopSelf()
}

جاوا

@OptIn(markerClass = UnstableApi.class)
@Override
public void onTaskRemoved(@Nullable Intent rootIntent) {
  pauseAllPlayersAndStopSelf();
}

برای هرگونه پیاده‌سازی دستی دیگر onTaskRemoved، می‌توانید از isPlaybackOngoing() استفاده کنید تا بررسی کنید آیا بازپخش درحال انجام درنظر گرفته می‌شود و سرویس پیش‌زمینه‌ای شروع شده است یا نه.

دسترسی به جلسه رسانه را فراهم می‌کند

روش onGetSession() را لغو کنید تا به مشتریان دیگر اجازه دهید به جلسه رسانه شما که هنگام ایجاد سرویس ساخته شده است دسترسی داشته باشند.

کاتلین

class PlaybackService : MediaSessionService() {

  // [...] lifecycle methods omitted

  override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
    mediaSession
}

جاوا

class PlaybackService extends MediaSessionService {

  // [...] lifecycle methods omitted

  @Override
  public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) {
    return mediaSession;
  }
}

اعلام سرویس در مانیفست

برنامه برای اجرای سرویس پیش‌زمینه‌ای بازپخش به اجازه‌های FOREGROUND_SERVICE و FOREGROUND_SERVICE_MEDIA_PLAYBACK نیاز دارد:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />

همچنین باید کلاس Service خود را در مانیفست با فیلتر قصد MediaSessionService و foregroundServiceType که شامل mediaPlayback است اعلام کنید.

<service
    android:name=".PlaybackService"
    android:foregroundServiceType="mediaPlayback"
    android:exported="true">
    <intent-filter>
        <action android:name="androidx.media3.session.MediaSessionService"/>
        <action android:name="android.media.browse.MediaBrowserService"/>
    </intent-filter>
</service>

کنترل بازپخش بااستفاده از MediaController

در «فعالیت» یا «قطعه» حاوی میانای کاربر پخش‌کننده، می‌توانید بااستفاده از MediaController پیوندی بین میانای کاربر و جلسه رسانه برقرار کنید. میانای کاربر شما از کنترل‌کننده رسانه برای ارسال فرمان‌ها از میانای کاربر به پخش‌کننده در جلسه استفاده می‌کند. برای جزئیات مربوط به ایجاد و استفاده از MediaController، راهنمای ایجاد MediaController را ببینید.

رسیدگی به فرمان‌های MediaController

MediaSession فرمان‌ها را ازطریق MediaSession.Callback خود از کنترل‌کننده دریافت می‌کند. مقداردهی اولیه MediaSession، پیاده‌سازی پیش‌فرضی از MediaSession.Callback ایجاد می‌کند که به‌طور خودکار همه فرمان‌هایی را که MediaController به پخش‌کننده شما ارسال می‌کند مدیریت می‌کند.

اعلان

MediaSessionService به‌طور خودکار MediaNotification برایتان ایجاد می‌کند که باید در بیشتر موارد کار کند. به‌طور پیش‌فرض، اعلان منتشرشده یک اعلان MediaStyle است که با جدیدترین اطلاعات جلسه رسانه شما به‌روز می‌ماند و کنترل‌های بازپخش را نمایش می‌دهد. MediaNotification از جلسه شما مطلع است و می‌تواند برای کنترل بازپخش هر برنامه دیگری که به همان جلسه متصل است استفاده شود.

برای مثال، برنامه جاری‌سازی موسیقی که از MediaSessionService استفاده می‌کند، MediaNotification ایجاد می‌کند که عنوان، هنرمند، و طرح آلبوم را برای فایل رسانه‌ای درحال پخش در کنار کنترل‌های بازپخش براساس پیکربندی MediaSession شما نمایش می‌دهد.

فراداده الزامی را می‌توان در رسانه ارائه کرد یا به‌عنوان بخشی از فایل رسانه‌ای اعلام کرد، مانند قطعه کد زیر:

کاتلین

val mediaItem =
  MediaItem.Builder()
    .setMediaId("media-1")
    .setUri(mediaUri)
    .setMediaMetadata(
      MediaMetadata.Builder()
        .setArtist("David Bowie")
        .setTitle("Heroes")
        .setArtworkUri(artworkUri)
        .build()
    )
    .build()

mediaController.setMediaItem(mediaItem)
mediaController.prepare()
mediaController.play()

جاوا

MediaItem mediaItem =
    new MediaItem.Builder()
        .setMediaId("media-1")
        .setUri(mediaUri)
        .setMediaMetadata(
            new MediaMetadata.Builder()
                .setArtist("David Bowie")
                .setTitle("Heroes")
                .setArtworkUri(artworkUri)
                .build())
        .build();

mediaController.setMediaItem(mediaItem);
mediaController.prepare();
mediaController.play();

چرخه حیات اعلان

به‌محض اینکه Player دارای MediaItem نمونه در فهرست پخش خود باشد، اعلان ایجاد می‌شود.

همه به‌روزرسانی‌های اعلان به‌طور خودکار براساس وضعیت Player و MediaSession انجام می‌شود.

تا زمانی که سرویس پیش‌زمینه‌ای درحال اجرا است، نمی‌توان اعلان را برداشت. برای برداشتن فوری اعلان، باید Player.release() را فراخوانی کنید یا فهرست پخش را بااستفاده از Player.clearMediaItems() پاک کنید.

اگر پخش‌کننده برای بیش‌از ۱۰ دقیقه بدون تعامل بیشتر کاربر موقتاً متوقف، متوقف، یا ناموفق شود، سرویس به‌طور خودکار از حالت سرویس پیش‌زمینه‌ای خارج می‌شود تا سیستم بتواند آن را ازبین ببرد. می‌توانید ازسرگیری بازپخش را پیاده‌سازی کنید تا به کاربر اجازه دهید چرخه حیات سرویس را بازراه‌اندازی کند و بازپخش را در زمان دیگری ازسر بگیرد.

سفارشی‌سازی اعلان

فراداده مربوط به موردی که درحال پخش است را می‌توان با اصلاح MediaItem.MediaMetadata سفارشی‌سازی کرد. اگر می‌خواهید فراداده یک مورد موجود را به‌روز کنید، می‌توانید از Player.replaceMediaItem برای به‌روزرسانی فراداده بدون قطع کردن بازپخش استفاده کنید.

همچنین می‌توانید با تنظیم اولویت‌های دکمه رسانه سفارشی برای کنترل‌های «رسانه Android»، برخی‌از دکمه‌های نشان‌داده‌شده در اعلان را سفارشی‌سازی کنید. درباره سفارشی‌سازی کنترل‌های «رسانه Android» بیشتر بخوانید.

برای سفارشی‌سازی بیشتر خود اعلان، MediaNotification.Provider با DefaultMediaNotificationProvider.Builder یا با ایجاد پیاده‌سازی سفارشی از میانای ارائه‌دهنده ایجاد کنید. ارائه‌دهنده‌تان را با setMediaNotificationProvider به MediaSessionService اضافه کنید.

ازسرگیری بازپخش

پس‌از پایان یافتن MediaSessionService، و حتی پس‌از بازراه‌اندازی دستگاه، می‌توان ازسرگیری بازپخش را ارائه داد تا کاربران بتوانند سرویس را بازراه‌اندازی کنند و بازپخش را از جایی که متوقف شده بود ازسر بگیرند. به‌طور پیش‌فرض، ازسرگیری بازپخش خاموش است. یعنی وقتی سرویس شما درحال اجرا نیست، کاربر نمی‌تواند بازپخش را ازسر بگیرد. برای موافقت با این ویژگی، باید گیرنده دکمه رسانه را اعلام کنید و روش onPlaybackResumption را پیاده‌سازی کنید.

اعلام گیرنده دکمه رسانه Media3

با اعلام MediaButtonReceiver در مانیفست خود شروع کنید:

<receiver android:name="androidx.media3.session.MediaButtonReceiver"
  android:exported="true">
  <intent-filter>
    <action android:name="android.intent.action.MEDIA_BUTTON" />
  </intent-filter>
</receiver>

اجرای فراخوانی ازسرگیری بازپخش

وقتی ازسوی دستگاه بلوتوث یا ویژگی ازسرگیری رابط کاربری سیستم Android درخواست ازسرگیری بازپخش می‌شود، روش فراخوانی برگشتی onPlaybackResumption() فراخوانده می‌شود.

کاتلین

override fun onPlaybackResumption(
  mediaSession: MediaSession,
  controller: MediaSession.ControllerInfo,
  isForPlayback: Boolean,
): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
  val settableFuture = SettableFuture.create<MediaSession.MediaItemsWithStartPosition>()
  settableFuture.addListener(
    {
      // Your app is responsible for storing the playlist, metadata (like title
      // and artwork) of the current item and the start position to use here.
      val resumptionPlaylist = restorePlaylist()
      settableFuture.set(resumptionPlaylist)
    },
    MoreExecutors.directExecutor(),
  )
  return settableFuture
}

جاوا

@Override
public ListenableFuture<MediaItemsWithStartPosition> onPlaybackResumption(
    MediaSession mediaSession, ControllerInfo controller, boolean isForPlayback) {
  SettableFuture<MediaItemsWithStartPosition> settableFuture = SettableFuture.create();
  settableFuture.addListener(
      () -> {
        // Your app is responsible for storing the playlist, metadata (like title
        // and artwork) of the current item and the start position to use here.
        MediaItemsWithStartPosition resumptionPlaylist = restorePlaylist();
        settableFuture.set(resumptionPlaylist);
      },
      MoreExecutors.directExecutor());
  return settableFuture;
}

اگر پارامترهای دیگری مانند سرعت بازپخش، حالت تکرار، یا حالت درهم را ذخیره کرده‌اید، onPlaybackResumption() مکان مناسبی برای پیکربندی پخش‌کننده با این پارامترها است، قبل‌از اینکه Media3 پخش‌کننده را آماده کند و بازپخش را پس‌از تکمیل فراخوانی برگشتی شروع کند.

این روش درطول زمان راه‌اندازی فراخوانی می‌شود تا اعلان ازسرگیری «رابط کاربری سیستم Android» را پس‌از بازراه‌اندازی دستگاه با isForPlayback تنظیم‌شده روی false ایجاد کند. برای اعلان غنی، توصیه می‌شود فیلدهای MediaMetadata مانند title و artworkData یا artworkUri مورد فعلی را با مقادیر دردسترس محلی پر کنید، زیرا ممکن است دسترسی به شبکه هنوز دردسترس نباشد. همچنین می‌توانید MediaConstants.EXTRAS_KEY_COMPLETION_STATUS و MediaConstants.EXTRAS_KEY_COMPLETION_PERCENTAGE را به MediaMetadata.extras اضافه کنید تا موقعیت بازپخش ازسرگیری را نشان دهید.

پیکربندی پیشرفته کنترل‌کننده و سازگاری با نسخه‌های قدیمی

یک سناریو رایج استفاده از MediaController در میانای کاربر برنامه برای کنترل بازپخش و نمایش فهرست پخش است. هم‌زمان، جلسه برای مشتریان خارجی مانند کنترل‌های رسانه Android و «دستیار» در تلفن همراه یا تلویزیون، Wear OS برای ساعت‌ها، و Android Auto در خودروها نمایش داده می‌شود. برنامه نمایشی جلسه Media3 نمونه‌ای از برنامه‌ای است که چنین سناریویی را پیاده‌سازی می‌کند.

این کارخواه‌های خارجی ممکن است از میاناهای برنامه‌سازی کاربردی مثل MediaControllerCompat از کتابخانه قدیمی AndroidX یا android.media.session.MediaController از پلاتفرم Android استفاده کنند. Media3 کاملاً با کتابخانه قدیمی سازگار است و با میانای برنامه‌سازی کاربردی پلاتفرم Android قابلیت همکاری دارد.

شناسایی کنترل‌کننده‌های مورداعتماد

هر برنامه‌ای می‌تواند برای اتصال به جلسه یا کتابخانه رسانه شما تلاش کند. اگر می‌خواهید دسترسی به کنترل‌کننده‌های سیستم، کنترل‌کننده‌های دارای اجازه کنترل محتوای رسانه، و برنامه خودتان را محدود کنید، می‌توانید از ControllerInfo.isTrusted() برای بررسی دسترسی پایه استفاده کنید. یا می‌توانید کنترل‌کننده‌های خاص‌تری را شناسایی کنید، مثل کنترل‌کننده اعلان رسانه یا کنترل‌کننده‌های Android Auto، همان‌طور که در بخش‌های زیر شرح داده شده است.

استفاده از کنترل‌کننده اعلان رسانه

باید بدانید که این کنترل‌کننده‌های قدیمی و پلاتفرم از یک وضعیت مشترک استفاده می‌کنند و امکان سفارشی‌سازی نمایان بودن براساس کنترل‌کننده وجود ندارد (برای مثال PlaybackState.getActions() و PlaybackState.getCustomActions() دردسترس). می‌توانید از کنترل‌کننده اعلان رسانه برای پیکربندی وضعیت تنظیم‌شده در جلسه رسانه پلاتفرم استفاده کنید تا با این کنترل‌کننده‌های قدیمی و پلاتفرم سازگار باشد.

برای مثال، یک برنامه می‌تواند پیاده‌سازی MediaSession.Callback.onConnect() را برای تنظیم فرمان‌های دردسترس و اولویت‌های دکمه رسانه به‌طور خاص برای جلسه پلاتفرم به‌صورت زیر ارائه دهد:

کاتلین

override fun onConnectAsync(
  session: MediaSession,
  controller: MediaSession.ControllerInfo,
): ListenableFuture<ConnectionResult> {
  if (session.isMediaNotificationController(controller)) {
    val playerCommands =
      ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
        .remove(COMMAND_SEEK_TO_PREVIOUS)
        .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
        .remove(COMMAND_SEEK_TO_NEXT)
        .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
        .build()
    // Custom button preferences and commands to configure the platform session.
    return immediateFuture(
      AcceptedResultBuilder(session, controller)
        .setMediaButtonPreferences(listOf(seekBackButton, seekForwardButton))
        .setAvailablePlayerCommands(playerCommands)
        .build()
    )
  }
  // Default commands with default button preferences for all other controllers.
  return immediateFuture(AcceptedResultBuilder(session, controller).build())
}

جاوا

@Override
public ListenableFuture<ConnectionResult> onConnectAsync(
    MediaSession session, MediaSession.ControllerInfo controller) {
  if (session.isMediaNotificationController(controller)) {
    Player.Commands playerCommands =
        ConnectionResult.DEFAULT_PLAYER_COMMANDS
            .buildUpon()
            .remove(COMMAND_SEEK_TO_PREVIOUS)
            .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
            .remove(COMMAND_SEEK_TO_NEXT)
            .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
            .build();
    // Custom button preferences and commands to configure the platform session.
    return immediateFuture(
        new AcceptedResultBuilder(session, controller)
            .setMediaButtonPreferences(ImmutableList.of(seekBackButton, seekForwardButton))
            .setAvailablePlayerCommands(playerCommands)
            .build());
  }
  // Default commands with default button preferences for all other controllers.
  return immediateFuture(new AcceptedResultBuilder(session, controller).build());
}

به Android Auto اجازه دهید فرمان‌های سفارشی ارسال کند

هنگام استفاده از MediaLibraryService و برای پشتیبانی از Android Auto با برنامه تلفن همراه، کنترل‌کننده Android Auto به فرمان‌های دردسترس مناسب نیاز دارد، درغیراین‌صورت Media3 فرمان‌های سفارشی ورودی از آن کنترل‌کننده را رد می‌کند:

کاتلین

override fun onConnectAsync(
  session: MediaSession,
  controller: MediaSession.ControllerInfo,
): ListenableFuture<ConnectionResult> {
  val sessionCommands =
    ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().add(customCommand).build()
  if (session.isMediaNotificationController(controller)) {
    // ... See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available commands to accept incoming custom commands from Auto.
    return immediateFuture(
      AcceptedResultBuilder(session, controller)
        .setAvailableSessionCommands(sessionCommands)
        .build()
    )
  }
  // Default commands for all other controllers.
  return immediateFuture(AcceptedResultBuilder(session, controller).build())
}

جاوا

@Override
public ListenableFuture<ConnectionResult> onConnectAsync(
    MediaSession session, MediaSession.ControllerInfo controller) {
  SessionCommands sessionCommands =
      ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().add(customCommand).build();
  if (session.isMediaNotificationController(controller)) {
    // ... See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available commands to accept incoming custom commands from Auto.
    return immediateFuture(
        new AcceptedResultBuilder(session, controller)
            .setAvailableSessionCommands(sessionCommands)
            .build());
  }
  // Default commands for all other controllers.
  return immediateFuture(new AcceptedResultBuilder(session, controller).build());
}

برنامه نمایشی جلسه دارای ماژول خودرویی است که پشتیبانی از Automotive OS را نشان می‌دهد و به APK جداگانه‌ای نیاز دارد.