بازپخش پس‌زمینه‌ای با 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» را نشان می‌دهد و به APK جداگانه نیاز دارد.