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

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

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

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

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

مسار التطبيق

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

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

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

ضبط ملف بيان Android

يتطلّب ملف AndroidManifest.xml الخاص بتطبيقك ضبط العناصر التالية لحزمة تطوير البرامج (SDK) لمنصّة Cast:

uses-sdk

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

<uses-sdk
        android:minSdkVersion="24"
        android:targetSdkVersion="35" />

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>

إعداد سياق Cast

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

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

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

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

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

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

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

إضافة زر البث

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

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

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

لمزيد من المعلومات، يمكنك الاطّلاع على إعادة توجيه البث على Web Receiver.

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

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

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

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

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

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

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

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

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

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

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 > عرض إشعارات جهاز التحكّم عن بُعد. (تتضمّن الإشعارات اختصارًا لتطبيق "الإعدادات"). لمزيد من التفاصيل، يُرجى الاطّلاع على إشعارات التحكّم عن بُعد في البث.

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

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