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

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

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

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

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

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

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

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

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

  • سيتم إغلاق "جلسات الوسائط" التي تم إنشاؤها للتشغيل المحلي وإعادة إنشائها عند التبديل إلى إشعار Cast SDK.

نقاط الإدخال

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

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

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

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

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

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

  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 هو جهاز استقبال بث يتيح نقل الوسائط بين الأجهزة التي تحتوي على واجهة مستخدم النظام. راجع مرجع Media TransferSubmitr للحصول على مزيد من المعلومات.

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

عندما يبدِّل المستخدم عملية التشغيل من الجهاز المحلي إلى جهاز التحكّم عن بُعد، ستبدأ حزمة تطوير البرامج (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.
    }
  }
}