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

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

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

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

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

سير عمل التطبيق

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

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

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

ضبط بيان Android

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

uses-sdk

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

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

android:theme

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

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:

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

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

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

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

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

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

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

إضافة زر Cast

تم تصميم واجهات برمجة تطبيقات MediaRouter على Android لتفعيل عرض الوسائط وتشغيلها على أجهزة ثانوية. يجب أن تتضمّن تطبيقات 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`، يحدّد تطبيق المرسِل معرّف تطبيق Web Receiver ، ويمكنه اختياريًا طلب فلترة مساحة الاسم من خلال ضبط supportedNamespaces في CastOptions. يحتوي CastContext على مرجع إلى MediaRouter داخليًا، وسيبدأ عملية الاكتشاف في الحالات التالية:

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

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

تقدّم حزمة Cast SDK مفهوم جلسة Cast، التي يجمع إنشاؤها بين خطوات الاتصال بجهاز وتشغيل تطبيق Web Receiver (أو الانضمام إليه) والاتصال بهذا التطبيق وتهيئة قناة للتحكّم في الوسائط. لمزيد من المعلومات عن جلسات Cast ودورة حياة Web Receiver، يُرجى الرجوع إلى دليل دورة حياة تطبيق Web Receiver .

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

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

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

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

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

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

يوفّر إطار العمل عنصر `MiniControllerFragment` مخصّصًا من نوع View يمكنك إضافته إلى أسفل ملف التنسيق لكل نشاط تريد عرض وحدة التحكّم المصغّرة فيه.

<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 أن يوفّر تطبيق المرسِل وحدة تحكّم موسّعة للوسائط التي يتم بثها. وحدة التحكّم الموسّعة هي إصدار بملء الشاشة من وحدة التحكّم المصغّرة.

توفر حزمة Cast SDK أداة لوحدة التحكم الموسعة تسمى 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();
}

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

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