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

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

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

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

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

مسار التطبيق

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

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

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

ضبط بيان Android

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

uses-sdk

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

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

android:theme

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

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

إعداد سياق البث

يحتوي إطار العمل على كائن "سينغلتون" عمومي، وهو CastContext، يعمل على تنسيق جميع تفاعلات إطار العمل.

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

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

    override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
        return null
    }
}
JavaScript
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)
    }
}
JavaScript
public class MyActivity extends FragmentActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        CastContext castContext = CastContext.getSharedInstance(this);
    }
}

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

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

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

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

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

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

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

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

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

إضافة زر بث

تم تصميم واجهات برمجة تطبيقات AndroidMediaRouter لتفعيل عرض الوسائط وتشغيلها على الأجهزة الثانوية. يجب أن تتضمن تطبيقات 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
}
JavaScript
// 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)
}
JavaScript
// MyActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_layout);

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

   mCastContext = CastContext.getSharedInstance(this);
}

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

ضبط إمكانية العثور على الجهاز

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

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

تتم إدارة الجلسات من خلال الصف 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
    }
}
JavaScript
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.

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

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

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

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

ستعرض جميع طرق RemoteMediaClient التي تصدر طلبات إلى مستلِم الويب كائن في انتظار المراجعة يمكن استخدامه لتتبّع هذا الطلب.

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

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

تمثّل الفئة 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))))
JavaScript
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())
JavaScript
MediaInfo mediaInfo = new MediaInfo.Builder(mSelectedMedia.getUrl())
        .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
        .setContentType("videos/mp4")
        .setMetadata(movieMetadata)
        .setStreamDuration(mSelectedMedia.getDuration() * 1000)
        .build();
RemoteMediaClient remoteMediaClient = mCastSession.getRemoteMediaClient();
remoteMediaClient.load(new MediaLoadRequestData.Builder().setMediaInfo(mediaInfo).build());

يمكنك أيضًا الاطّلاع على القسم حول استخدام المقاطع الصوتية الإعلامية.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

</menu>

أنشئ صفًا جديدًا لتوسيع نطاق ExpandedControllerActivity.

كوتلين
class ExpandedControlsActivity : ExpandedControllerActivity() {
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        super.onCreateOptionsMenu(menu)
        menuInflater.inflate(R.menu.expanded_controller, menu)
        CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item)
        return true
    }
}
JavaScript
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()
}
JavaScript
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 لعرض نشاطك الجديد عند تحميل الوسائط البعيدة:

كوتلين
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()
    )
}
JavaScript
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 Pen

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

كوتلين
class MyActivity : FragmentActivity() {
    override fun dispatchKeyEvent(event: KeyEvent): Boolean {
        return (CastContext.getSharedInstance(this)
            .onDispatchVolumeKeyEventBeforeJellyBean(event)
                || super.dispatchKeyEvent(event))
    }
}
JavaScript
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()
JavaScript
// 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()
JavaScript
// ... 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 تلقائيًا ميزة "التركيز الصوتي" نيابةً عنك.

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

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