مبدِّل النتائج

"أداة التبديل بين أجهزة التشغيل" هي ميزة في حزمة تطوير البرامج (SDK) لتقنية Cast تتيح النقل السلس للمحتوى بين التشغيل المحلي والبعيد عن بُعد بدءًا من الإصدار Android 13. والهدف من ذلك هو مساعدة تطبيقات المرسل على التحكم بسهولة وسرعة في مكان تشغيل المحتوى. "أداة التبديل بين أجهزة التشغيل" تستخدم مكتبة MediaRouter لتبديل تشغيل المحتوى بين مكبِّر صوت الهاتف والأجهزة المقترنة التي تتضمن بلوتوث والأجهزة التي تعمل عن بُعد والتي تعمل بتكنولوجيا Google Cast. يمكن تقسيم حالات الاستخدام إلى السيناريوهات التالية:

قم بتنزيل النموذج أدناه واستخدمه كمرجع حول كيفية تنفيذ مُبدل الإخراج في تطبيق الصوت. ويمكنك الاطلاع على README.md للحصول على إرشادات بشأن كيفية تشغيل النموذج.

تنزيل النموذج

يجب أن تكون "أداة التبديل بين أجهزة التشغيل" مُفعَّلة لإتاحة عمليات الاتصال عن بُعد أو عمليات الاتصال عن بُعد على المستوى المحلي باستخدام الخطوات التي يتناولها هذا الدليل. ليس هناك خطوات إضافية مطلوبة لإتاحة النقل بين مكبّرات صوت الجهاز المحلي وأجهزة بلوتوث المقترنة.

تطبيقات الصوت هي تطبيقات تتوافق مع Google Cast for Audio في إعدادات تطبيق الجهاز الاستقبال في وحدة تحكُّم مطوّري برامج Google Cast SDK.

واجهة مستخدم "أداة تبديل الإخراج"

تعرض "أداة التبديل بين أجهزة التشغيل" الأجهزة المحلية والبعيدة المتوفرة، بالإضافة إلى حالات الأجهزة الحالية، بما في ذلك اتصال الجهاز وحالته، إذا كان الجهاز محددًا. إذا كانت هناك أجهزة أخرى بالإضافة إلى الجهاز الحالي، يتيح لك النقر على جهاز آخر نقل تشغيل الوسائط إلى الجهاز المحدد.

المشاكل المعروفة

  • سيتم إغلاق جلسات الوسائط التي تم إنشاؤها للتشغيل المحلي وإعادة إنشائها عند التبديل إلى إشعار حزمة تطوير البرامج (SDK) الخاصة بالبث.

نقاط الإدخال

إشعار الوسائط

إذا نشر تطبيق ما إشعار وسائط باستخدام MediaSession للتشغيل المحلي (يتم التشغيل محليًا)، يعرض أعلى يسار إشعار الوسائط شريحة إشعار بها اسم الجهاز (مثل مكبّر صوت الهاتف) التي يتم تشغيل المحتوى عليها حاليًا. يؤدي النقر على شريحة الإشعارات إلى فتح واجهة المستخدم لنظام مربع الحوار "أداة تبديل الإخراج".

إعدادات مستوى الصوت

يمكن أيضًا تشغيل واجهة مستخدم نظام مربّع حوار "أداة تبديل الإخراج" من خلال النقر على أزرار مستوى الصوت الفعلية على الجهاز، والنقر على رمز الإعدادات في أسفل الشاشة، ثم النقر على نص "تشغيل <اسم التطبيق> على <جهاز البث>".

ملخّص الخطوات

المتطلّبات الأساسية

  1. يمكنك ترحيل تطبيق Android الحالي إلى AndroidX.
  2. يُرجى تحديث build.gradle في تطبيقك لاستخدام الحدّ الأدنى المطلوب من "حزمة تطوير البرامج (SDK) للمرسِل بواسطة Android" من أجل "أداة التبديل بين أجهزة التشغيل":
    dependencies {
      ...
      implementation 'com.google.android.gms:play-services-cast-framework:21.2.0'
      ...
    }
  3. يتيح التطبيق إشعارات الوسائط.
  4. جهاز يعمل بنظام التشغيل Android 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)
Java
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()
)
Java
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()
)
Java
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()
    );
}

سلوك إشعارات تطبيق الفيديو

يجب أن تتبع تطبيقات الفيديو أو التطبيقات الصوتية التي لا تتيح التشغيل المحلي في الخلفية سلوكًا محددًا لإشعارات الوسائط لتجنّب المشاكل المتعلقة بإرسال أوامر الوسائط في الحالات التي لا يكون فيها التشغيل متاحًا:

  • انشر إشعار الوسائط عند تشغيل الوسائط محليًا وكان التطبيق في المقدمة.
  • إيقاف التشغيل المحلي مؤقتًا وإغلاق الإشعار عندما يكون التطبيق في الخلفية.
  • عندما يعود التطبيق إلى المقدمة، يجب استئناف التشغيل على الجهاز وإعادة نشر الإشعار.

تفعيل "أداة التبديل بين الإخراج" في AndroidManifest.xml

لتفعيل "أداة التبديل بين أجهزة التشغيل"، يجب إضافة MediaTransferReceiver إلى AndroidManifest.xml في التطبيق. وإذا لم يكن مفعَّلاً، لن يتم تفعيل الميزة ولن تكون علامة ميزة "الاتصال عن بعد إلى المنطقة المحلية" صالحة أيضًا.

<application>
    ...
    <receiver
         android:name="androidx.mediarouter.media.MediaTransferReceiver"
         android:exported="true">
    </receiver>
    ...
</application>

MediaTransferReceiver هو مستقبِل بث يتيح نقل الوسائط بين الأجهزة من خلال واجهة مستخدم النظام. يُرجى الاطّلاع على مرجع MediaTransferReceiver للحصول على مزيد من المعلومات.

من المحلية إلى الاتصال عن بُعد

عندما يبدِّل المستخدم تشغيل المحتوى من الجهاز المحلي إلى جهاز التحكّم عن بُعد، ستبدأ حزمة تطوير البرامج (SDK) الخاصة بالبثّ تلقائيًا. ومع ذلك، يجب أن تتعامل التطبيقات مع التبديل من جهاز محلي إلى جهاز تحكّم عن بُعد، على سبيل المثال، إيقاف التشغيل المحلي وتحميل الوسائط على جهاز البث. يجب أن تستمع التطبيقات إلى ميزات البثّ SessionManagerListener، باستخدام onSessionStarted() وonSessionEnded() معاودة الاتصال، وتتعامل مع الإجراء عند تلقّي طلبات معاودة الاتصال SessionManager للإرسال. يجب أن تضمن التطبيقات أن هذه الاستدعاءات لا تزال نشطة عند فتح مربع حوار "أداة التبديل بين أجهزة التشغيل" ولا يكون التطبيق في المقدّمة.

تعديل SessionManagerListener للبث في الخلفية

تتيح تجربة البث القديمة إمكانية الاتصال عن بُعد بين الأجهزة المحلية عند تشغيل التطبيق في المقدّمة. تبدأ تجربة "البث" النموذجية عندما ينقر المستخدمون على رمز البثّ في التطبيق ويختارون جهازًا لبث الوسائط. في هذه الحالة، يجب على التطبيق التسجيل في SessionManagerListener، في onCreate() أو onStart() وإلغاء تسجيل المستمع في onStop() أو onDestroy() نشاط التطبيق.

بفضل التجربة الجديدة التي تتيح البث باستخدام "أداة التبديل بين أجهزة التشغيل"، يمكن للتطبيقات بدء البث عندما تعمل في الخلفية. وهذا مفيد بشكل خاص للتطبيقات الصوتية التي تنشر إشعارات عند تشغيلها في الخلفية. يمكن للتطبيقات تسجيل مستمعي 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)
        }
    }
}
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);
    }
  }
}

بفضل هذا التحديث، تعمل تقنية الاتصال عن بُعد بالطريقة نفسها التي تتّبعها في البث التقليدي عندما يكون التطبيق في الخلفية، وليس عليك اتّخاذ أي إجراءات إضافية للتبديل من أجهزة تتضمّن بلوتوث إلى أجهزة البث.

الوصول عن بعد إلى المناطق المحلية

تتيح لك هذه الأداة الانتقال من التشغيل عن بُعد إلى مكبِّر صوت الهاتف أو جهاز البلوتوث المحلي. ويمكن تفعيل ذلك من خلال ضبط علامة setRemoteToLocalEnabled على true في CastOptions.

في الحالات التي ينضم فيها جهاز المُرسِل الحالي إلى جلسة حالية مع عدة مُرسِلين ويحتاج التطبيق إلى التحقُّق ممّا إذا كان يُسمح بنقل الوسائط الحالية على الجهاز، يجب أن تستخدم التطبيقات معاودة الاتصال onTransferred بالرمز SessionTransferCallback للتحقّق من SessionState.

ضبط العلامة setRemoteToLocalEnabled

توفّر "CastOptions" setRemoteToLocalEnabled لإظهار أو إخفاء مكبّر صوت الهاتف وأجهزة البلوتوث المحلية كأهداف للنقل في مربّع حوار "أداة تبديل الإخراج" عندما تكون هناك جلسة بث نشِطة.

كوتلين
class CastOptionsProvider : OptionsProvider {
    fun getCastOptions(context: Context?): CastOptions {
        ...
        return Builder()
            ...
            .setRemoteToLocalEnabled(true)
            .build()
    }
}
Java
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.

SessionTransferCallback هي إضافة لعمليات استدعاء SessionManagerListener الحالية ويتم تشغيلها بعد تشغيل onSessionEnded. وبالتالي، يكون ترتيب استدعاءات الاتصالات المحلية عن بعد على النحو التالي:

  1. onTransferring
  2. onSessionEnding
  3. onSessionEnded
  4. onTransferred

وبما أنّه يمكن فتح "أداة التبديل بين أجهزة التشغيل" بواسطة شريحة إشعارات الوسائط عندما يكون التطبيق قيد التشغيل في الخلفية أثناء البث، تحتاج التطبيقات إلى معالجة عملية النقل إلى الإعدادات المحلية بشكل مختلف حسب ما إذا كانت تتيح التشغيل في الخلفية أم لا. في حال تعذُّر عملية النقل، سيتم تنشيط 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.
        }
    }
}
Java
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.
        }
    }
}
Java
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.
    }
  }
}