Output Switcher یکی از ویژگیهای Cast SDK است که انتقال بیوقفه بین پخش محلی و از راه دور محتوا را با شروع Android 13 امکانپذیر میکند. هدف کمک به برنامههای فرستنده است که به راحتی و به سرعت مکان پخش محتوا را کنترل کنند. Output Switcher از کتابخانه MediaRouter
برای تغییر پخش محتوا بین بلندگوی تلفن، دستگاههای بلوتوث جفت شده و دستگاههای دارای قابلیت Cast از راه دور استفاده میکند. موارد استفاده را می توان به سناریوهای زیر تقسیم کرد:
نمونه زیر را برای مرجع در مورد نحوه پیاده سازی Output Switcher در برنامه صوتی خود دانلود و استفاده کنید. برای دستورالعملهای مربوط به نحوه اجرای نمونه، به README.md همراه مراجعه کنید.
Output Switcher باید برای پشتیبانی از محلی به راه دور و از راه دور به محلی با استفاده از مراحل ارائه شده در این راهنما فعال باشد. هیچ مرحله اضافی برای پشتیبانی از انتقال بین بلندگوهای دستگاه محلی و دستگاه های بلوتوث جفت شده مورد نیاز نیست.
برنامههای صوتی برنامههایی هستند که از Google Cast for Audio در تنظیمات برنامه گیرنده در کنسول برنامهنویس Google Cast SDK پشتیبانی میکنند.
رابط کاربری سوئیچر خروجی
سوئیچر خروجی دستگاه های محلی و راه دور موجود و همچنین وضعیت فعلی دستگاه را نشان می دهد، از جمله در صورت انتخاب دستگاه، در حال اتصال، سطح صدای فعلی. اگر علاوه بر دستگاه فعلی دستگاه های دیگری نیز وجود دارد، با کلیک بر روی دستگاه دیگر می توانید پخش رسانه را به دستگاه انتخابی منتقل کنید.
مشکلات شناخته شده
- وقتی به اعلان Cast SDK بروید، جلسات رسانه ایجاد شده برای پخش محلی رد میشوند و دوباره ایجاد میشوند.
نقاط ورود
اطلاع رسانی رسانه ای
اگر یک برنامه یک اعلان رسانه با MediaSession
برای پخش محلی (پخش محلی) پست کند، گوشه سمت راست بالای اعلان رسانه یک تراشه اعلان را با نام دستگاه (مانند بلندگوی تلفن) که محتوا در حال پخش با آن است، نشان میدهد. با ضربه زدن بر روی تراشه اعلان، رابط کاربری سیستم گفت و گوی Output Switcher باز می شود.
تنظیمات میزان صدا
همچنین میتوانید با کلیک کردن روی دکمههای حجم فیزیکی دستگاه، ضربه زدن روی نماد تنظیمات در پایین، و ضربه زدن روی متن «Play <App Name> در <Cast Device>» فعال شود.
خلاصه مراحل
- از رعایت پیش نیازها اطمینان حاصل کنید
- گزینه Output Switcher را در AndroidManifest.xml فعال کنید
- SessionManagerListener را برای پخش پسزمینه بهروزرسانی کنید
- پرچم setRemoteToLocalEnabled را تنظیم کنید
- پخش را به صورت محلی ادامه دهید
پیش نیازها
- برنامه اندروید موجود خود را به AndroidX منتقل کنید.
-
build.gradle
برنامه خود را بهروزرسانی کنید تا از حداقل نسخه مورد نیاز Android Sender SDK برای خروجی Switcher استفاده کنید:dependencies { ... implementation 'com.google.android.gms:play-services-cast-framework:21.2.0' ... }
- برنامه از اعلان های رسانه پشتیبانی می کند.
- دستگاه دارای اندروید 13.
اعلانهای رسانه را تنظیم کنید
برای استفاده از سوئیچر خروجی، برنامههای صوتی و تصویری باید یک اعلان رسانه ایجاد کنند تا وضعیت پخش و کنترلهای رسانه خود را برای پخش محلی نمایش دهد. این کار مستلزم ایجاد MediaSession
، تنظیم MediaStyle
با توکن MediaSession
و تنظیم کنترلهای رسانه در اعلان است.
اگر در حال حاضر از MediaStyle
و MediaSession
استفاده نمیکنید، قطعه زیر نحوه تنظیم آنها را نشان میدهد و راهنماهایی برای تنظیم تماسهای جلسه رسانه برای برنامههای صوتی و تصویری موجود است:
// Create a media session. NotificationCompat.MediaStyle // PlayerService is your own Service or Activity responsible for media playback. val mediaSession = MediaSessionCompat(this, "PlayerService") // Create a MediaStyle object and supply your media session token to it. val mediaStyle = Notification.MediaStyle().setMediaSession(mediaSession.sessionToken) // Create a Notification which is styled by your MediaStyle object. // This connects your media session to the media controls. // Don't forget to include a small icon. val notification = Notification.Builder(this@PlayerService, CHANNEL_ID) .setStyle(mediaStyle) .setSmallIcon(R.drawable.ic_app_logo) .build() // Specify any actions which your users can perform, such as pausing and skipping to the next track. val pauseAction: Notification.Action = Notification.Action.Builder( pauseIcon, "Pause", pauseIntent ).build() notification.addAction(pauseAction)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { // Create a media session. NotificationCompat.MediaStyle // PlayerService is your own Service or Activity responsible for media playback. MediaSession mediaSession = new MediaSession(this, "PlayerService"); // Create a MediaStyle object and supply your media session token to it. Notification.MediaStyle mediaStyle = new Notification.MediaStyle().setMediaSession(mediaSession.getSessionToken()); // Specify any actions which your users can perform, such as pausing and skipping to the next track. Notification.Action pauseAction = Notification.Action.Builder(pauseIcon, "Pause", pauseIntent).build(); // Create a Notification which is styled by your MediaStyle object. // This connects your media session to the media controls. // Don't forget to include a small icon. String CHANNEL_ID = "CHANNEL_ID"; Notification notification = new Notification.Builder(this, CHANNEL_ID) .setStyle(mediaStyle) .setSmallIcon(R.drawable.ic_app_logo) .addAction(pauseAction) .build(); }
علاوه بر این، برای پر کردن اعلان با اطلاعات رسانه خود، باید فراداده و وضعیت پخش رسانه خود را به MediaSession
اضافه کنید.
برای افزودن متادیتا به MediaSession
، از setMetaData()
استفاده کنید و تمام ثابت های MediaMetadata
مربوطه را برای رسانه خود در MediaMetadataCompat.Builder()
ارائه دهید.
mediaSession.setMetadata(MediaMetadataCompat.Builder() // Title .putString(MediaMetadata.METADATA_KEY_TITLE, currentTrack.title) // Artist // Could also be the channel name or TV series. .putString(MediaMetadata.METADATA_KEY_ARTIST, currentTrack.artist) // Album art // Could also be a screenshot or hero image for video content // The URI scheme needs to be "content", "file", or "android.resource". .putString( MediaMetadata.METADATA_KEY_ALBUM_ART_URI, currentTrack.albumArtUri) ) // Duration // If duration isn't set, such as for live broadcasts, then the progress // indicator won't be shown on the seekbar. .putLong(MediaMetadata.METADATA_KEY_DURATION, currentTrack.duration) .build() )
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { mediaSession.setMetadata( new MediaMetadataCompat.Builder() // Title .putString(MediaMetadata.METADATA_KEY_TITLE, currentTrack.title) // Artist // Could also be the channel name or TV series. .putString(MediaMetadata.METADATA_KEY_ARTIST, currentTrack.artist) // Album art // Could also be a screenshot or hero image for video content // The URI scheme needs to be "content", "file", or "android.resource". .putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, currentTrack.albumArtUri) // Duration // If duration isn't set, such as for live broadcasts, then the progress // indicator won't be shown on the seekbar. .putLong(MediaMetadata.METADATA_KEY_DURATION, currentTrack.duration) .build() ); }
برای افزودن حالت پخش به MediaSession
، از setPlaybackState()
استفاده کنید و تمام ثابت های PlaybackStateCompat
مربوطه را برای رسانه خود در PlaybackStateCompat.Builder()
ارائه دهید.
mediaSession.setPlaybackState( PlaybackStateCompat.Builder() .setState( PlaybackStateCompat.STATE_PLAYING, // Playback position // Used to update the elapsed time and the progress bar. mediaPlayer.currentPosition.toLong(), // Playback speed // Determines the rate at which the elapsed time changes. playbackSpeed ) // isSeekable // Adding the SEEK_TO action indicates that seeking is supported // and makes the seekbar position marker draggable. If this is not // supplied seek will be disabled but progress will still be shown. .setActions(PlaybackStateCompat.ACTION_SEEK_TO) .build() )
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { mediaSession.setPlaybackState( new PlaybackStateCompat.Builder() .setState( PlaybackStateCompat.STATE_PLAYING, // Playback position // Used to update the elapsed time and the progress bar. mediaPlayer.currentPosition.toLong(), // Playback speed // Determines the rate at which the elapsed time changes. playbackSpeed ) // isSeekable // Adding the SEEK_TO action indicates that seeking is supported // and makes the seekbar position marker draggable. If this is not // supplied seek will be disabled but progress will still be shown. .setActions(PlaybackStateCompat.ACTION_SEEK_TO) .build() ); }
رفتار اعلان برنامه ویدیویی
برنامههای ویدیویی یا برنامههای صوتی که از پخش محلی در پسزمینه پشتیبانی نمیکنند، باید رفتار خاصی برای اعلانهای رسانه داشته باشند تا در شرایطی که پخش پشتیبانی نمیشود، مشکلی در ارسال دستورات رسانهای ایجاد نشود:
- هنگام پخش رسانه به صورت محلی، اعلان رسانه را ارسال کنید و برنامه در پیش زمینه است.
- پخش محلی را متوقف کنید و وقتی برنامه در پسزمینه است، اعلان را رد کنید.
- وقتی برنامه به پیشزمینه برمیگردد، پخش محلی باید از سر گرفته شود و اعلان دوباره ارسال شود.
گزینه Output Switcher را در AndroidManifest.xml فعال کنید
برای فعال کردن Output Switcher، MediaTransferReceiver
باید به AndroidManifest.xml
برنامه اضافه شود. اگر اینطور نباشد، این ویژگی فعال نخواهد شد و پرچم ویژگی راه دور به محلی نیز نامعتبر خواهد بود.
<application>
...
<receiver
android:name="androidx.mediarouter.media.MediaTransferReceiver"
android:exported="true">
</receiver>
...
</application>
MediaTransferReceiver
یک گیرنده پخش است که انتقال رسانه را در بین دستگاه های دارای رابط کاربری سیستم امکان پذیر می کند. برای اطلاعات بیشتر به مرجع MediaTransferReceiver مراجعه کنید.
محلی به راه دور
هنگامی که کاربر پخش را از محلی به راه دور تغییر می دهد، Cast SDK به طور خودکار جلسه Cast را شروع می کند. با این حال، برنامهها باید جابجایی از محلی به راه دور را انجام دهند، برای مثال پخش محلی را متوقف کنند و رسانه را در دستگاه Cast بارگیری کنند. برنامهها باید با استفاده از تماسهای onSessionStarted()
و onSessionEnded()
به Cast SessionManagerListener
گوش دهند و هنگام دریافت تماسهای Cast SessionManager
این عمل را انجام دهند. برنامهها باید اطمینان حاصل کنند که وقتی کادر گفتگوی «تغییرگر خروجی» باز میشود و برنامه در پیشزمینه نیست، این تماسها همچنان زنده هستند.
SessionManagerListener را برای پخش پسزمینه بهروزرسانی کنید
وقتی برنامه در پیشزمینه است، تجربه Cast قدیمی از حالت محلی به راه دور پشتیبانی میکند. یک تجربه Cast معمولی زمانی شروع میشود که کاربران روی نماد Cast در برنامه کلیک میکنند و دستگاهی را برای پخش جریانی رسانه انتخاب میکنند. در این مورد، برنامه باید در SessionManagerListener
، در onCreate()
یا onStart()
ثبت نام کند و شنونده را در onStop()
یا onDestroy()
فعالیت برنامه لغو ثبت کند.
با تجربه جدید ارسال محتوا با استفاده از Output Switcher، برنامهها میتوانند زمانی که در پسزمینه هستند، ارسال را شروع کنند. این به ویژه برای برنامه های صوتی که هنگام پخش در پس زمینه اعلان ارسال می کنند مفید است. برنامهها میتوانند شنوندگان SessionManager
را در onCreate()
سرویس ثبت کنند و در onDestroy()
سرویس لغو ثبت کنند. به این ترتیب، وقتی برنامه در پسزمینه است، برنامهها باید همیشه تماسهای محلی به راه دور (مانند onSessionStarted
) را دریافت کنند.
اگر برنامه از MediaBrowserService
استفاده می کند، توصیه می شود SessionManagerListener
در آنجا ثبت کنید.
class MyService : Service() { private var castContext: CastContext? = null protected fun onCreate() { castContext = CastContext.getSharedInstance(this) castContext .getSessionManager() .addSessionManagerListener(sessionManagerListener, CastSession::class.java) } protected fun onDestroy() { if (castContext != null) { castContext .getSessionManager() .removeSessionManagerListener(sessionManagerListener, CastSession::class.java) } } }
public class MyService extends Service { private CastContext castContext; @Override protected void onCreate() { castContext = CastContext.getSharedInstance(this); castContext .getSessionManager() .addSessionManagerListener(sessionManagerListener, CastSession.class); } @Override protected void onDestroy() { if (castContext != null) { castContext .getSessionManager() .removeSessionManagerListener(sessionManagerListener, CastSession.class); } } }
با استفاده از این بهروزرسانی، زمانی که برنامه در پسزمینه است و برای جابهجایی از دستگاههای بلوتوث به دستگاههای Cast نیازی به کار اضافی نیست، «لکهبهراهدور» همانند ارسالهای سنتی عمل میکند.
از راه دور به محلی
سوئیچر خروجی توانایی انتقال از پخش از راه دور به بلندگوی تلفن یا دستگاه بلوتوث محلی را فراهم می کند. این را می توان با تنظیم پرچم setRemoteToLocalEnabled
روی true
در CastOptions
فعال کرد.
برای مواردی که دستگاه فرستنده فعلی به یک جلسه موجود با چندین فرستنده ملحق می شود و برنامه باید بررسی کند که آیا رسانه فعلی مجاز به انتقال محلی است یا خیر، برنامه ها باید از پاسخ به تماس onTransferred
SessionTransferCallback
برای بررسی SessionState
استفاده کنند.
پرچم setRemoteToLocalEnabled را تنظیم کنید
CastOptions
یک setRemoteToLocalEnabled
برای نشان دادن یا پنهان کردن بلندگوی تلفن و دستگاههای بلوتوث محلی به عنوان اهداف انتقال به اهداف در گفتگوی Output Switcher در زمانی که یک جلسه Cast فعال وجود دارد، فراهم میکند.
class CastOptionsProvider : OptionsProvider { fun getCastOptions(context: Context?): CastOptions { ... return Builder() ... .setRemoteToLocalEnabled(true) .build() } }
public class CastOptionsProvider implements OptionsProvider { @Override public CastOptions getCastOptions(Context context) { ... return new CastOptions.Builder() ... .setRemoteToLocalEnabled(true) .build() } }
پخش را به صورت محلی ادامه دهید
برنامههایی که از راه دور به محلی پشتیبانی میکنند باید SessionTransferCallback
را ثبت کنند تا هنگام وقوع رویداد مطلع شوند تا بتوانند بررسی کنند که آیا رسانه باید مجاز به انتقال و ادامه پخش به صورت محلی باشد.
CastContext#addSessionTransferCallback(SessionTransferCallback)
به برنامه اجازه می دهد تا SessionTransferCallback
خود را ثبت کند و هنگامی که فرستنده به پخش محلی منتقل می شود، به تماس های onTransferred
و onTransferFailed
گوش دهد.
پس از اینکه برنامه SessionTransferCallback
خود را لغو ثبت کرد، برنامه دیگر SessionTransferCallback
s را دریافت نخواهد کرد.
SessionTransferCallback
یک فرمت از تماس های SessionManagerListener
موجود است و پس از راه اندازی onSessionEnded
فعال می شود. بنابراین، ترتیب تماسهای تماس از راه دور به محلی به صورت زیر است:
-
onTransferring
-
onSessionEnding
-
onSessionEnded
-
onTransferred
از آنجایی که وقتی برنامه در پسزمینه است و در حال ارسال است، میتوان Output Switcher را توسط تراشه اعلان رسانه باز کرد، برنامهها بسته به اینکه از پخش پسزمینه پشتیبانی میکنند یا نه، باید انتقال به محلی را متفاوت انجام دهند. در مورد انتقال ناموفق، onTransferFailed
در هر زمانی که خطا رخ دهد فعال می شود.
برنامه هایی که از پخش پس زمینه پشتیبانی می کنند
برای برنامههایی که از پخش در پسزمینه پشتیبانی میکنند (معمولاً برنامههای صوتی)، توصیه میشود از یک Service
(به عنوان مثال MediaBrowserService
) استفاده کنید. سرویسها باید به تماسهای onTransferred
گوش دهند و پخش را به صورت محلی از سر بگیرند، هم زمانی که برنامه در پیشزمینه یا پسزمینه است.
class MyService : Service() { private var castContext: CastContext? = null private var sessionTransferCallback: SessionTransferCallback? = null protected fun onCreate() { castContext = CastContext.getSharedInstance(this) castContext.getSessionManager() .addSessionManagerListener(sessionManagerListener, CastSession::class.java) sessionTransferCallback = MySessionTransferCallback() castContext.addSessionTransferCallback(sessionTransferCallback) } protected fun onDestroy() { if (castContext != null) { castContext.getSessionManager() .removeSessionManagerListener(sessionManagerListener, CastSession::class.java) if (sessionTransferCallback != null) { castContext.removeSessionTransferCallback(sessionTransferCallback) } } } class MySessionTransferCallback : SessionTransferCallback() { fun onTransferring(@SessionTransferCallback.TransferType transferType: Int) { // Perform necessary steps prior to onTransferred } fun onTransferred(@SessionTransferCallback.TransferType transferType: Int, sessionState: SessionState?) { if (transferType == SessionTransferCallback.TRANSFER_TYPE_FROM_REMOTE_TO_LOCAL) { // Remote stream is transferred to the local device. // Retrieve information from the SessionState to continue playback on the local player. } } fun onTransferFailed(@SessionTransferCallback.TransferType transferType: Int, @SessionTransferCallback.TransferFailedReason transferFailedReason: Int) { // Handle transfer failure. } } }
public class MyService extends Service { private CastContext castContext; private SessionTransferCallback sessionTransferCallback; @Override protected void onCreate() { castContext = CastContext.getSharedInstance(this); castContext.getSessionManager() .addSessionManagerListener(sessionManagerListener, CastSession.class); sessionTransferCallback = new MySessionTransferCallback(); castContext.addSessionTransferCallback(sessionTransferCallback); } @Override protected void onDestroy() { if (castContext != null) { castContext.getSessionManager() .removeSessionManagerListener(sessionManagerListener, CastSession.class); if (sessionTransferCallback != null) { castContext.removeSessionTransferCallback(sessionTransferCallback); } } } public static class MySessionTransferCallback extends SessionTransferCallback { public MySessionTransferCallback() {} @Override public void onTransferring(@SessionTransferCallback.TransferType int transferType) { // Perform necessary steps prior to onTransferred } @Override public void onTransferred(@SessionTransferCallback.TransferType int transferType, SessionState sessionState) { if (transferType==SessionTransferCallback.TRANSFER_TYPE_FROM_REMOTE_TO_LOCAL) { // Remote stream is transferred to the local device. // Retrieve information from the SessionState to continue playback on the local player. } } @Override public void onTransferFailed(@SessionTransferCallback.TransferType int transferType, @SessionTransferCallback.TransferFailedReason int transferFailedReason) { // Handle transfer failure. } } }
برنامه هایی که از پخش پس زمینه پشتیبانی نمی کنند
برای برنامههایی که از پخش پسزمینه پشتیبانی نمیکنند (معمولاً برنامههای ویدیویی)، توصیه میشود به تماسهای onTransferred
گوش دهید و اگر برنامه در پیشزمینه است، پخش را به صورت محلی از سر بگیرید.
اگر برنامه در پسزمینه باشد، باید پخش را متوقف کند و اطلاعات لازم را از SessionState
ذخیره کند (مثلاً ابرداده رسانه و موقعیت پخش). هنگامی که برنامه از پس زمینه پیش زمینه می شود، پخش محلی باید با اطلاعات ذخیره شده ادامه یابد.
class MyActivity : AppCompatActivity() { private var castContext: CastContext? = null private var sessionTransferCallback: SessionTransferCallback? = null protected fun onCreate() { castContext = CastContext.getSharedInstance(this) castContext.getSessionManager() .addSessionManagerListener(sessionManagerListener, CastSession::class.java) sessionTransferCallback = MySessionTransferCallback() castContext.addSessionTransferCallback(sessionTransferCallback) } protected fun onDestroy() { if (castContext != null) { castContext.getSessionManager() .removeSessionManagerListener(sessionManagerListener, CastSession::class.java) if (sessionTransferCallback != null) { castContext.removeSessionTransferCallback(sessionTransferCallback) } } } class MySessionTransferCallback : SessionTransferCallback() { fun onTransferring(@SessionTransferCallback.TransferType transferType: Int) { // Perform necessary steps prior to onTransferred } fun onTransferred(@SessionTransferCallback.TransferType transferType: Int, sessionState: SessionState?) { if (transferType == SessionTransferCallback.TRANSFER_TYPE_FROM_REMOTE_TO_LOCAL) { // Remote stream is transferred to the local device. // Retrieve information from the SessionState to continue playback on the local player. } } fun onTransferFailed(@SessionTransferCallback.TransferType transferType: Int, @SessionTransferCallback.TransferFailedReason transferFailedReason: Int) { // Handle transfer failure. } } }
public class MyActivity extends AppCompatActivity { private CastContext castContext; private SessionTransferCallback sessionTransferCallback; @Override protected void onCreate() { castContext = CastContext.getSharedInstance(this); castContext .getSessionManager() .addSessionManagerListener(sessionManagerListener, CastSession.class); sessionTransferCallback = new MySessionTransferCallback(); castContext.addSessionTransferCallback(sessionTransferCallback); } @Override protected void onDestroy() { if (castContext != null) { castContext .getSessionManager() .removeSessionManagerListener(sessionManagerListener, CastSession.class); if (sessionTransferCallback != null) { castContext.removeSessionTransferCallback(sessionTransferCallback); } } } public static class MySessionTransferCallback extends SessionTransferCallback { public MySessionTransferCallback() {} @Override public void onTransferring(@SessionTransferCallback.TransferType int transferType) { // Perform necessary steps prior to onTransferred } @Override public void onTransferred(@SessionTransferCallback.TransferType int transferType, SessionState sessionState) { if (transferType==SessionTransferCallback.TRANSFER_TYPE_FROM_REMOTE_TO_LOCAL) { // Remote stream is transferred to the local device. // Retrieve information from the SessionState to continue playback on the local player. } } @Override public void onTransferFailed(@SessionTransferCallback.TransferType int transferType, @SessionTransferCallback.TransferFailedReason int transferFailedReason) { // Handle transfer failure. } } }