أداة التبديل بين أجهزة التشغيل

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

نزِّل نموذج تطبيق CastVideos-android واستخدِمه لمعرفة كيفية تنفيذ ميزة "تبديل المُخرجات" في تطبيقك.

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

واجهة مستخدم "أداة التبديل بين أجهزة التشغيل"

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

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

  • سيتم تجاهل جلسات الوسائط التي تم إنشاؤها للتشغيل على الجهاز وإعادة إنشائها عند التبديل إلى إشعار Cast 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، يوضّح المقتطف أدناه كيفية إعدادهما، وتتوفّر أدلة لإعداد عمليات الاستدعاء لجلسة الوسائط لتطبيقات الصوت والفيديو:

Kotlin
// 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().

Kotlin
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().

Kotlin
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) لتطبيق Cast جلسة Cast تلقائيًا. ومع ذلك، يجب أن تتعامل التطبيقات مع التبديل من المحلي إلى البعيد، على سبيل المثال إيقاف التشغيل المحلي وتحميل الوسائط على جهاز Cast. يجب أن تستمع التطبيقات إلى طلبات Cast SessionManagerListener، باستخدام عمليات الاستدعاء onSessionStarted() و onSessionEnded()، والتعامل مع الإجراء عند تلقّي عمليات الاستدعاء SessionManager من Cast. يجب أن تضمن التطبيقات أنّ عمليات الاستدعاء هذه لا تزال نشطة عند فتح مربّع حوار "محوِّل الإخراج" وعدم تشغيل التطبيق في المقدّمة.

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

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

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

إذا كان التطبيق يستخدم MediaBrowserService، ننصحك بتسجيل SessionManagerListener هناك.

Kotlin
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);
    }
  }
}

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

من جهاز التحكّم عن بُعد إلى الجهاز المحلي

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

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

ضبط العلامة setRemoteToLocalEnabled

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

Kotlin
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 وأن تستأنف التشغيل محليًا عندما يكون التطبيق في المقدّمة أو الخلفية.

Kotlin
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 (على سبيل المثال، البيانات الوصفية للوسائط وموضع التشغيل). وعندما يتم نقل التطبيق من الخلفية إلى المقدّمة، يجب أن يستمر تشغيل المحتوى على الجهاز باستخدام المعلومات المخزّنة.

Kotlin
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.
    }
  }
}

من جهاز تحكّم عن بُعد إلى جهاز تحكّم عن بُعد

تتيح "أداة التبديل بين أجهزة التشغيل" إمكانية توسيع نطاق البث ليشمل عدة أجهزة مكبّرات صوت تتيح استخدام Google Cast لتطبيقات الصوت باستخدام ميزة "توسعة البث".

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

ميزة "توسعة البث" مع مكبّرات الصوت

تتيح ميزة "توسعة البث" لتطبيقات الصوت التي تستخدم "أداة تبديل المخرجات" إمكانية توسيع نطاق الصوت ليشمل عدة مكبّرات صوت تتيح استخدام Google Cast أثناء جلسة بث.

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

للحصول على اسم المجموعة الموسّعة الجديد أثناء توسعة البث، يجب تسجيل Cast.Listener باستخدام CastSession#addCastListener. بعد ذلك، يمكنك الاتصال بالرقم CastSession#getCastDevice() خلال معاودة الاتصال من خلال onDeviceNameChanged.

Kotlin
class MyActivity : Activity() {
    private var mCastSession: CastSession? = null
    private lateinit var mCastContext: CastContext
    private lateinit var mSessionManager: SessionManager
    private val mSessionManagerListener: SessionManagerListener<CastSession> =
        SessionManagerListenerImpl()
    private val mCastListener = CastListener()

    private inner class SessionManagerListenerImpl : SessionManagerListener<CastSession?> {
        override fun onSessionStarting(session: CastSession?) {}

        override fun onSessionStarted(session: CastSession?, sessionId: String) {
            addCastListener(session)
        }

        override fun onSessionStartFailed(session: CastSession?, error: Int) {}

        override fun onSessionSuspended(session: CastSession?, reason Int) {
            removeCastListener()
        }

        override fun onSessionResuming(session: CastSession?, sessionId: String) {}

        override fun onSessionResumed(session: CastSession?, wasSuspended: Boolean) {
            addCastListener(session)
        }

        override fun onSessionResumeFailed(session: CastSession?, error: Int) {}

        override fun onSessionEnding(session: CastSession?) {}

        override fun onSessionEnded(session: CastSession?, error: Int) {
            removeCastListener()
        }
    }

    private inner class CastListener : Cast.Listener() {
        override fun onDeviceNameChanged() {
            mCastSession?.let {
                val castDevice = it.castDevice
                val deviceName = castDevice.friendlyName
                // Update UIs with the new cast device name.
            }
        }
    }

    private fun addCastListener(castSession: CastSession) {
        mCastSession = castSession
        mCastSession?.addCastListener(mCastListener)
    }

    private fun removeCastListener() {
        mCastSession?.removeCastListener(mCastListener)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mCastContext = CastContext.getSharedInstance(this)
        mSessionManager = mCastContext.sessionManager
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession::class.java)
    }

    override fun onDestroy() {
        super.onDestroy()
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession::class.java)
    }
}
Java
public class MyActivity extends Activity {
    private CastContext mCastContext;
    private CastSession mCastSession;
    private SessionManager mSessionManager;
    private SessionManagerListener<CastSession> mSessionManagerListener =
            new SessionManagerListenerImpl();
    private Cast.Listener mCastListener = new CastListener();

    private class SessionManagerListenerImpl implements SessionManagerListener<CastSession> {
        @Override
        public void onSessionStarting(CastSession session) {}
        @Override
        public void onSessionStarted(CastSession session, String sessionId) {
            addCastListener(session);
        }
        @Override
        public void onSessionStartFailed(CastSession session, int error) {}
        @Override
        public void onSessionSuspended(CastSession session, int reason) {
            removeCastListener();
        }
        @Override
        public void onSessionResuming(CastSession session, String sessionId) {}
        @Override
        public void onSessionResumed(CastSession session, boolean wasSuspended) {
            addCastListener(session);
        }
        @Override
        public void onSessionResumeFailed(CastSession session, int error) {}
        @Override
        public void onSessionEnding(CastSession session) {}
        @Override
        public void onSessionEnded(CastSession session, int error) {
            removeCastListener();
        }
    }

    private class CastListener extends Cast.Listener {
         @Override
         public void onDeviceNameChanged() {
             if (mCastSession == null) {
                 return;
             }
             CastDevice castDevice = mCastSession.getCastDevice();
             String deviceName = castDevice.getFriendlyName();
             // Update UIs with the new cast device name.
         }
    }

    private void addCastListener(CastSession castSession) {
        mCastSession = castSession;
        mCastSession.addCastListener(mCastListener);
    }

    private void removeCastListener() {
        if (mCastSession != null) {
            mCastSession.removeCastListener(mCastListener);
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mCastContext = CastContext.getSharedInstance(this);
        mSessionManager = mCastContext.getSessionManager();
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession.class);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession.class);
    }
}

اختبار الاتصال بين جهازين عن بُعد

لاختبار الميزة، اتّبِع الخطوات التالية:

  1. يمكنك بث المحتوى إلى جهاز يتيح استخدام Google Cast باستخدام البث التقليدي أو من خلال البث من جهاز محلي إلى جهاز عن بُعد.
  2. افتح "مبدّل المخرجات" باستخدام إحدى نقاط الدخول.
  3. انقر على جهاز آخر يتيح استخدام Google Cast، وستوسّع تطبيقات الصوت المحتوى ليشمل الجهاز الإضافي، ما يؤدي إلى إنشاء مجموعة ديناميكية.
  4. انقر على الجهاز الذي يتيح استخدام Google Cast مرة أخرى، وستتم إزالته من المجموعة الديناميكية.