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

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

استفاده از یک سرویس MediaSession

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

سرویس MediaSession به جلسه رسانه اجازه می‌دهد تا جدا از فعالیت برنامه اجرا شود.
شکل 1 : سرویس MediaSessionService به جلسه رسانه اجازه می‌دهد تا جدا از فعالیت برنامه اجرا شود.

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

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

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

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

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

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

کاتلین

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

  // Create your player and media session 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()
  }
}

جاوا

public 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 fun onTaskRemoved(rootIntent: Intent?) {
  pauseAllPlayersAndStopSelf()
}

جاوا

@Override
public void onTaskRemoved(@Nullable Intent rootIntent) {
  pauseAllPlayersAndStopSelf();
}

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

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

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

کاتلین

class PlaybackService : MediaSessionService() {
  private var mediaSession: MediaSession? = null
  // [...] lifecycle methods omitted

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

جاوا

public class PlaybackService extends MediaSessionService {
  private MediaSession mediaSession = null;
  // [...] 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 خود را در مانیفست با یک فیلتر intent از 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

در Activity یا Fragment حاوی رابط کاربری پخش‌کننده، می‌توانید با استفاده از 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() پاک کنید.

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

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

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

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

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

از سرگیری پخش

After the MediaSessionService has been terminated, and even after the device has been rebooted, it is possible to offer playback resumption to let users restart the service and resume playback where they left off. By default, playback resumption is turned off. This means the user can't resume playback when your service isn't running. To opt-in to this feature, you need to declare a media button receiver and implement the onPlaybackResumption method.

گیرنده دکمه رسانه 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>

پیاده‌سازی فراخوانی از سرگیری پخش

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

کاتلین

override fun onPlaybackResumption(
    mediaSession: MediaSession,
    controller: ControllerInfo
): ListenableFuture<MediaItemsWithStartPosition> {
  val settable = SettableFuture.create<MediaItemsWithStartPosition>()
  scope.launch {
    // 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()
    settable.set(resumptionPlaylist)
  }
  return settable
}

جاوا

@Override
public ListenableFuture<MediaItemsWithStartPosition> onPlaybackResumption(
    MediaSession mediaSession,
    ControllerInfo controller
) {
  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 پخش‌کننده را آماده کند و پس از اتمام فراخوانی، پخش را شروع کند.

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

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

یک سناریوی رایج، استفاده از یک MediaController در رابط کاربری برنامه برای کنترل پخش و نمایش لیست پخش است. در عین حال، جلسه در معرض کلاینت‌های خارجی مانند کنترل‌های رسانه‌ای اندروید و دستیار در موبایل یا تلویزیون، Wear OS برای ساعت‌ها و Android Auto در خودروها قرار می‌گیرد. برنامه آزمایشی جلسه Media3 نمونه‌ای از برنامه‌ای است که چنین سناریویی را پیاده‌سازی می‌کند.

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

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

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

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

کاتلین

override fun onConnect(
  session: MediaSession,
  controller: MediaSession.ControllerInfo
): ConnectionResult {
  if (session.isMediaNotificationController(controller)) {
    val sessionCommands =
      ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
        .add(customCommandSeekBackward)
        .add(customCommandSeekForward)
        .build()
    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 AcceptedResultBuilder(session)
      .setMediaButtonPreferences(
        ImmutableList.of(
          createSeekBackwardButton(customCommandSeekBackward),
          createSeekForwardButton(customCommandSeekForward))
      )
      .setAvailablePlayerCommands(playerCommands)
      .setAvailableSessionCommands(sessionCommands)
      .build()
  }
  // Default commands with default button preferences for all other controllers.
  return AcceptedResultBuilder(session).build()
}

جاوا

@Override
public ConnectionResult onConnect(
    MediaSession session, MediaSession.ControllerInfo controller) {
  if (session.isMediaNotificationController(controller)) {
    SessionCommands sessionCommands =
        ConnectionResult.DEFAULT_SESSION_COMMANDS
            .buildUpon()
            .add(customCommandSeekBackward)
            .add(customCommandSeekForward)
            .build();
    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 new AcceptedResultBuilder(session)
        .setMediaButtonPreferences(
            ImmutableList.of(
                createSeekBackwardButton(customCommandSeekBackward),
                createSeekForwardButton(customCommandSeekForward)))
        .setAvailablePlayerCommands(playerCommands)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
  // Default commands with default button preferences for all other controllers.
  return new AcceptedResultBuilder(session).build();
}

به اندروید اتو اجازه دهید دستورات سفارشی ارسال کند

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

کاتلین

override fun onConnect(
  session: MediaSession,
  controller: MediaSession.ControllerInfo
): ConnectionResult {
  val sessionCommands =
    ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon()
      .add(customCommandSeekBackward)
      .add(customCommandSeekForward)
      .build()
  if (session.isMediaNotificationController(controller)) {
    // [...] See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available session commands to accept incoming custom commands from Auto.
    return AcceptedResultBuilder(session)
      .setAvailableSessionCommands(sessionCommands)
      .build()
  }
  // Default commands for all other controllers.
  return AcceptedResultBuilder(session).build()
}

جاوا

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

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