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

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

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

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

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

مسار التطبيق

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

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

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

إعداد بيان Android

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

uses-sdk

يمكنك ضبط الحدّ الأدنى لمستويات واجهة برمجة تطبيقات Android التي تتوافق مع حزمة تطوير البرامج (SDK) الخاصة بالبث. الحد الأدنى حاليًا هو المستوى 21 من واجهة برمجة التطبيقات والهدف هو المستوى 28 من واجهة برمجة التطبيقات.

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

android:theme

يمكنك ضبط مظهر تطبيقك استنادًا إلى الحد الأدنى لإصدار حزمة تطوير البرامج (SDK) لنظام التشغيل Android. على سبيل المثال، إذا لم تكن تطبّق المظهر الخاص بك، عليك استخدام أحد خيارات Theme.AppCompat عند استهداف إصدار Android SDK كحد أدنى يعمل قبل 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);
    }
}

تطبيقات Cast UX Widgets

يوفّر إطار عمل Google Cast التطبيقات المصغّرة التي تتوافق مع قائمة التحقّق من تصميم Google 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" />
كوتلين
// 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>
كوتلين
// 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، يُحدِّد تطبيق المُرسِل رقم تعريف تطبيق WebRecipient، ويمكنه طلب فلترة مساحات الاسم اختياريًا من خلال ضبط 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) لتكنولوجيا Cast مفهوم جلسة 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 يمكن تفعيله من خلال تطبيق المرسِل لمعالجة إعادة الربط في العديد من الحالات البسيطة، مثل:

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

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

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

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

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

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

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

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

وفقًا لقائمة التحقّق من تصميم 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) لتكنولوجيا 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 تلقائيًا تركيز الصوت نيابةً عنك.

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

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