دمج ميزة البث في تطبيق Android

يوضّح دليل المطوِّر هذا طريقة إضافة دعم Google Cast إلى تطبيق المرسِل على Android باستخدام حزمة Android Sender SDK.

الجهاز الجوّال أو الكمبيوتر المحمول هو المُرسِل الذي يتحكم في التشغيل، وجهاز Google Cast هو جهاز الاستقبال الذي يعرض المحتوى على التلفزيون.

يشير إطار عمل المُرسِل إلى البرنامج الثنائي لمكتبة فئة الإرسال والموارد المرتبطة بها المتوفّرة في وقت التشغيل على المُرسِل. يشير تطبيق المُرسِل أو تطبيق البث إلى تطبيق يعمل أيضًا على المُرسِل. ويشير تطبيق مستقبِل الويب إلى تطبيق HTML الذي يعمل على الجهاز الذي يعمل بتكنولوجيا Google Cast.

يستخدم إطار عمل المرسل تصميمًا غير متزامن لمعاودة الاتصال لإبلاغ تطبيق المرسِل بالأحداث وللانتقال بين الحالات المختلفة لدورة حياة تطبيق البثّ.

مسار التطبيق

تصف الخطوات التالية تدفق التنفيذ عالي المستوى النموذجي لتطبيق Android للمرسل:

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

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

إعداد بيان Android

يتطلب ملف AndroidManifest.xml من تطبيقك إعداد العناصر التالية لـ Cast SDK:

uses-sdk

يمكنك ضبط الحد الأدنى لمستويات واجهة برمجة التطبيقات Android API واستهدافها التي تتوافق مع Cast SDK. الحدّ الأدنى حاليًا هو المستوى 21 من واجهة برمجة التطبيقات والهدف هو المستوى 28 من واجهة برمجة التطبيقات.

<uses-sdk
        android:minSdkVersion="21"
        android:targetSdkVersion="28" />

android:theme

حدِّد مظهر تطبيقك استنادًا إلى الحد الأدنى لإصدار حزمة تطوير البرامج (SDK) لنظام التشغيل Android. على سبيل المثال، إذا لم تكن تريد تنفيذ المظهر الخاص بك، عليك استخدام أحد صيغ Theme.AppCompat عند استهداف حد أدنى من إصدار حزمة تطوير البرامج (SDK) لنظام التشغيل Android يكون الذي يسبق إصدار Lollipop.

<application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.AppCompat" >
       ...
</application>

تهيئة سياق البث

يشتمل إطار العمل على عنصر سينغلتون عالمي، وهو CastContext، يعمل على تنسيق جميع تفاعلات إطار العمل.

يجب أن ينفِّذ تطبيقك واجهة OptionsProvider لتوفير خيارات العرض اللازمة لإعداد CastContext سينغلتون. توفر OptionsProvider مثيلاً عن CastOptions الذي يحتوي على خيارات تؤثر في سلوك إطار العمل. وأهم هذه العناصر هو معرّف تطبيق "جهاز استقبال الويب" الذي يُستخدم لفلترة نتائج الاكتشاف وتشغيل تطبيق جهاز استقبال الويب عند بدء جلسة البث.

كولين
class CastOptionsProvider : OptionsProvider {
    override fun getCastOptions(context: Context): CastOptions {
        return Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .build()
    }

    override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
        return null
    }
}
Java
public class CastOptionsProvider implements OptionsProvider {
    @Override
    public CastOptions getCastOptions(Context context) {
        CastOptions castOptions = new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .build();
        return castOptions;
    }
    @Override
    public List<SessionProvider> getAdditionalSessionProviders(Context context) {
        return null;
    }
}

يجب الإفصاح عن الاسم المؤهّل بالكامل لتطبيق OptionsProvider الذي تم تنفيذه كحقل بيانات وصفية في ملف AndroidManifest.xml من تطبيق المرسِل:

<application>
    ...
    <meta-data
        android:name=
            "com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
        android:value="com.foo.CastOptionsProvider" />
</application>

يتم إعداد CastContext بطريقة كسول عند استدعاء CastContext.getSharedInstance().

كولين
class MyActivity : FragmentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        val castContext = CastContext.getSharedInstance(this)
    }
}
Java
public class MyActivity extends FragmentActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        CastContext castContext = CastContext.getSharedInstance(this);
    }
}

التطبيقات المصغّرة لتجربة المستخدم على Google Cast

يوفر إطار عمل البث التطبيقات المصغّرة التي تتوافق مع قائمة التحقق من تصميم البث:

  • تراكب تمهيدي: يوفّر إطار العمل طريقة عرض مخصّصة، IntroductoryOverlay، تظهر للمستخدم للفت الانتباه إلى زر البثّ في المرة الأولى التي يتوفّر فيها جهاز استقبال. يمكن لتطبيق Sender تخصيص النص وموضع نص العنوان.

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

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

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

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

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

يتضمن الدليل التالي وصفًا لكيفية إضافة هذه الأدوات إلى تطبيقك.

إضافة زر بث

تم تصميم واجهات برمجة تطبيقات MediaRouter Android لتفعيل عرض الوسائط وتشغيلها على الأجهزة الثانوية. يجب أن تتضمّن تطبيقات Android التي تستخدم MediaRouter API زر بث كجزء من واجهة المستخدم لكي تسمح للمستخدمين باختيار مسار وسائط لتشغيل الوسائط على جهاز ثانوي مثل جهاز بث.

يسهّل إطار العمل إضافة MediaRouteButton كـ Cast button. يجب أولاً إضافة عنصر قائمة أو MediaRouteButton في ملف XML الذي يحدّد قائمتك، واستخدام CastButtonFactory لربطه بإطار العمل.

// To add a Cast button, add the following snippet.
// menu.xml
<item
    android:id="@+id/media_route_menu_item"
    android:title="@string/media_route_menu_title"
    app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
    app:showAsAction="always" />
Kotlin
// Then override the onCreateOptionMenu() for each of your activities.
// MyActivity.kt
override fun onCreateOptionsMenu(menu: Menu): Boolean {
    super.onCreateOptionsMenu(menu)
    menuInflater.inflate(R.menu.main, menu)
    CastButtonFactory.setUpMediaRouteButton(
        applicationContext,
        menu,
        R.id.media_route_menu_item
    )
    return true
}
Java
// Then override the onCreateOptionMenu() for each of your activities.
// MyActivity.java
@Override public boolean onCreateOptionsMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);
    getMenuInflater().inflate(R.menu.main, menu);
    CastButtonFactory.setUpMediaRouteButton(getApplicationContext(),
                                            menu,
                                            R.id.media_route_menu_item);
    return true;
}

بعد ذلك، إذا كان Activity يكتسب من FragmentActivity، يمكنك إضافة MediaRouteButton إلى التنسيق.

// activity_layout.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:gravity="center_vertical"
   android:orientation="horizontal" >

   <androidx.mediarouter.app.MediaRouteButton
       android:id="@+id/media_route_button"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_weight="1"
       android:mediaRouteTypes="user"
       android:visibility="gone" />

</LinearLayout>
Kotlin
// MyActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_layout)

    mMediaRouteButton = findViewById<View>(R.id.media_route_button) as MediaRouteButton
    CastButtonFactory.setUpMediaRouteButton(applicationContext, mMediaRouteButton)

    mCastContext = CastContext.getSharedInstance(this)
}
Java
// MyActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_layout);

   mMediaRouteButton = (MediaRouteButton) findViewById(R.id.media_route_button);
   CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), mMediaRouteButton);

   mCastContext = CastContext.getSharedInstance(this);
}

لضبط مظهر زر البث باستخدام مظهر، يمكنك الاطّلاع على تخصيص زر البث.

ضبط ميزة اكتشاف الأجهزة

تتم إدارة اكتشاف الجهاز بالكامل من خلال CastContext. عند إعداد CastContext، يحدّد تطبيق المُرسِل رقم تعريف تطبيق "جهاز استقبال الويب" ويمكنه طلب فلترة مساحة الاسم اختياريًا عن طريق ضبط supportedNamespaces في CastOptions. يشير CastContext إلى MediaRouter داخليًا، وسيبدأ عملية الاكتشاف في حال استيفاء الشروط التالية:

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

وسيتم إيقاف عملية الاكتشاف عند إغلاق مربّع حوار البث أو دخول تطبيق المرسِل إلى الخلفية.

كولين
class CastOptionsProvider : OptionsProvider {
    companion object {
        const val CUSTOM_NAMESPACE = "urn:x-cast:custom_namespace"
    }

    override fun getCastOptions(appContext: Context): CastOptions {
        val supportedNamespaces: MutableList<String> = ArrayList()
        supportedNamespaces.add(CUSTOM_NAMESPACE)

        return CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setSupportedNamespaces(supportedNamespaces)
            .build()
    }

    override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
        return null
    }
}
Java
class CastOptionsProvider implements OptionsProvider {
    public static final String CUSTOM_NAMESPACE = "urn:x-cast:custom_namespace";

    @Override
    public CastOptions getCastOptions(Context appContext) {
        List<String> supportedNamespaces = new ArrayList<>();
        supportedNamespaces.add(CUSTOM_NAMESPACE);

        CastOptions castOptions = new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setSupportedNamespaces(supportedNamespaces)
            .build();
        return castOptions;
    }

    @Override
    public List<SessionProvider> getAdditionalSessionProviders(Context context) {
        return null;
    }
}

آلية عمل إدارة الجلسات

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

تتم إدارة الجلسات من خلال الصف SessionManager، الذي يمكن لتطبيقك الوصول إليه من خلال CastContext.getSessionManager(). ويتم تمثيل الجلسات الفردية بفئات فرعية من الفئة Session. على سبيل المثال، يمثل الرمز CastSession الجلسات مع أجهزة البث. يمكن لتطبيقك الوصول إلى جلسة البث النشطة حاليًا من خلال SessionManager.getCurrentCastSession().

يمكن أن يستخدم تطبيقك الفئة SessionManagerListener لمراقبة أحداث الجلسات، مثل إنشائها وتعليقها واستئنافها وإنهائها. يحاول إطار العمل تلقائيًا استئنافه بعد حدوث الإنهاء غير الطبيعي/المفاجئ عندما كانت الجلسة نشطة.

يتم إنشاء الجلسات وإنهاؤها تلقائيًا استجابةً لإيماءات المستخدم من مربّعات حوار MediaRouter.

لفهم أخطاء بدء البث بشكل أفضل، يمكن للتطبيقات استخدام CastContext#getCastReasonCodeForCastStatusCode(int) لتحويل خطأ بدء الجلسة إلى CastReasonCodes. يُرجى العِلم أنّ بعض أخطاء بدء الجلسة (مثل CastReasonCodes#CAST_CANCELLED) تعدّ سلوكًا مقصودًا ويجب عدم تسجيلها كخطأ.

إذا كنت بحاجة إلى الانتباه إلى تغييرات الحالة الخاصة بالجلسة، يمكنك تطبيق علامة SessionManagerListener. يرصد هذا المثال مدى توفّر CastSession في Activity.

كولين
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 inner class SessionManagerListenerImpl : SessionManagerListener<CastSession?> {
        override fun onSessionStarting(session: CastSession?) {}

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

        override fun onSessionStartFailed(session: CastSession?, error: Int) {
            val castReasonCode = mCastContext.getCastReasonCodeForCastStatusCode(error)
            // Handle error
        }

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

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

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

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

        override fun onSessionEnding(session: CastSession?) {}

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

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mCastContext = CastContext.getSharedInstance(this)
        mSessionManager = mCastContext.sessionManager
    }

    override fun onResume() {
        super.onResume()
        mCastSession = mSessionManager.currentCastSession
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession::class.java)
    }

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

    private class SessionManagerListenerImpl implements SessionManagerListener<CastSession> {
        @Override
        public void onSessionStarting(CastSession session) {}
        @Override
        public void onSessionStarted(CastSession session, String sessionId) {
            invalidateOptionsMenu();
        }
        @Override
        public void onSessionStartFailed(CastSession session, int error) {
            int castReasonCode = mCastContext.getCastReasonCodeForCastStatusCode(error);
            // Handle error
        }
        @Override
        public void onSessionSuspended(CastSession session, int reason) {}
        @Override
        public void onSessionResuming(CastSession session, String sessionId) {}
        @Override
        public void onSessionResumed(CastSession session, boolean wasSuspended) {
            invalidateOptionsMenu();
        }
        @Override
        public void onSessionResumeFailed(CastSession session, int error) {}
        @Override
        public void onSessionEnding(CastSession session) {}
        @Override
        public void onSessionEnded(CastSession session, int error) {
            finish();
        }
    }

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

نقل البث

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

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

راجِع نقل البث على جهاز استقبال الويب للحصول على مزيد من المعلومات.

إعادة الاتصال التلقائي

يوفر إطار العمل عنصر ReconnectionService يمكن تفعيله من خلال تطبيق المرسِل لمعالجة إعادة الربط في العديد من الحالات الدقيقة، مثل:

  • الاسترداد بعد فقدان مؤقت لشبكة WiFi
  • استرداد البيانات بعد وضع الجهاز في وضع السكون
  • استرداد البيانات بعد استخدام التطبيق في الخلفية
  • الاسترداد إذا تعطّل التطبيق

تكون هذه الخدمة مفعّلة تلقائيًا، ويمكن إيقافها في CastOptions.Builder.

يمكن دمج هذه الخدمة تلقائيًا في ملف بيان تطبيقك في حال تفعيل الدمج التلقائي في ملف Gradle.

سيبدأ إطار العمل الخدمة عند توفّر جلسة وسائط، وسيوقفها عند انتهاء جلسة الوسائط.

طريقة عمل ميزة "التحكّم في الوسائط"

يوقف إطار عمل Google Cast الفئة RemoteMediaPlayer من Cast 2.x لصالح فئة جديدة RemoteMediaClient، والتي توفّر الوظائف نفسها مع مجموعة من واجهات برمجة التطبيقات الأكثر ملاءمة، وتتجنب الاضطرار إلى التمرير إلى GoogleApiClient.

عندما ينشئ تطبيقك CastSession باستخدام تطبيق استقبال الويب يتيح مساحة اسم الوسائط، سيتم إنشاء مثيل RemoteMediaClient تلقائيًا من خلال إطار العمل. ويمكن لتطبيقك الوصول إليه من خلال استدعاء طريقة getRemoteMediaClient() على مثيل CastSession.

إنّ جميع طُرق RemoteMediaClient التي تُصدر طلبات إلى جهاز استقبال الويب ستعرض عنصر PendingResult الذي يمكن استخدامه لتتبُّع هذا الطلب.

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

ضبط البيانات الوصفية للوسائط

تمثل الفئة MediaMetadata المعلومات المتعلقة بعنصر الوسائط الذي تريد بثه. يقوم المثال التالي بإنشاء مثيل MediaMetadata جديد لفيلم ويضبط العنوان والعنوان الفرعي وصورتين.

كولين
val movieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE)

movieMetadata.putString(MediaMetadata.KEY_TITLE, mSelectedMedia.getTitle())
movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, mSelectedMedia.getStudio())
movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia.getImage(0))))
movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia.getImage(1))))
Java
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);

movieMetadata.putString(MediaMetadata.KEY_TITLE, mSelectedMedia.getTitle());
movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, mSelectedMedia.getStudio());
movieMetadata.addImage(new WebImage(Uri.parse(mSelectedMedia.getImage(0))));
movieMetadata.addImage(new WebImage(Uri.parse(mSelectedMedia.getImage(1))));

راجِع القسم اختيار الصور حول استخدام الصور التي تتضمّن بيانات وصفية للوسائط.

تحميل الوسائط

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

كولين
val mediaInfo = MediaInfo.Builder(mSelectedMedia.getUrl())
    .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
    .setContentType("videos/mp4")
    .setMetadata(movieMetadata)
    .setStreamDuration(mSelectedMedia.getDuration() * 1000)
    .build()
val remoteMediaClient = mCastSession.getRemoteMediaClient()
remoteMediaClient.load(MediaLoadRequestData.Builder().setMediaInfo(mediaInfo).build())
Java
MediaInfo mediaInfo = new MediaInfo.Builder(mSelectedMedia.getUrl())
        .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
        .setContentType("videos/mp4")
        .setMetadata(movieMetadata)
        .setStreamDuration(mSelectedMedia.getDuration() * 1000)
        .build();
RemoteMediaClient remoteMediaClient = mCastSession.getRemoteMediaClient();
remoteMediaClient.load(new MediaLoadRequestData.Builder().setMediaInfo(mediaInfo).build());

يمكنك أيضًا مراجعة القسم حول استخدام مقاطع الوسائط.

تنسيق فيديو بدقة 4K

للتحقق من تنسيق الفيديو، استخدِم الرمز getVideoInfo() في MediaStatus للحصول على المثيل الحالي من VideoInfo. يحتوي هذا المثيل على نوع تنسيق HDR TV وارتفاع العرض والعرض بالبكسل. ويُشار إلى صيغ تنسيق 4K بالثوابت HDR_TYPE_*.

إرسال إشعارات من جهاز التحكّم عن بُعد إلى أجهزة متعدّدة

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

إضافة وحدة تحكّم صغيرة

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

يوفر إطار العمل طريقة عرض مخصصة، MiniControllerFragment، والتي يمكنك إضافتها أسفل ملف التنسيق لكل نشاط تريد إظهار وحدة التحكم الصغيرة فيه.

<fragment
    android:id="@+id/castMiniController"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:visibility="gone"
    class="com.google.android.gms.cast.framework.media.widget.MiniControllerFragment" />

عندما يشغّل تطبيق المرسِل فيديو أو بثًا صوتيًا مباشرًا، تعرض حزمة تطوير البرامج (SDK) تلقائيًا زر التشغيل/الإيقاف بدلاً من زر التشغيل/الإيقاف المؤقت في وحدة التحكّم المصغّرة.

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

إضافة وحدة تحكّم موسّعة

تتطلّب قائمة التحقّق من تصميم Google Cast أن يوفّر تطبيق المرسِل وحدة تحكّم موسّعة بالوسائط التي يتم إرسالها. وحدة التحكم الموسعة هي إصدار ملء الشاشة من وحدة التحكم الصغيرة.

توفّر حزمة تطوير البرامج (SDK) الخاصة بتكنولوجيا Google Cast أداة لوحدة التحكّم الموسّعة التي تُسمى ExpandedControllerActivity. هذه فئة مجردة يجب عليك إضافتها إلى فئة فرعية لإضافة زر البث.

أولاً، أنشئ ملف مورد قائمة جديدًا لوحدة التحكم الموسّعة لتوفير زر البث:

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
            android:id="@+id/media_route_menu_item"
            android:title="@string/media_route_menu_title"
            app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
            app:showAsAction="always"/>

</menu>

أنشِئ صفًا جديدًا يشمل الصف ExpandedControllerActivity.

كولين
class ExpandedControlsActivity : ExpandedControllerActivity() {
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        super.onCreateOptionsMenu(menu)
        menuInflater.inflate(R.menu.expanded_controller, menu)
        CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item)
        return true
    }
}
Java
public class ExpandedControlsActivity extends ExpandedControllerActivity {
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        getMenuInflater().inflate(R.menu.expanded_controller, menu);
        CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item);
        return true;
    }
}

عليك الآن الإعلان عن نشاطك الجديد في بيان التطبيق ضمن علامة application:

<application>
...
<activity
        android:name=".expandedcontrols.ExpandedControlsActivity"
        android:label="@string/app_name"
        android:launchMode="singleTask"
        android:theme="@style/Theme.CastVideosDark"
        android:screenOrientation="portrait"
        android:parentActivityName="com.google.sample.cast.refplayer.VideoBrowserActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
    </intent-filter>
</activity>
...
</application>

يمكنك تعديل CastOptionsProvider وتغيير NotificationOptions وCastMediaOptions لضبط النشاط المستهدف على نشاطك الجديد:

كولين
override fun getCastOptions(context: Context): CastOptions? {
    val notificationOptions = NotificationOptions.Builder()
        .setTargetActivityClassName(ExpandedControlsActivity::class.java.name)
        .build()
    val mediaOptions = CastMediaOptions.Builder()
        .setNotificationOptions(notificationOptions)
        .setExpandedControllerActivityClassName(ExpandedControlsActivity::class.java.name)
        .build()

    return CastOptions.Builder()
        .setReceiverApplicationId(context.getString(R.string.app_id))
        .setCastMediaOptions(mediaOptions)
        .build()
}
Java
public CastOptions getCastOptions(Context context) {
    NotificationOptions notificationOptions = new NotificationOptions.Builder()
            .setTargetActivityClassName(ExpandedControlsActivity.class.getName())
            .build();
    CastMediaOptions mediaOptions = new CastMediaOptions.Builder()
            .setNotificationOptions(notificationOptions)
            .setExpandedControllerActivityClassName(ExpandedControlsActivity.class.getName())
            .build();

    return new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setCastMediaOptions(mediaOptions)
            .build();
}

يمكنك تعديل طريقة loadRemoteMedia LocalPlayerActivity لعرض نشاطك الجديد عند تحميل الوسائط البعيدة:

كولين
private fun loadRemoteMedia(position: Int, autoPlay: Boolean) {
    val remoteMediaClient = mCastSession?.remoteMediaClient ?: return

    remoteMediaClient.registerCallback(object : RemoteMediaClient.Callback() {
        override fun onStatusUpdated() {
            val intent = Intent(this@LocalPlayerActivity, ExpandedControlsActivity::class.java)
            startActivity(intent)
            remoteMediaClient.unregisterCallback(this)
        }
    })

    remoteMediaClient.load(
        MediaLoadRequestData.Builder()
            .setMediaInfo(mSelectedMedia)
            .setAutoplay(autoPlay)
            .setCurrentTime(position.toLong()).build()
    )
}
Java
private void loadRemoteMedia(int position, boolean autoPlay) {
    if (mCastSession == null) {
        return;
    }
    final RemoteMediaClient remoteMediaClient = mCastSession.getRemoteMediaClient();
    if (remoteMediaClient == null) {
        return;
    }
    remoteMediaClient.registerCallback(new RemoteMediaClient.Callback() {
        @Override
        public void onStatusUpdated() {
            Intent intent = new Intent(LocalPlayerActivity.this, ExpandedControlsActivity.class);
            startActivity(intent);
            remoteMediaClient.unregisterCallback(this);
        }
    });
    remoteMediaClient.load(new MediaLoadRequestData.Builder()
            .setMediaInfo(mSelectedMedia)
            .setAutoplay(autoPlay)
            .setCurrentTime(position).build());
}

عندما يشغّل تطبيق المرسِل فيديو أو بثًا صوتيًا مباشرًا، تعرض حزمة تطوير البرامج (SDK) تلقائيًا زر التشغيل/الإيقاف بدلاً من زر التشغيل/الإيقاف المؤقت في وحدة التحكّم الموسّعة.

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

التحكم في مستوى الصوت

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

التحكّم في مستوى صوت الزرّ الفعلي

على نظام التشغيل Android، يمكن استخدام الأزرار الفعلية على جهاز المرسِل لتغيير مستوى صوت جلسة البث على "جهاز استقبال الويب" تلقائيًا لأي جهاز يستخدم Jelly Bean أو إصدار أحدث.

التحكم في مستوى صوت الزر الفعلي قبل Jelly Bean

لاستخدام مفاتيح مستوى الصوت للتحكم في مستوى صوت جهاز استقبال الويب على أجهزة Android الأقدم من Jelly Bean، يجب على تطبيق المرسِل إلغاء dispatchKeyEvent في أنشطة المستخدم وطلب CastContext.onDispatchVolumeKeyEventBeforeJellyBean():

كولين
class MyActivity : FragmentActivity() {
    override fun dispatchKeyEvent(event: KeyEvent): Boolean {
        return (CastContext.getSharedInstance(this)
            .onDispatchVolumeKeyEventBeforeJellyBean(event)
                || super.dispatchKeyEvent(event))
    }
}
Java
class MyActivity extends FragmentActivity {
    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        return CastContext.getSharedInstance(this)
            .onDispatchVolumeKeyEventBeforeJellyBean(event)
            || super.dispatchKeyEvent(event);
    }
}

إضافة عناصر التحكّم في الوسائط إلى الإشعار وشاشة القفل

على نظام التشغيل Android فقط، تتطلّب قائمة التحقّق من تصميم Google Cast من تطبيق المرسِل تنفيذ عناصر التحكّم في الوسائط في إشعار وفي شاشة القفل، حيث يبث المرسِل المحتوى بدون التركيز على تطبيق المرسِل. يوفر إطار العمل MediaNotificationService وMediaIntentReceiver لمساعدة تطبيق المرسل في إنشاء عناصر تحكم في الوسائط من خلال إشعار وفي شاشة القفل.

يتم تشغيل "MediaNotificationService" أثناء البثّ من خلال عرض إشعار مع صورة مصغّرة ومعلومات حول العنصر الحالي للبث وزر التشغيل/الإيقاف المؤقت وزر الإيقاف.

إنّ MediaIntentReceiver عبارة عن BroadcastReceiver يعالج إجراءات المستخدم من الإشعار.

يمكن لتطبيقك ضبط التحكّم في الإشعارات والوسائط من شاشة القفل من خلال NotificationOptions. يمكن لتطبيقك ضبط أزرار التحكّم التي ستظهر في الإشعار وتحديد Activity التي سيتم فتحها عندما ينقر المستخدم على الإشعار. إذا لم يتم تقديم الإجراءات بشكل صريح، سيتم استخدام القيم التلقائية MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK وMediaIntentReceiver.ACTION_STOP_CASTING.

كولين
// Example showing 4 buttons: "rewind", "play/pause", "forward" and "stop casting".
val buttonActions: MutableList<String> = ArrayList()
buttonActions.add(MediaIntentReceiver.ACTION_REWIND)
buttonActions.add(MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK)
buttonActions.add(MediaIntentReceiver.ACTION_FORWARD)
buttonActions.add(MediaIntentReceiver.ACTION_STOP_CASTING)

// Showing "play/pause" and "stop casting" in the compat view of the notification.
val compatButtonActionsIndices = intArrayOf(1, 3)

// Builds a notification with the above actions. Each tap on the "rewind" and "forward" buttons skips 30 seconds.
// Tapping on the notification opens an Activity with class VideoBrowserActivity.
val notificationOptions = NotificationOptions.Builder()
    .setActions(buttonActions, compatButtonActionsIndices)
    .setSkipStepMs(30 * DateUtils.SECOND_IN_MILLIS)
    .setTargetActivityClassName(VideoBrowserActivity::class.java.name)
    .build()
Java
// Example showing 4 buttons: "rewind", "play/pause", "forward" and "stop casting".
List<String> buttonActions = new ArrayList<>();
buttonActions.add(MediaIntentReceiver.ACTION_REWIND);
buttonActions.add(MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK);
buttonActions.add(MediaIntentReceiver.ACTION_FORWARD);
buttonActions.add(MediaIntentReceiver.ACTION_STOP_CASTING);

// Showing "play/pause" and "stop casting" in the compat view of the notification.
int[] compatButtonActionsIndices = new int[]{1, 3};

// Builds a notification with the above actions. Each tap on the "rewind" and "forward" buttons skips 30 seconds.
// Tapping on the notification opens an Activity with class VideoBrowserActivity.
NotificationOptions notificationOptions = new NotificationOptions.Builder()
    .setActions(buttonActions, compatButtonActionsIndices)
    .setSkipStepMs(30 * DateUtils.SECOND_IN_MILLIS)
    .setTargetActivityClassName(VideoBrowserActivity.class.getName())
    .build();

تكون ميزة عرض عناصر التحكّم في الوسائط من الإشعار وشاشة القفل مفعّلة تلقائيًا ويمكن إيقافها من خلال طلب الرمز setNotificationOptions بدون قيمة فارغة في CastMediaOptions.Builder. في الوقت الحالي، يتم تفعيل ميزة شاشة القفل طالما كانت الإشعارات مفعّلة.

كولين
// ... continue with the NotificationOptions built above
val mediaOptions = CastMediaOptions.Builder()
    .setNotificationOptions(notificationOptions)
    .build()
val castOptions: CastOptions = Builder()
    .setReceiverApplicationId(context.getString(R.string.app_id))
    .setCastMediaOptions(mediaOptions)
    .build()
Java
// ... continue with the NotificationOptions built above
CastMediaOptions mediaOptions = new CastMediaOptions.Builder()
        .setNotificationOptions(notificationOptions)
        .build();
CastOptions castOptions = new CastOptions.Builder()
        .setReceiverApplicationId(context.getString(R.string.app_id))
        .setCastMediaOptions(mediaOptions)
        .build();

عندما يشغّل التطبيق التابع للمرسل فيديو أو بثًا صوتيًا مباشرًا، تعرض حزمة SDK تلقائيًا زر التشغيل/الإيقاف بدلاً من زر التشغيل/الإيقاف المؤقت على عنصر التحكم في الإشعارات ولكن ليس على عنصر التحكم في شاشة القفل.

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

التعامل مع الأخطاء

من المهم جدًا أن تتعامل تطبيقات المرسِل مع جميع عمليات معاودة الاتصال بالأخطاء وتحديد أفضل استجابة لكل مرحلة من مراحل دورة حياة البث. يمكن للتطبيق عرض مربعات حوار الخطأ للمستخدم أو يمكن أن يقرر قطع الاتصال بمستلم الويب.