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

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

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

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

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

مسار المستخدم في التطبيق

توضّح الخطوات التالية خطوات التنفيذ العالية المستوى المعتادة لتطبيق Android المرسِل:

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

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

ضبط ملف بيان Android

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

uses-sdk

اضبط الحد الأدنى والمستوى المستهدَف لواجهة برمجة تطبيقات Android التي تتوافق معها حزمة تطوير البرامج (SDK) لتطبيقات Cast. الحد الأدنى حاليًا هو المستوى 23 لواجهة برمجة التطبيقات، والمستوى المستهدف هو المستوى 34 لواجهة برمجة التطبيقات.

<uses-sdk
        android:minSdkVersion="23"
        android:targetSdkVersion="34" />

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

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

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

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

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

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

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

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

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

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

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

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

إضافة زر بث

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

يسهّل إطار العمل إضافة 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) لتطبيق Cast استرداد جلسة Cast.

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

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

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

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

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

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

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

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 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
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession::class.java)
    }

    override fun onResume() {
        super.onResume()
        mCastSession = mSessionManager.currentCastSession
    }

    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 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();
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession.class);
    }

    @Override
    protected void onResume() {
        super.onResume();
        mCastSession = mSessionManager.getCurrentCastSession();
    }

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

إعادة توجيه البث

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

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

اطّلِع على نقل البث على Web Receiver للحصول على مزيد من المعلومات.

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

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

  • استرداد البيانات بعد فقدان الاتصال بشبكة Wi-Fi مؤقتًا
  • استعادة البيانات بعد تنشيط الجهاز من وضع السكون
  • استعادة التطبيق من وضع "التشغيل في الخلفية"
  • استرداد البيانات في حال تعطُّل التطبيق

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

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

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

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

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

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

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

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

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

تمثّل فئة MediaMetadata المعلومات عن عنصر وسائط تريد بثّه. ينشئ المثال التالي مثيلًا جديدًا من MediaMetadata لفيلم ويضبط العنوان والترجمة والشرح وصورتَين.

Kotlin
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 لتشغيل تطبيق مشغّل وسائط وإيقافه مؤقتًا والتحكّم فيه بأي شكل آخر على Web Receiver.

Kotlin
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 وارتفاع الشاشة وعرضها بالبكسل. يتم الإشارة إلى الصيغ المختلفة لتنسيق 4K باستخدام الثوابت HDR_TYPE_*.

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

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

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

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

Kotlin
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 لضبط النشاط المستهدَف على نشاطك الجديد:

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

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

Kotlin
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) تلقائيًا زر تشغيل/إيقاف بدلاً من زر التشغيل/الإيقاف المؤقت في وحدة التحكّم الموسّعة.

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

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

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

التحكّم في مستوى الصوت باستخدام أزرار خارجية

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

التحكّم في مستوى الصوت باستخدام الأزرار الخارجية قبل الإصدار Jelly Bean

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

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

Kotlin
// 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 مع القيمة null في CastMediaOptions.Builder. في الوقت الحالي، تكون ميزة شاشة القفل مفعّلة ما دام الإشعار مفعّلاً.

Kotlin
// ... 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 سيطلب تلقائيًا ميزة "التركيز على الصوت" نيابةً عنك.

معالجة الأخطاء

من المهم جدًا أن تعالج تطبيقات المُرسِلين جميع عمليات الاستدعاء المتعلّقة بالأخطاء وأن تقرّر أفضل استجابة لكل مرحلة من مراحل دورة حياة Cast. يمكن للتطبيق عرض مربعات حوار بشأن الأخطاء للمستخدم أو يمكنه أن يقرر إنهاء الاتصال بتطبيق Web Receiver.