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

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

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

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

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

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

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

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

نقاط الإدخال

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

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

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

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

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

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

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

إعداد "إشعارات الوسائط"

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

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

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

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

من الجهاز إلى الجهاز البعيد

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

تعديل SessionManagerListener لبث المحتوى في الخلفية

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

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

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

تتيح لك أداة التبديل بين أجهزة التشغيل إمكانية نقل المحتوى من التشغيل عن بُعد إلى مكبّر صوت الهاتف أو جهاز بلوتوث محلي. ويمكن تفعيل ذلك من خلال ضبط العلامة 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. ترتيب callbacks المستبعَدة من جهاز بعيد إلى جهاز محلي هو:

  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.

توسيع نطاق البث باستخدام مكبّرات الصوت

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

تتوفّر هذه الميزة على منصة 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 مرة أخرى، وستتم إزالته من المجموعة الديناميكية.