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

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

از MediaSessionService استفاده کنید

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

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

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

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

چرخه عمر سرویس را پیاده سازی کنید

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

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

کاتلین

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()
  }

  // The user dismissed the app from the recent tasks
  override fun onTaskRemoved(rootIntent: Intent?) {
    val player = mediaSession?.player!!
    if (!player.playWhenReady
        || player.mediaItemCount == 0
        || player.playbackState == Player.STATE_ENDED) {
      // Stop the service if not playing, continue playing in the background
      // otherwise.
      stopSelf()
    }
  }

  // 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();
  }

  // The user dismissed the app from the recent tasks
  @Override
  public void onTaskRemoved(@Nullable Intent rootIntent) {
    Player player = mediaSession.getPlayer();
    if (!player.getPlayWhenReady()
        || player.getMediaItemCount() == 0
        || player.getPlaybackState() == Player.STATE_ENDED) {
      // Stop the service if not playing, continue playing in the background
      // otherwise.
      stopSelf();
    }
  }

  // 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?) {
  val player = mediaSession.player
  if (player.playWhenReady) {
    // Make sure the service is not in foreground.
    player.pause()
  }
  stopSelf()
}

جاوا

@Override
public void onTaskRemoved(@Nullable Intent rootIntent) {
  Player player = mediaSession.getPlayer();
  if (player.getPlayWhenReady()) {
    // Make sure the service is not in foreground.
    player.pause();
  }
  stopSelf();
}

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

روش 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 را به مانیفست اضافه کنید، و اگر API 34 و بالاتر را هدف قرار می دهید FOREGROUND_SERVICE_MEDIA_PLAYBACK نیز اضافه کنید:

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

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

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

هنگامی که برنامه شما روی دستگاهی با Android 10 (سطح API 29) و بالاتر اجرا می شود، باید یک foregroundServiceType که شامل mediaPlayback باشد، تعریف کنید.

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

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

دستورات UI را مدیریت کنید

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();

برنامه‌ها می‌توانند دکمه‌های فرمان کنترل‌های Android Media را سفارشی کنند. درباره سفارشی کردن کنترل‌های Android Media بیشتر بخوانید .

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

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

از سرگیری پخش

دکمه‌های رسانه، دکمه‌های سخت‌افزاری هستند که در دستگاه‌های Android و سایر دستگاه‌های جانبی، مانند دکمه پخش یا مکث در هدست بلوتوث یافت می‌شوند. Media3 ورودی های دکمه رسانه را برای شما در هنگام اجرای سرویس کنترل می کند.

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

Media3 دارای یک API است که به کاربران امکان می دهد پس از پایان برنامه و حتی پس از راه اندازی مجدد دستگاه، پخش را از سر بگیرند. به طور پیش فرض، از سرگیری پخش خاموش است. این بدان معناست که کاربر نمی تواند پخش را در زمانی که سرویس شما اجرا نمی کند، از سر بگیرد. برای شرکت کردن، ابتدا 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 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 and the start position
    // to use here
    MediaItemsWithStartPosition resumptionPlaylist = restorePlaylist();
    settableFuture.set(resumptionPlaylist);
  }, MoreExecutors.directExecutor());
  return settableFuture;
}

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

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

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

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

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

درک این نکته مهم است که این کنترلرهای قدیمی یا چارچوب همان مقادیر را از چارچوب PlaybackState.getActions() و PlaybackState.getCustomActions() می خوانند. برای تعیین اقدامات و اقدامات سفارشی جلسه چارچوب، یک برنامه می تواند از کنترلر اعلان رسانه استفاده کند و دستورات موجود و طرح بندی سفارشی خود را تنظیم کند. این سرویس کنترل‌کننده اعلان رسانه را به جلسه شما متصل می‌کند، و جلسه از ConnectionResult بازگردانده شده توسط onConnect() پاسخ تماس شما برای پیکربندی اقدامات و اقدامات سفارشی جلسه چارچوب استفاده می‌کند.

با توجه به یک سناریوی فقط برای موبایل، یک برنامه می‌تواند پیاده‌سازی 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 layout and available commands to configure the legacy/framework session.
    return AcceptedResultBuilder(session)
      .setCustomLayout(
        ImmutableList.of(
          createSeekBackwardButton(customCommandSeekBackward),
          createSeekForwardButton(customCommandSeekForward))
      )
      .setAvailablePlayerCommands(playerCommands)
      .setAvailableSessionCommands(sessionCommands)
      .build()
  }
  // Default commands with default custom layout 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 layout and available commands to configure the legacy/framework session.
    return new AcceptedResultBuilder(session)
        .setCustomLayout(
            ImmutableList.of(
                createSeekBackwardButton(customCommandSeekBackward),
                createSeekForwardButton(customCommandSeekForward)))
        .setAvailablePlayerCommands(playerCommands)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
  // Default commands without default custom layout for all other controllers.
  return new AcceptedResultBuilder(session).build();
}

مجوز Android Auto برای ارسال دستورات سفارشی

هنگام استفاده از 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 with default custom layout 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 without default custom layout for all other controllers.
  return new AcceptedResultBuilder(session).build();
}

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